@adatechnology/game-vfx-three 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-ALDCQ5RH.js +1904 -0
- package/dist/chunk-ALDCQ5RH.js.map +1 -0
- package/dist/index.d.ts +9 -32
- package/dist/index.global.js +203 -33
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +21 -1792
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +11 -0
- package/dist/testing.js.map +1 -0
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -1,1796 +1,25 @@
|
|
|
1
|
-
// src/renderer.ts
|
|
2
|
-
import { markKitOrigin } from "@adatechnology/game-contracts";
|
|
3
|
-
import { colorOfChunk, createVfx, hexToLinearRgb } from "@adatechnology/game-vfx";
|
|
4
|
-
|
|
5
|
-
// src/shaders/glow.ts
|
|
6
|
-
var GLOW_VERTEX = `attribute vec4 aP;attribute vec4 aC;varying vec2 vU;varying vec4 vC;
|
|
7
|
-
void main(){vec3 R=vec3(viewMatrix[0][0],viewMatrix[1][0],viewMatrix[2][0]);vec3 Up=vec3(viewMatrix[0][1],viewMatrix[1][1],viewMatrix[2][1]);
|
|
8
|
-
vec3 wp=aP.xyz+(R*position.x+Up*position.y)*aP.w;vU=position.xy;vC=aC;gl_Position=projectionMatrix*viewMatrix*vec4(wp,1.);}`;
|
|
9
|
-
var GLOW_FRAGMENT = `varying vec2 vU;varying vec4 vC;void main(){float r=dot(vU,vU);if(r>1.)discard;float f=exp(-r*4.2)-.015;gl_FragColor=vec4(vC.rgb*vC.a*max(f,0.),1.);
|
|
10
|
-
#include <tonemapping_fragment>
|
|
11
|
-
#include <colorspace_fragment>
|
|
12
|
-
}`;
|
|
13
|
-
|
|
14
|
-
// src/shaders/noise.ts
|
|
15
|
-
var GL_NOISE = `
|
|
16
|
-
float h3(vec3 p){p=fract(p*.3183099+.1);p*=17.0;return fract(p.x*p.y*p.z*(p.x+p.y+p.z));}
|
|
17
|
-
float vn(vec3 x){vec3 i=floor(x),f=fract(x);f=f*f*(3.-2.*f);
|
|
18
|
-
return mix(mix(mix(h3(i),h3(i+vec3(1,0,0)),f.x),mix(h3(i+vec3(0,1,0)),h3(i+vec3(1,1,0)),f.x),f.y),
|
|
19
|
-
mix(mix(h3(i+vec3(0,0,1)),h3(i+vec3(1,0,1)),f.x),mix(h3(i+vec3(0,1,1)),h3(i+vec3(1,1,1)),f.x),f.y),f.z);}
|
|
20
|
-
`;
|
|
21
|
-
|
|
22
|
-
// src/shaders/puff.ts
|
|
23
|
-
var PUFF_VERTEX = `${GL_NOISE}
|
|
24
|
-
attribute vec4 aP;attribute vec4 aD;attribute vec3 aC;
|
|
25
|
-
varying vec3 vWP;varying vec3 vL;varying vec4 vD;varying vec3 vC;
|
|
26
|
-
void main(){
|
|
27
|
-
vec3 p=position;
|
|
28
|
-
float s=aD.z;
|
|
29
|
-
float n=vn(p*1.6+vec3(s*11.0,s*7.0,s*3.0));
|
|
30
|
-
p*=0.78+0.5*n;
|
|
31
|
-
float a=s*6.2831;float c=cos(a),si=sin(a);p.xz=mat2(c,-si,si,c)*p.xz;
|
|
32
|
-
vL=p;
|
|
33
|
-
vec3 wp=aP.xyz+p*aP.w;
|
|
34
|
-
vWP=wp;vD=aD;vC=aC;
|
|
35
|
-
gl_Position=projectionMatrix*viewMatrix*vec4(wp,1.0);
|
|
36
|
-
}`;
|
|
37
|
-
var PUFF_FRAGMENT = `${GL_NOISE}
|
|
38
|
-
uniform vec3 uLight;uniform vec3 uSky;uniform vec3 uGround;uniform vec3 uGlowPos;uniform vec3 uGlowCol;uniform float uGlowI;
|
|
39
|
-
varying vec3 vWP;varying vec3 vL;varying vec4 vD;varying vec3 vC;
|
|
40
|
-
vec3 L(vec3 c){return pow(c,vec3(2.2));}
|
|
41
|
-
vec3 ramp(float h){
|
|
42
|
-
vec3 c0=L(vec3(.36,.07,.03)),c1=L(vec3(.76,.21,.1)),c2=L(vec3(1.,.54,.16)),c3=L(vec3(1.,.83,.42)),c4=L(vec3(1.,.96,.84));
|
|
43
|
-
if(h<.25)return mix(c0,c1,h/.25);
|
|
44
|
-
if(h<.5)return mix(c1,c2,(h-.25)/.25);
|
|
45
|
-
if(h<.8)return mix(c2,c3,(h-.5)/.3);
|
|
46
|
-
return mix(c3,c4,clamp((h-.8)/.4,0.,1.));
|
|
47
|
-
}
|
|
48
|
-
void main(){
|
|
49
|
-
vec3 N=normalize(cross(dFdx(vWP),dFdy(vWP)));
|
|
50
|
-
float age=vD.x,heat=vD.y,kind=vD.w;
|
|
51
|
-
float n=vn(vL*2.4+vD.z*13.0);
|
|
52
|
-
float diss=smoothstep(.5,1.,age);
|
|
53
|
-
if(n<diss*1.08-.04)discard;
|
|
54
|
-
float lam=max(dot(N,uLight),0.);
|
|
55
|
-
float band=lam>.55?1.:lam>.15?.68:.42;
|
|
56
|
-
vec3 col=vC*(vec3(.5,.56,.7)*band*1.05+vec3(.2,.19,.18)+uGround*(.4-.4*N.y));
|
|
57
|
-
vec3 tg=uGlowPos-vWP;float dg=length(tg)+.001;
|
|
58
|
-
float gi=uGlowI/(1.+dg*dg*.05)*(.3+.7*max(dot(N,tg/dg),0.));
|
|
59
|
-
col+=uGlowCol*gi*vC*1.6;
|
|
60
|
-
float h=heat*(.72+.56*n)+heat*(-N.y)*.18;
|
|
61
|
-
if(h>.05){
|
|
62
|
-
float hq=floor(h*5.+.5)/5.;
|
|
63
|
-
vec3 fc=ramp(clamp(hq,0.,1.2))*(1.1+3.1*hq);
|
|
64
|
-
col=mix(col,fc,smoothstep(.04,.2,h));
|
|
65
|
-
}
|
|
66
|
-
if(kind>2.5&&kind<3.5){col+=vec3(.02,.025,.03);}
|
|
67
|
-
gl_FragColor=vec4(col,1.);
|
|
68
|
-
#include <tonemapping_fragment>
|
|
69
|
-
#include <colorspace_fragment>
|
|
70
|
-
}`;
|
|
71
|
-
|
|
72
|
-
// src/shaders/spark.ts
|
|
73
|
-
var SPARK_VERTEX = `
|
|
74
|
-
attribute vec3 aPos;attribute vec3 aVel;attribute vec4 aE;varying vec2 vU;varying vec4 vE;
|
|
75
|
-
void main(){
|
|
76
|
-
float sp=length(aVel)+1e-4;vec3 d=aVel/sp;
|
|
77
|
-
vec3 tc=normalize(cameraPosition-aPos);
|
|
78
|
-
vec3 sd=normalize(cross(d,tc)+vec3(1e-5));
|
|
79
|
-
float len=max(aE.y*2.5,min(sp*aE.z,2.2));
|
|
80
|
-
vec3 wp=aPos+sd*position.x*aE.y-d*position.y*len;
|
|
81
|
-
vU=position.xy;vE=aE;
|
|
82
|
-
gl_Position=projectionMatrix*viewMatrix*vec4(wp,1.);
|
|
83
|
-
}`;
|
|
84
|
-
var SPARK_FRAGMENT = `
|
|
85
|
-
varying vec2 vU;varying vec4 vE;
|
|
86
|
-
void main(){
|
|
87
|
-
float age=vE.x,k=vE.w;
|
|
88
|
-
float w=1.-abs(vU.x)*2.;w=w*w;
|
|
89
|
-
float tail=1.-vU.y;
|
|
90
|
-
vec3 c;
|
|
91
|
-
if(k<.5){c=mix(vec3(4.,3.3,2.2),vec3(2.4,.7,.12),smoothstep(0.,.55,age));c=mix(c,vec3(.5,.06,.02),smoothstep(.55,1.,age));}
|
|
92
|
-
else if(k<1.5){c=mix(vec3(1.6,1.6,1.6),vec3(.35,.35,.36),smoothstep(0.,.4,age));}
|
|
93
|
-
else if(k<2.5){c=mix(vec3(5.,5.,6.),vec3(1.,1.3,2.),smoothstep(0.,.5,age));}
|
|
94
|
-
else if(k<3.5){c=vec3(.9,1.2,1.4)*(1.-age);}
|
|
95
|
-
else {c=mix(vec3(3.2,1.4,.3),vec3(1.2,.25,.05),age);}
|
|
96
|
-
float a=w*(.35+.65*tail)*(1.-smoothstep(.75,1.,age));
|
|
97
|
-
gl_FragColor=vec4(c*a,1.);
|
|
98
|
-
#include <tonemapping_fragment>
|
|
99
|
-
#include <colorspace_fragment>
|
|
100
|
-
}`;
|
|
101
|
-
|
|
102
|
-
// src/instanced.ts
|
|
103
|
-
function dynamic(THREE, array, size) {
|
|
104
|
-
return new THREE.InstancedBufferAttribute(array, size).setUsage(THREE.DynamicDrawUsage);
|
|
105
|
-
}
|
|
106
|
-
function createPuffLayer(THREE, pool, uniforms) {
|
|
107
|
-
const capacity = pool.capacity;
|
|
108
|
-
const geometry = new THREE.InstancedBufferGeometry();
|
|
109
|
-
geometry.setAttribute("position", new THREE.IcosahedronGeometry(1, 1).getAttribute("position"));
|
|
110
|
-
const a = new Float32Array(capacity * 4);
|
|
111
|
-
const d = new Float32Array(capacity * 4);
|
|
112
|
-
const c = new Float32Array(capacity * 3);
|
|
113
|
-
const attributes = [dynamic(THREE, a, 4), dynamic(THREE, d, 4), dynamic(THREE, c, 3)];
|
|
114
|
-
geometry.setAttribute("aP", attributes[0]);
|
|
115
|
-
geometry.setAttribute("aD", attributes[1]);
|
|
116
|
-
geometry.setAttribute("aC", attributes[2]);
|
|
117
|
-
geometry.instanceCount = 0;
|
|
118
|
-
const material = new THREE.ShaderMaterial({ uniforms, vertexShader: PUFF_VERTEX, fragmentShader: PUFF_FRAGMENT });
|
|
119
|
-
const mesh = new THREE.Mesh(geometry, material);
|
|
120
|
-
mesh.name = "vfx-puffs";
|
|
121
|
-
mesh.frustumCulled = false;
|
|
122
|
-
return {
|
|
123
|
-
mesh,
|
|
124
|
-
sync() {
|
|
125
|
-
let n = 0;
|
|
126
|
-
for (const p of pool.items) {
|
|
127
|
-
a[n * 4] = p.x;
|
|
128
|
-
a[n * 4 + 1] = p.y;
|
|
129
|
-
a[n * 4 + 2] = p.z;
|
|
130
|
-
a[n * 4 + 3] = p.size;
|
|
131
|
-
d[n * 4] = p.age;
|
|
132
|
-
d[n * 4 + 1] = p.heat;
|
|
133
|
-
d[n * 4 + 2] = p.seed;
|
|
134
|
-
d[n * 4 + 3] = p.kind;
|
|
135
|
-
c[n * 3] = p.color[0];
|
|
136
|
-
c[n * 3 + 1] = p.color[1];
|
|
137
|
-
c[n * 3 + 2] = p.color[2];
|
|
138
|
-
n++;
|
|
139
|
-
}
|
|
140
|
-
geometry.instanceCount = n;
|
|
141
|
-
for (const attribute of attributes) attribute.needsUpdate = true;
|
|
142
|
-
return n;
|
|
143
|
-
},
|
|
144
|
-
dispose() {
|
|
145
|
-
geometry.dispose();
|
|
146
|
-
material.dispose();
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function createSparkLayer(THREE, pool) {
|
|
151
|
-
const capacity = pool.capacity;
|
|
152
|
-
const geometry = new THREE.InstancedBufferGeometry();
|
|
153
|
-
geometry.setAttribute("position", new THREE.Float32BufferAttribute([-0.5, 0, 0, 0.5, 0, 0, 0.5, 1, 0, -0.5, 1, 0], 3));
|
|
154
|
-
geometry.setIndex([0, 1, 2, 0, 2, 3]);
|
|
155
|
-
const position = new Float32Array(capacity * 3);
|
|
156
|
-
const velocity = new Float32Array(capacity * 3);
|
|
157
|
-
const extra = new Float32Array(capacity * 4);
|
|
158
|
-
const attributes = [dynamic(THREE, position, 3), dynamic(THREE, velocity, 3), dynamic(THREE, extra, 4)];
|
|
159
|
-
geometry.setAttribute("aPos", attributes[0]);
|
|
160
|
-
geometry.setAttribute("aVel", attributes[1]);
|
|
161
|
-
geometry.setAttribute("aE", attributes[2]);
|
|
162
|
-
geometry.instanceCount = 0;
|
|
163
|
-
const material = new THREE.ShaderMaterial({ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, vertexShader: SPARK_VERTEX, fragmentShader: SPARK_FRAGMENT });
|
|
164
|
-
const mesh = new THREE.Mesh(geometry, material);
|
|
165
|
-
mesh.name = "vfx-sparks";
|
|
166
|
-
mesh.frustumCulled = false;
|
|
167
|
-
mesh.renderOrder = 5;
|
|
168
|
-
return {
|
|
169
|
-
mesh,
|
|
170
|
-
sync() {
|
|
171
|
-
let n = 0;
|
|
172
|
-
for (const s of pool.items) {
|
|
173
|
-
position[n * 3] = s.x;
|
|
174
|
-
position[n * 3 + 1] = s.y;
|
|
175
|
-
position[n * 3 + 2] = s.z;
|
|
176
|
-
velocity[n * 3] = s.vx;
|
|
177
|
-
velocity[n * 3 + 1] = s.vy;
|
|
178
|
-
velocity[n * 3 + 2] = s.vz;
|
|
179
|
-
extra[n * 4] = s.age;
|
|
180
|
-
extra[n * 4 + 1] = s.w;
|
|
181
|
-
extra[n * 4 + 2] = s.st;
|
|
182
|
-
extra[n * 4 + 3] = s.kind;
|
|
183
|
-
n++;
|
|
184
|
-
}
|
|
185
|
-
geometry.instanceCount = n;
|
|
186
|
-
for (const attribute of attributes) attribute.needsUpdate = true;
|
|
187
|
-
return n;
|
|
188
|
-
},
|
|
189
|
-
dispose() {
|
|
190
|
-
geometry.dispose();
|
|
191
|
-
material.dispose();
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
function createGlowLayer(THREE, pool) {
|
|
196
|
-
const capacity = pool.capacity;
|
|
197
|
-
const geometry = new THREE.InstancedBufferGeometry();
|
|
198
|
-
geometry.setAttribute("position", new THREE.Float32BufferAttribute([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0], 3));
|
|
199
|
-
geometry.setIndex([0, 1, 2, 0, 2, 3]);
|
|
200
|
-
const where = new Float32Array(capacity * 4);
|
|
201
|
-
const color = new Float32Array(capacity * 4);
|
|
202
|
-
const attributes = [dynamic(THREE, where, 4), dynamic(THREE, color, 4)];
|
|
203
|
-
geometry.setAttribute("aP", attributes[0]);
|
|
204
|
-
geometry.setAttribute("aC", attributes[1]);
|
|
205
|
-
geometry.instanceCount = 0;
|
|
206
|
-
const material = new THREE.ShaderMaterial({ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, vertexShader: GLOW_VERTEX, fragmentShader: GLOW_FRAGMENT });
|
|
207
|
-
const mesh = new THREE.Mesh(geometry, material);
|
|
208
|
-
mesh.name = "vfx-glows";
|
|
209
|
-
mesh.frustumCulled = false;
|
|
210
|
-
mesh.renderOrder = 6;
|
|
211
|
-
return {
|
|
212
|
-
mesh,
|
|
213
|
-
sync() {
|
|
214
|
-
let n = 0;
|
|
215
|
-
for (const g of pool.items) {
|
|
216
|
-
where[n * 4] = g.outX;
|
|
217
|
-
where[n * 4 + 1] = g.outY;
|
|
218
|
-
where[n * 4 + 2] = g.outZ;
|
|
219
|
-
where[n * 4 + 3] = g.outSize;
|
|
220
|
-
color[n * 4] = g.color[0];
|
|
221
|
-
color[n * 4 + 1] = g.color[1];
|
|
222
|
-
color[n * 4 + 2] = g.color[2];
|
|
223
|
-
color[n * 4 + 3] = g.outIntensity;
|
|
224
|
-
n++;
|
|
225
|
-
}
|
|
226
|
-
geometry.instanceCount = n;
|
|
227
|
-
for (const attribute of attributes) attribute.needsUpdate = true;
|
|
228
|
-
return n;
|
|
229
|
-
},
|
|
230
|
-
dispose() {
|
|
231
|
-
geometry.dispose();
|
|
232
|
-
material.dispose();
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
function createChunkLayer(THREE, pool, shapes, colorOf) {
|
|
237
|
-
const capacity = pool.capacity;
|
|
238
|
-
const rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.85, flatShading: true });
|
|
239
|
-
const plateMaterial = rockMaterial.clone();
|
|
240
|
-
const make = (geometry, material) => {
|
|
241
|
-
const mesh = new THREE.InstancedMesh(geometry, material, capacity);
|
|
242
|
-
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
243
|
-
mesh.castShadow = true;
|
|
244
|
-
mesh.receiveShadow = true;
|
|
245
|
-
mesh.frustumCulled = false;
|
|
246
|
-
mesh.count = 0;
|
|
247
|
-
mesh.setColorAt(0, new THREE.Color(1, 1, 1));
|
|
248
|
-
return mesh;
|
|
249
|
-
};
|
|
250
|
-
const rocks = make(shapes.rock, rockMaterial);
|
|
251
|
-
const plates = make(shapes.plate, plateMaterial);
|
|
252
|
-
const matrix = new THREE.Matrix4();
|
|
253
|
-
const quaternion = new THREE.Quaternion();
|
|
254
|
-
const euler = new THREE.Euler();
|
|
255
|
-
const position = new THREE.Vector3();
|
|
256
|
-
const scale = new THREE.Vector3();
|
|
257
|
-
return {
|
|
258
|
-
rocks,
|
|
259
|
-
plates,
|
|
260
|
-
sync() {
|
|
261
|
-
let rockCount = 0;
|
|
262
|
-
let plateCount = 0;
|
|
263
|
-
for (const chunk of pool.items) {
|
|
264
|
-
euler.set(chunk.rx, chunk.ry, chunk.rz);
|
|
265
|
-
quaternion.setFromEuler(euler);
|
|
266
|
-
matrix.compose(position.set(chunk.x, chunk.y, chunk.z), quaternion, scale.set(chunk.sx, chunk.sy, chunk.sz));
|
|
267
|
-
const mesh = chunk.plate ? plates : rocks;
|
|
268
|
-
const index = chunk.plate ? plateCount++ : rockCount++;
|
|
269
|
-
mesh.setMatrixAt(index, matrix);
|
|
270
|
-
const color = colorOf(chunk);
|
|
271
|
-
const colors = mesh.instanceColor.array;
|
|
272
|
-
colors[index * 3] = color[0];
|
|
273
|
-
colors[index * 3 + 1] = color[1];
|
|
274
|
-
colors[index * 3 + 2] = color[2];
|
|
275
|
-
}
|
|
276
|
-
rocks.count = rockCount;
|
|
277
|
-
plates.count = plateCount;
|
|
278
|
-
for (const mesh of [rocks, plates]) {
|
|
279
|
-
mesh.instanceMatrix.needsUpdate = true;
|
|
280
|
-
mesh.instanceColor.needsUpdate = true;
|
|
281
|
-
}
|
|
282
|
-
return rockCount + plateCount;
|
|
283
|
-
},
|
|
284
|
-
dispose() {
|
|
285
|
-
rockMaterial.dispose();
|
|
286
|
-
plateMaterial.dispose();
|
|
287
|
-
rocks.dispose();
|
|
288
|
-
plates.dispose();
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// src/lights.ts
|
|
294
|
-
function createLightLayer(THREE, pool, scale) {
|
|
295
|
-
const group = new THREE.Group();
|
|
296
|
-
const blast = pool.blast.map(() => {
|
|
297
|
-
const light = new THREE.PointLight(16752720, 0, 30, 2);
|
|
298
|
-
light.position.set(0, -50, 0);
|
|
299
|
-
group.add(light);
|
|
300
|
-
return light;
|
|
301
|
-
});
|
|
302
|
-
const fire = pool.fire.map(() => {
|
|
303
|
-
const light = new THREE.PointLight(16742954, 0, 16, 2);
|
|
304
|
-
light.position.set(0, -50, 0);
|
|
305
|
-
group.add(light);
|
|
306
|
-
return light;
|
|
307
|
-
});
|
|
308
|
-
return {
|
|
309
|
-
group,
|
|
310
|
-
sync() {
|
|
311
|
-
let lit = 0;
|
|
312
|
-
pool.blast.forEach((entry, index) => {
|
|
313
|
-
const light = blast[index];
|
|
314
|
-
light.position.set(entry.x, entry.y, entry.z);
|
|
315
|
-
light.distance = entry.distance;
|
|
316
|
-
light.color.setRGB(entry.color[0], entry.color[1], entry.color[2], THREE.LinearSRGBColorSpace);
|
|
317
|
-
light.intensity = entry.intensity * scale;
|
|
318
|
-
if (entry.intensity > 0.05) lit++;
|
|
319
|
-
});
|
|
320
|
-
pool.fire.forEach((entry, index) => {
|
|
321
|
-
const light = fire[index];
|
|
322
|
-
light.position.set(entry.x, entry.y, entry.z);
|
|
323
|
-
light.distance = entry.distance;
|
|
324
|
-
light.intensity = entry.intensity * scale;
|
|
325
|
-
if (entry.intensity > 0.05) lit++;
|
|
326
|
-
});
|
|
327
|
-
return lit;
|
|
328
|
-
},
|
|
329
|
-
dispose() {
|
|
330
|
-
for (const light of [...blast, ...fire]) light.dispose();
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// src/canvas.ts
|
|
336
|
-
import { lehmer } from "@adatechnology/game-vfx";
|
|
337
|
-
var SCORCH_SEED = 5;
|
|
338
|
-
var TEXTURE_SIZE = 256;
|
|
339
|
-
var FULL_TURN = Math.PI * 2;
|
|
340
|
-
function createTextureStream() {
|
|
341
|
-
return lehmer(SCORCH_SEED);
|
|
342
|
-
}
|
|
343
|
-
function blankCanvas() {
|
|
344
|
-
return { width: 0, height: 0, getContext: () => null };
|
|
345
|
-
}
|
|
346
|
-
function defaultCanvas() {
|
|
347
|
-
if (typeof document === "undefined") return blankCanvas();
|
|
348
|
-
return document.createElement("canvas");
|
|
349
|
-
}
|
|
350
|
-
function paint(createCanvas, draw) {
|
|
351
|
-
const canvas = createCanvas();
|
|
352
|
-
canvas.width = TEXTURE_SIZE;
|
|
353
|
-
canvas.height = TEXTURE_SIZE;
|
|
354
|
-
const context = canvas.getContext("2d");
|
|
355
|
-
if (context) draw(context, TEXTURE_SIZE);
|
|
356
|
-
return canvas;
|
|
357
|
-
}
|
|
358
|
-
function drawScorch(context, size, random) {
|
|
359
|
-
context.clearRect(0, 0, size, size);
|
|
360
|
-
const center = size / 2;
|
|
361
|
-
for (let ray = 0; ray < 60; ray++) {
|
|
362
|
-
const angle = random.next() * FULL_TURN;
|
|
363
|
-
const length = center * (0.55 + random.next() * 0.42);
|
|
364
|
-
context.strokeStyle = `rgba(8,6,5,${0.25 + random.next() * 0.35})`;
|
|
365
|
-
context.lineWidth = 2 + random.next() * 6;
|
|
366
|
-
context.beginPath();
|
|
367
|
-
context.moveTo(center, center);
|
|
368
|
-
context.lineTo(center + Math.cos(angle) * length, center + Math.sin(angle) * length);
|
|
369
|
-
context.stroke();
|
|
370
|
-
}
|
|
371
|
-
const gradient = context.createRadialGradient(center, center, 0, center, center, center * 0.72);
|
|
372
|
-
gradient.addColorStop(0, "rgba(4,3,3,.96)");
|
|
373
|
-
gradient.addColorStop(0.55, "rgba(10,8,7,.85)");
|
|
374
|
-
gradient.addColorStop(1, "rgba(14,11,9,0)");
|
|
375
|
-
context.fillStyle = gradient;
|
|
376
|
-
context.fillRect(0, 0, size, size);
|
|
377
|
-
for (let spot = 0; spot < 26; spot++) {
|
|
378
|
-
context.fillStyle = `rgba(60,40,28,${0.15 + random.next() * 0.2})`;
|
|
379
|
-
context.beginPath();
|
|
380
|
-
context.arc(center + random.between(-0.4, 0.4) * center, center + random.between(-0.4, 0.4) * center, 2 + random.next() * 7, 0, 7);
|
|
381
|
-
context.fill();
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
function drawDirt(context, size, random) {
|
|
385
|
-
const center = size / 2;
|
|
386
|
-
context.clearRect(0, 0, size, size);
|
|
387
|
-
const gradient = context.createRadialGradient(center, center, 0, center, center, center);
|
|
388
|
-
gradient.addColorStop(0, "rgba(92,72,52,1)");
|
|
389
|
-
gradient.addColorStop(0.6, "rgba(118,92,64,1)");
|
|
390
|
-
gradient.addColorStop(0.82, "rgba(80,64,50,.8)");
|
|
391
|
-
gradient.addColorStop(1, "rgba(40,36,34,0)");
|
|
392
|
-
context.fillStyle = gradient;
|
|
393
|
-
context.fillRect(0, 0, size, size);
|
|
394
|
-
for (let speck = 0; speck < 420; speck++) {
|
|
395
|
-
const angle = random.next() * FULL_TURN;
|
|
396
|
-
const distance = Math.sqrt(random.next()) * center * 0.8;
|
|
397
|
-
context.fillStyle = random.next() < 0.5 ? "rgba(40,30,22,.5)" : "rgba(160,132,96,.5)";
|
|
398
|
-
context.beginPath();
|
|
399
|
-
context.arc(center + Math.cos(angle) * distance, center + Math.sin(angle) * distance, 1 + random.next() * 3.5, 0, 7);
|
|
400
|
-
context.fill();
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
function createMarkTextures(THREE, createCanvas) {
|
|
404
|
-
const make = createCanvas ?? defaultCanvas;
|
|
405
|
-
const random = createTextureStream();
|
|
406
|
-
const texture = (canvas) => {
|
|
407
|
-
const result = new THREE.CanvasTexture(canvas);
|
|
408
|
-
result.colorSpace = THREE.SRGBColorSpace;
|
|
409
|
-
result.anisotropy = 4;
|
|
410
|
-
return result;
|
|
411
|
-
};
|
|
412
|
-
const scorch = [0, 1, 2].map(() => texture(paint(make, (context, size) => drawScorch(context, size, random))));
|
|
413
|
-
const dirt = texture(paint(make, (context, size) => drawDirt(context, size, random)));
|
|
414
|
-
return { scorch, dirt };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// src/marks.ts
|
|
418
|
-
var MAX_CONFORM_STEPS = 48;
|
|
419
|
-
var MIN_CONFORM_STEPS = 16;
|
|
420
|
-
var BLOOD_GROW_RATE = 1.2;
|
|
421
|
-
var BLOOD_START_GROWTH = 0.05;
|
|
422
|
-
var DEFAULT_MAX_GROUND_MARKS = 64;
|
|
423
|
-
var DEFAULT_MAX_ROCKS = 512;
|
|
424
|
-
var DEFAULT_MAX_BLOOD = 256;
|
|
425
|
-
var MAX_SPARE_GROUND_GEOMETRIES = 16;
|
|
426
|
-
function createMarkLayer(THREE, options) {
|
|
427
|
-
const group = new THREE.Group();
|
|
428
|
-
const textures = createMarkTextures(THREE, options.createCanvas);
|
|
429
|
-
const owned = [...textures.scorch, textures.dirt];
|
|
430
|
-
const scorchMaterials = textures.scorch.map((map) => {
|
|
431
|
-
const material = new THREE.MeshStandardMaterial({ map, transparent: true, depthWrite: false, roughness: 1, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 });
|
|
432
|
-
owned.push(material);
|
|
433
|
-
return material;
|
|
434
|
-
});
|
|
435
|
-
const dirtMaterial = new THREE.MeshStandardMaterial({
|
|
436
|
-
map: textures.dirt,
|
|
437
|
-
transparent: true,
|
|
438
|
-
depthWrite: false,
|
|
439
|
-
roughness: 1,
|
|
440
|
-
flatShading: true,
|
|
441
|
-
polygonOffset: true,
|
|
442
|
-
polygonOffsetFactor: -1,
|
|
443
|
-
polygonOffsetUnits: -1
|
|
444
|
-
});
|
|
445
|
-
owned.push(dirtMaterial);
|
|
446
|
-
const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
|
|
447
|
-
const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
|
|
448
|
-
owned.push(bloodMaterial, bloodGeometry);
|
|
449
|
-
const maxGroundMarks = options.maxGroundMarks ?? DEFAULT_MAX_GROUND_MARKS;
|
|
450
|
-
const instanced = {};
|
|
451
|
-
function rockMaterial() {
|
|
452
|
-
if (!instanced.rockMaterial) {
|
|
453
|
-
instanced.rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.9, metalness: 0, flatShading: true });
|
|
454
|
-
owned.push(instanced.rockMaterial);
|
|
455
|
-
}
|
|
456
|
-
return instanced.rockMaterial;
|
|
457
|
-
}
|
|
458
|
-
function bloodMesh() {
|
|
459
|
-
return instanced.blood ?? (instanced.blood = attach(createInstanced(bloodGeometry, bloodMaterial, options.maxBlood ?? DEFAULT_MAX_BLOOD)));
|
|
460
|
-
}
|
|
461
|
-
function rockMesh() {
|
|
462
|
-
return instanced.rock ?? (instanced.rock = attach(createInstanced(options.rockGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
|
|
463
|
-
}
|
|
464
|
-
function plateMesh() {
|
|
465
|
-
return instanced.plate ?? (instanced.plate = attach(createInstanced(options.plateGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
|
|
466
|
-
}
|
|
467
|
-
function attach(mesh, castShadow = false) {
|
|
468
|
-
mesh.castShadow = castShadow;
|
|
469
|
-
group.add(mesh);
|
|
470
|
-
return mesh;
|
|
471
|
-
}
|
|
472
|
-
const groundMarks = [];
|
|
473
|
-
const spareGeometries = [];
|
|
474
|
-
const blood = [];
|
|
475
|
-
const placed = { blood: 0, rock: 0, plate: 0 };
|
|
476
|
-
const matrix = new THREE.Matrix4();
|
|
477
|
-
const position = new THREE.Vector3();
|
|
478
|
-
const quaternion = new THREE.Quaternion();
|
|
479
|
-
const euler = new THREE.Euler();
|
|
480
|
-
const scale = new THREE.Vector3();
|
|
481
|
-
const color = new THREE.Color();
|
|
482
|
-
const up = new THREE.Vector3(0, 1, 0);
|
|
483
|
-
function createInstanced(geometry, material, capacity) {
|
|
484
|
-
const mesh = new THREE.InstancedMesh(geometry, material, capacity);
|
|
485
|
-
mesh.count = 0;
|
|
486
|
-
mesh.frustumCulled = false;
|
|
487
|
-
mesh.receiveShadow = true;
|
|
488
|
-
return mesh;
|
|
489
|
-
}
|
|
490
|
-
function takeSpare(vertexCount) {
|
|
491
|
-
const found = spareGeometries.findIndex((geometry) => geometry.getAttribute("position").count === vertexCount);
|
|
492
|
-
return found < 0 ? void 0 : spareGeometries.splice(found, 1)[0];
|
|
493
|
-
}
|
|
494
|
-
function evictOldestGroundMark() {
|
|
495
|
-
const oldest = groundMarks.shift();
|
|
496
|
-
group.remove(oldest.mesh);
|
|
497
|
-
spareGeometries.push(oldest.mesh.geometry);
|
|
498
|
-
if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
|
|
499
|
-
}
|
|
500
|
-
function conform(mark, material) {
|
|
501
|
-
const radius = mark.radius;
|
|
502
|
-
const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
|
|
503
|
-
if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
|
|
504
|
-
const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
|
|
505
|
-
const grid = geometry.attributes.position;
|
|
506
|
-
const cell = 2 / steps;
|
|
507
|
-
const cos = Math.cos(mark.rotation);
|
|
508
|
-
const sin = Math.sin(mark.rotation);
|
|
509
|
-
for (let index = 0; index < grid.count; index++) {
|
|
510
|
-
const localX = Math.fround(index % (steps + 1) * cell - 1) * radius;
|
|
511
|
-
const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
|
|
512
|
-
const worldX = mark.x + localX * cos - localZ * sin;
|
|
513
|
-
const worldZ = mark.z + localX * sin + localZ * cos;
|
|
514
|
-
grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
|
|
515
|
-
}
|
|
516
|
-
grid.needsUpdate = true;
|
|
517
|
-
geometry.computeVertexNormals();
|
|
518
|
-
geometry.attributes.normal.needsUpdate = true;
|
|
519
|
-
geometry.boundingSphere = null;
|
|
520
|
-
geometry.boundingBox = null;
|
|
521
|
-
const mesh = new THREE.Mesh(geometry, material);
|
|
522
|
-
mesh.receiveShadow = true;
|
|
523
|
-
group.add(mesh);
|
|
524
|
-
groundMarks.push({ mesh, steps });
|
|
525
|
-
}
|
|
526
|
-
function addRock(mark) {
|
|
527
|
-
const mesh = mark.plate ? plateMesh() : rockMesh();
|
|
528
|
-
const slot = mark.plate ? "plate" : "rock";
|
|
529
|
-
const index = placed[slot]++ % mesh.instanceMatrix.count;
|
|
530
|
-
quaternion.setFromEuler(euler.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]));
|
|
531
|
-
mesh.setMatrixAt(index, matrix.compose(position.set(mark.x, mark.y, mark.z), quaternion, scale.set(mark.scale[0], mark.scale[1], mark.scale[2])));
|
|
532
|
-
mesh.setColorAt(index, color.set(mark.color));
|
|
533
|
-
mesh.count = Math.min(placed[slot], mesh.instanceMatrix.count);
|
|
534
|
-
mesh.instanceMatrix.needsUpdate = true;
|
|
535
|
-
mesh.instanceColor.needsUpdate = true;
|
|
536
|
-
}
|
|
537
|
-
function writeBlood(index, drawn, growth) {
|
|
538
|
-
quaternion.setFromAxisAngle(up, drawn.rotation);
|
|
539
|
-
const mesh = bloodMesh();
|
|
540
|
-
mesh.setMatrixAt(index, matrix.compose(position.set(drawn.position[0], drawn.position[1], drawn.position[2]), quaternion, scale.set(drawn.scaleX * growth, 1, drawn.scaleZ * growth)));
|
|
541
|
-
mesh.instanceMatrix.needsUpdate = true;
|
|
542
|
-
}
|
|
543
|
-
function addBlood(mark) {
|
|
544
|
-
const index = placed.blood++ % bloodMesh().instanceMatrix.count;
|
|
545
|
-
const drawn = { position: [mark.x, mark.y, mark.z], rotation: mark.rotation, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 };
|
|
546
|
-
blood[index] = drawn;
|
|
547
|
-
writeBlood(index, drawn, BLOOD_START_GROWTH);
|
|
548
|
-
bloodMesh().count = Math.min(placed.blood, bloodMesh().instanceMatrix.count);
|
|
549
|
-
}
|
|
550
|
-
function build(mark) {
|
|
551
|
-
if (mark.kind === "scorch") conform(mark, scorchMaterials[mark.variant]);
|
|
552
|
-
else if (mark.kind === "dirt") conform(mark, dirtMaterial);
|
|
553
|
-
else if (mark.kind === "rock") addRock(mark);
|
|
554
|
-
else addBlood(mark);
|
|
555
|
-
}
|
|
556
|
-
function clearMarks() {
|
|
557
|
-
for (const drawn of groundMarks) group.remove(drawn.mesh);
|
|
558
|
-
for (const drawn of groundMarks) drawn.mesh.geometry.dispose();
|
|
559
|
-
groundMarks.length = 0;
|
|
560
|
-
for (const geometry of spareGeometries) geometry.dispose();
|
|
561
|
-
spareGeometries.length = 0;
|
|
562
|
-
blood.length = 0;
|
|
563
|
-
placed.blood = placed.rock = placed.plate = 0;
|
|
564
|
-
for (const mesh of [instanced.blood, instanced.rock, instanced.plate]) if (mesh) mesh.count = 0;
|
|
565
|
-
}
|
|
566
|
-
return {
|
|
567
|
-
group,
|
|
568
|
-
handle(event) {
|
|
569
|
-
if (event.type === "clear") clearMarks();
|
|
570
|
-
else build(event.mark);
|
|
571
|
-
},
|
|
572
|
-
prepare() {
|
|
573
|
-
bloodMesh();
|
|
574
|
-
rockMesh();
|
|
575
|
-
plateMesh();
|
|
576
|
-
},
|
|
577
|
-
update(dt) {
|
|
578
|
-
for (let index = 0; index < blood.length; index++) {
|
|
579
|
-
const drawn = blood[index];
|
|
580
|
-
if (!drawn || drawn.age > 1) continue;
|
|
581
|
-
drawn.age += dt * BLOOD_GROW_RATE;
|
|
582
|
-
writeBlood(index, drawn, Math.max(BLOOD_START_GROWTH, 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5)));
|
|
583
|
-
}
|
|
584
|
-
},
|
|
585
|
-
count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh) => mesh && mesh.count > 0).length,
|
|
586
|
-
dispose() {
|
|
587
|
-
clearMarks();
|
|
588
|
-
for (const mesh of [instanced.blood, instanced.rock, instanced.plate]) mesh?.dispose();
|
|
589
|
-
for (const item of owned) item.dispose();
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// src/mesh-layer.ts
|
|
595
|
-
var SPHERE_DEFAULT_WIDTH = 8;
|
|
596
|
-
var SPHERE_DEFAULT_HEIGHT = 6;
|
|
597
|
-
var MAX_PARKED_MESHES_PER_KIND = 256;
|
|
598
|
-
function geometryOf(THREE, particle) {
|
|
599
|
-
switch (particle.shape) {
|
|
600
|
-
case "cone":
|
|
601
|
-
return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, false);
|
|
602
|
-
case "cone_open":
|
|
603
|
-
return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, true);
|
|
604
|
-
case "sphere":
|
|
605
|
-
return new THREE.SphereGeometry(1, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
|
|
606
|
-
case "icosahedron":
|
|
607
|
-
return new THREE.IcosahedronGeometry(1, particle.segments);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
function isCone(particle) {
|
|
611
|
-
return particle.shape === "cone" || particle.shape === "cone_open";
|
|
612
|
-
}
|
|
613
|
-
function materialOf(THREE, particle) {
|
|
614
|
-
if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
|
|
615
|
-
return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
|
|
616
|
-
}
|
|
617
|
-
function createMeshLayerProbes(THREE) {
|
|
618
|
-
const geometry = new THREE.BoxGeometry(1e-3, 1e-3, 1e-3);
|
|
619
|
-
return [{ lit: true, additive: false }, { lit: false, additive: false }, { lit: false, additive: true }].map(
|
|
620
|
-
(kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
function geometryKeyOf(particle) {
|
|
624
|
-
return `${particle.shape}:${particle.segments}:${particle.segments2}`;
|
|
625
|
-
}
|
|
626
|
-
function createMeshLayer(THREE, pool) {
|
|
627
|
-
const group = new THREE.Group();
|
|
628
|
-
group.name = "workshop-meshes";
|
|
629
|
-
const drawn = /* @__PURE__ */ new Map();
|
|
630
|
-
const geometries = /* @__PURE__ */ new Map();
|
|
631
|
-
const parked = /* @__PURE__ */ new Map();
|
|
632
|
-
const frame = new THREE.Quaternion();
|
|
633
|
-
const euler = new THREE.Euler();
|
|
634
|
-
const turn = new THREE.Quaternion();
|
|
635
|
-
const offset = new THREE.Vector3();
|
|
636
|
-
let stamp = 0;
|
|
637
|
-
function geometryFor(particle) {
|
|
638
|
-
const key = geometryKeyOf(particle);
|
|
639
|
-
let geometry = geometries.get(key);
|
|
640
|
-
if (!geometry) {
|
|
641
|
-
geometry = geometryOf(THREE, particle);
|
|
642
|
-
geometries.set(key, geometry);
|
|
643
|
-
}
|
|
644
|
-
return geometry;
|
|
645
|
-
}
|
|
646
|
-
function acquire(particle) {
|
|
647
|
-
const parkingKey = `${geometryKeyOf(particle)}|${particle.lit}|${particle.additive}`;
|
|
648
|
-
const mesh = parked.get(parkingKey)?.pop() ?? new THREE.Mesh(geometryFor(particle), materialOf(THREE, particle));
|
|
649
|
-
mesh.material.color.set(particle.color);
|
|
650
|
-
mesh.frustumCulled = false;
|
|
651
|
-
mesh.renderOrder = particle.additive ? 5 : 3;
|
|
652
|
-
group.add(mesh);
|
|
653
|
-
return { mesh, parkingKey, stamp };
|
|
654
|
-
}
|
|
655
|
-
function park(entry) {
|
|
656
|
-
entry.mesh.removeFromParent();
|
|
657
|
-
let free = parked.get(entry.parkingKey);
|
|
658
|
-
if (!free) {
|
|
659
|
-
free = [];
|
|
660
|
-
parked.set(entry.parkingKey, free);
|
|
661
|
-
}
|
|
662
|
-
if (free.length < MAX_PARKED_MESHES_PER_KIND) free.push(entry.mesh);
|
|
663
|
-
else entry.mesh.material.dispose();
|
|
664
|
-
}
|
|
665
|
-
return {
|
|
666
|
-
group,
|
|
667
|
-
sync() {
|
|
668
|
-
stamp++;
|
|
669
|
-
for (const particle of pool.particles) {
|
|
670
|
-
let entry = drawn.get(particle.id);
|
|
671
|
-
if (!entry) {
|
|
672
|
-
entry = acquire(particle);
|
|
673
|
-
drawn.set(particle.id, entry);
|
|
674
|
-
}
|
|
675
|
-
entry.stamp = stamp;
|
|
676
|
-
const mesh = entry.mesh;
|
|
677
|
-
frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
|
|
678
|
-
offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
|
|
679
|
-
mesh.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
|
|
680
|
-
turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
|
|
681
|
-
mesh.quaternion.copy(frame).multiply(turn);
|
|
682
|
-
mesh.scale.set(particle.radius * particle.scale, (isCone(particle) ? particle.height : particle.radius) * particle.scale, particle.radius * particle.scale);
|
|
683
|
-
mesh.material.opacity = particle.opacity;
|
|
684
|
-
}
|
|
685
|
-
for (const [id, entry] of drawn) {
|
|
686
|
-
if (entry.stamp === stamp) continue;
|
|
687
|
-
park(entry);
|
|
688
|
-
drawn.delete(id);
|
|
689
|
-
}
|
|
690
|
-
return pool.count;
|
|
691
|
-
},
|
|
692
|
-
dispose() {
|
|
693
|
-
for (const entry of drawn.values()) park(entry);
|
|
694
|
-
drawn.clear();
|
|
695
|
-
for (const free of parked.values()) for (const mesh of free) mesh.material.dispose();
|
|
696
|
-
parked.clear();
|
|
697
|
-
for (const geometry of geometries.values()) geometry.dispose();
|
|
698
|
-
geometries.clear();
|
|
699
|
-
group.removeFromParent();
|
|
700
|
-
}
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// src/screen-overlay.ts
|
|
705
|
-
var clamp = (value, min, max) => value < min ? min : value > max ? max : value;
|
|
706
|
-
function layer(container, name, blend) {
|
|
707
|
-
const element = container.ownerDocument.createElement("div");
|
|
708
|
-
element.dataset.vfxOverlay = name;
|
|
709
|
-
Object.assign(element.style, { position: "absolute", inset: "0", pointerEvents: "none", opacity: "0" });
|
|
710
|
-
if (blend) element.style.mixBlendMode = blend;
|
|
711
|
-
container.appendChild(element);
|
|
712
|
-
return element;
|
|
713
|
-
}
|
|
714
|
-
function createScreenOverlay(container) {
|
|
715
|
-
const flash = layer(container, "flash", "screen");
|
|
716
|
-
flash.style.background = "#fff";
|
|
717
|
-
const white = layer(container, "white");
|
|
718
|
-
const red = layer(container, "red");
|
|
719
|
-
const sides = layer(container, "sides");
|
|
720
|
-
const gas = layer(container, "gas");
|
|
721
|
-
const elements = [flash, white, red, sides, gas];
|
|
722
|
-
return {
|
|
723
|
-
update(state) {
|
|
724
|
-
flash.style.opacity = clamp(state.flash, 0, 1).toFixed(3);
|
|
725
|
-
white.style.opacity = "1";
|
|
726
|
-
white.style.background = `rgba(235,240,255,${clamp(state.white, 0, 1).toFixed(2)})`;
|
|
727
|
-
red.style.opacity = "1";
|
|
728
|
-
red.style.background = `radial-gradient(ellipse at center, transparent 40%, rgba(170,16,12,${(clamp(state.red, 0, 1) * 0.85).toFixed(2)}) 100%)`;
|
|
729
|
-
sides.style.opacity = "1";
|
|
730
|
-
sides.style.background = `linear-gradient(90deg, rgba(190,20,14,${(state.directionLeft * 0.8).toFixed(2)}) 0%, transparent 35%, transparent 65%, rgba(190,20,14,${(state.directionRight * 0.8).toFixed(2)}) 100%)`;
|
|
731
|
-
gas.style.opacity = "1";
|
|
732
|
-
gas.style.background = state.gas > 0.01 ? `rgba(90,130,40,${(state.gas * 0.55).toFixed(2)})` : "transparent";
|
|
733
|
-
},
|
|
734
|
-
dispose() {
|
|
735
|
-
for (const element of elements) element.remove();
|
|
736
|
-
}
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// src/shaders/shock.ts
|
|
741
|
-
var SHELL_VERTEX = `varying vec3 vN;varying vec3 vV;void main(){vec4 wp=modelMatrix*vec4(position,1.);vN=normalize(mat3(modelMatrix)*normal);vV=normalize(cameraPosition-wp.xyz);gl_Position=projectionMatrix*viewMatrix*wp;}`;
|
|
742
|
-
var SHELL_FRAGMENT = `uniform float uA;varying vec3 vN;varying vec3 vV;void main(){float f=1.-abs(dot(vN,vV));f=pow(f,5.);gl_FragColor=vec4(vec3(1.,.93,.82)*f*uA*.45,1.);
|
|
743
|
-
#include <tonemapping_fragment>
|
|
744
|
-
#include <colorspace_fragment>
|
|
745
|
-
}`;
|
|
746
|
-
var RING_VERTEX = `varying vec2 vU;void main(){vU=position.xz;gl_Position=projectionMatrix*viewMatrix*modelMatrix*vec4(position,1.);}`;
|
|
747
|
-
var RING_FRAGMENT = `uniform float uA;uniform float uW;uniform vec3 uC;varying vec2 vU;
|
|
748
|
-
float h(vec2 p){return fract(sin(dot(p,vec2(12.9898,78.233)))*43758.5453);}
|
|
749
|
-
void main(){float r=length(vU);float b=smoothstep(1.,1.-uW,r)*smoothstep(1.-uW*3.5,1.-uW,r);float a=atan(vU.y,vU.x);float n=.7+.3*h(vec2(floor(a*14.),1.));gl_FragColor=vec4(uC,b*uA*n);
|
|
750
|
-
#include <tonemapping_fragment>
|
|
751
|
-
#include <colorspace_fragment>
|
|
752
|
-
}`;
|
|
753
|
-
|
|
754
|
-
// src/shocks.ts
|
|
755
|
-
var RING_HEIGHT = 0.05;
|
|
756
|
-
function createShockLayer(THREE, pool) {
|
|
757
|
-
const group = new THREE.Group();
|
|
758
|
-
const shellGeometry = new THREE.SphereGeometry(1, 28, 18);
|
|
759
|
-
const ringGeometry = new THREE.PlaneGeometry(2, 2).rotateX(-Math.PI / 2);
|
|
760
|
-
const pairs = [];
|
|
761
|
-
function makePair() {
|
|
762
|
-
const shellAlpha = { value: 0 };
|
|
763
|
-
const ringAlpha = { value: 0 };
|
|
764
|
-
const shellMaterial = new THREE.ShaderMaterial({ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, uniforms: { uA: shellAlpha }, vertexShader: SHELL_VERTEX, fragmentShader: SHELL_FRAGMENT });
|
|
765
|
-
const ringMaterial = new THREE.ShaderMaterial({
|
|
766
|
-
transparent: true,
|
|
767
|
-
depthWrite: false,
|
|
768
|
-
uniforms: { uA: ringAlpha, uW: { value: 0.12 }, uC: { value: new THREE.Color(0.55, 0.5, 0.44) } },
|
|
769
|
-
vertexShader: RING_VERTEX,
|
|
770
|
-
fragmentShader: RING_FRAGMENT
|
|
771
|
-
});
|
|
772
|
-
const shell = new THREE.Mesh(shellGeometry, shellMaterial);
|
|
773
|
-
shell.renderOrder = 4;
|
|
774
|
-
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
|
775
|
-
ring.renderOrder = 3;
|
|
776
|
-
group.add(shell, ring);
|
|
777
|
-
return { shell, ring, shellAlpha, ringAlpha };
|
|
778
|
-
}
|
|
779
|
-
return {
|
|
780
|
-
group,
|
|
781
|
-
sync() {
|
|
782
|
-
const shocks = pool.items;
|
|
783
|
-
for (let index = 0; index < shocks.length; index++) {
|
|
784
|
-
const pair = pairs[index] ?? (pairs[index] = makePair());
|
|
785
|
-
const shock = shocks[index];
|
|
786
|
-
pair.shell.visible = pair.ring.visible = true;
|
|
787
|
-
pair.shell.position.set(shock.x, shock.y, shock.z);
|
|
788
|
-
pair.shell.scale.setScalar(shock.shellScale);
|
|
789
|
-
pair.shellAlpha.value = shock.shellAlpha;
|
|
790
|
-
pair.ring.position.set(shock.x, RING_HEIGHT, shock.z);
|
|
791
|
-
pair.ring.scale.setScalar(shock.ringScale);
|
|
792
|
-
pair.ringAlpha.value = shock.ringAlpha;
|
|
793
|
-
}
|
|
794
|
-
for (let index = shocks.length; index < pairs.length; index++) pairs[index].shell.visible = pairs[index].ring.visible = false;
|
|
795
|
-
return shocks.length;
|
|
796
|
-
},
|
|
797
|
-
dispose() {
|
|
798
|
-
for (const pair of pairs) {
|
|
799
|
-
;
|
|
800
|
-
pair.shell.material.dispose();
|
|
801
|
-
pair.ring.material.dispose();
|
|
802
|
-
}
|
|
803
|
-
shellGeometry.dispose();
|
|
804
|
-
ringGeometry.dispose();
|
|
805
|
-
}
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// src/shaders/sprite.ts
|
|
810
|
-
var SPRITE_VERTEX = `
|
|
811
|
-
uniform float uScale;
|
|
812
|
-
attribute float size;
|
|
813
|
-
attribute float alpha;
|
|
814
|
-
attribute vec3 pcolor;
|
|
815
|
-
varying float vAlpha;
|
|
816
|
-
varying vec3 vColor;
|
|
817
|
-
void main() {
|
|
818
|
-
vAlpha = alpha;
|
|
819
|
-
vColor = pcolor;
|
|
820
|
-
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
821
|
-
gl_PointSize = min(size * uScale / max(0.1, -mv.z), 256.0);
|
|
822
|
-
gl_Position = projectionMatrix * mv;
|
|
823
|
-
}`;
|
|
824
|
-
var SPRITE_FRAGMENT = `
|
|
825
|
-
varying float vAlpha;
|
|
826
|
-
varying vec3 vColor;
|
|
827
|
-
void main() {
|
|
828
|
-
float d = length(gl_PointCoord - vec2(0.5)) * 2.0;
|
|
829
|
-
float a = (1.0 - smoothstep(0.35, 1.0, d)) * vAlpha;
|
|
830
|
-
if (a < 0.004) discard;
|
|
831
|
-
gl_FragColor = vec4(vColor, a);
|
|
832
|
-
}`;
|
|
833
|
-
|
|
834
|
-
// src/sprites.ts
|
|
835
|
-
function drawPool(THREE, pool, additive, pointScale) {
|
|
836
|
-
const geometry = new THREE.BufferGeometry();
|
|
837
|
-
const attributes = [["position", pool.position, 3], ["pcolor", pool.color, 3], ["alpha", pool.alpha, 1], ["size", pool.size, 1]];
|
|
838
|
-
const buffers = attributes.map(([name, array, size]) => {
|
|
839
|
-
const attribute = new THREE.BufferAttribute(array, size).setUsage(THREE.DynamicDrawUsage);
|
|
840
|
-
geometry.setAttribute(name, attribute);
|
|
841
|
-
return attribute;
|
|
842
|
-
});
|
|
843
|
-
geometry.setDrawRange(0, 0);
|
|
844
|
-
const material = new THREE.ShaderMaterial({
|
|
845
|
-
uniforms: { uScale: { value: pointScale } },
|
|
846
|
-
vertexShader: SPRITE_VERTEX,
|
|
847
|
-
fragmentShader: SPRITE_FRAGMENT,
|
|
848
|
-
transparent: true,
|
|
849
|
-
depthWrite: false,
|
|
850
|
-
blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending
|
|
851
|
-
});
|
|
852
|
-
const points = new THREE.Points(geometry, material);
|
|
853
|
-
points.frustumCulled = false;
|
|
854
|
-
points.name = additive ? "vfx-sprites-additive" : "vfx-sprites";
|
|
855
|
-
points.renderOrder = additive ? 11 : 10;
|
|
856
|
-
return {
|
|
857
|
-
points,
|
|
858
|
-
sync() {
|
|
859
|
-
geometry.setDrawRange(0, pool.drawCount);
|
|
860
|
-
for (const buffer of buffers) buffer.needsUpdate = true;
|
|
861
|
-
return pool.drawCount;
|
|
862
|
-
},
|
|
863
|
-
dispose() {
|
|
864
|
-
geometry.dispose();
|
|
865
|
-
material.dispose();
|
|
866
|
-
}
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
function createSpriteLayer(THREE, system, pointScale) {
|
|
870
|
-
const additive = drawPool(THREE, system.additive, true, pointScale);
|
|
871
|
-
const normal = drawPool(THREE, system.normal, false, pointScale);
|
|
872
|
-
return {
|
|
873
|
-
additive: additive.points,
|
|
874
|
-
normal: normal.points,
|
|
875
|
-
sync: () => additive.sync() + normal.sync(),
|
|
876
|
-
dispose() {
|
|
877
|
-
additive.dispose();
|
|
878
|
-
normal.dispose();
|
|
879
|
-
}
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// src/shaders/workshop-sprite.ts
|
|
884
|
-
var WORKSHOP_SPRITE_VERTEX = `attribute float size;attribute float alpha;attribute vec3 pcol;varying vec3 vC;varying float vA;uniform float uScale;
|
|
885
|
-
void main(){vC=pcol;vA=alpha;vec4 mv=modelViewMatrix*vec4(position,1.);gl_PointSize=clamp(size*uScale/-mv.z,1.,512.);gl_Position=projectionMatrix*mv;}`;
|
|
886
|
-
var WORKSHOP_SPRITE_FRAGMENT = `uniform float uSoft;varying vec3 vC;varying float vA;
|
|
887
|
-
void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a=mix(smoothstep(1.,.78,r),pow(1.-r,1.5),uSoft);gl_FragColor=vec4(vC,vA*a);}`;
|
|
888
|
-
|
|
889
|
-
// src/workshop-layers.ts
|
|
890
|
-
var DEBRIS_CAPACITY = 500;
|
|
891
|
-
var DECAL_CAPACITY = 400;
|
|
892
|
-
var RING_CAPACITY = 120;
|
|
893
|
-
var RIPPLE_SEGMENTS = 12;
|
|
894
|
-
var FADE_SECONDS = 0.5;
|
|
895
|
-
var FIRE_LIGHT_COLOR = 16742954;
|
|
896
|
-
var FIRE_LIGHT_DISTANCE = 14;
|
|
897
|
-
var GLOSS_DRIED_AT = 0.85;
|
|
898
|
-
var BROWN = 2758160;
|
|
899
|
-
var GROUND_COLOR = 8947848;
|
|
900
|
-
var FUEL_DRIED = 8947848;
|
|
901
|
-
var clamp2 = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
|
|
902
|
-
function spriteLayer(THREE, pool, additive, soft, scale) {
|
|
903
|
-
const geometry = new THREE.BufferGeometry();
|
|
904
|
-
const arrays = [["position", pool.position, 3], ["pcol", pool.color, 3], ["size", pool.size, 1], ["alpha", pool.alpha, 1]];
|
|
905
|
-
const buffers = arrays.map(([name, array, size]) => {
|
|
906
|
-
const attribute = new THREE.BufferAttribute(array, size).setUsage(THREE.DynamicDrawUsage);
|
|
907
|
-
geometry.setAttribute(name, attribute);
|
|
908
|
-
return attribute;
|
|
909
|
-
});
|
|
910
|
-
geometry.setDrawRange(0, 0);
|
|
911
|
-
const material = new THREE.ShaderMaterial({
|
|
912
|
-
uniforms: { uScale: { value: scale }, uSoft: { value: soft } },
|
|
913
|
-
vertexShader: WORKSHOP_SPRITE_VERTEX,
|
|
914
|
-
fragmentShader: WORKSHOP_SPRITE_FRAGMENT,
|
|
915
|
-
transparent: true,
|
|
916
|
-
depthWrite: false,
|
|
917
|
-
blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending
|
|
918
|
-
});
|
|
919
|
-
const points = new THREE.Points(geometry, material);
|
|
920
|
-
points.frustumCulled = false;
|
|
921
|
-
points.renderOrder = additive ? 3 : 2;
|
|
922
|
-
points.name = additive ? "workshop-add" : "workshop-normal";
|
|
923
|
-
return {
|
|
924
|
-
object: points,
|
|
925
|
-
sync() {
|
|
926
|
-
geometry.setDrawRange(0, pool.count);
|
|
927
|
-
for (const buffer of buffers) buffer.needsUpdate = true;
|
|
928
|
-
return pool.count > 0;
|
|
929
|
-
},
|
|
930
|
-
dispose() {
|
|
931
|
-
geometry.dispose();
|
|
932
|
-
material.dispose();
|
|
933
|
-
}
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
function streakLayer(THREE, pool) {
|
|
937
|
-
const geometry = new THREE.BufferGeometry();
|
|
938
|
-
const position = new THREE.BufferAttribute(pool.segment, 3).setUsage(THREE.DynamicDrawUsage);
|
|
939
|
-
const color = new THREE.BufferAttribute(pool.segmentColor, 3).setUsage(THREE.DynamicDrawUsage);
|
|
940
|
-
geometry.setAttribute("position", position);
|
|
941
|
-
geometry.setAttribute("color", color);
|
|
942
|
-
geometry.setDrawRange(0, 0);
|
|
943
|
-
const material = new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false });
|
|
944
|
-
const lines = new THREE.LineSegments(geometry, material);
|
|
945
|
-
lines.frustumCulled = false;
|
|
946
|
-
lines.renderOrder = 4;
|
|
947
|
-
lines.name = "workshop-streaks";
|
|
948
|
-
return {
|
|
949
|
-
object: lines,
|
|
950
|
-
sync() {
|
|
951
|
-
geometry.setDrawRange(0, pool.count * 2);
|
|
952
|
-
position.needsUpdate = color.needsUpdate = true;
|
|
953
|
-
return pool.count > 0;
|
|
954
|
-
},
|
|
955
|
-
dispose() {
|
|
956
|
-
geometry.dispose();
|
|
957
|
-
material.dispose();
|
|
958
|
-
}
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
function rainLayer(THREE, rain) {
|
|
962
|
-
const geometry = new THREE.BufferGeometry();
|
|
963
|
-
const position = new THREE.BufferAttribute(rain.positions, 3).setUsage(THREE.DynamicDrawUsage);
|
|
964
|
-
geometry.setAttribute("position", position);
|
|
965
|
-
geometry.setDrawRange(0, 0);
|
|
966
|
-
const material = new THREE.LineBasicMaterial({ color: 10466255, transparent: true, opacity: 0.35, depthWrite: false });
|
|
967
|
-
const lines = new THREE.LineSegments(geometry, material);
|
|
968
|
-
lines.frustumCulled = false;
|
|
969
|
-
lines.name = "workshop-rain";
|
|
970
|
-
return {
|
|
971
|
-
object: lines,
|
|
972
|
-
sync() {
|
|
973
|
-
const drawn = rain.on && rain.count > 0;
|
|
974
|
-
lines.visible = drawn;
|
|
975
|
-
geometry.setDrawRange(0, drawn ? rain.count * 2 : 0);
|
|
976
|
-
position.needsUpdate = true;
|
|
977
|
-
return drawn;
|
|
978
|
-
},
|
|
979
|
-
dispose() {
|
|
980
|
-
geometry.dispose();
|
|
981
|
-
material.dispose();
|
|
982
|
-
}
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
function debrisLayer(THREE, pool) {
|
|
986
|
-
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
|
987
|
-
const material = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.45, metalness: 0.25, flatShading: true });
|
|
988
|
-
const mesh = new THREE.InstancedMesh(geometry, material, DEBRIS_CAPACITY);
|
|
989
|
-
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
990
|
-
mesh.frustumCulled = false;
|
|
991
|
-
mesh.name = "workshop-debris";
|
|
992
|
-
const euler = new THREE.Euler();
|
|
993
|
-
const quaternion = new THREE.Quaternion();
|
|
994
|
-
const position = new THREE.Vector3();
|
|
995
|
-
const scale = new THREE.Vector3();
|
|
996
|
-
const matrix = new THREE.Matrix4();
|
|
997
|
-
const colour = new THREE.Color();
|
|
998
|
-
const hidden = new THREE.Matrix4().makeScale(0, 0, 0);
|
|
999
|
-
for (let index = 0; index < DEBRIS_CAPACITY; index++) mesh.setMatrixAt(index, hidden);
|
|
1000
|
-
mesh.setColorAt(0, colour.setHex(16777215));
|
|
1001
|
-
return {
|
|
1002
|
-
object: mesh,
|
|
1003
|
-
sync() {
|
|
1004
|
-
let drawn = false;
|
|
1005
|
-
for (let index = 0; index < DEBRIS_CAPACITY; index++) {
|
|
1006
|
-
const piece = pool.pieces[index];
|
|
1007
|
-
if (!piece.on) {
|
|
1008
|
-
mesh.setMatrixAt(index, hidden);
|
|
1009
|
-
continue;
|
|
1010
|
-
}
|
|
1011
|
-
drawn = true;
|
|
1012
|
-
const shrink = Math.min(1, piece.life / FADE_SECONDS);
|
|
1013
|
-
euler.set(piece.rx, piece.ry, piece.rz);
|
|
1014
|
-
quaternion.setFromEuler(euler);
|
|
1015
|
-
matrix.compose(position.set(piece.x, piece.y, piece.z), quaternion, scale.set(piece.sx * shrink, piece.sy * shrink, piece.sz * shrink));
|
|
1016
|
-
mesh.setMatrixAt(index, matrix);
|
|
1017
|
-
mesh.setColorAt(index, colour.setHex(piece.color));
|
|
1018
|
-
}
|
|
1019
|
-
mesh.instanceMatrix.needsUpdate = true;
|
|
1020
|
-
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
|
|
1021
|
-
return drawn;
|
|
1022
|
-
},
|
|
1023
|
-
dispose() {
|
|
1024
|
-
geometry.dispose();
|
|
1025
|
-
material.dispose();
|
|
1026
|
-
}
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
function decalLook(decal, colour, dryColour, brown, ground, index) {
|
|
1030
|
-
const grown = decal.grow > 0 ? 1 - Math.pow(1 - Math.min(1, decal.t / decal.grow), 2.4) : 1;
|
|
1031
|
-
let sx = decal.sx * grown;
|
|
1032
|
-
let sy = decal.sy * grown;
|
|
1033
|
-
colour.setHex(decal.color);
|
|
1034
|
-
let dried = false;
|
|
1035
|
-
const fluid = decal.fluid;
|
|
1036
|
-
if (fluid) {
|
|
1037
|
-
const creep = fluid === "oil" ? 0.5 : fluid === "fuel" ? 0.7 : fluid === "bile" ? 0.25 : fluid === "water" ? 0.2 : 0.3;
|
|
1038
|
-
const age = decal.t;
|
|
1039
|
-
if (decal.dryTime > 0) {
|
|
1040
|
-
const k = clamp2((age - decal.dryAt) / decal.dryTime, 0, 1);
|
|
1041
|
-
if (decal.dryColor >= 0) dryColour.setHex(decal.dryColor);
|
|
1042
|
-
else dryColour.copy(colour).multiplyScalar(0.45).lerp(brown, 0.45);
|
|
1043
|
-
dried = k > GLOSS_DRIED_AT;
|
|
1044
|
-
const spread = dried ? 1 + creep : 1 + creep * (1 - Math.exp(-age / 6));
|
|
1045
|
-
sx *= dried ? spread : spread * (1 + 0.04 * Math.sin(age * 1.9 + index));
|
|
1046
|
-
sy *= dried ? spread : spread * (1 + 0.04 * Math.cos(age * 1.5 + index * 0.7));
|
|
1047
|
-
colour.lerp(dryColour, k);
|
|
1048
|
-
} else {
|
|
1049
|
-
const spread = 1 + creep * (1 - Math.exp(-age / 6));
|
|
1050
|
-
sx *= spread * (1 + 0.04 * Math.sin(age * 1.9 + index));
|
|
1051
|
-
sy *= spread * (1 + 0.04 * Math.cos(age * 1.5 + index * 0.7));
|
|
1052
|
-
if (fluid === "fuel" || fluid === "water") {
|
|
1053
|
-
const [wait, span] = fluid === "fuel" ? [6, 5] : [40, 20];
|
|
1054
|
-
const gone = clamp2((age - wait) / span, 0, 1);
|
|
1055
|
-
sx *= 1 - gone * 0.85;
|
|
1056
|
-
sy *= 1 - gone * 0.85;
|
|
1057
|
-
colour.lerp(ground.setHex(fluid === "fuel" ? FUEL_DRIED : GROUND_COLOR), gone * 0.8);
|
|
1058
|
-
if (gone >= 1) return null;
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
return { sx, sy, dried };
|
|
1063
|
-
}
|
|
1064
|
-
function decalLayer(THREE, pool) {
|
|
1065
|
-
const geometry = new THREE.CircleGeometry(0.5, RIPPLE_SEGMENTS);
|
|
1066
|
-
const positions = geometry.getAttribute("position");
|
|
1067
|
-
for (let index = 1; index < positions.count; index++) {
|
|
1068
|
-
const k = 0.72 + (index * 2654435761 >>> 0) / 4294967296 * 0.4;
|
|
1069
|
-
positions.setXY(index, positions.getX(index) * k, positions.getY(index) * k);
|
|
1070
|
-
}
|
|
1071
|
-
const wet = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.1, metalness: 0.3, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 });
|
|
1072
|
-
const dry = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 1, metalness: 0, flatShading: true, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 });
|
|
1073
|
-
const wetMesh = new THREE.InstancedMesh(geometry, wet, DECAL_CAPACITY);
|
|
1074
|
-
const dryMesh = new THREE.InstancedMesh(geometry, dry, DECAL_CAPACITY);
|
|
1075
|
-
const group = new THREE.Group();
|
|
1076
|
-
group.name = "workshop-decals";
|
|
1077
|
-
for (const mesh of [wetMesh, dryMesh]) {
|
|
1078
|
-
mesh.frustumCulled = false;
|
|
1079
|
-
mesh.renderOrder = 1;
|
|
1080
|
-
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
1081
|
-
group.add(mesh);
|
|
1082
|
-
}
|
|
1083
|
-
const hidden = new THREE.Matrix4().makeScale(0, 0, 0);
|
|
1084
|
-
const colour = new THREE.Color();
|
|
1085
|
-
const dryColour = new THREE.Color();
|
|
1086
|
-
const brown = new THREE.Color(BROWN);
|
|
1087
|
-
const ground = new THREE.Color();
|
|
1088
|
-
const normal = new THREE.Vector3();
|
|
1089
|
-
const forward = new THREE.Vector3(0, 0, 1);
|
|
1090
|
-
const turn = new THREE.Quaternion();
|
|
1091
|
-
const quaternion = new THREE.Quaternion();
|
|
1092
|
-
const position = new THREE.Vector3();
|
|
1093
|
-
const scale = new THREE.Vector3();
|
|
1094
|
-
const matrix = new THREE.Matrix4();
|
|
1095
|
-
wetMesh.setColorAt(0, colour.setHex(0));
|
|
1096
|
-
dryMesh.setColorAt(0, colour.setHex(0));
|
|
1097
|
-
for (let index = 0; index < DECAL_CAPACITY; index++) {
|
|
1098
|
-
wetMesh.setMatrixAt(index, hidden);
|
|
1099
|
-
dryMesh.setMatrixAt(index, hidden);
|
|
1100
|
-
}
|
|
1101
|
-
return {
|
|
1102
|
-
object: group,
|
|
1103
|
-
sync() {
|
|
1104
|
-
let drawn = false;
|
|
1105
|
-
for (let index = 0; index < DECAL_CAPACITY; index++) {
|
|
1106
|
-
const decal = pool.decals[index];
|
|
1107
|
-
wetMesh.setMatrixAt(index, hidden);
|
|
1108
|
-
dryMesh.setMatrixAt(index, hidden);
|
|
1109
|
-
if (!decal.on || decal.t < 0) continue;
|
|
1110
|
-
const look = decalLook(decal, colour, dryColour, brown, ground, index);
|
|
1111
|
-
if (!look) continue;
|
|
1112
|
-
drawn = true;
|
|
1113
|
-
normal.set(decal.normal[0], decal.normal[1], decal.normal[2]);
|
|
1114
|
-
quaternion.setFromUnitVectors(forward, normal);
|
|
1115
|
-
quaternion.multiply(turn.setFromAxisAngle(forward, index * 2.399963));
|
|
1116
|
-
const offset = 4e-3 + index % 40 * 4e-4;
|
|
1117
|
-
matrix.compose(position.set(decal.x + normal.x * offset, decal.y + normal.y * offset, decal.z + normal.z * offset), quaternion, scale.set(look.sx, look.sy, 1));
|
|
1118
|
-
const mesh = look.dried ? dryMesh : wetMesh;
|
|
1119
|
-
mesh.setMatrixAt(index, matrix);
|
|
1120
|
-
mesh.setColorAt(index, colour);
|
|
1121
|
-
}
|
|
1122
|
-
for (const mesh of [wetMesh, dryMesh]) {
|
|
1123
|
-
mesh.instanceMatrix.needsUpdate = true;
|
|
1124
|
-
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
|
|
1125
|
-
}
|
|
1126
|
-
return drawn;
|
|
1127
|
-
},
|
|
1128
|
-
dispose() {
|
|
1129
|
-
geometry.dispose();
|
|
1130
|
-
wet.dispose();
|
|
1131
|
-
dry.dispose();
|
|
1132
|
-
}
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
function ringLayer(THREE, pool) {
|
|
1136
|
-
const geometry = new THREE.RingGeometry(0.86, 1, 40);
|
|
1137
|
-
const material = new THREE.MeshBasicMaterial({ color: 16777215, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide });
|
|
1138
|
-
const mesh = new THREE.InstancedMesh(geometry, material, RING_CAPACITY);
|
|
1139
|
-
mesh.frustumCulled = false;
|
|
1140
|
-
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
1141
|
-
mesh.name = "workshop-rings";
|
|
1142
|
-
const hidden = new THREE.Matrix4().makeScale(0, 0, 0);
|
|
1143
|
-
const colour = new THREE.Color();
|
|
1144
|
-
const normal = new THREE.Vector3();
|
|
1145
|
-
const forward = new THREE.Vector3(0, 0, 1);
|
|
1146
|
-
const quaternion = new THREE.Quaternion();
|
|
1147
|
-
const position = new THREE.Vector3();
|
|
1148
|
-
const scale = new THREE.Vector3();
|
|
1149
|
-
const matrix = new THREE.Matrix4();
|
|
1150
|
-
mesh.setColorAt(0, colour.setHex(0));
|
|
1151
|
-
for (let index = 0; index < RING_CAPACITY; index++) mesh.setMatrixAt(index, hidden);
|
|
1152
|
-
return {
|
|
1153
|
-
object: mesh,
|
|
1154
|
-
sync() {
|
|
1155
|
-
let drawn = false;
|
|
1156
|
-
for (let index = 0; index < RING_CAPACITY; index++) {
|
|
1157
|
-
const ring = pool.rings[index];
|
|
1158
|
-
if (!ring.on) {
|
|
1159
|
-
mesh.setMatrixAt(index, hidden);
|
|
1160
|
-
continue;
|
|
1161
|
-
}
|
|
1162
|
-
drawn = true;
|
|
1163
|
-
const k = Math.min(1, ring.t / ring.duration);
|
|
1164
|
-
const radius = ring.from + (ring.to - ring.from) * (1 - Math.pow(1 - k, 2));
|
|
1165
|
-
quaternion.setFromUnitVectors(forward, normal.set(ring.normal[0], ring.normal[1], ring.normal[2]));
|
|
1166
|
-
matrix.compose(position.set(ring.point[0], ring.point[1], ring.point[2]), quaternion, scale.set(radius, radius, 1));
|
|
1167
|
-
mesh.setMatrixAt(index, matrix);
|
|
1168
|
-
mesh.setColorAt(index, colour.setHex(ring.color).multiplyScalar(ring.hold ? ring.hold : 1 - k));
|
|
1169
|
-
}
|
|
1170
|
-
mesh.instanceMatrix.needsUpdate = true;
|
|
1171
|
-
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
|
|
1172
|
-
return drawn;
|
|
1173
|
-
},
|
|
1174
|
-
dispose() {
|
|
1175
|
-
geometry.dispose();
|
|
1176
|
-
material.dispose();
|
|
1177
|
-
}
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
function createWorkshopLayers(THREE, engine, pointScale, lightScale) {
|
|
1181
|
-
const group = new THREE.Group();
|
|
1182
|
-
group.name = "workshop";
|
|
1183
|
-
const layers = [
|
|
1184
|
-
spriteLayer(THREE, engine.add, true, 1, pointScale),
|
|
1185
|
-
spriteLayer(THREE, engine.normal, false, 0.55, pointScale),
|
|
1186
|
-
streakLayer(THREE, engine.streaks),
|
|
1187
|
-
rainLayer(THREE, engine.rain),
|
|
1188
|
-
debrisLayer(THREE, engine.debris),
|
|
1189
|
-
decalLayer(THREE, engine.decals),
|
|
1190
|
-
ringLayer(THREE, engine.rings)
|
|
1191
|
-
];
|
|
1192
|
-
for (const layer2 of layers) group.add(layer2.object);
|
|
1193
|
-
const meshes = createMeshLayer(THREE, engine.meshes);
|
|
1194
|
-
group.add(meshes.group);
|
|
1195
|
-
const flashLights = engine.lights.lights.map(() => new THREE.PointLight(16760944, 0, 14));
|
|
1196
|
-
const fireLights = engine.lights.fire.map(() => new THREE.PointLight(FIRE_LIGHT_COLOR, 0, FIRE_LIGHT_DISTANCE));
|
|
1197
|
-
for (const light of [...flashLights, ...fireLights]) group.add(light);
|
|
1198
|
-
return {
|
|
1199
|
-
group,
|
|
1200
|
-
sync() {
|
|
1201
|
-
let drawCalls = 0;
|
|
1202
|
-
for (const layer2 of layers) if (layer2.sync()) drawCalls++;
|
|
1203
|
-
drawCalls += meshes.sync();
|
|
1204
|
-
engine.lights.lights.forEach((source, index) => {
|
|
1205
|
-
const light = flashLights[index];
|
|
1206
|
-
light.position.set(source.x, source.y, source.z);
|
|
1207
|
-
light.color.setHex(source.color);
|
|
1208
|
-
light.distance = source.distance;
|
|
1209
|
-
light.intensity = source.intensity * lightScale;
|
|
1210
|
-
});
|
|
1211
|
-
engine.lights.fire.forEach((source, index) => {
|
|
1212
|
-
const light = fireLights[index];
|
|
1213
|
-
light.position.set(source.x, source.y, source.z);
|
|
1214
|
-
light.intensity = source.intensity * lightScale;
|
|
1215
|
-
});
|
|
1216
|
-
return drawCalls;
|
|
1217
|
-
},
|
|
1218
|
-
dispose() {
|
|
1219
|
-
for (const layer2 of layers) layer2.dispose();
|
|
1220
|
-
meshes.dispose();
|
|
1221
|
-
group.clear();
|
|
1222
|
-
}
|
|
1223
|
-
};
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
// src/renderer.ts
|
|
1227
|
-
var LAB_LIGHT_DIRECTION = [-14, 26, 12];
|
|
1228
|
-
var SKY_COLOR = 7306386;
|
|
1229
|
-
var GROUND_COLOR2 = 2761759;
|
|
1230
|
-
var LOD_BANDS = { auto: null, near: 1, middle: 0.5, far: 0.2 };
|
|
1231
|
-
var QUALITY_LEVELS = { high: 0, medium: 1, low: 2 };
|
|
1232
|
-
var VFX_LAYERS = ["puffs", "sparks", "glows", "debris", "shock", "sprites"];
|
|
1233
|
-
var LAYER_PREFIX = "layer_";
|
|
1234
|
-
var SHAKE_REACH = 0.18;
|
|
1235
|
-
var SHAKE_FLOOR = 1e-3;
|
|
1236
|
-
function isWorld(value) {
|
|
1237
|
-
return typeof value === "object" && value !== null && "explosion" in value && typeof value.explosion === "function";
|
|
1238
|
-
}
|
|
1239
|
-
function mergeScreens(lab, workshop) {
|
|
1240
|
-
if (!workshop) return lab;
|
|
1241
|
-
const merged = { ...lab };
|
|
1242
|
-
for (const key of Object.keys(lab)) merged[key] = Math.max(lab[key], workshop[key]);
|
|
1243
|
-
return merged;
|
|
1244
|
-
}
|
|
1245
|
-
function prefersReducedMotion() {
|
|
1246
|
-
return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1247
|
-
}
|
|
1248
|
-
function createVfxRenderer(THREE, options) {
|
|
1249
|
-
const reducedMotion = options.reducedMotion ?? prefersReducedMotion();
|
|
1250
|
-
const requested = options.vfx;
|
|
1251
|
-
const world = isWorld(requested) ? requested : createVfx({ profile: options.quality ?? "medium", reducedMotion, ...requested });
|
|
1252
|
-
const pools = world.pools();
|
|
1253
|
-
let anchors;
|
|
1254
|
-
const releaseAnchors = () => {
|
|
1255
|
-
if (!anchors) return;
|
|
1256
|
-
anchors.meshes[0]?.geometry.dispose();
|
|
1257
|
-
for (const mesh of anchors.meshes) mesh.material.dispose();
|
|
1258
|
-
anchors = void 0;
|
|
1259
|
-
};
|
|
1260
|
-
const root = markKitOrigin(new THREE.Group(), { owner: "game-vfx-three", kind: "effect" });
|
|
1261
|
-
options.scene.add(root);
|
|
1262
|
-
const direction = new THREE.Vector3(...options.lightDirection ?? LAB_LIGHT_DIRECTION).normalize();
|
|
1263
|
-
const sky = hexToLinearRgb(SKY_COLOR);
|
|
1264
|
-
const ground = hexToLinearRgb(GROUND_COLOR2);
|
|
1265
|
-
const uniforms = {
|
|
1266
|
-
uLight: { value: direction },
|
|
1267
|
-
uSky: { value: new THREE.Vector3(sky[0] * 0.55, sky[1] * 0.55, sky[2] * 0.55) },
|
|
1268
|
-
uGround: { value: new THREE.Vector3(ground[0] * 0.6, ground[1] * 0.6, ground[2] * 0.6) },
|
|
1269
|
-
uGlowPos: { value: new THREE.Vector3() },
|
|
1270
|
-
uGlowCol: { value: new THREE.Vector3(1, 0.5, 0.2) },
|
|
1271
|
-
uGlowI: { value: 0 },
|
|
1272
|
-
uTime: { value: 0 }
|
|
1273
|
-
};
|
|
1274
|
-
const puffs = createPuffLayer(THREE, pools.puffs, uniforms);
|
|
1275
|
-
const sparks = createSparkLayer(THREE, pools.sparks);
|
|
1276
|
-
const glows = createGlowLayer(THREE, pools.glows);
|
|
1277
|
-
const rockGeometry = new THREE.DodecahedronGeometry(0.5, 0);
|
|
1278
|
-
const plateGeometry = new THREE.BoxGeometry(1, 1, 1);
|
|
1279
|
-
const chunks = createChunkLayer(THREE, pools.chunks, { rock: rockGeometry, plate: plateGeometry }, colorOfChunk);
|
|
1280
|
-
const shocks = createShockLayer(THREE, pools.shocks);
|
|
1281
|
-
const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
|
|
1282
|
-
const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
|
|
1283
|
-
const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
|
|
1284
|
-
root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
|
|
1285
|
-
const stopMarks = world.onMarks((event) => marks.handle(event));
|
|
1286
|
-
const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
|
|
1287
|
-
let drawCalls = 0;
|
|
1288
|
-
let workshopLayers;
|
|
1289
|
-
let workshopEngine;
|
|
1290
|
-
const layerOn = /* @__PURE__ */ new Map();
|
|
1291
|
-
const layerObjects = () => ({
|
|
1292
|
-
puffs: [puffs.mesh],
|
|
1293
|
-
sparks: [sparks.mesh],
|
|
1294
|
-
glows: [glows.mesh],
|
|
1295
|
-
debris: [chunks.rocks, chunks.plates],
|
|
1296
|
-
shock: [shocks.group],
|
|
1297
|
-
sprites: [sprites.additive, sprites.normal, ...workshopLayers ? [workshopLayers.group] : []]
|
|
1298
|
-
});
|
|
1299
|
-
const applyLayers = () => {
|
|
1300
|
-
for (const [id, objects] of Object.entries(layerObjects())) for (const object of objects) object.visible = layerOn.get(id) !== false;
|
|
1301
|
-
};
|
|
1302
|
-
const screenNow = () => mergeScreens(world.screen(), world.peekWorkshop()?.screen);
|
|
1303
|
-
let clock = 0;
|
|
1304
|
-
const lastDrawn = { puffs: 0, sparks: 0, glows: 0, chunks: 0, shocks: 0, sprites: 0 };
|
|
1305
|
-
function followCamera() {
|
|
1306
|
-
const position = options.camera.position;
|
|
1307
|
-
world.setCamera([position.x, position.y, position.z]);
|
|
1308
|
-
}
|
|
1309
|
-
function attachWorkshopLayers() {
|
|
1310
|
-
const engine = world.peekWorkshop();
|
|
1311
|
-
if (!engine || engine === workshopEngine) return;
|
|
1312
|
-
workshopLayers?.dispose();
|
|
1313
|
-
if (workshopLayers) root.remove(workshopLayers.group);
|
|
1314
|
-
workshopEngine = engine;
|
|
1315
|
-
workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
|
|
1316
|
-
root.add(workshopLayers.group);
|
|
1317
|
-
applyLayers();
|
|
1318
|
-
}
|
|
1319
|
-
function syncAll() {
|
|
1320
|
-
lastDrawn.puffs = puffs.sync();
|
|
1321
|
-
lastDrawn.sparks = sparks.sync();
|
|
1322
|
-
lastDrawn.glows = glows.sync();
|
|
1323
|
-
lastDrawn.chunks = chunks.sync();
|
|
1324
|
-
lastDrawn.shocks = shocks.sync();
|
|
1325
|
-
lastDrawn.sprites = sprites.sync();
|
|
1326
|
-
lights.sync();
|
|
1327
|
-
attachWorkshopLayers();
|
|
1328
|
-
const workshopCalls = workshopLayers?.sync() ?? 0;
|
|
1329
|
-
const glow = pools.lights.glow;
|
|
1330
|
-
uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
|
|
1331
|
-
uniforms.uGlowCol.value.set(glow.color[0], glow.color[1], glow.color[2]);
|
|
1332
|
-
uniforms.uGlowI.value = glow.intensity;
|
|
1333
|
-
drawCalls = Number(lastDrawn.puffs > 0) + Number(lastDrawn.sparks > 0) + Number(lastDrawn.glows > 0) + Number(chunks.rocks.count > 0) + Number(chunks.plates.count > 0) + Number(sprites.additive.geometry.drawRange.count > 0) + Number(sprites.normal.geometry.drawRange.count > 0) + lastDrawn.shocks * 2 + marks.count() + workshopCalls;
|
|
1334
|
-
}
|
|
1335
|
-
return {
|
|
1336
|
-
world,
|
|
1337
|
-
play(request) {
|
|
1338
|
-
followCamera();
|
|
1339
|
-
const explosion = world.explosion(request.effect, request.point, {
|
|
1340
|
-
...request.direction ? { direction: request.direction } : {},
|
|
1341
|
-
...request.seed !== void 0 ? { seed: request.seed } : {},
|
|
1342
|
-
...request.scale !== void 0 ? { scale: request.scale } : {}
|
|
1343
|
-
});
|
|
1344
|
-
if (explosion !== null) return explosion;
|
|
1345
|
-
return world.play(request.effect, request.point, {
|
|
1346
|
-
...request.direction ? { direction: request.direction } : {},
|
|
1347
|
-
...request.normal ? { normal: request.normal } : {},
|
|
1348
|
-
...request.orientation ? { orientation: request.orientation } : {},
|
|
1349
|
-
...request.seed !== void 0 ? { seed: request.seed } : {},
|
|
1350
|
-
...request.param !== void 0 ? { param: request.param } : {},
|
|
1351
|
-
...request.values ? { values: request.values } : {}
|
|
1352
|
-
});
|
|
1353
|
-
},
|
|
1354
|
-
stop: (handle) => world.stop(handle),
|
|
1355
|
-
// a module's closed space (a shut car's cabin): the world's smoke, dust and steam go round it
|
|
1356
|
-
enclose: (id, enclosure) => world.enclose(id, enclosure),
|
|
1357
|
-
configure(settings) {
|
|
1358
|
-
const band = settings.lod_band;
|
|
1359
|
-
if (typeof band === "string" && band in LOD_BANDS) world.forceLod(LOD_BANDS[band] ?? null);
|
|
1360
|
-
const quality = settings.quality;
|
|
1361
|
-
if (typeof quality === "string" && quality in QUALITY_LEVELS) world.workshop().setQuality(QUALITY_LEVELS[quality] ?? 0);
|
|
1362
|
-
let layersChanged = false;
|
|
1363
|
-
for (const [key, value] of Object.entries(settings)) {
|
|
1364
|
-
if (!key.startsWith(LAYER_PREFIX) || typeof value !== "boolean") continue;
|
|
1365
|
-
layerOn.set(key.slice(LAYER_PREFIX.length), value);
|
|
1366
|
-
layersChanged = true;
|
|
1367
|
-
}
|
|
1368
|
-
if (layersChanged) applyLayers();
|
|
1369
|
-
},
|
|
1370
|
-
catalog: () => [...world.catalog(), ...world.effectIds()],
|
|
1371
|
-
update(dt) {
|
|
1372
|
-
clock += dt;
|
|
1373
|
-
uniforms.uTime.value = clock;
|
|
1374
|
-
followCamera();
|
|
1375
|
-
world.update(dt);
|
|
1376
|
-
marks.update(dt);
|
|
1377
|
-
syncAll();
|
|
1378
|
-
overlay?.update(screenNow());
|
|
1379
|
-
},
|
|
1380
|
-
warmUpProbes() {
|
|
1381
|
-
releaseAnchors();
|
|
1382
|
-
marks.prepare();
|
|
1383
|
-
world.workshop();
|
|
1384
|
-
attachWorkshopLayers();
|
|
1385
|
-
const meshes = createMeshLayerProbes(THREE);
|
|
1386
|
-
for (const mesh of meshes) mesh.frustumCulled = false;
|
|
1387
|
-
const group = new THREE.Group();
|
|
1388
|
-
group.add(...meshes);
|
|
1389
|
-
anchors = { meshes, group };
|
|
1390
|
-
return { group, dispose: () => {
|
|
1391
|
-
} };
|
|
1392
|
-
},
|
|
1393
|
-
applyCamera() {
|
|
1394
|
-
const shake = screenNow().shake;
|
|
1395
|
-
if (reducedMotion || !(shake > SHAKE_FLOOR)) return () => {
|
|
1396
|
-
};
|
|
1397
|
-
const camera = options.camera;
|
|
1398
|
-
const base = camera.position.clone();
|
|
1399
|
-
const reach = shake * SHAKE_REACH;
|
|
1400
|
-
camera.position.x += Math.sin(clock * 71) * reach;
|
|
1401
|
-
camera.position.y += Math.sin(clock * 53 + 1) * reach * 0.7;
|
|
1402
|
-
camera.position.z += Math.sin(clock * 67 + 2) * reach;
|
|
1403
|
-
return () => camera.position.copy(base);
|
|
1404
|
-
},
|
|
1405
|
-
stats() {
|
|
1406
|
-
const { lastBlast, ...counters } = world.stats();
|
|
1407
|
-
return { ...counters, last_puffs: lastBlast.puffs, last_sparks: lastBlast.sparks, last_chunks: lastBlast.chunks, last_lights: lastBlast.lights, drawCalls };
|
|
1408
|
-
},
|
|
1409
|
-
screen: screenNow,
|
|
1410
|
-
dispose() {
|
|
1411
|
-
releaseAnchors();
|
|
1412
|
-
stopMarks();
|
|
1413
|
-
overlay?.dispose();
|
|
1414
|
-
workshopLayers?.dispose();
|
|
1415
|
-
for (const layer2 of [puffs, sparks, glows, chunks, shocks, lights, marks, sprites]) layer2.dispose();
|
|
1416
|
-
rockGeometry.dispose();
|
|
1417
|
-
plateGeometry.dispose();
|
|
1418
|
-
options.scene.remove(root);
|
|
1419
|
-
}
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// src/bench/module.ts
|
|
1424
1
|
import {
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
controls: { effect: "Efeito", variant: "Variante", replay: "Repetir", auto_repeat: "Repeti\xE7\xE3o autom\xE1tica", distance: "Dist\xE2ncia", lod_band: "Faixa de LOD", quality: "Qualidade" },
|
|
1447
|
-
lod: { auto: "Autom\xE1tico", near: "Perto", middle: "Meio", far: "Longe" },
|
|
1448
|
-
quality: { high: "Alta", medium: "M\xE9dia", low: "Baixa" },
|
|
1449
|
-
layers: { puffs: "Fuma\xE7a e fogo", sparks: "Fa\xEDscas", glows: "Brilhos", debris: "Destro\xE7os", shock: "Onda de choque", sprites: "Gotas e receitas" },
|
|
1450
|
-
metrics: { puffs: "Nuvens", sparks: "Fa\xEDscas", debris: "Destro\xE7os", lights: "Luzes", budget_particles: "Part\xEDculas do efeito", lod: "Escala de LOD", phase: "Tempo desde a repeti\xE7\xE3o", blast_time: "Tempo desde a explos\xE3o" },
|
|
1451
|
-
sheet: { explosion: "Explos\xE3o", lethal: "Raio letal", injury: "Raio de ferimento", radius: "Bola de fogo", fireballs: "Bolas de fogo", particles: "Part\xEDculas", scale: "Escala" },
|
|
1452
|
-
none: "Nenhuma"
|
|
1453
|
-
}
|
|
1454
|
-
};
|
|
1455
|
-
var benchText = (locale) => benchTexts[locale] ?? benchTexts.en;
|
|
1456
|
-
|
|
1457
|
-
// src/bench/module.ts
|
|
1458
|
-
var DEFAULT_EFFECT = "m67";
|
|
1459
|
-
var FALLBACK_EXPLOSION = "generic";
|
|
1460
|
-
var NO_VARIANT = "none";
|
|
1461
|
-
var BLAST_POINT = [0, 0.1, 0];
|
|
1462
|
-
var DEFAULT_DISTANCE = 14;
|
|
1463
|
-
var DEFAULT_REPEAT_SECONDS = 4;
|
|
1464
|
-
var LIGHT_BUDGET = EVENT_BUDGET.blastLights + EVENT_BUDGET.fireLights;
|
|
1465
|
-
var SEGMENT_COLORS = ["#ffb347", "#ff7a2a", "#8a8f98", "#5c6370"];
|
|
1466
|
-
var TEAMS = [
|
|
1467
|
-
"creatures",
|
|
1468
|
-
"weapons",
|
|
1469
|
-
"destruction",
|
|
1470
|
-
"world",
|
|
1471
|
-
"vehicles",
|
|
1472
|
-
"screen"
|
|
1473
|
-
];
|
|
1474
|
-
var LAYER_ID = "show_";
|
|
1475
|
-
var humanize = (id) => id.replace(/_/g, " ");
|
|
1476
|
-
var eyeFor = (distance) => [
|
|
1477
|
-
distance * 0.6,
|
|
1478
|
-
distance * 0.35 + 1.4,
|
|
1479
|
-
distance * 0.8
|
|
1480
|
-
];
|
|
1481
|
-
function timelineOf(explosion, recipe, metric) {
|
|
1482
|
-
if (explosion) {
|
|
1483
|
-
const flash = explosion.flash.life ?? 0.2;
|
|
1484
|
-
const smoke = Math.max(explosion.smoke.life, 1);
|
|
1485
|
-
return {
|
|
1486
|
-
segments: [
|
|
1487
|
-
{ name: "flash", start: 0, end: flash, color: SEGMENT_COLORS[0] },
|
|
1488
|
-
{
|
|
1489
|
-
name: "fireball",
|
|
1490
|
-
start: 0,
|
|
1491
|
-
end: Math.min(smoke, 2.4),
|
|
1492
|
-
color: SEGMENT_COLORS[1]
|
|
1493
|
-
},
|
|
1494
|
-
{ name: "smoke", start: 0.4, end: smoke, color: SEGMENT_COLORS[2] }
|
|
1495
|
-
],
|
|
1496
|
-
ticks: Array.from({ length: Math.ceil(smoke) }, (_, index) => index + 1),
|
|
1497
|
-
metric
|
|
1498
|
-
};
|
|
1499
|
-
}
|
|
1500
|
-
const span = recipe?.loop && recipe.loop > 0 ? recipe.loop : 4;
|
|
1501
|
-
return {
|
|
1502
|
-
segments: [
|
|
1503
|
-
{ name: recipe?.id ?? "", start: 0, end: span, color: SEGMENT_COLORS[1] }
|
|
1504
|
-
],
|
|
1505
|
-
ticks: Array.from({ length: Math.floor(span) }, (_, index) => index + 1),
|
|
1506
|
-
metric
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
1509
|
-
function createVfxTestModule(options = {}) {
|
|
1510
|
-
const locale = options.locale ?? "en";
|
|
1511
|
-
const recipes = options.effects ?? GAME_EFFECTS;
|
|
1512
|
-
const explosions = options.explosions ?? EXPLOSIONS;
|
|
1513
|
-
const text = benchText(locale);
|
|
1514
|
-
const strings = resolveStrings(locale);
|
|
1515
|
-
const effectOptions = () => [
|
|
1516
|
-
...Object.keys(explosions).filter((id) => id !== FALLBACK_EXPLOSION).map((id) => ({
|
|
1517
|
-
id,
|
|
1518
|
-
name: humanize(id),
|
|
1519
|
-
group: strings.groups.explosions ?? text.sheet.explosion
|
|
1520
|
-
})),
|
|
1521
|
-
// an id the explosions already use is the explosion's: the sink tries the explosions first, so the recipe of the same name could not be reached
|
|
1522
|
-
...TEAMS.flatMap(
|
|
1523
|
-
(team) => Object.values(recipes).filter((def) => def.team === team && !(def.id in explosions)).map((def) => ({
|
|
1524
|
-
id: def.id,
|
|
1525
|
-
name: strings.effects[def.id]?.name ?? humanize(def.id),
|
|
1526
|
-
group: strings.groups[def.team] ?? def.team
|
|
1527
|
-
}))
|
|
1528
|
-
)
|
|
1529
|
-
];
|
|
1530
|
-
const sheetFor = (id) => {
|
|
1531
|
-
const explosion = explosions[id];
|
|
1532
|
-
if (explosion) {
|
|
1533
|
-
return {
|
|
1534
|
-
kicker: strings.groups.explosions ?? text.sheet.explosion,
|
|
1535
|
-
title: humanize(id),
|
|
1536
|
-
facts: [
|
|
1537
|
-
{ label: text.sheet.lethal, value: `${explosion.lethal} m` },
|
|
1538
|
-
{ label: text.sheet.injury, value: `${explosion.injury} m` },
|
|
1539
|
-
{ label: text.sheet.radius, value: `${explosion.visualRadius} m` },
|
|
1540
|
-
{ label: text.sheet.fireballs, value: String(explosion.fireballs) },
|
|
1541
|
-
{
|
|
1542
|
-
label: text.sheet.particles,
|
|
1543
|
-
value: `${estimateCost(explosion)} / ${EVENT_BUDGET.particles}`
|
|
1544
|
-
}
|
|
1545
|
-
],
|
|
1546
|
-
timeline: timelineOf(explosion, void 0, "blast_time")
|
|
1547
|
-
};
|
|
1548
|
-
}
|
|
1549
|
-
const recipe = recipes[id];
|
|
1550
|
-
const words = strings.effects[id];
|
|
1551
|
-
const labels = strings.sheet;
|
|
1552
|
-
return {
|
|
1553
|
-
kicker: recipe ? strings.groups[recipe.team] ?? recipe.team : void 0,
|
|
1554
|
-
title: words?.name ?? humanize(id),
|
|
1555
|
-
...words ? { body: words.short } : {},
|
|
1556
|
-
facts: words ? [{ label: labels.budget, value: `${words.cost} \xB7 ${words.costNote}` }] : [],
|
|
1557
|
-
steps: words ? [
|
|
1558
|
-
{ at: labels.when, text: words.when },
|
|
1559
|
-
{ at: labels.look, text: words.look },
|
|
1560
|
-
{ at: labels.budget, text: words.budget },
|
|
1561
|
-
{ at: labels.network, text: words.network }
|
|
1562
|
-
] : [],
|
|
1563
|
-
timeline: timelineOf(void 0, recipe, "phase")
|
|
1564
|
-
};
|
|
1565
|
-
};
|
|
1566
|
-
const controls = () => [
|
|
1567
|
-
{
|
|
1568
|
-
kind: "choice",
|
|
1569
|
-
id: "effect",
|
|
1570
|
-
name: text.controls.effect,
|
|
1571
|
-
options: effectOptions(),
|
|
1572
|
-
default: DEFAULT_EFFECT in explosions ? DEFAULT_EFFECT : effectOptions()[0].id
|
|
1573
|
-
},
|
|
1574
|
-
{
|
|
1575
|
-
kind: "choice",
|
|
1576
|
-
id: "variant",
|
|
1577
|
-
name: text.controls.variant,
|
|
1578
|
-
options: [{ id: NO_VARIANT, name: text.none }],
|
|
1579
|
-
default: NO_VARIANT,
|
|
1580
|
-
hidden: true
|
|
1581
|
-
},
|
|
1582
|
-
{ kind: "action", id: "replay", name: text.controls.replay },
|
|
1583
|
-
{
|
|
1584
|
-
kind: "boolean",
|
|
1585
|
-
id: "auto_repeat",
|
|
1586
|
-
name: text.controls.auto_repeat,
|
|
1587
|
-
default: false
|
|
1588
|
-
},
|
|
1589
|
-
{
|
|
1590
|
-
kind: "number",
|
|
1591
|
-
id: "distance",
|
|
1592
|
-
name: text.controls.distance,
|
|
1593
|
-
min: 4,
|
|
1594
|
-
max: 120,
|
|
1595
|
-
step: 1,
|
|
1596
|
-
unit: "m",
|
|
1597
|
-
default: DEFAULT_DISTANCE
|
|
1598
|
-
},
|
|
1599
|
-
{
|
|
1600
|
-
kind: "choice",
|
|
1601
|
-
id: "lod_band",
|
|
1602
|
-
name: text.controls.lod_band,
|
|
1603
|
-
options: ["auto", "near", "middle", "far"].map((id) => ({
|
|
1604
|
-
id,
|
|
1605
|
-
name: text.lod[id]
|
|
1606
|
-
})),
|
|
1607
|
-
default: "auto"
|
|
1608
|
-
},
|
|
1609
|
-
{
|
|
1610
|
-
kind: "choice",
|
|
1611
|
-
id: "quality",
|
|
1612
|
-
name: text.controls.quality,
|
|
1613
|
-
options: ["high", "medium", "low"].map((id) => ({
|
|
1614
|
-
id,
|
|
1615
|
-
name: text.quality[id]
|
|
1616
|
-
})),
|
|
1617
|
-
default: "high"
|
|
1618
|
-
}
|
|
1619
|
-
];
|
|
1620
|
-
const layers = () => VFX_LAYERS.map((id) => ({ id: `${LAYER_ID}${id}`, name: text.layers[id] }));
|
|
1621
|
-
let read = () => ({ stats: {}, phase: 0, budget: 0 });
|
|
1622
|
-
const metrics = () => [
|
|
1623
|
-
{
|
|
1624
|
-
id: "puffs",
|
|
1625
|
-
name: text.metrics.puffs,
|
|
1626
|
-
digits: 0,
|
|
1627
|
-
read: () => read().stats.puffs ?? 0
|
|
1628
|
-
},
|
|
1629
|
-
{
|
|
1630
|
-
id: "sparks",
|
|
1631
|
-
name: text.metrics.sparks,
|
|
1632
|
-
digits: 0,
|
|
1633
|
-
read: () => read().stats.sparks ?? 0
|
|
1634
|
-
},
|
|
1635
|
-
{
|
|
1636
|
-
id: "debris",
|
|
1637
|
-
name: text.metrics.debris,
|
|
1638
|
-
digits: 0,
|
|
1639
|
-
max: EVENT_BUDGET.debris,
|
|
1640
|
-
read: () => read().stats.chunks ?? 0
|
|
1641
|
-
},
|
|
1642
|
-
{
|
|
1643
|
-
id: "lights",
|
|
1644
|
-
name: text.metrics.lights,
|
|
1645
|
-
digits: 0,
|
|
1646
|
-
max: LIGHT_BUDGET,
|
|
1647
|
-
read: () => (read().stats.blastLights ?? 0) + (read().stats.fireLights ?? 0)
|
|
1648
|
-
},
|
|
1649
|
-
{
|
|
1650
|
-
id: "budget_particles",
|
|
1651
|
-
name: text.metrics.budget_particles,
|
|
1652
|
-
digits: 0,
|
|
1653
|
-
max: EVENT_BUDGET.particles,
|
|
1654
|
-
read: () => read().budget
|
|
1655
|
-
},
|
|
1656
|
-
{
|
|
1657
|
-
id: "lod",
|
|
1658
|
-
name: text.metrics.lod,
|
|
1659
|
-
digits: 2,
|
|
1660
|
-
read: () => read().stats.lod ?? 1
|
|
1661
|
-
},
|
|
1662
|
-
{
|
|
1663
|
-
id: "phase",
|
|
1664
|
-
name: text.metrics.phase,
|
|
1665
|
-
unit: "s",
|
|
1666
|
-
digits: 2,
|
|
1667
|
-
read: () => read().phase
|
|
1668
|
-
},
|
|
1669
|
-
{
|
|
1670
|
-
id: "blast_time",
|
|
1671
|
-
name: text.metrics.blast_time,
|
|
1672
|
-
unit: "s",
|
|
1673
|
-
digits: 2,
|
|
1674
|
-
read: () => Math.max(0, read().stats.blastTime ?? 0)
|
|
1675
|
-
}
|
|
1676
|
-
];
|
|
1677
|
-
return {
|
|
1678
|
-
id: "vfx",
|
|
1679
|
-
name: text.module,
|
|
1680
|
-
ground: { surface: "dirt" },
|
|
1681
|
-
group: "effects",
|
|
1682
|
-
items: () => [],
|
|
1683
|
-
controls,
|
|
1684
|
-
layers,
|
|
1685
|
-
metrics,
|
|
1686
|
-
// opening the effects area puts the camera at the bench view (the initial distance does not move it: that would take the player's view on load)
|
|
1687
|
-
area: () => ({
|
|
1688
|
-
placements: [],
|
|
1689
|
-
view: { eye: eyeFor(DEFAULT_DISTANCE), target: [0, 1.2, 0] }
|
|
1690
|
-
}),
|
|
1691
|
-
attach(host) {
|
|
1692
|
-
const bench = host.bench;
|
|
1693
|
-
let own;
|
|
1694
|
-
let sink = host.effects;
|
|
1695
|
-
if (!sink) {
|
|
1696
|
-
own = createVfxRenderer(host.THREE, {
|
|
1697
|
-
scene: host.scene,
|
|
1698
|
-
camera: host.camera
|
|
1699
|
-
});
|
|
1700
|
-
sink = own;
|
|
1701
|
-
}
|
|
1702
|
-
const effects = sink;
|
|
1703
|
-
let effect = "m67";
|
|
1704
|
-
let variant = NO_VARIANT;
|
|
1705
|
-
let distance = DEFAULT_DISTANCE;
|
|
1706
|
-
let auto = false;
|
|
1707
|
-
let phase = 0;
|
|
1708
|
-
let handle = null;
|
|
1709
|
-
let sinceRepeat = 0;
|
|
1710
|
-
let live = false;
|
|
1711
|
-
read = () => {
|
|
1712
|
-
const def = explosions[effect];
|
|
1713
|
-
const recipe = recipes[effect];
|
|
1714
|
-
const budget = def ? estimateCost(def) : recipe?.budget.particles ?? 0;
|
|
1715
|
-
return { stats: effects.stats?.() ?? {}, phase, budget };
|
|
1716
|
-
};
|
|
1717
|
-
function replay() {
|
|
1718
|
-
if (handle !== null) effects.stop(handle);
|
|
1719
|
-
phase = 0;
|
|
1720
|
-
sinceRepeat = 0;
|
|
1721
|
-
handle = effects.play({
|
|
1722
|
-
effect,
|
|
1723
|
-
point: [BLAST_POINT[0], BLAST_POINT[1], BLAST_POINT[2]],
|
|
1724
|
-
direction: [1, 0.2, 0.4],
|
|
1725
|
-
normal: [0, 1, 0],
|
|
1726
|
-
...variant !== NO_VARIANT && recipes[effect]?.params?.options.includes(variant) ? { param: variant } : {}
|
|
1727
|
-
});
|
|
1728
|
-
}
|
|
1729
|
-
function choose(id) {
|
|
1730
|
-
effect = id;
|
|
1731
|
-
const params = recipes[id]?.params;
|
|
1732
|
-
variant = params ? params.options[params.default] ?? params.options[0] ?? NO_VARIANT : NO_VARIANT;
|
|
1733
|
-
bench?.patch(
|
|
1734
|
-
"variant",
|
|
1735
|
-
params ? {
|
|
1736
|
-
hidden: false,
|
|
1737
|
-
options: params.options.map((option) => ({
|
|
1738
|
-
id: option,
|
|
1739
|
-
name: option
|
|
1740
|
-
})),
|
|
1741
|
-
value: variant
|
|
1742
|
-
} : {
|
|
1743
|
-
hidden: true,
|
|
1744
|
-
options: [{ id: NO_VARIANT, name: text.none }],
|
|
1745
|
-
value: NO_VARIANT
|
|
1746
|
-
}
|
|
1747
|
-
);
|
|
1748
|
-
bench?.sheet?.(sheetFor(id));
|
|
1749
|
-
if (live) replay();
|
|
1750
|
-
}
|
|
1751
|
-
function look() {
|
|
1752
|
-
const eye = eyeFor(distance);
|
|
1753
|
-
bench?.look(eye, [0, 1.2, 0]);
|
|
1754
|
-
}
|
|
1755
|
-
const apply = (change) => {
|
|
1756
|
-
if (change.kind === "layer")
|
|
1757
|
-
effects.configure?.({
|
|
1758
|
-
[`layer_${change.id.slice(LAYER_ID.length)}`]: change.on
|
|
1759
|
-
});
|
|
1760
|
-
else if (change.kind === "action") {
|
|
1761
|
-
if (change.id === "replay") replay();
|
|
1762
|
-
} else if (change.id === "effect" && typeof change.value === "string")
|
|
1763
|
-
choose(change.value);
|
|
1764
|
-
else if (change.id === "variant" && typeof change.value === "string") {
|
|
1765
|
-
variant = change.value;
|
|
1766
|
-
if (live && recipes[effect]?.params) replay();
|
|
1767
|
-
} else if (change.id === "auto_repeat") auto = change.value === true;
|
|
1768
|
-
else if (change.id === "distance" && typeof change.value === "number") {
|
|
1769
|
-
distance = change.value;
|
|
1770
|
-
look();
|
|
1771
|
-
} else if (change.id === "lod_band" || change.id === "quality")
|
|
1772
|
-
effects.configure?.({ [change.id]: change.value });
|
|
1773
|
-
};
|
|
1774
|
-
const offChange = bench?.onChange(apply);
|
|
1775
|
-
const offFrame = host.onFrame((dt) => {
|
|
1776
|
-
live = true;
|
|
1777
|
-
phase += dt;
|
|
1778
|
-
sinceRepeat += dt;
|
|
1779
|
-
own?.update(dt);
|
|
1780
|
-
if (auto && sinceRepeat >= (options.repeatSeconds ?? DEFAULT_REPEAT_SECONDS))
|
|
1781
|
-
replay();
|
|
1782
|
-
});
|
|
1783
|
-
return () => {
|
|
1784
|
-
offChange?.();
|
|
1785
|
-
offFrame();
|
|
1786
|
-
if (handle !== null) effects.stop(handle);
|
|
1787
|
-
bench?.sheet?.(null);
|
|
1788
|
-
own?.dispose();
|
|
1789
|
-
read = () => ({ stats: {}, phase: 0, budget: 0 });
|
|
1790
|
-
};
|
|
1791
|
-
}
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
2
|
+
GLOW_FRAGMENT,
|
|
3
|
+
GLOW_VERTEX,
|
|
4
|
+
PUFF_FRAGMENT,
|
|
5
|
+
PUFF_VERTEX,
|
|
6
|
+
RING_FRAGMENT,
|
|
7
|
+
RING_VERTEX,
|
|
8
|
+
SHELL_FRAGMENT,
|
|
9
|
+
SHELL_VERTEX,
|
|
10
|
+
SPARK_FRAGMENT,
|
|
11
|
+
SPARK_VERTEX,
|
|
12
|
+
SPRITE_FRAGMENT,
|
|
13
|
+
SPRITE_VERTEX,
|
|
14
|
+
VFX_LAYERS,
|
|
15
|
+
benchText,
|
|
16
|
+
benchTexts,
|
|
17
|
+
createMarkLayer,
|
|
18
|
+
createScreenOverlay,
|
|
19
|
+
createSpriteLayer,
|
|
20
|
+
createVfxRenderer,
|
|
21
|
+
createVfxTestModule
|
|
22
|
+
} from "./chunk-ALDCQ5RH.js";
|
|
1794
23
|
|
|
1795
24
|
// src/ground-patch.ts
|
|
1796
25
|
var DEFAULT_TEXTURE_METRES = 17;
|