lecodes-gl-lite 2.0.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/README.md +27 -0
- package/package.json +33 -0
- package/src/assets/decoders/README.md +13 -0
- package/src/assets/decoders/basis_transcoder.js +19 -0
- package/src/assets/decoders/basis_transcoder.wasm +0 -0
- package/src/assets/decoders/draco_decoder.wasm +0 -0
- package/src/assets/decoders/draco_wasm_wrapper.js +117 -0
- package/src/assets/neutral_ibl128.ktx +0 -0
- package/src/core/animBridge.ts +51 -0
- package/src/core/boneNames.ts +22 -0
- package/src/core/frame.ts +230 -0
- package/src/core/glbStructure.ts +100 -0
- package/src/core/headlessBridge.ts +92 -0
- package/src/core/mapView.ts +195 -0
- package/src/core/math3d.ts +275 -0
- package/src/core/particleBridge.ts +154 -0
- package/src/core/physicsBridge.ts +305 -0
- package/src/core/recording.ts +59 -0
- package/src/core/sceneCore.ts +833 -0
- package/src/core/serialize.ts +144 -0
- package/src/gen/unsupported.gen.ts +212 -0
- package/src/index.ts +5 -0
- package/src/vendor/creator-anim/dist/creatorAnim.d.ts +123 -0
- package/src/vendor/creator-anim/dist/creatorAnim.js +14 -0
- package/src/vendor/creator-anim/js/index.ts +656 -0
- package/src/vendor/creator-nav/dist/creatorNav.d.ts +56 -0
- package/src/vendor/creator-nav/dist/creatorNav.js +14 -0
- package/src/vendor/creator-nav/js/index.ts +400 -0
- package/src/vendor/creator-particles/dist/creatorParticles.d.ts +43 -0
- package/src/vendor/creator-particles/dist/creatorParticles.js +14 -0
- package/src/vendor/creator-particles/js/index.ts +137 -0
- package/src/vendor/creator-physics/dist/creatorPhysics.d.ts +107 -0
- package/src/vendor/creator-physics/dist/creatorPhysics.js +14 -0
- package/src/vendor/creator-physics/js/index.ts +515 -0
- package/src/vite-env.d.ts +1 -0
- package/src/webgl/batching.ts +453 -0
- package/src/webgl/createViewerLite.ts +1627 -0
- package/src/webgl/customMaterial.ts +228 -0
- package/src/webgl/decoders/basis.ts +123 -0
- package/src/webgl/decoders/draco.ts +110 -0
- package/src/webgl/decoders/index.ts +68 -0
- package/src/webgl/decoders/worker.ts +45 -0
- package/src/webgl/editor.ts +1297 -0
- package/src/webgl/glb.ts +593 -0
- package/src/webgl/ibl.ts +137 -0
- package/src/webgl/shaders.ts +779 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
// GLSL 300 es shaders for the lite 3D engine. The shading model follows Filament's published BRDF
|
|
2
|
+
// (GGX + Smith height-correlated visibility + Schlick fresnel, Lambert diffuse) with Filament's
|
|
3
|
+
// photometric units: light intensities arrive in lux (sun) / nits-ish (IBL) and are pre-exposed by
|
|
4
|
+
// the same default-camera exposure Filament uses (f/16, 1/125s, ISO 100), then tonemapped with an
|
|
5
|
+
// ACES fit and sRGB-encoded. That keeps the SDK's real-world intensity values (sun 100 000, IBL
|
|
6
|
+
// 20 000) producing the same overall brightness as the Filament build.
|
|
7
|
+
|
|
8
|
+
// Filament default camera: EV100 = log2(N^2 / t * 100/S) with N=16, t=1/125, S=100 → ~14.965.
|
|
9
|
+
// exposure = 1 / (1.2 * 2^EV100).
|
|
10
|
+
export const EXPOSURE = 1 / (1.2 * Math.pow(2, Math.log2((16 * 16) / (1 / 125))))
|
|
11
|
+
|
|
12
|
+
// ---- shared chunks ----------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const COMMON_VERT_HEADER = /* glsl */ `#version 300 es
|
|
15
|
+
precision highp float;
|
|
16
|
+
in vec3 position;
|
|
17
|
+
in vec3 normal;
|
|
18
|
+
in vec2 uv;
|
|
19
|
+
uniform mat4 modelMatrix;
|
|
20
|
+
uniform mat4 modelViewMatrix;
|
|
21
|
+
uniform mat4 projectionMatrix;
|
|
22
|
+
uniform mat3 normalMatrix;
|
|
23
|
+
out vec3 vWorldPos;
|
|
24
|
+
out vec3 vNormal;
|
|
25
|
+
out vec2 vUv;
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
// Skinning chunk (same bone-texture layout as OGL's GLTFSkin).
|
|
29
|
+
const SKIN_CHUNK = /* glsl */ `
|
|
30
|
+
in vec4 skinIndex;
|
|
31
|
+
in vec4 skinWeight;
|
|
32
|
+
uniform sampler2D boneTexture;
|
|
33
|
+
uniform int boneTextureSize;
|
|
34
|
+
|
|
35
|
+
mat4 getBoneMatrix(const in float i) {
|
|
36
|
+
float j = i * 4.0;
|
|
37
|
+
float x = mod(j, float(boneTextureSize));
|
|
38
|
+
float y = floor(j / float(boneTextureSize));
|
|
39
|
+
float dx = 1.0 / float(boneTextureSize);
|
|
40
|
+
float dy = 1.0 / float(boneTextureSize);
|
|
41
|
+
y = dy * (y + 0.5);
|
|
42
|
+
vec4 v1 = texture(boneTexture, vec2(dx * (x + 0.5), y));
|
|
43
|
+
vec4 v2 = texture(boneTexture, vec2(dx * (x + 1.5), y));
|
|
44
|
+
vec4 v3 = texture(boneTexture, vec2(dx * (x + 2.5), y));
|
|
45
|
+
vec4 v4 = texture(boneTexture, vec2(dx * (x + 3.5), y));
|
|
46
|
+
return mat4(v1, v2, v3, v4);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
mat4 getSkinMatrix() {
|
|
50
|
+
mat4 boneMatX = getBoneMatrix(skinIndex.x);
|
|
51
|
+
mat4 boneMatY = getBoneMatrix(skinIndex.y);
|
|
52
|
+
mat4 boneMatZ = getBoneMatrix(skinIndex.z);
|
|
53
|
+
mat4 boneMatW = getBoneMatrix(skinIndex.w);
|
|
54
|
+
return boneMatX * skinWeight.x + boneMatY * skinWeight.y + boneMatZ * skinWeight.z + boneMatW * skinWeight.w;
|
|
55
|
+
}
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
// Morph-target (blendshape) chunk — the three.js r133+ approach: every target's deltas live in one
|
|
59
|
+
// RGBA32F data texture fetched by gl_VertexID, so there is no vertex-attribute limit. Layout is
|
|
60
|
+
// target-major: texel = targetIndex * vertexCount + vertexId; normal-delta blocks (when present)
|
|
61
|
+
// follow all position blocks at offset count * vertexCount.
|
|
62
|
+
const MORPH_CHUNK = /* glsl */ `
|
|
63
|
+
uniform sampler2D uMorphTex;
|
|
64
|
+
uniform int uMorphCount; // active target count (capped by glb.ts)
|
|
65
|
+
uniform int uMorphVertexCount;
|
|
66
|
+
uniform int uMorphTexWidth;
|
|
67
|
+
uniform float uMorphWeights[8];
|
|
68
|
+
|
|
69
|
+
vec3 morphFetch(int idx) {
|
|
70
|
+
return texelFetch(uMorphTex, ivec2(idx % uMorphTexWidth, idx / uMorphTexWidth), 0).rgb;
|
|
71
|
+
}
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
// INSTANCED (batching.ts defines it on a program's twin): the world matrix is the per-instance
|
|
75
|
+
// `instanceMatrix` attribute and the view comes in separately — modelMatrix / modelViewMatrix /
|
|
76
|
+
// normalMatrix go inactive. Same math either way, so a batch and a single can't shade differently.
|
|
77
|
+
const vertex = (defines: string): string => /* glsl */ `${COMMON_VERT_HEADER}
|
|
78
|
+
#ifdef INSTANCED
|
|
79
|
+
in mat4 instanceMatrix;
|
|
80
|
+
uniform mat4 viewMatrix;
|
|
81
|
+
#endif
|
|
82
|
+
${defines.includes('HAS_SKIN') ? SKIN_CHUNK : ''}
|
|
83
|
+
${defines.includes('HAS_MORPH') ? MORPH_CHUNK : ''}
|
|
84
|
+
${defines.includes('HAS_COLOR') ? 'in vec4 color;\nout vec4 vColor;' : ''}
|
|
85
|
+
uniform mat4 shadowMatrix;
|
|
86
|
+
out vec4 vShadowCoord;
|
|
87
|
+
|
|
88
|
+
void main() {
|
|
89
|
+
vec4 pos = vec4(position, 1.0);
|
|
90
|
+
vec3 nrm = normal;
|
|
91
|
+
${defines.includes('HAS_MORPH') ? /* glsl — morph BEFORE skinning, per glTF spec */ `
|
|
92
|
+
for (int t = 0; t < uMorphCount; t++) {
|
|
93
|
+
if (uMorphWeights[t] != 0.0) {
|
|
94
|
+
pos.xyz += uMorphWeights[t] * morphFetch(t * uMorphVertexCount + gl_VertexID);
|
|
95
|
+
${defines.includes('HAS_MORPH_NORMALS') ? 'nrm += uMorphWeights[t] * morphFetch((uMorphCount + t) * uMorphVertexCount + gl_VertexID);' : ''}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
` : ''}
|
|
99
|
+
${defines.includes('HAS_SKIN') ? `
|
|
100
|
+
mat4 skinMatrix = getSkinMatrix();
|
|
101
|
+
pos = skinMatrix * pos;
|
|
102
|
+
nrm = mat3(skinMatrix) * nrm;
|
|
103
|
+
` : ''}
|
|
104
|
+
${defines.includes('HAS_COLOR') ? 'vColor = color;' : ''}
|
|
105
|
+
#ifdef INSTANCED
|
|
106
|
+
mat4 model = instanceMatrix;
|
|
107
|
+
#else
|
|
108
|
+
mat4 model = modelMatrix;
|
|
109
|
+
#endif
|
|
110
|
+
vec4 worldPos = model * pos;
|
|
111
|
+
vWorldPos = worldPos.xyz;
|
|
112
|
+
vNormal = normalize(mat3(model) * nrm);
|
|
113
|
+
vUv = uv;
|
|
114
|
+
vShadowCoord = shadowMatrix * worldPos;
|
|
115
|
+
#ifdef INSTANCED
|
|
116
|
+
gl_Position = projectionMatrix * viewMatrix * worldPos;
|
|
117
|
+
#else
|
|
118
|
+
gl_Position = projectionMatrix * modelViewMatrix * pos;
|
|
119
|
+
#endif
|
|
120
|
+
}
|
|
121
|
+
`
|
|
122
|
+
|
|
123
|
+
// Fragment lighting shared by lit + glTF PBR + custom lit materials (customMaterial.ts).
|
|
124
|
+
// Requires uniforms:
|
|
125
|
+
// sunDirection (unit, TO the scene), sunColorIntensity (linear rgb * lux), iblLuminance (intensity),
|
|
126
|
+
// iblSH[9], envMap (prefiltered cube, 4 mips), shadowMap, shadowEnabled, exposure, cameraPosition.
|
|
127
|
+
export const LIGHTING_CHUNK = /* glsl */ `
|
|
128
|
+
uniform vec3 sunDirection;
|
|
129
|
+
uniform vec4 sunColorIntensity; // rgb = linear color, a = intensity (lux)
|
|
130
|
+
uniform float iblLuminance;
|
|
131
|
+
uniform vec3 iblSH[9];
|
|
132
|
+
uniform samplerCube envMap;
|
|
133
|
+
uniform float envMapMaxLod;
|
|
134
|
+
uniform sampler2D shadowMap;
|
|
135
|
+
uniform float shadowEnabled;
|
|
136
|
+
uniform float shadowMapSize;
|
|
137
|
+
uniform float exposure;
|
|
138
|
+
in vec4 vShadowCoord;
|
|
139
|
+
|
|
140
|
+
#define PI 3.14159265359
|
|
141
|
+
|
|
142
|
+
float D_GGX(float NoH, float a) {
|
|
143
|
+
float a2 = a * a;
|
|
144
|
+
float f = (NoH * a2 - NoH) * NoH + 1.0;
|
|
145
|
+
return a2 / (PI * f * f);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
float V_SmithGGXCorrelated(float NoV, float NoL, float a) {
|
|
149
|
+
float a2 = a * a;
|
|
150
|
+
float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
|
|
151
|
+
float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
|
|
152
|
+
return 0.5 / (GGXV + GGXL + 1e-5);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
vec3 F_Schlick(float u, vec3 f0) {
|
|
156
|
+
float f = pow(1.0 - u, 5.0);
|
|
157
|
+
return f0 + (vec3(1.0) - f0) * f;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
vec3 evalSH(vec3 n) {
|
|
161
|
+
return max(
|
|
162
|
+
iblSH[0]
|
|
163
|
+
+ iblSH[1] * n.y
|
|
164
|
+
+ iblSH[2] * n.z
|
|
165
|
+
+ iblSH[3] * n.x
|
|
166
|
+
+ iblSH[4] * (n.y * n.x)
|
|
167
|
+
+ iblSH[5] * (n.y * n.z)
|
|
168
|
+
+ iblSH[6] * (3.0 * n.z * n.z - 1.0)
|
|
169
|
+
+ iblSH[7] * (n.z * n.x)
|
|
170
|
+
+ iblSH[8] * (n.x * n.x - n.y * n.y)
|
|
171
|
+
, vec3(0.0));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
float sampleShadowPCF() {
|
|
175
|
+
if (shadowEnabled < 0.5) return 1.0;
|
|
176
|
+
vec3 coord = vShadowCoord.xyz / vShadowCoord.w;
|
|
177
|
+
coord = coord * 0.5 + 0.5;
|
|
178
|
+
if (coord.x < 0.0 || coord.x > 1.0 || coord.y < 0.0 || coord.y > 1.0 || coord.z > 1.0) return 1.0;
|
|
179
|
+
float bias = 0.0018;
|
|
180
|
+
float texel = 1.0 / shadowMapSize;
|
|
181
|
+
float sum = 0.0;
|
|
182
|
+
for (int x = -1; x <= 1; x++) {
|
|
183
|
+
for (int y = -1; y <= 1; y++) {
|
|
184
|
+
float depth = texture(shadowMap, coord.xy + vec2(float(x), float(y)) * texel).r;
|
|
185
|
+
sum += (coord.z - bias) > depth ? 0.0 : 1.0;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return sum / 9.0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// diffuseColor: linear albedo (already metallic-adjusted); f0: specular reflectance at normal.
|
|
192
|
+
vec3 shadePBR(vec3 n, vec3 v, vec3 diffuseColor, vec3 f0, float perceptualRoughness, float occlusion) {
|
|
193
|
+
float roughness = clamp(perceptualRoughness, 0.045, 1.0);
|
|
194
|
+
float a = roughness * roughness;
|
|
195
|
+
float NoV = abs(dot(n, v)) + 1e-5;
|
|
196
|
+
|
|
197
|
+
vec3 color = vec3(0.0);
|
|
198
|
+
|
|
199
|
+
// --- sun ---
|
|
200
|
+
if (sunColorIntensity.a > 0.0) {
|
|
201
|
+
vec3 l = normalize(-sunDirection);
|
|
202
|
+
float NoL = clamp(dot(n, l), 0.0, 1.0);
|
|
203
|
+
if (NoL > 0.0) {
|
|
204
|
+
vec3 h = normalize(v + l);
|
|
205
|
+
float NoH = clamp(dot(n, h), 0.0, 1.0);
|
|
206
|
+
float LoH = clamp(dot(l, h), 0.0, 1.0);
|
|
207
|
+
float D = D_GGX(NoH, a);
|
|
208
|
+
float V = V_SmithGGXCorrelated(NoV, NoL, a);
|
|
209
|
+
vec3 F = F_Schlick(LoH, f0);
|
|
210
|
+
vec3 Fr = D * V * F;
|
|
211
|
+
vec3 Fd = diffuseColor * (1.0 / PI);
|
|
212
|
+
float shadow = sampleShadowPCF();
|
|
213
|
+
// "illuminance" model: lux * NoL, pre-exposed
|
|
214
|
+
color += (Fd + Fr) * sunColorIntensity.rgb * (sunColorIntensity.a * exposure * NoL * shadow);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- IBL ---
|
|
219
|
+
if (iblLuminance > 0.0) {
|
|
220
|
+
// cmgen's SH coefficients are pre-convolved irradiance — no extra 1/PI (matches Filament).
|
|
221
|
+
vec3 irradiance = evalSH(n);
|
|
222
|
+
vec3 Fd = diffuseColor * irradiance;
|
|
223
|
+
|
|
224
|
+
vec3 r = reflect(-v, n);
|
|
225
|
+
float lod = roughness * envMapMaxLod;
|
|
226
|
+
vec3 prefiltered = textureLod(envMap, r, lod).rgb;
|
|
227
|
+
// Simple DFG approximation (Karis): avoids a DFG LUT.
|
|
228
|
+
vec3 specular = prefiltered * (f0 * (1.0 - roughness) + roughness * 0.25 * f0 + 0.05 * (1.0 - roughness));
|
|
229
|
+
color += (Fd + specular) * (iblLuminance * exposure * occlusion);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return color;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ACES filmic fit (Narkowicz) + sRGB OETF — stands in for Filament's default ACES color grading.
|
|
236
|
+
vec3 tonemap(vec3 x) {
|
|
237
|
+
const float A = 2.51; const float B = 0.03; const float C = 2.43; const float D = 0.59; const float E = 0.14;
|
|
238
|
+
vec3 t = clamp((x * (A * x + B)) / (x * (C * x + D) + E), 0.0, 1.0);
|
|
239
|
+
return pow(t, vec3(1.0 / 2.2));
|
|
240
|
+
}
|
|
241
|
+
`
|
|
242
|
+
|
|
243
|
+
// ---- programs ---------------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
export const litFragment = /* glsl */ `#version 300 es
|
|
246
|
+
precision highp float;
|
|
247
|
+
in vec3 vWorldPos;
|
|
248
|
+
in vec3 vNormal;
|
|
249
|
+
in vec2 vUv;
|
|
250
|
+
uniform vec3 cameraPosition;
|
|
251
|
+
uniform vec3 baseColor; // linear factor over baseColorMap (both default white)
|
|
252
|
+
uniform sampler2D baseColorMap;
|
|
253
|
+
uniform float roughness;
|
|
254
|
+
uniform float metallic;
|
|
255
|
+
${LIGHTING_CHUNK}
|
|
256
|
+
out vec4 fragColor;
|
|
257
|
+
|
|
258
|
+
void main() {
|
|
259
|
+
vec3 albedo = baseColor * texture(baseColorMap, vUv).rgb;
|
|
260
|
+
vec3 n = normalize(vNormal);
|
|
261
|
+
vec3 v = normalize(cameraPosition - vWorldPos);
|
|
262
|
+
if (dot(n, v) < 0.0) n = -n;
|
|
263
|
+
vec3 diffuseColor = albedo * (1.0 - metallic);
|
|
264
|
+
vec3 f0 = mix(vec3(0.04), albedo, metallic);
|
|
265
|
+
vec3 color = shadePBR(n, v, diffuseColor, f0, roughness, 1.0);
|
|
266
|
+
fragColor = vec4(tonemap(color), 1.0);
|
|
267
|
+
}
|
|
268
|
+
`
|
|
269
|
+
|
|
270
|
+
// terrain.mat (docs/terrain-plan.md §1.5): four albedo layers + two packed normal-map textures
|
|
271
|
+
// blended by a control map on the terrain's own grid; UV = the chunk's local XZ in metres (the SDK's
|
|
272
|
+
// terrainMesh.ts writes it), so `tiling` is metres per repeat and the control map is addressed as
|
|
273
|
+
// (uv / cellSize + 0.5) / gridSize. The tangent frame comes from screen-space derivatives (the lite
|
|
274
|
+
// chunk geometry carries no tangents). No lightmap block here — lite has no bake.
|
|
275
|
+
export const terrainFragment = /* glsl */ `#version 300 es
|
|
276
|
+
precision highp float;
|
|
277
|
+
in vec3 vWorldPos;
|
|
278
|
+
in vec3 vNormal;
|
|
279
|
+
in vec2 vUv;
|
|
280
|
+
uniform vec3 cameraPosition;
|
|
281
|
+
uniform sampler2D control;
|
|
282
|
+
uniform sampler2D albedo0;
|
|
283
|
+
uniform sampler2D albedo1;
|
|
284
|
+
uniform sampler2D albedo2;
|
|
285
|
+
uniform sampler2D albedo3;
|
|
286
|
+
uniform sampler2D normals01;
|
|
287
|
+
uniform sampler2D normals23;
|
|
288
|
+
uniform vec4 tiling;
|
|
289
|
+
uniform vec4 roughness;
|
|
290
|
+
uniform vec4 normalScale;
|
|
291
|
+
uniform vec4 triplanar;
|
|
292
|
+
uniform vec2 gridSize;
|
|
293
|
+
uniform float cellSize;
|
|
294
|
+
uniform vec3 tint;
|
|
295
|
+
${LIGHTING_CHUNK}
|
|
296
|
+
out vec4 fragColor;
|
|
297
|
+
|
|
298
|
+
vec4 triplanarSample(sampler2D tex, vec3 p, vec3 bw, float s) {
|
|
299
|
+
return texture(tex, p.zy / s) * bw.x + texture(tex, p.xz / s) * bw.y + texture(tex, p.xy / s) * bw.z;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
void splatLayer(sampler2D alb, sampler2D pack, float slot, float tl, float nscale, float tri, float w,
|
|
303
|
+
vec2 uv, vec3 wp, vec3 bw, inout vec3 albedo, inout vec3 nrm) {
|
|
304
|
+
if (w <= 0.0) return;
|
|
305
|
+
float s = max(tl, 1e-3);
|
|
306
|
+
if (tri > 0.5) {
|
|
307
|
+
albedo += triplanarSample(alb, wp, bw, s).rgb * w;
|
|
308
|
+
nrm += vec3(0.0, 0.0, 1.0) * w;
|
|
309
|
+
} else {
|
|
310
|
+
vec2 luv = uv / s;
|
|
311
|
+
albedo += texture(alb, luv).rgb * w;
|
|
312
|
+
if (nscale > 0.0) {
|
|
313
|
+
vec4 p = texture(pack, luv);
|
|
314
|
+
vec2 xy = (slot > 0.5 ? p.ba : p.rg) * 2.0 - 1.0;
|
|
315
|
+
xy *= nscale;
|
|
316
|
+
nrm += vec3(xy, sqrt(max(1.0 - dot(xy, xy), 0.0))) * w;
|
|
317
|
+
} else {
|
|
318
|
+
nrm += vec3(0.0, 0.0, 1.0) * w;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
vec3 applyTangentNormal(vec3 nTangent, vec3 ng) {
|
|
324
|
+
vec3 pos_dx = dFdx(vWorldPos);
|
|
325
|
+
vec3 pos_dy = dFdy(vWorldPos);
|
|
326
|
+
vec2 tex_dx = dFdx(vUv);
|
|
327
|
+
vec2 tex_dy = dFdy(vUv);
|
|
328
|
+
float det = tex_dx.x * tex_dy.y - tex_dy.x * tex_dx.y;
|
|
329
|
+
if (abs(det) < 1e-8) return ng;
|
|
330
|
+
vec3 t = normalize(pos_dx * tex_dy.y - pos_dy * tex_dx.y);
|
|
331
|
+
t = normalize(t - ng * dot(ng, t));
|
|
332
|
+
vec3 b = normalize(cross(ng, t)) * sign(det);
|
|
333
|
+
return normalize(mat3(t, b, ng) * nTangent);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
void main() {
|
|
337
|
+
vec2 cuv = (vUv / max(cellSize, 1e-4) + 0.5) / max(gridSize, vec2(1.0));
|
|
338
|
+
vec4 w = texture(control, cuv);
|
|
339
|
+
float ws = w.r + w.g + w.b + w.a;
|
|
340
|
+
w = ws > 1e-4 ? w / ws : vec4(1.0, 0.0, 0.0, 0.0);
|
|
341
|
+
vec3 ng = normalize(vNormal);
|
|
342
|
+
vec3 bw = abs(ng);
|
|
343
|
+
bw = bw / max(bw.x + bw.y + bw.z, 1e-4);
|
|
344
|
+
vec3 albedo = vec3(0.0);
|
|
345
|
+
vec3 nrm = vec3(0.0);
|
|
346
|
+
splatLayer(albedo0, normals01, 0.0, tiling.x, normalScale.x, triplanar.x, w.r, vUv, vWorldPos, bw, albedo, nrm);
|
|
347
|
+
splatLayer(albedo1, normals01, 1.0, tiling.y, normalScale.y, triplanar.y, w.g, vUv, vWorldPos, bw, albedo, nrm);
|
|
348
|
+
splatLayer(albedo2, normals23, 0.0, tiling.z, normalScale.z, triplanar.z, w.b, vUv, vWorldPos, bw, albedo, nrm);
|
|
349
|
+
splatLayer(albedo3, normals23, 1.0, tiling.w, normalScale.w, triplanar.w, w.a, vUv, vWorldPos, bw, albedo, nrm);
|
|
350
|
+
vec3 n = applyTangentNormal(normalize(nrm), ng);
|
|
351
|
+
vec3 v = normalize(cameraPosition - vWorldPos);
|
|
352
|
+
if (dot(ng, v) < 0.0) n = -n;
|
|
353
|
+
vec3 diffuseColor = albedo * tint;
|
|
354
|
+
vec3 color = shadePBR(n, v, diffuseColor, vec3(0.04), dot(roughness, w), 1.0);
|
|
355
|
+
fragColor = vec4(tonemap(color), 1.0);
|
|
356
|
+
}
|
|
357
|
+
`
|
|
358
|
+
|
|
359
|
+
// The twin of unlit.mat: factor x map, both default white. OPAQUE — alpha plays no part (Filament's
|
|
360
|
+
// unlit.mat declares baseColor as a float3 and ignores the map's alpha), so the fragment forces 1.
|
|
361
|
+
export const unlitFragment = /* glsl */ `#version 300 es
|
|
362
|
+
precision highp float;
|
|
363
|
+
in vec3 vWorldPos;
|
|
364
|
+
in vec3 vNormal;
|
|
365
|
+
in vec2 vUv;
|
|
366
|
+
uniform vec4 baseColor; // linear, premultiplied; factor over baseColorMap
|
|
367
|
+
uniform sampler2D baseColorMap;
|
|
368
|
+
out vec4 fragColor;
|
|
369
|
+
|
|
370
|
+
vec3 tonemapUnlit(vec3 x) {
|
|
371
|
+
const float A = 2.51; const float B = 0.03; const float C = 2.43; const float D = 0.59; const float E = 0.14;
|
|
372
|
+
vec3 t = clamp((x * (A * x + B)) / (x * (C * x + D) + E), 0.0, 1.0);
|
|
373
|
+
return pow(t, vec3(1.0 / 2.2));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
void main() {
|
|
377
|
+
vec4 c = baseColor * texture(baseColorMap, vUv);
|
|
378
|
+
fragColor = vec4(tonemapUnlit(c.rgb), 1.0);
|
|
379
|
+
}
|
|
380
|
+
`
|
|
381
|
+
|
|
382
|
+
// The twin of unlit-transparent.mat: same maths on a float4 colour, PREMULTIPLIED at the end because
|
|
383
|
+
// the blend func is (ONE, ONE_MINUS_SRC_ALPHA) — the straight-alpha inputs (a decoded PNG, and a
|
|
384
|
+
// colour that PREMULTIPLIED_sRGB only converts without multiplying) are premultiplied exactly once,
|
|
385
|
+
// after tonemapping. Getting this wrong is the classic bright halo around a faded sprite.
|
|
386
|
+
export const unlitBlendFragment = /* glsl */ `#version 300 es
|
|
387
|
+
precision highp float;
|
|
388
|
+
in vec3 vWorldPos;
|
|
389
|
+
in vec3 vNormal;
|
|
390
|
+
in vec2 vUv;
|
|
391
|
+
uniform vec4 baseColor; // linear, straight alpha; factor over baseColorMap
|
|
392
|
+
uniform sampler2D baseColorMap;
|
|
393
|
+
out vec4 fragColor;
|
|
394
|
+
|
|
395
|
+
vec3 tonemapUnlit(vec3 x) {
|
|
396
|
+
const float A = 2.51; const float B = 0.03; const float C = 2.43; const float D = 0.59; const float E = 0.14;
|
|
397
|
+
vec3 t = clamp((x * (A * x + B)) / (x * (C * x + D) + E), 0.0, 1.0);
|
|
398
|
+
return pow(t, vec3(1.0 / 2.2));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
void main() {
|
|
402
|
+
vec4 c = baseColor * texture(baseColorMap, vUv);
|
|
403
|
+
fragColor = vec4(tonemapUnlit(c.rgb) * c.a, c.a);
|
|
404
|
+
}
|
|
405
|
+
`
|
|
406
|
+
|
|
407
|
+
// video.mat: unlit external sample, inverse-tonemapped by Filament so the pipeline round-trips.
|
|
408
|
+
// Here we just pass the (sRGB-encoded) video texel through untouched.
|
|
409
|
+
export const videoFragment = /* glsl */ `#version 300 es
|
|
410
|
+
precision highp float;
|
|
411
|
+
in vec2 vUv;
|
|
412
|
+
in vec3 vWorldPos;
|
|
413
|
+
in vec3 vNormal;
|
|
414
|
+
uniform sampler2D baseColorMap;
|
|
415
|
+
out vec4 fragColor;
|
|
416
|
+
void main() {
|
|
417
|
+
fragColor = vec4(texture(baseColorMap, vUv).rgb, 1.0);
|
|
418
|
+
}
|
|
419
|
+
`
|
|
420
|
+
|
|
421
|
+
// shadow.mat: invisible shadow-catcher plane (shadowMultiplier) — opacity = color.a where shadowed.
|
|
422
|
+
export const shadowCatcherFragment = /* glsl */ `#version 300 es
|
|
423
|
+
precision highp float;
|
|
424
|
+
in vec3 vWorldPos;
|
|
425
|
+
in vec3 vNormal;
|
|
426
|
+
in vec2 vUv;
|
|
427
|
+
uniform vec4 color; // linear, premultiplied
|
|
428
|
+
${LIGHTING_CHUNK}
|
|
429
|
+
out vec4 fragColor;
|
|
430
|
+
void main() {
|
|
431
|
+
float vis = sampleShadowPCF();
|
|
432
|
+
float shadowAmount = 1.0 - vis;
|
|
433
|
+
fragColor = vec4(color.rgb * shadowAmount, color.a * shadowAmount);
|
|
434
|
+
}
|
|
435
|
+
`
|
|
436
|
+
|
|
437
|
+
// glTF PBR (metallic-roughness) — configured with #define flags per material.
|
|
438
|
+
export const gltfVertex = (defines: string): string => vertex(defines)
|
|
439
|
+
export const litVertex = vertex('')
|
|
440
|
+
|
|
441
|
+
export const gltfFragment = (defines: string): string => /* glsl */ `#version 300 es
|
|
442
|
+
precision highp float;
|
|
443
|
+
${defines}
|
|
444
|
+
in vec3 vWorldPos;
|
|
445
|
+
in vec3 vNormal;
|
|
446
|
+
in vec2 vUv;
|
|
447
|
+
#ifdef HAS_COLOR
|
|
448
|
+
in vec4 vColor;
|
|
449
|
+
#endif
|
|
450
|
+
uniform vec3 cameraPosition;
|
|
451
|
+
uniform vec4 uBaseColorFactor;
|
|
452
|
+
uniform float uMetallic;
|
|
453
|
+
uniform float uRoughness;
|
|
454
|
+
uniform vec3 uEmissive;
|
|
455
|
+
uniform float uAlphaCutoff;
|
|
456
|
+
#ifdef HAS_BASECOLOR_MAP
|
|
457
|
+
uniform sampler2D uBaseColorMap;
|
|
458
|
+
#endif
|
|
459
|
+
#ifdef HAS_MR_MAP
|
|
460
|
+
uniform sampler2D uMRMap;
|
|
461
|
+
#endif
|
|
462
|
+
#ifdef HAS_NORMAL_MAP
|
|
463
|
+
uniform sampler2D uNormalMap;
|
|
464
|
+
#endif
|
|
465
|
+
#ifdef HAS_OCCLUSION_MAP
|
|
466
|
+
uniform sampler2D uOcclusionMap;
|
|
467
|
+
#endif
|
|
468
|
+
#ifdef HAS_EMISSIVE_MAP
|
|
469
|
+
uniform sampler2D uEmissiveMap;
|
|
470
|
+
#endif
|
|
471
|
+
${LIGHTING_CHUNK}
|
|
472
|
+
out vec4 fragColor;
|
|
473
|
+
|
|
474
|
+
#ifdef HAS_NORMAL_MAP
|
|
475
|
+
vec3 getNormal() {
|
|
476
|
+
vec3 pos_dx = dFdx(vWorldPos);
|
|
477
|
+
vec3 pos_dy = dFdy(vWorldPos);
|
|
478
|
+
vec2 tex_dx = dFdx(vUv);
|
|
479
|
+
vec2 tex_dy = dFdy(vUv);
|
|
480
|
+
float det = tex_dx.x * tex_dy.y - tex_dy.x * tex_dx.y;
|
|
481
|
+
if (abs(det) < 1e-8) return normalize(vNormal);
|
|
482
|
+
vec3 t = normalize(pos_dx * tex_dy.y - pos_dy * tex_dx.y);
|
|
483
|
+
vec3 ng = normalize(vNormal);
|
|
484
|
+
t = normalize(t - ng * dot(ng, t));
|
|
485
|
+
vec3 b = normalize(cross(ng, t)) * sign(det);
|
|
486
|
+
mat3 tbn = mat3(t, b, ng);
|
|
487
|
+
vec3 n = texture(uNormalMap, vUv).rgb * 2.0 - 1.0;
|
|
488
|
+
return normalize(tbn * n);
|
|
489
|
+
}
|
|
490
|
+
#endif
|
|
491
|
+
|
|
492
|
+
void main() {
|
|
493
|
+
vec4 base = uBaseColorFactor;
|
|
494
|
+
#ifdef HAS_BASECOLOR_MAP
|
|
495
|
+
base *= texture(uBaseColorMap, vUv);
|
|
496
|
+
#endif
|
|
497
|
+
#ifdef HAS_COLOR
|
|
498
|
+
base *= vColor;
|
|
499
|
+
#endif
|
|
500
|
+
#ifdef ALPHA_MASK
|
|
501
|
+
if (base.a < uAlphaCutoff) discard;
|
|
502
|
+
#endif
|
|
503
|
+
|
|
504
|
+
#ifdef UNLIT
|
|
505
|
+
// KHR_materials_unlit: the base colour IS the shaded colour — no lights, no IBL, no exposure
|
|
506
|
+
// (Filament's UNLIT shading model does the same, then tone-maps in post like everything else).
|
|
507
|
+
#ifdef ALPHA_BLEND
|
|
508
|
+
fragColor = vec4(tonemap(base.rgb), base.a);
|
|
509
|
+
#else
|
|
510
|
+
fragColor = vec4(tonemap(base.rgb), 1.0);
|
|
511
|
+
#endif
|
|
512
|
+
return;
|
|
513
|
+
#endif
|
|
514
|
+
|
|
515
|
+
float metallic = uMetallic;
|
|
516
|
+
float roughness = uRoughness;
|
|
517
|
+
#ifdef HAS_MR_MAP
|
|
518
|
+
vec3 mr = texture(uMRMap, vUv).rgb;
|
|
519
|
+
roughness *= mr.g;
|
|
520
|
+
metallic *= mr.b;
|
|
521
|
+
#endif
|
|
522
|
+
|
|
523
|
+
float occlusion = 1.0;
|
|
524
|
+
#ifdef HAS_OCCLUSION_MAP
|
|
525
|
+
occlusion = texture(uOcclusionMap, vUv).r;
|
|
526
|
+
#endif
|
|
527
|
+
|
|
528
|
+
#ifdef HAS_NORMAL_MAP
|
|
529
|
+
vec3 n = getNormal();
|
|
530
|
+
#else
|
|
531
|
+
vec3 n = normalize(vNormal);
|
|
532
|
+
#endif
|
|
533
|
+
vec3 v = normalize(cameraPosition - vWorldPos);
|
|
534
|
+
#ifdef DOUBLE_SIDED
|
|
535
|
+
if (dot(n, v) < 0.0) n = -n;
|
|
536
|
+
#endif
|
|
537
|
+
|
|
538
|
+
vec3 diffuseColor = base.rgb * (1.0 - metallic);
|
|
539
|
+
vec3 f0 = mix(vec3(0.04), base.rgb, metallic);
|
|
540
|
+
vec3 color = shadePBR(n, v, diffuseColor, f0, roughness, occlusion);
|
|
541
|
+
|
|
542
|
+
vec3 emissive = uEmissive;
|
|
543
|
+
#ifdef HAS_EMISSIVE_MAP
|
|
544
|
+
emissive *= texture(uEmissiveMap, vUv).rgb;
|
|
545
|
+
#endif
|
|
546
|
+
color += emissive * iblLuminance * exposure;
|
|
547
|
+
|
|
548
|
+
#ifdef ALPHA_BLEND
|
|
549
|
+
fragColor = vec4(tonemap(color), base.a);
|
|
550
|
+
#else
|
|
551
|
+
fragColor = vec4(tonemap(color), 1.0);
|
|
552
|
+
#endif
|
|
553
|
+
}
|
|
554
|
+
`
|
|
555
|
+
|
|
556
|
+
// ---- wireframe (editor viewport modes) --------------------------------------------------------
|
|
557
|
+
// Solid-color fragment for the LINES companion meshes (createViewerLite _setWireframeMode). Reuses
|
|
558
|
+
// the standard vertex shaders (litVertex / gltfVertex('HAS_SKIN')) so skinned wires pose correctly;
|
|
559
|
+
// the extra varyings they emit are simply unused here.
|
|
560
|
+
export const wireFragment = /* glsl */ `#version 300 es
|
|
561
|
+
precision highp float;
|
|
562
|
+
in vec3 vWorldPos;
|
|
563
|
+
in vec3 vNormal;
|
|
564
|
+
in vec2 vUv;
|
|
565
|
+
uniform vec3 uWireColor;
|
|
566
|
+
out vec4 fragColor;
|
|
567
|
+
void main() { fragColor = vec4(uWireColor, 1.0); }
|
|
568
|
+
`
|
|
569
|
+
|
|
570
|
+
// ---- shadow (depth) pass ----------------------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
/** Depth-only vertex stage (shadow map, selection mask). `instanced` reads the world matrix from
|
|
573
|
+
* the batch stream instead of the modelMatrix uniform (batching.ts). */
|
|
574
|
+
export const depthVertex = (skin: boolean, instanced = false): string => /* glsl */ `#version 300 es
|
|
575
|
+
precision highp float;
|
|
576
|
+
in vec3 position;
|
|
577
|
+
${skin ? 'in vec3 normal;\nin vec2 uv;' : ''}
|
|
578
|
+
${instanced ? 'in mat4 instanceMatrix;' : 'uniform mat4 modelMatrix;'}
|
|
579
|
+
uniform mat4 shadowViewProjection;
|
|
580
|
+
${skin ? SKIN_CHUNK : ''}
|
|
581
|
+
void main() {
|
|
582
|
+
vec4 pos = vec4(position, 1.0);
|
|
583
|
+
${skin ? 'pos = getSkinMatrix() * pos;' : ''}
|
|
584
|
+
gl_Position = shadowViewProjection * ${instanced ? 'instanceMatrix' : 'modelMatrix'} * pos;
|
|
585
|
+
}
|
|
586
|
+
`
|
|
587
|
+
|
|
588
|
+
export const depthFragment = /* glsl */ `#version 300 es
|
|
589
|
+
precision highp float;
|
|
590
|
+
out vec4 fragColor;
|
|
591
|
+
void main() { fragColor = vec4(1.0); }
|
|
592
|
+
`
|
|
593
|
+
|
|
594
|
+
// ---- color helpers ----------------------------------------------------------------------------
|
|
595
|
+
|
|
596
|
+
export const srgbToLinear = (c: number): number =>
|
|
597
|
+
c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
|
|
598
|
+
|
|
599
|
+
/** JS-side twin of the shader tonemap — for values written outside a shader (the clear color). */
|
|
600
|
+
export const tonemapJs = (x: number): number => {
|
|
601
|
+
const t = Math.min(1, Math.max(0, (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14)))
|
|
602
|
+
return Math.pow(t, 1 / 2.2)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** 0xRRGGBB → linear [r,g,b] (setUniformRgb / RgbType::sRGB). */
|
|
606
|
+
export const hexToLinearRgb = (hex: number): [number, number, number] => [
|
|
607
|
+
srgbToLinear(((hex >>> 16) & 0xff) / 255),
|
|
608
|
+
srgbToLinear(((hex >>> 8) & 0xff) / 255),
|
|
609
|
+
srgbToLinear((hex & 0xff) / 255),
|
|
610
|
+
]
|
|
611
|
+
|
|
612
|
+
/** 0xRRGGBBAA → linear premultiplied [r,g,b,a] (setUniformRgba / RgbaType::PREMULTIPLIED_sRGB). */
|
|
613
|
+
export const hexToLinearRgba = (hex: number): [number, number, number, number] => [
|
|
614
|
+
srgbToLinear(((hex >>> 24) & 0xff) / 255),
|
|
615
|
+
srgbToLinear(((hex >>> 16) & 0xff) / 255),
|
|
616
|
+
srgbToLinear(((hex >>> 8) & 0xff) / 255),
|
|
617
|
+
(hex & 0xff) / 255,
|
|
618
|
+
]
|
|
619
|
+
|
|
620
|
+
// ---- particles (creator-particles streams; twins of creator-gl/materials/src/particles*.mat) ----
|
|
621
|
+
// The stream carries per-vertex color + custom (size, rotation, opacity, frame); the look (flipbook
|
|
622
|
+
// cells, softness, emission, additive, billboarding / stretch / ribbon) is the shader, driven by the
|
|
623
|
+
// same uniforms the Filament materials declare. Output is premultiplied, tonemapped like unlit.
|
|
624
|
+
// Not here: soft particles (depthFade) — the lite pipeline has no depth prepass to read.
|
|
625
|
+
|
|
626
|
+
const PARTICLE_FRAG_COMMON = /* glsl */ `
|
|
627
|
+
uniform sampler2D baseColorMap;
|
|
628
|
+
uniform vec2 sheetSize; // (cols, rows) of the flipbook sheet, both >= 1 (the SDK primes it)
|
|
629
|
+
uniform float emissive;
|
|
630
|
+
uniform float additive;
|
|
631
|
+
uniform float softness;
|
|
632
|
+
uniform float depthFade; // declared so the SDK's write lands somewhere; ignored (no depth buffer)
|
|
633
|
+
|
|
634
|
+
vec3 tonemapParticle(vec3 x) {
|
|
635
|
+
const float A = 2.51; const float B = 0.03; const float C = 2.43; const float D = 0.59; const float E = 0.14;
|
|
636
|
+
vec3 t = clamp((x * (A * x + B)) / (x * (C * x + D) + E), 0.0, 1.0);
|
|
637
|
+
return pow(t, vec3(1.0 / 2.2));
|
|
638
|
+
}
|
|
639
|
+
// Integer cell math: a float floor(frame / cols) lands one row early at row starts on the GPU.
|
|
640
|
+
vec2 sheetCell(int fi) {
|
|
641
|
+
int sx = int(sheetSize.x + 0.5);
|
|
642
|
+
return vec2(float(fi - (fi / sx) * sx), float(fi / sx));
|
|
643
|
+
}
|
|
644
|
+
`
|
|
645
|
+
|
|
646
|
+
export const particlesPointVertex = /* glsl */ `#version 300 es
|
|
647
|
+
precision highp float;
|
|
648
|
+
in vec3 position;
|
|
649
|
+
in vec4 color;
|
|
650
|
+
in vec4 custom;
|
|
651
|
+
uniform mat4 modelMatrix;
|
|
652
|
+
uniform mat4 viewMatrix;
|
|
653
|
+
uniform mat4 projectionMatrix;
|
|
654
|
+
uniform float perspScale; // viewportHeightPx / (2 tan(fov/2)): a true world-space diameter
|
|
655
|
+
out vec4 vColor;
|
|
656
|
+
out vec4 vCustom;
|
|
657
|
+
void main() {
|
|
658
|
+
vec4 viewPos = viewMatrix * modelMatrix * vec4(position, 1.0);
|
|
659
|
+
float z = max(-viewPos.z, 1e-3);
|
|
660
|
+
gl_PointSize = custom.x * perspScale / z;
|
|
661
|
+
vColor = color;
|
|
662
|
+
vCustom = custom;
|
|
663
|
+
gl_Position = projectionMatrix * viewPos;
|
|
664
|
+
}
|
|
665
|
+
`
|
|
666
|
+
|
|
667
|
+
export const particlesPointFragment = /* glsl */ `#version 300 es
|
|
668
|
+
precision highp float;
|
|
669
|
+
in vec4 vColor;
|
|
670
|
+
in vec4 vCustom;
|
|
671
|
+
${PARTICLE_FRAG_COMMON}
|
|
672
|
+
out vec4 fragColor;
|
|
673
|
+
void main() {
|
|
674
|
+
vec2 uv = gl_PointCoord.xy;
|
|
675
|
+
float rot = vCustom.y;
|
|
676
|
+
if (rot != 0.0) {
|
|
677
|
+
float co = cos(rot);
|
|
678
|
+
float si = sin(rot);
|
|
679
|
+
vec2 p = uv - vec2(0.5);
|
|
680
|
+
uv = vec2(co * p.x - si * p.y, si * p.x + co * p.y) + vec2(0.5);
|
|
681
|
+
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) discard;
|
|
682
|
+
}
|
|
683
|
+
int total = int(sheetSize.x + 0.5) * int(sheetSize.y + 0.5);
|
|
684
|
+
int fi = int(floor(vCustom.w) + 0.5);
|
|
685
|
+
fi = fi - (fi / total) * total;
|
|
686
|
+
vec4 tex = texture(baseColorMap, (sheetCell(fi) + uv) / sheetSize);
|
|
687
|
+
float radial = 1.0 - smoothstep(0.0, 0.5, length(gl_PointCoord.xy - vec2(0.5)));
|
|
688
|
+
float shape = mix(1.0, radial, clamp(softness, 0.0, 1.0));
|
|
689
|
+
float alpha = tex.a * vColor.a * vCustom.z * shape;
|
|
690
|
+
vec3 rgb = tex.rgb * vColor.rgb * (1.0 + emissive);
|
|
691
|
+
fragColor = vec4(tonemapParticle(rgb) * alpha, alpha * (1.0 - clamp(additive, 0.0, 1.0)));
|
|
692
|
+
}
|
|
693
|
+
`
|
|
694
|
+
|
|
695
|
+
export const particlesQuadVertex = /* glsl */ `#version 300 es
|
|
696
|
+
precision highp float;
|
|
697
|
+
in vec3 position;
|
|
698
|
+
in vec4 color;
|
|
699
|
+
in vec4 custom; // size, rotation, opacity, frame
|
|
700
|
+
in vec3 aux; // quad: velocity (stretch axis); ribbon: strip tangent — sim space
|
|
701
|
+
in vec2 corner; // quad: ±0.5 square; ribbon: (u01 - 0.5, ±0.5 side)
|
|
702
|
+
uniform mat4 modelMatrix;
|
|
703
|
+
uniform mat4 viewMatrix;
|
|
704
|
+
uniform mat4 projectionMatrix;
|
|
705
|
+
uniform float mode; // 0 camera billboard, 1 velocity-stretched, 2 ribbon strip
|
|
706
|
+
uniform float stretch; // (mode 1) extra length per unit of speed: length = size + stretch · |v|
|
|
707
|
+
out vec4 vColor;
|
|
708
|
+
out vec4 vCustom;
|
|
709
|
+
out vec4 vQuadUv; // xy: in-cell uv, zw: the softness basis
|
|
710
|
+
void main() {
|
|
711
|
+
vec4 wp = modelMatrix * vec4(position, 1.0);
|
|
712
|
+
vec3 a = mat3(modelMatrix) * aux;
|
|
713
|
+
// the camera's world axes: rows of the view rotation (= columns of its inverse)
|
|
714
|
+
vec3 camX = vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);
|
|
715
|
+
vec3 camY = vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);
|
|
716
|
+
vec3 camZ = vec3(viewMatrix[0][2], viewMatrix[1][2], viewMatrix[2][2]); // toward the camera
|
|
717
|
+
vec3 offset;
|
|
718
|
+
vec2 suv = corner;
|
|
719
|
+
if (mode < 0.5) {
|
|
720
|
+
float co = cos(custom.y);
|
|
721
|
+
float si = sin(custom.y);
|
|
722
|
+
vec2 rc = vec2(co * corner.x - si * corner.y, si * corner.x + co * corner.y);
|
|
723
|
+
offset = (camX * rc.x + camY * rc.y) * custom.x;
|
|
724
|
+
} else if (mode < 1.5) {
|
|
725
|
+
float speed = length(a);
|
|
726
|
+
vec3 dir = speed > 1e-4 ? a / speed : camY;
|
|
727
|
+
vec3 y = dir - camZ * dot(dir, camZ);
|
|
728
|
+
float yl = length(y);
|
|
729
|
+
y = yl > 1e-4 ? y / yl : camY;
|
|
730
|
+
vec3 x = normalize(cross(y, camZ));
|
|
731
|
+
offset = x * corner.x * custom.x + y * corner.y * (custom.x + stretch * speed * yl);
|
|
732
|
+
} else {
|
|
733
|
+
vec3 x = cross(a, camZ);
|
|
734
|
+
float xl = length(x);
|
|
735
|
+
x = xl > 1e-4 ? x / xl : camX;
|
|
736
|
+
offset = x * corner.y * custom.x;
|
|
737
|
+
suv = vec2(0.0, corner.y);
|
|
738
|
+
}
|
|
739
|
+
wp.xyz += offset;
|
|
740
|
+
vColor = color;
|
|
741
|
+
vCustom = custom;
|
|
742
|
+
vQuadUv = vec4(corner.x + 0.5, 0.5 - corner.y, suv.x, suv.y);
|
|
743
|
+
gl_Position = projectionMatrix * viewMatrix * wp;
|
|
744
|
+
}
|
|
745
|
+
`
|
|
746
|
+
|
|
747
|
+
export const particlesQuadFragment = /* glsl */ `#version 300 es
|
|
748
|
+
precision highp float;
|
|
749
|
+
in vec4 vColor;
|
|
750
|
+
in vec4 vCustom;
|
|
751
|
+
in vec4 vQuadUv;
|
|
752
|
+
${PARTICLE_FRAG_COMMON}
|
|
753
|
+
uniform sampler2D emissiveMap;
|
|
754
|
+
uniform vec3 emissiveColor;
|
|
755
|
+
uniform float frameBlend;
|
|
756
|
+
out vec4 fragColor;
|
|
757
|
+
void main() {
|
|
758
|
+
vec2 uv = vQuadUv.xy;
|
|
759
|
+
float total = sheetSize.x * sheetSize.y;
|
|
760
|
+
float fidx = mod(vCustom.w, total);
|
|
761
|
+
float frame = floor(fidx);
|
|
762
|
+
int fi = int(frame + 0.5);
|
|
763
|
+
vec2 tuv = (sheetCell(fi) + uv) / sheetSize;
|
|
764
|
+
vec4 tex = texture(baseColorMap, tuv);
|
|
765
|
+
vec3 em = texture(emissiveMap, tuv).rgb;
|
|
766
|
+
if (frameBlend > 0.5) {
|
|
767
|
+
int ni = min(fi + 1, int(total + 0.5) - 1);
|
|
768
|
+
vec2 tuv2 = (sheetCell(ni) + uv) / sheetSize;
|
|
769
|
+
float f = fidx - frame;
|
|
770
|
+
tex = mix(tex, texture(baseColorMap, tuv2), f);
|
|
771
|
+
em = mix(em, texture(emissiveMap, tuv2).rgb, f);
|
|
772
|
+
}
|
|
773
|
+
float radial = 1.0 - smoothstep(0.0, 0.5, length(vQuadUv.zw));
|
|
774
|
+
float shape = mix(1.0, radial, clamp(softness, 0.0, 1.0));
|
|
775
|
+
float alpha = tex.a * vColor.a * vCustom.z * shape;
|
|
776
|
+
vec3 rgb = tex.rgb * vColor.rgb * (1.0 + emissive) + em * emissiveColor;
|
|
777
|
+
fragColor = vec4(tonemapParticle(rgb) * alpha, alpha * (1.0 - clamp(additive, 0.0, 1.0)));
|
|
778
|
+
}
|
|
779
|
+
`
|