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,211 @@
1
+ import { vec3 } from "gl-matrix";
2
+ import { Collider } from "../components/Collider";
3
+ import { RigidBody } from "../components/RigidBody";
4
+ import { Transform } from "../components/Transform";
5
+ import { System } from "../ecs/System";
6
+
7
+ const GRAVITY = -9.81;
8
+
9
+ export class PhysicsSystem extends System {
10
+ update(dt: number) {
11
+ const bodies = this.world.query(Transform, RigidBody);
12
+
13
+ for (const entity of bodies) {
14
+ const rb = this.world.getComponent(entity, RigidBody)!;
15
+ const transform = this.world.getComponent(entity, Transform)!;
16
+
17
+ if (rb.isKinematic) continue;
18
+
19
+ rb.acceleration[1] = GRAVITY * rb.gravityScale;
20
+
21
+ rb.velocity[0] += rb.acceleration[0] * dt;
22
+ rb.velocity[1] += rb.acceleration[1] * dt;
23
+ rb.velocity[2] += rb.acceleration[2] * dt;
24
+
25
+ transform.localPosition[0] += rb.velocity[0] * dt;
26
+ transform.localPosition[1] += rb.velocity[1] * dt;
27
+ transform.localPosition[2] += rb.velocity[2] * dt;
28
+
29
+ transform.updateMatrix();
30
+ }
31
+
32
+ const collidersEntities = this.world.query(Transform, Collider);
33
+
34
+ // Track previous collisions and clear current state
35
+ const previousCollisionsMap = new Map<number, number[]>();
36
+ for (const entity of collidersEntities) {
37
+ const collider = this.world.getComponent(entity, Collider)!;
38
+ previousCollisionsMap.set(entity, [...collider.collisions]);
39
+ collider.collisions.length = 0;
40
+ collider.enterCollisions.length = 0;
41
+ collider.exitCollisions.length = 0;
42
+ }
43
+ for (let i = 0; i < collidersEntities.length; i++) {
44
+ const e1 = collidersEntities[i];
45
+ const t1 = this.world.getComponent(e1, Transform)!;
46
+ const c1 = this.world.getComponent(e1, Collider)!;
47
+ const r1 = this.world.getComponent(e1, RigidBody);
48
+
49
+ for (let j = i + 1; j < collidersEntities.length; j++) {
50
+ const e2 = collidersEntities[j];
51
+ const t2 = this.world.getComponent(e2, Transform)!;
52
+ const c2 = this.world.getComponent(e2, Collider)!;
53
+ const r2 = this.world.getComponent(e2, RigidBody);
54
+
55
+ if (c1.type === "AABB" && c2.type === "AABB") {
56
+ // Skip static-to-static overlap checks. Overlaps are only relevant if at least one is dynamic.
57
+ if (!r1 && !r2) continue;
58
+
59
+ if (this.checkAABBOverlap(t1, c1, t2, c2)) {
60
+ c1.collisions.push(e2);
61
+ c2.collisions.push(e1);
62
+
63
+ // Only resolve if NEITHER is a trigger and at least one is dynamic
64
+ if (!c1.isTrigger && !c2.isTrigger) {
65
+ if ((!r1 || r1.isKinematic) && (!r2 || r2.isKinematic)) continue;
66
+ this.resolveAABBOverlap(t1, c1, r1, t2, c2, r2);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ // Compute enter and exit collisions
74
+ for (const entity of collidersEntities) {
75
+ const collider = this.world.getComponent(entity, Collider)!;
76
+ const prev = previousCollisionsMap.get(entity) || [];
77
+
78
+ for (const colEntity of collider.collisions) {
79
+ if (!prev.includes(colEntity)) {
80
+ collider.enterCollisions.push(colEntity);
81
+ }
82
+ }
83
+
84
+ for (const oldColEntity of prev) {
85
+ if (!collider.collisions.includes(oldColEntity)) {
86
+ collider.exitCollisions.push(oldColEntity);
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ private checkAABBOverlap(
93
+ t1: Transform,
94
+ c1: Collider,
95
+ t2: Transform,
96
+ c2: Collider,
97
+ ): boolean {
98
+ const p1 = vec3.create();
99
+ vec3.add(p1, t1.position, c1.centerOffset);
100
+
101
+ const p2 = vec3.create();
102
+ vec3.add(p2, t2.position, c2.centerOffset);
103
+
104
+ const h1x = (c1.size[0] * Math.abs(t1.scale[0])) / 2;
105
+ const h1y = (c1.size[1] * Math.abs(t1.scale[1])) / 2;
106
+ const h1z = (c1.size[2] * Math.abs(t1.scale[2])) / 2;
107
+
108
+ const h2x = (c2.size[0] * Math.abs(t2.scale[0])) / 2;
109
+ const h2y = (c2.size[1] * Math.abs(t2.scale[1])) / 2;
110
+ const h2z = (c2.size[2] * Math.abs(t2.scale[2])) / 2;
111
+
112
+ const dx = p1[0] - p2[0];
113
+ const dy = p1[1] - p2[1];
114
+ const dz = p1[2] - p2[2];
115
+
116
+ const ox = h1x + h2x - Math.abs(dx);
117
+ const oy = h1y + h2y - Math.abs(dy);
118
+ const oz = h1z + h2z - Math.abs(dz);
119
+
120
+ return ox > 0 && oy > 0 && oz > 0;
121
+ }
122
+
123
+ private resolveAABBOverlap(
124
+ t1: Transform,
125
+ c1: Collider,
126
+ r1: RigidBody | undefined,
127
+ t2: Transform,
128
+ c2: Collider,
129
+ r2: RigidBody | undefined,
130
+ ) {
131
+ const p1 = vec3.create();
132
+ vec3.add(p1, t1.position, c1.centerOffset);
133
+
134
+ const p2 = vec3.create();
135
+ vec3.add(p2, t2.position, c2.centerOffset);
136
+
137
+ const h1x = (c1.size[0] * Math.abs(t1.scale[0])) / 2;
138
+ const h1y = (c1.size[1] * Math.abs(t1.scale[1])) / 2;
139
+ const h1z = (c1.size[2] * Math.abs(t1.scale[2])) / 2;
140
+
141
+ const h2x = (c2.size[0] * Math.abs(t2.scale[0])) / 2;
142
+ const h2y = (c2.size[1] * Math.abs(t2.scale[1])) / 2;
143
+ const h2z = (c2.size[2] * Math.abs(t2.scale[2])) / 2;
144
+
145
+ const dx = p1[0] - p2[0];
146
+ const dy = p1[1] - p2[1];
147
+ const dz = p1[2] - p2[2];
148
+
149
+ const ox = h1x + h2x - Math.abs(dx);
150
+ const oy = h1y + h2y - Math.abs(dy);
151
+ const oz = h1z + h2z - Math.abs(dz);
152
+
153
+ if (ox > 0 && oy > 0 && oz > 0) {
154
+ let pushX = 0,
155
+ pushY = 0,
156
+ pushZ = 0;
157
+ if (ox < oy && ox < oz) {
158
+ pushX = dx > 0 ? ox : -ox;
159
+ } else if (oy < ox && oy < oz) {
160
+ pushY = dy > 0 ? oy : -oy;
161
+ } else {
162
+ pushZ = dz > 0 ? oz : -oz;
163
+ }
164
+
165
+ const m1 = r1 && !r1.isKinematic ? r1.mass : Number.POSITIVE_INFINITY;
166
+ const m2 = r2 && !r2.isKinematic ? r2.mass : Number.POSITIVE_INFINITY;
167
+
168
+ const totalMass =
169
+ (m1 === Number.POSITIVE_INFINITY ? 0 : m1) +
170
+ (m2 === Number.POSITIVE_INFINITY ? 0 : m2);
171
+ if (totalMass === 0) return;
172
+
173
+ const ratio1 =
174
+ m1 === Number.POSITIVE_INFINITY
175
+ ? 0
176
+ : m2 === Number.POSITIVE_INFINITY
177
+ ? 1
178
+ : m2 / totalMass;
179
+ const ratio2 =
180
+ m2 === Number.POSITIVE_INFINITY
181
+ ? 0
182
+ : m1 === Number.POSITIVE_INFINITY
183
+ ? 1
184
+ : m1 / totalMass;
185
+
186
+ if (r1 && !r1.isKinematic) {
187
+ t1.localPosition[0] += pushX * ratio1;
188
+ t1.localPosition[1] += pushY * ratio1;
189
+ t1.localPosition[2] += pushZ * ratio1;
190
+
191
+ if (pushX !== 0) r1.velocity[0] = 0;
192
+ if (pushY !== 0) r1.velocity[1] = 0;
193
+ if (pushZ !== 0) r1.velocity[2] = 0;
194
+
195
+ t1.updateMatrix();
196
+ }
197
+
198
+ if (r2 && !r2.isKinematic) {
199
+ t2.localPosition[0] -= pushX * ratio2;
200
+ t2.localPosition[1] -= pushY * ratio2;
201
+ t2.localPosition[2] -= pushZ * ratio2;
202
+
203
+ if (pushX !== 0) r2.velocity[0] = 0;
204
+ if (pushY !== 0) r2.velocity[1] = 0;
205
+ if (pushZ !== 0) r2.velocity[2] = 0;
206
+
207
+ t2.updateMatrix();
208
+ }
209
+ }
210
+ }
211
+ }
@@ -0,0 +1,165 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+ import { Collider } from "../components/Collider";
3
+ import { Light } from "../components/Light";
4
+ import { MeshRenderer } from "../components/MeshRenderer";
5
+ import { ParticleEmitter } from "../components/ParticleEmitter";
6
+ import { RigidBody } from "../components/RigidBody";
7
+ import { Transform } from "../components/Transform";
8
+ import { World } from "../ecs/World";
9
+ import { LightingSystem } from "./LightingSystem";
10
+ import { ParticleSystem } from "./ParticleSystem";
11
+ import { PhysicsSystem } from "./PhysicsSystem";
12
+
13
+ describe("ECS Systems", () => {
14
+ let world: World;
15
+
16
+ beforeEach(() => {
17
+ world = new World();
18
+ });
19
+
20
+ test("PhysicsSystem applies gravity to RigidBodies", () => {
21
+ const physics = new PhysicsSystem(world);
22
+ const e1 = world.createEntity();
23
+ world.addComponent(e1, Transform);
24
+ const rb = world.addComponent(e1, RigidBody);
25
+
26
+ physics.update(1.0); // 1 second
27
+ expect(rb.velocity[1]).toBeCloseTo(-9.81);
28
+ });
29
+
30
+ test("PhysicsSystem AABB collision resolution", () => {
31
+ const physics = new PhysicsSystem(world);
32
+
33
+ const e1 = world.createEntity();
34
+ const t1 = world.addComponent(e1, Transform);
35
+ const rb1 = world.addComponent(e1, RigidBody);
36
+ const c1 = world.addComponent(e1, Collider); // 1x1 default
37
+ t1.localPosition[0] = 0;
38
+
39
+ const e2 = world.createEntity();
40
+ const t2 = world.addComponent(e2, Transform);
41
+ const _rb2 = world.addComponent(e2, RigidBody);
42
+ const c2 = world.addComponent(e2, Collider); // 1x1 default
43
+ t2.localPosition[0] = 0.5; // Overlapping by 0.5
44
+
45
+ t1.updateMatrix();
46
+ t2.updateMatrix();
47
+
48
+ physics.update(0); // dt 0, just test resolution separation
49
+
50
+ // They should push each other apart by 0.25 (since equal mass)
51
+ expect(t1.localPosition[0]).toBeCloseTo(-0.25);
52
+ expect(t2.localPosition[0]).toBeCloseTo(0.75);
53
+
54
+ // Test Trigger logic
55
+ c1.isTrigger = true;
56
+ t1.localPosition[0] = 0;
57
+ t2.localPosition[0] = 0.5;
58
+ t1.updateMatrix();
59
+ t2.updateMatrix();
60
+ physics.update(0);
61
+ expect(t1.localPosition[0]).toBe(0); // Ignored since isTrigger
62
+ c1.isTrigger = false;
63
+
64
+ // Test Kinematic logic
65
+ rb1.isKinematic = true;
66
+ t1.localPosition[0] = 0;
67
+ t2.localPosition[0] = 0.5;
68
+ t1.updateMatrix();
69
+ t2.updateMatrix();
70
+ physics.update(0);
71
+ // Kinematic doesn't move, e2 takes full push (0.5)
72
+ expect(t1.localPosition[0]).toBe(0);
73
+ expect(t2.localPosition[0]).toBeCloseTo(1);
74
+
75
+ // Reset for Y test
76
+ rb1.isKinematic = false;
77
+ c1.size[0] = 100;
78
+ c2.size[0] = 100; // X overlap huge
79
+ c1.size[2] = 100;
80
+ c2.size[2] = 100; // Z overlap huge
81
+ c1.size[1] = 1;
82
+ c2.size[1] = 1; // Y overlap small
83
+ t1.localPosition[0] = 0;
84
+ t1.localPosition[1] = 0;
85
+ t1.localPosition[2] = 0;
86
+ t2.localPosition[0] = 0;
87
+ t2.localPosition[1] = 0.5;
88
+ t2.localPosition[2] = 0;
89
+ t1.updateMatrix();
90
+ t2.updateMatrix();
91
+ physics.update(0);
92
+ expect(t2.localPosition[1]).toBeCloseTo(0.75); // Resolves along Y axis
93
+
94
+ // Reset for Z test
95
+ c1.size[1] = 100;
96
+ c2.size[1] = 100; // Y overlap huge
97
+ c1.size[2] = 1;
98
+ c2.size[2] = 1; // Z overlap small
99
+ t1.localPosition[0] = 0;
100
+ t1.localPosition[1] = 0;
101
+ t1.localPosition[2] = 0;
102
+ t2.localPosition[0] = 0;
103
+ t2.localPosition[1] = 0;
104
+ t2.localPosition[2] = -0.5; // push negative
105
+ t1.updateMatrix();
106
+ t2.updateMatrix();
107
+ physics.update(0);
108
+ expect(t2.localPosition[2]).toBeCloseTo(-0.75); // Resolves along Z axis
109
+ });
110
+
111
+ test("LightingSystem correctly aggregates global lighting buffers", () => {
112
+ const lightSys = new LightingSystem(world);
113
+
114
+ const e1 = world.createEntity();
115
+ const t1 = world.addComponent(e1, Transform);
116
+ t1.localPosition[0] = 10;
117
+ t1.localPosition[1] = 20;
118
+ t1.localPosition[2] = 30;
119
+ world.addComponent(e1, Light, "Point");
120
+
121
+ const e2 = world.createEntity();
122
+ world.addComponent(e2, Transform);
123
+ const ambient = world.addComponent(e2, Light, "Ambient");
124
+ ambient.intensity = 0.5;
125
+
126
+ lightSys.update(0.1);
127
+
128
+ expect(LightingSystem.activePointLights).toBe(1);
129
+
130
+ // Point Light transform translation should exist in uniform array
131
+ // (Assuming mat4 defaults to identity and translate modifies [12,13,14])
132
+ // In local Transform initialization, it builds matrix from pos.
133
+ expect(LightingSystem.lightPositions[0]).toBe(10);
134
+ expect(LightingSystem.lightPositions[1]).toBe(20);
135
+ expect(LightingSystem.lightPositions[2]).toBe(30);
136
+
137
+ // Ambient should sum up
138
+ expect(LightingSystem.ambientColor[0]).toBe(0.5); // white default
139
+ });
140
+
141
+ test("ParticleSystem spawning logic", () => {
142
+ const ps = new ParticleSystem(world);
143
+ const e1 = world.createEntity();
144
+ world.addComponent(e1, Transform);
145
+ const emitter = world.addComponent(e1, ParticleEmitter, 100);
146
+ emitter.emissionRate = 10;
147
+
148
+ ps.update(0.5); // 0.5s @ 10/s = 5 particles
149
+
150
+ expect(emitter.activeCount).toBe(5);
151
+ expect(emitter.pLife[0]).toBe(0.5);
152
+
153
+ // The mesh renderer should be automatically added
154
+ const mesh = world.getComponent(e1, MeshRenderer);
155
+ expect(mesh).toBeDefined();
156
+ // 5 active particles * 6 indices each = 30 indices to draw
157
+ expect(mesh?.indexCount).toBe(30);
158
+ expect(mesh?.isDirty).toBe(true);
159
+
160
+ // Force particle death by expiring life natively
161
+ emitter.pLife[0] = -1;
162
+ ps.update(0.1); // Advances sim and kills the trailing particle
163
+ expect(emitter.activeCount).toBe(5); // 5 existed, 1 died, wait, 0.1s spawned 1 more. So 5 - 1 + 1 = 5
164
+ });
165
+ });
@@ -0,0 +1,153 @@
1
+ import { BitmapText } from "../components/BitmapText";
2
+ import { MeshRenderer } from "../components/MeshRenderer";
3
+ import { Transform } from "../components/Transform";
4
+ import { GlobalAssets } from "../core/AssetManager";
5
+ import { System } from "../ecs/System";
6
+ import { Material } from "../renderer/Material";
7
+
8
+ export const TEXT_VS = `
9
+ attribute vec4 aVertexPosition;
10
+ attribute vec2 aTextureCoord;
11
+ attribute vec4 aVertexColor;
12
+ uniform mat4 uModelViewMatrix;
13
+ uniform mat4 uProjectionMatrix;
14
+ varying highp vec2 vTextureCoord;
15
+ varying lowp vec4 vColor;
16
+ void main(void) {
17
+ gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
18
+ vTextureCoord = aTextureCoord;
19
+ vColor = aVertexColor;
20
+ }
21
+ `;
22
+
23
+ export const TEXT_FS = `
24
+ varying highp vec2 vTextureCoord;
25
+ varying lowp vec4 vColor;
26
+ uniform sampler2D uSampler;
27
+ void main(void) {
28
+ gl_FragColor = texture2D(uSampler, vTextureCoord) * vColor;
29
+ }
30
+ `;
31
+
32
+ export class TextSystem extends System {
33
+ update(_dt: number) {
34
+ const texts = this.world.query(Transform, BitmapText);
35
+
36
+ for (const entity of texts) {
37
+ const bt = this.world.getComponent(entity, BitmapText)!;
38
+ if (!bt.isDirty) continue;
39
+
40
+ const font = GlobalAssets.getFont(bt.fontId);
41
+ if (!font?.textureAsset.loaded) continue;
42
+
43
+ let mesh = this.world.getComponent(entity, MeshRenderer);
44
+
45
+ const len = bt.text.length;
46
+ const positions = new Float32Array(len * 4 * 3);
47
+ const colors = new Float32Array(len * 4 * 4);
48
+ const uvs = new Float32Array(len * 4 * 2);
49
+ const indices = new Uint16Array(len * 6);
50
+
51
+ let cursorX = 0;
52
+ let cursorY = 0;
53
+
54
+ const cr = bt.color.rNorm;
55
+ const cg = bt.color.gNorm;
56
+ const cb = bt.color.bNorm;
57
+ const ca = bt.color.aNorm;
58
+
59
+ for (let i = 0; i < len; i++) {
60
+ const charCode = bt.text.charCodeAt(i);
61
+ if (charCode === 10) {
62
+ cursorY += font.lineHeight;
63
+ cursorX = 0;
64
+ continue;
65
+ }
66
+
67
+ const charData = font.chars.get(charCode);
68
+ if (!charData) continue;
69
+
70
+ const x = cursorX + charData.xoffset;
71
+ const y = cursorY + charData.yoffset;
72
+ const w = charData.width;
73
+ const h = charData.height;
74
+
75
+ const baseV = i * 12;
76
+ positions[baseV] = x;
77
+ positions[baseV + 1] = y;
78
+ positions[baseV + 2] = 0;
79
+ positions[baseV + 3] = x + w;
80
+ positions[baseV + 4] = y;
81
+ positions[baseV + 5] = 0;
82
+ positions[baseV + 6] = x + w;
83
+ positions[baseV + 7] = y + h;
84
+ positions[baseV + 8] = 0;
85
+ positions[baseV + 9] = x;
86
+ positions[baseV + 10] = y + h;
87
+ positions[baseV + 11] = 0;
88
+
89
+ const tw = font.textureWidth;
90
+ const th = font.textureHeight;
91
+ const u0 = charData.x / tw;
92
+ const v0 = charData.y / th;
93
+ const u1 = (charData.x + w) / tw;
94
+ const v1 = (charData.y + h) / th;
95
+
96
+ const baseU = i * 8;
97
+ uvs[baseU] = u0;
98
+ uvs[baseU + 1] = v0;
99
+ uvs[baseU + 2] = u1;
100
+ uvs[baseU + 3] = v0;
101
+ uvs[baseU + 4] = u1;
102
+ uvs[baseU + 5] = v1;
103
+ uvs[baseU + 6] = u0;
104
+ uvs[baseU + 7] = v1;
105
+
106
+ const baseC = i * 16;
107
+ for (let v = 0; v < 4; v++) {
108
+ colors[baseC + v * 4] = cr;
109
+ colors[baseC + v * 4 + 1] = cg;
110
+ colors[baseC + v * 4 + 2] = cb;
111
+ colors[baseC + v * 4 + 3] = ca;
112
+ }
113
+
114
+ const baseI = i * 6;
115
+ const ix = i * 4;
116
+ indices[baseI] = ix;
117
+ indices[baseI + 1] = ix + 1;
118
+ indices[baseI + 2] = ix + 2;
119
+ indices[baseI + 3] = ix;
120
+ indices[baseI + 4] = ix + 2;
121
+ indices[baseI + 5] = ix + 3;
122
+
123
+ cursorX += charData.xadvance;
124
+ }
125
+
126
+ if (!mesh) {
127
+ const material = new Material(TEXT_VS, TEXT_FS);
128
+ material.transparent = true;
129
+ material.setUniform("uSampler", font.textureAsset);
130
+
131
+ mesh = this.world.addComponent(
132
+ entity,
133
+ MeshRenderer,
134
+ Array.from(positions),
135
+ Array.from(colors),
136
+ Array.from(uvs),
137
+ Array.from(indices),
138
+ material,
139
+ );
140
+ } else {
141
+ mesh.positions = positions;
142
+ mesh.colors = colors;
143
+ mesh.uvs = uvs;
144
+ mesh.indices = indices;
145
+ mesh.vertexCount = positions.length / 3;
146
+ mesh.indexCount = indices.length;
147
+ mesh.isDirty = true;
148
+ }
149
+
150
+ bt.isDirty = false;
151
+ }
152
+ }
153
+ }