skinview3d-node 3.4.2-node.1
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/LICENSE +22 -0
- package/README.md +210 -0
- package/assets/minecraft.woff2 +0 -0
- package/libs/animation.d.ts +469 -0
- package/libs/model.d.ts +76 -0
- package/libs/nametag.d.ts +89 -0
- package/libs/skinview3d.cjs +2922 -0
- package/libs/skinview3d.d.ts +4 -0
- package/libs/skinview3d.mjs +2898 -0
- package/libs/utils/index.d.ts +3 -0
- package/libs/utils/load-image.d.ts +7 -0
- package/libs/utils/process.d.ts +7 -0
- package/libs/utils/types.d.ts +5 -0
- package/libs/viewer.d.ts +328 -0
- package/package.json +78 -0
|
@@ -0,0 +1,2898 @@
|
|
|
1
|
+
import { Image, Canvas, FontLibrary } from 'skia-canvas';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { Group, MeshStandardMaterial, FrontSide, DoubleSide, BoxGeometry, Mesh, Vector2, DataTexture, RGBAFormat, OrthographicCamera, BufferGeometry, Float32BufferAttribute, ShaderMaterial, UniformsUtils, WebGLRenderTarget, HalfFloatType, NoBlending, Clock, Color, Sprite, SpriteMaterial, NearestFilter, AmbientLight, PointLight, Scene, PerspectiveCamera, ColorManagement, WebGLRenderer, LinearFilter, UnsignedByteType, EquirectangularReflectionMapping, Texture, Object3D, Quaternion, Vector3 } from 'three';
|
|
5
|
+
import { PNG } from 'pngjs';
|
|
6
|
+
import gl from 'gl';
|
|
7
|
+
|
|
8
|
+
function setUVs(box, u, v, width, height, depth, textureWidth, textureHeight) {
|
|
9
|
+
const toFaceVertices = (x1, y1, x2, y2) => [
|
|
10
|
+
new Vector2(x1 / textureWidth, 1.0 - y2 / textureHeight),
|
|
11
|
+
new Vector2(x2 / textureWidth, 1.0 - y2 / textureHeight),
|
|
12
|
+
new Vector2(x2 / textureWidth, 1.0 - y1 / textureHeight),
|
|
13
|
+
new Vector2(x1 / textureWidth, 1.0 - y1 / textureHeight)
|
|
14
|
+
];
|
|
15
|
+
const top = toFaceVertices(u + depth, v, u + width + depth, v + depth);
|
|
16
|
+
const bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth);
|
|
17
|
+
const left = toFaceVertices(u, v + depth, u + depth, v + depth + height);
|
|
18
|
+
const front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height);
|
|
19
|
+
const right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth);
|
|
20
|
+
const back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth);
|
|
21
|
+
const uvAttr = box.attributes.uv;
|
|
22
|
+
const uvRight = [right[3], right[2], right[0], right[1]];
|
|
23
|
+
const uvLeft = [left[3], left[2], left[0], left[1]];
|
|
24
|
+
const uvTop = [top[3], top[2], top[0], top[1]];
|
|
25
|
+
const uvBottom = [bottom[0], bottom[1], bottom[3], bottom[2]];
|
|
26
|
+
const uvFront = [front[3], front[2], front[0], front[1]];
|
|
27
|
+
const uvBack = [back[3], back[2], back[0], back[1]];
|
|
28
|
+
// Create a new array to hold the modified UV data
|
|
29
|
+
const newUVData = [];
|
|
30
|
+
// Iterate over the arrays and copy the data to uvData
|
|
31
|
+
for (const uvArray of [uvRight, uvLeft, uvTop, uvBottom, uvFront, uvBack]) {
|
|
32
|
+
for (const uv of uvArray) {
|
|
33
|
+
newUVData.push(uv.x, uv.y);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
uvAttr.set(new Float32Array(newUVData));
|
|
37
|
+
uvAttr.needsUpdate = true;
|
|
38
|
+
}
|
|
39
|
+
function setSkinUVs(box, u, v, width, height, depth) {
|
|
40
|
+
setUVs(box, u, v, width, height, depth, 64, 64);
|
|
41
|
+
}
|
|
42
|
+
function setCapeUVs(box, u, v, width, height, depth) {
|
|
43
|
+
setUVs(box, u, v, width, height, depth, 64, 32);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Notice that innerLayer and outerLayer may NOT be the direct children of the Group.
|
|
47
|
+
*/
|
|
48
|
+
class BodyPart extends Group {
|
|
49
|
+
innerLayer;
|
|
50
|
+
outerLayer;
|
|
51
|
+
constructor(innerLayer, outerLayer) {
|
|
52
|
+
super();
|
|
53
|
+
this.innerLayer = innerLayer;
|
|
54
|
+
this.outerLayer = outerLayer;
|
|
55
|
+
innerLayer.name = 'inner';
|
|
56
|
+
outerLayer.name = 'outer';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
class SkinObject extends Group {
|
|
60
|
+
// body parts
|
|
61
|
+
head;
|
|
62
|
+
body;
|
|
63
|
+
rightArm;
|
|
64
|
+
leftArm;
|
|
65
|
+
rightLeg;
|
|
66
|
+
leftLeg;
|
|
67
|
+
modelListeners = []; // called when model(slim property) is changed
|
|
68
|
+
slim = false;
|
|
69
|
+
_map = null;
|
|
70
|
+
layer1Material;
|
|
71
|
+
layer1MaterialBiased;
|
|
72
|
+
layer2Material;
|
|
73
|
+
layer2MaterialBiased;
|
|
74
|
+
constructor() {
|
|
75
|
+
super();
|
|
76
|
+
this.layer1Material = new MeshStandardMaterial({
|
|
77
|
+
side: FrontSide
|
|
78
|
+
});
|
|
79
|
+
this.layer2Material = new MeshStandardMaterial({
|
|
80
|
+
side: DoubleSide,
|
|
81
|
+
transparent: true,
|
|
82
|
+
alphaTest: 1e-5
|
|
83
|
+
});
|
|
84
|
+
this.layer1MaterialBiased = this.layer1Material.clone();
|
|
85
|
+
this.layer1MaterialBiased.polygonOffset = true;
|
|
86
|
+
this.layer1MaterialBiased.polygonOffsetFactor = 1.0;
|
|
87
|
+
this.layer1MaterialBiased.polygonOffsetUnits = 1.0;
|
|
88
|
+
this.layer2MaterialBiased = this.layer2Material.clone();
|
|
89
|
+
this.layer2MaterialBiased.polygonOffset = true;
|
|
90
|
+
this.layer2MaterialBiased.polygonOffsetFactor = 1.0;
|
|
91
|
+
this.layer2MaterialBiased.polygonOffsetUnits = 1.0;
|
|
92
|
+
// Head
|
|
93
|
+
const headBox = new BoxGeometry(8, 8, 8);
|
|
94
|
+
setSkinUVs(headBox, 0, 0, 8, 8, 8);
|
|
95
|
+
const headMesh = new Mesh(headBox, this.layer1Material);
|
|
96
|
+
const head2Box = new BoxGeometry(9, 9, 9);
|
|
97
|
+
setSkinUVs(head2Box, 32, 0, 8, 8, 8);
|
|
98
|
+
const head2Mesh = new Mesh(head2Box, this.layer2Material);
|
|
99
|
+
this.head = new BodyPart(headMesh, head2Mesh);
|
|
100
|
+
this.head.name = 'head';
|
|
101
|
+
this.head.add(headMesh, head2Mesh);
|
|
102
|
+
headMesh.position.y = 4;
|
|
103
|
+
head2Mesh.position.y = 4;
|
|
104
|
+
this.add(this.head);
|
|
105
|
+
// Body
|
|
106
|
+
const bodyBox = new BoxGeometry(8, 12, 4);
|
|
107
|
+
setSkinUVs(bodyBox, 16, 16, 8, 12, 4);
|
|
108
|
+
const bodyMesh = new Mesh(bodyBox, this.layer1Material);
|
|
109
|
+
const body2Box = new BoxGeometry(8.5, 12.5, 4.5);
|
|
110
|
+
setSkinUVs(body2Box, 16, 32, 8, 12, 4);
|
|
111
|
+
const body2Mesh = new Mesh(body2Box, this.layer2Material);
|
|
112
|
+
this.body = new BodyPart(bodyMesh, body2Mesh);
|
|
113
|
+
this.body.name = 'body';
|
|
114
|
+
this.body.add(bodyMesh, body2Mesh);
|
|
115
|
+
this.body.position.y = -6;
|
|
116
|
+
this.add(this.body);
|
|
117
|
+
// Right Arm
|
|
118
|
+
const rightArmBox = new BoxGeometry();
|
|
119
|
+
const rightArmMesh = new Mesh(rightArmBox, this.layer1MaterialBiased);
|
|
120
|
+
this.modelListeners.push(() => {
|
|
121
|
+
rightArmMesh.scale.x = this.slim ? 3 : 4;
|
|
122
|
+
rightArmMesh.scale.y = 12;
|
|
123
|
+
rightArmMesh.scale.z = 4;
|
|
124
|
+
setSkinUVs(rightArmBox, 40, 16, this.slim ? 3 : 4, 12, 4);
|
|
125
|
+
});
|
|
126
|
+
const rightArm2Box = new BoxGeometry();
|
|
127
|
+
const rightArm2Mesh = new Mesh(rightArm2Box, this.layer2MaterialBiased);
|
|
128
|
+
this.modelListeners.push(() => {
|
|
129
|
+
rightArm2Mesh.scale.x = this.slim ? 3.5 : 4.5;
|
|
130
|
+
rightArm2Mesh.scale.y = 12.5;
|
|
131
|
+
rightArm2Mesh.scale.z = 4.5;
|
|
132
|
+
setSkinUVs(rightArm2Box, 40, 32, this.slim ? 3 : 4, 12, 4);
|
|
133
|
+
});
|
|
134
|
+
const rightArmPivot = new Group();
|
|
135
|
+
rightArmPivot.add(rightArmMesh, rightArm2Mesh);
|
|
136
|
+
this.modelListeners.push(() => {
|
|
137
|
+
rightArmPivot.position.x = this.slim ? -0.5 : -1;
|
|
138
|
+
});
|
|
139
|
+
rightArmPivot.position.y = -4;
|
|
140
|
+
this.rightArm = new BodyPart(rightArmMesh, rightArm2Mesh);
|
|
141
|
+
this.rightArm.name = 'rightArm';
|
|
142
|
+
this.rightArm.add(rightArmPivot);
|
|
143
|
+
this.rightArm.position.x = -5;
|
|
144
|
+
this.rightArm.position.y = -2;
|
|
145
|
+
this.add(this.rightArm);
|
|
146
|
+
// Left Arm
|
|
147
|
+
const leftArmBox = new BoxGeometry();
|
|
148
|
+
const leftArmMesh = new Mesh(leftArmBox, this.layer1MaterialBiased);
|
|
149
|
+
this.modelListeners.push(() => {
|
|
150
|
+
leftArmMesh.scale.x = this.slim ? 3 : 4;
|
|
151
|
+
leftArmMesh.scale.y = 12;
|
|
152
|
+
leftArmMesh.scale.z = 4;
|
|
153
|
+
setSkinUVs(leftArmBox, 32, 48, this.slim ? 3 : 4, 12, 4);
|
|
154
|
+
});
|
|
155
|
+
const leftArm2Box = new BoxGeometry();
|
|
156
|
+
const leftArm2Mesh = new Mesh(leftArm2Box, this.layer2MaterialBiased);
|
|
157
|
+
this.modelListeners.push(() => {
|
|
158
|
+
leftArm2Mesh.scale.x = this.slim ? 3.5 : 4.5;
|
|
159
|
+
leftArm2Mesh.scale.y = 12.5;
|
|
160
|
+
leftArm2Mesh.scale.z = 4.5;
|
|
161
|
+
setSkinUVs(leftArm2Box, 48, 48, this.slim ? 3 : 4, 12, 4);
|
|
162
|
+
});
|
|
163
|
+
const leftArmPivot = new Group();
|
|
164
|
+
leftArmPivot.add(leftArmMesh, leftArm2Mesh);
|
|
165
|
+
this.modelListeners.push(() => {
|
|
166
|
+
leftArmPivot.position.x = this.slim ? 0.5 : 1;
|
|
167
|
+
});
|
|
168
|
+
leftArmPivot.position.y = -4;
|
|
169
|
+
this.leftArm = new BodyPart(leftArmMesh, leftArm2Mesh);
|
|
170
|
+
this.leftArm.name = 'leftArm';
|
|
171
|
+
this.leftArm.add(leftArmPivot);
|
|
172
|
+
this.leftArm.position.x = 5;
|
|
173
|
+
this.leftArm.position.y = -2;
|
|
174
|
+
this.add(this.leftArm);
|
|
175
|
+
// Right Leg
|
|
176
|
+
const rightLegBox = new BoxGeometry(4, 12, 4);
|
|
177
|
+
setSkinUVs(rightLegBox, 0, 16, 4, 12, 4);
|
|
178
|
+
const rightLegMesh = new Mesh(rightLegBox, this.layer1MaterialBiased);
|
|
179
|
+
const rightLeg2Box = new BoxGeometry(4.5, 12.5, 4.5);
|
|
180
|
+
setSkinUVs(rightLeg2Box, 0, 32, 4, 12, 4);
|
|
181
|
+
const rightLeg2Mesh = new Mesh(rightLeg2Box, this.layer2MaterialBiased);
|
|
182
|
+
const rightLegPivot = new Group();
|
|
183
|
+
rightLegPivot.add(rightLegMesh, rightLeg2Mesh);
|
|
184
|
+
rightLegPivot.position.y = -6;
|
|
185
|
+
this.rightLeg = new BodyPart(rightLegMesh, rightLeg2Mesh);
|
|
186
|
+
this.rightLeg.name = 'rightLeg';
|
|
187
|
+
this.rightLeg.add(rightLegPivot);
|
|
188
|
+
this.rightLeg.position.x = -1.9;
|
|
189
|
+
this.rightLeg.position.y = -12;
|
|
190
|
+
this.rightLeg.position.z = -0.1;
|
|
191
|
+
this.add(this.rightLeg);
|
|
192
|
+
// Left Leg
|
|
193
|
+
const leftLegBox = new BoxGeometry(4, 12, 4);
|
|
194
|
+
setSkinUVs(leftLegBox, 16, 48, 4, 12, 4);
|
|
195
|
+
const leftLegMesh = new Mesh(leftLegBox, this.layer1MaterialBiased);
|
|
196
|
+
const leftLeg2Box = new BoxGeometry(4.5, 12.5, 4.5);
|
|
197
|
+
setSkinUVs(leftLeg2Box, 0, 48, 4, 12, 4);
|
|
198
|
+
const leftLeg2Mesh = new Mesh(leftLeg2Box, this.layer2MaterialBiased);
|
|
199
|
+
const leftLegPivot = new Group();
|
|
200
|
+
leftLegPivot.add(leftLegMesh, leftLeg2Mesh);
|
|
201
|
+
leftLegPivot.position.y = -6;
|
|
202
|
+
this.leftLeg = new BodyPart(leftLegMesh, leftLeg2Mesh);
|
|
203
|
+
this.leftLeg.name = 'leftLeg';
|
|
204
|
+
this.leftLeg.add(leftLegPivot);
|
|
205
|
+
this.leftLeg.position.x = 1.9;
|
|
206
|
+
this.leftLeg.position.y = -12;
|
|
207
|
+
this.leftLeg.position.z = -0.1;
|
|
208
|
+
this.add(this.leftLeg);
|
|
209
|
+
this.modelType = 'default';
|
|
210
|
+
}
|
|
211
|
+
get map() {
|
|
212
|
+
return this._map;
|
|
213
|
+
}
|
|
214
|
+
set map(newMap) {
|
|
215
|
+
this._map = newMap;
|
|
216
|
+
this.layer1Material.map = newMap;
|
|
217
|
+
this.layer1Material.needsUpdate = true;
|
|
218
|
+
this.layer1MaterialBiased.map = newMap;
|
|
219
|
+
this.layer1MaterialBiased.needsUpdate = true;
|
|
220
|
+
this.layer2Material.map = newMap;
|
|
221
|
+
this.layer2Material.needsUpdate = true;
|
|
222
|
+
this.layer2MaterialBiased.map = newMap;
|
|
223
|
+
this.layer2MaterialBiased.needsUpdate = true;
|
|
224
|
+
}
|
|
225
|
+
get modelType() {
|
|
226
|
+
return this.slim ? 'slim' : 'default';
|
|
227
|
+
}
|
|
228
|
+
set modelType(value) {
|
|
229
|
+
this.slim = value === 'slim';
|
|
230
|
+
this.modelListeners.forEach(listener => listener());
|
|
231
|
+
}
|
|
232
|
+
getBodyParts() {
|
|
233
|
+
return this.children.filter(it => it instanceof BodyPart);
|
|
234
|
+
}
|
|
235
|
+
setInnerLayerVisible(value) {
|
|
236
|
+
this.getBodyParts().forEach(part => (part.innerLayer.visible = value));
|
|
237
|
+
}
|
|
238
|
+
setOuterLayerVisible(value) {
|
|
239
|
+
this.getBodyParts().forEach(part => (part.outerLayer.visible = value));
|
|
240
|
+
}
|
|
241
|
+
resetJoints() {
|
|
242
|
+
this.head.rotation.set(0, 0, 0);
|
|
243
|
+
this.leftArm.rotation.set(0, 0, 0);
|
|
244
|
+
this.rightArm.rotation.set(0, 0, 0);
|
|
245
|
+
this.leftLeg.rotation.set(0, 0, 0);
|
|
246
|
+
this.rightLeg.rotation.set(0, 0, 0);
|
|
247
|
+
this.body.rotation.set(0, 0, 0);
|
|
248
|
+
this.head.position.y = 0;
|
|
249
|
+
this.body.position.y = -6;
|
|
250
|
+
this.body.position.z = 0;
|
|
251
|
+
this.rightArm.position.x = -5;
|
|
252
|
+
this.rightArm.position.y = -2;
|
|
253
|
+
this.rightArm.position.z = 0;
|
|
254
|
+
this.leftArm.position.x = 5;
|
|
255
|
+
this.leftArm.position.y = -2;
|
|
256
|
+
this.leftArm.position.z = 0;
|
|
257
|
+
this.rightLeg.position.x = -1.9;
|
|
258
|
+
this.rightLeg.position.y = -12;
|
|
259
|
+
this.rightLeg.position.z = -0.1;
|
|
260
|
+
this.leftLeg.position.x = 1.9;
|
|
261
|
+
this.leftLeg.position.y = -12;
|
|
262
|
+
this.leftLeg.position.z = -0.1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
class CapeObject extends Group {
|
|
266
|
+
cape;
|
|
267
|
+
material;
|
|
268
|
+
constructor() {
|
|
269
|
+
super();
|
|
270
|
+
this.material = new MeshStandardMaterial({
|
|
271
|
+
side: DoubleSide,
|
|
272
|
+
transparent: true,
|
|
273
|
+
alphaTest: 1e-5
|
|
274
|
+
});
|
|
275
|
+
// +z (front) - inside of cape
|
|
276
|
+
// -z (back) - outside of cape
|
|
277
|
+
const capeBox = new BoxGeometry(10, 16, 1);
|
|
278
|
+
setCapeUVs(capeBox, 0, 0, 10, 16, 1);
|
|
279
|
+
this.cape = new Mesh(capeBox, this.material);
|
|
280
|
+
this.cape.position.y = -8;
|
|
281
|
+
this.cape.position.z = 0.5;
|
|
282
|
+
this.add(this.cape);
|
|
283
|
+
}
|
|
284
|
+
get map() {
|
|
285
|
+
return this.material.map;
|
|
286
|
+
}
|
|
287
|
+
set map(newMap) {
|
|
288
|
+
this.material.map = newMap;
|
|
289
|
+
this.material.needsUpdate = true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
class ElytraObject extends Group {
|
|
293
|
+
leftWing;
|
|
294
|
+
rightWing;
|
|
295
|
+
material;
|
|
296
|
+
constructor() {
|
|
297
|
+
super();
|
|
298
|
+
this.material = new MeshStandardMaterial({
|
|
299
|
+
side: DoubleSide,
|
|
300
|
+
transparent: true,
|
|
301
|
+
alphaTest: 1e-5
|
|
302
|
+
});
|
|
303
|
+
const leftWingBox = new BoxGeometry(12, 22, 4);
|
|
304
|
+
setCapeUVs(leftWingBox, 22, 0, 10, 20, 2);
|
|
305
|
+
const leftWingMesh = new Mesh(leftWingBox, this.material);
|
|
306
|
+
leftWingMesh.position.x = -5;
|
|
307
|
+
leftWingMesh.position.y = -10;
|
|
308
|
+
leftWingMesh.position.z = -1;
|
|
309
|
+
this.leftWing = new Group();
|
|
310
|
+
this.leftWing.add(leftWingMesh);
|
|
311
|
+
this.add(this.leftWing);
|
|
312
|
+
const rightWingBox = new BoxGeometry(12, 22, 4);
|
|
313
|
+
setCapeUVs(rightWingBox, 22, 0, 10, 20, 2);
|
|
314
|
+
const rightWingMesh = new Mesh(rightWingBox, this.material);
|
|
315
|
+
rightWingMesh.scale.x = -1;
|
|
316
|
+
rightWingMesh.position.x = 5;
|
|
317
|
+
rightWingMesh.position.y = -10;
|
|
318
|
+
rightWingMesh.position.z = -1;
|
|
319
|
+
this.rightWing = new Group();
|
|
320
|
+
this.rightWing.add(rightWingMesh);
|
|
321
|
+
this.add(this.rightWing);
|
|
322
|
+
this.leftWing.position.x = 5;
|
|
323
|
+
this.leftWing.rotation.x = 0.2617994;
|
|
324
|
+
this.resetJoints();
|
|
325
|
+
}
|
|
326
|
+
resetJoints() {
|
|
327
|
+
this.leftWing.rotation.y = 0.01; // to avoid z-fighting
|
|
328
|
+
this.leftWing.rotation.z = 0.2617994;
|
|
329
|
+
this.updateRightWing();
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Mirrors the position & rotation of left wing,
|
|
333
|
+
* and apply them to the right wing.
|
|
334
|
+
*/
|
|
335
|
+
updateRightWing() {
|
|
336
|
+
this.rightWing.position.x = -this.leftWing.position.x;
|
|
337
|
+
this.rightWing.position.y = this.leftWing.position.y;
|
|
338
|
+
this.rightWing.rotation.x = this.leftWing.rotation.x;
|
|
339
|
+
this.rightWing.rotation.y = -this.leftWing.rotation.y;
|
|
340
|
+
this.rightWing.rotation.z = -this.leftWing.rotation.z;
|
|
341
|
+
}
|
|
342
|
+
get map() {
|
|
343
|
+
return this.material.map;
|
|
344
|
+
}
|
|
345
|
+
set map(newMap) {
|
|
346
|
+
this.material.map = newMap;
|
|
347
|
+
this.material.needsUpdate = true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
class EarsObject extends Group {
|
|
351
|
+
rightEar;
|
|
352
|
+
leftEar;
|
|
353
|
+
material;
|
|
354
|
+
constructor() {
|
|
355
|
+
super();
|
|
356
|
+
this.material = new MeshStandardMaterial({
|
|
357
|
+
side: FrontSide
|
|
358
|
+
});
|
|
359
|
+
const earBox = new BoxGeometry(8, 8, 4 / 3);
|
|
360
|
+
setUVs(earBox, 0, 0, 6, 6, 1, 14, 7);
|
|
361
|
+
this.rightEar = new Mesh(earBox, this.material);
|
|
362
|
+
this.rightEar.name = 'rightEar';
|
|
363
|
+
this.rightEar.position.x = -6;
|
|
364
|
+
this.add(this.rightEar);
|
|
365
|
+
this.leftEar = new Mesh(earBox, this.material);
|
|
366
|
+
this.leftEar.name = 'leftEar';
|
|
367
|
+
this.leftEar.position.x = 6;
|
|
368
|
+
this.add(this.leftEar);
|
|
369
|
+
}
|
|
370
|
+
get map() {
|
|
371
|
+
return this.material.map;
|
|
372
|
+
}
|
|
373
|
+
set map(newMap) {
|
|
374
|
+
this.material.map = newMap;
|
|
375
|
+
this.material.needsUpdate = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const CapeDefaultAngle = (10.8 * Math.PI) / 180;
|
|
379
|
+
class PlayerObject extends Group {
|
|
380
|
+
skin;
|
|
381
|
+
cape;
|
|
382
|
+
elytra;
|
|
383
|
+
ears;
|
|
384
|
+
nameTag;
|
|
385
|
+
constructor() {
|
|
386
|
+
super();
|
|
387
|
+
this.skin = new SkinObject();
|
|
388
|
+
this.skin.name = 'skin';
|
|
389
|
+
this.skin.position.y = 8;
|
|
390
|
+
this.add(this.skin);
|
|
391
|
+
this.cape = new CapeObject();
|
|
392
|
+
this.cape.name = 'cape';
|
|
393
|
+
this.cape.position.y = 8;
|
|
394
|
+
this.cape.position.z = -2;
|
|
395
|
+
this.cape.rotation.x = CapeDefaultAngle;
|
|
396
|
+
this.cape.rotation.y = Math.PI;
|
|
397
|
+
this.add(this.cape);
|
|
398
|
+
this.elytra = new ElytraObject();
|
|
399
|
+
this.elytra.name = 'elytra';
|
|
400
|
+
this.elytra.position.y = 8;
|
|
401
|
+
this.elytra.position.z = -2;
|
|
402
|
+
this.elytra.visible = false;
|
|
403
|
+
this.add(this.elytra);
|
|
404
|
+
this.ears = new EarsObject();
|
|
405
|
+
this.ears.name = 'ears';
|
|
406
|
+
this.ears.position.y = 10;
|
|
407
|
+
this.ears.position.z = 2 / 3;
|
|
408
|
+
this.ears.visible = false;
|
|
409
|
+
this.skin.head.add(this.ears);
|
|
410
|
+
}
|
|
411
|
+
get backEquipment() {
|
|
412
|
+
if (this.cape.visible) {
|
|
413
|
+
return 'cape';
|
|
414
|
+
}
|
|
415
|
+
else if (this.elytra.visible) {
|
|
416
|
+
return 'elytra';
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
set backEquipment(value) {
|
|
423
|
+
this.cape.visible = value === 'cape';
|
|
424
|
+
this.elytra.visible = value === 'elytra';
|
|
425
|
+
}
|
|
426
|
+
resetJoints() {
|
|
427
|
+
this.skin.resetJoints();
|
|
428
|
+
this.cape.rotation.x = CapeDefaultAngle;
|
|
429
|
+
this.cape.position.y = 8;
|
|
430
|
+
this.cape.position.z = -2;
|
|
431
|
+
this.elytra.position.y = 8;
|
|
432
|
+
this.elytra.position.z = -2;
|
|
433
|
+
this.elytra.rotation.x = 0;
|
|
434
|
+
this.elytra.resetJoints();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function isTextureSource(value) {
|
|
439
|
+
return (value instanceof Image ||
|
|
440
|
+
// value instanceof HTMLVideoElement ||
|
|
441
|
+
value instanceof Canvas //||
|
|
442
|
+
// (typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap) ||
|
|
443
|
+
// (typeof OffscreenCanvas !== 'undefined' && value instanceof OffscreenCanvas)
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function hasTransparency(context, x0, y0, w, h) {
|
|
448
|
+
const imgData = context.getImageData(x0, y0, w, h);
|
|
449
|
+
for (let x = 0; x < w; x++) {
|
|
450
|
+
for (let y = 0; y < h; y++) {
|
|
451
|
+
const offset = (x + y * w) * 4;
|
|
452
|
+
if (imgData.data[offset + 3] !== 0xff) {
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
function computeSkinScale(width) {
|
|
460
|
+
return width / 64.0;
|
|
461
|
+
}
|
|
462
|
+
function fixOpaqueSkin(context, width, format1_8) {
|
|
463
|
+
// see https://github.com/bs-community/skinview3d/issues/15
|
|
464
|
+
// see https://github.com/bs-community/skinview3d/issues/93
|
|
465
|
+
// check whether the skin has opaque background
|
|
466
|
+
if (format1_8) {
|
|
467
|
+
if (hasTransparency(context, 0, 0, width, width))
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
if (hasTransparency(context, 0, 0, width, width / 2))
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const scale = computeSkinScale(width);
|
|
475
|
+
const clearArea = (x, y, w, h) => context.clearRect(x * scale, y * scale, w * scale, h * scale);
|
|
476
|
+
clearArea(40, 0, 8, 8); // Helm Top
|
|
477
|
+
clearArea(48, 0, 8, 8); // Helm Bottom
|
|
478
|
+
clearArea(32, 8, 8, 8); // Helm Right
|
|
479
|
+
clearArea(40, 8, 8, 8); // Helm Front
|
|
480
|
+
clearArea(48, 8, 8, 8); // Helm Left
|
|
481
|
+
clearArea(56, 8, 8, 8); // Helm Back
|
|
482
|
+
if (format1_8) {
|
|
483
|
+
clearArea(4, 32, 4, 4); // Right Leg Layer 2 Top
|
|
484
|
+
clearArea(8, 32, 4, 4); // Right Leg Layer 2 Bottom
|
|
485
|
+
clearArea(0, 36, 4, 12); // Right Leg Layer 2 Right
|
|
486
|
+
clearArea(4, 36, 4, 12); // Right Leg Layer 2 Front
|
|
487
|
+
clearArea(8, 36, 4, 12); // Right Leg Layer 2 Left
|
|
488
|
+
clearArea(12, 36, 4, 12); // Right Leg Layer 2 Back
|
|
489
|
+
clearArea(20, 32, 8, 4); // Torso Layer 2 Top
|
|
490
|
+
clearArea(28, 32, 8, 4); // Torso Layer 2 Bottom
|
|
491
|
+
clearArea(16, 36, 4, 12); // Torso Layer 2 Right
|
|
492
|
+
clearArea(20, 36, 8, 12); // Torso Layer 2 Front
|
|
493
|
+
clearArea(28, 36, 4, 12); // Torso Layer 2 Left
|
|
494
|
+
clearArea(32, 36, 8, 12); // Torso Layer 2 Back
|
|
495
|
+
clearArea(44, 32, 4, 4); // Right Arm Layer 2 Top
|
|
496
|
+
clearArea(48, 32, 4, 4); // Right Arm Layer 2 Bottom
|
|
497
|
+
clearArea(40, 36, 4, 12); // Right Arm Layer 2 Right
|
|
498
|
+
clearArea(44, 36, 4, 12); // Right Arm Layer 2 Front
|
|
499
|
+
clearArea(48, 36, 4, 12); // Right Arm Layer 2 Left
|
|
500
|
+
clearArea(52, 36, 12, 12); // Right Arm Layer 2 Back
|
|
501
|
+
clearArea(4, 48, 4, 4); // Left Leg Layer 2 Top
|
|
502
|
+
clearArea(8, 48, 4, 4); // Left Leg Layer 2 Bottom
|
|
503
|
+
clearArea(0, 52, 4, 12); // Left Leg Layer 2 Right
|
|
504
|
+
clearArea(4, 52, 4, 12); // Left Leg Layer 2 Front
|
|
505
|
+
clearArea(8, 52, 4, 12); // Left Leg Layer 2 Left
|
|
506
|
+
clearArea(12, 52, 4, 12); // Left Leg Layer 2 Back
|
|
507
|
+
clearArea(52, 48, 4, 4); // Left Arm Layer 2 Top
|
|
508
|
+
clearArea(56, 48, 4, 4); // Left Arm Layer 2 Bottom
|
|
509
|
+
clearArea(48, 52, 4, 12); // Left Arm Layer 2 Right
|
|
510
|
+
clearArea(52, 52, 4, 12); // Left Arm Layer 2 Front
|
|
511
|
+
clearArea(56, 52, 4, 12); // Left Arm Layer 2 Left
|
|
512
|
+
clearArea(60, 52, 4, 12); // Left Arm Layer 2 Back
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function convertSkinTo1_8(context, width) {
|
|
516
|
+
// Copied parts are horizontally flipped
|
|
517
|
+
context.save();
|
|
518
|
+
context.scale(-1, 1);
|
|
519
|
+
const scale = computeSkinScale(width);
|
|
520
|
+
const copySkin = (sX, sY, w, h, dX, dY) => context.drawImage(context.canvas, sX * scale, sY * scale, w * scale, h * scale, -dX * scale - w * scale, dY * scale, w * scale, h * scale); // -w https://github.com/samizdatco/skia-canvas/issues/283
|
|
521
|
+
copySkin(4, 16, 4, 4, 20, 48); // Top Leg
|
|
522
|
+
copySkin(8, 16, 4, 4, 24, 48); // Bottom Leg
|
|
523
|
+
copySkin(0, 20, 4, 12, 24, 52); // Outer Leg
|
|
524
|
+
copySkin(4, 20, 4, 12, 20, 52); // Front Leg
|
|
525
|
+
copySkin(8, 20, 4, 12, 16, 52); // Inner Leg
|
|
526
|
+
copySkin(12, 20, 4, 12, 28, 52); // Back Leg
|
|
527
|
+
copySkin(44, 16, 4, 4, 36, 48); // Top Arm
|
|
528
|
+
copySkin(48, 16, 4, 4, 40, 48); // Bottom Arm
|
|
529
|
+
copySkin(40, 20, 4, 12, 40, 52); // Outer Arm
|
|
530
|
+
copySkin(44, 20, 4, 12, 36, 52); // Front Arm
|
|
531
|
+
copySkin(48, 20, 4, 12, 32, 52); // Inner Arm
|
|
532
|
+
copySkin(52, 20, 4, 12, 44, 52); // Back Arm
|
|
533
|
+
context.restore();
|
|
534
|
+
}
|
|
535
|
+
function loadSkinToCanvas(canvas, image) {
|
|
536
|
+
let isOldFormat = false;
|
|
537
|
+
if (image.width !== image.height) {
|
|
538
|
+
if (image.width === 2 * image.height) {
|
|
539
|
+
isOldFormat = true;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
throw new Error(`Bad skin size: ${image.width}x${image.height}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const context = canvas.getContext('2d' /* , { willReadFrequently: true } */);
|
|
546
|
+
if (isOldFormat) {
|
|
547
|
+
const sideLength = image.width;
|
|
548
|
+
canvas.width = sideLength;
|
|
549
|
+
canvas.height = sideLength;
|
|
550
|
+
context.clearRect(0, 0, sideLength, sideLength);
|
|
551
|
+
context.drawImage(image, 0, 0, sideLength, sideLength / 2.0);
|
|
552
|
+
convertSkinTo1_8(context, sideLength);
|
|
553
|
+
fixOpaqueSkin(context, canvas.width, false);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
canvas.width = image.width;
|
|
557
|
+
canvas.height = image.height;
|
|
558
|
+
context.clearRect(0, 0, image.width, image.height);
|
|
559
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
560
|
+
fixOpaqueSkin(context, canvas.width, true);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function computeCapeScale(image) {
|
|
564
|
+
if (image.width === 2 * image.height) {
|
|
565
|
+
// 64x32
|
|
566
|
+
return image.width / 64;
|
|
567
|
+
}
|
|
568
|
+
else if (image.width * 17 === image.height * 22) {
|
|
569
|
+
// 22x17
|
|
570
|
+
return image.width / 22;
|
|
571
|
+
}
|
|
572
|
+
else if (image.width * 11 === image.height * 23) {
|
|
573
|
+
// 46x22
|
|
574
|
+
return image.width / 46;
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
throw new Error(`Bad cape size: ${image.width}x${image.height}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function loadCapeToCanvas(canvas, image) {
|
|
581
|
+
const scale = computeCapeScale(image);
|
|
582
|
+
canvas.width = 64 * scale;
|
|
583
|
+
canvas.height = 32 * scale;
|
|
584
|
+
const context = canvas.getContext('2d');
|
|
585
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
586
|
+
context.drawImage(image, 0, 0, image.width, image.height);
|
|
587
|
+
}
|
|
588
|
+
function isAreaBlack(context, x0, y0, w, h) {
|
|
589
|
+
const imgData = context.getImageData(x0, y0, w, h);
|
|
590
|
+
for (let x = 0; x < w; x++) {
|
|
591
|
+
for (let y = 0; y < h; y++) {
|
|
592
|
+
const offset = (x + y * w) * 4;
|
|
593
|
+
if (!(imgData.data[offset + 0] === 0 && imgData.data[offset + 1] === 0 && imgData.data[offset + 2] === 0 && imgData.data[offset + 3] === 0xff)) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
function isAreaWhite(context, x0, y0, w, h) {
|
|
601
|
+
const imgData = context.getImageData(x0, y0, w, h);
|
|
602
|
+
for (let x = 0; x < w; x++) {
|
|
603
|
+
for (let y = 0; y < h; y++) {
|
|
604
|
+
const offset = (x + y * w) * 4;
|
|
605
|
+
if (!(imgData.data[offset + 0] === 0xff && imgData.data[offset + 1] === 0xff && imgData.data[offset + 2] === 0xff && imgData.data[offset + 3] === 0xff)) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
function inferModelType(canvas) {
|
|
613
|
+
// The right arm area of *default* skins:
|
|
614
|
+
// (44,16)->*-------*-------*
|
|
615
|
+
// (40,20) |top |bottom |
|
|
616
|
+
// \|/ |4x4 |4x4 |
|
|
617
|
+
// *-------*-------*-------*-------*
|
|
618
|
+
// |right |front |left |back |
|
|
619
|
+
// |4x12 |4x12 |4x12 |4x12 |
|
|
620
|
+
// *-------*-------*-------*-------*
|
|
621
|
+
// The right arm area of *slim* skins:
|
|
622
|
+
// (44,16)->*------*------*-*
|
|
623
|
+
// (40,20) |top |bottom| |<----[x0=50,y0=16,w=2,h=4]
|
|
624
|
+
// \|/ |3x4 |3x4 | |
|
|
625
|
+
// *-------*------*------***-----*-*
|
|
626
|
+
// |right |front |left |back | |<----[x0=54,y0=20,w=2,h=12]
|
|
627
|
+
// |4x12 |3x12 |4x12 |3x12 | |
|
|
628
|
+
// *-------*------*-------*------*-*
|
|
629
|
+
// Compared with default right arms, slim right arms have 2 unused areas.
|
|
630
|
+
//
|
|
631
|
+
// The same is true for left arm:
|
|
632
|
+
// The left arm area of *default* skins:
|
|
633
|
+
// (36,48)->*-------*-------*
|
|
634
|
+
// (32,52) |top |bottom |
|
|
635
|
+
// \|/ |4x4 |4x4 |
|
|
636
|
+
// *-------*-------*-------*-------*
|
|
637
|
+
// |right |front |left |back |
|
|
638
|
+
// |4x12 |4x12 |4x12 |4x12 |
|
|
639
|
+
// *-------*-------*-------*-------*
|
|
640
|
+
// The left arm area of *slim* skins:
|
|
641
|
+
// (36,48)->*------*------*-*
|
|
642
|
+
// (32,52) |top |bottom| |<----[x0=42,y0=48,w=2,h=4]
|
|
643
|
+
// \|/ |3x4 |3x4 | |
|
|
644
|
+
// *-------*------*------***-----*-*
|
|
645
|
+
// |right |front |left |back | |<----[x0=46,y0=52,w=2,h=12]
|
|
646
|
+
// |4x12 |3x12 |4x12 |3x12 | |
|
|
647
|
+
// *-------*------*-------*------*-*
|
|
648
|
+
//
|
|
649
|
+
// If there is a transparent pixel in any of the 4 unused areas, the skin must be slim,
|
|
650
|
+
// as transparent pixels are not allowed in the first layer.
|
|
651
|
+
// If the 4 areas are all black or all white, the skin is also considered as slim.
|
|
652
|
+
const scale = computeSkinScale(canvas.width);
|
|
653
|
+
const context = canvas.getContext('2d');
|
|
654
|
+
const checkTransparency = (x, y, w, h) => hasTransparency(context, x * scale, y * scale, w * scale, h * scale);
|
|
655
|
+
const checkBlack = (x, y, w, h) => isAreaBlack(context, x * scale, y * scale, w * scale, h * scale);
|
|
656
|
+
const checkWhite = (x, y, w, h) => isAreaWhite(context, x * scale, y * scale, w * scale, h * scale);
|
|
657
|
+
const isSlim = checkTransparency(50, 16, 2, 4) ||
|
|
658
|
+
checkTransparency(54, 20, 2, 12) ||
|
|
659
|
+
checkTransparency(42, 48, 2, 4) ||
|
|
660
|
+
checkTransparency(46, 52, 2, 12) ||
|
|
661
|
+
(checkBlack(50, 16, 2, 4) && checkBlack(54, 20, 2, 12) && checkBlack(42, 48, 2, 4) && checkBlack(46, 52, 2, 12)) ||
|
|
662
|
+
(checkWhite(50, 16, 2, 4) && checkWhite(54, 20, 2, 12) && checkWhite(42, 48, 2, 4) && checkWhite(46, 52, 2, 12));
|
|
663
|
+
return isSlim ? 'slim' : 'default';
|
|
664
|
+
}
|
|
665
|
+
function computeEarsScale(image) {
|
|
666
|
+
if (image.width === image.height * 2 && image.height % 7 === 0) {
|
|
667
|
+
return image.height / 7;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
throw new Error(`Bad ears size: ${image.width}x${image.height}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function loadEarsToCanvas(canvas, image) {
|
|
674
|
+
const scale = computeEarsScale(image);
|
|
675
|
+
canvas.width = 14 * scale;
|
|
676
|
+
canvas.height = 7 * scale;
|
|
677
|
+
const context = canvas.getContext('2d');
|
|
678
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
679
|
+
context.drawImage(image, 0, 0, image.width, image.height);
|
|
680
|
+
}
|
|
681
|
+
function loadEarsToCanvasFromSkin(canvas, image) {
|
|
682
|
+
if (image.width !== image.height && image.width !== 2 * image.height) {
|
|
683
|
+
throw new Error(`Bad skin size: ${image.width}x${image.height}`);
|
|
684
|
+
}
|
|
685
|
+
const scale = computeSkinScale(image.width);
|
|
686
|
+
const w = 14 * scale;
|
|
687
|
+
const h = 7 * scale;
|
|
688
|
+
canvas.width = w;
|
|
689
|
+
canvas.height = h;
|
|
690
|
+
const context = canvas.getContext('2d');
|
|
691
|
+
context.clearRect(0, 0, w, h);
|
|
692
|
+
context.drawImage(image, 24 * scale, 0, w, h, 0, 0, w, h);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function loadImage(source) {
|
|
696
|
+
const image = new Image();
|
|
697
|
+
return new Promise((resolve, reject) => {
|
|
698
|
+
image.onload = () => resolve(image);
|
|
699
|
+
image.onerror = reject;
|
|
700
|
+
if (typeof source === 'string') {
|
|
701
|
+
image.src = source;
|
|
702
|
+
}
|
|
703
|
+
else if (Buffer.isBuffer(source)) {
|
|
704
|
+
image.src = source;
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
image.src = source.src;
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
function canvas2DataTexture(canvas) {
|
|
712
|
+
const png = PNG.sync.read(canvas.toBufferSync('png'));
|
|
713
|
+
const texture = new DataTexture(Uint8Array.from(png.data), png.width, png.height, RGBAFormat);
|
|
714
|
+
texture.needsUpdate = true;
|
|
715
|
+
texture.flipY = true;
|
|
716
|
+
return texture;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Full-screen textured quad shader
|
|
721
|
+
*/
|
|
722
|
+
|
|
723
|
+
const CopyShader = {
|
|
724
|
+
|
|
725
|
+
name: 'CopyShader',
|
|
726
|
+
|
|
727
|
+
uniforms: {
|
|
728
|
+
|
|
729
|
+
'tDiffuse': { value: null },
|
|
730
|
+
'opacity': { value: 1.0 }
|
|
731
|
+
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
vertexShader: /* glsl */`
|
|
735
|
+
|
|
736
|
+
varying vec2 vUv;
|
|
737
|
+
|
|
738
|
+
void main() {
|
|
739
|
+
|
|
740
|
+
vUv = uv;
|
|
741
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
|
742
|
+
|
|
743
|
+
}`,
|
|
744
|
+
|
|
745
|
+
fragmentShader: /* glsl */`
|
|
746
|
+
|
|
747
|
+
uniform float opacity;
|
|
748
|
+
|
|
749
|
+
uniform sampler2D tDiffuse;
|
|
750
|
+
|
|
751
|
+
varying vec2 vUv;
|
|
752
|
+
|
|
753
|
+
void main() {
|
|
754
|
+
|
|
755
|
+
vec4 texel = texture2D( tDiffuse, vUv );
|
|
756
|
+
gl_FragColor = opacity * texel;
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
}`
|
|
760
|
+
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
class Pass {
|
|
764
|
+
|
|
765
|
+
constructor() {
|
|
766
|
+
|
|
767
|
+
this.isPass = true;
|
|
768
|
+
|
|
769
|
+
// if set to true, the pass is processed by the composer
|
|
770
|
+
this.enabled = true;
|
|
771
|
+
|
|
772
|
+
// if set to true, the pass indicates to swap read and write buffer after rendering
|
|
773
|
+
this.needsSwap = true;
|
|
774
|
+
|
|
775
|
+
// if set to true, the pass clears its buffer before rendering
|
|
776
|
+
this.clear = false;
|
|
777
|
+
|
|
778
|
+
// if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer.
|
|
779
|
+
this.renderToScreen = false;
|
|
780
|
+
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
setSize( /* width, height */ ) {}
|
|
784
|
+
|
|
785
|
+
render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
|
|
786
|
+
|
|
787
|
+
console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
|
|
788
|
+
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
dispose() {}
|
|
792
|
+
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Helper for passes that need to fill the viewport with a single quad.
|
|
796
|
+
|
|
797
|
+
const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
|
|
798
|
+
|
|
799
|
+
// https://github.com/mrdoob/three.js/pull/21358
|
|
800
|
+
|
|
801
|
+
const _geometry = new BufferGeometry();
|
|
802
|
+
_geometry.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
|
|
803
|
+
_geometry.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );
|
|
804
|
+
|
|
805
|
+
class FullScreenQuad {
|
|
806
|
+
|
|
807
|
+
constructor( material ) {
|
|
808
|
+
|
|
809
|
+
this._mesh = new Mesh( _geometry, material );
|
|
810
|
+
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
dispose() {
|
|
814
|
+
|
|
815
|
+
this._mesh.geometry.dispose();
|
|
816
|
+
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
render( renderer ) {
|
|
820
|
+
|
|
821
|
+
renderer.render( this._mesh, _camera );
|
|
822
|
+
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
get material() {
|
|
826
|
+
|
|
827
|
+
return this._mesh.material;
|
|
828
|
+
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
set material( value ) {
|
|
832
|
+
|
|
833
|
+
this._mesh.material = value;
|
|
834
|
+
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
class ShaderPass extends Pass {
|
|
840
|
+
|
|
841
|
+
constructor( shader, textureID ) {
|
|
842
|
+
|
|
843
|
+
super();
|
|
844
|
+
|
|
845
|
+
this.textureID = ( textureID !== undefined ) ? textureID : 'tDiffuse';
|
|
846
|
+
|
|
847
|
+
if ( shader instanceof ShaderMaterial ) {
|
|
848
|
+
|
|
849
|
+
this.uniforms = shader.uniforms;
|
|
850
|
+
|
|
851
|
+
this.material = shader;
|
|
852
|
+
|
|
853
|
+
} else if ( shader ) {
|
|
854
|
+
|
|
855
|
+
this.uniforms = UniformsUtils.clone( shader.uniforms );
|
|
856
|
+
|
|
857
|
+
this.material = new ShaderMaterial( {
|
|
858
|
+
|
|
859
|
+
name: ( shader.name !== undefined ) ? shader.name : 'unspecified',
|
|
860
|
+
defines: Object.assign( {}, shader.defines ),
|
|
861
|
+
uniforms: this.uniforms,
|
|
862
|
+
vertexShader: shader.vertexShader,
|
|
863
|
+
fragmentShader: shader.fragmentShader
|
|
864
|
+
|
|
865
|
+
} );
|
|
866
|
+
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
this.fsQuad = new FullScreenQuad( this.material );
|
|
870
|
+
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
|
|
874
|
+
|
|
875
|
+
if ( this.uniforms[ this.textureID ] ) {
|
|
876
|
+
|
|
877
|
+
this.uniforms[ this.textureID ].value = readBuffer.texture;
|
|
878
|
+
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
this.fsQuad.material = this.material;
|
|
882
|
+
|
|
883
|
+
if ( this.renderToScreen ) {
|
|
884
|
+
|
|
885
|
+
renderer.setRenderTarget( null );
|
|
886
|
+
this.fsQuad.render( renderer );
|
|
887
|
+
|
|
888
|
+
} else {
|
|
889
|
+
|
|
890
|
+
renderer.setRenderTarget( writeBuffer );
|
|
891
|
+
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
|
|
892
|
+
if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
|
|
893
|
+
this.fsQuad.render( renderer );
|
|
894
|
+
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
dispose() {
|
|
900
|
+
|
|
901
|
+
this.material.dispose();
|
|
902
|
+
|
|
903
|
+
this.fsQuad.dispose();
|
|
904
|
+
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
class MaskPass extends Pass {
|
|
910
|
+
|
|
911
|
+
constructor( scene, camera ) {
|
|
912
|
+
|
|
913
|
+
super();
|
|
914
|
+
|
|
915
|
+
this.scene = scene;
|
|
916
|
+
this.camera = camera;
|
|
917
|
+
|
|
918
|
+
this.clear = true;
|
|
919
|
+
this.needsSwap = false;
|
|
920
|
+
|
|
921
|
+
this.inverse = false;
|
|
922
|
+
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
|
|
926
|
+
|
|
927
|
+
const context = renderer.getContext();
|
|
928
|
+
const state = renderer.state;
|
|
929
|
+
|
|
930
|
+
// don't update color or depth
|
|
931
|
+
|
|
932
|
+
state.buffers.color.setMask( false );
|
|
933
|
+
state.buffers.depth.setMask( false );
|
|
934
|
+
|
|
935
|
+
// lock buffers
|
|
936
|
+
|
|
937
|
+
state.buffers.color.setLocked( true );
|
|
938
|
+
state.buffers.depth.setLocked( true );
|
|
939
|
+
|
|
940
|
+
// set up stencil
|
|
941
|
+
|
|
942
|
+
let writeValue, clearValue;
|
|
943
|
+
|
|
944
|
+
if ( this.inverse ) {
|
|
945
|
+
|
|
946
|
+
writeValue = 0;
|
|
947
|
+
clearValue = 1;
|
|
948
|
+
|
|
949
|
+
} else {
|
|
950
|
+
|
|
951
|
+
writeValue = 1;
|
|
952
|
+
clearValue = 0;
|
|
953
|
+
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
state.buffers.stencil.setTest( true );
|
|
957
|
+
state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE );
|
|
958
|
+
state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff );
|
|
959
|
+
state.buffers.stencil.setClear( clearValue );
|
|
960
|
+
state.buffers.stencil.setLocked( true );
|
|
961
|
+
|
|
962
|
+
// draw into the stencil buffer
|
|
963
|
+
|
|
964
|
+
renderer.setRenderTarget( readBuffer );
|
|
965
|
+
if ( this.clear ) renderer.clear();
|
|
966
|
+
renderer.render( this.scene, this.camera );
|
|
967
|
+
|
|
968
|
+
renderer.setRenderTarget( writeBuffer );
|
|
969
|
+
if ( this.clear ) renderer.clear();
|
|
970
|
+
renderer.render( this.scene, this.camera );
|
|
971
|
+
|
|
972
|
+
// unlock color and depth buffer and make them writable for subsequent rendering/clearing
|
|
973
|
+
|
|
974
|
+
state.buffers.color.setLocked( false );
|
|
975
|
+
state.buffers.depth.setLocked( false );
|
|
976
|
+
|
|
977
|
+
state.buffers.color.setMask( true );
|
|
978
|
+
state.buffers.depth.setMask( true );
|
|
979
|
+
|
|
980
|
+
// only render where stencil is set to 1
|
|
981
|
+
|
|
982
|
+
state.buffers.stencil.setLocked( false );
|
|
983
|
+
state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1
|
|
984
|
+
state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP );
|
|
985
|
+
state.buffers.stencil.setLocked( true );
|
|
986
|
+
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
class ClearMaskPass extends Pass {
|
|
992
|
+
|
|
993
|
+
constructor() {
|
|
994
|
+
|
|
995
|
+
super();
|
|
996
|
+
|
|
997
|
+
this.needsSwap = false;
|
|
998
|
+
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
|
|
1002
|
+
|
|
1003
|
+
renderer.state.buffers.stencil.setLocked( false );
|
|
1004
|
+
renderer.state.buffers.stencil.setTest( false );
|
|
1005
|
+
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
class EffectComposer {
|
|
1011
|
+
|
|
1012
|
+
constructor( renderer, renderTarget ) {
|
|
1013
|
+
|
|
1014
|
+
this.renderer = renderer;
|
|
1015
|
+
|
|
1016
|
+
this._pixelRatio = renderer.getPixelRatio();
|
|
1017
|
+
|
|
1018
|
+
if ( renderTarget === undefined ) {
|
|
1019
|
+
|
|
1020
|
+
const size = renderer.getSize( new Vector2() );
|
|
1021
|
+
this._width = size.width;
|
|
1022
|
+
this._height = size.height;
|
|
1023
|
+
|
|
1024
|
+
renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } );
|
|
1025
|
+
renderTarget.texture.name = 'EffectComposer.rt1';
|
|
1026
|
+
|
|
1027
|
+
} else {
|
|
1028
|
+
|
|
1029
|
+
this._width = renderTarget.width;
|
|
1030
|
+
this._height = renderTarget.height;
|
|
1031
|
+
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
this.renderTarget1 = renderTarget;
|
|
1035
|
+
this.renderTarget2 = renderTarget.clone();
|
|
1036
|
+
this.renderTarget2.texture.name = 'EffectComposer.rt2';
|
|
1037
|
+
|
|
1038
|
+
this.writeBuffer = this.renderTarget1;
|
|
1039
|
+
this.readBuffer = this.renderTarget2;
|
|
1040
|
+
|
|
1041
|
+
this.renderToScreen = true;
|
|
1042
|
+
|
|
1043
|
+
this.passes = [];
|
|
1044
|
+
|
|
1045
|
+
this.copyPass = new ShaderPass( CopyShader );
|
|
1046
|
+
this.copyPass.material.blending = NoBlending;
|
|
1047
|
+
|
|
1048
|
+
this.clock = new Clock();
|
|
1049
|
+
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
swapBuffers() {
|
|
1053
|
+
|
|
1054
|
+
const tmp = this.readBuffer;
|
|
1055
|
+
this.readBuffer = this.writeBuffer;
|
|
1056
|
+
this.writeBuffer = tmp;
|
|
1057
|
+
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
addPass( pass ) {
|
|
1061
|
+
|
|
1062
|
+
this.passes.push( pass );
|
|
1063
|
+
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
|
|
1064
|
+
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
insertPass( pass, index ) {
|
|
1068
|
+
|
|
1069
|
+
this.passes.splice( index, 0, pass );
|
|
1070
|
+
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
|
|
1071
|
+
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
removePass( pass ) {
|
|
1075
|
+
|
|
1076
|
+
const index = this.passes.indexOf( pass );
|
|
1077
|
+
|
|
1078
|
+
if ( index !== - 1 ) {
|
|
1079
|
+
|
|
1080
|
+
this.passes.splice( index, 1 );
|
|
1081
|
+
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
isLastEnabledPass( passIndex ) {
|
|
1087
|
+
|
|
1088
|
+
for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {
|
|
1089
|
+
|
|
1090
|
+
if ( this.passes[ i ].enabled ) {
|
|
1091
|
+
|
|
1092
|
+
return false;
|
|
1093
|
+
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
return true;
|
|
1099
|
+
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
render( deltaTime ) {
|
|
1103
|
+
|
|
1104
|
+
// deltaTime value is in seconds
|
|
1105
|
+
|
|
1106
|
+
if ( deltaTime === undefined ) {
|
|
1107
|
+
|
|
1108
|
+
deltaTime = this.clock.getDelta();
|
|
1109
|
+
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const currentRenderTarget = this.renderer.getRenderTarget();
|
|
1113
|
+
|
|
1114
|
+
let maskActive = false;
|
|
1115
|
+
|
|
1116
|
+
for ( let i = 0, il = this.passes.length; i < il; i ++ ) {
|
|
1117
|
+
|
|
1118
|
+
const pass = this.passes[ i ];
|
|
1119
|
+
|
|
1120
|
+
if ( pass.enabled === false ) continue;
|
|
1121
|
+
|
|
1122
|
+
pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
|
|
1123
|
+
pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
|
|
1124
|
+
|
|
1125
|
+
if ( pass.needsSwap ) {
|
|
1126
|
+
|
|
1127
|
+
if ( maskActive ) {
|
|
1128
|
+
|
|
1129
|
+
const context = this.renderer.getContext();
|
|
1130
|
+
const stencil = this.renderer.state.buffers.stencil;
|
|
1131
|
+
|
|
1132
|
+
//context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );
|
|
1133
|
+
stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );
|
|
1134
|
+
|
|
1135
|
+
this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );
|
|
1136
|
+
|
|
1137
|
+
//context.stencilFunc( context.EQUAL, 1, 0xffffffff );
|
|
1138
|
+
stencil.setFunc( context.EQUAL, 1, 0xffffffff );
|
|
1139
|
+
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
this.swapBuffers();
|
|
1143
|
+
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if ( MaskPass !== undefined ) {
|
|
1147
|
+
|
|
1148
|
+
if ( pass instanceof MaskPass ) {
|
|
1149
|
+
|
|
1150
|
+
maskActive = true;
|
|
1151
|
+
|
|
1152
|
+
} else if ( pass instanceof ClearMaskPass ) {
|
|
1153
|
+
|
|
1154
|
+
maskActive = false;
|
|
1155
|
+
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
this.renderer.setRenderTarget( currentRenderTarget );
|
|
1163
|
+
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
reset( renderTarget ) {
|
|
1167
|
+
|
|
1168
|
+
if ( renderTarget === undefined ) {
|
|
1169
|
+
|
|
1170
|
+
const size = this.renderer.getSize( new Vector2() );
|
|
1171
|
+
this._pixelRatio = this.renderer.getPixelRatio();
|
|
1172
|
+
this._width = size.width;
|
|
1173
|
+
this._height = size.height;
|
|
1174
|
+
|
|
1175
|
+
renderTarget = this.renderTarget1.clone();
|
|
1176
|
+
renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
|
|
1177
|
+
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
this.renderTarget1.dispose();
|
|
1181
|
+
this.renderTarget2.dispose();
|
|
1182
|
+
this.renderTarget1 = renderTarget;
|
|
1183
|
+
this.renderTarget2 = renderTarget.clone();
|
|
1184
|
+
|
|
1185
|
+
this.writeBuffer = this.renderTarget1;
|
|
1186
|
+
this.readBuffer = this.renderTarget2;
|
|
1187
|
+
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
setSize( width, height ) {
|
|
1191
|
+
|
|
1192
|
+
this._width = width;
|
|
1193
|
+
this._height = height;
|
|
1194
|
+
|
|
1195
|
+
const effectiveWidth = this._width * this._pixelRatio;
|
|
1196
|
+
const effectiveHeight = this._height * this._pixelRatio;
|
|
1197
|
+
|
|
1198
|
+
this.renderTarget1.setSize( effectiveWidth, effectiveHeight );
|
|
1199
|
+
this.renderTarget2.setSize( effectiveWidth, effectiveHeight );
|
|
1200
|
+
|
|
1201
|
+
for ( let i = 0; i < this.passes.length; i ++ ) {
|
|
1202
|
+
|
|
1203
|
+
this.passes[ i ].setSize( effectiveWidth, effectiveHeight );
|
|
1204
|
+
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
setPixelRatio( pixelRatio ) {
|
|
1210
|
+
|
|
1211
|
+
this._pixelRatio = pixelRatio;
|
|
1212
|
+
|
|
1213
|
+
this.setSize( this._width, this._height );
|
|
1214
|
+
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
dispose() {
|
|
1218
|
+
|
|
1219
|
+
this.renderTarget1.dispose();
|
|
1220
|
+
this.renderTarget2.dispose();
|
|
1221
|
+
|
|
1222
|
+
this.copyPass.dispose();
|
|
1223
|
+
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
class RenderPass extends Pass {
|
|
1229
|
+
|
|
1230
|
+
constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) {
|
|
1231
|
+
|
|
1232
|
+
super();
|
|
1233
|
+
|
|
1234
|
+
this.scene = scene;
|
|
1235
|
+
this.camera = camera;
|
|
1236
|
+
|
|
1237
|
+
this.overrideMaterial = overrideMaterial;
|
|
1238
|
+
|
|
1239
|
+
this.clearColor = clearColor;
|
|
1240
|
+
this.clearAlpha = clearAlpha;
|
|
1241
|
+
|
|
1242
|
+
this.clear = true;
|
|
1243
|
+
this.clearDepth = false;
|
|
1244
|
+
this.needsSwap = false;
|
|
1245
|
+
this._oldClearColor = new Color();
|
|
1246
|
+
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
|
|
1250
|
+
|
|
1251
|
+
const oldAutoClear = renderer.autoClear;
|
|
1252
|
+
renderer.autoClear = false;
|
|
1253
|
+
|
|
1254
|
+
let oldClearAlpha, oldOverrideMaterial;
|
|
1255
|
+
|
|
1256
|
+
if ( this.overrideMaterial !== null ) {
|
|
1257
|
+
|
|
1258
|
+
oldOverrideMaterial = this.scene.overrideMaterial;
|
|
1259
|
+
|
|
1260
|
+
this.scene.overrideMaterial = this.overrideMaterial;
|
|
1261
|
+
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if ( this.clearColor !== null ) {
|
|
1265
|
+
|
|
1266
|
+
renderer.getClearColor( this._oldClearColor );
|
|
1267
|
+
renderer.setClearColor( this.clearColor );
|
|
1268
|
+
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if ( this.clearAlpha !== null ) {
|
|
1272
|
+
|
|
1273
|
+
oldClearAlpha = renderer.getClearAlpha();
|
|
1274
|
+
renderer.setClearAlpha( this.clearAlpha );
|
|
1275
|
+
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if ( this.clearDepth == true ) {
|
|
1279
|
+
|
|
1280
|
+
renderer.clearDepth();
|
|
1281
|
+
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );
|
|
1285
|
+
|
|
1286
|
+
if ( this.clear === true ) {
|
|
1287
|
+
|
|
1288
|
+
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
|
|
1289
|
+
renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
|
|
1290
|
+
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
renderer.render( this.scene, this.camera );
|
|
1294
|
+
|
|
1295
|
+
// restore
|
|
1296
|
+
|
|
1297
|
+
if ( this.clearColor !== null ) {
|
|
1298
|
+
|
|
1299
|
+
renderer.setClearColor( this._oldClearColor );
|
|
1300
|
+
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if ( this.clearAlpha !== null ) {
|
|
1304
|
+
|
|
1305
|
+
renderer.setClearAlpha( oldClearAlpha );
|
|
1306
|
+
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if ( this.overrideMaterial !== null ) {
|
|
1310
|
+
|
|
1311
|
+
this.scene.overrideMaterial = oldOverrideMaterial;
|
|
1312
|
+
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
renderer.autoClear = oldAutoClear;
|
|
1316
|
+
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* NVIDIA FXAA by Timothy Lottes
|
|
1323
|
+
* https://developer.download.nvidia.com/assets/gamedev/files/sdk/11/FXAA_WhitePaper.pdf
|
|
1324
|
+
* - WebGL port by @supereggbert
|
|
1325
|
+
* http://www.glge.org/demos/fxaa/
|
|
1326
|
+
* Further improved by Daniel Sturk
|
|
1327
|
+
*/
|
|
1328
|
+
|
|
1329
|
+
const FXAAShader = {
|
|
1330
|
+
|
|
1331
|
+
uniforms: {
|
|
1332
|
+
|
|
1333
|
+
'tDiffuse': { value: null },
|
|
1334
|
+
'resolution': { value: new Vector2( 1 / 1024, 1 / 512 ) }
|
|
1335
|
+
|
|
1336
|
+
},
|
|
1337
|
+
|
|
1338
|
+
vertexShader: /* glsl */`
|
|
1339
|
+
|
|
1340
|
+
varying vec2 vUv;
|
|
1341
|
+
|
|
1342
|
+
void main() {
|
|
1343
|
+
|
|
1344
|
+
vUv = uv;
|
|
1345
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
|
1346
|
+
|
|
1347
|
+
}`,
|
|
1348
|
+
|
|
1349
|
+
fragmentShader: `
|
|
1350
|
+
precision highp float;
|
|
1351
|
+
|
|
1352
|
+
uniform sampler2D tDiffuse;
|
|
1353
|
+
|
|
1354
|
+
uniform vec2 resolution;
|
|
1355
|
+
|
|
1356
|
+
varying vec2 vUv;
|
|
1357
|
+
|
|
1358
|
+
// FXAA 3.11 implementation by NVIDIA, ported to WebGL by Agost Biro (biro@archilogic.com)
|
|
1359
|
+
|
|
1360
|
+
//----------------------------------------------------------------------------------
|
|
1361
|
+
// File: es3-kepler\FXAA\assets\shaders/FXAA_DefaultES.frag
|
|
1362
|
+
// SDK Version: v3.00
|
|
1363
|
+
// Email: gameworks@nvidia.com
|
|
1364
|
+
// Site: http://developer.nvidia.com/
|
|
1365
|
+
//
|
|
1366
|
+
// Copyright (c) 2014-2015, NVIDIA CORPORATION. All rights reserved.
|
|
1367
|
+
//
|
|
1368
|
+
// Redistribution and use in source and binary forms, with or without
|
|
1369
|
+
// modification, are permitted provided that the following conditions
|
|
1370
|
+
// are met:
|
|
1371
|
+
// * Redistributions of source code must retain the above copyright
|
|
1372
|
+
// notice, this list of conditions and the following disclaimer.
|
|
1373
|
+
// * Redistributions in binary form must reproduce the above copyright
|
|
1374
|
+
// notice, this list of conditions and the following disclaimer in the
|
|
1375
|
+
// documentation and/or other materials provided with the distribution.
|
|
1376
|
+
// * Neither the name of NVIDIA CORPORATION nor the names of its
|
|
1377
|
+
// contributors may be used to endorse or promote products derived
|
|
1378
|
+
// from this software without specific prior written permission.
|
|
1379
|
+
//
|
|
1380
|
+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
|
|
1381
|
+
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
1382
|
+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
1383
|
+
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
1384
|
+
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
1385
|
+
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
1386
|
+
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
1387
|
+
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
1388
|
+
// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
1389
|
+
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
1390
|
+
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
1391
|
+
//
|
|
1392
|
+
//----------------------------------------------------------------------------------
|
|
1393
|
+
|
|
1394
|
+
#ifndef FXAA_DISCARD
|
|
1395
|
+
//
|
|
1396
|
+
// Only valid for PC OpenGL currently.
|
|
1397
|
+
// Probably will not work when FXAA_GREEN_AS_LUMA = 1.
|
|
1398
|
+
//
|
|
1399
|
+
// 1 = Use discard on pixels which don't need AA.
|
|
1400
|
+
// For APIs which enable concurrent TEX+ROP from same surface.
|
|
1401
|
+
// 0 = Return unchanged color on pixels which don't need AA.
|
|
1402
|
+
//
|
|
1403
|
+
#define FXAA_DISCARD 0
|
|
1404
|
+
#endif
|
|
1405
|
+
|
|
1406
|
+
/*--------------------------------------------------------------------------*/
|
|
1407
|
+
#define FxaaTexTop(t, p) texture2D(t, p, -100.0)
|
|
1408
|
+
#define FxaaTexOff(t, p, o, r) texture2D(t, p + (o * r), -100.0)
|
|
1409
|
+
/*--------------------------------------------------------------------------*/
|
|
1410
|
+
|
|
1411
|
+
#define NUM_SAMPLES 5
|
|
1412
|
+
|
|
1413
|
+
// assumes colors have premultipliedAlpha, so that the calculated color contrast is scaled by alpha
|
|
1414
|
+
float contrast( vec4 a, vec4 b ) {
|
|
1415
|
+
vec4 diff = abs( a - b );
|
|
1416
|
+
return max( max( max( diff.r, diff.g ), diff.b ), diff.a );
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/*============================================================================
|
|
1420
|
+
|
|
1421
|
+
FXAA3 QUALITY - PC
|
|
1422
|
+
|
|
1423
|
+
============================================================================*/
|
|
1424
|
+
|
|
1425
|
+
/*--------------------------------------------------------------------------*/
|
|
1426
|
+
vec4 FxaaPixelShader(
|
|
1427
|
+
vec2 posM,
|
|
1428
|
+
sampler2D tex,
|
|
1429
|
+
vec2 fxaaQualityRcpFrame,
|
|
1430
|
+
float fxaaQualityEdgeThreshold,
|
|
1431
|
+
float fxaaQualityinvEdgeThreshold
|
|
1432
|
+
) {
|
|
1433
|
+
vec4 rgbaM = FxaaTexTop(tex, posM);
|
|
1434
|
+
vec4 rgbaS = FxaaTexOff(tex, posM, vec2( 0.0, 1.0), fxaaQualityRcpFrame.xy);
|
|
1435
|
+
vec4 rgbaE = FxaaTexOff(tex, posM, vec2( 1.0, 0.0), fxaaQualityRcpFrame.xy);
|
|
1436
|
+
vec4 rgbaN = FxaaTexOff(tex, posM, vec2( 0.0,-1.0), fxaaQualityRcpFrame.xy);
|
|
1437
|
+
vec4 rgbaW = FxaaTexOff(tex, posM, vec2(-1.0, 0.0), fxaaQualityRcpFrame.xy);
|
|
1438
|
+
// . S .
|
|
1439
|
+
// W M E
|
|
1440
|
+
// . N .
|
|
1441
|
+
|
|
1442
|
+
bool earlyExit = max( max( max(
|
|
1443
|
+
contrast( rgbaM, rgbaN ),
|
|
1444
|
+
contrast( rgbaM, rgbaS ) ),
|
|
1445
|
+
contrast( rgbaM, rgbaE ) ),
|
|
1446
|
+
contrast( rgbaM, rgbaW ) )
|
|
1447
|
+
< fxaaQualityEdgeThreshold;
|
|
1448
|
+
// . 0 .
|
|
1449
|
+
// 0 0 0
|
|
1450
|
+
// . 0 .
|
|
1451
|
+
|
|
1452
|
+
#if (FXAA_DISCARD == 1)
|
|
1453
|
+
if(earlyExit) FxaaDiscard;
|
|
1454
|
+
#else
|
|
1455
|
+
if(earlyExit) return rgbaM;
|
|
1456
|
+
#endif
|
|
1457
|
+
|
|
1458
|
+
float contrastN = contrast( rgbaM, rgbaN );
|
|
1459
|
+
float contrastS = contrast( rgbaM, rgbaS );
|
|
1460
|
+
float contrastE = contrast( rgbaM, rgbaE );
|
|
1461
|
+
float contrastW = contrast( rgbaM, rgbaW );
|
|
1462
|
+
|
|
1463
|
+
float relativeVContrast = ( contrastN + contrastS ) - ( contrastE + contrastW );
|
|
1464
|
+
relativeVContrast *= fxaaQualityinvEdgeThreshold;
|
|
1465
|
+
|
|
1466
|
+
bool horzSpan = relativeVContrast > 0.;
|
|
1467
|
+
// . 1 .
|
|
1468
|
+
// 0 0 0
|
|
1469
|
+
// . 1 .
|
|
1470
|
+
|
|
1471
|
+
// 45 deg edge detection and corners of objects, aka V/H contrast is too similar
|
|
1472
|
+
if( abs( relativeVContrast ) < .3 ) {
|
|
1473
|
+
// locate the edge
|
|
1474
|
+
vec2 dirToEdge;
|
|
1475
|
+
dirToEdge.x = contrastE > contrastW ? 1. : -1.;
|
|
1476
|
+
dirToEdge.y = contrastS > contrastN ? 1. : -1.;
|
|
1477
|
+
// . 2 . . 1 .
|
|
1478
|
+
// 1 0 2 ~= 0 0 1
|
|
1479
|
+
// . 1 . . 0 .
|
|
1480
|
+
|
|
1481
|
+
// tap 2 pixels and see which ones are "outside" the edge, to
|
|
1482
|
+
// determine if the edge is vertical or horizontal
|
|
1483
|
+
|
|
1484
|
+
vec4 rgbaAlongH = FxaaTexOff(tex, posM, vec2( dirToEdge.x, -dirToEdge.y ), fxaaQualityRcpFrame.xy);
|
|
1485
|
+
float matchAlongH = contrast( rgbaM, rgbaAlongH );
|
|
1486
|
+
// . 1 .
|
|
1487
|
+
// 0 0 1
|
|
1488
|
+
// . 0 H
|
|
1489
|
+
|
|
1490
|
+
vec4 rgbaAlongV = FxaaTexOff(tex, posM, vec2( -dirToEdge.x, dirToEdge.y ), fxaaQualityRcpFrame.xy);
|
|
1491
|
+
float matchAlongV = contrast( rgbaM, rgbaAlongV );
|
|
1492
|
+
// V 1 .
|
|
1493
|
+
// 0 0 1
|
|
1494
|
+
// . 0 .
|
|
1495
|
+
|
|
1496
|
+
relativeVContrast = matchAlongV - matchAlongH;
|
|
1497
|
+
relativeVContrast *= fxaaQualityinvEdgeThreshold;
|
|
1498
|
+
|
|
1499
|
+
if( abs( relativeVContrast ) < .3 ) { // 45 deg edge
|
|
1500
|
+
// 1 1 .
|
|
1501
|
+
// 0 0 1
|
|
1502
|
+
// . 0 1
|
|
1503
|
+
|
|
1504
|
+
// do a simple blur
|
|
1505
|
+
return mix(
|
|
1506
|
+
rgbaM,
|
|
1507
|
+
(rgbaN + rgbaS + rgbaE + rgbaW) * .25,
|
|
1508
|
+
.4
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
horzSpan = relativeVContrast > 0.;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if(!horzSpan) rgbaN = rgbaW;
|
|
1516
|
+
if(!horzSpan) rgbaS = rgbaE;
|
|
1517
|
+
// . 0 . 1
|
|
1518
|
+
// 1 0 1 -> 0
|
|
1519
|
+
// . 0 . 1
|
|
1520
|
+
|
|
1521
|
+
bool pairN = contrast( rgbaM, rgbaN ) > contrast( rgbaM, rgbaS );
|
|
1522
|
+
if(!pairN) rgbaN = rgbaS;
|
|
1523
|
+
|
|
1524
|
+
vec2 offNP;
|
|
1525
|
+
offNP.x = (!horzSpan) ? 0.0 : fxaaQualityRcpFrame.x;
|
|
1526
|
+
offNP.y = ( horzSpan) ? 0.0 : fxaaQualityRcpFrame.y;
|
|
1527
|
+
|
|
1528
|
+
bool doneN = false;
|
|
1529
|
+
bool doneP = false;
|
|
1530
|
+
|
|
1531
|
+
float nDist = 0.;
|
|
1532
|
+
float pDist = 0.;
|
|
1533
|
+
|
|
1534
|
+
vec2 posN = posM;
|
|
1535
|
+
vec2 posP = posM;
|
|
1536
|
+
|
|
1537
|
+
int iterationsUsed = 0;
|
|
1538
|
+
int iterationsUsedN = 0;
|
|
1539
|
+
int iterationsUsedP = 0;
|
|
1540
|
+
for( int i = 0; i < NUM_SAMPLES; i++ ) {
|
|
1541
|
+
iterationsUsed = i;
|
|
1542
|
+
|
|
1543
|
+
float increment = float(i + 1);
|
|
1544
|
+
|
|
1545
|
+
if(!doneN) {
|
|
1546
|
+
nDist += increment;
|
|
1547
|
+
posN = posM + offNP * nDist;
|
|
1548
|
+
vec4 rgbaEndN = FxaaTexTop(tex, posN.xy);
|
|
1549
|
+
doneN = contrast( rgbaEndN, rgbaM ) > contrast( rgbaEndN, rgbaN );
|
|
1550
|
+
iterationsUsedN = i;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
if(!doneP) {
|
|
1554
|
+
pDist += increment;
|
|
1555
|
+
posP = posM - offNP * pDist;
|
|
1556
|
+
vec4 rgbaEndP = FxaaTexTop(tex, posP.xy);
|
|
1557
|
+
doneP = contrast( rgbaEndP, rgbaM ) > contrast( rgbaEndP, rgbaN );
|
|
1558
|
+
iterationsUsedP = i;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
if(doneN || doneP) break;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
if ( !doneP && !doneN ) return rgbaM; // failed to find end of edge
|
|
1566
|
+
|
|
1567
|
+
float dist = min(
|
|
1568
|
+
doneN ? float( iterationsUsedN ) / float( NUM_SAMPLES - 1 ) : 1.,
|
|
1569
|
+
doneP ? float( iterationsUsedP ) / float( NUM_SAMPLES - 1 ) : 1.
|
|
1570
|
+
);
|
|
1571
|
+
|
|
1572
|
+
// hacky way of reduces blurriness of mostly diagonal edges
|
|
1573
|
+
// but reduces AA quality
|
|
1574
|
+
dist = pow(dist, .5);
|
|
1575
|
+
|
|
1576
|
+
dist = 1. - dist;
|
|
1577
|
+
|
|
1578
|
+
return mix(
|
|
1579
|
+
rgbaM,
|
|
1580
|
+
rgbaN,
|
|
1581
|
+
dist * .5
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
void main() {
|
|
1586
|
+
const float edgeDetectionQuality = .2;
|
|
1587
|
+
const float invEdgeDetectionQuality = 1. / edgeDetectionQuality;
|
|
1588
|
+
|
|
1589
|
+
gl_FragColor = FxaaPixelShader(
|
|
1590
|
+
vUv,
|
|
1591
|
+
tDiffuse,
|
|
1592
|
+
resolution,
|
|
1593
|
+
edgeDetectionQuality, // [0,1] contrast needed, otherwise early discard
|
|
1594
|
+
invEdgeDetectionQuality
|
|
1595
|
+
);
|
|
1596
|
+
|
|
1597
|
+
}
|
|
1598
|
+
`
|
|
1599
|
+
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
/**
|
|
1603
|
+
* A Minecraft name tag, i.e. a text label with background.
|
|
1604
|
+
*/
|
|
1605
|
+
class NameTagObject extends Sprite {
|
|
1606
|
+
/**
|
|
1607
|
+
* A promise that is resolved after the name tag is fully painted.
|
|
1608
|
+
*
|
|
1609
|
+
* This will be a resolved promise, if
|
|
1610
|
+
* {@link NameTagOptions.repaintAfterLoaded} is `false`, or
|
|
1611
|
+
* the desired font is available when the `NameTagObject` is created.
|
|
1612
|
+
*
|
|
1613
|
+
* If {@link NameTagOptions.repaintAfterLoaded} is `true`, and
|
|
1614
|
+
* the desired font hasn't been loaded when the `NameTagObject` is created,
|
|
1615
|
+
* the name tag will be painted with the fallback font first, and then
|
|
1616
|
+
* repainted with the desired font after it's loaded. This promise is
|
|
1617
|
+
* resolved after repainting is done.
|
|
1618
|
+
*/
|
|
1619
|
+
painted;
|
|
1620
|
+
text;
|
|
1621
|
+
font;
|
|
1622
|
+
margin;
|
|
1623
|
+
textStyle;
|
|
1624
|
+
backgroundStyle;
|
|
1625
|
+
height;
|
|
1626
|
+
textMaterial;
|
|
1627
|
+
constructor(text = '', options = {}) {
|
|
1628
|
+
const material = new SpriteMaterial({
|
|
1629
|
+
transparent: true,
|
|
1630
|
+
alphaTest: 1e-5
|
|
1631
|
+
});
|
|
1632
|
+
super(material);
|
|
1633
|
+
this.textMaterial = material;
|
|
1634
|
+
this.text = text;
|
|
1635
|
+
this.font = options.font === undefined ? '48px Minecraft' : options.font;
|
|
1636
|
+
this.margin = options.margin === undefined ? [5, 10, 5, 10] : options.margin;
|
|
1637
|
+
this.textStyle = options.textStyle === undefined ? 'white' : options.textStyle;
|
|
1638
|
+
this.backgroundStyle = options.backgroundStyle === undefined ? 'rgba(0,0,0,.25)' : options.backgroundStyle;
|
|
1639
|
+
this.height = options.height === undefined ? 4.0 : options.height;
|
|
1640
|
+
const repaintAfterLoaded = options.repaintAfterLoaded ?? true;
|
|
1641
|
+
if (repaintAfterLoaded /* && !document.fonts.check(this.font, this.text) */) {
|
|
1642
|
+
this.paint();
|
|
1643
|
+
this.painted = this.loadAndPaint();
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
this.paint();
|
|
1647
|
+
this.painted = Promise.resolve();
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
async loadAndPaint() {
|
|
1651
|
+
// await document.fonts.load(this.font, this.text);
|
|
1652
|
+
this.paint();
|
|
1653
|
+
}
|
|
1654
|
+
paint() {
|
|
1655
|
+
const canvas = new Canvas();
|
|
1656
|
+
// Measure the text size
|
|
1657
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1658
|
+
let ctx = canvas.getContext('2d');
|
|
1659
|
+
ctx.font = this.font;
|
|
1660
|
+
const metrics = ctx.measureText(this.text);
|
|
1661
|
+
// Compute canvas size
|
|
1662
|
+
canvas.width = this.margin[3] + metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight + this.margin[1];
|
|
1663
|
+
canvas.height = this.margin[0] + metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent + this.margin[2];
|
|
1664
|
+
// After change canvas size, the context needs to be re-created
|
|
1665
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1666
|
+
ctx = canvas.getContext('2d');
|
|
1667
|
+
ctx.font = this.font;
|
|
1668
|
+
// Fill background
|
|
1669
|
+
ctx.fillStyle = this.backgroundStyle;
|
|
1670
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
1671
|
+
// Draw text
|
|
1672
|
+
ctx.fillStyle = this.textStyle;
|
|
1673
|
+
ctx.fillText(this.text, this.margin[3] + metrics.actualBoundingBoxLeft, this.margin[0] + metrics.actualBoundingBoxAscent);
|
|
1674
|
+
// Apply texture
|
|
1675
|
+
const texture = canvas2DataTexture(canvas);
|
|
1676
|
+
texture.magFilter = NearestFilter;
|
|
1677
|
+
texture.minFilter = NearestFilter;
|
|
1678
|
+
this.textMaterial.map = texture;
|
|
1679
|
+
this.textMaterial.needsUpdate = true;
|
|
1680
|
+
// Update size
|
|
1681
|
+
this.scale.x = (canvas.width / canvas.height) * this.height;
|
|
1682
|
+
this.scale.y = this.height;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* The SkinViewer renders the player on a canvas.
|
|
1688
|
+
*/
|
|
1689
|
+
class SkinViewer {
|
|
1690
|
+
/**
|
|
1691
|
+
* The canvas where the renderer draws its output.
|
|
1692
|
+
*/
|
|
1693
|
+
canvas;
|
|
1694
|
+
ctx;
|
|
1695
|
+
scene;
|
|
1696
|
+
camera;
|
|
1697
|
+
renderer;
|
|
1698
|
+
/**
|
|
1699
|
+
* The OrbitControls component which is used to implement the mouse control function.
|
|
1700
|
+
*
|
|
1701
|
+
* @see {@link https://threejs.org/docs/#examples/en/controls/OrbitControls | OrbitControls - three.js docs}
|
|
1702
|
+
*/
|
|
1703
|
+
// readonly controls: OrbitControls;
|
|
1704
|
+
/**
|
|
1705
|
+
* The player object.
|
|
1706
|
+
*/
|
|
1707
|
+
playerObject;
|
|
1708
|
+
/**
|
|
1709
|
+
* A group that wraps the player object.
|
|
1710
|
+
* It is used to center the player in the world.
|
|
1711
|
+
*/
|
|
1712
|
+
playerWrapper;
|
|
1713
|
+
globalLight = new AmbientLight(0xffffff, 3);
|
|
1714
|
+
cameraLight = new PointLight(0xffffff, 0.6);
|
|
1715
|
+
composer;
|
|
1716
|
+
renderPass;
|
|
1717
|
+
fxaaPass;
|
|
1718
|
+
skinCanvas;
|
|
1719
|
+
capeCanvas;
|
|
1720
|
+
earsCanvas;
|
|
1721
|
+
skinTexture = null;
|
|
1722
|
+
capeTexture = null;
|
|
1723
|
+
earsTexture = null;
|
|
1724
|
+
backgroundTexture = null;
|
|
1725
|
+
_disposed = false;
|
|
1726
|
+
_zoom;
|
|
1727
|
+
/**
|
|
1728
|
+
* Whether to rotate the player along the y axis.
|
|
1729
|
+
*
|
|
1730
|
+
* @defaultValue `false`
|
|
1731
|
+
*/
|
|
1732
|
+
autoRotate = false;
|
|
1733
|
+
/**
|
|
1734
|
+
* The angular velocity of the player, in rad/s.
|
|
1735
|
+
*
|
|
1736
|
+
* @defaultValue `1.0`
|
|
1737
|
+
* @see {@link autoRotate}
|
|
1738
|
+
*/
|
|
1739
|
+
autoRotateSpeed = 1.0;
|
|
1740
|
+
_animation;
|
|
1741
|
+
// private clock: Clock;
|
|
1742
|
+
// private animationID: number | null;
|
|
1743
|
+
// private onContextLost: (event: Event) => void;
|
|
1744
|
+
// private onContextRestored: () => void;
|
|
1745
|
+
_pixelRatio;
|
|
1746
|
+
// private devicePixelRatioQuery: MediaQueryList | null;
|
|
1747
|
+
// private onDevicePixelRatioChange: () => void;
|
|
1748
|
+
_nameTag = null;
|
|
1749
|
+
nameTagYOffset = 20;
|
|
1750
|
+
_loadPromiseArr = [];
|
|
1751
|
+
/** 读取各部位 */
|
|
1752
|
+
ready;
|
|
1753
|
+
constructor(options = {}) {
|
|
1754
|
+
this.canvas = options.canvas || new Canvas();
|
|
1755
|
+
this.ctx = options.ctx || gl(300, 300, { preserveDrawingBuffer: options.preserveDrawingBuffer });
|
|
1756
|
+
this.skinCanvas = new Canvas();
|
|
1757
|
+
this.capeCanvas = new Canvas();
|
|
1758
|
+
this.earsCanvas = new Canvas();
|
|
1759
|
+
this.scene = new Scene();
|
|
1760
|
+
this.camera = new PerspectiveCamera();
|
|
1761
|
+
this.camera.add(this.cameraLight);
|
|
1762
|
+
this.scene.add(this.camera);
|
|
1763
|
+
this.scene.add(this.globalLight);
|
|
1764
|
+
ColorManagement.enabled = false;
|
|
1765
|
+
this.renderer = new WebGLRenderer({
|
|
1766
|
+
canvas: this.canvas,
|
|
1767
|
+
context: this.ctx,
|
|
1768
|
+
preserveDrawingBuffer: options.preserveDrawingBuffer === true // default: false
|
|
1769
|
+
});
|
|
1770
|
+
// this.onDevicePixelRatioChange = () => {
|
|
1771
|
+
// this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
1772
|
+
// this.updateComposerSize();
|
|
1773
|
+
// if (this._pixelRatio === "match-device") {
|
|
1774
|
+
// this.devicePixelRatioQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
1775
|
+
// this.devicePixelRatioQuery.addEventListener("change", this.onDevicePixelRatioChange, { once: true });
|
|
1776
|
+
// }
|
|
1777
|
+
// };
|
|
1778
|
+
// if (options.pixelRatio === undefined || options.pixelRatio === "match-device") {
|
|
1779
|
+
// this._pixelRatio = "match-device";
|
|
1780
|
+
// this.devicePixelRatioQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
1781
|
+
// this.devicePixelRatioQuery.addEventListener("change", this.onDevicePixelRatioChange, { once: true });
|
|
1782
|
+
// this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
1783
|
+
// } else {
|
|
1784
|
+
this._pixelRatio = options.pixelRatio || 1;
|
|
1785
|
+
// this.devicePixelRatioQuery = null;
|
|
1786
|
+
this.renderer.setPixelRatio(1);
|
|
1787
|
+
// }
|
|
1788
|
+
this.renderer.setClearColor(0, 0);
|
|
1789
|
+
// if (this.renderer.capabilities.isWebGL2) {
|
|
1790
|
+
// Use float precision depth if possible
|
|
1791
|
+
// see https://github.com/bs-community/skinview3d/issues/111
|
|
1792
|
+
const renderTarget = new WebGLRenderTarget(0, 0, {
|
|
1793
|
+
// depthTexture: new DepthTexture(0, 0, FloatType),
|
|
1794
|
+
minFilter: LinearFilter,
|
|
1795
|
+
magFilter: LinearFilter,
|
|
1796
|
+
format: RGBAFormat,
|
|
1797
|
+
type: UnsignedByteType
|
|
1798
|
+
});
|
|
1799
|
+
// }
|
|
1800
|
+
this.composer = new EffectComposer(this.renderer, renderTarget);
|
|
1801
|
+
this.renderPass = new RenderPass(this.scene, this.camera);
|
|
1802
|
+
this.fxaaPass = new ShaderPass(FXAAShader);
|
|
1803
|
+
this.composer.addPass(this.renderPass);
|
|
1804
|
+
this.composer.addPass(this.fxaaPass);
|
|
1805
|
+
this.playerObject = new PlayerObject();
|
|
1806
|
+
this.playerObject.name = 'player';
|
|
1807
|
+
this.playerObject.skin.visible = false;
|
|
1808
|
+
this.playerObject.cape.visible = false;
|
|
1809
|
+
this.playerWrapper = new Group();
|
|
1810
|
+
this.playerWrapper.add(this.playerObject);
|
|
1811
|
+
this.scene.add(this.playerWrapper);
|
|
1812
|
+
// this.controls = new OrbitControls(this.camera, this.canvas);
|
|
1813
|
+
// this.controls.enablePan = false; // disable pan by default
|
|
1814
|
+
// this.controls.minDistance = 10;
|
|
1815
|
+
// this.controls.maxDistance = 256;
|
|
1816
|
+
// if (options.enableControls === false) {
|
|
1817
|
+
// this.controls.enabled = false;
|
|
1818
|
+
// }
|
|
1819
|
+
if (options.skin !== undefined) {
|
|
1820
|
+
this._loadPromiseArr.push(this.loadSkin(options.skin, {
|
|
1821
|
+
model: options.model,
|
|
1822
|
+
ears: options.ears === 'current-skin'
|
|
1823
|
+
}));
|
|
1824
|
+
}
|
|
1825
|
+
if (options.cape !== undefined) {
|
|
1826
|
+
this._loadPromiseArr.push(this.loadCape(options.cape, options.capeLoadOptions));
|
|
1827
|
+
}
|
|
1828
|
+
if (options.ears !== undefined && options.ears !== 'current-skin') {
|
|
1829
|
+
this._loadPromiseArr.push(this.loadEars(options.ears.source, {
|
|
1830
|
+
textureType: options.ears.textureType
|
|
1831
|
+
}));
|
|
1832
|
+
}
|
|
1833
|
+
if (options.width !== undefined) {
|
|
1834
|
+
this.width = options.width;
|
|
1835
|
+
}
|
|
1836
|
+
if (options.height !== undefined) {
|
|
1837
|
+
this.height = options.height;
|
|
1838
|
+
}
|
|
1839
|
+
if (options.background !== undefined) {
|
|
1840
|
+
this.background = options.background;
|
|
1841
|
+
}
|
|
1842
|
+
if (options.panorama !== undefined) {
|
|
1843
|
+
this._loadPromiseArr.push(this.loadPanorama(options.panorama));
|
|
1844
|
+
}
|
|
1845
|
+
if (options.nameTag !== undefined) {
|
|
1846
|
+
this.nameTag = options.nameTag;
|
|
1847
|
+
}
|
|
1848
|
+
this.camera.position.z = 1;
|
|
1849
|
+
this._zoom = options.zoom === undefined ? 0.9 : options.zoom;
|
|
1850
|
+
this.fov = options.fov === undefined ? 50 : options.fov;
|
|
1851
|
+
this._animation = options.animation === undefined ? null : options.animation;
|
|
1852
|
+
// this.clock = new Clock();
|
|
1853
|
+
// this.onContextLost = (event: Event) => {
|
|
1854
|
+
// event.preventDefault();
|
|
1855
|
+
// if (this.animationID !== null) {
|
|
1856
|
+
// window.cancelAnimationFrame(this.animationID);
|
|
1857
|
+
// this.animationID = null;
|
|
1858
|
+
// }
|
|
1859
|
+
// };
|
|
1860
|
+
// this.onContextRestored = () => {
|
|
1861
|
+
// this.renderer.setClearColor(0, 0); // Clear color might be lost
|
|
1862
|
+
// if (!this._renderPaused && !this._disposed && this.animationID === null) {
|
|
1863
|
+
// this.animationID = window.requestAnimationFrame(() => this.draw());
|
|
1864
|
+
// }
|
|
1865
|
+
// };
|
|
1866
|
+
// this.canvas.addEventListener("webglcontextlost", this.onContextLost, false);
|
|
1867
|
+
// this.canvas.addEventListener("webglcontextrestored", this.onContextRestored, false);
|
|
1868
|
+
// this.canvas.addEventListener(
|
|
1869
|
+
// "mousedown",
|
|
1870
|
+
// () => {
|
|
1871
|
+
// this.isUserRotating = true;
|
|
1872
|
+
// },
|
|
1873
|
+
// false
|
|
1874
|
+
// );
|
|
1875
|
+
// this.canvas.addEventListener(
|
|
1876
|
+
// "mouseup",
|
|
1877
|
+
// () => {
|
|
1878
|
+
// this.isUserRotating = false;
|
|
1879
|
+
// },
|
|
1880
|
+
// false
|
|
1881
|
+
// );
|
|
1882
|
+
// this.canvas.addEventListener(
|
|
1883
|
+
// "touchmove",
|
|
1884
|
+
// e => {
|
|
1885
|
+
// if (e.touches.length === 1) {
|
|
1886
|
+
// this.isUserRotating = true;
|
|
1887
|
+
// } else {
|
|
1888
|
+
// this.isUserRotating = false;
|
|
1889
|
+
// }
|
|
1890
|
+
// },
|
|
1891
|
+
// false
|
|
1892
|
+
// );
|
|
1893
|
+
// this.canvas.addEventListener(
|
|
1894
|
+
// "touchend",
|
|
1895
|
+
// () => {
|
|
1896
|
+
// this.isUserRotating = false;
|
|
1897
|
+
// },
|
|
1898
|
+
// false
|
|
1899
|
+
// );
|
|
1900
|
+
this.ready = Promise.allSettled(this._loadPromiseArr);
|
|
1901
|
+
}
|
|
1902
|
+
updateComposerSize() {
|
|
1903
|
+
this.composer.setSize(this.width, this.height);
|
|
1904
|
+
const pixelRatio = this.renderer.getPixelRatio();
|
|
1905
|
+
this.composer.setPixelRatio(pixelRatio);
|
|
1906
|
+
this.fxaaPass.material.uniforms['resolution'].value.x = 1 / (this.width * pixelRatio);
|
|
1907
|
+
this.fxaaPass.material.uniforms['resolution'].value.y = 1 / (this.height * pixelRatio);
|
|
1908
|
+
}
|
|
1909
|
+
recreateSkinTexture() {
|
|
1910
|
+
if (this.skinTexture !== null) {
|
|
1911
|
+
this.skinTexture.dispose();
|
|
1912
|
+
}
|
|
1913
|
+
this.skinTexture = canvas2DataTexture(this.skinCanvas);
|
|
1914
|
+
this.skinTexture.magFilter = NearestFilter;
|
|
1915
|
+
this.skinTexture.minFilter = NearestFilter;
|
|
1916
|
+
this.playerObject.skin.map = this.skinTexture;
|
|
1917
|
+
}
|
|
1918
|
+
recreateCapeTexture() {
|
|
1919
|
+
if (this.capeTexture !== null) {
|
|
1920
|
+
this.capeTexture.dispose();
|
|
1921
|
+
}
|
|
1922
|
+
this.capeTexture = canvas2DataTexture(this.capeCanvas);
|
|
1923
|
+
this.capeTexture.magFilter = NearestFilter;
|
|
1924
|
+
this.capeTexture.minFilter = NearestFilter;
|
|
1925
|
+
this.playerObject.cape.map = this.capeTexture;
|
|
1926
|
+
this.playerObject.elytra.map = this.capeTexture;
|
|
1927
|
+
}
|
|
1928
|
+
recreateEarsTexture() {
|
|
1929
|
+
if (this.earsTexture !== null) {
|
|
1930
|
+
this.earsTexture.dispose();
|
|
1931
|
+
}
|
|
1932
|
+
this.earsTexture = canvas2DataTexture(this.earsCanvas);
|
|
1933
|
+
this.earsTexture.magFilter = NearestFilter;
|
|
1934
|
+
this.earsTexture.minFilter = NearestFilter;
|
|
1935
|
+
this.playerObject.ears.map = this.earsTexture;
|
|
1936
|
+
}
|
|
1937
|
+
loadSkin(source, options = {}) {
|
|
1938
|
+
if (source === null) {
|
|
1939
|
+
this.resetSkin();
|
|
1940
|
+
}
|
|
1941
|
+
else if (isTextureSource(source)) {
|
|
1942
|
+
loadSkinToCanvas(this.skinCanvas, source);
|
|
1943
|
+
this.recreateSkinTexture();
|
|
1944
|
+
if (options.model === undefined || options.model === 'auto-detect') {
|
|
1945
|
+
this.playerObject.skin.modelType = inferModelType(this.skinCanvas);
|
|
1946
|
+
}
|
|
1947
|
+
else {
|
|
1948
|
+
this.playerObject.skin.modelType = options.model;
|
|
1949
|
+
}
|
|
1950
|
+
if (options.makeVisible !== false) {
|
|
1951
|
+
this.playerObject.skin.visible = true;
|
|
1952
|
+
}
|
|
1953
|
+
if (options.ears === true || options.ears == 'load-only') {
|
|
1954
|
+
loadEarsToCanvasFromSkin(this.earsCanvas, source);
|
|
1955
|
+
this.recreateEarsTexture();
|
|
1956
|
+
if (options.ears === true) {
|
|
1957
|
+
this.playerObject.ears.visible = true;
|
|
1958
|
+
if (this._nameTag) {
|
|
1959
|
+
this.nameTagYOffset = 25;
|
|
1960
|
+
this._nameTag.position.y = this.nameTagYOffset;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
else {
|
|
1966
|
+
return loadImage(source).then(image => this.loadSkin(image, options));
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
resetSkin() {
|
|
1970
|
+
this.playerObject.skin.visible = false;
|
|
1971
|
+
this.playerObject.skin.map = null;
|
|
1972
|
+
if (this.skinTexture !== null) {
|
|
1973
|
+
this.skinTexture.dispose();
|
|
1974
|
+
this.skinTexture = null;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
loadCape(source, options = {}) {
|
|
1978
|
+
if (source === null) {
|
|
1979
|
+
this.resetCape();
|
|
1980
|
+
}
|
|
1981
|
+
else if (isTextureSource(source)) {
|
|
1982
|
+
loadCapeToCanvas(this.capeCanvas, source);
|
|
1983
|
+
this.recreateCapeTexture();
|
|
1984
|
+
if (options.makeVisible !== false) {
|
|
1985
|
+
this.playerObject.backEquipment = options.backEquipment || 'cape';
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
else {
|
|
1989
|
+
return loadImage(source).then(image => this.loadCape(image, options));
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
resetCape() {
|
|
1993
|
+
this.playerObject.backEquipment = null;
|
|
1994
|
+
this.playerObject.cape.map = null;
|
|
1995
|
+
this.playerObject.elytra.map = null;
|
|
1996
|
+
if (this.capeTexture !== null) {
|
|
1997
|
+
this.capeTexture.dispose();
|
|
1998
|
+
this.capeTexture = null;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
loadEars(source, options = {}) {
|
|
2002
|
+
if (source === null) {
|
|
2003
|
+
this.resetEars();
|
|
2004
|
+
}
|
|
2005
|
+
else if (isTextureSource(source)) {
|
|
2006
|
+
if (options.textureType === 'skin') {
|
|
2007
|
+
loadEarsToCanvasFromSkin(this.earsCanvas, source);
|
|
2008
|
+
}
|
|
2009
|
+
else {
|
|
2010
|
+
loadEarsToCanvas(this.earsCanvas, source);
|
|
2011
|
+
}
|
|
2012
|
+
this.recreateEarsTexture();
|
|
2013
|
+
if (options.makeVisible !== false) {
|
|
2014
|
+
this.playerObject.ears.visible = true;
|
|
2015
|
+
if (this._nameTag) {
|
|
2016
|
+
this.nameTagYOffset = 25;
|
|
2017
|
+
this._nameTag.position.y = this.nameTagYOffset;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
else {
|
|
2022
|
+
return loadImage(source).then(image => this.loadEars(image, options));
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
resetEars() {
|
|
2026
|
+
this.playerObject.ears.visible = false;
|
|
2027
|
+
if (this._nameTag) {
|
|
2028
|
+
this.nameTagYOffset = 20;
|
|
2029
|
+
this._nameTag.position.y = this.nameTagYOffset;
|
|
2030
|
+
}
|
|
2031
|
+
this.playerObject.ears.map = null;
|
|
2032
|
+
if (this.earsTexture !== null) {
|
|
2033
|
+
this.earsTexture.dispose();
|
|
2034
|
+
this.earsTexture = null;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
loadPanorama(source) {
|
|
2038
|
+
return this.loadBackground(source, EquirectangularReflectionMapping);
|
|
2039
|
+
}
|
|
2040
|
+
loadBackground(source, mapping) {
|
|
2041
|
+
if (isTextureSource(source)) {
|
|
2042
|
+
if (this.backgroundTexture !== null) {
|
|
2043
|
+
this.backgroundTexture.dispose();
|
|
2044
|
+
}
|
|
2045
|
+
this.backgroundTexture = new Texture();
|
|
2046
|
+
this.backgroundTexture.image = source;
|
|
2047
|
+
if (mapping !== undefined) {
|
|
2048
|
+
this.backgroundTexture.mapping = mapping;
|
|
2049
|
+
}
|
|
2050
|
+
this.backgroundTexture.needsUpdate = true;
|
|
2051
|
+
this.scene.background = this.backgroundTexture;
|
|
2052
|
+
}
|
|
2053
|
+
else {
|
|
2054
|
+
return loadImage(source).then(image => this.loadBackground(image, mapping));
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
toRGBA() {
|
|
2058
|
+
const buf = new Uint8Array(this.width * this.height * 4);
|
|
2059
|
+
this.ctx.readPixels(0, 0, this.width, this.height, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, buf);
|
|
2060
|
+
const res = new Uint8Array(this.width * this.height * 4);
|
|
2061
|
+
for (let y = 0; y < this.height; y++) {
|
|
2062
|
+
for (let x = 0; x < this.width; x++) {
|
|
2063
|
+
const srcIdx = ((this.height - 1 - y) * this.width + x) * 4;
|
|
2064
|
+
const dstIdx = (y * this.width + x) * 4;
|
|
2065
|
+
res[dstIdx] = buf[srcIdx];
|
|
2066
|
+
res[dstIdx + 1] = buf[srcIdx + 1];
|
|
2067
|
+
res[dstIdx + 2] = buf[srcIdx + 2];
|
|
2068
|
+
res[dstIdx + 3] = buf[srcIdx + 3];
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return res;
|
|
2072
|
+
}
|
|
2073
|
+
toBuffer(format = 'png', options) {
|
|
2074
|
+
const buf = this.toRGBA();
|
|
2075
|
+
const outCanvas = new Canvas(this.width, this.height);
|
|
2076
|
+
const ctx2d = outCanvas.getContext('2d');
|
|
2077
|
+
const imageData = ctx2d.createImageData(this.width, this.height);
|
|
2078
|
+
for (let y = 0; y < this.height; y++) {
|
|
2079
|
+
for (let x = 0; x < this.width; x++) {
|
|
2080
|
+
const srcIdx = (y * this.width + x) * 4;
|
|
2081
|
+
const dstIdx = (y * this.width + x) * 4;
|
|
2082
|
+
imageData.data[dstIdx] = buf[srcIdx];
|
|
2083
|
+
imageData.data[dstIdx + 1] = buf[srcIdx + 1];
|
|
2084
|
+
imageData.data[dstIdx + 2] = buf[srcIdx + 2];
|
|
2085
|
+
imageData.data[dstIdx + 3] = buf[srcIdx + 3];
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
ctx2d.putImageData(imageData, 0, 0);
|
|
2089
|
+
return outCanvas.toBufferSync(format, options);
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Renders the scene to the canvas.
|
|
2093
|
+
* This method does not change the animation progress.
|
|
2094
|
+
*/
|
|
2095
|
+
render() {
|
|
2096
|
+
this.composer.render();
|
|
2097
|
+
}
|
|
2098
|
+
renderAnimationFrame(progress, binary = false) {
|
|
2099
|
+
if (this._animation == null) {
|
|
2100
|
+
throw new Error('No animation.');
|
|
2101
|
+
}
|
|
2102
|
+
return () => {
|
|
2103
|
+
this._animation.render(this.playerObject, progress);
|
|
2104
|
+
this.render();
|
|
2105
|
+
return binary ? this.toBuffer('png') : this.toRGBA();
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
renderAnimationLoop(frames = 30, binary = false) {
|
|
2109
|
+
if (this._animation == null) {
|
|
2110
|
+
throw new Error('No animation.');
|
|
2111
|
+
}
|
|
2112
|
+
return new Array(frames).fill(null).map((_, i) => {
|
|
2113
|
+
return () => {
|
|
2114
|
+
// this.playerObject.resetJoints();
|
|
2115
|
+
// this.playerObject.position.set(0, 0, 0);
|
|
2116
|
+
// this.playerObject.rotation.set(0, 0, 0);
|
|
2117
|
+
this._animation.render(this.playerObject, i / frames);
|
|
2118
|
+
this.render();
|
|
2119
|
+
return binary ? this.toBuffer('png') : this.toRGBA();
|
|
2120
|
+
};
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
setSize(width, height) {
|
|
2124
|
+
this.camera.aspect = width / height;
|
|
2125
|
+
this.camera.updateProjectionMatrix();
|
|
2126
|
+
this.renderer.setSize(width, height);
|
|
2127
|
+
this.updateComposerSize();
|
|
2128
|
+
}
|
|
2129
|
+
dispose() {
|
|
2130
|
+
this._disposed = true;
|
|
2131
|
+
// this.canvas.removeEventListener("webglcontextlost", this.onContextLost, false);
|
|
2132
|
+
// this.canvas.removeEventListener("webglcontextrestored", this.onContextRestored, false);
|
|
2133
|
+
// if (this.devicePixelRatioQuery !== null) {
|
|
2134
|
+
// this.devicePixelRatioQuery.removeEventListener("change", this.onDevicePixelRatioChange);
|
|
2135
|
+
// this.devicePixelRatioQuery = null;
|
|
2136
|
+
// }
|
|
2137
|
+
// if (this.animationID !== null) {
|
|
2138
|
+
// window.cancelAnimationFrame(this.animationID);
|
|
2139
|
+
// this.animationID = null;
|
|
2140
|
+
// }
|
|
2141
|
+
// this.controls.dispose();
|
|
2142
|
+
try {
|
|
2143
|
+
this.renderer.dispose();
|
|
2144
|
+
}
|
|
2145
|
+
catch (error) {
|
|
2146
|
+
// https://github.com/mrdoob/three.js/issues/33157
|
|
2147
|
+
}
|
|
2148
|
+
this.resetSkin();
|
|
2149
|
+
this.resetCape();
|
|
2150
|
+
this.resetEars();
|
|
2151
|
+
this.background = null;
|
|
2152
|
+
this.fxaaPass.fsQuad.dispose();
|
|
2153
|
+
}
|
|
2154
|
+
get disposed() {
|
|
2155
|
+
return this._disposed;
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Whether rendering and animations are paused.
|
|
2159
|
+
* Setting this property to true will stop both rendering and animation loops.
|
|
2160
|
+
* Setting it back to false will resume them.
|
|
2161
|
+
*/
|
|
2162
|
+
// get renderPaused(): boolean {
|
|
2163
|
+
// return this._renderPaused;
|
|
2164
|
+
// }
|
|
2165
|
+
// set renderPaused(value: boolean) {
|
|
2166
|
+
// this._renderPaused = value;
|
|
2167
|
+
// if (this._renderPaused && this.animationID !== null) {
|
|
2168
|
+
// window.cancelAnimationFrame(this.animationID);
|
|
2169
|
+
// this.animationID = null;
|
|
2170
|
+
// this.clock.stop();
|
|
2171
|
+
// this.clock.autoStart = true;
|
|
2172
|
+
// } else if (
|
|
2173
|
+
// !this._renderPaused &&
|
|
2174
|
+
// !this._disposed &&
|
|
2175
|
+
// !this.renderer.getContext().isContextLost() &&
|
|
2176
|
+
// this.animationID == null
|
|
2177
|
+
// ) {
|
|
2178
|
+
// this.animationID = window.requestAnimationFrame(() => this.draw());
|
|
2179
|
+
// }
|
|
2180
|
+
// }
|
|
2181
|
+
get width() {
|
|
2182
|
+
return this.renderer.getSize(new Vector2()).width;
|
|
2183
|
+
}
|
|
2184
|
+
set width(newWidth) {
|
|
2185
|
+
this.setSize(newWidth, this.height);
|
|
2186
|
+
}
|
|
2187
|
+
get height() {
|
|
2188
|
+
return this.renderer.getSize(new Vector2()).height;
|
|
2189
|
+
}
|
|
2190
|
+
set height(newHeight) {
|
|
2191
|
+
this.setSize(this.width, newHeight);
|
|
2192
|
+
}
|
|
2193
|
+
get background() {
|
|
2194
|
+
return this.scene.background;
|
|
2195
|
+
}
|
|
2196
|
+
set background(value) {
|
|
2197
|
+
if (value === null || value instanceof Color || value instanceof Texture) {
|
|
2198
|
+
this.scene.background = value;
|
|
2199
|
+
}
|
|
2200
|
+
else {
|
|
2201
|
+
this.scene.background = new Color(value);
|
|
2202
|
+
}
|
|
2203
|
+
if (this.backgroundTexture !== null && value !== this.backgroundTexture) {
|
|
2204
|
+
this.backgroundTexture.dispose();
|
|
2205
|
+
this.backgroundTexture = null;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
adjustCameraDistance() {
|
|
2209
|
+
let distance = 4.5 + 16.5 / Math.tan(((this.fov / 180) * Math.PI) / 2) / this.zoom;
|
|
2210
|
+
// limit distance between 10 ~ 256 (default min / max distance of OrbitControls)
|
|
2211
|
+
if (distance < 10) {
|
|
2212
|
+
distance = 10;
|
|
2213
|
+
}
|
|
2214
|
+
else if (distance > 256) {
|
|
2215
|
+
distance = 256;
|
|
2216
|
+
}
|
|
2217
|
+
this.camera.position.multiplyScalar(distance / this.camera.position.length());
|
|
2218
|
+
this.camera.updateProjectionMatrix();
|
|
2219
|
+
}
|
|
2220
|
+
resetCameraPose() {
|
|
2221
|
+
this.camera.position.set(0, 0, 1);
|
|
2222
|
+
this.camera.rotation.set(0, 0, 0);
|
|
2223
|
+
this.adjustCameraDistance();
|
|
2224
|
+
}
|
|
2225
|
+
get fov() {
|
|
2226
|
+
return this.camera.fov;
|
|
2227
|
+
}
|
|
2228
|
+
set fov(value) {
|
|
2229
|
+
this.camera.fov = value;
|
|
2230
|
+
this.adjustCameraDistance();
|
|
2231
|
+
}
|
|
2232
|
+
get zoom() {
|
|
2233
|
+
return this._zoom;
|
|
2234
|
+
}
|
|
2235
|
+
set zoom(value) {
|
|
2236
|
+
this._zoom = value;
|
|
2237
|
+
this.adjustCameraDistance();
|
|
2238
|
+
}
|
|
2239
|
+
get pixelRatio() {
|
|
2240
|
+
return this._pixelRatio;
|
|
2241
|
+
}
|
|
2242
|
+
set pixelRatio(newValue) {
|
|
2243
|
+
// if (newValue === "match-device") {
|
|
2244
|
+
// if (this._pixelRatio !== "match-device") {
|
|
2245
|
+
// this._pixelRatio = newValue;
|
|
2246
|
+
// this.onDevicePixelRatioChange();
|
|
2247
|
+
// }
|
|
2248
|
+
// } else {
|
|
2249
|
+
// if (this._pixelRatio === "match-device" && this.devicePixelRatioQuery !== null) {
|
|
2250
|
+
// this.devicePixelRatioQuery.removeEventListener("change", this.onDevicePixelRatioChange);
|
|
2251
|
+
// this.devicePixelRatioQuery = null;
|
|
2252
|
+
// }
|
|
2253
|
+
this._pixelRatio = newValue;
|
|
2254
|
+
this.renderer.setPixelRatio(newValue);
|
|
2255
|
+
this.updateComposerSize();
|
|
2256
|
+
// }
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* The animation that is current playing, or `null` if no animation is playing.
|
|
2260
|
+
*
|
|
2261
|
+
* Setting this property to a different value will change the current animation.
|
|
2262
|
+
* The player's pose and the progress of the new animation will be reset before playing.
|
|
2263
|
+
*
|
|
2264
|
+
* Setting this property to `null` will stop the current animation and reset the player's pose.
|
|
2265
|
+
*/
|
|
2266
|
+
get animation() {
|
|
2267
|
+
return this._animation;
|
|
2268
|
+
}
|
|
2269
|
+
set animation(animation) {
|
|
2270
|
+
if (this._animation !== animation) {
|
|
2271
|
+
this.playerObject.resetJoints();
|
|
2272
|
+
this.playerObject.position.set(0, 0, 0);
|
|
2273
|
+
this.playerObject.rotation.set(0, 0, 0);
|
|
2274
|
+
if (this._nameTag) {
|
|
2275
|
+
this._nameTag.position.y = this.nameTagYOffset;
|
|
2276
|
+
}
|
|
2277
|
+
// this.clock.stop();
|
|
2278
|
+
// this.clock.autoStart = true;
|
|
2279
|
+
}
|
|
2280
|
+
// if (animation !== null) {
|
|
2281
|
+
// animation.progress = 0;
|
|
2282
|
+
// }
|
|
2283
|
+
this._animation = animation;
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* The name tag to display above the player, or `null` if there is none.
|
|
2287
|
+
*
|
|
2288
|
+
* When setting this property to a `string` value, a {@link NameTagObject}
|
|
2289
|
+
* will be automatically created with default options.
|
|
2290
|
+
*
|
|
2291
|
+
* @example
|
|
2292
|
+
* ```
|
|
2293
|
+
* skinViewer.nameTag = "hello";
|
|
2294
|
+
* skinViewer.nameTag = new NameTagObject("hello", { textStyle: "yellow" });
|
|
2295
|
+
* skinViewer.nameTag = null;
|
|
2296
|
+
* ```
|
|
2297
|
+
*/
|
|
2298
|
+
get nameTag() {
|
|
2299
|
+
return this._nameTag;
|
|
2300
|
+
}
|
|
2301
|
+
set nameTag(newVal) {
|
|
2302
|
+
if (this._nameTag !== null) {
|
|
2303
|
+
// Remove the old name tag from the scene
|
|
2304
|
+
this.playerWrapper.remove(this._nameTag);
|
|
2305
|
+
}
|
|
2306
|
+
if (newVal !== null) {
|
|
2307
|
+
if (!(newVal instanceof Object3D)) {
|
|
2308
|
+
newVal = new NameTagObject(newVal);
|
|
2309
|
+
}
|
|
2310
|
+
// Add the new name tag to the scene
|
|
2311
|
+
this.playerWrapper.add(newVal);
|
|
2312
|
+
// Set y position
|
|
2313
|
+
this.nameTagYOffset = this.playerObject.ears.visible ? 25 : 20;
|
|
2314
|
+
newVal.position.y = this.nameTagYOffset;
|
|
2315
|
+
}
|
|
2316
|
+
this._nameTag = newVal;
|
|
2317
|
+
this.playerObject.nameTag = newVal || undefined;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
const defaultParams = {
|
|
2322
|
+
multiple: {
|
|
2323
|
+
value: 1,
|
|
2324
|
+
desc: '整数倍频率'
|
|
2325
|
+
},
|
|
2326
|
+
delay: {
|
|
2327
|
+
value: 5,
|
|
2328
|
+
desc: '每帧延迟时间 1/100ms, 1=10ms'
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
function getParamsValue(params) {
|
|
2332
|
+
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, value.value]));
|
|
2333
|
+
}
|
|
2334
|
+
class PlayerAnimation {
|
|
2335
|
+
static params = {
|
|
2336
|
+
...defaultParams
|
|
2337
|
+
};
|
|
2338
|
+
// 这一层只需要暴露一个接口给外部调用
|
|
2339
|
+
render(player, progress) {
|
|
2340
|
+
// 确保 progress 在 0-1 之间 (可选,视需求而定)
|
|
2341
|
+
// const p = progress % 1.0;
|
|
2342
|
+
this.animate(player, progress);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
/**
|
|
2346
|
+
* A class that helps you create an animation from a function.
|
|
2347
|
+
*
|
|
2348
|
+
* @example
|
|
2349
|
+
* To create an animation that rotates the player:
|
|
2350
|
+
* ```
|
|
2351
|
+
* new FunctionAnimation((player, progress) => player.rotation.y = progress)
|
|
2352
|
+
* ```
|
|
2353
|
+
*/
|
|
2354
|
+
class FunctionAnimation extends PlayerAnimation {
|
|
2355
|
+
static title = '函数';
|
|
2356
|
+
static params = {
|
|
2357
|
+
...defaultParams
|
|
2358
|
+
};
|
|
2359
|
+
params = getParamsValue(PlayerAnimation.params);
|
|
2360
|
+
fn;
|
|
2361
|
+
constructor(fn) {
|
|
2362
|
+
super();
|
|
2363
|
+
this.fn = fn;
|
|
2364
|
+
}
|
|
2365
|
+
animate(player, progress) {
|
|
2366
|
+
this.fn(player, progress);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
class IdleAnimation extends PlayerAnimation {
|
|
2370
|
+
static title = '常态';
|
|
2371
|
+
static params = {
|
|
2372
|
+
...defaultParams
|
|
2373
|
+
};
|
|
2374
|
+
params = getParamsValue(IdleAnimation.params);
|
|
2375
|
+
animate(player, progress) {
|
|
2376
|
+
// Multiply by animation's natural speed
|
|
2377
|
+
const t = progress * 2 * Math.PI * this.params.multiple;
|
|
2378
|
+
// Arm swing
|
|
2379
|
+
const basicArmRotationZ = Math.PI * 0.02;
|
|
2380
|
+
player.skin.leftArm.rotation.z = Math.cos(t) * 0.03 + basicArmRotationZ;
|
|
2381
|
+
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ;
|
|
2382
|
+
// Always add an angle for cape around the x axis
|
|
2383
|
+
const basicCapeRotationX = Math.PI * 0.06;
|
|
2384
|
+
player.cape.rotation.x = Math.sin(t) * 0.01 + basicCapeRotationX;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
class WalkingAnimation extends PlayerAnimation {
|
|
2388
|
+
/**
|
|
2389
|
+
* Whether to shake head when walking.
|
|
2390
|
+
*
|
|
2391
|
+
* @defaultValue `true`
|
|
2392
|
+
*/
|
|
2393
|
+
// headBobbing: boolean = true;
|
|
2394
|
+
static title = '行走';
|
|
2395
|
+
static params = {
|
|
2396
|
+
...defaultParams,
|
|
2397
|
+
headBobbing: {
|
|
2398
|
+
value: true,
|
|
2399
|
+
desc: '是否摇晃头部'
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
params = getParamsValue(WalkingAnimation.params);
|
|
2403
|
+
animate(player, progress) {
|
|
2404
|
+
// 基础周期:0 -> 2PI
|
|
2405
|
+
// 所有的动作都基于这个 base
|
|
2406
|
+
const t = progress * 2 * Math.PI * this.params.multiple;
|
|
2407
|
+
// 肢体摆动 (频率 1x)
|
|
2408
|
+
player.skin.leftLeg.rotation.x = Math.sin(t) * 0.5;
|
|
2409
|
+
player.skin.rightLeg.rotation.x = Math.sin(t + Math.PI) * 0.5;
|
|
2410
|
+
player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.5;
|
|
2411
|
+
player.skin.rightArm.rotation.x = Math.sin(t) * 0.5;
|
|
2412
|
+
const basicArmRotationZ = Math.PI * 0.02;
|
|
2413
|
+
player.skin.leftArm.rotation.z = Math.cos(t) * 0.03 + basicArmRotationZ;
|
|
2414
|
+
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ;
|
|
2415
|
+
// 头部摇晃 (频率 2x - 保证在 0-1 周期内摇晃整数次)
|
|
2416
|
+
// 原版是 /4, /5,这里为了完美闭环,必须设为整数倍
|
|
2417
|
+
// 比如设定:走一圈,头晃 2 次
|
|
2418
|
+
if (this.params.headBobbing) {
|
|
2419
|
+
// Head shaking with different frequency & amplitude
|
|
2420
|
+
player.skin.head.rotation.y = Math.sin(t * 1) * 0.1; // 左右晃
|
|
2421
|
+
player.skin.head.rotation.x = Math.sin(t * 2) * 0.05; // 上下晃
|
|
2422
|
+
}
|
|
2423
|
+
else {
|
|
2424
|
+
player.skin.head.rotation.y = 0;
|
|
2425
|
+
player.skin.head.rotation.x = 0;
|
|
2426
|
+
}
|
|
2427
|
+
// 披风 (频率 1x 或 2x)
|
|
2428
|
+
player.cape.rotation.x = Math.sin(t) * 0.06 + Math.PI * 0.06;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
class RunningAnimation extends PlayerAnimation {
|
|
2432
|
+
static title = '跑步';
|
|
2433
|
+
static params = {
|
|
2434
|
+
...defaultParams,
|
|
2435
|
+
multiply: {
|
|
2436
|
+
value: 3,
|
|
2437
|
+
desc: defaultParams.multiple.desc
|
|
2438
|
+
}
|
|
2439
|
+
// jump: {
|
|
2440
|
+
// value: true,
|
|
2441
|
+
// desc: '是否跳起'
|
|
2442
|
+
// }
|
|
2443
|
+
};
|
|
2444
|
+
params = getParamsValue(RunningAnimation.params);
|
|
2445
|
+
animate(player, progress) {
|
|
2446
|
+
// 基础周期:0 -> 2PI
|
|
2447
|
+
const t = progress * 2 * Math.PI * this.params.multiple + Math.PI * 0.5; // 保留相位偏移
|
|
2448
|
+
// Leg swing with larger amplitude
|
|
2449
|
+
player.skin.leftLeg.rotation.x = Math.cos(t + Math.PI) * 1.3;
|
|
2450
|
+
player.skin.rightLeg.rotation.x = Math.cos(t) * 1.3;
|
|
2451
|
+
// Arm swing
|
|
2452
|
+
player.skin.leftArm.rotation.x = Math.cos(t) * 1.5;
|
|
2453
|
+
player.skin.rightArm.rotation.x = Math.cos(t + Math.PI) * 1.5;
|
|
2454
|
+
// 身体跳动 (频率 2x)
|
|
2455
|
+
// 跑一步跳一下,左右各一步,所以跳两下 -> 2x
|
|
2456
|
+
player.position.y = Math.cos(t * 2);
|
|
2457
|
+
// Dodging when running
|
|
2458
|
+
player.position.x = Math.cos(t) * 0.15;
|
|
2459
|
+
// Slightly tilting when running
|
|
2460
|
+
player.rotation.z = Math.cos(t + Math.PI) * 0.01;
|
|
2461
|
+
// Apply higher swing frequency, lower amplitude,
|
|
2462
|
+
// and greater basic rotation around x axis,
|
|
2463
|
+
// to cape when running.
|
|
2464
|
+
// 披风 (频率 2x)
|
|
2465
|
+
player.cape.rotation.x = Math.sin(t * 2) * 0.1 + Math.PI * 0.3;
|
|
2466
|
+
// What about head shaking?
|
|
2467
|
+
// You shouldn't glance right and left when running dude :P
|
|
2468
|
+
if (player.nameTag)
|
|
2469
|
+
player.nameTag.position.y = player.position.y + 20;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
function clamp(num, min, max) {
|
|
2473
|
+
return Math.min(Math.max(num, min), max);
|
|
2474
|
+
}
|
|
2475
|
+
class FlyingAnimation extends PlayerAnimation {
|
|
2476
|
+
static title = '飞行';
|
|
2477
|
+
static params = {
|
|
2478
|
+
...defaultParams,
|
|
2479
|
+
multiple: {
|
|
2480
|
+
value: 2,
|
|
2481
|
+
desc: defaultParams.multiple.desc
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
params = getParamsValue(FlyingAnimation.params);
|
|
2485
|
+
animate(player, progress) {
|
|
2486
|
+
// Body rotation finishes in 0.5s
|
|
2487
|
+
// Elytra expansion finishes in 3.3s
|
|
2488
|
+
const t = progress * Math.PI * 2 * this.params.multiple;
|
|
2489
|
+
const startProgress = clamp((t * t) / 100, 0, 1);
|
|
2490
|
+
player.rotation.x = (startProgress * Math.PI) / 2;
|
|
2491
|
+
player.skin.head.rotation.x = startProgress > 0.5 ? Math.PI / 4 - player.rotation.x : 0;
|
|
2492
|
+
const basicArmRotationZ = Math.PI * 0.25 * startProgress;
|
|
2493
|
+
player.skin.leftArm.rotation.z = basicArmRotationZ;
|
|
2494
|
+
player.skin.rightArm.rotation.z = -basicArmRotationZ;
|
|
2495
|
+
const elytraRotationX = 0.34906584;
|
|
2496
|
+
const elytraRotationZ = Math.PI / 2;
|
|
2497
|
+
const interpolation = Math.pow(0.9, t);
|
|
2498
|
+
player.elytra.leftWing.rotation.x = elytraRotationX + interpolation * (0.2617994 - elytraRotationX);
|
|
2499
|
+
player.elytra.leftWing.rotation.z = elytraRotationZ + interpolation * (0.2617994 - elytraRotationZ);
|
|
2500
|
+
player.elytra.updateRightWing();
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
class WaveAnimation extends PlayerAnimation {
|
|
2504
|
+
static title = '挥手';
|
|
2505
|
+
static params = {
|
|
2506
|
+
...defaultParams,
|
|
2507
|
+
whichArm: {
|
|
2508
|
+
value: 'left',
|
|
2509
|
+
desc: '挥动哪个胳膊',
|
|
2510
|
+
choices: ['left', 'right', 'both']
|
|
2511
|
+
},
|
|
2512
|
+
sameDirection: {
|
|
2513
|
+
value: true,
|
|
2514
|
+
desc: '挥动方向相同'
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
params = getParamsValue(WaveAnimation.params);
|
|
2518
|
+
animate(player, progress) {
|
|
2519
|
+
const t = progress * 2 * Math.PI * this.params.multiple;
|
|
2520
|
+
const targetArm = this.params.whichArm === 'left' ? player.skin.leftArm : player.skin.rightArm;
|
|
2521
|
+
targetArm.rotation.x = 180;
|
|
2522
|
+
targetArm.rotation.z = Math.sin(t) * 0.5;
|
|
2523
|
+
if (this.params.whichArm === 'both') {
|
|
2524
|
+
player.skin.leftArm.rotation.x = targetArm.rotation.x;
|
|
2525
|
+
player.skin.leftArm.rotation.z = this.params.sameDirection ? targetArm.rotation.z : -Math.sin(t) * 0.5;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
class CrouchAnimation extends PlayerAnimation {
|
|
2530
|
+
static title = '蹲伏';
|
|
2531
|
+
static params = {
|
|
2532
|
+
...defaultParams,
|
|
2533
|
+
showProgress: {
|
|
2534
|
+
value: false,
|
|
2535
|
+
desc: '是否启用平滑过渡。 false (默认): 像 Minecraft 游戏内一样,瞬间在站立和蹲下间切换。true: 平滑地蹲下和起立(适合展示动画)。'
|
|
2536
|
+
},
|
|
2537
|
+
isStatic: {
|
|
2538
|
+
value: false,
|
|
2539
|
+
desc: '是否保持静止的蹲下状态。为 true 则忽略 progress,始终保持完全蹲下的姿态。'
|
|
2540
|
+
},
|
|
2541
|
+
isRunningHitAnimation: {
|
|
2542
|
+
value: false,
|
|
2543
|
+
desc: '是否同时播放攻击(挥手)动画'
|
|
2544
|
+
},
|
|
2545
|
+
hitCycles: {
|
|
2546
|
+
value: 8,
|
|
2547
|
+
desc: '攻击动画的次数'
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
params = getParamsValue(CrouchAnimation.params);
|
|
2551
|
+
animate(player, progress) {
|
|
2552
|
+
// 1. 计算蹲下系数 (crouchFactor)
|
|
2553
|
+
// 范围 0.0 (站立) 到 1.0 (完全蹲下)
|
|
2554
|
+
let crouchFactor;
|
|
2555
|
+
if (this.params.isStatic) {
|
|
2556
|
+
crouchFactor = 1.0; // 始终蹲下
|
|
2557
|
+
}
|
|
2558
|
+
else {
|
|
2559
|
+
// 将 0-1 的进度映射为 0-1-0 的正弦波 (蹲下再站起)
|
|
2560
|
+
// progress: 0 -> 0.5 -> 1.0
|
|
2561
|
+
// angle: 0 -> PI -> 2PI
|
|
2562
|
+
// sin: 0 -> 1 -> 0
|
|
2563
|
+
const t = progress * 2 * Math.PI * this.params.multiple;
|
|
2564
|
+
crouchFactor = Math.abs(Math.sin(t / 2));
|
|
2565
|
+
}
|
|
2566
|
+
// 2. 处理 "瞬间切换" 逻辑 (Minecraft 原版风格)
|
|
2567
|
+
if (!this.params.showProgress && !this.params.isStatic) {
|
|
2568
|
+
// 如果进度 > 0.5 则算蹲下,否则算站立
|
|
2569
|
+
crouchFactor = crouchFactor > (this.params.showProgress ? 0 : 0.4) ? 1.0 : 0.0;
|
|
2570
|
+
}
|
|
2571
|
+
// --- 应用身体位移和旋转 (使用 crouchFactor 插值) ---
|
|
2572
|
+
// 身体前倾
|
|
2573
|
+
player.skin.body.rotation.x = 0.4537860552 * crouchFactor;
|
|
2574
|
+
// 身体位置调整 (Y轴下降, Z轴后退)
|
|
2575
|
+
player.skin.body.position.y = -6 - 2.103677462 * crouchFactor;
|
|
2576
|
+
player.skin.body.position.z = 1.3256181 * crouchFactor - 3.4500310377 * crouchFactor;
|
|
2577
|
+
// 头部位置 (跟随身体下沉)
|
|
2578
|
+
player.skin.head.position.y = -3.618325234674 * crouchFactor;
|
|
2579
|
+
// 手臂位置和旋转
|
|
2580
|
+
// 基础位置偏移
|
|
2581
|
+
const armZ = 3.618325234674 * crouchFactor - 3.4500310377 * crouchFactor;
|
|
2582
|
+
const armY = -2 - 2.53943318 * crouchFactor;
|
|
2583
|
+
player.skin.leftArm.position.z = armZ;
|
|
2584
|
+
player.skin.rightArm.position.z = armZ;
|
|
2585
|
+
player.skin.leftArm.position.y = armY;
|
|
2586
|
+
player.skin.rightArm.position.y = armY;
|
|
2587
|
+
// 手臂旋转 (保持垂直或跟随身体)
|
|
2588
|
+
player.skin.leftArm.rotation.x = 0.410367746202 * crouchFactor;
|
|
2589
|
+
player.skin.rightArm.rotation.x = player.skin.leftArm.rotation.x;
|
|
2590
|
+
// 手臂微张
|
|
2591
|
+
player.skin.leftArm.rotation.z = 0.1;
|
|
2592
|
+
player.skin.rightArm.rotation.z = -0.1;
|
|
2593
|
+
// 腿部位置
|
|
2594
|
+
player.skin.leftLeg.position.z = -3.4500310377 * crouchFactor;
|
|
2595
|
+
player.skin.rightLeg.position.z = -3.4500310377 * crouchFactor;
|
|
2596
|
+
// 披风 (Cape) 调整
|
|
2597
|
+
player.cape.position.y = 8 - 1.851236166577372 * crouchFactor;
|
|
2598
|
+
player.cape.position.z = -2 + 3.786619432 * crouchFactor - 3.4500310377 * crouchFactor;
|
|
2599
|
+
player.cape.rotation.x = (10.8 * Math.PI) / 180 + 0.294220265771 * crouchFactor;
|
|
2600
|
+
// --- 鞘翅 (Elytra) 逻辑重写 ---
|
|
2601
|
+
// 移除 isCrouched 状态,直接基于 crouchFactor 计算位置
|
|
2602
|
+
player.elytra.position.x = player.cape.position.x;
|
|
2603
|
+
player.elytra.position.y = player.cape.position.y;
|
|
2604
|
+
player.elytra.position.z = player.cape.position.z;
|
|
2605
|
+
player.elytra.rotation.x = player.cape.rotation.x - (10.8 * Math.PI) / 180;
|
|
2606
|
+
// 鞘翅开合角度:
|
|
2607
|
+
// 站立时 (crouchFactor=0) -> 0.26 rad
|
|
2608
|
+
// 蹲下时 (crouchFactor=1) -> 0.72 rad (根据原代码逻辑推算)
|
|
2609
|
+
// 使用线性插值替代原有的复杂状态机
|
|
2610
|
+
const wingRotZ = 0.26179944 + 0.4582006 * crouchFactor;
|
|
2611
|
+
player.elytra.leftWing.rotation.z = wingRotZ;
|
|
2612
|
+
player.elytra.updateRightWing();
|
|
2613
|
+
// --- 攻击 (Hit) 动画逻辑 ---
|
|
2614
|
+
if (this.params.isRunningHitAnimation) {
|
|
2615
|
+
if (crouchFactor !== 1) {
|
|
2616
|
+
// 只在蹲下时播放
|
|
2617
|
+
if (crouchFactor === 0) {
|
|
2618
|
+
player.skin.body.rotation.y = 0;
|
|
2619
|
+
}
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
// 为了保证循环完美,攻击频率必须是主循环的整数倍。
|
|
2623
|
+
// 比如主循环是蹲下再起来 (1次),期间挥动 2 次手。
|
|
2624
|
+
const t = progress * 2 * Math.PI * this.params.hitCycles;
|
|
2625
|
+
// 基础手臂旋转 Z
|
|
2626
|
+
const basicArmRotationZ = 0.01 * Math.PI + 0.06;
|
|
2627
|
+
// 右手攻击
|
|
2628
|
+
// 叠加在蹲下的旋转基础上
|
|
2629
|
+
const crouchOffsetX = 0.4537860552 * crouchFactor;
|
|
2630
|
+
// Right Arm Swing
|
|
2631
|
+
player.skin.rightArm.rotation.x = -crouchOffsetX + 2 * Math.sin(t + Math.PI) * 0.3 - crouchOffsetX;
|
|
2632
|
+
player.skin.rightArm.rotation.z = -Math.cos(t) * 0.403 + basicArmRotationZ;
|
|
2633
|
+
// Body Twist
|
|
2634
|
+
player.skin.body.rotation.y = -Math.cos(t) * 0.06;
|
|
2635
|
+
// Left Arm Compensation (摆动平衡)
|
|
2636
|
+
player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.077 + 0.47 * crouchFactor;
|
|
2637
|
+
player.skin.leftArm.rotation.z = -Math.cos(t) * 0.015 + 0.13 - 0.05 * (1 - crouchFactor);
|
|
2638
|
+
// 左手位置微调 (仅在站立时明显,原代码逻辑)
|
|
2639
|
+
// 使用 (1 - crouchFactor) 来限制仅在站立附近生效
|
|
2640
|
+
const standFactor = 1 - crouchFactor;
|
|
2641
|
+
player.skin.leftArm.position.z += Math.cos(t) * 0.3 * standFactor;
|
|
2642
|
+
player.skin.leftArm.position.x = 5 - Math.cos(t) * 0.05 * standFactor;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
class HitAnimation extends PlayerAnimation {
|
|
2647
|
+
// multiple: number = 2;
|
|
2648
|
+
static title = '击打';
|
|
2649
|
+
static params = {
|
|
2650
|
+
...defaultParams,
|
|
2651
|
+
multiple: {
|
|
2652
|
+
value: 10,
|
|
2653
|
+
desc: defaultParams.multiple.desc
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
params = getParamsValue(HitAnimation.params);
|
|
2657
|
+
animate(player, progress) {
|
|
2658
|
+
const t = progress * Math.PI * 2 * this.params.multiple;
|
|
2659
|
+
player.skin.rightArm.rotation.x = -0.4537860552 * 2 + 2 * Math.sin(t + Math.PI) * 0.3;
|
|
2660
|
+
const basicArmRotationZ = 0.01 * Math.PI + 0.06;
|
|
2661
|
+
player.skin.rightArm.rotation.z = -Math.cos(t) * 0.403 + basicArmRotationZ;
|
|
2662
|
+
player.skin.body.rotation.y = -Math.cos(t) * 0.06;
|
|
2663
|
+
player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.077;
|
|
2664
|
+
player.skin.leftArm.rotation.z = -Math.cos(t) * 0.015 + 0.13 - 0.05;
|
|
2665
|
+
player.skin.leftArm.position.z = Math.cos(t) * 0.3;
|
|
2666
|
+
player.skin.leftArm.position.x = 5 - Math.cos(t) * 0.05;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
class SwimAnimation extends PlayerAnimation {
|
|
2670
|
+
static title = '游泳';
|
|
2671
|
+
static params = {
|
|
2672
|
+
...defaultParams,
|
|
2673
|
+
multiple: {
|
|
2674
|
+
value: 1,
|
|
2675
|
+
desc: defaultParams.multiple.desc
|
|
2676
|
+
}
|
|
2677
|
+
};
|
|
2678
|
+
params = getParamsValue(SwimAnimation.params);
|
|
2679
|
+
animate(player, progress) {
|
|
2680
|
+
player.position.y = -5;
|
|
2681
|
+
player.rotation.x = Math.PI / 2;
|
|
2682
|
+
player.skin.head.rotation.x = -Math.PI / 4;
|
|
2683
|
+
player.cape.rotation.x = Math.PI / 4;
|
|
2684
|
+
const loopProgress = (progress * this.params.multiple) % 1;
|
|
2685
|
+
// keyframe timing points
|
|
2686
|
+
const times = [0, 0.7 / 1.3, 1.1 / 1.3, 1.0];
|
|
2687
|
+
const leftEulerDeg = [
|
|
2688
|
+
{ z: 180, y: 180, x: 0 },
|
|
2689
|
+
{ z: 287.2, y: 180, x: 0 },
|
|
2690
|
+
{ z: 180, y: 180, x: 90 },
|
|
2691
|
+
{ z: 180, y: 180, x: 0 }
|
|
2692
|
+
];
|
|
2693
|
+
const rightEulerDeg = [
|
|
2694
|
+
{ z: -180, y: 180, x: 0 },
|
|
2695
|
+
{ z: -287.2, y: 180, x: 0 },
|
|
2696
|
+
{ z: -180, y: 180, x: 90 },
|
|
2697
|
+
{ z: -180, y: 180, x: 0 }
|
|
2698
|
+
];
|
|
2699
|
+
const toRad = Math.PI / 180;
|
|
2700
|
+
function eulerZYXToQuat(z, y, x) {
|
|
2701
|
+
const qz = new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), z);
|
|
2702
|
+
const qy = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), y);
|
|
2703
|
+
const qx = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), x);
|
|
2704
|
+
return qx.multiply(qy).multiply(qz);
|
|
2705
|
+
}
|
|
2706
|
+
const leftQuats = leftEulerDeg.map(e => eulerZYXToQuat(e.z * toRad, e.y * toRad, e.x * toRad));
|
|
2707
|
+
const rightQuats = rightEulerDeg.map(e => eulerZYXToQuat(e.z * toRad, e.y * toRad, e.x * toRad));
|
|
2708
|
+
function findSegment(t) {
|
|
2709
|
+
for (let i = 0; i < times.length - 1; i++) {
|
|
2710
|
+
if (t >= times[i] && t <= times[i + 1]) {
|
|
2711
|
+
return { i, t0: times[i], t1: times[i + 1] };
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
return { i: times.length - 2, t0: times[times.length - 2], t1: times[times.length - 1] };
|
|
2715
|
+
}
|
|
2716
|
+
const seg = findSegment(loopProgress);
|
|
2717
|
+
const p = (loopProgress - seg.t0) / (seg.t1 - seg.t0);
|
|
2718
|
+
const i = seg.i;
|
|
2719
|
+
const qLeft = new Quaternion().copy(leftQuats[i]).slerp(leftQuats[i + 1], p);
|
|
2720
|
+
const qRight = new Quaternion().copy(rightQuats[i]).slerp(rightQuats[i + 1], p);
|
|
2721
|
+
player.skin.leftArm.quaternion.copy(qLeft);
|
|
2722
|
+
player.skin.rightArm.quaternion.copy(qRight);
|
|
2723
|
+
const legAmp = 17.2 * toRad;
|
|
2724
|
+
const legPhase = loopProgress * Math.PI * 2;
|
|
2725
|
+
const leftLegX = legAmp * Math.cos(legPhase + Math.PI);
|
|
2726
|
+
const rightLegX = legAmp * Math.cos(legPhase);
|
|
2727
|
+
player.skin.leftLeg.rotation.x = leftLegX;
|
|
2728
|
+
player.skin.leftLeg.rotation.y = -0.1 * toRad;
|
|
2729
|
+
player.skin.leftLeg.rotation.z = -0.1 * toRad;
|
|
2730
|
+
player.skin.rightLeg.rotation.x = rightLegX;
|
|
2731
|
+
player.skin.rightLeg.rotation.y = 0.1 * toRad;
|
|
2732
|
+
player.skin.rightLeg.rotation.z = 0.1 * toRad;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
class SpinUpAnimation extends PlayerAnimation {
|
|
2736
|
+
static title = '旋转起飞';
|
|
2737
|
+
static params = {
|
|
2738
|
+
...defaultParams,
|
|
2739
|
+
maxHeight: {
|
|
2740
|
+
value: 90,
|
|
2741
|
+
desc: '最高高度'
|
|
2742
|
+
},
|
|
2743
|
+
rotationTurns: {
|
|
2744
|
+
value: 6,
|
|
2745
|
+
desc: '旋转圈数'
|
|
2746
|
+
},
|
|
2747
|
+
/**分头行动 */
|
|
2748
|
+
headOff: {
|
|
2749
|
+
value: false,
|
|
2750
|
+
desc: '分头行动,为 true 时只有头飞走'
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
params = getParamsValue(SpinUpAnimation.params);
|
|
2754
|
+
animate(player, progress) {
|
|
2755
|
+
// --- 1. 手臂平举动画 (前 30% 时间) ---
|
|
2756
|
+
const armProgress = clamp(progress / 0.3, 0, 1);
|
|
2757
|
+
// 使用 sin(x * PI/2) 可以在 x=1 时精确到达 1,实现平滑过渡
|
|
2758
|
+
const armSpreadFactor = Math.sin((armProgress * Math.PI) / 2);
|
|
2759
|
+
player.skin.leftArm.rotation.z = armSpreadFactor * (Math.PI / 2);
|
|
2760
|
+
player.skin.rightArm.rotation.z = -armSpreadFactor * (Math.PI / 2);
|
|
2761
|
+
// --- 2. 身体旋转 ---
|
|
2762
|
+
player.rotation.y = Math.pow(progress, 2) * (Math.PI * 2) * this.params.rotationTurns;
|
|
2763
|
+
const a = Math.pow(clamp(progress * 2, 0, 1), 2);
|
|
2764
|
+
player.cape.rotation.x = a * (Math.PI / 2);
|
|
2765
|
+
player.elytra.rotation.x = a * (Math.PI / 2 - 0.2); // -0.2 让鞘翅看起来不偏上
|
|
2766
|
+
// --- 3. 身体上升 (后 70% 时间) ---
|
|
2767
|
+
const flyStartThreshold = 0.3;
|
|
2768
|
+
let riseHeight = 0;
|
|
2769
|
+
if (progress > flyStartThreshold) {
|
|
2770
|
+
// 将 0.3 ~ 1.0 映射为 0 ~ 1
|
|
2771
|
+
const flyProgress = (progress - flyStartThreshold) / (1 - flyStartThreshold);
|
|
2772
|
+
riseHeight = Math.pow(flyProgress, 3) * this.params.maxHeight;
|
|
2773
|
+
}
|
|
2774
|
+
if (this.params.headOff)
|
|
2775
|
+
player.skin.head.position.y = riseHeight;
|
|
2776
|
+
else
|
|
2777
|
+
player.position.y = riseHeight;
|
|
2778
|
+
if (player.nameTag)
|
|
2779
|
+
player.nameTag.position.y = riseHeight + 20;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
class RotateAnimation extends PlayerAnimation {
|
|
2783
|
+
static title = '旋转展示';
|
|
2784
|
+
static params = {
|
|
2785
|
+
...defaultParams
|
|
2786
|
+
};
|
|
2787
|
+
params = getParamsValue(RotateAnimation.params);
|
|
2788
|
+
animate(player, progress) {
|
|
2789
|
+
const t = progress * 2 * Math.PI * this.params.multiple;
|
|
2790
|
+
player.rotation.y = t;
|
|
2791
|
+
player.skin.leftArm.rotation.z = 0.1;
|
|
2792
|
+
player.skin.rightArm.rotation.z = -0.1;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
class NodAnimation extends PlayerAnimation {
|
|
2796
|
+
// delay = 2;
|
|
2797
|
+
// amp = 0.5;
|
|
2798
|
+
static title = '点头';
|
|
2799
|
+
static params = {
|
|
2800
|
+
...defaultParams,
|
|
2801
|
+
delay: {
|
|
2802
|
+
value: 2,
|
|
2803
|
+
desc: defaultParams.delay.desc
|
|
2804
|
+
},
|
|
2805
|
+
amp: {
|
|
2806
|
+
value: 0.5,
|
|
2807
|
+
desc: '点头幅度'
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
2810
|
+
params = getParamsValue(NodAnimation.params);
|
|
2811
|
+
animate(player, progress) {
|
|
2812
|
+
const t = progress * 2 * Math.PI * this.params.multiple;
|
|
2813
|
+
player.skin.head.rotation.x = Math.sin(t) * this.params.amp;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
// 辅助函数:线性插值
|
|
2817
|
+
// 当 t=0 返回 start,t=1 返回 end,中间平滑过渡
|
|
2818
|
+
function lerp(start, end, t) {
|
|
2819
|
+
return start + (end - start) * t;
|
|
2820
|
+
}
|
|
2821
|
+
class FlailAnimation extends PlayerAnimation {
|
|
2822
|
+
// multiple = 2;
|
|
2823
|
+
// delay = 4;
|
|
2824
|
+
static title = '手舞足蹈';
|
|
2825
|
+
static params = {
|
|
2826
|
+
...defaultParams,
|
|
2827
|
+
multiple: {
|
|
2828
|
+
value: 2,
|
|
2829
|
+
desc: defaultParams.multiple.desc
|
|
2830
|
+
},
|
|
2831
|
+
delay: {
|
|
2832
|
+
value: 4,
|
|
2833
|
+
desc: defaultParams.delay.desc
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
params = getParamsValue(FlailAnimation.params);
|
|
2837
|
+
animate(player, progress) {
|
|
2838
|
+
// 1. 定义时间参数
|
|
2839
|
+
// highFreq: 高频震动(用于手脚快速摆动)
|
|
2840
|
+
const highFreq = progress * 2 * Math.PI * this.params.multiple;
|
|
2841
|
+
// lowFreq: 低频变化(用于控制状态过渡),0 -> 1 -> 0
|
|
2842
|
+
// 使用 Math.sin(progress * Math.PI) 可以保证首尾都是 0 (跑步态),中间是 1 (发疯态)
|
|
2843
|
+
// 这样动画循环时是完美的:跑 -> 疯 -> 跑
|
|
2844
|
+
const blendFactor = Math.sin(progress * Math.PI);
|
|
2845
|
+
// 基础摆动幅度
|
|
2846
|
+
const swingRad = Math.sin(highFreq) * 1.9;
|
|
2847
|
+
// === 2. 腿部动作 (保持一直在跑) ===
|
|
2848
|
+
// 腿部不需要过渡,一直保持快速奔跑
|
|
2849
|
+
player.skin.leftLeg.rotation.x = -swingRad * 0.8;
|
|
2850
|
+
player.skin.rightLeg.rotation.x = swingRad * 0.8;
|
|
2851
|
+
// === 3. 手臂动作 (核心修改:平滑混合) ===
|
|
2852
|
+
// 状态 A: 跑步时手臂自然下垂 (Z轴接近0)
|
|
2853
|
+
const armZ_Run = 0.2;
|
|
2854
|
+
// 状态 B: 发疯时手臂平举 (Z轴 90度/PI/2)
|
|
2855
|
+
const armZ_Flail = Math.PI / 2 + 0.3;
|
|
2856
|
+
// 动态计算当前的 Z 轴角度:根据 blendFactor 在两者间平滑变化
|
|
2857
|
+
const currentArmZ = lerp(armZ_Run, armZ_Flail, blendFactor);
|
|
2858
|
+
player.skin.leftArm.rotation.z = currentArmZ;
|
|
2859
|
+
player.skin.rightArm.rotation.z = -currentArmZ;
|
|
2860
|
+
// 旋转轴混合:
|
|
2861
|
+
// 当 blendFactor 为 0 时,完全使用 X 轴旋转 (跑步摆臂)
|
|
2862
|
+
// 当 blendFactor 为 1 时,完全使用 Y 轴旋转 (直升机乱挥)
|
|
2863
|
+
// 中间状态会自动混合两个轴的旋转
|
|
2864
|
+
// X轴分量:跑步时满额,发疯时归零
|
|
2865
|
+
const rotX = swingRad * (1 - blendFactor);
|
|
2866
|
+
// Y轴分量:跑步时归零,发疯时满额
|
|
2867
|
+
const rotY = swingRad * blendFactor;
|
|
2868
|
+
player.skin.leftArm.rotation.x = rotX;
|
|
2869
|
+
player.skin.rightArm.rotation.x = -rotX;
|
|
2870
|
+
player.skin.leftArm.rotation.y = rotY;
|
|
2871
|
+
player.skin.rightArm.rotation.y = rotY;
|
|
2872
|
+
// === 4. 头部动作 (视线画圆) ===
|
|
2873
|
+
// 原理:X轴管上下,Y轴管左右。
|
|
2874
|
+
// 一个用 sin,一个用 cos,频率一致,就会形成圆周运动
|
|
2875
|
+
// 稍微降低一点频率(highFreq / 2),不要晃得太晕
|
|
2876
|
+
const headSpeed = highFreq * 0.5;
|
|
2877
|
+
const headAmp = 0.8; // 晃动幅度
|
|
2878
|
+
player.skin.head.rotation.y = Math.sin(headSpeed) * headAmp; // 左右
|
|
2879
|
+
player.skin.head.rotation.x = Math.cos(headSpeed) * headAmp; // 上下
|
|
2880
|
+
// 稍微加一点 Z 轴歪头,看起来更疯癫
|
|
2881
|
+
player.skin.head.rotation.z = Math.sin(headSpeed * 0.5) * 0.1;
|
|
2882
|
+
// === 5. 身体位移 ===
|
|
2883
|
+
// 上下跳动
|
|
2884
|
+
player.position.y = Math.sin(highFreq * 2) * 2;
|
|
2885
|
+
// 左右胡乱位移
|
|
2886
|
+
player.position.x = Math.cos(highFreq * 0.5) * 0.5;
|
|
2887
|
+
// 身体稍微前倾一点
|
|
2888
|
+
player.rotation.x = 0.15;
|
|
2889
|
+
player.cape.rotation.x = Math.sin(highFreq * 2) * 0.2 + Math.PI * 0.3;
|
|
2890
|
+
if (player.nameTag)
|
|
2891
|
+
player.nameTag.position.y = player.position.y + 20;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2896
|
+
FontLibrary.use([join(__dirname, '../assets/minecraft.woff2')]);
|
|
2897
|
+
|
|
2898
|
+
export { BodyPart, CapeObject, CrouchAnimation, EarsObject, ElytraObject, FlailAnimation, FlyingAnimation, FunctionAnimation, HitAnimation, IdleAnimation, NameTagObject, NodAnimation, PlayerAnimation, PlayerObject, RotateAnimation, RunningAnimation, SkinObject, SkinViewer, SpinUpAnimation, SwimAnimation, WalkingAnimation, WaveAnimation };
|