aether-engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +15 -0
  2. package/biome.json +51 -0
  3. package/bun.lock +192 -0
  4. package/index.ts +1 -0
  5. package/package.json +25 -0
  6. package/serve.ts +125 -0
  7. package/src/audio/AudioEngine.ts +61 -0
  8. package/src/components/Animator3D.ts +65 -0
  9. package/src/components/AudioSource.ts +26 -0
  10. package/src/components/BitmapText.ts +25 -0
  11. package/src/components/Camera.ts +33 -0
  12. package/src/components/CameraFollow.ts +5 -0
  13. package/src/components/Collider.ts +16 -0
  14. package/src/components/Components.test.ts +68 -0
  15. package/src/components/Light.ts +15 -0
  16. package/src/components/MeshRenderer.ts +58 -0
  17. package/src/components/ParticleEmitter.ts +59 -0
  18. package/src/components/RigidBody.ts +9 -0
  19. package/src/components/ShadowCaster.ts +3 -0
  20. package/src/components/SkinnedMeshRenderer.ts +25 -0
  21. package/src/components/SpriteAnimator.ts +42 -0
  22. package/src/components/SpriteRenderer.ts +26 -0
  23. package/src/components/Transform.test.ts +39 -0
  24. package/src/components/Transform.ts +54 -0
  25. package/src/core/AssetManager.ts +123 -0
  26. package/src/core/Input.test.ts +67 -0
  27. package/src/core/Input.ts +94 -0
  28. package/src/core/Scene.ts +24 -0
  29. package/src/core/SceneManager.ts +57 -0
  30. package/src/core/Storage.ts +161 -0
  31. package/src/desktop/SteamClient.ts +52 -0
  32. package/src/ecs/System.ts +11 -0
  33. package/src/ecs/World.test.ts +29 -0
  34. package/src/ecs/World.ts +149 -0
  35. package/src/index.ts +115 -0
  36. package/src/math/Color.ts +100 -0
  37. package/src/math/Vector2.ts +96 -0
  38. package/src/math/Vector3.ts +103 -0
  39. package/src/math/math.test.ts +168 -0
  40. package/src/renderer/GlowMaterial.ts +66 -0
  41. package/src/renderer/LitMaterial.ts +337 -0
  42. package/src/renderer/Material.test.ts +23 -0
  43. package/src/renderer/Material.ts +80 -0
  44. package/src/renderer/OcclusionMaterial.ts +43 -0
  45. package/src/renderer/ParticleMaterial.ts +66 -0
  46. package/src/renderer/Shader.ts +44 -0
  47. package/src/renderer/SkinnedLitMaterial.ts +55 -0
  48. package/src/renderer/WaterMaterial.ts +298 -0
  49. package/src/renderer/WebGLRenderer.ts +917 -0
  50. package/src/systems/Animation3DSystem.ts +148 -0
  51. package/src/systems/AnimationSystem.ts +58 -0
  52. package/src/systems/AudioSystem.ts +62 -0
  53. package/src/systems/LightingSystem.ts +114 -0
  54. package/src/systems/ParticleSystem.ts +278 -0
  55. package/src/systems/PhysicsSystem.ts +211 -0
  56. package/src/systems/Systems.test.ts +165 -0
  57. package/src/systems/TextSystem.ts +153 -0
  58. package/src/ui/AnimationEditor.tsx +639 -0
  59. package/src/ui/BottomPanel.tsx +443 -0
  60. package/src/ui/EntityExplorer.tsx +420 -0
  61. package/src/ui/GameState.ts +286 -0
  62. package/src/ui/Icons.tsx +239 -0
  63. package/src/ui/InventoryPanel.tsx +335 -0
  64. package/src/ui/PlayerHUD.tsx +250 -0
  65. package/src/ui/SpriteEditor.tsx +3241 -0
  66. package/src/ui/SpriteSheetManager.tsx +198 -0
  67. package/src/utils/GLTFLoader.ts +257 -0
  68. package/src/utils/ObjLoader.ts +81 -0
  69. package/src/utils/idb.ts +137 -0
  70. package/src/utils/packer.ts +85 -0
  71. package/test_obj.ts +12 -0
  72. package/tsconfig.json +21 -0
  73. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,337 @@
