@combos-fun/plugin-renderer-3d-graphics 0.0.41 → 0.0.44
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 +1 -1
- package/agent-skill.md +77 -10
- package/combos-plugin.json +1 -1
- package/dist/plugin-renderer-3d-graphics.cjs.js +656 -62
- package/dist/plugin-renderer-3d-graphics.cjs.js.map +1 -1
- package/dist/plugin-renderer-3d-graphics.cjs.prod.js +1 -1
- package/dist/plugin-renderer-3d-graphics.d.ts +36 -6
- package/dist/plugin-renderer-3d-graphics.esm.js +658 -64
- package/dist/plugin-renderer-3d-graphics.esm.js.map +1 -1
- package/package.json +8 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { __decorate } from 'tslib';
|
|
2
2
|
import { Component, OBSERVER_TYPE, decorators } from '@combos-fun/engine';
|
|
3
3
|
import { Field } from '@combos-fun/inspector-decorator';
|
|
4
|
-
import { Renderer3D, Renderer3DSystem, tagObject3D } from '@combos-fun/plugin-renderer-3d';
|
|
5
|
-
import {
|
|
4
|
+
import { COMBOS_INSTANCE_GAME_OBJECT_IDS, textureFromNamedImage, Renderer3D, Renderer3DSystem, Transform3D, is3DSceneParent, has3DChildGameObjects, VisualPoseBridge, tagObject3D } from '@combos-fun/plugin-renderer-3d';
|
|
5
|
+
import { Vector3, Quaternion, Euler, Matrix4, InstancedMesh, BoxGeometry, CapsuleGeometry, TorusGeometry, ConeGeometry, PlaneGeometry, CylinderGeometry, SphereGeometry, DoubleSide, FrontSide, MeshStandardMaterial, MeshToonMaterial, MeshLambertMaterial, MeshBasicMaterial, Group, Mesh } from 'three';
|
|
6
6
|
|
|
7
7
|
class Graphics3D extends Component {
|
|
8
8
|
constructor() {
|
|
@@ -14,7 +14,8 @@ class Graphics3D extends Component {
|
|
|
14
14
|
this.height = 1;
|
|
15
15
|
this.depth = 1;
|
|
16
16
|
this.radius = 0.5;
|
|
17
|
-
this.segments =
|
|
17
|
+
this.segments = 12;
|
|
18
|
+
this.tube = 0;
|
|
18
19
|
this.positionX = 0;
|
|
19
20
|
this.positionY = 0;
|
|
20
21
|
this.positionZ = 0;
|
|
@@ -25,6 +26,13 @@ class Graphics3D extends Component {
|
|
|
25
26
|
this.scaleY = 1;
|
|
26
27
|
this.scaleZ = 1;
|
|
27
28
|
this.opacity = 1;
|
|
29
|
+
this.roughness = 1;
|
|
30
|
+
this.metalness = 0;
|
|
31
|
+
this.emissive = 0;
|
|
32
|
+
this.map = '';
|
|
33
|
+
this.doubleSide = false;
|
|
34
|
+
this.material = 'standard';
|
|
35
|
+
this.instanced = true;
|
|
28
36
|
}
|
|
29
37
|
static { this.componentName = 'Graphics3D'; }
|
|
30
38
|
init(obj) {
|
|
@@ -38,7 +46,8 @@ __decorate([
|
|
|
38
46
|
group: 'Graphics3D',
|
|
39
47
|
label: 'shape',
|
|
40
48
|
description: 'Primitive shape kind.',
|
|
41
|
-
editor: '
|
|
49
|
+
editor: 'enum',
|
|
50
|
+
enumOptions: ['box', 'sphere', 'cylinder', 'plane', 'cone', 'torus', 'capsule'],
|
|
42
51
|
})
|
|
43
52
|
], Graphics3D.prototype, "shape", void 0);
|
|
44
53
|
__decorate([
|
|
@@ -109,6 +118,16 @@ __decorate([
|
|
|
109
118
|
editor: 'number-stepper',
|
|
110
119
|
})
|
|
111
120
|
], Graphics3D.prototype, "segments", void 0);
|
|
121
|
+
__decorate([
|
|
122
|
+
Field({
|
|
123
|
+
type: 'number',
|
|
124
|
+
step: 0.05,
|
|
125
|
+
group: 'Graphics3D',
|
|
126
|
+
label: 'tube',
|
|
127
|
+
description: 'Torus tube radius. 0 means 0.4 × radius.',
|
|
128
|
+
editor: 'number-stepper',
|
|
129
|
+
})
|
|
130
|
+
], Graphics3D.prototype, "tube", void 0);
|
|
112
131
|
__decorate([
|
|
113
132
|
Field({
|
|
114
133
|
type: 'number',
|
|
@@ -211,31 +230,498 @@ __decorate([
|
|
|
211
230
|
editor: 'number-slider',
|
|
212
231
|
})
|
|
213
232
|
], Graphics3D.prototype, "opacity", void 0);
|
|
233
|
+
__decorate([
|
|
234
|
+
Field({
|
|
235
|
+
type: 'number',
|
|
236
|
+
step: 0.05,
|
|
237
|
+
min: 0,
|
|
238
|
+
max: 1,
|
|
239
|
+
group: 'Graphics3D',
|
|
240
|
+
label: 'roughness',
|
|
241
|
+
description: 'PBR roughness (standard material).',
|
|
242
|
+
editor: 'number-slider',
|
|
243
|
+
})
|
|
244
|
+
], Graphics3D.prototype, "roughness", void 0);
|
|
245
|
+
__decorate([
|
|
246
|
+
Field({
|
|
247
|
+
type: 'number',
|
|
248
|
+
step: 0.05,
|
|
249
|
+
min: 0,
|
|
250
|
+
max: 1,
|
|
251
|
+
group: 'Graphics3D',
|
|
252
|
+
label: 'metalness',
|
|
253
|
+
description: 'PBR metalness (standard material).',
|
|
254
|
+
editor: 'number-slider',
|
|
255
|
+
})
|
|
256
|
+
], Graphics3D.prototype, "metalness", void 0);
|
|
257
|
+
__decorate([
|
|
258
|
+
Field({
|
|
259
|
+
type: 'number',
|
|
260
|
+
group: 'Graphics3D',
|
|
261
|
+
label: 'emissive',
|
|
262
|
+
description: 'Emissive color (hex).',
|
|
263
|
+
editor: 'number-stepper',
|
|
264
|
+
})
|
|
265
|
+
], Graphics3D.prototype, "emissive", void 0);
|
|
266
|
+
__decorate([
|
|
267
|
+
Field({
|
|
268
|
+
type: 'string',
|
|
269
|
+
group: 'Graphics3D',
|
|
270
|
+
label: 'map',
|
|
271
|
+
description: 'Named IMAGE resource used as a color map.',
|
|
272
|
+
editor: 'text',
|
|
273
|
+
})
|
|
274
|
+
], Graphics3D.prototype, "map", void 0);
|
|
275
|
+
__decorate([
|
|
276
|
+
Field({
|
|
277
|
+
type: 'boolean',
|
|
278
|
+
group: 'Graphics3D',
|
|
279
|
+
label: 'doubleSide',
|
|
280
|
+
description: 'Render both sides of the mesh.',
|
|
281
|
+
editor: 'toggle',
|
|
282
|
+
})
|
|
283
|
+
], Graphics3D.prototype, "doubleSide", void 0);
|
|
284
|
+
__decorate([
|
|
285
|
+
Field({
|
|
286
|
+
type: 'string',
|
|
287
|
+
group: 'Graphics3D',
|
|
288
|
+
label: 'material',
|
|
289
|
+
description: 'Surface shader.',
|
|
290
|
+
editor: 'enum',
|
|
291
|
+
enumOptions: ['standard', 'basic', 'lambert', 'toon'],
|
|
292
|
+
})
|
|
293
|
+
], Graphics3D.prototype, "material", void 0);
|
|
294
|
+
__decorate([
|
|
295
|
+
Field({
|
|
296
|
+
type: 'boolean',
|
|
297
|
+
group: 'Graphics3D',
|
|
298
|
+
label: 'instanced',
|
|
299
|
+
description: 'Share an InstancedMesh with identical primitives.',
|
|
300
|
+
editor: 'toggle',
|
|
301
|
+
})
|
|
302
|
+
], Graphics3D.prototype, "instanced", void 0);
|
|
303
|
+
|
|
304
|
+
const _pos = new Vector3();
|
|
305
|
+
const _quat = new Quaternion();
|
|
306
|
+
const _scale = new Vector3();
|
|
307
|
+
const _euler = new Euler();
|
|
308
|
+
const _mat = new Matrix4();
|
|
309
|
+
const _swap = new Matrix4();
|
|
310
|
+
function composePose(px, py, pz, rx, ry, rz, sx, sy, sz, target = _mat) {
|
|
311
|
+
_pos.set(px, py, pz);
|
|
312
|
+
_euler.set(rx, ry, rz);
|
|
313
|
+
_quat.setFromEuler(_euler);
|
|
314
|
+
_scale.set(sx, sy, sz);
|
|
315
|
+
return target.compose(_pos, _quat, _scale);
|
|
316
|
+
}
|
|
317
|
+
class InstanceBuckets {
|
|
318
|
+
constructor(scene, shadows) {
|
|
319
|
+
this.scene = scene;
|
|
320
|
+
this.shadows = shadows;
|
|
321
|
+
this.buckets = new Map();
|
|
322
|
+
}
|
|
323
|
+
add(key, id, geometry, material, matrix) {
|
|
324
|
+
let bucket = this.buckets.get(key);
|
|
325
|
+
if (!bucket) {
|
|
326
|
+
bucket = this.create(key, geometry, material);
|
|
327
|
+
}
|
|
328
|
+
if (bucket.live >= bucket.capacity) {
|
|
329
|
+
this.grow(bucket);
|
|
330
|
+
}
|
|
331
|
+
const index = bucket.live;
|
|
332
|
+
bucket.ids[index] = id;
|
|
333
|
+
bucket.indexOf.set(id, index);
|
|
334
|
+
bucket.mesh.setMatrixAt(index, matrix);
|
|
335
|
+
bucket.live += 1;
|
|
336
|
+
bucket.mesh.count = bucket.live;
|
|
337
|
+
bucket.mesh.instanceMatrix.needsUpdate = true;
|
|
338
|
+
}
|
|
339
|
+
setMatrix(key, id, matrix) {
|
|
340
|
+
const bucket = this.buckets.get(key);
|
|
341
|
+
if (!bucket)
|
|
342
|
+
return;
|
|
343
|
+
const index = bucket.indexOf.get(id);
|
|
344
|
+
if (index == null)
|
|
345
|
+
return;
|
|
346
|
+
bucket.mesh.setMatrixAt(index, matrix);
|
|
347
|
+
bucket.mesh.instanceMatrix.needsUpdate = true;
|
|
348
|
+
}
|
|
349
|
+
remove(key, id) {
|
|
350
|
+
const bucket = this.buckets.get(key);
|
|
351
|
+
if (!bucket)
|
|
352
|
+
return;
|
|
353
|
+
const index = bucket.indexOf.get(id);
|
|
354
|
+
if (index == null)
|
|
355
|
+
return;
|
|
356
|
+
const last = bucket.live - 1;
|
|
357
|
+
if (index !== last) {
|
|
358
|
+
const lastId = bucket.ids[last];
|
|
359
|
+
bucket.mesh.getMatrixAt(last, _swap);
|
|
360
|
+
bucket.mesh.setMatrixAt(index, _swap);
|
|
361
|
+
bucket.ids[index] = lastId;
|
|
362
|
+
if (typeof lastId === 'number') {
|
|
363
|
+
bucket.indexOf.set(lastId, index);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
bucket.ids[last] = undefined;
|
|
367
|
+
bucket.indexOf.delete(id);
|
|
368
|
+
bucket.live -= 1;
|
|
369
|
+
bucket.mesh.count = bucket.live;
|
|
370
|
+
bucket.mesh.instanceMatrix.needsUpdate = true;
|
|
371
|
+
if (bucket.live <= 0) {
|
|
372
|
+
this.scene.remove(bucket.mesh);
|
|
373
|
+
bucket.mesh.dispose();
|
|
374
|
+
this.buckets.delete(key);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
clear() {
|
|
378
|
+
for (const bucket of this.buckets.values()) {
|
|
379
|
+
this.scene.remove(bucket.mesh);
|
|
380
|
+
bucket.mesh.dispose();
|
|
381
|
+
}
|
|
382
|
+
this.buckets.clear();
|
|
383
|
+
}
|
|
384
|
+
create(key, geometry, material) {
|
|
385
|
+
const capacity = 8;
|
|
386
|
+
const mesh = new InstancedMesh(geometry, material, capacity);
|
|
387
|
+
mesh.count = 0;
|
|
388
|
+
mesh.frustumCulled = false;
|
|
389
|
+
mesh.castShadow = this.shadows;
|
|
390
|
+
mesh.receiveShadow = this.shadows;
|
|
391
|
+
const ids = new Array(capacity);
|
|
392
|
+
mesh.userData[COMBOS_INSTANCE_GAME_OBJECT_IDS] = ids;
|
|
393
|
+
this.scene.add(mesh);
|
|
394
|
+
const bucket = {
|
|
395
|
+
mesh,
|
|
396
|
+
ids,
|
|
397
|
+
indexOf: new Map(),
|
|
398
|
+
live: 0,
|
|
399
|
+
capacity,
|
|
400
|
+
geometry,
|
|
401
|
+
material,
|
|
402
|
+
};
|
|
403
|
+
this.buckets.set(key, bucket);
|
|
404
|
+
return bucket;
|
|
405
|
+
}
|
|
406
|
+
grow(bucket) {
|
|
407
|
+
const capacity = bucket.capacity * 2;
|
|
408
|
+
const mesh = new InstancedMesh(bucket.geometry, bucket.material, capacity);
|
|
409
|
+
mesh.count = bucket.live;
|
|
410
|
+
mesh.frustumCulled = false;
|
|
411
|
+
mesh.castShadow = this.shadows;
|
|
412
|
+
mesh.receiveShadow = this.shadows;
|
|
413
|
+
const ids = new Array(capacity);
|
|
414
|
+
for (let i = 0; i < bucket.live; i++) {
|
|
415
|
+
bucket.mesh.getMatrixAt(i, _swap);
|
|
416
|
+
mesh.setMatrixAt(i, _swap);
|
|
417
|
+
ids[i] = bucket.ids[i];
|
|
418
|
+
}
|
|
419
|
+
mesh.userData[COMBOS_INSTANCE_GAME_OBJECT_IDS] = ids;
|
|
420
|
+
this.scene.remove(bucket.mesh);
|
|
421
|
+
bucket.mesh.dispose();
|
|
422
|
+
this.scene.add(mesh);
|
|
423
|
+
bucket.mesh = mesh;
|
|
424
|
+
bucket.ids = ids;
|
|
425
|
+
bucket.capacity = capacity;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
214
428
|
|
|
215
|
-
|
|
216
|
-
|
|
429
|
+
function torusTube(radius, tube) {
|
|
430
|
+
return tube && tube > 0 ? tube : radius * 0.4;
|
|
431
|
+
}
|
|
432
|
+
function resolvePrimitive(part, fallback) {
|
|
433
|
+
const src = { ...fallback, ...part };
|
|
434
|
+
const radius = src.radius ?? 0.5;
|
|
435
|
+
return {
|
|
436
|
+
shape: src.shape || 'box',
|
|
437
|
+
color: src.color ?? 0x00ff00,
|
|
438
|
+
wireframe: !!src.wireframe,
|
|
439
|
+
width: src.width ?? 1,
|
|
440
|
+
height: src.height ?? 1,
|
|
441
|
+
depth: src.depth ?? 1,
|
|
442
|
+
radius,
|
|
443
|
+
segments: Math.max(3, Math.round(src.segments ?? 12)),
|
|
444
|
+
tube: torusTube(radius, src.tube),
|
|
445
|
+
positionX: src.positionX ?? 0,
|
|
446
|
+
positionY: src.positionY ?? 0,
|
|
447
|
+
positionZ: src.positionZ ?? 0,
|
|
448
|
+
rotationX: src.rotationX ?? 0,
|
|
449
|
+
rotationY: src.rotationY ?? 0,
|
|
450
|
+
rotationZ: src.rotationZ ?? 0,
|
|
451
|
+
scaleX: src.scaleX ?? 1,
|
|
452
|
+
scaleY: src.scaleY ?? 1,
|
|
453
|
+
scaleZ: src.scaleZ ?? 1,
|
|
454
|
+
opacity: src.opacity ?? 1,
|
|
455
|
+
roughness: src.roughness ?? 1,
|
|
456
|
+
metalness: src.metalness ?? 0,
|
|
457
|
+
emissive: src.emissive ?? 0,
|
|
458
|
+
map: src.map || '',
|
|
459
|
+
doubleSide: !!src.doubleSide,
|
|
460
|
+
material: src.material || 'standard',
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function geometryKey(p) {
|
|
464
|
+
switch (p.shape) {
|
|
465
|
+
case 'sphere':
|
|
466
|
+
return `sphere:${p.radius}:${p.segments}`;
|
|
467
|
+
case 'cylinder':
|
|
468
|
+
return `cylinder:${p.radius}:${p.height}:${p.segments}`;
|
|
469
|
+
case 'cone':
|
|
470
|
+
return `cone:${p.radius}:${p.height}:${p.segments}`;
|
|
471
|
+
case 'plane':
|
|
472
|
+
return `plane:${p.width}:${p.height}:${p.segments}`;
|
|
473
|
+
case 'torus':
|
|
474
|
+
return `torus:${p.radius}:${p.tube}:${p.segments}`;
|
|
475
|
+
case 'capsule':
|
|
476
|
+
return `capsule:${p.radius}:${p.height}:${p.segments}`;
|
|
477
|
+
case 'box':
|
|
478
|
+
default:
|
|
479
|
+
return `box:${p.width}:${p.height}:${p.depth}`;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function createGeometry(p) {
|
|
483
|
+
const segs = p.segments;
|
|
484
|
+
switch (p.shape) {
|
|
485
|
+
case 'sphere':
|
|
486
|
+
return new SphereGeometry(p.radius, segs, segs);
|
|
487
|
+
case 'cylinder':
|
|
488
|
+
return new CylinderGeometry(p.radius, p.radius, p.height, segs);
|
|
489
|
+
case 'plane':
|
|
490
|
+
return new PlaneGeometry(p.width, p.height, segs, segs);
|
|
491
|
+
case 'cone':
|
|
492
|
+
return new ConeGeometry(p.radius, p.height, segs);
|
|
493
|
+
case 'torus':
|
|
494
|
+
return new TorusGeometry(p.radius, p.tube, 8, segs);
|
|
495
|
+
case 'capsule': {
|
|
496
|
+
const length = Math.max(0, p.height - 2 * p.radius);
|
|
497
|
+
return new CapsuleGeometry(p.radius, length, 4, segs);
|
|
498
|
+
}
|
|
499
|
+
case 'box':
|
|
500
|
+
default:
|
|
501
|
+
return new BoxGeometry(p.width, p.height, p.depth);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function materialKey(p) {
|
|
505
|
+
return [
|
|
506
|
+
p.material,
|
|
507
|
+
p.color,
|
|
508
|
+
p.wireframe ? 1 : 0,
|
|
509
|
+
p.opacity,
|
|
510
|
+
p.roughness,
|
|
511
|
+
p.metalness,
|
|
512
|
+
p.emissive,
|
|
513
|
+
p.map,
|
|
514
|
+
p.doubleSide ? 1 : 0,
|
|
515
|
+
].join(':');
|
|
516
|
+
}
|
|
517
|
+
function poseKey(px, py, pz, rx, ry, rz, sx, sy, sz) {
|
|
518
|
+
return `${px},${py},${pz},${rx},${ry},${rz},${sx},${sy},${sz}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
class RefCache {
|
|
522
|
+
constructor() {
|
|
523
|
+
this.items = new Map();
|
|
524
|
+
}
|
|
525
|
+
get(key) {
|
|
526
|
+
return this.items.get(key)?.value;
|
|
527
|
+
}
|
|
528
|
+
acquire(key, create) {
|
|
529
|
+
const hit = this.items.get(key);
|
|
530
|
+
if (hit) {
|
|
531
|
+
hit.refs += 1;
|
|
532
|
+
return hit.value;
|
|
533
|
+
}
|
|
534
|
+
const value = create();
|
|
535
|
+
this.items.set(key, { value, refs: 1 });
|
|
536
|
+
return value;
|
|
537
|
+
}
|
|
538
|
+
release(key, dispose) {
|
|
539
|
+
const hit = this.items.get(key);
|
|
540
|
+
if (!hit)
|
|
541
|
+
return;
|
|
542
|
+
hit.refs -= 1;
|
|
543
|
+
if (hit.refs > 0)
|
|
544
|
+
return;
|
|
545
|
+
dispose(hit.value);
|
|
546
|
+
this.items.delete(key);
|
|
547
|
+
}
|
|
548
|
+
clear(dispose) {
|
|
549
|
+
for (const hit of this.items.values()) {
|
|
550
|
+
dispose(hit.value);
|
|
551
|
+
}
|
|
552
|
+
this.items.clear();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
class GeometryCache {
|
|
556
|
+
constructor() {
|
|
557
|
+
this.cache = new RefCache();
|
|
558
|
+
}
|
|
559
|
+
acquire(primitive) {
|
|
560
|
+
const key = geometryKey(primitive);
|
|
561
|
+
return { key, geometry: this.cache.acquire(key, () => createGeometry(primitive)) };
|
|
562
|
+
}
|
|
563
|
+
release(key) {
|
|
564
|
+
this.cache.release(key, (geometry) => geometry.dispose());
|
|
565
|
+
}
|
|
566
|
+
clear() {
|
|
567
|
+
this.cache.clear((geometry) => geometry.dispose());
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
class TextureCache {
|
|
571
|
+
constructor() {
|
|
572
|
+
this.items = new Map();
|
|
573
|
+
this.inflight = new Map();
|
|
574
|
+
}
|
|
575
|
+
async acquire(name) {
|
|
576
|
+
const hit = this.items.get(name);
|
|
577
|
+
if (hit) {
|
|
578
|
+
hit.refs += 1;
|
|
579
|
+
return hit.texture;
|
|
580
|
+
}
|
|
581
|
+
let pending = this.inflight.get(name);
|
|
582
|
+
if (!pending) {
|
|
583
|
+
pending = textureFromNamedImage(name);
|
|
584
|
+
this.inflight.set(name, pending);
|
|
585
|
+
}
|
|
586
|
+
const texture = await pending;
|
|
587
|
+
this.inflight.delete(name);
|
|
588
|
+
const existing = this.items.get(name);
|
|
589
|
+
if (existing) {
|
|
590
|
+
existing.refs += 1;
|
|
591
|
+
return existing.texture;
|
|
592
|
+
}
|
|
593
|
+
this.items.set(name, { texture, refs: 1 });
|
|
594
|
+
return texture;
|
|
595
|
+
}
|
|
596
|
+
release(name) {
|
|
597
|
+
if (!name)
|
|
598
|
+
return;
|
|
599
|
+
const hit = this.items.get(name);
|
|
600
|
+
if (!hit)
|
|
601
|
+
return;
|
|
602
|
+
hit.refs -= 1;
|
|
603
|
+
if (hit.refs > 0)
|
|
604
|
+
return;
|
|
605
|
+
hit.texture.dispose();
|
|
606
|
+
this.items.delete(name);
|
|
607
|
+
}
|
|
608
|
+
clear() {
|
|
609
|
+
for (const hit of this.items.values()) {
|
|
610
|
+
hit.texture.dispose();
|
|
611
|
+
}
|
|
612
|
+
this.items.clear();
|
|
613
|
+
this.inflight.clear();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function createMaterial(p, map) {
|
|
617
|
+
const common = {
|
|
618
|
+
color: p.color,
|
|
619
|
+
wireframe: p.wireframe,
|
|
620
|
+
transparent: p.opacity < 1 || !!map,
|
|
621
|
+
opacity: p.opacity,
|
|
622
|
+
side: p.doubleSide ? DoubleSide : FrontSide,
|
|
623
|
+
map,
|
|
624
|
+
};
|
|
625
|
+
switch (p.material) {
|
|
626
|
+
case 'basic':
|
|
627
|
+
return new MeshBasicMaterial(common);
|
|
628
|
+
case 'lambert':
|
|
629
|
+
return new MeshLambertMaterial(common);
|
|
630
|
+
case 'toon':
|
|
631
|
+
return new MeshToonMaterial(common);
|
|
632
|
+
case 'standard':
|
|
633
|
+
default:
|
|
634
|
+
return new MeshStandardMaterial({
|
|
635
|
+
...common,
|
|
636
|
+
roughness: p.roughness,
|
|
637
|
+
metalness: p.metalness,
|
|
638
|
+
emissive: p.emissive,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
class MaterialCache {
|
|
643
|
+
constructor() {
|
|
644
|
+
this.cache = new RefCache();
|
|
645
|
+
this.maps = new Map();
|
|
646
|
+
this.textures = new TextureCache();
|
|
647
|
+
}
|
|
648
|
+
async acquire(primitive) {
|
|
649
|
+
const key = materialKey(primitive);
|
|
650
|
+
const hit = this.cache.get(key);
|
|
651
|
+
if (hit) {
|
|
652
|
+
this.cache.acquire(key, () => hit);
|
|
653
|
+
return { key, material: hit };
|
|
654
|
+
}
|
|
655
|
+
const texture = primitive.map ? await this.textures.acquire(primitive.map) : null;
|
|
656
|
+
const again = this.cache.get(key);
|
|
657
|
+
if (again) {
|
|
658
|
+
if (primitive.map)
|
|
659
|
+
this.textures.release(primitive.map);
|
|
660
|
+
this.cache.acquire(key, () => again);
|
|
661
|
+
return { key, material: again };
|
|
662
|
+
}
|
|
663
|
+
const material = createMaterial(primitive, texture);
|
|
664
|
+
this.cache.acquire(key, () => material);
|
|
665
|
+
if (primitive.map)
|
|
666
|
+
this.maps.set(key, primitive.map);
|
|
667
|
+
return { key, material };
|
|
668
|
+
}
|
|
669
|
+
release(key) {
|
|
670
|
+
const mapName = this.maps.get(key);
|
|
671
|
+
this.cache.release(key, (material) => {
|
|
672
|
+
material.dispose();
|
|
673
|
+
if (mapName) {
|
|
674
|
+
this.textures.release(mapName);
|
|
675
|
+
this.maps.delete(key);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
clear() {
|
|
680
|
+
this.cache.clear((material) => material.dispose());
|
|
681
|
+
this.maps.clear();
|
|
682
|
+
this.textures.clear();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const POSE_PROPS = new Set([
|
|
687
|
+
'positionX', 'positionY', 'positionZ',
|
|
688
|
+
'rotationX', 'rotationY', 'rotationZ',
|
|
689
|
+
'scaleX', 'scaleY', 'scaleZ',
|
|
690
|
+
]);
|
|
217
691
|
let Graphics3DSystem = class Graphics3DSystem extends Renderer3D {
|
|
218
692
|
constructor() {
|
|
219
693
|
super(...arguments);
|
|
220
694
|
this.name = 'Graphics3DSystem';
|
|
221
695
|
this.entries = new Map();
|
|
696
|
+
this.geos = new GeometryCache();
|
|
697
|
+
this.mats = new MaterialCache();
|
|
698
|
+
this.buckets = null;
|
|
222
699
|
}
|
|
223
700
|
static { this.systemName = 'Graphics3DSystem'; }
|
|
224
701
|
init() {
|
|
225
702
|
const renderer3DSystem = this.game.getSystem(Renderer3DSystem);
|
|
226
703
|
renderer3DSystem.rendererManager.register(this);
|
|
704
|
+
this.buckets = new InstanceBuckets(this.threeContext.scene, !!this.threeContext.shadows);
|
|
227
705
|
}
|
|
228
706
|
componentChanged(changed) {
|
|
707
|
+
if (changed.componentName === 'Transform') {
|
|
708
|
+
this.handleHierarchyChange(changed.gameObject);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
229
711
|
if (changed.componentName !== 'Graphics3D')
|
|
230
712
|
return;
|
|
231
713
|
if (changed.type === OBSERVER_TYPE.ADD) {
|
|
232
|
-
this.
|
|
714
|
+
this.mount(changed.gameObject, changed.component);
|
|
233
715
|
}
|
|
234
716
|
else if (changed.type === OBSERVER_TYPE.REMOVE) {
|
|
235
717
|
this.handleRemove(changed.gameObject.id);
|
|
236
718
|
}
|
|
237
719
|
else {
|
|
238
|
-
|
|
720
|
+
const changedProp = changed.prop?.prop?.[0];
|
|
721
|
+
if (POSE_PROPS.has(changedProp))
|
|
722
|
+
return;
|
|
723
|
+
this.handleRemove(changed.gameObject.id);
|
|
724
|
+
this.mount(changed.gameObject, changed.component);
|
|
239
725
|
}
|
|
240
726
|
}
|
|
241
727
|
rendererUpdate(gameObject) {
|
|
@@ -245,87 +731,195 @@ let Graphics3DSystem = class Graphics3DSystem extends Renderer3D {
|
|
|
245
731
|
const entry = this.entries.get(gameObject.id);
|
|
246
732
|
if (!entry)
|
|
247
733
|
return;
|
|
248
|
-
|
|
249
|
-
entry.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
this.threeContext.scene.add(mesh);
|
|
266
|
-
this.entries.set(gameObject.id, { mesh });
|
|
267
|
-
}
|
|
268
|
-
handleChange(changed) {
|
|
269
|
-
const component = changed.component;
|
|
270
|
-
const changedProp = changed.prop?.prop?.[0];
|
|
271
|
-
const entry = this.entries.get(changed.gameObject.id);
|
|
272
|
-
if (!entry)
|
|
734
|
+
const transform = gameObject.getComponent(Transform3D);
|
|
735
|
+
const applyVisualPose = entry.poseBridge.sync(component, transform);
|
|
736
|
+
const next = poseKey(component.positionX, component.positionY, component.positionZ, component.rotationX, component.rotationY, component.rotationZ, component.scaleX, component.scaleY, component.scaleZ);
|
|
737
|
+
if (!applyVisualPose) {
|
|
738
|
+
entry.lastPose = next;
|
|
739
|
+
if (entry.group) {
|
|
740
|
+
entry.group.position.set(0, 0, 0);
|
|
741
|
+
entry.group.rotation.set(0, 0, 0);
|
|
742
|
+
entry.group.scale.set(1, 1, 1);
|
|
743
|
+
}
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (entry.lastPose === next)
|
|
747
|
+
return;
|
|
748
|
+
entry.lastPose = next;
|
|
749
|
+
if (entry.mode === 'instance' && entry.bucketKey) {
|
|
750
|
+
this.buckets?.setMatrix(entry.bucketKey, gameObject.id, composePose(component.positionX, component.positionY, component.positionZ, component.rotationX, component.rotationY, component.rotationZ, component.scaleX, component.scaleY, component.scaleZ));
|
|
273
751
|
return;
|
|
274
|
-
if (GEOMETRY_PROPS.includes(changedProp)) {
|
|
275
|
-
entry.mesh.geometry.dispose();
|
|
276
|
-
entry.mesh.geometry = this.createGeometry(component);
|
|
277
752
|
}
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
753
|
+
if (entry.group) {
|
|
754
|
+
entry.group.position.set(component.positionX, component.positionY, component.positionZ);
|
|
755
|
+
entry.group.rotation.set(component.rotationX, component.rotationY, component.rotationZ);
|
|
756
|
+
entry.group.scale.set(component.scaleX, component.scaleY, component.scaleZ);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async mount(gameObject, component) {
|
|
760
|
+
const asyncId = this.increaseAsyncId(gameObject.id);
|
|
761
|
+
let prepared;
|
|
762
|
+
try {
|
|
763
|
+
prepared = await this.prepare(component);
|
|
764
|
+
}
|
|
765
|
+
catch (err) {
|
|
766
|
+
console.error(`Graphics3D prepare failed for ${gameObject.name}`, err);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (!this.validateAsyncId(gameObject.id, asyncId)) {
|
|
770
|
+
this.releasePrepared(prepared);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
this.attach(gameObject, component, prepared);
|
|
774
|
+
}
|
|
775
|
+
async prepare(component) {
|
|
776
|
+
const specs = this.resolveParts(component);
|
|
777
|
+
const prepared = [];
|
|
778
|
+
try {
|
|
779
|
+
for (const primitive of specs) {
|
|
780
|
+
const { key: geoKey, geometry } = this.geos.acquire(primitive);
|
|
781
|
+
try {
|
|
782
|
+
const { key: matKey, material } = await this.mats.acquire(primitive);
|
|
783
|
+
prepared.push({ primitive, geoKey, matKey, geometry, material });
|
|
784
|
+
}
|
|
785
|
+
catch (err) {
|
|
786
|
+
this.geos.release(geoKey);
|
|
787
|
+
throw err;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch (err) {
|
|
792
|
+
this.releasePrepared(prepared);
|
|
793
|
+
throw err;
|
|
794
|
+
}
|
|
795
|
+
return prepared;
|
|
796
|
+
}
|
|
797
|
+
resolveParts(component) {
|
|
798
|
+
if (component.parts && component.parts.length > 0) {
|
|
799
|
+
return component.parts.map((part) => resolvePrimitive(part, component));
|
|
800
|
+
}
|
|
801
|
+
return [resolvePrimitive(component)];
|
|
802
|
+
}
|
|
803
|
+
handleHierarchyChange(gameObject) {
|
|
804
|
+
const parent = gameObject.parent;
|
|
805
|
+
if (parent && parent !== gameObject.scene) {
|
|
806
|
+
this.ensureGroupMode(parent);
|
|
807
|
+
}
|
|
808
|
+
this.ensureGroupMode(gameObject);
|
|
809
|
+
this.threeContext.reparentGameObject(gameObject);
|
|
810
|
+
}
|
|
811
|
+
ensureGroupMode(gameObject) {
|
|
812
|
+
const entry = this.entries.get(gameObject.id);
|
|
813
|
+
if (!entry || entry.mode !== 'instance')
|
|
814
|
+
return;
|
|
815
|
+
const component = gameObject.getComponent(Graphics3D);
|
|
816
|
+
if (!component)
|
|
817
|
+
return;
|
|
818
|
+
if (entry.bucketKey) {
|
|
819
|
+
this.buckets?.remove(entry.bucketKey, gameObject.id);
|
|
820
|
+
}
|
|
821
|
+
this.attachGroup(gameObject, component, entry.prepared, entry.lastPose);
|
|
822
|
+
}
|
|
823
|
+
attach(gameObject, component, prepared) {
|
|
824
|
+
const useInstance = component.instanced !== false
|
|
825
|
+
&& prepared.length === 1
|
|
826
|
+
&& is3DSceneParent(gameObject)
|
|
827
|
+
&& !has3DChildGameObjects(gameObject)
|
|
828
|
+
&& !gameObject.getComponent(Transform3D)
|
|
829
|
+
&& !this.threeContext.hasTransform3DRoot(gameObject.id);
|
|
830
|
+
const lastPose = poseKey(component.positionX, component.positionY, component.positionZ, component.rotationX, component.rotationY, component.rotationZ, component.scaleX, component.scaleY, component.scaleZ);
|
|
831
|
+
if (useInstance) {
|
|
832
|
+
const part = prepared[0];
|
|
833
|
+
const bucketKey = `${part.geoKey}|${part.matKey}`;
|
|
834
|
+
this.buckets?.add(bucketKey, gameObject.id, part.geometry, part.material, composePose(component.positionX, component.positionY, component.positionZ, component.rotationX, component.rotationY, component.rotationZ, component.scaleX, component.scaleY, component.scaleZ));
|
|
835
|
+
this.entries.set(gameObject.id, {
|
|
836
|
+
mode: 'instance',
|
|
837
|
+
bucketKey,
|
|
838
|
+
prepared,
|
|
839
|
+
lastPose,
|
|
840
|
+
poseBridge: new VisualPoseBridge(),
|
|
841
|
+
});
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
this.attachGroup(gameObject, component, prepared, lastPose);
|
|
845
|
+
}
|
|
846
|
+
attachGroup(gameObject, component, prepared, lastPose) {
|
|
847
|
+
const group = new Group();
|
|
848
|
+
const shadows = !!this.threeContext.shadows;
|
|
849
|
+
for (const part of prepared) {
|
|
850
|
+
const mesh = new Mesh(part.geometry, part.material);
|
|
851
|
+
mesh.position.set(part.primitive.positionX, part.primitive.positionY, part.primitive.positionZ);
|
|
852
|
+
mesh.rotation.set(part.primitive.rotationX, part.primitive.rotationY, part.primitive.rotationZ);
|
|
853
|
+
mesh.scale.set(part.primitive.scaleX, part.primitive.scaleY, part.primitive.scaleZ);
|
|
854
|
+
mesh.castShadow = shadows;
|
|
855
|
+
mesh.receiveShadow = shadows;
|
|
856
|
+
group.add(mesh);
|
|
857
|
+
}
|
|
858
|
+
tagObject3D(group, gameObject.id);
|
|
859
|
+
const transform = gameObject.getComponent(Transform3D);
|
|
860
|
+
const poseBridge = new VisualPoseBridge();
|
|
861
|
+
const applyVisualPose = poseBridge.sync(component, transform);
|
|
862
|
+
if (applyVisualPose) {
|
|
863
|
+
group.position.set(component.positionX, component.positionY, component.positionZ);
|
|
864
|
+
group.rotation.set(component.rotationX, component.rotationY, component.rotationZ);
|
|
865
|
+
group.scale.set(component.scaleX, component.scaleY, component.scaleZ);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
group.position.set(0, 0, 0);
|
|
869
|
+
group.rotation.set(0, 0, 0);
|
|
870
|
+
group.scale.set(1, 1, 1);
|
|
871
|
+
}
|
|
872
|
+
this.threeContext.attachVisual(gameObject.id, group, gameObject);
|
|
873
|
+
this.entries.set(gameObject.id, {
|
|
874
|
+
mode: 'group',
|
|
875
|
+
group,
|
|
876
|
+
prepared,
|
|
877
|
+
lastPose,
|
|
878
|
+
poseBridge,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
releasePrepared(prepared) {
|
|
882
|
+
for (const part of prepared) {
|
|
883
|
+
this.geos.release(part.geoKey);
|
|
884
|
+
this.mats.release(part.matKey);
|
|
284
885
|
}
|
|
285
886
|
}
|
|
286
887
|
handleRemove(id) {
|
|
888
|
+
this.increaseAsyncId(id);
|
|
287
889
|
const entry = this.entries.get(id);
|
|
288
890
|
if (!entry)
|
|
289
891
|
return;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
entry.mesh.material.dispose();
|
|
293
|
-
this.entries.delete(id);
|
|
294
|
-
}
|
|
295
|
-
createGeometry(component) {
|
|
296
|
-
switch (component.shape) {
|
|
297
|
-
case 'sphere':
|
|
298
|
-
return new SphereGeometry(component.radius, component.segments, component.segments);
|
|
299
|
-
case 'cylinder':
|
|
300
|
-
return new CylinderGeometry(component.radius, component.radius, component.height, component.segments);
|
|
301
|
-
case 'plane':
|
|
302
|
-
return new PlaneGeometry(component.width, component.height);
|
|
303
|
-
case 'cone':
|
|
304
|
-
return new ConeGeometry(component.radius, component.height, component.segments);
|
|
305
|
-
case 'torus':
|
|
306
|
-
return new TorusGeometry(component.radius, component.radius * 0.4, 16, component.segments);
|
|
307
|
-
case 'box':
|
|
308
|
-
default:
|
|
309
|
-
return new BoxGeometry(component.width, component.height, component.depth);
|
|
892
|
+
if (entry.mode === 'instance' && entry.bucketKey) {
|
|
893
|
+
this.buckets?.remove(entry.bucketKey, id);
|
|
310
894
|
}
|
|
895
|
+
else if (entry.group) {
|
|
896
|
+
this.threeContext.detachVisual(id, entry.group);
|
|
897
|
+
}
|
|
898
|
+
this.releasePrepared(entry.prepared);
|
|
899
|
+
this.entries.delete(id);
|
|
311
900
|
}
|
|
312
901
|
onDestroy() {
|
|
313
902
|
for (const [id] of this.entries) {
|
|
314
903
|
this.handleRemove(id);
|
|
315
904
|
}
|
|
316
905
|
this.entries.clear();
|
|
906
|
+
this.buckets?.clear();
|
|
907
|
+
this.geos.clear();
|
|
908
|
+
this.mats.clear();
|
|
317
909
|
}
|
|
318
910
|
};
|
|
319
911
|
Graphics3DSystem = __decorate([
|
|
320
912
|
decorators.componentObserver({
|
|
321
913
|
Graphics3D: [
|
|
322
914
|
'shape', 'color', 'wireframe',
|
|
323
|
-
'width', 'height', 'depth', 'radius', 'segments',
|
|
915
|
+
'width', 'height', 'depth', 'radius', 'segments', 'tube',
|
|
324
916
|
'positionX', 'positionY', 'positionZ',
|
|
325
917
|
'rotationX', 'rotationY', 'rotationZ',
|
|
326
918
|
'scaleX', 'scaleY', 'scaleZ',
|
|
327
|
-
'opacity',
|
|
919
|
+
'opacity', 'roughness', 'metalness', 'emissive',
|
|
920
|
+
'map', 'doubleSide', 'material', 'instanced', 'parts',
|
|
328
921
|
],
|
|
922
|
+
Transform: ['_parent'],
|
|
329
923
|
})
|
|
330
924
|
], Graphics3DSystem);
|
|
331
925
|
var Graphics3DSystem_default = Graphics3DSystem;
|