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,917 @@
1
+ import { mat4 } from "gl-matrix";
2
+ import { Camera } from "../components/Camera";
3
+ import { MeshRenderer } from "../components/MeshRenderer";
4
+ import { SkinnedMeshRenderer } from "../components/SkinnedMeshRenderer";
5
+ import { SpriteRenderer } from "../components/SpriteRenderer";
6
+ import { Transform } from "../components/Transform";
7
+ import { GlobalAssets } from "../core/AssetManager";
8
+ import { System } from "../ecs/System";
9
+ import type { World } from "../ecs/World";
10
+ import { LightingSystem } from "../systems/LightingSystem";
11
+ import type { Material } from "./Material";
12
+ import { Shader } from "./Shader";
13
+ import { OcclusionMaterial } from "./OcclusionMaterial";
14
+ import { ShadowCaster } from "../components/ShadowCaster";
15
+
16
+ type CompiledShader = {
17
+ shader: Shader;
18
+ attribLocations: Record<string, number>;
19
+ uniformLocations: Record<string, WebGLUniformLocation>;
20
+ };
21
+
22
+ export class WebGLRenderer extends System {
23
+ private gl: WebGLRenderingContext;
24
+
25
+ private quadPositionBuffer: WebGLBuffer;
26
+ private quadTexCoordBuffer: WebGLBuffer;
27
+
28
+ private compiledShaders = new Map<string, CompiledShader>();
29
+
30
+ private occlusionFBO: WebGLFramebuffer | null = null;
31
+ private occlusionTexture: WebGLTexture | null = null;
32
+ private occlusionMaterial: OcclusionMaterial;
33
+
34
+ constructor(world: World, canvas: HTMLCanvasElement) {
35
+ super(world);
36
+ const gl = canvas.getContext("webgl");
37
+ if (!gl) throw new Error("WebGL not supported");
38
+ this.gl = gl;
39
+
40
+ // Shared quad for sprites
41
+ this.quadPositionBuffer = gl.createBuffer()!;
42
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadPositionBuffer);
43
+ const positions = [
44
+ 1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0,
45
+ ];
46
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
47
+
48
+ this.quadTexCoordBuffer = gl.createBuffer()!;
49
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadTexCoordBuffer);
50
+ const texcoords = [1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0];
51
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texcoords), gl.STATIC_DRAW);
52
+
53
+ // Configure gl
54
+ gl.clearColor(0.005, 0.005, 0.015, 1.0);
55
+ gl.clearDepth(1.0);
56
+ gl.enable(gl.DEPTH_TEST);
57
+ gl.depthFunc(gl.LEQUAL);
58
+ gl.enable(gl.CULL_FACE);
59
+ gl.enable(gl.BLEND);
60
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
61
+
62
+ this.occlusionMaterial = new OcclusionMaterial();
63
+
64
+ this.resize(canvas.clientWidth, canvas.clientHeight);
65
+ window.addEventListener("resize", () => {
66
+ this.resize(canvas.clientWidth, canvas.clientHeight);
67
+ });
68
+ }
69
+
70
+ resize(width: number, height: number) {
71
+ this.gl.canvas.width = width;
72
+ this.gl.canvas.height = height;
73
+ this.gl.viewport(0, 0, width, height);
74
+
75
+ if (!this.occlusionTexture) {
76
+ this.occlusionTexture = this.gl.createTexture()!;
77
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.occlusionTexture);
78
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
79
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
80
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
81
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
82
+
83
+ this.occlusionFBO = this.gl.createFramebuffer()!;
84
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.occlusionFBO);
85
+ this.gl.framebufferTexture2D(
86
+ this.gl.FRAMEBUFFER,
87
+ this.gl.COLOR_ATTACHMENT0,
88
+ this.gl.TEXTURE_2D,
89
+ this.occlusionTexture,
90
+ 0
91
+ );
92
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
93
+ }
94
+
95
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.occlusionTexture);
96
+ this.gl.texImage2D(
97
+ this.gl.TEXTURE_2D,
98
+ 0,
99
+ this.gl.RGBA,
100
+ width,
101
+ height,
102
+ 0,
103
+ this.gl.RGBA,
104
+ this.gl.UNSIGNED_BYTE,
105
+ null
106
+ );
107
+ }
108
+
109
+ private magentaTexture: WebGLTexture | null = null;
110
+ private getMagentaTexture(gl: WebGLRenderingContext): WebGLTexture {
111
+ if (this.magentaTexture) return this.magentaTexture;
112
+ this.magentaTexture = gl.createTexture()!;
113
+ gl.bindTexture(gl.TEXTURE_2D, this.magentaTexture);
114
+ const pixel = new Uint8Array([255, 0, 255, 255]);
115
+ gl.texImage2D(
116
+ gl.TEXTURE_2D,
117
+ 0,
118
+ gl.RGBA,
119
+ 1,
120
+ 1,
121
+ 0,
122
+ gl.RGBA,
123
+ gl.UNSIGNED_BYTE,
124
+ pixel,
125
+ );
126
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
127
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
128
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
129
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
130
+ return this.magentaTexture;
131
+ }
132
+
133
+ public updateTexture(
134
+ spriteRenderer: SpriteRenderer,
135
+ source: HTMLCanvasElement | HTMLImageElement,
136
+ ) {
137
+ if (!this.gl) return;
138
+ const gl = this.gl;
139
+ if (!spriteRenderer.texture) {
140
+ spriteRenderer.texture = gl.createTexture() || undefined;
141
+ }
142
+ gl.bindTexture(gl.TEXTURE_2D, spriteRenderer.texture || null);
143
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
144
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
145
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
146
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
147
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
148
+ spriteRenderer._loaded = true;
149
+ }
150
+
151
+ private getCompiledShader(material: Material): CompiledShader {
152
+ if (this.compiledShaders.has(material.programHash)) {
153
+ return this.compiledShaders.get(material.programHash)!;
154
+ }
155
+
156
+ const shader = new Shader(
157
+ this.gl,
158
+ material.vertexShader,
159
+ material.fragmentShader,
160
+ );
161
+ const gl = this.gl;
162
+
163
+ const attribLocations: Record<string, number> = {};
164
+ const numAttribs = gl.getProgramParameter(
165
+ shader.program,
166
+ gl.ACTIVE_ATTRIBUTES,
167
+ );
168
+ for (let i = 0; i < numAttribs; ++i) {
169
+ const info = gl.getActiveAttrib(shader.program, i);
170
+ if (info)
171
+ attribLocations[info.name] = gl.getAttribLocation(
172
+ shader.program,
173
+ info.name,
174
+ );
175
+ }
176
+
177
+ const uniformLocations: Record<string, WebGLUniformLocation> = {};
178
+ const numUniforms = gl.getProgramParameter(
179
+ shader.program,
180
+ gl.ACTIVE_UNIFORMS,
181
+ );
182
+ for (let i = 0; i < numUniforms; ++i) {
183
+ const info = gl.getActiveUniform(shader.program, i);
184
+ if (info) {
185
+ const loc = gl.getUniformLocation(shader.program, info.name)!;
186
+ uniformLocations[info.name] = loc;
187
+ const baseName = info.name.split("[")[0];
188
+ if (baseName !== info.name) {
189
+ uniformLocations[baseName] = loc;
190
+ }
191
+ }
192
+ }
193
+
194
+ const compiled: CompiledShader = {
195
+ shader,
196
+ attribLocations,
197
+ uniformLocations,
198
+ };
199
+ this.compiledShaders.set(material.programHash, compiled);
200
+ return compiled;
201
+ }
202
+
203
+ update(_dt: number): void {
204
+ const gl = this.gl;
205
+
206
+ let projectionMatrix = mat4.create();
207
+ const viewMatrix = mat4.create();
208
+
209
+ let cullBox: {
210
+ minX: number;
211
+ maxX: number;
212
+ minY: number;
213
+ maxY: number;
214
+ } | null = null;
215
+
216
+ const cameraEntities = this.world.query(Camera, Transform);
217
+ if (cameraEntities.length > 0) {
218
+ const camParams = this.world.getComponent(cameraEntities[0], Camera)!;
219
+ const camTransform = this.world.getComponent(
220
+ cameraEntities[0],
221
+ Transform,
222
+ )!;
223
+ camParams.updateProjection(gl.canvas.width, gl.canvas.height);
224
+ projectionMatrix = camParams.projectionMatrix;
225
+ camTransform.updateMatrix();
226
+ mat4.invert(viewMatrix, camTransform.matrix);
227
+
228
+ if (camParams.isOrthographic) {
229
+ const w = camParams.orthoSize * camParams.aspect;
230
+ const h = camParams.orthoSize;
231
+ const px = camTransform.position[0];
232
+ const py = camTransform.position[1];
233
+ cullBox = {
234
+ minX: px - w,
235
+ maxX: px + w,
236
+ minY: py - h,
237
+ maxY: py + h,
238
+ };
239
+ }
240
+ } else {
241
+ const aspect = gl.canvas.width / Math.max(gl.canvas.height, 1);
242
+ mat4.perspective(
243
+ projectionMatrix,
244
+ (45 * Math.PI) / 180,
245
+ aspect,
246
+ 0.1,
247
+ 100,
248
+ );
249
+ mat4.translate(viewMatrix, viewMatrix, [0, 0, -10]);
250
+ }
251
+
252
+ const meshEntities = this.world.query(Transform, MeshRenderer);
253
+ const skinnedMeshEntities = this.world.query(Transform, SkinnedMeshRenderer);
254
+ const allMeshEntities = [...meshEntities, ...skinnedMeshEntities];
255
+
256
+ const spriteEntities = this.world.query(Transform, SpriteRenderer);
257
+
258
+ // Grouping logic
259
+ const meshBatches = new Map<
260
+ string,
261
+ Array<{ entity: number; renderer: MeshRenderer; transform: Transform }>
262
+ >();
263
+ for (const e of allMeshEntities) {
264
+ const r = this.world.getComponent(e, MeshRenderer) || this.world.getComponent(e, SkinnedMeshRenderer)!;
265
+ const hash = r.material.programHash;
266
+ if (!meshBatches.has(hash)) meshBatches.set(hash, []);
267
+ meshBatches.get(hash)?.push({
268
+ entity: e,
269
+ renderer: r,
270
+ transform: this.world.getComponent(e, Transform)!,
271
+ });
272
+ }
273
+
274
+ const spriteBatches = new Map<
275
+ string,
276
+ Array<{ entity: number; renderer: SpriteRenderer; transform: Transform }>
277
+ >();
278
+ for (const e of spriteEntities) {
279
+ const transform = this.world.getComponent(e, Transform)!;
280
+
281
+ if (cullBox) {
282
+ const maxS = Math.max(
283
+ Math.abs(transform.scale[0]),
284
+ Math.abs(transform.scale[1]),
285
+ );
286
+ const radius = maxS * 1.5;
287
+ const px = transform.position[0];
288
+ const py = transform.position[1];
289
+
290
+ if (
291
+ px + radius < cullBox.minX ||
292
+ px - radius > cullBox.maxX ||
293
+ py + radius < cullBox.minY ||
294
+ py - radius > cullBox.maxY
295
+ ) {
296
+ continue;
297
+ }
298
+ }
299
+
300
+ const r = this.world.getComponent(e, SpriteRenderer)!;
301
+ const hash = r.material.programHash;
302
+ if (!spriteBatches.has(hash)) spriteBatches.set(hash, []);
303
+ spriteBatches.get(hash)?.push({
304
+ entity: e,
305
+ renderer: r,
306
+ transform: transform,
307
+ });
308
+ }
309
+
310
+ // --- PASS 1: OCCLUSION MAP ---
311
+ if (this.occlusionFBO) {
312
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.occlusionFBO);
313
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
314
+ gl.clearColor(1.0, 1.0, 1.0, 1.0); // White background = unoccluded
315
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
316
+
317
+ const occProgInfo = this.getCompiledShader(this.occlusionMaterial);
318
+ gl.useProgram(occProgInfo.shader.program);
319
+
320
+ if (occProgInfo.uniformLocations.uProjectionMatrix) {
321
+ gl.uniformMatrix4fv(occProgInfo.uniformLocations.uProjectionMatrix, false, projectionMatrix);
322
+ }
323
+
324
+ if ("aVertexPosition" in occProgInfo.attribLocations) {
325
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadPositionBuffer);
326
+ gl.vertexAttribPointer(occProgInfo.attribLocations.aVertexPosition, 3, gl.FLOAT, false, 0, 0);
327
+ gl.enableVertexAttribArray(occProgInfo.attribLocations.aVertexPosition);
328
+ }
329
+
330
+ if ("aTextureCoord" in occProgInfo.attribLocations) {
331
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadTexCoordBuffer);
332
+ gl.vertexAttribPointer(occProgInfo.attribLocations.aTextureCoord, 2, gl.FLOAT, false, 0, 0);
333
+ gl.enableVertexAttribArray(occProgInfo.attribLocations.aTextureCoord);
334
+ }
335
+
336
+ // Draw shadow casters
337
+ const shadowCasters = this.world.query(Transform, SpriteRenderer, ShadowCaster);
338
+
339
+ const depthSorted = [];
340
+ for (const e of shadowCasters) {
341
+ depthSorted.push({
342
+ transform: this.world.getComponent(e, Transform)!,
343
+ sprite: this.world.getComponent(e, SpriteRenderer)!
344
+ });
345
+ }
346
+ depthSorted.sort((a, b) => a.transform.localPosition[2] - b.transform.localPosition[2]);
347
+
348
+ for (const { sprite, transform } of depthSorted) {
349
+ if (!sprite._loaded) continue;
350
+
351
+ const modelViewMatrix = mat4.create();
352
+ mat4.multiply(modelViewMatrix, viewMatrix, transform.matrix);
353
+
354
+ if (occProgInfo.uniformLocations.uModelViewMatrix)
355
+ gl.uniformMatrix4fv(occProgInfo.uniformLocations.uModelViewMatrix, false, modelViewMatrix);
356
+ if (occProgInfo.uniformLocations.uModelMatrix)
357
+ gl.uniformMatrix4fv(occProgInfo.uniformLocations.uModelMatrix, false, transform.matrix);
358
+ if (occProgInfo.uniformLocations.uColor)
359
+ gl.uniform4fv(occProgInfo.uniformLocations.uColor, sprite.color);
360
+ if (occProgInfo.uniformLocations.uTexOffset)
361
+ gl.uniform2fv(occProgInfo.uniformLocations.uTexOffset, sprite.uvOffset);
362
+ if (occProgInfo.uniformLocations.uTexScale)
363
+ gl.uniform2fv(occProgInfo.uniformLocations.uTexScale, sprite.uvScale);
364
+
365
+ if (occProgInfo.uniformLocations.uSampler) {
366
+ gl.activeTexture(gl.TEXTURE0);
367
+ gl.bindTexture(gl.TEXTURE_2D, sprite.texture || null);
368
+ gl.uniform1i(occProgInfo.uniformLocations.uSampler, 0);
369
+ }
370
+
371
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
372
+ }
373
+ }
374
+
375
+ // --- PASS 2: MAIN SCREEN ---
376
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
377
+ gl.clearColor(0.005, 0.005, 0.015, 1.0);
378
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
379
+
380
+ // DRAW SPRITES
381
+ for (const [_hash, batch] of spriteBatches) {
382
+ if (batch.length === 0) continue;
383
+
384
+ const firstMat = batch[0].renderer.material;
385
+ const progInfo = this.getCompiledShader(firstMat);
386
+
387
+ gl.useProgram(progInfo.shader.program);
388
+
389
+ if (firstMat.cullFace) gl.enable(gl.CULL_FACE);
390
+ else gl.disable(gl.CULL_FACE);
391
+ if (firstMat.depthTest) gl.enable(gl.DEPTH_TEST);
392
+ else gl.disable(gl.DEPTH_TEST);
393
+
394
+ gl.depthMask(firstMat.depthWrite);
395
+ if (firstMat.blendMode === "Additive") {
396
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
397
+ } else if (firstMat.blendMode === "Screen") {
398
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_COLOR);
399
+ } else {
400
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
401
+ }
402
+
403
+ if (progInfo.uniformLocations.uProjectionMatrix) {
404
+ gl.uniformMatrix4fv(
405
+ progInfo.uniformLocations.uProjectionMatrix,
406
+ false,
407
+ projectionMatrix,
408
+ );
409
+ }
410
+
411
+ if (progInfo.uniformLocations.uViewMatrix) {
412
+ gl.uniformMatrix4fv(progInfo.uniformLocations.uViewMatrix, false, viewMatrix);
413
+ }
414
+ if (progInfo.uniformLocations.uResolution) {
415
+ gl.uniform2f(progInfo.uniformLocations.uResolution, gl.canvas.width, gl.canvas.height);
416
+ }
417
+ if (progInfo.uniformLocations.uTime) {
418
+ gl.uniform1f(progInfo.uniformLocations.uTime, (performance.now() % 100000) / 1000.0);
419
+ }
420
+ if (progInfo.uniformLocations.uOcclusionSampler) {
421
+ gl.activeTexture(gl.TEXTURE1);
422
+ gl.bindTexture(gl.TEXTURE_2D, this.occlusionTexture);
423
+ gl.uniform1i(progInfo.uniformLocations.uOcclusionSampler, 1);
424
+ }
425
+
426
+ if (progInfo.uniformLocations.uAmbientColor)
427
+ gl.uniform3fv(
428
+ progInfo.uniformLocations.uAmbientColor,
429
+ LightingSystem.ambientColor,
430
+ );
431
+ if (progInfo.uniformLocations.uActivePointLights)
432
+ gl.uniform1i(
433
+ progInfo.uniformLocations.uActivePointLights,
434
+ LightingSystem.activePointLights,
435
+ );
436
+ if (progInfo.uniformLocations.uLightPositions)
437
+ gl.uniform3fv(
438
+ progInfo.uniformLocations.uLightPositions,
439
+ LightingSystem.lightPositions,
440
+ );
441
+ if (progInfo.uniformLocations.uLightColors)
442
+ gl.uniform3fv(
443
+ progInfo.uniformLocations.uLightColors,
444
+ LightingSystem.lightColors,
445
+ );
446
+ if (progInfo.uniformLocations.uLightParams)
447
+ gl.uniform4fv(
448
+ progInfo.uniformLocations.uLightParams,
449
+ LightingSystem.lightParams,
450
+ );
451
+ if (progInfo.uniformLocations.uActiveParticleLights)
452
+ gl.uniform1i(
453
+ progInfo.uniformLocations.uActiveParticleLights,
454
+ LightingSystem.activeParticleLights,
455
+ );
456
+ if (progInfo.uniformLocations.uParticleLightPositions)
457
+ gl.uniform4fv(
458
+ progInfo.uniformLocations.uParticleLightPositions,
459
+ LightingSystem.particleLightPositions,
460
+ );
461
+ if (progInfo.uniformLocations.uParticleLightColors)
462
+ gl.uniform3fv(
463
+ progInfo.uniformLocations.uParticleLightColors,
464
+ LightingSystem.particleLightColors,
465
+ );
466
+
467
+ if ("aVertexPosition" in progInfo.attribLocations) {
468
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadPositionBuffer);
469
+ gl.vertexAttribPointer(
470
+ progInfo.attribLocations.aVertexPosition,
471
+ 3,
472
+ gl.FLOAT,
473
+ false,
474
+ 0,
475
+ 0,
476
+ );
477
+ gl.enableVertexAttribArray(progInfo.attribLocations.aVertexPosition);
478
+ }
479
+
480
+ if ("aTextureCoord" in progInfo.attribLocations) {
481
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadTexCoordBuffer);
482
+ gl.vertexAttribPointer(
483
+ progInfo.attribLocations.aTextureCoord,
484
+ 2,
485
+ gl.FLOAT,
486
+ false,
487
+ 0,
488
+ 0,
489
+ );
490
+ gl.enableVertexAttribArray(progInfo.attribLocations.aTextureCoord);
491
+ }
492
+
493
+ for (const [name, val] of Object.entries(firstMat.uniforms)) {
494
+ const loc = progInfo.uniformLocations[name];
495
+ if (!loc) continue;
496
+ if (val instanceof Float32Array || Array.isArray(val)) {
497
+ if (val.length === 2) gl.uniform2fv(loc, val);
498
+ else if (val.length === 3) gl.uniform3fv(loc, val);
499
+ else if (val.length === 4) gl.uniform4fv(loc, val);
500
+ } else if (typeof val === "number") {
501
+ gl.uniform1f(loc, val);
502
+ }
503
+ }
504
+
505
+ // Sort batch back-to-front (Painter's Algorithm) to resolve semi-transparent depth buffer culling clipping
506
+ batch.sort(
507
+ (a, b) => a.transform.localPosition[2] - b.transform.localPosition[2],
508
+ );
509
+
510
+ for (const { renderer: sprite, transform } of batch) {
511
+ if (!sprite._loaded && sprite.imageSrc) {
512
+ let texAsset = GlobalAssets.getTexture(sprite.imageSrc);
513
+ if (!texAsset) {
514
+ texAsset = GlobalAssets.loadTexture(sprite.imageSrc);
515
+ }
516
+
517
+ if (texAsset?.loaded) {
518
+ if (!texAsset.glTexture) {
519
+ const texture = gl.createTexture()!;
520
+ gl.bindTexture(gl.TEXTURE_2D, texture);
521
+ gl.texImage2D(
522
+ gl.TEXTURE_2D,
523
+ 0,
524
+ gl.RGBA,
525
+ gl.RGBA,
526
+ gl.UNSIGNED_BYTE,
527
+ texAsset.image,
528
+ );
529
+ gl.texParameteri(
530
+ gl.TEXTURE_2D,
531
+ gl.TEXTURE_WRAP_S,
532
+ gl.CLAMP_TO_EDGE,
533
+ );
534
+ gl.texParameteri(
535
+ gl.TEXTURE_2D,
536
+ gl.TEXTURE_WRAP_T,
537
+ gl.CLAMP_TO_EDGE,
538
+ );
539
+ gl.texParameteri(
540
+ gl.TEXTURE_2D,
541
+ gl.TEXTURE_MIN_FILTER,
542
+ gl.NEAREST,
543
+ );
544
+ gl.texParameteri(
545
+ gl.TEXTURE_2D,
546
+ gl.TEXTURE_MAG_FILTER,
547
+ gl.NEAREST,
548
+ );
549
+ texAsset.glTexture = texture;
550
+ }
551
+ sprite.texture = texAsset.glTexture;
552
+ sprite._loaded = true;
553
+ } else if (!sprite.texture) {
554
+ sprite.texture = this.getMagentaTexture(gl);
555
+ }
556
+ }
557
+
558
+ const modelViewMatrix = mat4.create();
559
+ mat4.multiply(modelViewMatrix, viewMatrix, transform.matrix);
560
+
561
+ if (progInfo.uniformLocations.uModelViewMatrix)
562
+ gl.uniformMatrix4fv(
563
+ progInfo.uniformLocations.uModelViewMatrix,
564
+ false,
565
+ modelViewMatrix,
566
+ );
567
+ if (progInfo.uniformLocations.uModelMatrix)
568
+ gl.uniformMatrix4fv(
569
+ progInfo.uniformLocations.uModelMatrix,
570
+ false,
571
+ transform.matrix,
572
+ );
573
+ if (progInfo.uniformLocations.uColor)
574
+ gl.uniform4fv(progInfo.uniformLocations.uColor, sprite.color);
575
+ if (progInfo.uniformLocations.uTexOffset)
576
+ gl.uniform2fv(progInfo.uniformLocations.uTexOffset, sprite.uvOffset);
577
+ if (progInfo.uniformLocations.uTexScale)
578
+ gl.uniform2fv(progInfo.uniformLocations.uTexScale, sprite.uvScale);
579
+
580
+ if (progInfo.uniformLocations.uSampler) {
581
+ gl.activeTexture(gl.TEXTURE0);
582
+ gl.bindTexture(gl.TEXTURE_2D, sprite.texture || null);
583
+ gl.uniform1i(progInfo.uniformLocations.uSampler, 0);
584
+ }
585
+
586
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
587
+ }
588
+ }
589
+
590
+ // DRAW MESHES
591
+ for (const [_hash, batch] of meshBatches) {
592
+ if (batch.length === 0) continue;
593
+
594
+ const firstMat = batch[0].renderer.material;
595
+ const progInfo = this.getCompiledShader(firstMat);
596
+
597
+ gl.useProgram(progInfo.shader.program);
598
+
599
+ if (firstMat.cullFace) gl.enable(gl.CULL_FACE);
600
+ else gl.disable(gl.CULL_FACE);
601
+ if (firstMat.depthTest) gl.enable(gl.DEPTH_TEST);
602
+ else gl.disable(gl.DEPTH_TEST);
603
+
604
+ gl.depthMask(firstMat.depthWrite);
605
+ if (firstMat.blendMode === "Additive") {
606
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
607
+ } else if (firstMat.blendMode === "Screen") {
608
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_COLOR);
609
+ } else {
610
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
611
+ }
612
+
613
+ if (progInfo.uniformLocations.uProjectionMatrix) {
614
+ gl.uniformMatrix4fv(
615
+ progInfo.uniformLocations.uProjectionMatrix,
616
+ false,
617
+ projectionMatrix,
618
+ );
619
+ }
620
+
621
+ if (progInfo.uniformLocations.uViewMatrix) {
622
+ gl.uniformMatrix4fv(progInfo.uniformLocations.uViewMatrix, false, viewMatrix);
623
+ }
624
+ if (progInfo.uniformLocations.uResolution) {
625
+ gl.uniform2f(progInfo.uniformLocations.uResolution, gl.canvas.width, gl.canvas.height);
626
+ }
627
+ if (progInfo.uniformLocations.uTime) {
628
+ gl.uniform1f(progInfo.uniformLocations.uTime, (performance.now() % 100000) / 1000.0);
629
+ }
630
+ if (progInfo.uniformLocations.uOcclusionSampler) {
631
+ gl.activeTexture(gl.TEXTURE1);
632
+ gl.bindTexture(gl.TEXTURE_2D, this.occlusionTexture);
633
+ gl.uniform1i(progInfo.uniformLocations.uOcclusionSampler, 1);
634
+ }
635
+
636
+ if (progInfo.uniformLocations.uAmbientColor)
637
+ gl.uniform3fv(
638
+ progInfo.uniformLocations.uAmbientColor,
639
+ LightingSystem.ambientColor,
640
+ );
641
+ if (progInfo.uniformLocations.uActivePointLights)
642
+ gl.uniform1i(
643
+ progInfo.uniformLocations.uActivePointLights,
644
+ LightingSystem.activePointLights,
645
+ );
646
+ if (progInfo.uniformLocations.uLightPositions)
647
+ gl.uniform3fv(
648
+ progInfo.uniformLocations.uLightPositions,
649
+ LightingSystem.lightPositions,
650
+ );
651
+ if (progInfo.uniformLocations.uLightColors)
652
+ gl.uniform3fv(
653
+ progInfo.uniformLocations.uLightColors,
654
+ LightingSystem.lightColors,
655
+ );
656
+ if (progInfo.uniformLocations.uLightParams)
657
+ gl.uniform4fv(
658
+ progInfo.uniformLocations.uLightParams,
659
+ LightingSystem.lightParams,
660
+ );
661
+ if (progInfo.uniformLocations.uActiveParticleLights)
662
+ gl.uniform1i(
663
+ progInfo.uniformLocations.uActiveParticleLights,
664
+ LightingSystem.activeParticleLights,
665
+ );
666
+ if (progInfo.uniformLocations.uParticleLightPositions)
667
+ gl.uniform4fv(
668
+ progInfo.uniformLocations.uParticleLightPositions,
669
+ LightingSystem.particleLightPositions,
670
+ );
671
+ if (progInfo.uniformLocations.uParticleLightColors)
672
+ gl.uniform3fv(
673
+ progInfo.uniformLocations.uParticleLightColors,
674
+ LightingSystem.particleLightColors,
675
+ );
676
+
677
+ // Apply custom uniforms for material once per batch
678
+ let textureUnit = 0;
679
+ for (const [name, val] of Object.entries(firstMat.uniforms)) {
680
+ const loc = progInfo.uniformLocations[name];
681
+ if (!loc) continue;
682
+
683
+ if (
684
+ val &&
685
+ typeof val === "object" &&
686
+ "image" in val &&
687
+ "loaded" in val
688
+ ) {
689
+ const texAsset = val as any;
690
+ if (texAsset.loaded) {
691
+ if (!texAsset.glTexture) {
692
+ const texture = gl.createTexture()!;
693
+ gl.bindTexture(gl.TEXTURE_2D, texture);
694
+ gl.texImage2D(
695
+ gl.TEXTURE_2D,
696
+ 0,
697
+ gl.RGBA,
698
+ gl.RGBA,
699
+ gl.UNSIGNED_BYTE,
700
+ texAsset.image,
701
+ );
702
+ gl.texParameteri(
703
+ gl.TEXTURE_2D,
704
+ gl.TEXTURE_WRAP_S,
705
+ gl.CLAMP_TO_EDGE,
706
+ );
707
+ gl.texParameteri(
708
+ gl.TEXTURE_2D,
709
+ gl.TEXTURE_WRAP_T,
710
+ gl.CLAMP_TO_EDGE,
711
+ );
712
+ gl.texParameteri(
713
+ gl.TEXTURE_2D,
714
+ gl.TEXTURE_MIN_FILTER,
715
+ gl.NEAREST,
716
+ );
717
+ gl.texParameteri(
718
+ gl.TEXTURE_2D,
719
+ gl.TEXTURE_MAG_FILTER,
720
+ gl.NEAREST,
721
+ );
722
+ texAsset.glTexture = texture;
723
+ }
724
+ gl.activeTexture(gl.TEXTURE0 + textureUnit);
725
+ gl.bindTexture(gl.TEXTURE_2D, texAsset.glTexture);
726
+ gl.uniform1i(loc, textureUnit);
727
+ textureUnit++;
728
+ }
729
+ } else if (val instanceof Float32Array || Array.isArray(val)) {
730
+ if (val.length === 2) gl.uniform2fv(loc, val);
731
+ else if (val.length === 3) gl.uniform3fv(loc, val);
732
+ else if (val.length === 4) gl.uniform4fv(loc, val);
733
+ } else if (typeof val === "number") {
734
+ gl.uniform1f(loc, val);
735
+ }
736
+ }
737
+
738
+ for (const { renderer: meshBase, transform, entity } of batch) {
739
+ const mesh: any = meshBase; // Cast to any to access Skinned properties if they exist
740
+
741
+ if (!mesh.positionBuffer || mesh.isDirty) {
742
+ if (!mesh.positionBuffer) mesh.positionBuffer = gl.createBuffer()!;
743
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.positionBuffer);
744
+ gl.bufferData(gl.ARRAY_BUFFER, mesh.positions, gl.DYNAMIC_DRAW);
745
+ }
746
+ if ((!mesh.colorBuffer || mesh.isDirty) && mesh.colors) {
747
+ if (!mesh.colorBuffer) mesh.colorBuffer = gl.createBuffer()!;
748
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.colorBuffer);
749
+ gl.bufferData(gl.ARRAY_BUFFER, mesh.colors, gl.DYNAMIC_DRAW);
750
+ }
751
+ if ((!mesh.normalBuffer || mesh.isDirty) && mesh.normals) {
752
+ if (!mesh.normalBuffer) mesh.normalBuffer = gl.createBuffer()!;
753
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.normalBuffer);
754
+ gl.bufferData(gl.ARRAY_BUFFER, mesh.normals, gl.DYNAMIC_DRAW);
755
+ }
756
+ if ((!mesh.uvBuffer || mesh.isDirty) && mesh.uvs) {
757
+ if (!mesh.uvBuffer) mesh.uvBuffer = gl.createBuffer()!;
758
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.uvBuffer);
759
+ gl.bufferData(gl.ARRAY_BUFFER, mesh.uvs, gl.DYNAMIC_DRAW);
760
+ }
761
+
762
+ // SkinnedMeshRenderer Additions
763
+ if ("joints" in mesh) {
764
+ if (!mesh.jointBuffer || mesh.isDirty) {
765
+ if (!mesh.jointBuffer) mesh.jointBuffer = gl.createBuffer()!;
766
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.jointBuffer);
767
+ gl.bufferData(gl.ARRAY_BUFFER, mesh.joints, gl.DYNAMIC_DRAW);
768
+ }
769
+ if (!mesh.weightBuffer || mesh.isDirty) {
770
+ if (!mesh.weightBuffer) mesh.weightBuffer = gl.createBuffer()!;
771
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.weightBuffer);
772
+ gl.bufferData(gl.ARRAY_BUFFER, mesh.weights, gl.DYNAMIC_DRAW);
773
+ }
774
+ }
775
+
776
+ if ((!mesh.indexBuffer || mesh.isDirty) && mesh.indices) {
777
+ if (!mesh.indexBuffer) mesh.indexBuffer = gl.createBuffer()!;
778
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.indexBuffer);
779
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, mesh.indices, gl.DYNAMIC_DRAW);
780
+ }
781
+ mesh.isDirty = false;
782
+
783
+ if ("aVertexPosition" in progInfo.attribLocations) {
784
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.positionBuffer);
785
+ gl.vertexAttribPointer(
786
+ progInfo.attribLocations.aVertexPosition,
787
+ 3,
788
+ gl.FLOAT,
789
+ false,
790
+ 0,
791
+ 0,
792
+ );
793
+ gl.enableVertexAttribArray(progInfo.attribLocations.aVertexPosition);
794
+ }
795
+
796
+ if ("aVertexColor" in progInfo.attribLocations) {
797
+ if (mesh.colorBuffer) {
798
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.colorBuffer);
799
+ gl.vertexAttribPointer(
800
+ progInfo.attribLocations.aVertexColor,
801
+ 4,
802
+ gl.FLOAT,
803
+ false,
804
+ 0,
805
+ 0,
806
+ );
807
+ gl.enableVertexAttribArray(progInfo.attribLocations.aVertexColor);
808
+ } else {
809
+ gl.disableVertexAttribArray(progInfo.attribLocations.aVertexColor);
810
+ gl.vertexAttrib4fv(
811
+ progInfo.attribLocations.aVertexColor,
812
+ [1, 1, 1, 1],
813
+ );
814
+ }
815
+ }
816
+
817
+ if ("aVertexNormal" in progInfo.attribLocations) {
818
+ if (mesh.normalBuffer) {
819
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.normalBuffer);
820
+ gl.vertexAttribPointer(
821
+ progInfo.attribLocations.aVertexNormal,
822
+ 3,
823
+ gl.FLOAT,
824
+ false,
825
+ 0,
826
+ 0,
827
+ );
828
+ gl.enableVertexAttribArray(progInfo.attribLocations.aVertexNormal);
829
+ } else {
830
+ gl.disableVertexAttribArray(progInfo.attribLocations.aVertexNormal);
831
+ gl.vertexAttrib3fv(
832
+ progInfo.attribLocations.aVertexNormal,
833
+ [0, 0, 0],
834
+ );
835
+ }
836
+ }
837
+
838
+ if ("aTextureCoord" in progInfo.attribLocations) {
839
+ if (mesh.uvBuffer) {
840
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.uvBuffer);
841
+ gl.vertexAttribPointer(
842
+ progInfo.attribLocations.aTextureCoord,
843
+ 2,
844
+ gl.FLOAT,
845
+ false,
846
+ 0,
847
+ 0,
848
+ );
849
+ gl.enableVertexAttribArray(progInfo.attribLocations.aTextureCoord);
850
+ } else {
851
+ gl.disableVertexAttribArray(progInfo.attribLocations.aTextureCoord);
852
+ }
853
+ }
854
+
855
+ // Skinned attributes bindings
856
+ if ("aJoints" in progInfo.attribLocations && mesh.jointBuffer) {
857
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.jointBuffer);
858
+ gl.vertexAttribPointer(progInfo.attribLocations.aJoints, 4, gl.FLOAT, false, 0, 0);
859
+ gl.enableVertexAttribArray(progInfo.attribLocations.aJoints);
860
+ }
861
+ if ("aWeights" in progInfo.attribLocations && mesh.weightBuffer) {
862
+ gl.bindBuffer(gl.ARRAY_BUFFER, mesh.weightBuffer);
863
+ gl.vertexAttribPointer(progInfo.attribLocations.aWeights, 4, gl.FLOAT, false, 0, 0);
864
+ gl.enableVertexAttribArray(progInfo.attribLocations.aWeights);
865
+ }
866
+
867
+ // Setup joint matrices uniform if SkinnedLitMaterial
868
+ if (progInfo.uniformLocations.uJointMatrices) {
869
+ const compMap = this.world.getComponentsForEntity(entity);
870
+ if (compMap) {
871
+ let animator: any = null;
872
+ for (const [cls, comp] of compMap.entries()) {
873
+ if (cls.name === "Animator3D") {
874
+ animator = comp;
875
+ break;
876
+ }
877
+ }
878
+
879
+ if (animator && animator.jointMatrices) {
880
+ gl.uniformMatrix4fv(progInfo.uniformLocations.uJointMatrices, false, animator.jointMatrices);
881
+ }
882
+ }
883
+ }
884
+
885
+ const modelViewMatrix = mat4.create();
886
+ mat4.multiply(modelViewMatrix, viewMatrix, transform.matrix);
887
+
888
+ if (progInfo.uniformLocations.uModelViewMatrix) {
889
+ gl.uniformMatrix4fv(
890
+ progInfo.uniformLocations.uModelViewMatrix,
891
+ false,
892
+ modelViewMatrix,
893
+ );
894
+ }
895
+ if (progInfo.uniformLocations.uModelMatrix) {
896
+ gl.uniformMatrix4fv(
897
+ progInfo.uniformLocations.uModelMatrix,
898
+ false,
899
+ transform.matrix,
900
+ );
901
+ }
902
+
903
+ if (mesh.indexBuffer && mesh.indices) {
904
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.indexBuffer);
905
+ gl.drawElements(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0);
906
+ } else {
907
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.vertexCount);
908
+ }
909
+ }
910
+ }
911
+
912
+ // Always enable cull face back for future draws if disabled manually
913
+ gl.enable(gl.CULL_FACE);
914
+ gl.depthMask(true);
915
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
916
+ }
917
+ }