three-base 2.0.142 → 2.0.166
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/DirectionalLight.d.ts +6 -6
- package/DirectionalLight.js +68 -68
- package/EventMap.d.ts +103 -103
- package/EventMap.js +17 -17
- package/EventMesh.d.ts +18 -18
- package/EventMesh.js +59 -59
- package/ExtrudeMesh.d.ts +112 -112
- package/ExtrudeMesh.js +342 -342
- package/FPScontrol.d.ts +531 -511
- package/FPScontrol.js +1866 -1799
- package/FPScontrol.ts +1922 -1855
- package/Fire/Fire.d.ts +28 -28
- package/Fire/Fire.js +80 -80
- package/Fire/FireGeometry.d.ts +9 -9
- package/Fire/FireGeometry.js +40 -40
- package/Fire/FireMaterial.d.ts +28 -28
- package/Fire/FireMaterial.js +32 -32
- package/Fire/index.d.ts +2 -2
- package/Fire/index.js +2 -2
- package/GLTFDracoLoader.d.ts +71 -71
- package/GLTFDracoLoader.js +266 -266
- package/GLTFDracoLoader.ts +1 -1
- package/Grass/Grass.d.ts +8 -8
- package/Grass/Grass.js +38 -38
- package/Grass/index.d.ts +2 -2
- package/Grass/index.js +2 -2
- package/Ground1/Ground1.d.ts +10 -10
- package/Ground1/Ground1.js +71 -71
- package/Ground1/index.d.ts +1 -1
- package/Ground1/index.js +1 -1
- package/MeshBase.d.ts +36 -36
- package/MeshBase.js +251 -247
- package/MeshBase.ts +6 -2
- package/Mirror.d.ts +25 -25
- package/Mirror.js +143 -143
- package/MktMesh/MktMesh.d.ts +301 -291
- package/MktMesh/MktMesh.js +1064 -1045
- package/MktMesh/MktMesh.ts +32 -11
- package/MktMesh/assetUrl.d.ts +59 -35
- package/MktMesh/assetUrl.js +165 -69
- package/MktMesh/assetUrl.ts +164 -69
- package/MktMesh/mktMappingShader.d.ts +12 -12
- package/MktMesh/mktMappingShader.js +138 -138
- package/MktMesh/mktMappingShader.ts +139 -139
- package/MktMesh/mktShader.d.ts +263 -263
- package/MktMesh/mktShader.js +1236 -1236
- package/MktMesh/mktShader.ts +1344 -1344
- package/MktMesh/panoBitmapTexture.d.ts +25 -25
- package/MktMesh/panoBitmapTexture.js +52 -52
- package/MktMesh/panoBitmapTexture.ts +54 -54
- package/MktMesh/panoSlotBind.d.ts +29 -29
- package/MktMesh/panoSlotBind.js +106 -106
- package/MktMesh/panoSlotBind.ts +153 -153
- package/MktMesh/panoSlotRoster.d.ts +22 -22
- package/MktMesh/panoSlotRoster.js +169 -169
- package/MktMesh/panoSlotRoster.ts +168 -168
- package/MktMesh/panoTileStitcher.d.ts +119 -119
- package/MktMesh/panoTileStitcher.js +529 -529
- package/MktMesh/panoTileStitcher.ts +616 -616
- package/MktMesh/types.d.ts +75 -75
- package/MktMesh/types.js +1 -1
- package/MktMesh/wallCapTessellate.d.ts +33 -33
- package/MktMesh/wallCapTessellate.js +97 -97
- package/MktMesh/wallCapTessellate.ts +68 -68
- package/PipeMesh/PathGeometry.d.ts +44 -44
- package/PipeMesh/PathGeometry.js +307 -307
- package/PipeMesh/PathPoint.d.ts +17 -17
- package/PipeMesh/PathPoint.js +31 -31
- package/PipeMesh/PathPointList.d.ts +31 -31
- package/PipeMesh/PathPointList.js +209 -209
- package/PipeMesh/PathTubeGeometry.d.ts +25 -25
- package/PipeMesh/PathTubeGeometry.js +130 -130
- package/PipeMesh/PipeMesh.d.ts +25 -25
- package/PipeMesh/PipeMesh.js +63 -63
- package/PipeMesh/index.d.ts +1 -1
- package/PipeMesh/index.js +1 -1
- package/PlaneModal1/PlaneModal1.d.ts +34 -34
- package/PlaneModal1/PlaneModal1.js +109 -109
- package/PlaneModal1/index.d.ts +1 -1
- package/PlaneModal1/index.js +1 -1
- package/PlaneText.d.ts +27 -27
- package/PlaneText.js +35 -35
- package/PlaneWithHtml.d.ts +13 -13
- package/PlaneWithHtml.js +66 -64
- package/PlaneWithHtml.ts +3 -1
- package/PointLight/PointLight.d.ts +13 -13
- package/PointLight/PointLight.js +64 -64
- package/PointLight/index.d.ts +1 -1
- package/PointLight/index.js +1 -1
- package/Sky/Sky.d.ts +9 -9
- package/Sky/Sky.js +22 -22
- package/Sky/index.d.ts +1 -1
- package/Sky/index.js +1 -1
- package/SpotLight.d.ts +6 -6
- package/SpotLight.js +72 -72
- package/SpriteModal1/SpriteModal1.d.ts +33 -33
- package/SpriteModal1/SpriteModal1.js +116 -116
- package/SpriteModal1/index.d.ts +1 -1
- package/SpriteModal1/index.js +1 -1
- package/SpriteText.d.ts +46 -46
- package/SpriteText.js +176 -176
- package/SpriteText2.d.ts +48 -48
- package/SpriteText2.js +223 -223
- package/SpriteText3.d.ts +86 -86
- package/SpriteText3.js +517 -517
- package/SpriteText3.ts +599 -599
- package/SpriteWithHtml.d.ts +9 -9
- package/SpriteWithHtml.js +46 -44
- package/SpriteWithHtml.ts +3 -1
- package/Text3D/Text3D.d.ts +20 -20
- package/Text3D/Text3D.js +58 -58
- package/Text3D/fontController.d.ts +60 -60
- package/Text3D/fontController.js +166 -166
- package/Text3D/index.d.ts +1 -1
- package/Text3D/index.js +1 -1
- package/ThreeBase.d.ts +248 -248
- package/ThreeBase.js +1202 -1202
- package/ThreeBase.ts +1314 -1314
- package/ThreeMouse.d.ts +53 -54
- package/ThreeMouse.js +453 -453
- package/Transition.d.ts +45 -45
- package/Transition.js +209 -209
- package/Water/Water.d.ts +5 -5
- package/Water/Water.js +44 -44
- package/Water/index.d.ts +2 -2
- package/Water/index.js +2 -2
- package/WaterFlow/FloatingDuck.d.ts +88 -88
- package/WaterFlow/FloatingDuck.js +307 -307
- package/WaterFlow/FloatingDuck.ts +445 -445
- package/WaterFlow/GPUFluidSimulation.d.ts +56 -56
- package/WaterFlow/GPUFluidSimulation.js +781 -781
- package/WaterFlow/GPUFluidSimulation.ts +827 -827
- package/WaterFlow/ReservoirSystem.d.ts +106 -106
- package/WaterFlow/ReservoirSystem.js +611 -611
- package/WaterFlow/ReservoirSystem.ts +754 -754
- package/WaterFlow/SPHField.d.ts +35 -35
- package/WaterFlow/SPHField.js +203 -203
- package/WaterFlow/SPHField.ts +228 -228
- package/WaterFlow/ScreenSpaceFluidPass.d.ts +69 -69
- package/WaterFlow/ScreenSpaceFluidPass.js +728 -728
- package/WaterFlow/ScreenSpaceFluidPass.ts +789 -789
- package/WaterFlow/ShallowWaterSurface.d.ts +45 -45
- package/WaterFlow/ShallowWaterSurface.js +438 -438
- package/WaterFlow/ShallowWaterSurface.ts +510 -510
- package/WaterFlow/WaterFlow.d.ts +87 -87
- package/WaterFlow/WaterFlow.js +171 -171
- package/WaterFlow/WaterFlow.ts +219 -219
- package/WaterFlow/types.d.ts +161 -161
- package/WaterFlow/types.js +1 -1
- package/WaterFlow/types.ts +189 -189
- package/WaterFlow/waterFlowPass.d.ts +3 -3
- package/WaterFlow/waterFlowPass.js +20 -20
- package/WaterFlow/waterFlowPass.ts +40 -40
- package/collisionOBB.d.ts +42 -42
- package/collisionOBB.js +112 -112
- package/collisionOBB.ts +108 -108
- package/declare.d.ts +25 -25
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/instanced/InstancedBatch.d.ts +57 -57
- package/instanced/InstancedBatch.js +348 -348
- package/instanced/InstancedBatch.ts +406 -406
- package/instanced/InstancedChild.d.ts +174 -174
- package/instanced/InstancedChild.js +264 -264
- package/instanced/InstancedChild.ts +327 -327
- package/instanced/InstancedGroup.d.ts +43 -43
- package/instanced/InstancedGroup.js +126 -126
- package/instanced/InstancedGroup.ts +137 -137
- package/instanced/InstancedMeshBase.d.ts +38 -38
- package/instanced/InstancedMeshBase.js +282 -282
- package/instanced/InstancedTemplate.d.ts +46 -46
- package/instanced/InstancedTemplate.js +125 -125
- package/keyControls.d.ts +29 -30
- package/keyControls.js +101 -101
- package/lensflare/LensFlare.d.ts +39 -39
- package/lensflare/LensFlare.js +284 -284
- package/package.json +6 -1
- package/pass/bloom_pass.d.ts +2 -2
- package/pass/bloom_pass.js +174 -174
- package/pass/cover_pass.d.ts +2 -2
- package/pass/cover_pass.js +108 -108
- package/pass/debugDepthState.d.ts +20 -20
- package/pass/debugDepthState.js +9 -9
- package/pass/debugDepthState.ts +22 -22
- package/pass/depthTestBias.d.ts +12 -12
- package/pass/depthTestBias.js +12 -12
- package/pass/depthTestBias.ts +12 -12
- package/pass/effectUserData.d.ts +27 -27
- package/pass/effectUserData.js +1 -1
- package/pass/effectUserData.ts +46 -46
- package/pass/frosted_glass_pass.d.ts +2 -2
- package/pass/frosted_glass_pass.js +150 -150
- package/pass/fsQuad.d.ts +9 -9
- package/pass/fsQuad.js +28 -28
- package/pass/glow_pass.d.ts +2 -2
- package/pass/glow_pass.js +350 -350
- package/pass/screen_bloom_pass.d.ts +11 -11
- package/pass/screen_bloom_pass.js +136 -136
- package/pass/ssao_pass.d.ts +88 -88
- package/pass/ssao_pass.js +799 -799
- package/pass/ssao_pass.ts +918 -918
- package/pass/stroke_pass.d.ts +2 -2
- package/pass/stroke_pass.js +267 -267
- package/shaders/FXAAShaderMaterial.d.ts +3 -3
- package/shaders/FXAAShaderMaterial.js +20 -20
- package/shaders/baseDepthVisShader.d.ts +11 -11
- package/shaders/baseDepthVisShader.js +36 -36
- package/shaders/baseDepthVisShader.ts +36 -36
- package/shaders/baseTextureToRenderShader.d.ts +2 -2
- package/shaders/baseTextureToRenderShader.js +19 -19
- package/shaders/copyShader.d.ts +3 -3
- package/shaders/copyShader.js +13 -13
- package/shaders/debugDepthCompareShader.d.ts +14 -14
- package/shaders/debugDepthCompareShader.js +49 -49
- package/shaders/debugDepthCompareShader.ts +50 -50
- package/shaders/debugDepthJudgeShader.d.ts +18 -18
- package/shaders/debugDepthJudgeShader.js +52 -52
- package/shaders/debugDepthJudgeShader.ts +54 -54
- package/shaders/debugExcludedShader.d.ts +13 -13
- package/shaders/debugExcludedShader.js +46 -46
- package/shaders/debugExcludedShader.ts +48 -48
- package/spriteTextBg.d.ts +1 -1
- package/spriteTextBg.js +20 -20
- package/spriteTextBg.ts +20 -20
- package/threeLoader.d.ts +35 -35
- package/threeLoader.js +518 -518
- package/threeUtils.d.ts +71 -53
- package/threeUtils.js +363 -329
- package/threeUtils.ts +35 -0
- package/types.d.ts +2 -2
- package/types.js +6 -6
|
@@ -1,348 +1,348 @@
|
|
|
1
|
-
import { AnimationMixer, DynamicDrawUsage, Group, InstancedBufferAttribute, InstancedMesh, Matrix4, Vector3, } from "three";
|
|
2
|
-
import { loadTemplate } from "./InstancedTemplate";
|
|
3
|
-
import FPScontrol from "../FPScontrol";
|
|
4
|
-
const _m = new Matrix4();
|
|
5
|
-
const _v = new Vector3();
|
|
6
|
-
const _n = new Vector3();
|
|
7
|
-
const _skinMat = new Matrix4();
|
|
8
|
-
const _tmpMat = new Matrix4();
|
|
9
|
-
/**
|
|
10
|
-
* 把 skeleton 当前姿态烘焙进 geometry(线性混合蒙皮,LBS)。
|
|
11
|
-
* 由于同一 batch 内所有实例共享同一套骨骼矩阵(共享姿态),每帧只需烘焙一份几何,
|
|
12
|
-
* 所有实例复用 —— 这是把"GPU skinning + instancing"降级为"每帧 CPU 烘焙一份共享几何 + instancing"的关键,
|
|
13
|
-
* 复杂度从自定义 shader 降到几何更新,天然兼容项目所有材质与后处理。
|
|
14
|
-
*/
|
|
15
|
-
function bakeSkinning(srcGeom, dstGeom, skeleton, bindMatrix, bindMatrixInverse) {
|
|
16
|
-
// 从 srcGeom(原始 bind-pose,只读)逐顶点读取,计算结果写入 dstGeom(渲染用几何)。
|
|
17
|
-
// 绝不能读改写同一份几何 —— 否则上一帧烘焙结果被当成 bind-pose 再烤一遍,蒙皮累积错位。
|
|
18
|
-
const pos = srcGeom.attributes.position;
|
|
19
|
-
const norm = srcGeom.attributes.normal;
|
|
20
|
-
const si = srcGeom.attributes.skinIndex;
|
|
21
|
-
const sw = srcGeom.attributes.skinWeight;
|
|
22
|
-
const dpos = dstGeom.attributes.position;
|
|
23
|
-
const dnorm = dstGeom.attributes.normal;
|
|
24
|
-
if (!pos || !si || !sw || !dpos) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const boneMatrices = skeleton.boneMatrices; // Float32Array,长度 = bones * 16
|
|
28
|
-
if (!boneMatrices) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
for (let i = 0; i < pos.count; i++) {
|
|
32
|
-
// 与 three 着色器保持一致:先 bindMatrix 把顶点转到"骨骼局部坐标系",
|
|
33
|
-
// 再叠加骨骼混合矩阵,最后 bindMatrixInverse 回到模型空间。顺序反了动画姿态会错。
|
|
34
|
-
_v.fromBufferAttribute(pos, i).applyMatrix4(bindMatrix);
|
|
35
|
-
_skinMat.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
36
|
-
for (let j = 0; j < 4; j++) {
|
|
37
|
-
const w = sw.getComponent(i, j);
|
|
38
|
-
if (w === 0) {
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
const bi = si.getComponent(i, j);
|
|
42
|
-
_tmpMat.fromArray(boneMatrices, bi * 16);
|
|
43
|
-
_tmpMat.multiplyScalar(w);
|
|
44
|
-
const se = _skinMat.elements;
|
|
45
|
-
const te = _tmpMat.elements;
|
|
46
|
-
for (let k = 0; k < 16; k++) {
|
|
47
|
-
se[k] += te[k];
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
_v.applyMatrix4(_skinMat).applyMatrix4(bindMatrixInverse);
|
|
51
|
-
dpos.setXYZ(i, _v.x, _v.y, _v.z);
|
|
52
|
-
if (norm && dnorm) {
|
|
53
|
-
_n.fromBufferAttribute(norm, i).applyMatrix4(_skinMat).normalize();
|
|
54
|
-
dnorm.setXYZ(i, _n.x, _n.y, _n.z);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
dpos.needsUpdate = true;
|
|
58
|
-
if (dnorm) {
|
|
59
|
-
dnorm.needsUpdate = true;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* 把节点相对根的世界变换烘焙进几何(非骨骼节点动画的实例化方案)。
|
|
64
|
-
* 与骨骼 LBS 同理:把"节点层级变换"降级为每帧 CPU 几何变换,
|
|
65
|
-
* 所有实例复用一份变换后的几何,天然兼容项目所有材质与后处理。
|
|
66
|
-
*/
|
|
67
|
-
function bakeNodeTransform(geometry, local, m) {
|
|
68
|
-
const pos = geometry.attributes.position;
|
|
69
|
-
const lp = local.attributes.position;
|
|
70
|
-
if (!pos || !lp) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
for (let i = 0; i < lp.count; i++) {
|
|
74
|
-
_v.fromBufferAttribute(lp, i).applyMatrix4(m);
|
|
75
|
-
pos.setXYZ(i, _v.x, _v.y, _v.z);
|
|
76
|
-
}
|
|
77
|
-
pos.needsUpdate = true;
|
|
78
|
-
const norm = geometry.attributes.normal;
|
|
79
|
-
const ln = local.attributes.normal;
|
|
80
|
-
if (norm && ln) {
|
|
81
|
-
for (let i = 0; i < ln.count; i++) {
|
|
82
|
-
_n.fromBufferAttribute(ln, i).applyMatrix4(m).normalize();
|
|
83
|
-
norm.setXYZ(i, _n.x, _n.y, _n.z);
|
|
84
|
-
}
|
|
85
|
-
norm.needsUpdate = true;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* 内部类:封装一个 batch 的真实 InstancedMesh(多 submesh 时每个 submesh 一个,共享同一组实例序号)。
|
|
90
|
-
* - 按 batchKey 分组(相同 geometry+material 或相同 instanceUrl+animation 为一类);
|
|
91
|
-
* - 增量同步:仅 needUpdate=true 的实例才重写 matrix/color,写完后立即置 false;
|
|
92
|
-
* - 列表变化(增删/重排)时全量 compact 重写并重建索引;
|
|
93
|
-
* - 含骨骼动画时:用共享姿态 AnimationMixer 驱动 skeleton,每帧烘焙到共享几何(LBS);
|
|
94
|
-
* - 含非骨骼节点动画时:同样用共享姿态 AnimationMixer 驱动节点变换,每帧把"节点相对根的世界变换"
|
|
95
|
-
* 烘焙进几何(节点变换动画的实例化方案)。
|
|
96
|
-
*/
|
|
97
|
-
export default class InstancedBatch {
|
|
98
|
-
key;
|
|
99
|
-
/** 渲染组:持有本 batch 的 InstancedMesh,由 InstancedGroup add 进 scene */
|
|
100
|
-
group = new Group();
|
|
101
|
-
template;
|
|
102
|
-
ready = false;
|
|
103
|
-
_children = [];
|
|
104
|
-
_meshes = [];
|
|
105
|
-
_capacity;
|
|
106
|
-
_step;
|
|
107
|
-
_loading;
|
|
108
|
-
_mixer;
|
|
109
|
-
_clipName;
|
|
110
|
-
/** 当前正在播放的动画剪辑名(运行时可由 child.playClip 切换) */
|
|
111
|
-
_currentClip;
|
|
112
|
-
/** 首例实例是否声明碰撞(构造时快照,buildMeshes 用它给 InstancedMesh 打标记) */
|
|
113
|
-
_sampleFpsCollision = false;
|
|
114
|
-
/** 当前正在播放的 AnimationAction(切换时先 stop 旧的) */
|
|
115
|
-
_currentAction;
|
|
116
|
-
/** 非骨骼动画:需每帧烘焙节点变换的几何列表(与 _meshes 顺序对齐) */
|
|
117
|
-
_nodeBake = [];
|
|
118
|
-
constructor(key, sample, capacity = 100, step = 100) {
|
|
119
|
-
this.key = key;
|
|
120
|
-
this._capacity = capacity;
|
|
121
|
-
this._step = step;
|
|
122
|
-
// 用实例当前生效动画(playClip 优先,否则 static animation)构建本 batch 的 mixer;
|
|
123
|
-
// 因 key 已含动画名,相同 glb+动画的实例都进同一个常驻 batch
|
|
124
|
-
this._clipName = sample.getEffectiveAnimation();
|
|
125
|
-
this._sampleFpsCollision = !!sample.fpsCollision;
|
|
126
|
-
this._loading = loadTemplate(sample).then((t) => {
|
|
127
|
-
const ctor = sample.constructor;
|
|
128
|
-
// GLB 模板就绪后,交给子类按需修正材质(如瓶子 metalness 修正)
|
|
129
|
-
if (typeof ctor.prepareTemplate === "function") {
|
|
130
|
-
ctor.prepareTemplate(t);
|
|
131
|
-
}
|
|
132
|
-
this.template = t;
|
|
133
|
-
this.buildMeshes();
|
|
134
|
-
this.ready = true;
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
/** 模板就绪后,为每个 submesh 建一个 InstancedMesh,并启动动画(若有) */
|
|
138
|
-
buildMeshes() {
|
|
139
|
-
if (!this.template) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
for (const sub of this.template.submeshes) {
|
|
143
|
-
const geom = sub.skinned && this.template.sharedGeometry
|
|
144
|
-
? this.template.sharedGeometry
|
|
145
|
-
: sub.geometry;
|
|
146
|
-
const im = new InstancedMesh(geom, sub.material, this._capacity);
|
|
147
|
-
im.frustumCulled = false;
|
|
148
|
-
im.instanceMatrix.setUsage(DynamicDrawUsage);
|
|
149
|
-
// 预创建 instanceColor(默认白,setColorAt 会覆盖)
|
|
150
|
-
im.instanceColor = new InstancedBufferAttribute(new Float32Array(this._capacity * 3).fill(1), 3);
|
|
151
|
-
im.count = 0;
|
|
152
|
-
im.userData.__batchKey = this.key;
|
|
153
|
-
// 若本 batch 的实例声明了碰撞检测,则把真实 InstancedMesh 标记为碰撞体,
|
|
154
|
-
// 供 FPScontrol 的碰撞射线命中(实例化对象的几何合并在 InstancedMesh 里,只能整体标记)。
|
|
155
|
-
im.userData.fpsCollision = this._sampleFpsCollision;
|
|
156
|
-
this._meshes.push(im);
|
|
157
|
-
this.group.add(im);
|
|
158
|
-
// 非骨骼动画:记录节点烘焙信息,每帧把节点相对根的世界变换写进几何
|
|
159
|
-
if (sub.node && this.template.animRoot && !this.template.skeleton && sub.localGeometry) {
|
|
160
|
-
this._nodeBake.push({ node: sub.node, local: sub.localGeometry, render: sub.geometry });
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// 模板就绪即标记碰撞体,并通知所有 FPScontrol 重新收集(模板是异步加载的,
|
|
164
|
-
// 此时控制器多半已先创建,必须主动通知它重建碰撞列表,否则实例化碰撞体不生效)。
|
|
165
|
-
if (this._sampleFpsCollision) {
|
|
166
|
-
FPScontrol.markAllCollidersDirty();
|
|
167
|
-
}
|
|
168
|
-
// 动画:共享姿态,一个 mixer 驱动整批(骨骼 LBS 与非骨骼节点动画都走这里)。
|
|
169
|
-
// 一个 batch 只对应一种动画(key = glb+动画),故 mixer 启动即播放该动画、整批同源同步。
|
|
170
|
-
// 仅当本 batch 的动画名(_clipName)存在且 GLB 自带剪辑才启动;否则渲染静态绑定姿态(bind pose)。
|
|
171
|
-
if (this._clipName && this.template.animRoot && this.template.animations.length) {
|
|
172
|
-
const clip = this.template.animations.find((c) => c.name === this._clipName) ||
|
|
173
|
-
this.template.animations[0];
|
|
174
|
-
if (clip) {
|
|
175
|
-
this._mixer = new AnimationMixer(this.template.animRoot);
|
|
176
|
-
const action = this._mixer.clipAction(clip);
|
|
177
|
-
action.play();
|
|
178
|
-
this._currentAction = action;
|
|
179
|
-
this._currentClip = clip.name;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
/** 每帧同步:children 为按 batchKey 收集的 InstancedChild 列表 */
|
|
184
|
-
sync(children, delta) {
|
|
185
|
-
if (!this.ready || !this.template) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const listChanged = this._children.length !== children.length ||
|
|
189
|
-
this._children.some((c, i) => c !== children[i]);
|
|
190
|
-
if (listChanged) {
|
|
191
|
-
this._children = children;
|
|
192
|
-
this.ensureCapacity(children.length);
|
|
193
|
-
this.compactWrite();
|
|
194
|
-
// 列表变化(含首帧就绪):重新同步碰撞标记——实例可能在创建后才改 fpsCollision,
|
|
195
|
-
// 用最新 children[0] 的标记覆盖,并在开启时通知 FPScontrol 重建碰撞列表。
|
|
196
|
-
this._applyCollisionFlag();
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
// 增量:只写 needUpdate 的实例,写完后立即置 false(避免重复写)
|
|
200
|
-
for (let i = 0; i < children.length; i++) {
|
|
201
|
-
const c = children[i];
|
|
202
|
-
if (c.needUpdate) {
|
|
203
|
-
this.writeInstance(i, c);
|
|
204
|
-
c.needUpdate = false;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// 动画(共享姿态):更新骨骼/节点 → 烘焙到几何
|
|
209
|
-
// 注意:本 batch 只对应一种动画(key 已含动画名),动作切换由「实例迁移到另一个常驻 batch」完成,
|
|
210
|
-
// 故这里不再做 batch 内的 mixer 切换,只需推进当前动画即可。
|
|
211
|
-
if (this._mixer && this.template.animRoot) {
|
|
212
|
-
this._mixer.update(delta);
|
|
213
|
-
this.template.animRoot.updateMatrixWorld(true); // 触发骨架/skeleton 计算
|
|
214
|
-
if (this.template.skeleton &&
|
|
215
|
-
this.template.sourceGeometry &&
|
|
216
|
-
this.template.sharedGeometry &&
|
|
217
|
-
this.template.bindMatrix &&
|
|
218
|
-
this.template.bindMatrixInverse) {
|
|
219
|
-
// 关键:mixer 已更新 bone 的世界矩阵,但骨骼矩阵(boneMatrices)需显式重算,
|
|
220
|
-
// 否则每帧都停在构造时的绑定姿态(士兵会变成静止 T-pose 甚至退化不可见)。
|
|
221
|
-
this.template.skeleton.update();
|
|
222
|
-
// 骨骼 LBS 烘焙:从 sourceGeometry(bind-pose)读、写到 sharedGeometry(渲染用),避免累积。
|
|
223
|
-
bakeSkinning(this.template.sourceGeometry, this.template.sharedGeometry, this.template.skeleton, this.template.bindMatrix, this.template.bindMatrixInverse);
|
|
224
|
-
}
|
|
225
|
-
else if (this._nodeBake.length) {
|
|
226
|
-
// 非骨骼节点动画:把每个节点相对根的世界变换烘焙进几何
|
|
227
|
-
const rootInv = _tmpMat.copy(this.template.animRoot.matrixWorld).invert();
|
|
228
|
-
for (const nb of this._nodeBake) {
|
|
229
|
-
_m.copy(nb.node.matrixWorld).multiply(rootInv);
|
|
230
|
-
bakeNodeTransform(nb.render, nb.local, _m);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
// 渲染数 = 当前子实例数
|
|
235
|
-
for (const im of this._meshes) {
|
|
236
|
-
im.count = children.length;
|
|
237
|
-
// ⚠️ 实例数量/矩阵变化后必须清空包围球:three 的 InstancedMesh.raycast 只在
|
|
238
|
-
// boundingSphere === null 时才重算,而它首次被计算时 count 往往还是 0
|
|
239
|
-
// (模板异步加载),算出的是「空球」radius = -1 并被永久缓存 →
|
|
240
|
-
// 后续所有射线检测(FPS 碰撞、鼠标 hover/click)在包围球剔除阶段就被全部跳过。
|
|
241
|
-
// 置 null 后由下一次 raycast 按需重算(本类 frustumCulled=false,不会每帧白算)。
|
|
242
|
-
im.boundingSphere = null;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
/** 容量不足时按 step 扩容(只扩不缩,避免频繁 realloc) */
|
|
246
|
-
ensureCapacity(n) {
|
|
247
|
-
if (n <= this._capacity) {
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
// 容量按 100 为一阶阶梯增长、只增不降:1 个对象→100 矩阵,101 个→200 矩阵,201 个→300 ...
|
|
251
|
-
// 避免每超出即整段 realloc,也避免从 200 回降到 100 造成反复分配/收缩。
|
|
252
|
-
const newCap = Math.ceil(n / 100) * 100;
|
|
253
|
-
for (const im of this._meshes) {
|
|
254
|
-
const imMat = new InstancedBufferAttribute(new Float32Array(newCap * 16), 16);
|
|
255
|
-
const imCol = new InstancedBufferAttribute(new Float32Array(newCap * 3).fill(1), 3);
|
|
256
|
-
for (let i = 0; i < im.count; i++) {
|
|
257
|
-
imMat.copyAt(i, im.instanceMatrix, i);
|
|
258
|
-
if (im.instanceColor) {
|
|
259
|
-
imCol.copyAt(i, im.instanceColor, i);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
im.instanceMatrix = imMat;
|
|
263
|
-
im.instanceColor = imCol;
|
|
264
|
-
}
|
|
265
|
-
this._capacity = newCap;
|
|
266
|
-
}
|
|
267
|
-
/** 全量重写所有实例(列表变化时调用) */
|
|
268
|
-
compactWrite() {
|
|
269
|
-
for (let i = 0; i < this._children.length; i++) {
|
|
270
|
-
this.writeInstance(i, this._children[i]);
|
|
271
|
-
this._children[i].needUpdate = false;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
/** 根据当前首批实例的 fpsCollision 标记,把真实 InstancedMesh 的 userData.fpsCollision 同步过去,
|
|
275
|
-
* 并在开启时通知所有 FPScontrol 重建碰撞列表。碰撞以整个 batch 为单位开关。 */
|
|
276
|
-
_applyCollisionFlag() {
|
|
277
|
-
const col = this._children.length > 0 && this._children[0].fpsCollision;
|
|
278
|
-
for (const im of this._meshes) {
|
|
279
|
-
im.userData.fpsCollision = col;
|
|
280
|
-
}
|
|
281
|
-
if (col) {
|
|
282
|
-
FPScontrol.markAllCollidersDirty();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
/** 把单个实例的 matrix + color 写入所有 submesh 的对应槽位 */
|
|
286
|
-
writeInstance(i, child) {
|
|
287
|
-
for (let k = 0; k < this._meshes.length; k++) {
|
|
288
|
-
const im = this._meshes[k];
|
|
289
|
-
const sub = this.template.submeshes[k];
|
|
290
|
-
// 叠加 GLB 节点层级的世界变换(如 Soldier.glb 根节点 scale=0.01),
|
|
291
|
-
// 使实例化渲染与"把整棵 GLB 场景图加进场景"一致(对齐 MeshBase 行为)。
|
|
292
|
-
_m.copy(child.matrixWorld);
|
|
293
|
-
if (sub.nodeWorldMatrix) {
|
|
294
|
-
_m.multiply(sub.nodeWorldMatrix);
|
|
295
|
-
}
|
|
296
|
-
im.setMatrixAt(i, _m);
|
|
297
|
-
im.instanceMatrix.needsUpdate = true;
|
|
298
|
-
if (im.instanceColor) {
|
|
299
|
-
im.setColorAt(i, child.color);
|
|
300
|
-
im.instanceColor.needsUpdate = true;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
/** 事件路由:给定 InstancedMesh 与 instanceId,返回对应 InstancedChild */
|
|
305
|
-
resolveInstance(mesh, instanceId) {
|
|
306
|
-
if (!mesh.userData.__batchKey || mesh.userData.__batchKey !== this.key) {
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
return this._children[instanceId] ?? null;
|
|
310
|
-
}
|
|
311
|
-
/** 该 batch 是否有任一 child 注册了指定类型监听(供 ThreeMouse 决定是否可拾取) */
|
|
312
|
-
hasListener(types) {
|
|
313
|
-
for (const c of this._children) {
|
|
314
|
-
const listeners = c._listeners;
|
|
315
|
-
if (listeners && types.some((t) => listeners[t] && listeners[t].length)) {
|
|
316
|
-
return true;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
/** 该 batch 是否有任一 child 设置了鼠标手势(cursor),供 InstancedGroup 决定是否收集 */
|
|
322
|
-
hasCursor() {
|
|
323
|
-
for (const c of this._children) {
|
|
324
|
-
if (c.userData && c.userData.cursor) {
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
330
|
-
/** 拾取用 InstancedMesh 列表 */
|
|
331
|
-
get pickables() {
|
|
332
|
-
return this._meshes;
|
|
333
|
-
}
|
|
334
|
-
dispose() {
|
|
335
|
-
for (const im of this._meshes) {
|
|
336
|
-
// 只释放 InstancedMesh 自带的实例缓冲(instanceMatrix / instanceColor),
|
|
337
|
-
// 不能 dispose geometry/material —— 它们来自 static geometry 或模板缓存,是共享资源,
|
|
338
|
-
// 一旦 dispose,后续重建同 batch 时会拿到已销毁的几何/材质导致渲染异常。
|
|
339
|
-
im.dispose();
|
|
340
|
-
this.group.remove(im);
|
|
341
|
-
}
|
|
342
|
-
this._meshes = [];
|
|
343
|
-
this._nodeBake = [];
|
|
344
|
-
if (this._mixer) {
|
|
345
|
-
this._mixer.stopAllAction();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
1
|
+
import { AnimationMixer, DynamicDrawUsage, Group, InstancedBufferAttribute, InstancedMesh, Matrix4, Vector3, } from "three";
|
|
2
|
+
import { loadTemplate } from "./InstancedTemplate";
|
|
3
|
+
import FPScontrol from "../FPScontrol";
|
|
4
|
+
const _m = new Matrix4();
|
|
5
|
+
const _v = new Vector3();
|
|
6
|
+
const _n = new Vector3();
|
|
7
|
+
const _skinMat = new Matrix4();
|
|
8
|
+
const _tmpMat = new Matrix4();
|
|
9
|
+
/**
|
|
10
|
+
* 把 skeleton 当前姿态烘焙进 geometry(线性混合蒙皮,LBS)。
|
|
11
|
+
* 由于同一 batch 内所有实例共享同一套骨骼矩阵(共享姿态),每帧只需烘焙一份几何,
|
|
12
|
+
* 所有实例复用 —— 这是把"GPU skinning + instancing"降级为"每帧 CPU 烘焙一份共享几何 + instancing"的关键,
|
|
13
|
+
* 复杂度从自定义 shader 降到几何更新,天然兼容项目所有材质与后处理。
|
|
14
|
+
*/
|
|
15
|
+
function bakeSkinning(srcGeom, dstGeom, skeleton, bindMatrix, bindMatrixInverse) {
|
|
16
|
+
// 从 srcGeom(原始 bind-pose,只读)逐顶点读取,计算结果写入 dstGeom(渲染用几何)。
|
|
17
|
+
// 绝不能读改写同一份几何 —— 否则上一帧烘焙结果被当成 bind-pose 再烤一遍,蒙皮累积错位。
|
|
18
|
+
const pos = srcGeom.attributes.position;
|
|
19
|
+
const norm = srcGeom.attributes.normal;
|
|
20
|
+
const si = srcGeom.attributes.skinIndex;
|
|
21
|
+
const sw = srcGeom.attributes.skinWeight;
|
|
22
|
+
const dpos = dstGeom.attributes.position;
|
|
23
|
+
const dnorm = dstGeom.attributes.normal;
|
|
24
|
+
if (!pos || !si || !sw || !dpos) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const boneMatrices = skeleton.boneMatrices; // Float32Array,长度 = bones * 16
|
|
28
|
+
if (!boneMatrices) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (let i = 0; i < pos.count; i++) {
|
|
32
|
+
// 与 three 着色器保持一致:先 bindMatrix 把顶点转到"骨骼局部坐标系",
|
|
33
|
+
// 再叠加骨骼混合矩阵,最后 bindMatrixInverse 回到模型空间。顺序反了动画姿态会错。
|
|
34
|
+
_v.fromBufferAttribute(pos, i).applyMatrix4(bindMatrix);
|
|
35
|
+
_skinMat.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
36
|
+
for (let j = 0; j < 4; j++) {
|
|
37
|
+
const w = sw.getComponent(i, j);
|
|
38
|
+
if (w === 0) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const bi = si.getComponent(i, j);
|
|
42
|
+
_tmpMat.fromArray(boneMatrices, bi * 16);
|
|
43
|
+
_tmpMat.multiplyScalar(w);
|
|
44
|
+
const se = _skinMat.elements;
|
|
45
|
+
const te = _tmpMat.elements;
|
|
46
|
+
for (let k = 0; k < 16; k++) {
|
|
47
|
+
se[k] += te[k];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
_v.applyMatrix4(_skinMat).applyMatrix4(bindMatrixInverse);
|
|
51
|
+
dpos.setXYZ(i, _v.x, _v.y, _v.z);
|
|
52
|
+
if (norm && dnorm) {
|
|
53
|
+
_n.fromBufferAttribute(norm, i).applyMatrix4(_skinMat).normalize();
|
|
54
|
+
dnorm.setXYZ(i, _n.x, _n.y, _n.z);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
dpos.needsUpdate = true;
|
|
58
|
+
if (dnorm) {
|
|
59
|
+
dnorm.needsUpdate = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 把节点相对根的世界变换烘焙进几何(非骨骼节点动画的实例化方案)。
|
|
64
|
+
* 与骨骼 LBS 同理:把"节点层级变换"降级为每帧 CPU 几何变换,
|
|
65
|
+
* 所有实例复用一份变换后的几何,天然兼容项目所有材质与后处理。
|
|
66
|
+
*/
|
|
67
|
+
function bakeNodeTransform(geometry, local, m) {
|
|
68
|
+
const pos = geometry.attributes.position;
|
|
69
|
+
const lp = local.attributes.position;
|
|
70
|
+
if (!pos || !lp) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (let i = 0; i < lp.count; i++) {
|
|
74
|
+
_v.fromBufferAttribute(lp, i).applyMatrix4(m);
|
|
75
|
+
pos.setXYZ(i, _v.x, _v.y, _v.z);
|
|
76
|
+
}
|
|
77
|
+
pos.needsUpdate = true;
|
|
78
|
+
const norm = geometry.attributes.normal;
|
|
79
|
+
const ln = local.attributes.normal;
|
|
80
|
+
if (norm && ln) {
|
|
81
|
+
for (let i = 0; i < ln.count; i++) {
|
|
82
|
+
_n.fromBufferAttribute(ln, i).applyMatrix4(m).normalize();
|
|
83
|
+
norm.setXYZ(i, _n.x, _n.y, _n.z);
|
|
84
|
+
}
|
|
85
|
+
norm.needsUpdate = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 内部类:封装一个 batch 的真实 InstancedMesh(多 submesh 时每个 submesh 一个,共享同一组实例序号)。
|
|
90
|
+
* - 按 batchKey 分组(相同 geometry+material 或相同 instanceUrl+animation 为一类);
|
|
91
|
+
* - 增量同步:仅 needUpdate=true 的实例才重写 matrix/color,写完后立即置 false;
|
|
92
|
+
* - 列表变化(增删/重排)时全量 compact 重写并重建索引;
|
|
93
|
+
* - 含骨骼动画时:用共享姿态 AnimationMixer 驱动 skeleton,每帧烘焙到共享几何(LBS);
|
|
94
|
+
* - 含非骨骼节点动画时:同样用共享姿态 AnimationMixer 驱动节点变换,每帧把"节点相对根的世界变换"
|
|
95
|
+
* 烘焙进几何(节点变换动画的实例化方案)。
|
|
96
|
+
*/
|
|
97
|
+
export default class InstancedBatch {
|
|
98
|
+
key;
|
|
99
|
+
/** 渲染组:持有本 batch 的 InstancedMesh,由 InstancedGroup add 进 scene */
|
|
100
|
+
group = new Group();
|
|
101
|
+
template;
|
|
102
|
+
ready = false;
|
|
103
|
+
_children = [];
|
|
104
|
+
_meshes = [];
|
|
105
|
+
_capacity;
|
|
106
|
+
_step;
|
|
107
|
+
_loading;
|
|
108
|
+
_mixer;
|
|
109
|
+
_clipName;
|
|
110
|
+
/** 当前正在播放的动画剪辑名(运行时可由 child.playClip 切换) */
|
|
111
|
+
_currentClip;
|
|
112
|
+
/** 首例实例是否声明碰撞(构造时快照,buildMeshes 用它给 InstancedMesh 打标记) */
|
|
113
|
+
_sampleFpsCollision = false;
|
|
114
|
+
/** 当前正在播放的 AnimationAction(切换时先 stop 旧的) */
|
|
115
|
+
_currentAction;
|
|
116
|
+
/** 非骨骼动画:需每帧烘焙节点变换的几何列表(与 _meshes 顺序对齐) */
|
|
117
|
+
_nodeBake = [];
|
|
118
|
+
constructor(key, sample, capacity = 100, step = 100) {
|
|
119
|
+
this.key = key;
|
|
120
|
+
this._capacity = capacity;
|
|
121
|
+
this._step = step;
|
|
122
|
+
// 用实例当前生效动画(playClip 优先,否则 static animation)构建本 batch 的 mixer;
|
|
123
|
+
// 因 key 已含动画名,相同 glb+动画的实例都进同一个常驻 batch
|
|
124
|
+
this._clipName = sample.getEffectiveAnimation();
|
|
125
|
+
this._sampleFpsCollision = !!sample.fpsCollision;
|
|
126
|
+
this._loading = loadTemplate(sample).then((t) => {
|
|
127
|
+
const ctor = sample.constructor;
|
|
128
|
+
// GLB 模板就绪后,交给子类按需修正材质(如瓶子 metalness 修正)
|
|
129
|
+
if (typeof ctor.prepareTemplate === "function") {
|
|
130
|
+
ctor.prepareTemplate(t);
|
|
131
|
+
}
|
|
132
|
+
this.template = t;
|
|
133
|
+
this.buildMeshes();
|
|
134
|
+
this.ready = true;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** 模板就绪后,为每个 submesh 建一个 InstancedMesh,并启动动画(若有) */
|
|
138
|
+
buildMeshes() {
|
|
139
|
+
if (!this.template) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
for (const sub of this.template.submeshes) {
|
|
143
|
+
const geom = sub.skinned && this.template.sharedGeometry
|
|
144
|
+
? this.template.sharedGeometry
|
|
145
|
+
: sub.geometry;
|
|
146
|
+
const im = new InstancedMesh(geom, sub.material, this._capacity);
|
|
147
|
+
im.frustumCulled = false;
|
|
148
|
+
im.instanceMatrix.setUsage(DynamicDrawUsage);
|
|
149
|
+
// 预创建 instanceColor(默认白,setColorAt 会覆盖)
|
|
150
|
+
im.instanceColor = new InstancedBufferAttribute(new Float32Array(this._capacity * 3).fill(1), 3);
|
|
151
|
+
im.count = 0;
|
|
152
|
+
im.userData.__batchKey = this.key;
|
|
153
|
+
// 若本 batch 的实例声明了碰撞检测,则把真实 InstancedMesh 标记为碰撞体,
|
|
154
|
+
// 供 FPScontrol 的碰撞射线命中(实例化对象的几何合并在 InstancedMesh 里,只能整体标记)。
|
|
155
|
+
im.userData.fpsCollision = this._sampleFpsCollision;
|
|
156
|
+
this._meshes.push(im);
|
|
157
|
+
this.group.add(im);
|
|
158
|
+
// 非骨骼动画:记录节点烘焙信息,每帧把节点相对根的世界变换写进几何
|
|
159
|
+
if (sub.node && this.template.animRoot && !this.template.skeleton && sub.localGeometry) {
|
|
160
|
+
this._nodeBake.push({ node: sub.node, local: sub.localGeometry, render: sub.geometry });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// 模板就绪即标记碰撞体,并通知所有 FPScontrol 重新收集(模板是异步加载的,
|
|
164
|
+
// 此时控制器多半已先创建,必须主动通知它重建碰撞列表,否则实例化碰撞体不生效)。
|
|
165
|
+
if (this._sampleFpsCollision) {
|
|
166
|
+
FPScontrol.markAllCollidersDirty();
|
|
167
|
+
}
|
|
168
|
+
// 动画:共享姿态,一个 mixer 驱动整批(骨骼 LBS 与非骨骼节点动画都走这里)。
|
|
169
|
+
// 一个 batch 只对应一种动画(key = glb+动画),故 mixer 启动即播放该动画、整批同源同步。
|
|
170
|
+
// 仅当本 batch 的动画名(_clipName)存在且 GLB 自带剪辑才启动;否则渲染静态绑定姿态(bind pose)。
|
|
171
|
+
if (this._clipName && this.template.animRoot && this.template.animations.length) {
|
|
172
|
+
const clip = this.template.animations.find((c) => c.name === this._clipName) ||
|
|
173
|
+
this.template.animations[0];
|
|
174
|
+
if (clip) {
|
|
175
|
+
this._mixer = new AnimationMixer(this.template.animRoot);
|
|
176
|
+
const action = this._mixer.clipAction(clip);
|
|
177
|
+
action.play();
|
|
178
|
+
this._currentAction = action;
|
|
179
|
+
this._currentClip = clip.name;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** 每帧同步:children 为按 batchKey 收集的 InstancedChild 列表 */
|
|
184
|
+
sync(children, delta) {
|
|
185
|
+
if (!this.ready || !this.template) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const listChanged = this._children.length !== children.length ||
|
|
189
|
+
this._children.some((c, i) => c !== children[i]);
|
|
190
|
+
if (listChanged) {
|
|
191
|
+
this._children = children;
|
|
192
|
+
this.ensureCapacity(children.length);
|
|
193
|
+
this.compactWrite();
|
|
194
|
+
// 列表变化(含首帧就绪):重新同步碰撞标记——实例可能在创建后才改 fpsCollision,
|
|
195
|
+
// 用最新 children[0] 的标记覆盖,并在开启时通知 FPScontrol 重建碰撞列表。
|
|
196
|
+
this._applyCollisionFlag();
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// 增量:只写 needUpdate 的实例,写完后立即置 false(避免重复写)
|
|
200
|
+
for (let i = 0; i < children.length; i++) {
|
|
201
|
+
const c = children[i];
|
|
202
|
+
if (c.needUpdate) {
|
|
203
|
+
this.writeInstance(i, c);
|
|
204
|
+
c.needUpdate = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// 动画(共享姿态):更新骨骼/节点 → 烘焙到几何
|
|
209
|
+
// 注意:本 batch 只对应一种动画(key 已含动画名),动作切换由「实例迁移到另一个常驻 batch」完成,
|
|
210
|
+
// 故这里不再做 batch 内的 mixer 切换,只需推进当前动画即可。
|
|
211
|
+
if (this._mixer && this.template.animRoot) {
|
|
212
|
+
this._mixer.update(delta);
|
|
213
|
+
this.template.animRoot.updateMatrixWorld(true); // 触发骨架/skeleton 计算
|
|
214
|
+
if (this.template.skeleton &&
|
|
215
|
+
this.template.sourceGeometry &&
|
|
216
|
+
this.template.sharedGeometry &&
|
|
217
|
+
this.template.bindMatrix &&
|
|
218
|
+
this.template.bindMatrixInverse) {
|
|
219
|
+
// 关键:mixer 已更新 bone 的世界矩阵,但骨骼矩阵(boneMatrices)需显式重算,
|
|
220
|
+
// 否则每帧都停在构造时的绑定姿态(士兵会变成静止 T-pose 甚至退化不可见)。
|
|
221
|
+
this.template.skeleton.update();
|
|
222
|
+
// 骨骼 LBS 烘焙:从 sourceGeometry(bind-pose)读、写到 sharedGeometry(渲染用),避免累积。
|
|
223
|
+
bakeSkinning(this.template.sourceGeometry, this.template.sharedGeometry, this.template.skeleton, this.template.bindMatrix, this.template.bindMatrixInverse);
|
|
224
|
+
}
|
|
225
|
+
else if (this._nodeBake.length) {
|
|
226
|
+
// 非骨骼节点动画:把每个节点相对根的世界变换烘焙进几何
|
|
227
|
+
const rootInv = _tmpMat.copy(this.template.animRoot.matrixWorld).invert();
|
|
228
|
+
for (const nb of this._nodeBake) {
|
|
229
|
+
_m.copy(nb.node.matrixWorld).multiply(rootInv);
|
|
230
|
+
bakeNodeTransform(nb.render, nb.local, _m);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// 渲染数 = 当前子实例数
|
|
235
|
+
for (const im of this._meshes) {
|
|
236
|
+
im.count = children.length;
|
|
237
|
+
// ⚠️ 实例数量/矩阵变化后必须清空包围球:three 的 InstancedMesh.raycast 只在
|
|
238
|
+
// boundingSphere === null 时才重算,而它首次被计算时 count 往往还是 0
|
|
239
|
+
// (模板异步加载),算出的是「空球」radius = -1 并被永久缓存 →
|
|
240
|
+
// 后续所有射线检测(FPS 碰撞、鼠标 hover/click)在包围球剔除阶段就被全部跳过。
|
|
241
|
+
// 置 null 后由下一次 raycast 按需重算(本类 frustumCulled=false,不会每帧白算)。
|
|
242
|
+
im.boundingSphere = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** 容量不足时按 step 扩容(只扩不缩,避免频繁 realloc) */
|
|
246
|
+
ensureCapacity(n) {
|
|
247
|
+
if (n <= this._capacity) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// 容量按 100 为一阶阶梯增长、只增不降:1 个对象→100 矩阵,101 个→200 矩阵,201 个→300 ...
|
|
251
|
+
// 避免每超出即整段 realloc,也避免从 200 回降到 100 造成反复分配/收缩。
|
|
252
|
+
const newCap = Math.ceil(n / 100) * 100;
|
|
253
|
+
for (const im of this._meshes) {
|
|
254
|
+
const imMat = new InstancedBufferAttribute(new Float32Array(newCap * 16), 16);
|
|
255
|
+
const imCol = new InstancedBufferAttribute(new Float32Array(newCap * 3).fill(1), 3);
|
|
256
|
+
for (let i = 0; i < im.count; i++) {
|
|
257
|
+
imMat.copyAt(i, im.instanceMatrix, i);
|
|
258
|
+
if (im.instanceColor) {
|
|
259
|
+
imCol.copyAt(i, im.instanceColor, i);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
im.instanceMatrix = imMat;
|
|
263
|
+
im.instanceColor = imCol;
|
|
264
|
+
}
|
|
265
|
+
this._capacity = newCap;
|
|
266
|
+
}
|
|
267
|
+
/** 全量重写所有实例(列表变化时调用) */
|
|
268
|
+
compactWrite() {
|
|
269
|
+
for (let i = 0; i < this._children.length; i++) {
|
|
270
|
+
this.writeInstance(i, this._children[i]);
|
|
271
|
+
this._children[i].needUpdate = false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** 根据当前首批实例的 fpsCollision 标记,把真实 InstancedMesh 的 userData.fpsCollision 同步过去,
|
|
275
|
+
* 并在开启时通知所有 FPScontrol 重建碰撞列表。碰撞以整个 batch 为单位开关。 */
|
|
276
|
+
_applyCollisionFlag() {
|
|
277
|
+
const col = this._children.length > 0 && this._children[0].fpsCollision;
|
|
278
|
+
for (const im of this._meshes) {
|
|
279
|
+
im.userData.fpsCollision = col;
|
|
280
|
+
}
|
|
281
|
+
if (col) {
|
|
282
|
+
FPScontrol.markAllCollidersDirty();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** 把单个实例的 matrix + color 写入所有 submesh 的对应槽位 */
|
|
286
|
+
writeInstance(i, child) {
|
|
287
|
+
for (let k = 0; k < this._meshes.length; k++) {
|
|
288
|
+
const im = this._meshes[k];
|
|
289
|
+
const sub = this.template.submeshes[k];
|
|
290
|
+
// 叠加 GLB 节点层级的世界变换(如 Soldier.glb 根节点 scale=0.01),
|
|
291
|
+
// 使实例化渲染与"把整棵 GLB 场景图加进场景"一致(对齐 MeshBase 行为)。
|
|
292
|
+
_m.copy(child.matrixWorld);
|
|
293
|
+
if (sub.nodeWorldMatrix) {
|
|
294
|
+
_m.multiply(sub.nodeWorldMatrix);
|
|
295
|
+
}
|
|
296
|
+
im.setMatrixAt(i, _m);
|
|
297
|
+
im.instanceMatrix.needsUpdate = true;
|
|
298
|
+
if (im.instanceColor) {
|
|
299
|
+
im.setColorAt(i, child.color);
|
|
300
|
+
im.instanceColor.needsUpdate = true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** 事件路由:给定 InstancedMesh 与 instanceId,返回对应 InstancedChild */
|
|
305
|
+
resolveInstance(mesh, instanceId) {
|
|
306
|
+
if (!mesh.userData.__batchKey || mesh.userData.__batchKey !== this.key) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return this._children[instanceId] ?? null;
|
|
310
|
+
}
|
|
311
|
+
/** 该 batch 是否有任一 child 注册了指定类型监听(供 ThreeMouse 决定是否可拾取) */
|
|
312
|
+
hasListener(types) {
|
|
313
|
+
for (const c of this._children) {
|
|
314
|
+
const listeners = c._listeners;
|
|
315
|
+
if (listeners && types.some((t) => listeners[t] && listeners[t].length)) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
/** 该 batch 是否有任一 child 设置了鼠标手势(cursor),供 InstancedGroup 决定是否收集 */
|
|
322
|
+
hasCursor() {
|
|
323
|
+
for (const c of this._children) {
|
|
324
|
+
if (c.userData && c.userData.cursor) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
/** 拾取用 InstancedMesh 列表 */
|
|
331
|
+
get pickables() {
|
|
332
|
+
return this._meshes;
|
|
333
|
+
}
|
|
334
|
+
dispose() {
|
|
335
|
+
for (const im of this._meshes) {
|
|
336
|
+
// 只释放 InstancedMesh 自带的实例缓冲(instanceMatrix / instanceColor),
|
|
337
|
+
// 不能 dispose geometry/material —— 它们来自 static geometry 或模板缓存,是共享资源,
|
|
338
|
+
// 一旦 dispose,后续重建同 batch 时会拿到已销毁的几何/材质导致渲染异常。
|
|
339
|
+
im.dispose();
|
|
340
|
+
this.group.remove(im);
|
|
341
|
+
}
|
|
342
|
+
this._meshes = [];
|
|
343
|
+
this._nodeBake = [];
|
|
344
|
+
if (this._mixer) {
|
|
345
|
+
this._mixer.stopAllAction();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|