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.
- package/README.md +15 -0
- package/biome.json +51 -0
- package/bun.lock +192 -0
- package/index.ts +1 -0
- package/package.json +25 -0
- package/serve.ts +125 -0
- package/src/audio/AudioEngine.ts +61 -0
- package/src/components/Animator3D.ts +65 -0
- package/src/components/AudioSource.ts +26 -0
- package/src/components/BitmapText.ts +25 -0
- package/src/components/Camera.ts +33 -0
- package/src/components/CameraFollow.ts +5 -0
- package/src/components/Collider.ts +16 -0
- package/src/components/Components.test.ts +68 -0
- package/src/components/Light.ts +15 -0
- package/src/components/MeshRenderer.ts +58 -0
- package/src/components/ParticleEmitter.ts +59 -0
- package/src/components/RigidBody.ts +9 -0
- package/src/components/ShadowCaster.ts +3 -0
- package/src/components/SkinnedMeshRenderer.ts +25 -0
- package/src/components/SpriteAnimator.ts +42 -0
- package/src/components/SpriteRenderer.ts +26 -0
- package/src/components/Transform.test.ts +39 -0
- package/src/components/Transform.ts +54 -0
- package/src/core/AssetManager.ts +123 -0
- package/src/core/Input.test.ts +67 -0
- package/src/core/Input.ts +94 -0
- package/src/core/Scene.ts +24 -0
- package/src/core/SceneManager.ts +57 -0
- package/src/core/Storage.ts +161 -0
- package/src/desktop/SteamClient.ts +52 -0
- package/src/ecs/System.ts +11 -0
- package/src/ecs/World.test.ts +29 -0
- package/src/ecs/World.ts +149 -0
- package/src/index.ts +115 -0
- package/src/math/Color.ts +100 -0
- package/src/math/Vector2.ts +96 -0
- package/src/math/Vector3.ts +103 -0
- package/src/math/math.test.ts +168 -0
- package/src/renderer/GlowMaterial.ts +66 -0
- package/src/renderer/LitMaterial.ts +337 -0
- package/src/renderer/Material.test.ts +23 -0
- package/src/renderer/Material.ts +80 -0
- package/src/renderer/OcclusionMaterial.ts +43 -0
- package/src/renderer/ParticleMaterial.ts +66 -0
- package/src/renderer/Shader.ts +44 -0
- package/src/renderer/SkinnedLitMaterial.ts +55 -0
- package/src/renderer/WaterMaterial.ts +298 -0
- package/src/renderer/WebGLRenderer.ts +917 -0
- package/src/systems/Animation3DSystem.ts +148 -0
- package/src/systems/AnimationSystem.ts +58 -0
- package/src/systems/AudioSystem.ts +62 -0
- package/src/systems/LightingSystem.ts +114 -0
- package/src/systems/ParticleSystem.ts +278 -0
- package/src/systems/PhysicsSystem.ts +211 -0
- package/src/systems/Systems.test.ts +165 -0
- package/src/systems/TextSystem.ts +153 -0
- package/src/ui/AnimationEditor.tsx +639 -0
- package/src/ui/BottomPanel.tsx +443 -0
- package/src/ui/EntityExplorer.tsx +420 -0
- package/src/ui/GameState.ts +286 -0
- package/src/ui/Icons.tsx +239 -0
- package/src/ui/InventoryPanel.tsx +335 -0
- package/src/ui/PlayerHUD.tsx +250 -0
- package/src/ui/SpriteEditor.tsx +3241 -0
- package/src/ui/SpriteSheetManager.tsx +198 -0
- package/src/utils/GLTFLoader.ts +257 -0
- package/src/utils/ObjLoader.ts +81 -0
- package/src/utils/idb.ts +137 -0
- package/src/utils/packer.ts +85 -0
- package/test_obj.ts +12 -0
- package/tsconfig.json +21 -0
- 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
|
+
}
|