nova64 0.2.6 → 0.2.7
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/dist/examples/strider-demo-3d/fix-game.sh +0 -0
- package/dist/runtime/api-2d.js +1158 -0
- package/dist/runtime/api-3d/camera.js +73 -0
- package/dist/runtime/api-3d/instancing.js +180 -0
- package/dist/runtime/api-3d/lights.js +51 -0
- package/dist/runtime/api-3d/materials.js +47 -0
- package/dist/runtime/api-3d/models.js +84 -0
- package/dist/runtime/api-3d/particles.js +296 -0
- package/dist/runtime/api-3d/pbr.js +113 -0
- package/dist/runtime/api-3d/primitives.js +304 -0
- package/dist/runtime/api-3d/scene.js +169 -0
- package/dist/runtime/api-3d/transforms.js +161 -0
- package/dist/runtime/api-3d.js +166 -0
- package/dist/runtime/api-effects.js +840 -0
- package/dist/runtime/api-gameutils.js +476 -0
- package/dist/runtime/api-generative.js +610 -0
- package/dist/runtime/api-presets.js +85 -0
- package/dist/runtime/api-skybox.js +232 -0
- package/dist/runtime/api-sprites.js +100 -0
- package/dist/runtime/api-voxel.js +712 -0
- package/dist/runtime/api.js +201 -0
- package/dist/runtime/assets.js +27 -0
- package/dist/runtime/audio.js +114 -0
- package/dist/runtime/collision.js +47 -0
- package/dist/runtime/console.js +101 -0
- package/dist/runtime/editor.js +233 -0
- package/dist/runtime/font.js +233 -0
- package/dist/runtime/framebuffer.js +28 -0
- package/dist/runtime/fullscreen-button.js +185 -0
- package/dist/runtime/gpu-canvas2d.js +47 -0
- package/dist/runtime/gpu-threejs.js +643 -0
- package/dist/runtime/gpu-webgl2.js +310 -0
- package/dist/runtime/index.d.ts +682 -0
- package/dist/runtime/index.js +22 -0
- package/dist/runtime/input.js +225 -0
- package/dist/runtime/logger.js +60 -0
- package/dist/runtime/physics.js +101 -0
- package/dist/runtime/screens.js +213 -0
- package/dist/runtime/storage.js +38 -0
- package/dist/runtime/store.js +151 -0
- package/dist/runtime/textinput.js +68 -0
- package/dist/runtime/ui/buttons.js +124 -0
- package/dist/runtime/ui/panels.js +105 -0
- package/dist/runtime/ui/text.js +86 -0
- package/dist/runtime/ui/widgets.js +141 -0
- package/dist/runtime/ui.js +111 -0
- package/package.json +34 -32
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// runtime/api-3d/particles.js
|
|
2
|
+
// GPU particle system: typed-array physics simulation, single InstancedMesh draw call
|
|
3
|
+
//
|
|
4
|
+
// API:
|
|
5
|
+
// createParticleSystem(maxParticles, options) → systemId
|
|
6
|
+
// setParticleEmitter(systemId, emitter) configure emission
|
|
7
|
+
// emitParticle(systemId, overrides?) fire one particle
|
|
8
|
+
// burstParticles(systemId, count, overrides?) fire N particles at once
|
|
9
|
+
// updateParticles(dt) step all systems (call each frame)
|
|
10
|
+
// removeParticleSystem(systemId) cleanup
|
|
11
|
+
|
|
12
|
+
import * as THREE from 'three';
|
|
13
|
+
|
|
14
|
+
export function particlesModule({ scene, counters }) {
|
|
15
|
+
// Per-particle typed arrays layout (indices into Float32Array pools):
|
|
16
|
+
// px py pz — position
|
|
17
|
+
// vx vy vz — velocity
|
|
18
|
+
// age life — lifetime tracking
|
|
19
|
+
// r g b — color (0-1)
|
|
20
|
+
// size — scale
|
|
21
|
+
// active — 1 = alive, 0 = dead
|
|
22
|
+
|
|
23
|
+
const STRIDE = 13; // slots per particle
|
|
24
|
+
|
|
25
|
+
const particleSystems = new Map();
|
|
26
|
+
let psCounter = 0;
|
|
27
|
+
|
|
28
|
+
const _dummy = new THREE.Object3D();
|
|
29
|
+
const _color = new THREE.Color();
|
|
30
|
+
|
|
31
|
+
function createParticleSystem(maxParticles = 200, options = {}) {
|
|
32
|
+
const {
|
|
33
|
+
shape = 'sphere',
|
|
34
|
+
size = 0.15,
|
|
35
|
+
segments = 4,
|
|
36
|
+
color = 0xffaa00,
|
|
37
|
+
emissive = 0xff6600,
|
|
38
|
+
emissiveIntensity = 2.0,
|
|
39
|
+
gravity = -9.8,
|
|
40
|
+
drag = 0.95,
|
|
41
|
+
// Emitter defaults
|
|
42
|
+
emitterX = 0,
|
|
43
|
+
emitterY = 0,
|
|
44
|
+
emitterZ = 0,
|
|
45
|
+
emitRate = 20, // particles per second
|
|
46
|
+
minLife = 0.8,
|
|
47
|
+
maxLife = 2.0,
|
|
48
|
+
minSpeed = 2,
|
|
49
|
+
maxSpeed = 8,
|
|
50
|
+
spread = Math.PI, // half-angle cone spread (PI = hemisphere)
|
|
51
|
+
minSize = 0.05,
|
|
52
|
+
maxSize = 0.3,
|
|
53
|
+
startColor = color,
|
|
54
|
+
endColor = 0x000000,
|
|
55
|
+
} = options;
|
|
56
|
+
|
|
57
|
+
// Geometry
|
|
58
|
+
let geometry;
|
|
59
|
+
if (shape === 'cube') {
|
|
60
|
+
geometry = new THREE.BoxGeometry(size, size, size);
|
|
61
|
+
} else {
|
|
62
|
+
geometry = new THREE.SphereGeometry(size, segments, segments);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const material = new THREE.MeshBasicMaterial({ color, vertexColors: false });
|
|
66
|
+
material.emissive = new THREE.Color(emissive);
|
|
67
|
+
// MeshBasicMaterial doesn't have emissive — use MeshStandardMaterial instead
|
|
68
|
+
const stdMat = new THREE.MeshStandardMaterial({
|
|
69
|
+
color,
|
|
70
|
+
emissive: new THREE.Color(emissive),
|
|
71
|
+
emissiveIntensity,
|
|
72
|
+
roughness: 0.8,
|
|
73
|
+
metalness: 0.0,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const mesh = new THREE.InstancedMesh(geometry, stdMat, maxParticles);
|
|
77
|
+
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
78
|
+
mesh.castShadow = false;
|
|
79
|
+
mesh.receiveShadow = false;
|
|
80
|
+
|
|
81
|
+
// Hide all instances initially by scaling to zero
|
|
82
|
+
_dummy.position.set(0, -9999, 0);
|
|
83
|
+
_dummy.scale.set(0, 0, 0);
|
|
84
|
+
_dummy.updateMatrix();
|
|
85
|
+
for (let i = 0; i < maxParticles; i++) {
|
|
86
|
+
mesh.setMatrixAt(i, _dummy.matrix);
|
|
87
|
+
}
|
|
88
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
89
|
+
|
|
90
|
+
// Enable per-instance color
|
|
91
|
+
mesh.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(maxParticles * 3), 3);
|
|
92
|
+
|
|
93
|
+
scene.add(mesh);
|
|
94
|
+
|
|
95
|
+
// Typed-array particle state pool
|
|
96
|
+
const pool = new Float32Array(maxParticles * STRIDE);
|
|
97
|
+
// All start inactive (age = life = 0, active slot = 0)
|
|
98
|
+
|
|
99
|
+
const id = ++psCounter;
|
|
100
|
+
particleSystems.set(id, {
|
|
101
|
+
mesh,
|
|
102
|
+
pool,
|
|
103
|
+
maxParticles,
|
|
104
|
+
freeList: Array.from({ length: maxParticles }, (_, i) => i),
|
|
105
|
+
activeCount: 0,
|
|
106
|
+
emitAccum: 0,
|
|
107
|
+
emitter: {
|
|
108
|
+
x: emitterX,
|
|
109
|
+
y: emitterY,
|
|
110
|
+
z: emitterZ,
|
|
111
|
+
emitRate,
|
|
112
|
+
minLife,
|
|
113
|
+
maxLife,
|
|
114
|
+
minSpeed,
|
|
115
|
+
maxSpeed,
|
|
116
|
+
spread,
|
|
117
|
+
minSize,
|
|
118
|
+
maxSize,
|
|
119
|
+
},
|
|
120
|
+
config: {
|
|
121
|
+
gravity,
|
|
122
|
+
drag,
|
|
123
|
+
startColor: new THREE.Color(startColor),
|
|
124
|
+
endColor: new THREE.Color(endColor),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
counters.particle = (counters.particle || 0) + 1;
|
|
129
|
+
return id;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setParticleEmitter(systemId, emitter = {}) {
|
|
133
|
+
const sys = particleSystems.get(systemId);
|
|
134
|
+
if (!sys) return;
|
|
135
|
+
Object.assign(sys.emitter, emitter);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function _spawnParticle(sys, overrides = {}) {
|
|
139
|
+
if (sys.freeList.length === 0) return; // pool full
|
|
140
|
+
const idx = sys.freeList.pop();
|
|
141
|
+
const base = idx * STRIDE;
|
|
142
|
+
const { pool, emitter } = sys;
|
|
143
|
+
|
|
144
|
+
const ex = overrides.x ?? emitter.x;
|
|
145
|
+
const ey = overrides.y ?? emitter.y;
|
|
146
|
+
const ez = overrides.z ?? emitter.z;
|
|
147
|
+
|
|
148
|
+
// Random direction within spread cone (pointing up by default)
|
|
149
|
+
const phi = Math.random() * Math.PI * 2;
|
|
150
|
+
const theta = Math.random() * (overrides.spread ?? emitter.spread);
|
|
151
|
+
const ct = Math.cos(theta);
|
|
152
|
+
const st = Math.sin(theta);
|
|
153
|
+
const speed = emitter.minSpeed + Math.random() * (emitter.maxSpeed - emitter.minSpeed);
|
|
154
|
+
const vx = overrides.vx ?? st * Math.cos(phi) * speed;
|
|
155
|
+
const vy = overrides.vy ?? ct * speed;
|
|
156
|
+
const vz = overrides.vz ?? st * Math.sin(phi) * speed;
|
|
157
|
+
|
|
158
|
+
const life = emitter.minLife + Math.random() * (emitter.maxLife - emitter.minLife);
|
|
159
|
+
const sz = emitter.minSize + Math.random() * (emitter.maxSize - emitter.minSize);
|
|
160
|
+
|
|
161
|
+
// position
|
|
162
|
+
pool[base + 0] = ex;
|
|
163
|
+
pool[base + 1] = ey;
|
|
164
|
+
pool[base + 2] = ez;
|
|
165
|
+
// velocity
|
|
166
|
+
pool[base + 3] = vx;
|
|
167
|
+
pool[base + 4] = vy;
|
|
168
|
+
pool[base + 5] = vz;
|
|
169
|
+
// age, life
|
|
170
|
+
pool[base + 6] = 0;
|
|
171
|
+
pool[base + 7] = life > 0 ? life : 1;
|
|
172
|
+
// color (start)
|
|
173
|
+
const sc = sys.config.startColor;
|
|
174
|
+
pool[base + 8] = overrides.r ?? sc.r;
|
|
175
|
+
pool[base + 9] = overrides.g ?? sc.g;
|
|
176
|
+
pool[base + 10] = overrides.b ?? sc.b;
|
|
177
|
+
// size
|
|
178
|
+
pool[base + 11] = sz;
|
|
179
|
+
// active
|
|
180
|
+
pool[base + 12] = 1;
|
|
181
|
+
|
|
182
|
+
sys.activeCount++;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function emitParticle(systemId, overrides = {}) {
|
|
186
|
+
const sys = particleSystems.get(systemId);
|
|
187
|
+
if (sys) _spawnParticle(sys, overrides);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function burstParticles(systemId, count = 10, overrides = {}) {
|
|
191
|
+
const sys = particleSystems.get(systemId);
|
|
192
|
+
if (!sys) return;
|
|
193
|
+
for (let i = 0; i < count; i++) _spawnParticle(sys, overrides);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function updateParticles(dt) {
|
|
197
|
+
particleSystems.forEach(sys => {
|
|
198
|
+
const { pool, maxParticles, mesh, config, emitter } = sys;
|
|
199
|
+
|
|
200
|
+
// Auto-emit based on rate
|
|
201
|
+
sys.emitAccum += emitter.emitRate * dt;
|
|
202
|
+
while (sys.emitAccum >= 1 && sys.freeList.length > 0) {
|
|
203
|
+
_spawnParticle(sys);
|
|
204
|
+
sys.emitAccum -= 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let needsUpdate = false;
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < maxParticles; i++) {
|
|
210
|
+
const base = i * STRIDE;
|
|
211
|
+
if (pool[base + 12] === 0) continue; // inactive
|
|
212
|
+
|
|
213
|
+
// Step age
|
|
214
|
+
pool[base + 6] += dt;
|
|
215
|
+
const age = pool[base + 6];
|
|
216
|
+
const life = pool[base + 7];
|
|
217
|
+
|
|
218
|
+
if (age >= life) {
|
|
219
|
+
// Kill particle
|
|
220
|
+
pool[base + 12] = 0;
|
|
221
|
+
sys.freeList.push(i);
|
|
222
|
+
sys.activeCount--;
|
|
223
|
+
|
|
224
|
+
// Scale to zero
|
|
225
|
+
_dummy.position.set(0, -9999, 0);
|
|
226
|
+
_dummy.scale.set(0, 0, 0);
|
|
227
|
+
_dummy.updateMatrix();
|
|
228
|
+
mesh.setMatrixAt(i, _dummy.matrix);
|
|
229
|
+
needsUpdate = true;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Physics integration
|
|
234
|
+
pool[base + 3] *= config.drag; // drag on vx
|
|
235
|
+
pool[base + 5] *= config.drag; // drag on vz
|
|
236
|
+
pool[base + 4] += config.gravity * dt; // gravity on vy
|
|
237
|
+
|
|
238
|
+
pool[base + 0] += pool[base + 3] * dt; // px
|
|
239
|
+
pool[base + 1] += pool[base + 4] * dt; // py
|
|
240
|
+
pool[base + 2] += pool[base + 5] * dt; // pz
|
|
241
|
+
|
|
242
|
+
// Interpolate color start→end
|
|
243
|
+
const t = age / life;
|
|
244
|
+
const r = pool[base + 8] * (1 - t) + config.endColor.r * t;
|
|
245
|
+
const g = pool[base + 9] * (1 - t) + config.endColor.g * t;
|
|
246
|
+
const b = pool[base + 10] * (1 - t) + config.endColor.b * t;
|
|
247
|
+
_color.setRGB(r, g, b);
|
|
248
|
+
mesh.setColorAt(i, _color);
|
|
249
|
+
|
|
250
|
+
// Shrink as particle ages
|
|
251
|
+
const sz = pool[base + 11] * (1 - t * 0.8);
|
|
252
|
+
|
|
253
|
+
_dummy.position.set(pool[base + 0], pool[base + 1], pool[base + 2]);
|
|
254
|
+
_dummy.scale.set(sz, sz, sz);
|
|
255
|
+
_dummy.updateMatrix();
|
|
256
|
+
mesh.setMatrixAt(i, _dummy.matrix);
|
|
257
|
+
needsUpdate = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (needsUpdate) {
|
|
261
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
262
|
+
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function removeParticleSystem(systemId) {
|
|
268
|
+
const sys = particleSystems.get(systemId);
|
|
269
|
+
if (!sys) return false;
|
|
270
|
+
scene.remove(sys.mesh);
|
|
271
|
+
sys.mesh.geometry?.dispose();
|
|
272
|
+
sys.mesh.material?.dispose();
|
|
273
|
+
particleSystems.delete(systemId);
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function getParticleStats(systemId) {
|
|
278
|
+
const sys = particleSystems.get(systemId);
|
|
279
|
+
if (!sys) return null;
|
|
280
|
+
return {
|
|
281
|
+
active: sys.activeCount,
|
|
282
|
+
max: sys.maxParticles,
|
|
283
|
+
free: sys.freeList.length,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
createParticleSystem,
|
|
289
|
+
setParticleEmitter,
|
|
290
|
+
emitParticle,
|
|
291
|
+
burstParticles,
|
|
292
|
+
updateParticles,
|
|
293
|
+
removeParticleSystem,
|
|
294
|
+
getParticleStats,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// runtime/api-3d/pbr.js
|
|
2
|
+
// Normal maps, PBR roughness/metalness/AO maps
|
|
3
|
+
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
|
|
6
|
+
export function pbrModule({ meshes }) {
|
|
7
|
+
const _texLoader = new THREE.TextureLoader();
|
|
8
|
+
|
|
9
|
+
function loadNormalMap(url) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
_texLoader.load(
|
|
12
|
+
url,
|
|
13
|
+
texture => {
|
|
14
|
+
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
|
15
|
+
resolve(texture);
|
|
16
|
+
},
|
|
17
|
+
undefined,
|
|
18
|
+
err => reject(err)
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _upgradeToStandard(mesh) {
|
|
24
|
+
if (!mesh?.material) return;
|
|
25
|
+
if (mesh.material.isMeshStandardMaterial) return;
|
|
26
|
+
const old = mesh.material;
|
|
27
|
+
const standard = new THREE.MeshStandardMaterial({
|
|
28
|
+
color: old.color?.clone() ?? new THREE.Color(0xffffff),
|
|
29
|
+
map: old.map ?? null,
|
|
30
|
+
metalness: 0.0,
|
|
31
|
+
roughness: 0.6,
|
|
32
|
+
transparent: old.transparent ?? false,
|
|
33
|
+
opacity: old.opacity ?? 1.0,
|
|
34
|
+
});
|
|
35
|
+
old.dispose();
|
|
36
|
+
mesh.material = standard;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setNormalMap(meshId, texture) {
|
|
40
|
+
const mesh = meshes.get(meshId);
|
|
41
|
+
if (!mesh) return false;
|
|
42
|
+
_upgradeToStandard(mesh);
|
|
43
|
+
mesh.material.normalMap = texture;
|
|
44
|
+
mesh.material.needsUpdate = true;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setPBRMaps(meshId, maps = {}) {
|
|
49
|
+
const mesh = meshes.get(meshId);
|
|
50
|
+
if (!mesh) return false;
|
|
51
|
+
_upgradeToStandard(mesh);
|
|
52
|
+
const mat = mesh.material;
|
|
53
|
+
if (maps.normalMap) {
|
|
54
|
+
mat.normalMap = maps.normalMap;
|
|
55
|
+
}
|
|
56
|
+
if (maps.roughnessMap) {
|
|
57
|
+
mat.roughnessMap = maps.roughnessMap;
|
|
58
|
+
}
|
|
59
|
+
if (maps.aoMap) {
|
|
60
|
+
mat.aoMap = maps.aoMap;
|
|
61
|
+
}
|
|
62
|
+
if (maps.metalness !== undefined) mat.metalness = maps.metalness;
|
|
63
|
+
if (maps.roughness !== undefined) mat.roughness = maps.roughness;
|
|
64
|
+
mat.needsUpdate = true;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set PBR scalar properties on a mesh without requiring texture maps.
|
|
70
|
+
* Clones the material so the change only affects this specific mesh.
|
|
71
|
+
*
|
|
72
|
+
* @param {number} meshId - ID returned by createSphere / createCube etc.
|
|
73
|
+
* @param {object} [opts]
|
|
74
|
+
* @param {number} [opts.metalness] - 0 (dielectric) → 1 (fully metallic)
|
|
75
|
+
* @param {number} [opts.roughness] - 0 (mirror-smooth) → 1 (fully rough)
|
|
76
|
+
* @param {number} [opts.envMapIntensity] - Strength of environment reflections
|
|
77
|
+
* @param {number} [opts.color] - Override surface colour (hex)
|
|
78
|
+
* @returns {boolean} true on success
|
|
79
|
+
*/
|
|
80
|
+
function setPBRProperties(meshId, opts = {}) {
|
|
81
|
+
const mesh = meshes.get(meshId);
|
|
82
|
+
if (!mesh) return false;
|
|
83
|
+
|
|
84
|
+
// Replace with a fresh MeshStandardMaterial so we own it exclusively
|
|
85
|
+
// (avoids mutating a shared cached material that other meshes may use)
|
|
86
|
+
const old = mesh.material;
|
|
87
|
+
mesh.material = new THREE.MeshStandardMaterial({
|
|
88
|
+
color:
|
|
89
|
+
opts.color !== undefined
|
|
90
|
+
? new THREE.Color(opts.color)
|
|
91
|
+
: (old.color?.clone() ?? new THREE.Color(0xffffff)),
|
|
92
|
+
map: old.map ?? null,
|
|
93
|
+
metalness:
|
|
94
|
+
opts.metalness !== undefined
|
|
95
|
+
? Math.max(0, Math.min(1, opts.metalness))
|
|
96
|
+
: (old.metalness ?? 0.0),
|
|
97
|
+
roughness:
|
|
98
|
+
opts.roughness !== undefined
|
|
99
|
+
? Math.max(0, Math.min(1, opts.roughness))
|
|
100
|
+
: (old.roughness ?? 0.6),
|
|
101
|
+
envMapIntensity: opts.envMapIntensity ?? old.envMapIntensity ?? 1.0,
|
|
102
|
+
emissive: old.emissive?.clone() ?? new THREE.Color(0),
|
|
103
|
+
emissiveIntensity: old.emissiveIntensity ?? 0,
|
|
104
|
+
transparent: old.transparent ?? false,
|
|
105
|
+
opacity: old.opacity ?? 1.0,
|
|
106
|
+
side: old.side ?? THREE.FrontSide,
|
|
107
|
+
});
|
|
108
|
+
mesh.material.needsUpdate = true;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { loadNormalMap, setNormalMap, setPBRMaps, setPBRProperties };
|
|
113
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// runtime/api-3d/primitives.js
|
|
2
|
+
// Mesh creation (createMesh) and all shape primitives
|
|
3
|
+
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
import { logger } from '../logger.js';
|
|
6
|
+
|
|
7
|
+
export function primitivesModule({
|
|
8
|
+
scene,
|
|
9
|
+
gpu,
|
|
10
|
+
meshes,
|
|
11
|
+
mixers,
|
|
12
|
+
modelAnimations,
|
|
13
|
+
counters,
|
|
14
|
+
getCachedMaterial,
|
|
15
|
+
}) {
|
|
16
|
+
function createMesh(geometry, material, position = [0, 0, 0]) {
|
|
17
|
+
if (!geometry || !material) {
|
|
18
|
+
logger.error('createMesh: geometry and material are required');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let mesh;
|
|
23
|
+
if (geometry.type && !geometry.isBufferGeometry) {
|
|
24
|
+
// Mock object path (used in tests)
|
|
25
|
+
mesh = {
|
|
26
|
+
type: 'Mesh',
|
|
27
|
+
geometry,
|
|
28
|
+
material,
|
|
29
|
+
position: {
|
|
30
|
+
x: 0,
|
|
31
|
+
y: 0,
|
|
32
|
+
z: 0,
|
|
33
|
+
set(x, y, z) {
|
|
34
|
+
this.x = x;
|
|
35
|
+
this.y = y;
|
|
36
|
+
this.z = z;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
rotation: {
|
|
40
|
+
x: 0,
|
|
41
|
+
y: 0,
|
|
42
|
+
z: 0,
|
|
43
|
+
set(x, y, z) {
|
|
44
|
+
this.x = x;
|
|
45
|
+
this.y = y;
|
|
46
|
+
this.z = z;
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
scale: {
|
|
50
|
+
x: 1,
|
|
51
|
+
y: 1,
|
|
52
|
+
z: 1,
|
|
53
|
+
set(x, y, z) {
|
|
54
|
+
this.x = x;
|
|
55
|
+
this.y = y;
|
|
56
|
+
this.z = z;
|
|
57
|
+
},
|
|
58
|
+
setScalar(s) {
|
|
59
|
+
this.x = s;
|
|
60
|
+
this.y = s;
|
|
61
|
+
this.z = s;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
castShadow: true,
|
|
65
|
+
receiveShadow: true,
|
|
66
|
+
};
|
|
67
|
+
} else {
|
|
68
|
+
mesh = new THREE.Mesh(geometry, material);
|
|
69
|
+
mesh.castShadow = true;
|
|
70
|
+
mesh.receiveShadow = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(position) && position.length >= 3) {
|
|
74
|
+
mesh.position.set(position[0], position[1], position[2]);
|
|
75
|
+
} else if (typeof position === 'object' && position.x !== undefined) {
|
|
76
|
+
mesh.position.set(position.x, position.y || 0, position.z || 0);
|
|
77
|
+
} else {
|
|
78
|
+
mesh.position.set(0, 0, 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (scene.add) scene.add(mesh);
|
|
82
|
+
if (material?.userData?.animated && gpu.registerAnimatedMesh) {
|
|
83
|
+
gpu.registerAnimatedMesh(mesh);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const id = ++counters.mesh;
|
|
87
|
+
meshes.set(id, mesh);
|
|
88
|
+
return id;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function destroyMesh(id) {
|
|
92
|
+
const mesh = meshes.get(id);
|
|
93
|
+
if (mesh) {
|
|
94
|
+
scene.remove(mesh);
|
|
95
|
+
mesh.geometry?.dispose();
|
|
96
|
+
if (mesh.material?.map) mesh.material.map.dispose();
|
|
97
|
+
mesh.material?.dispose();
|
|
98
|
+
meshes.delete(id);
|
|
99
|
+
mixers.delete(id);
|
|
100
|
+
modelAnimations.delete(id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getMesh(id) {
|
|
105
|
+
return meshes.get(id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createCube(size = 1, color = 0xffffff, position = [0, 0, 0], options = {}) {
|
|
109
|
+
try {
|
|
110
|
+
if (typeof size !== 'number' || size <= 0) {
|
|
111
|
+
logger.warn('createCube: invalid size, using default');
|
|
112
|
+
size = 1;
|
|
113
|
+
}
|
|
114
|
+
if (typeof color !== 'number') {
|
|
115
|
+
logger.warn('createCube: invalid color, using white');
|
|
116
|
+
color = 0xffffff;
|
|
117
|
+
}
|
|
118
|
+
return createMesh(
|
|
119
|
+
gpu.createBoxGeometry(size, size, size),
|
|
120
|
+
getCachedMaterial({ color, ...options }),
|
|
121
|
+
position
|
|
122
|
+
);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
logger.error('createCube failed:', e);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createAdvancedCube(size = 1, materialOptions = {}, position = [0, 0, 0]) {
|
|
130
|
+
try {
|
|
131
|
+
if (typeof size !== 'number' || size <= 0) size = 1;
|
|
132
|
+
return createMesh(
|
|
133
|
+
gpu.createBoxGeometry(size, size, size),
|
|
134
|
+
getCachedMaterial(materialOptions),
|
|
135
|
+
position
|
|
136
|
+
);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
logger.error('createAdvancedCube failed:', e);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createSphere(
|
|
144
|
+
radius = 1,
|
|
145
|
+
color = 0xffffff,
|
|
146
|
+
position = [0, 0, 0],
|
|
147
|
+
segments = 8,
|
|
148
|
+
options = {}
|
|
149
|
+
) {
|
|
150
|
+
try {
|
|
151
|
+
if (typeof radius !== 'number' || radius <= 0) {
|
|
152
|
+
logger.warn('createSphere: invalid radius, using default');
|
|
153
|
+
radius = 1;
|
|
154
|
+
}
|
|
155
|
+
if (typeof segments !== 'number' || segments < 3) {
|
|
156
|
+
logger.warn('createSphere: invalid segments, using default');
|
|
157
|
+
segments = 8;
|
|
158
|
+
}
|
|
159
|
+
return createMesh(
|
|
160
|
+
gpu.createSphereGeometry(radius, segments),
|
|
161
|
+
getCachedMaterial({ color, ...options }),
|
|
162
|
+
position
|
|
163
|
+
);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
logger.error('createSphere failed:', e);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function createAdvancedSphere(
|
|
171
|
+
radius = 1,
|
|
172
|
+
materialOptions = {},
|
|
173
|
+
position = [0, 0, 0],
|
|
174
|
+
segments = 16
|
|
175
|
+
) {
|
|
176
|
+
try {
|
|
177
|
+
if (typeof radius !== 'number' || radius <= 0) radius = 1;
|
|
178
|
+
if (typeof segments !== 'number' || segments < 3) segments = 16;
|
|
179
|
+
return createMesh(
|
|
180
|
+
gpu.createSphereGeometry(radius, segments),
|
|
181
|
+
getCachedMaterial(materialOptions),
|
|
182
|
+
position
|
|
183
|
+
);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
logger.error('createAdvancedSphere failed:', e);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function createPlane(width = 1, height = 1, color = 0xffffff, position = [0, 0, 0]) {
|
|
191
|
+
try {
|
|
192
|
+
if (typeof width !== 'number' || width <= 0) {
|
|
193
|
+
logger.warn('createPlane: invalid width, using default');
|
|
194
|
+
width = 1;
|
|
195
|
+
}
|
|
196
|
+
if (typeof height !== 'number' || height <= 0) {
|
|
197
|
+
logger.warn('createPlane: invalid height, using default');
|
|
198
|
+
height = 1;
|
|
199
|
+
}
|
|
200
|
+
return createMesh(
|
|
201
|
+
gpu.createPlaneGeometry(width, height),
|
|
202
|
+
getCachedMaterial({ color }),
|
|
203
|
+
position
|
|
204
|
+
);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
logger.error('createPlane failed:', e);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createCylinder(
|
|
212
|
+
radiusTop = 1,
|
|
213
|
+
radiusBottom = 1,
|
|
214
|
+
height = 1,
|
|
215
|
+
color = 0xffffff,
|
|
216
|
+
position = [0, 0, 0],
|
|
217
|
+
options = {}
|
|
218
|
+
) {
|
|
219
|
+
try {
|
|
220
|
+
const geom = gpu.createCylinderGeometry
|
|
221
|
+
? gpu.createCylinderGeometry(radiusTop, radiusBottom, height, options.segments || 16)
|
|
222
|
+
: gpu.createBoxGeometry(radiusTop, height, radiusTop);
|
|
223
|
+
return createMesh(geom, getCachedMaterial({ ...options, color }), position);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
logger.error('createCylinder err', e);
|
|
226
|
+
return createCube(radiusTop * 2, color, position);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function createTorus(
|
|
231
|
+
radius = 1,
|
|
232
|
+
tube = 0.3,
|
|
233
|
+
color = 0xffffff,
|
|
234
|
+
position = [0, 0, 0],
|
|
235
|
+
options = {}
|
|
236
|
+
) {
|
|
237
|
+
try {
|
|
238
|
+
const geometry = new THREE.TorusGeometry(
|
|
239
|
+
radius,
|
|
240
|
+
tube,
|
|
241
|
+
options.radialSegments || 8,
|
|
242
|
+
options.tubularSegments || 16
|
|
243
|
+
);
|
|
244
|
+
return createMesh(geometry, gpu.createN64Material({ color, ...options }), position);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
logger.error('createTorus failed:', e);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function createCone(
|
|
252
|
+
radius = 1,
|
|
253
|
+
height = 2,
|
|
254
|
+
color = 0xffffff,
|
|
255
|
+
position = [0, 0, 0],
|
|
256
|
+
options = {}
|
|
257
|
+
) {
|
|
258
|
+
try {
|
|
259
|
+
return createMesh(
|
|
260
|
+
gpu.createConeGeometry(radius, height, options.segments || 16),
|
|
261
|
+
getCachedMaterial({ color, ...options }),
|
|
262
|
+
position
|
|
263
|
+
);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
logger.error('createCone failed:', e);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function createCapsule(
|
|
271
|
+
radius = 0.5,
|
|
272
|
+
height = 1,
|
|
273
|
+
color = 0xffffff,
|
|
274
|
+
position = [0, 0, 0],
|
|
275
|
+
options = {}
|
|
276
|
+
) {
|
|
277
|
+
try {
|
|
278
|
+
return createMesh(
|
|
279
|
+
gpu.createCapsuleGeometry(radius, height, options.segments || 8),
|
|
280
|
+
getCachedMaterial({ color, ...options }),
|
|
281
|
+
position
|
|
282
|
+
);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
logger.error('createCapsule failed:', e);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
createMesh,
|
|
291
|
+
destroyMesh,
|
|
292
|
+
getMesh,
|
|
293
|
+
removeMesh: destroyMesh,
|
|
294
|
+
createCube,
|
|
295
|
+
createAdvancedCube,
|
|
296
|
+
createSphere,
|
|
297
|
+
createAdvancedSphere,
|
|
298
|
+
createPlane,
|
|
299
|
+
createCylinder,
|
|
300
|
+
createTorus,
|
|
301
|
+
createCone,
|
|
302
|
+
createCapsule,
|
|
303
|
+
};
|
|
304
|
+
}
|