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