1
+ import { Material } from "./Material";
2
+
3
+ export const LIT_SPRITE_VS = `
4
+ attribute vec4 aVertexPosition;
5
+ attribute vec2 aTextureCoord;
6
+ attribute vec3 aVertexNormal;
7
+
8
+ uniform mat4 uModelMatrix;
9
+ uniform mat4 uModelViewMatrix;
10
+ uniform mat4 uProjectionMatrix;
11
+ uniform vec2 uTexOffset;
12
+ uniform vec2 uTexScale;
13
+
14
+ varying highp vec2 vTextureCoord;
15
+ varying highp vec3 vWorldPos;
16
+ varying highp vec3 vNormal;
17
+
18
+ void main(void) {
19
+ gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
20
+ vTextureCoord = (aTextureCoord * uTexScale) + uTexOffset;
21
+
22
+ // Compute world position for lighting calculations
23
+ vWorldPos = (uModelMatrix * aVertexPosition).xyz;
24
+
25
+ // Approximate normal transformation using ModelMatrix directly
26
+ // This is valid for Translation/Rotation uniform scaling
27
+ vNormal = normalize(mat3(uModelMatrix) * aVertexNormal);
28
+ }
29
+ `;
30
+
31
+ export const LIT_SPRITE_FS = `
32
+ precision mediump float;
33
+
34
+ varying highp vec2 vTextureCoord;
35
+ varying highp vec3 vWorldPos;
36
+ varying highp vec3 vNormal;
37
+
38
+ uniform sampler2D uSampler;
39
+ uniform lowp vec4 uColor;
40
+
41
+ // Lighting Uniforms
42
+ uniform lowp vec3 uAmbientColor;
43
+ uniform int uActivePointLights;
44
+ uniform highp vec3 uLightPositions[16];
45
+ uniform lowp vec3 uLightColors[16];
46
+ uniform highp vec4 uLightParams[16]; // x: intensity, y: radius, z: physicalElevation, w: flickerIntensity
47
+
48
+ // Occlusion map for shadows and screen properties
49
+ uniform sampler2D uOcclusionSampler;
50
+ uniform highp vec2 uResolution;
51
+ uniform highp mat4 uViewMatrix;
52
+ uniform highp mat4 uProjectionMatrix;
53
+
54
+ // Particle Shading System
55
+ uniform int uActiveParticleLights;
56
+ uniform highp vec4 uParticleLightPositions[64];
57
+ uniform lowp vec3 uParticleLightColors[64];
58
+
59
+ // Global Time for dynamic effects
60
+ uniform highp float uTime;
61
+
62
+ // Interleaved Gradient Noise for Dithering
63
+ float IGN(highp vec2 fragCoord) {
64
+ vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189);
65
+ return fract(magic.z * fract(dot(fragCoord, magic.xy)));
66
+ }
67
+
68
+ // Tuneable random delay/decay pattern for natural gas flickering
69
+ float getFlicker(float time, vec3 pos) {
70
+ // Offset time radically per-lightsource based on prime-scaled coordinates
71
+ float localTimeOffset = pos.x * 137.0 + pos.y * 311.0;
72
+ float absoluteTime = time + localTimeOffset;
73
+
74
+ // Determine a distinct 10-second interval block for this exact lamppost
75
+ float decasecond = floor(absoluteTime * 0.1);
76
+
77
+ // Dice roll strictly once per block independently!
78
+ float dice = fract(sin(decasecond * 12.9898 + pos.x * 78.233) * 43758.5453);
79
+
80
+ // 1% chance to stutter (e.g. extremely seldom, averages 1 flutter every 16 minutes per lamp)
81
+ if (dice < 0.015) {
82
+ float localProgress = fract(absoluteTime * 0.1) * 10.0;
83
+
84
+ if (localProgress < 1.0) {
85
+ // Chaotic 1-second flutter fading out naturally
86
+ float noise = fract(sin(time * 50.0 + pos.y) * 43758.5453);
87
+ float envelope = 1.0 - localProgress;
88
+ return 1.0 - (noise * 0.7 * envelope);
89
+ }
90
+ }
91
+
92
+ return 1.0;
93
+ }
94
+
95
+ void main(void) {
96
+ vec4 texColor = texture2D(uSampler, vTextureCoord);
97
+ if (texColor.a < 0.05) discard;
98
+
99
+ vec4 baseColor = texColor * uColor;
100
+
101
+ vec3 lightAdded = vec3(0.0);
102
+ vec3 specularAdded = vec3(0.0);
103
+ vec2 fragUV = gl_FragCoord.xy / uResolution;
104
+
105
+ // Unequivocal structural detection of 2D (Orthographic) vs 3D (Perspective) camera matrices natively.
106
+ // uProjectionMatrix[3][3] equals 1.0 purely for Orthographic, and 0.0 for Perspective.
107
+ float is2D = uProjectionMatrix[3][3] > 0.5 ? 1.0 : 0.0;
108
+
109
+ vec3 norm = normalize(vNormal);
110
+ // Fallback for 2D sprites without normal mappings: Bypass diffuse angle scaling safely
111
+ if (is2D > 0.5) {
112
+ norm = vec3(0.0, 0.0, 1.0);
113
+ }
114
+
115
+ // View direction for Specular (approximate isometric camera angle roughly 45 deg down)
116
+ vec3 viewDir = normalize(vec3(0.0, 0.707, 0.707));
117
+
118
+ // A. CORE POINT LIGHTS (Heavy 12-pass Raymarching shadows)
119
+ for (int i = 0; i < 16; i++) {
120
+ if (i >= uActivePointLights) break;
121
+
122
+ vec3 lightPos = uLightPositions[i];
123
+ vec3 lightColor = uLightColors[i];
124
+ float intensity = uLightParams[i].x;
125
+ float radius = uLightParams[i].y;
126
+ float height = uLightParams[i].z;
127
+ float flickerIntensity = uLightParams[i].w;
128
+
129
+ // Authentic pseudo-3D coordinate reconstruction for robust volumetric 2.5D lighting!
130
+ vec3 physPos = vWorldPos;
131
+
132
+ if (is2D > 0.5) {
133
+ // Check if this is a standing sprite inherently sorted on Z, or a background/ground tile (-5.0)
134
+ if (vWorldPos.z > -4.0) {
135
+ // Standing Sprite! Reconstruct its true 2.5D physical footprint Anchor Y geographically from the internal Z-buffer formula.
136
+ float trueBaseY = -vWorldPos.z * 100.0;
137
+
138
+ // The geometric Altitude & Elevation of the exact pixel on the sheet!
139
+ float pixelElevation = max(0.0, vWorldPos.y - trueBaseY);
140
+
141
+ // We construct a TRUE geometric volumetric point using Footprint Y as topological distance, and pixel Altitude as Elevation Z!
142
+ physPos = vec3(vWorldPos.x, trueBaseY, pixelElevation);
143
+ } else {
144
+ // Flat Ground Tile (Z = -5.0). True geological Elevation is strictly 0.0 ground plane.
145
+ physPos = vec3(vWorldPos.x, vWorldPos.y, 0.0);
146
+ }
147
+ }
148
+
149
+ vec3 diff3D = lightPos - physPos; // true physical direction un-normalized
150
+ float distSq = dot(diff3D, diff3D);
151
+ float radiusSq = radius * radius;
152
+
153
+ if (distSq < radiusSq) {
154
+ // Prevent NaN glitches where geometric center aligns flawlessly with a pixel
155
+ float dist = max(0.001, sqrt(distSq));
156
+ vec3 lightDir = diff3D / dist;
157
+ float normDist = dist / radius;
158
+
159
+ // --- Spotlight Cone Directionality ---
160
+ // A real lamppost has a cap and physically points DOWN and outward natively.
161
+ // lightDir.z tracks downward angle perfectly natively in our 2.5D projection!
162
+ // If pixel is directly below (lightDir.z is large positive), weight is 1.0.
163
+ // If pixel is horizontal canopy (lightDir.z ~ 0.0), weight smoothly fades to 0.0.
164
+ float spotlightCone = smoothstep(0.05, 0.4, lightDir.z);
165
+
166
+ // --- Reflectance & Specular ---
167
+ float diffuse = dot(norm, lightDir) * 0.5 + 0.5;
168
+ vec3 specular = vec3(0.0);
169
+
170
+ if (is2D > 0.5) {
171
+ // Disable pure mathematical 3D specular for flat 2D billboard sprites.
172
+ // This prevents intense radioactive blowouts when isometric angles perfectly align with height offsets.
173
+ if (vWorldPos.z < -4.0) {
174
+ // Tile Ground Plane (Z=-5 internal rendering depth). Inject pseudo-texture bump mapping.
175
+ float bump = fract(sin(dot(floor(vWorldPos.xy * 16.0), vec2(12.9898, 78.233))) * 43758.5453);
176
+ diffuse = mix(0.9, 0.7 + bump * 0.4, clamp(1.0 - normDist, 0.0, 1.0));
177
+ } else {
178
+ // 100% stable standing 2D Sprite. Use static Half-Lambert facing up to receive
179
+ // soft volume lighting that fades uniformly along the edges of the light cone!
180
+ diffuse = 0.6 + (dot(vec3(0,0,1), lightDir) * 0.4);
181
+ }
182
+ } else {
183
+ // Authentic 3D Mesh rendering (e.g. Dungeon walls). Execute true structural Specular
184
+ vec3 halfVector = normalize(lightDir + viewDir);
185
+ float spec = pow(max(dot(norm, halfVector), 0.0), 16.0); // Shininess = 16.0
186
+ specular = spec * lightColor * intensity * 0.8;
187
+ }
188
+
189
+ // Gaussian soft falloff for natural glowing
190
+ float attenuation = max(0.0, (exp(-normDist * normDist * 4.0) - 0.0183) / 0.9817);
191
+
192
+ // Incorporate Downward Cone (Spotlight Cutoff) for 2D sprites.
193
+ // In pure 3D scenes (like Dungeon), we might want spherical lights. We dynamically apply cone
194
+ // ONLY if is2D > 0.5 because the cone is essential to prevent horizontal Canopy bleeding in 2D overlays!
195
+ if (is2D > 0.5) {
196
+ // If lightDir.z = 1.0 (looking straight down), weight is 1.0.
197
+ // If lightDir.z < 0.6 (looking primarily horizontally across the scene at tall geometries), weight drops permanently to 0.0!
198
+ float spotlightCone = smoothstep(0.55, 0.85, lightDir.z);
199
+ attenuation *= spotlightCone;
200
+ }
201
+
202
+ // Natural, tuneable delay/decay flicker pattern mixed per-light
203
+ float flickerAmount = getFlicker(uTime, lightPos);
204
+ attenuation *= mix(1.0, flickerAmount, flickerIntensity);
205
+
206
+ float shadowMultiplier = 1.0;
207
+
208
+ // Disable screen-space raymarch shadows for Point Lights in heavily 2D Isometric modes.
209
+ // Screen-space Z-depth occlusion inherently destroys 2.5D lighting mathematics
210
+ // by forcing upright billboard sprites to cast infinite absolute black shadows linearly across the ground plane.
211
+ if (is2D < 0.5) {
212
+ // RAYMARCH SHADOWS:
213
+ vec4 lClip = uProjectionMatrix * uViewMatrix * vec4(lightPos, 1.0);
214
+ vec2 lightScreenUV = (lClip.xy / lClip.w) * 0.5 + 0.5;
215
+
216
+ vec2 rayVec = lightScreenUV - fragUV;
217
+ float stepsNum = 12.0;
218
+ vec2 stepUV = rayVec / stepsNum;
219
+
220
+ vec2 sampleUV = fragUV;
221
+ sampleUV += stepUV * (IGN(gl_FragCoord.xy) - 0.5);
222
+
223
+ for (int k = 0; k < 12; k++) {
224
+ sampleUV += stepUV;
225
+ if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) {
226
+ break;
227
+ }
228
+ vec4 occData = texture2D(uOcclusionSampler, sampleUV);
229
+ float isOccluder = 1.0 - occData.b;
230
+
231
+ if (isOccluder > 0.5) {
232
+ float normZ = occData.r + occData.g * (1.0/255.0);
233
+ float occluderZ = normZ * 20.0 - 10.0;
234
+ // For 3D geometry ONLY, cast dynamic shadows downward
235
+ if (occluderZ > vWorldPos.z + 0.005) {
236
+ shadowMultiplier = 0.0;
237
+ break;
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ // Accumulate
244
+ lightAdded += lightColor * intensity * attenuation * diffuse * shadowMultiplier;
245
+ specularAdded += specular * attenuation * shadowMultiplier;
246
+ }
247
+ }
248
+
249
+ // B. SECONDARY PARTICLE LIGHTS (Fast Path - No shadows, specific radius)
250
+ vec3 particleAdded = vec3(0.0);
251
+
252
+ for (int j = 0; j < 64; j++) {
253
+ if (j >= uActiveParticleLights) break;
254
+
255
+ vec3 pLightPos = uParticleLightPositions[j].xyz;
256
+ float pRadius = uParticleLightPositions[j].w;
257
+ vec3 pLightColor = uParticleLightColors[j]; // Already scaled by intensity
258
+
259
+ vec3 posToLight = vWorldPos;
260
+ if (is2D > 0.5) {
261
+ // Terraria-style pixelated attenuation
262
+ // Assuming 1 unit = 1 tile = 16 pixels. Quantize position logically to grid.
263
+ posToLight = floor(vWorldPos * 16.0) / 16.0;
264
+ }
265
+
266
+ vec3 pDiff3D = pLightPos - posToLight;
267
+ if (is2D > 0.5) {
268
+ // Particles assume a slight consistent height over the 2D plane
269
+ pDiff3D.z = 0.5;
270
+ }
271
+
272
+ float pDistSq = dot(pDiff3D, pDiff3D);
273
+ float pRadiusSq = pRadius * pRadius;
274
+
275
+ if (pDistSq < pRadiusSq) {
276
+ float pDist = sqrt(pDistSq);
277
+ vec3 pLightDir = pDiff3D / pDist;
278
+
279
+ // Half-Lambert
280
+ float pDiffuse = dot(norm, pLightDir) * 0.5 + 0.5;
281
+ if (is2D > 0.5) pDiffuse = 1.0;
282
+
283
+ // Gaussian soft falloff
284
+ float pNormDist = pDist / pRadius;
285
+ float pAtten = max(0.0, (exp(-pNormDist * pNormDist * 4.0) - 0.0183) / 0.9817);
286
+
287
+ // pure consistent particle light radius logic, particles don't independently sputter
288
+
289
+ if (is2D > 0.5) {
290
+ // Harsh visual steps
291
+ pAtten = floor(pAtten * 6.0) / 6.0;
292
+ }
293
+
294
+ particleAdded += pLightColor * pAtten * pDiffuse;
295
+
296
+ // Slight specular for particles too!
297
+ vec3 pHalfVector = normalize(pLightDir + viewDir);
298
+ float pSpec = pow(max(dot(norm, pHalfVector), 0.0), 32.0); // very shiny points
299
+ specularAdded += pSpec * pLightColor * pAtten * 0.4;
300
+ }
301
+ }
302
+
303
+ // 1. Multiply Blend for ambient context (shadows)
304
+ vec3 shadedBase = baseColor.rgb * uAmbientColor;
305
+
306
+ // 2. Diffuse mapped light
307
+ vec3 mappedLight = baseColor.rgb * lightAdded;
308
+
309
+ // 3. To rescue extremely dark flat ground textures from absorbing illumination invisibly,
310
+ // we softly inject pure light geometrically. We STRICTLY MUST ONLY do this for Ground Tiles
311
+ // (Z < -4) rather than structural sprites like Trees, because Trees possess real color matrices
312
+ // that detonate into neon radioactive glitches when synthetic inverse-glow is stacked!
313
+ vec3 ambientGlow = vec3(0.0);
314
+ if (is2D > 0.5 && vWorldPos.z < -4.0) {
315
+ vec3 safeGlowEnergy = min(lightAdded, vec3(1.1));
316
+ ambientGlow = safeGlowEnergy * (vec3(1.0) - baseColor.rgb) * 0.9;
317
+ }
318
+
319
+ vec3 litColor = shadedBase + mappedLight + (baseColor.rgb * particleAdded) + specularAdded + ambientGlow + (particleAdded * 0.1);
320
+
321
+ // No global curve tone mapping. The engine runs naturally unbounded and cleanly handles standard overflow.
322
+ litColor = clamp(litColor, 0.0, 1.0);
323
+
324
+ // Screen-space Dithering to completely eliminate 8-bit color banding
325
+ float dither = IGN(gl_FragCoord.xy) * (1.0 / 255.0);
326
+ litColor += dither - (0.5 / 255.0);
327
+
328
+ gl_FragColor = vec4(litColor, baseColor.a);
329
+ }
330
+ `;
331
+
332
+ export class LitMaterial extends Material {
333
+ constructor() {
334
+ super(LIT_SPRITE_VS, LIT_SPRITE_FS);
335
+ }
336
+ }
337
+
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Color } from "../math/Color";
3
+ import { Material } from "./Material";
4
+
5
+ describe("Material System", () => {
6
+ test("Program hashing", () => {
7
+ const mat1 = new Material("VS", "FS");
8
+ const mat2 = new Material("VS", "FS");
9
+ const mat3 = new Material("VS2", "FS");
10
+
11
+ expect(mat1.programHash).toBe(mat2.programHash);
12
+ expect(mat1.programHash).not.toBe(mat3.programHash);
13
+ });
14
+
15
+ test("Uniforms storage", () => {
16
+ const mat = new Material("VS", "FS");
17
+ mat.setUniform("uColor", new Color(255, 0, 0));
18
+ mat.setUniform("uIntensity", 1.5);
19
+
20
+ expect(mat.uniforms.uColor).toBeInstanceOf(Color);
21
+ expect(mat.uniforms.uIntensity).toBe(1.5);
22
+ });
23
+ });
@@ -0,0 +1,80 @@
1
+ import type { TextureAsset } from "../core/AssetManager";
2
+ import type { Color } from "../math/Color";
3
+
4
+ export const DEFAULT_MESH_VS = `
5
+ attribute vec4 aVertexPosition;
6
+ attribute vec4 aVertexColor;
7
+ uniform mat4 uModelViewMatrix;
8
+ uniform mat4 uProjectionMatrix;
9
+ varying lowp vec4 vColor;
10
+ void main(void) {
11
+ gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
12
+ vColor = aVertexColor;
13
+ }
14
+ `;
15
+
16
+ export const DEFAULT_MESH_FS = `
17
+ varying lowp vec4 vColor;
18
+ void main(void) {
19
+ gl_FragColor = vColor;
20
+ }
21
+ `;
22
+
23
+ export const DEFAULT_SPRITE_VS = `
24
+ attribute vec4 aVertexPosition;
25
+ attribute vec2 aTextureCoord;
26
+ uniform mat4 uModelViewMatrix;
27
+ uniform mat4 uProjectionMatrix;
28
+ uniform vec2 uTexOffset;
29
+ uniform vec2 uTexScale;
30
+ varying highp vec2 vTextureCoord;
31
+ void main(void) {
32
+ gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
33
+ vTextureCoord = (aTextureCoord * uTexScale) + uTexOffset;
34
+ }
35
+ `;
36
+
37
+ export const DEFAULT_SPRITE_FS = `
38
+ precision mediump float;
39
+ varying highp vec2 vTextureCoord;
40
+ uniform sampler2D uSampler;
41
+ uniform lowp vec4 uColor;
42
+ void main(void) {
43
+ vec4 texColor = texture2D(uSampler, vTextureCoord);
44
+ if (texColor.a < 0.05) discard;
45
+ gl_FragColor = texColor * uColor;
46
+ }
47
+ `;
48
+
49
+ export type UniformValue =
50
+ | number
51
+ | number[]
52
+ | Float32Array
53
+ | Int32Array
54
+ | Color
55
+ | TextureAsset
56
+ | any;
57
+
58
+ export class Material {
59
+ public vertexShader: string;
60
+ public fragmentShader: string;
61
+ public uniforms: Record<string, UniformValue> = {};
62
+ public transparent: boolean = false;
63
+ public depthTest: boolean = true;
64
+ public depthWrite: boolean = true;
65
+ public cullFace: boolean = true;
66
+ public blendMode: "Alpha" | "Additive" | "Screen" = "Alpha";
67
+
68
+ public get programHash(): string {
69
+ return `${this.vertexShader}|${this.fragmentShader}`;
70
+ }
71
+
72
+ constructor(vs: string, fs: string) {
73
+ this.vertexShader = vs;
74
+ this.fragmentShader = fs;
75
+ }
76
+
77
+ setUniform(name: string, value: UniformValue) {
78
+ this.uniforms[name] = value;
79
+ }
80
+ }
@@ -0,0 +1,43 @@
1
+ import { Material } from "./Material";
2
+ import { LIT_SPRITE_VS } from "./LitMaterial";
3
+
4
+ export const OCCLUSION_FS = `
5
+ precision mediump float;
6
+
7
+ varying highp vec2 vTextureCoord;
8
+ varying highp vec3 vWorldPos; // Must match vertex shader exactly
9
+
10
+ uniform sampler2D uSampler;
11
+ uniform lowp vec4 uColor;
12
+ uniform highp vec2 uTexOffset;
13
+ uniform highp vec2 uTexScale;
14
+
15
+ void main(void) {
16
+ vec4 texColor = texture2D(uSampler, vTextureCoord);
17
+ if (texColor.a < 0.1 || uColor.a < 0.1) discard;
18
+
19
+ // Only cast shadows from the bottom 25% of the sprite
20
+ // This creates a footprint-based shadow in 2D top-down instead of a full-body shadow,
21
+ // which prevents objects (like trees) from heavily self-shadowing their own canopies
22
+ vec2 localUV = (vTextureCoord - uTexOffset) / uTexScale;
23
+ if (localUV.y < 0.75) discard;
24
+
25
+ // Output pure black (with alpha) for the occlusion map.
26
+ // We encode the Z-depth of the occluder into the Red and Green channels to support depth-aware shadows.
27
+ // Z is mapped from [-10.0, 10.0] to [0.0, 1.0].
28
+ float normZ = clamp((vWorldPos.z + 10.0) / 20.0, 0.0, 1.0);
29
+ vec2 enc = vec2(1.0, 255.0) * normZ;
30
+ enc = fract(enc);
31
+ enc.x -= enc.y * (1.0/255.0);
32
+
33
+ // We use Blue = 0.0 to indicate this is an occluder, since clear color is White (1.0, 1.0, 1.0, 1.0).
34
+ gl_FragColor = vec4(enc.x, enc.y, 0.0, 1.0);
35
+ }
36
+ `;
37
+
38
+ export class OcclusionMaterial extends Material {
39
+ constructor() {
40
+ // We can safely reuse the standard LIT_SPRITE_VS as it just passes position and scale
41
+ super(LIT_SPRITE_VS, OCCLUSION_FS);
42
+ }
43
+ }
@@ -0,0 +1,66 @@
1
+ import { Material } from "./Material";
2
+
3
+ export const PARTICLE_VS = `
4
+ attribute vec4 aVertexPosition;
5
+ attribute vec2 aTextureCoord;
6
+ attribute vec4 aVertexColor; // Need color buffer mapped!
7
+
8
+ uniform mat4 uModelMatrix;
9
+ uniform mat4 uModelViewMatrix;
10
+ uniform mat4 uProjectionMatrix;
11
+
12
+ varying highp vec2 vTextureCoord;
13
+ varying lowp vec4 vColor;
14
+
15
+ void main(void) {
16
+ // Note: since ParticleSystem calculates world or relative positions
17
+ // locally if it is handling billboarding, uModelViewMatrix covers camera projection cleanly
18
+ gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
19
+ vTextureCoord = aTextureCoord;
20
+ vColor = aVertexColor;
21
+ }
22
+ `;
23
+
24
+ export const PARTICLE_FS = `
25
+ precision mediump float;
26
+
27
+ varying highp vec2 vTextureCoord;
28
+ varying lowp vec4 vColor;
29
+
30
+ uniform sampler2D uSampler;
31
+ uniform int uUseTexture;
32
+ uniform int uShape; // 0: square, 1: circle
33
+
34
+ void main(void) {
35
+ vec4 texColor = vec4(1.0);
36
+
37
+ if (uUseTexture == 1) {
38
+ texColor = texture2D(uSampler, vTextureCoord);
39
+ } else if (uShape == 1) {
40
+ // Procedural soft circle glow
41
+ vec2 center = vec2(0.5, 0.5);
42
+ float dist = distance(vTextureCoord, center);
43
+
44
+ // Soft gradient fade based on distance (fixed undefined edge boundaries)
45
+ float glow = 1.0 - smoothstep(0.0, 0.5, dist);
46
+ texColor = vec4(1.0, 1.0, 1.0, glow);
47
+ }
48
+
49
+ vec4 finalColor = texColor * vColor;
50
+
51
+ if (finalColor.a < 0.01) discard;
52
+
53
+ gl_FragColor = finalColor;
54
+ }
55
+ `;
56
+
57
+ export class ParticleMaterial extends Material {
58
+ constructor() {
59
+ super(PARTICLE_VS, PARTICLE_FS);
60
+ this.uniforms.uUseTexture = new Int32Array([0]);
61
+ this.uniforms.uShape = new Int32Array([1]); // default to circle
62
+ this.blendMode = "Additive";
63
+ this.depthTest = true;
64
+ this.depthWrite = false;
65
+ }
66
+ }
@@ -0,0 +1,44 @@
1
+ export class Shader {
2
+ public program: WebGLProgram;
3
+
4
+ constructor(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
5
+ const vertexShader = this.compileShader(gl, gl.VERTEX_SHADER, vsSource);
6
+ const fragmentShader = this.compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
7
+
8
+ const program = gl.createProgram();
9
+ if (!program) throw new Error("Failed to create WebGL program");
10
+
11
+ gl.attachShader(program, vertexShader);
12
+ gl.attachShader(program, fragmentShader);
13
+ gl.linkProgram(program);
14
+
15
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
16
+ throw new Error(
17
+ "Unable to initialize the shader program: " +
18
+ gl.getProgramInfoLog(program),
19
+ );
20
+ }
21
+
22
+ this.program = program;
23
+ }
24
+
25
+ private compileShader(
26
+ gl: WebGLRenderingContext,
27
+ type: number,
28
+ source: string,
29
+ ): WebGLShader {
30
+ const shader = gl.createShader(type);
31
+ if (!shader) throw new Error("Failed to create WebGL shader");
32
+
33
+ gl.shaderSource(shader, source);
34
+ gl.compileShader(shader);
35
+
36
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
37
+ const info = gl.getShaderInfoLog(shader);
38
+ document.body.innerHTML += `<div style="color:red;font-size:24px;position:absolute;z-index:9999;top:0;left:0">${info}</div>`;
39
+ gl.deleteShader(shader);
40
+ throw new Error("An error occurred compiling the shaders: " + info);
41
+ }
42
+ return shader;
43
+ }
44
+ }
@@ -0,0 +1,55 @@
1
+ import { LIT_SPRITE_FS, LIT_SPRITE_VS } from "./LitMaterial";
2
+ import { Material } from "./Material";
3
+
4
+ export const SKINNED_LIT_VS = `
5
+ attribute vec4 aVertexPosition;
6
+ attribute vec2 aTextureCoord;
7
+ attribute vec3 aVertexNormal;
8
+
9
+ // Skinning variables
10
+ attribute vec4 aJoints;
11
+ attribute vec4 aWeights;
12
+ uniform mat4 uJointMatrices[64];
13
+
14
+ uniform mat4 uModelMatrix;
15
+ uniform mat4 uModelViewMatrix;
16
+ uniform mat4 uProjectionMatrix;
17
+ uniform vec2 uTexOffset;
18
+ uniform vec2 uTexScale;
19
+
20
+ varying highp vec2 vTextureCoord;
21
+ varying highp vec3 vWorldPos;
22
+ varying highp vec3 vNormal;
23
+
24
+ void main(void) {
25
+ // 1. Calculate Skin Matrix
26
+ mat4 skinMatrix =
27
+ aWeights.x * uJointMatrices[int(aJoints.x)] +
28
+ aWeights.y * uJointMatrices[int(aJoints.y)] +
29
+ aWeights.z * uJointMatrices[int(aJoints.z)] +
30
+ aWeights.w * uJointMatrices[int(aJoints.w)];
31
+
32
+ // 2. Skinned local position
33
+ vec4 localPos = skinMatrix * aVertexPosition;
34
+
35
+ // 3. Final projection
36
+ gl_Position = uProjectionMatrix * uModelViewMatrix * localPos;
37
+
38
+ vTextureCoord = (aTextureCoord * uTexScale) + uTexOffset;
39
+
40
+ // Compute world position for lighting calculations
41
+ vWorldPos = (uModelMatrix * localPos).xyz;
42
+
43
+ // Approximate normal transformation using ModelMatrix directly
44
+ // This is valid for Translation/Rotation uniform scaling
45
+ mat3 normalSkinMatrix = mat3(skinMatrix);
46
+ vNormal = normalize(mat3(uModelMatrix) * normalSkinMatrix * aVertexNormal);
47
+ }
48
+ `;
49
+
50
+ export class SkinnedLitMaterial extends Material {
51
+ constructor() {
52
+ // Uses the exact same Fragment Shader from LitMaterial for a unified lighting pipeline
53
+ super(SKINNED_LIT_VS, LIT_SPRITE_FS);
54
+ }
55
+ }