babylon.quarks 0.26.0 → 0.28.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +1194 -8
  2. package/README.md +13 -9
  3. package/dist/babylon.quarks.cjs +1 -1
  4. package/dist/babylon.quarks.cjs.map +1 -1
  5. package/dist/babylon.quarks.esm.js +1 -1
  6. package/dist/babylon.quarks.esm.js.map +1 -1
  7. package/dist/babylon.quarks.umd.min.js +1 -1
  8. package/dist/babylon.quarks.umd.min.js.map +1 -1
  9. package/dist/types/index.d.cts +10108 -0
  10. package/dist/types/index.d.ts +5794 -998
  11. package/package.json +18 -7
  12. package/src/BatchedRenderer.ts +274 -156
  13. package/src/EffectPool.ts +94 -12
  14. package/src/MeshSurfaceEmitter.ts +50 -14
  15. package/src/ParticleEmitter.ts +75 -17
  16. package/src/ParticleSystem.ts +981 -2066
  17. package/src/QuarksLoader.ts +666 -194
  18. package/src/QuarksPrefab.ts +367 -73
  19. package/src/QuarksSerializer.ts +512 -0
  20. package/src/QuarksUtil.ts +77 -7
  21. package/src/SpriteBatch.ts +127 -252
  22. package/src/TrailBatch.ts +545 -348
  23. package/src/VFXBatch.ts +238 -596
  24. package/src/batchKey.ts +239 -0
  25. package/src/batchSettings.ts +546 -0
  26. package/src/effectDocumentJSON.ts +261 -0
  27. package/src/effectResources.ts +76 -0
  28. package/src/envAtlas.ts +126 -18
  29. package/src/geometryUtil.ts +1 -1
  30. package/src/index.ts +69 -35
  31. package/src/nodeId.ts +36 -0
  32. package/src/shaderProgram.ts +419 -0
  33. package/src/shaderVariants.ts +245 -0
  34. package/src/shaders/chunks/blend.ts +31 -0
  35. package/src/shaders/chunks/fog.ts +12 -5
  36. package/src/shaders/chunks/sheet.ts +111 -0
  37. package/src/shaders/chunks/soft.ts +85 -0
  38. package/src/shaders/chunks/surface.ts +12 -2
  39. package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
  40. package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
  41. package/src/shaders/particle_frag.glsl.ts +5 -22
  42. package/src/shaders/particle_frag.wgsl.ts +5 -23
  43. package/src/shaders/particle_physics_frag.glsl.ts +16 -26
  44. package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
  45. package/src/shaders/particle_vert.glsl.ts +5 -27
  46. package/src/shaders/particle_vert.wgsl.ts +5 -26
  47. package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
  48. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
  49. package/src/shaders/trail_frag.glsl.ts +13 -0
  50. package/src/shaders/trail_frag.wgsl.ts +13 -0
  51. package/src/shaders/trail_vert.glsl.ts +45 -36
  52. package/src/shaders/trail_vert.wgsl.ts +46 -40
  53. package/src/softDepth.ts +136 -0
  54. package/src/system/BehaviorPlan.ts +85 -0
  55. package/src/system/BoundsTracker.ts +181 -0
  56. package/src/system/EmissionController.ts +358 -0
  57. package/src/system/EventDispatcher.ts +60 -0
  58. package/src/system/ParticleSpawner.ts +274 -0
  59. package/src/system/SimulationClock.ts +130 -0
  60. package/src/system/geometry.ts +104 -0
  61. package/src/system/integrate.ts +428 -0
  62. package/src/system/materialResolve.ts +165 -0
  63. package/src/system/normalize.ts +100 -0
  64. package/src/system/serialization.ts +422 -0
  65. package/src/system/settingsSchema.ts +339 -0
  66. package/src/system/types.ts +449 -0
  67. package/src/textureSource.ts +30 -0
  68. package/src/trailSettings.ts +54 -0
  69. package/src/BatchedParticleRenderer.ts +0 -3
  70. package/src/materials/ParticleMaterials.ts +0 -23
  71. package/src/materials/index.ts +0 -1
  72. package/src/shaders/chunks/index.ts +0 -1
  73. package/src/shaders/chunks/register-shader-chunks.ts +0 -32
  74. package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
  75. package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
  76. package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
  77. package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
  78. package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
  79. package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
  80. package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
  81. package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
  82. package/src/shaders/index.ts +0 -9
  83. package/src/shaders/local_particle_vert.glsl.ts +0 -50
@@ -0,0 +1,245 @@
1
+ import {Constants} from '@babylonjs/core/Engines/constants';
2
+ import type {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
3
+ import {Vector3} from 'quarks.core';
4
+ import {ShaderContext, shaderProgram, ShaderProgram, ShaderSettings} from './shaderProgram';
5
+ import {
6
+ normalizeErosion,
7
+ normalizeGradientMap,
8
+ normalizeMapUV,
9
+ normalizeMeshRim,
10
+ normalizeUVDistortion,
11
+ RenderMode,
12
+ } from './VFXBatch';
13
+
14
+ /**
15
+ * One shader the library can compile: a {@link ShaderProgram} with the settings that produce it,
16
+ * named for the renderer's benefit. See {@link shaderVariants}.
17
+ */
18
+ export interface ShaderVariant {
19
+ /** The render mode and the features compiled in, for a report. */
20
+ name: string;
21
+ /** The program a batch with these settings builds. */
22
+ program: ShaderProgram;
23
+ }
24
+
25
+ /** A texture reference for settings that only ask whether one is set; it is never sampled. */
26
+ const TEXTURE = {} as BaseTexture;
27
+
28
+ /** The settings of a bare batch in `renderMode`: no texture, no effects, fog off, 1×1 sheet. */
29
+ function bare(renderMode: RenderMode): ShaderSettings {
30
+ return {
31
+ renderMode,
32
+ texture: null,
33
+ reflectionAtlas: null,
34
+ uTileCount: 1,
35
+ vTileCount: 1,
36
+ blendTiles: false,
37
+ softParticles: false,
38
+ materialAlphaTest: 0,
39
+ materialTransparent: true,
40
+ materialBlendMode: Constants.ALPHA_COMBINE,
41
+ applyFog: false,
42
+ pivot: undefined,
43
+ minParticleSize: 0,
44
+ maxParticleSize: Infinity,
45
+ lighting: renderMode === RenderMode.Mesh ? 'fixed' : undefined,
46
+ rim: undefined,
47
+ mapUV: undefined,
48
+ uvDistortion: undefined,
49
+ gradientMap: undefined,
50
+ erosion: undefined,
51
+ };
52
+ }
53
+
54
+ /** A settings change with the name it adds to the variant. */
55
+ interface Axis {
56
+ name: string;
57
+ settings?: Partial<ShaderSettings>;
58
+ context?: Partial<ShaderContext>;
59
+ }
60
+
61
+ const NONE: Axis = {name: ''};
62
+
63
+ /** The render modes and the mode-specific features: quad pivot and size limits, mesh lighting, rim and reflection. */
64
+ const TARGETS: Axis[] = [
65
+ {name: 'billboard', settings: {renderMode: RenderMode.BillBoard}},
66
+ {name: 'vertical billboard', settings: {renderMode: RenderMode.VerticalBillBoard}},
67
+ {name: 'horizontal billboard', settings: {renderMode: RenderMode.HorizontalBillBoard}},
68
+ {name: 'stretched', settings: {renderMode: RenderMode.StretchedBillBoard}},
69
+ {name: 'beam', settings: {renderMode: RenderMode.Beam}},
70
+ {name: 'mesh', settings: {renderMode: RenderMode.Mesh}},
71
+ {name: 'mesh scene lit', settings: {renderMode: RenderMode.Mesh, lighting: 'scene'}},
72
+ {name: 'mesh unlit', settings: {renderMode: RenderMode.Mesh, lighting: 'unlit'}},
73
+ {name: 'mesh rim', settings: {renderMode: RenderMode.Mesh, rim: normalizeMeshRim({mode: 'add'})}},
74
+ {
75
+ name: 'mesh unlit rim mix',
76
+ settings: {renderMode: RenderMode.Mesh, lighting: 'unlit', rim: normalizeMeshRim({mode: 'mix'})},
77
+ },
78
+ {
79
+ name: 'mesh unlit rim fade',
80
+ settings: {renderMode: RenderMode.Mesh, lighting: 'unlit', rim: normalizeMeshRim({mode: 'fade'})},
81
+ },
82
+ {
83
+ name: 'mesh reflecting',
84
+ settings: {renderMode: RenderMode.Mesh, reflectionAtlas: TEXTURE},
85
+ context: {envAtlasReady: true},
86
+ },
87
+ {
88
+ name: 'mesh unlit reflecting rim fade',
89
+ settings: {
90
+ renderMode: RenderMode.Mesh,
91
+ lighting: 'unlit',
92
+ reflectionAtlas: TEXTURE,
93
+ rim: normalizeMeshRim({mode: 'fade'}),
94
+ },
95
+ context: {envAtlasReady: true},
96
+ },
97
+ {name: 'trail', settings: {renderMode: RenderMode.Trail}},
98
+ ];
99
+
100
+ /** Pivot and screen-size limits, which only the quad modes read. */
101
+ const QUAD: Axis[] = [
102
+ NONE,
103
+ {name: 'pivot', settings: {pivot: new Vector3(0.5, 0, 0)}},
104
+ {name: 'size limits', settings: {minParticleSize: 0.01}},
105
+ {name: 'pivot size limits', settings: {pivot: new Vector3(0, 0.5, 0.5), maxParticleSize: 0.5}},
106
+ ];
107
+
108
+ const TILE: Axis[] = [
109
+ NONE,
110
+ {name: 'sheet', settings: {uTileCount: 4, vTileCount: 2}},
111
+ {name: 'sheet blend', settings: {uTileCount: 4, vTileCount: 2, blendTiles: true}},
112
+ ];
113
+ const MAP: Axis[] = [NONE, {name: 'map', settings: {texture: TEXTURE}}];
114
+ const SOFT: Axis[] = [
115
+ NONE,
116
+ {name: 'soft', settings: {softParticles: true}, context: {depth: {packed: false}}},
117
+ {name: 'soft packed', settings: {softParticles: true}, context: {depth: {packed: true}}},
118
+ ];
119
+ const ALPHA_TEST: Axis[] = [NONE, {name: 'alpha test', settings: {materialAlphaTest: 0.5}}];
120
+ const FOG: Axis[] = [
121
+ NONE,
122
+ {name: 'fog', settings: {applyFog: true}},
123
+ {name: 'fog additive', settings: {applyFog: true, materialBlendMode: Constants.ALPHA_ADD}},
124
+ ];
125
+
126
+ /** The blend resolves (see `blendCategory`), with and without the fog each one takes. */
127
+ const BLEND: Axis[] = [
128
+ {name: 'premultiplied', settings: {materialBlendMode: Constants.ALPHA_PREMULTIPLIED}},
129
+ {name: 'premultiplied fog', settings: {materialBlendMode: Constants.ALPHA_PREMULTIPLIED, applyFog: true}},
130
+ {name: 'one-one', settings: {materialBlendMode: Constants.ALPHA_ONEONE}},
131
+ {name: 'one-one fog', settings: {materialBlendMode: Constants.ALPHA_ONEONE, applyFog: true}},
132
+ {name: 'multiply', settings: {materialBlendMode: Constants.ALPHA_MULTIPLY}},
133
+ {name: 'multiply fog', settings: {materialBlendMode: Constants.ALPHA_MULTIPLY, applyFog: true}},
134
+ {
135
+ name: 'multiply fog alpha test',
136
+ settings: {materialBlendMode: Constants.ALPHA_MULTIPLY, applyFog: true, materialAlphaTest: 0.5},
137
+ },
138
+ {name: 'opaque', settings: {materialTransparent: false}},
139
+ ];
140
+
141
+ /** Texture effects (`chunks/surface.ts`), alone and together, on both UV channels. */
142
+ const SURFACE: Axis[] = (() => {
143
+ const uv0 = normalizeMapUV({tiling: [2, 2]});
144
+ const uv1 = normalizeMapUV({channel: 1, tiling: [2, 2]});
145
+ const distortion0 = normalizeUVDistortion({map: TEXTURE});
146
+ const distortion1 = normalizeUVDistortion({map: TEXTURE, mapUV: {channel: 1}});
147
+ const gradient = normalizeGradientMap({ramp: TEXTURE});
148
+ const erosion = normalizeErosion({});
149
+ const erosionMask0 = normalizeErosion({mask: TEXTURE});
150
+ const erosionMask1 = normalizeErosion({mask: TEXTURE, maskUV: {channel: 1}});
151
+ return [
152
+ {name: 'map transform', settings: {mapUV: uv0}},
153
+ {name: 'map transform uv1', settings: {mapUV: uv1}},
154
+ {name: 'gradient map', settings: {gradientMap: gradient}},
155
+ {name: 'erosion', settings: {erosion}},
156
+ {name: 'erosion mask', settings: {erosion: erosionMask0}},
157
+ {name: 'erosion mask uv1', settings: {erosion: erosionMask1}},
158
+ {name: 'distortion', settings: {uvDistortion: distortion0}},
159
+ {name: 'distortion uv1', settings: {uvDistortion: distortion1}},
160
+ {
161
+ name: 'map transform distortion gradient erosion mask',
162
+ settings: {mapUV: uv0, uvDistortion: distortion0, gradientMap: gradient, erosion: erosionMask0},
163
+ },
164
+ {
165
+ name: 'map transform uv1 distortion gradient erosion',
166
+ settings: {mapUV: uv1, uvDistortion: distortion0, gradientMap: gradient, erosion},
167
+ },
168
+ {
169
+ name: 'every effect on uv1',
170
+ settings: {mapUV: uv1, uvDistortion: distortion1, gradientMap: gradient, erosion: erosionMask1},
171
+ },
172
+ ];
173
+ })();
174
+
175
+ /** The targets the texture effects are tried on: one per vertex shader and fragment family. */
176
+ const SURFACE_TARGETS = [
177
+ 'billboard',
178
+ 'stretched',
179
+ 'beam',
180
+ 'mesh',
181
+ 'mesh unlit',
182
+ 'mesh unlit rim mix',
183
+ 'mesh reflecting',
184
+ 'trail',
185
+ ];
186
+
187
+ /** A mesh batch whose geometry has a second UV set. */
188
+ const MESH_UV2: Axis = {name: 'uv2', context: {meshUV2: true}};
189
+
190
+ /** Soft, alpha-tested and fogged at once, to try the effects with the rest of the fragment. */
191
+ const EVERYTHING_ELSE: Axis = {
192
+ name: 'soft packed alpha test fog one-one',
193
+ settings: {softParticles: true, materialAlphaTest: 0.5, applyFog: true, materialBlendMode: Constants.ALPHA_ONEONE},
194
+ context: {depth: {packed: true}},
195
+ };
196
+
197
+ /**
198
+ * Every shader program the library can compile, one per distinct set of defines: every render
199
+ * mode with every combination of sheet, map, soft particles, alpha test and fog, every blend
200
+ * resolve with and without fog and soft particles, and the texture effects alone and together on
201
+ * each shader family. What a shader validator compiles, and what
202
+ * an application can warm the shader cache with before its first frame (Unity's
203
+ * ShaderVariantCollection). Built by {@link shaderProgram}, so a variant here is a material a
204
+ * batch would build.
205
+ */
206
+ export function shaderVariants(): ShaderVariant[] {
207
+ const seen = new Set<string>();
208
+ const out: ShaderVariant[] = [];
209
+ const add = (axes: Axis[]) => {
210
+ const settings = bare(RenderMode.BillBoard);
211
+ const context: ShaderContext = {depth: null, envAtlasReady: false, meshUV2: false};
212
+ for (const axis of axes) {
213
+ Object.assign(settings, axis.settings);
214
+ Object.assign(context, axis.context);
215
+ }
216
+ const program = shaderProgram(settings, context);
217
+ const key = program.name + ' ' + program.defines.join(',');
218
+ if (seen.has(key)) {
219
+ return;
220
+ }
221
+ seen.add(key);
222
+ const features = axes.map((axis) => axis.name).filter((name) => name !== '');
223
+ out.push({name: `${features.join(', ')} [${program.defines.join(',') || 'none'}]`, program});
224
+ };
225
+
226
+ for (const target of TARGETS)
227
+ for (const quad of QUAD)
228
+ for (const tile of TILE)
229
+ for (const map of MAP)
230
+ for (const soft of SOFT)
231
+ for (const alphaTest of ALPHA_TEST)
232
+ for (const fog of FOG) add([target, quad, tile, map, soft, alphaTest, fog]);
233
+
234
+ for (const target of TARGETS)
235
+ for (const blend of BLEND)
236
+ for (const soft of SOFT.slice(0, 2)) for (const map of MAP) add([target, blend, soft, map]);
237
+
238
+ for (const target of TARGETS.filter((t) => SURFACE_TARGETS.includes(t.name)))
239
+ for (const uv2 of target.settings?.renderMode === RenderMode.Mesh ? [NONE, MESH_UV2] : [NONE])
240
+ for (const surface of SURFACE)
241
+ for (const map of MAP)
242
+ for (const tile of [NONE, TILE[2]])
243
+ for (const rest of [NONE, EVERYTHING_ELSE]) add([target, uv2, surface, map, tile, rest]);
244
+ return out;
245
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The blend resolve for the particle shaders, in both shader languages.
3
+ *
4
+ * Every fade in the shaders (soft particles, erosion, a rim fade, alpha over life) lowers alpha
5
+ * only, so that alpha means coverage. Blend modes that take the colour at full weight
6
+ * (`BLEND_PREMULTIPLY`: one-one, premultiplied, screen, subtract, …) would ignore it, so the
7
+ * resolve multiplies the colour by its alpha; modes that multiply the frame (`BLEND_MULTIPLY`)
8
+ * move it toward white instead. It runs last, after fog and the soft fade. See `blendCategory`.
9
+ */
10
+
11
+ /** Resolves the vec4 lvalue `color` for the blend mode, GLSL. */
12
+ export function blendFragmentGLSL(color: string): string {
13
+ return /* glsl */ `
14
+ #if defined(BLEND_PREMULTIPLY)
15
+ ${color}.rgb *= ${color}.a;
16
+ #elif defined(BLEND_MULTIPLY)
17
+ ${color}.rgb = mix(vec3(1.0), ${color}.rgb, ${color}.a);
18
+ #endif
19
+ `;
20
+ }
21
+
22
+ /** Resolves the `var` vec4 `color` for the blend mode, WGSL. */
23
+ export function blendFragmentWGSL(color: string): string {
24
+ return /* wgsl */ `
25
+ #if defined(BLEND_PREMULTIPLY)
26
+ ${color} = vec4f(${color}.rgb * ${color}.a, ${color}.a);
27
+ #elif defined(BLEND_MULTIPLY)
28
+ ${color} = vec4f(mix(vec3f(1.0), ${color}.rgb, ${color}.a), ${color}.a);
29
+ #endif
30
+ `;
31
+ }
@@ -11,7 +11,8 @@
11
11
  *
12
12
  * Additive particles cannot fade toward a fog colour (that would add fog to
13
13
  * the whole screen), so under `FOG_ADDITIVE` they fade toward black instead,
14
- * which is what Unity does for additive materials.
14
+ * and multiplying particles (`FOG_MULTIPLY`) toward white, which leaves the
15
+ * frame as it is: what Unity does for additive and multiply materials.
15
16
  */
16
17
 
17
18
  /** Vertex-side declaration, GLSL. */
@@ -44,7 +45,8 @@ float quarksFogFactor() {
44
45
  float fogDensity = vFogInfos.w;
45
46
  float fogDistance = length(vFogDistance);
46
47
  if (vFogInfos.x == 3.0) {
47
- fogCoeff = (fogEnd - fogDistance) / (fogEnd - fogStart);
48
+ // A zero-width band is a cut at its distance, not a division by zero.
49
+ fogCoeff = (fogEnd - fogDistance) / max(fogEnd - fogStart, 1e-6);
48
50
  } else if (vFogInfos.x == 1.0) {
49
51
  fogCoeff = exp(-fogDistance * fogDensity);
50
52
  } else if (vFogInfos.x == 2.0) {
@@ -61,8 +63,10 @@ export function fogFragmentGLSL(rgb: string): string {
61
63
  #ifdef FOG
62
64
  {
63
65
  float fogFactor = quarksFogFactor();
64
- #ifdef FOG_ADDITIVE
66
+ #if defined(FOG_ADDITIVE)
65
67
  ${rgb} *= fogFactor;
68
+ #elif defined(FOG_MULTIPLY)
69
+ ${rgb} = mix(vec3(1.0), ${rgb}, fogFactor);
66
70
  #else
67
71
  ${rgb} = mix(vFogColor, ${rgb}, fogFactor);
68
72
  #endif
@@ -101,15 +105,18 @@ fn quarksFog(rgb: vec3f, fogDistanceVector: vec3f) -> vec3f {
101
105
  let fogDensity = uniforms.vFogInfos.w;
102
106
  let fogDistance = length(fogDistanceVector);
103
107
  if (uniforms.vFogInfos.x == 3.0) {
104
- fogCoeff = (fogEnd - fogDistance) / (fogEnd - fogStart);
108
+ // A zero-width band is a cut at its distance, not a division by zero.
109
+ fogCoeff = (fogEnd - fogDistance) / max(fogEnd - fogStart, 1e-6);
105
110
  } else if (uniforms.vFogInfos.x == 1.0) {
106
111
  fogCoeff = exp(-fogDistance * fogDensity);
107
112
  } else if (uniforms.vFogInfos.x == 2.0) {
108
113
  fogCoeff = exp(-fogDistance * fogDistance * fogDensity * fogDensity);
109
114
  }
110
115
  let fogFactor = clamp(fogCoeff, 0.0, 1.0);
111
- #ifdef FOG_ADDITIVE
116
+ #if defined(FOG_ADDITIVE)
112
117
  return rgb * fogFactor;
118
+ #elif defined(FOG_MULTIPLY)
119
+ return mix(vec3f(1.0), rgb, fogFactor);
113
120
  #else
114
121
  return mix(uniforms.vFogColor, rgb, fogFactor);
115
122
  #endif
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The flipbook (sprite sheet) cell for the sprite, stretched and mesh vertex
3
+ * shaders, in both shader languages: Unity's Texture Sheet Animation, Unreal's
4
+ * SubUV.
5
+ *
6
+ * `uvTile` is the particle's frame, counted along the rows from the top-left
7
+ * cell; `tileCountX` and `tileCountY` are the sheet's columns and rows. The
8
+ * frame is wrapped into the sheet, so a frame past the last cell starts over
9
+ * and a negative one counts back from the end, in both languages alike (the
10
+ * WGSL `%` truncates where GLSL's `mod` floors, so the wrap is spelled out).
11
+ * Under `TILE_BLEND` the next frame, wrapped the same way, is passed with the
12
+ * fraction between the two, for the fragment to cross-fade (Unity's flipbook
13
+ * blending).
14
+ */
15
+
16
+ /**
17
+ * The cell of `frame` on a sheet of `columns` × `rows`, counted along the rows
18
+ * from the top-left cell and wrapped into the sheet, as the shaders count it.
19
+ * For a host that lays its cells out on the CPU, such as the trail.
20
+ */
21
+ export function sheetCell(frame: number, columns: number, rows: number): {column: number; row: number} {
22
+ const total = columns * rows;
23
+ const tile = wrap(Math.floor(frame), total);
24
+ return {column: wrap(tile, columns), row: Math.floor(tile / columns)};
25
+ }
26
+
27
+ /** A floored modulo, as GLSL's `mod`: never negative for a positive divisor. */
28
+ function wrap(value: number, divisor: number): number {
29
+ return value - divisor * Math.floor(value / divisor);
30
+ }
31
+
32
+ /** Vertex-side declarations, GLSL: the sheet's size and the cell varyings. */
33
+ export const sheetVertexDeclarationGLSL = /* glsl */ `
34
+ #ifdef UV_TILE
35
+ uniform float tileCountX;
36
+ uniform float tileCountY;
37
+ #endif
38
+ #ifdef TILE_BLEND
39
+ varying vec2 vUV2;
40
+ varying float vTileBlend;
41
+ #endif
42
+ `;
43
+
44
+ /**
45
+ * Writes `vUV` (and the blend varyings) from the quad UV expression `uv` and the
46
+ * frame `uvTile`, GLSL.
47
+ */
48
+ export function sheetVertexGLSL(uv: string): string {
49
+ return /* glsl */ `
50
+ #ifdef UV_TILE
51
+ {
52
+ vec2 tc = vec2(tileCountX, tileCountY);
53
+ float tileTotal = tc.x * tc.y;
54
+ float baseTile = mod(floor(uvTile), tileTotal);
55
+ vec2 tileUV = vec2(mod(baseTile, tc.x), tc.y - 1.0 - floor(baseTile / tc.x)) / tc;
56
+ vUV = ${uv} / tc + tileUV;
57
+ #ifdef TILE_BLEND
58
+ float nextTile = mod(baseTile + 1.0, tileTotal);
59
+ vec2 nextUV = vec2(mod(nextTile, tc.x), tc.y - 1.0 - floor(nextTile / tc.x)) / tc;
60
+ vUV2 = ${uv} / tc + nextUV;
61
+ vTileBlend = fract(uvTile);
62
+ #endif
63
+ }
64
+ #else
65
+ vUV = ${uv};
66
+ #endif
67
+ `;
68
+ }
69
+
70
+ /** Vertex-side declarations, WGSL: the sheet's size and the cell varyings. */
71
+ export const sheetVertexDeclarationWGSL = /* wgsl */ `
72
+ #ifdef UV_TILE
73
+ uniform tileCountX: f32;
74
+ uniform tileCountY: f32;
75
+
76
+ // A floored modulo, as GLSL's mod: never negative for a positive divisor.
77
+ fn quarksWrap(value: f32, divisor: f32) -> f32 {
78
+ return value - divisor * floor(value / divisor);
79
+ }
80
+ #endif
81
+ #ifdef TILE_BLEND
82
+ varying vUV2: vec2f;
83
+ varying vTileBlend: f32;
84
+ #endif
85
+ `;
86
+
87
+ /**
88
+ * Writes `vertexOutputs.vUV` (and the blend varyings) from the quad UV
89
+ * expression `uv` and the frame `vertexInputs.uvTile`, WGSL.
90
+ */
91
+ export function sheetVertexWGSL(uv: string): string {
92
+ return /* wgsl */ `
93
+ #ifdef UV_TILE
94
+ {
95
+ let tc = vec2f(uniforms.tileCountX, uniforms.tileCountY);
96
+ let tileTotal = tc.x * tc.y;
97
+ let baseTile = quarksWrap(floor(vertexInputs.uvTile), tileTotal);
98
+ let tileUV = vec2f(quarksWrap(baseTile, tc.x), tc.y - 1.0 - floor(baseTile / tc.x)) / tc;
99
+ vertexOutputs.vUV = ${uv} / tc + tileUV;
100
+ #ifdef TILE_BLEND
101
+ let nextTile = quarksWrap(baseTile + 1.0, tileTotal);
102
+ let nextUV = vec2f(quarksWrap(nextTile, tc.x), tc.y - 1.0 - floor(nextTile / tc.x)) / tc;
103
+ vertexOutputs.vUV2 = ${uv} / tc + nextUV;
104
+ vertexOutputs.vTileBlend = fract(vertexInputs.uvTile);
105
+ #endif
106
+ }
107
+ #else
108
+ vertexOutputs.vUV = ${uv};
109
+ #endif
110
+ `;
111
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Soft particles for the particle shaders, in both shader languages.
3
+ *
4
+ * The fragment reads the scene depth behind it from `depthTexture`, turns it into view depth with
5
+ * the coefficients in `projParams` (see `softDepthParams`), and fades out over the distance in
6
+ * `softParams` as it nears the scene, as Unity's soft particles and Unreal's Depth Fade do. The
7
+ * fade lowers alpha only and runs after fog and before the blend resolve (`blend.ts`), so it reads
8
+ * as coverage in every blend mode and both languages produce the same colour.
9
+ *
10
+ * `SOFT_DEPTH_PACKED` reads a depth packed into four 8-bit channels, as a Babylon
11
+ * `DepthRenderer` of type `UNSIGNED_BYTE` writes it.
12
+ */
13
+
14
+ /** Fragment-side declaration and depth decode, GLSL. */
15
+ export const softFragmentDeclarationGLSL = /* glsl */ `
16
+ #ifdef SOFT_PARTICLES
17
+ uniform sampler2D depthTexture;
18
+ uniform vec2 softParams;
19
+ uniform vec4 projParams;
20
+ varying vec4 projPosition;
21
+ varying float linearDepth;
22
+
23
+ float quarksSceneDepth(vec4 texel) {
24
+ #ifdef SOFT_DEPTH_PACKED
25
+ float d = dot(texel, vec4(1.0 / (255.0 * 255.0 * 255.0), 1.0 / (255.0 * 255.0), 1.0 / 255.0, 1.0));
26
+ #else
27
+ float d = texel.r;
28
+ #endif
29
+ float denominator = projParams.z * d + projParams.w;
30
+ float depth = abs(denominator) > 1e-8 ? abs((projParams.x * d + projParams.y) / denominator) : 0.0;
31
+ // Nothing drawn there reads as infinitely far.
32
+ return depth > 0.0 ? depth : 1e30;
33
+ }
34
+ #endif
35
+ `;
36
+
37
+ /** Fades the alpha of the vec4 lvalue `target` as the fragment nears the scene behind it, GLSL. */
38
+ export function softFragmentGLSL(target: string): string {
39
+ return /* glsl */ `
40
+ #ifdef SOFT_PARTICLES
41
+ {
42
+ vec2 softUV = 0.5 * (projPosition.xy / projPosition.w) + 0.5;
43
+ float sceneDepth = quarksSceneDepth(texture2D(depthTexture, softUV));
44
+ ${target}.a *= clamp(softParams.y * ((sceneDepth - softParams.x) - linearDepth), 0.0, 1.0);
45
+ }
46
+ #endif
47
+ `;
48
+ }
49
+
50
+ /** Fragment-side declaration and depth decode, WGSL. */
51
+ export const softFragmentDeclarationWGSL = /* wgsl */ `
52
+ #ifdef SOFT_PARTICLES
53
+ var depthTextureSampler: sampler;
54
+ var depthTexture: texture_2d<f32>;
55
+ uniform softParams: vec2f;
56
+ uniform projParams: vec4f;
57
+ varying projPosition: vec4f;
58
+ varying linearDepth: f32;
59
+
60
+ fn quarksSceneDepth(texel: vec4f) -> f32 {
61
+ #ifdef SOFT_DEPTH_PACKED
62
+ let d = dot(texel, vec4f(1.0 / (255.0 * 255.0 * 255.0), 1.0 / (255.0 * 255.0), 1.0 / 255.0, 1.0));
63
+ #else
64
+ let d = texel.r;
65
+ #endif
66
+ let denominator = uniforms.projParams.z * d + uniforms.projParams.w;
67
+ let depth = select(0.0, abs((uniforms.projParams.x * d + uniforms.projParams.y) / denominator), abs(denominator) > 1e-8);
68
+ // Nothing drawn there reads as infinitely far.
69
+ return select(1e30, depth, depth > 0.0);
70
+ }
71
+ #endif
72
+ `;
73
+
74
+ /** Fades the alpha of the `var` vec4 `target` as the fragment nears the scene behind it, WGSL. */
75
+ export function softFragmentWGSL(target: string): string {
76
+ return /* wgsl */ `
77
+ #ifdef SOFT_PARTICLES
78
+ {
79
+ let softUV = 0.5 * (fragmentInputs.projPosition.xy / fragmentInputs.projPosition.w) + 0.5;
80
+ let sceneDepth = quarksSceneDepth(textureSample(depthTexture, depthTextureSampler, softUV));
81
+ ${target}.a *= clamp(uniforms.softParams.y * ((sceneDepth - uniforms.softParams.x) - fragmentInputs.linearDepth), 0.0, 1.0);
82
+ }
83
+ #endif
84
+ `;
85
+ }
@@ -35,8 +35,8 @@
35
35
  * on the way down the shape eats away from its lowest mask values first — a
36
36
  * noise mask dissolves it, a gradient mask wipes it. `erosionParams` is
37
37
  * (softness, edge width); the edge band just above the cut glows in
38
- * `erosionEdgeColor`. Erosion only changes alpha, so blend modes that
39
- * ignore alpha (one-one, premultiplied) show a hard cut.
38
+ * `erosionEdgeColor`. Erosion only changes alpha, which the blend resolve
39
+ * (`blend.ts`) turns into coverage in every blend mode.
40
40
  */
41
41
 
42
42
  /** Fragment-side declarations, GLSL. */
@@ -108,6 +108,7 @@ export function surfaceColorGLSL(tileBlend: boolean): string {
108
108
  baseColor.a = 1.0;
109
109
  float erosionMaskValue = 1.0;
110
110
  #endif
111
+ #if defined(USE_MAP) || defined(GRADIENT_MAP)
111
112
  #ifdef USE_MAP
112
113
  vec4 texColor = texture2D(map, mapUV);
113
114
  ${
@@ -130,6 +131,10 @@ export function surfaceColorGLSL(tileBlend: boolean): string {
130
131
  #endif`
131
132
  : ''
132
133
  }
134
+ #else
135
+ // No map: the ramp is looked up at white, its far end.
136
+ vec4 texColor = vec4(1.0);
137
+ #endif
133
138
  #ifdef GRADIENT_MAP
134
139
  float rampAt = clamp(dot(texColor, gradientMapChannel), 0.0, 1.0);
135
140
  vec4 ramp = texture2D(gradientMap, vec2(mix(gradientMapRange.x, gradientMapRange.y, rampAt), 0.5));
@@ -221,6 +226,7 @@ export function surfaceColorWGSL(tileBlend: boolean): string {
221
226
  baseColor.a = 1.0;
222
227
  var erosionMaskValue = 1.0;
223
228
  #endif
229
+ #if defined(USE_MAP) || defined(GRADIENT_MAP)
224
230
  #ifdef USE_MAP
225
231
  var texColor = textureSample(map, mapSampler, mapUV);
226
232
  ${
@@ -243,6 +249,10 @@ export function surfaceColorWGSL(tileBlend: boolean): string {
243
249
  #endif`
244
250
  : ''
245
251
  }
252
+ #else
253
+ // No map: the ramp is looked up at white, its far end.
254
+ var texColor = vec4f(1.0);
255
+ #endif
246
256
  #ifdef GRADIENT_MAP
247
257
  let rampAt = clamp(dot(texColor, uniforms.gradientMapChannel), 0.0, 1.0);
248
258
  let ramp = textureSample(gradientMap, gradientMapSampler, vec2f(mix(uniforms.gradientMapRange.x, uniforms.gradientMapRange.y, rampAt), 0.5));
@@ -1,4 +1,5 @@
1
1
  import {fogVertexDeclarationGLSL, fogVertexGLSL} from './chunks/fog';
2
+ import {sheetVertexDeclarationGLSL, sheetVertexGLSL} from './chunks/sheet';
2
3
 
3
4
  export default /* glsl */ `
4
5
  attribute vec3 position;
@@ -13,11 +14,7 @@ attribute float uvTile;
13
14
  uniform mat4 world;
14
15
  uniform mat4 view;
15
16
  uniform mat4 projection;
16
-
17
- #ifdef UV_TILE
18
- uniform float tileCountX;
19
- uniform float tileCountY;
20
- #endif
17
+ ${sheetVertexDeclarationGLSL}
21
18
 
22
19
  varying vec2 vUV;
23
20
  varying vec4 vColor;
@@ -32,11 +29,6 @@ varying vec2 vUV1;
32
29
  varying vec3 vNormal;
33
30
  varying vec3 vWorldPos;
34
31
 
35
- #ifdef TILE_BLEND
36
- varying vec2 vUV2;
37
- varying float vTileBlend;
38
- #endif
39
-
40
32
  #ifdef SOFT_PARTICLES
41
33
  varying vec4 projPosition;
42
34
  varying float linearDepth;
@@ -53,7 +45,15 @@ ${fogVertexDeclarationGLSL}
53
45
  void main() {
54
46
  vec3 scaledPosition = position * size;
55
47
  vec3 rotatedPosition = applyQuaternion(scaledPosition, rotation);
56
- vec3 rotatedNormal = normalize(applyQuaternion(normal, rotation));
48
+ // A normal scales by the inverse of the size, so a stretched mesh keeps its normals
49
+ // perpendicular; a flat size counts as a sliver rather than dividing by zero.
50
+ vec3 safeSize = vec3(
51
+ abs(size.x) > 1e-6 ? size.x : 1e-6,
52
+ abs(size.y) > 1e-6 ? size.y : 1e-6,
53
+ abs(size.z) > 1e-6 ? size.z : 1e-6);
54
+ // A mirrored size (an odd number of negative components) turns the winding inside out, so the
55
+ // normal is turned with it and the fragment's front-face test agrees with the geometry.
56
+ vec3 rotatedNormal = normalize(applyQuaternion(normal / safeSize, rotation)) * sign(safeSize.x * safeSize.y * safeSize.z);
57
57
  vec3 localPos = rotatedPosition + offset;
58
58
 
59
59
  vec4 worldPos = world * vec4(localPos, 1.0);
@@ -66,25 +66,11 @@ ${fogVertexGLSL('viewPos.xyz')}
66
66
 
67
67
  #ifdef SOFT_PARTICLES
68
68
  projPosition = gl_Position;
69
- linearDepth = -viewPos.z;
69
+ // View depth is +z ahead of a left-handed camera and -z ahead of a right-handed one.
70
+ linearDepth = abs(viewPos.z);
70
71
  #endif
71
72
 
72
- #ifdef UV_TILE
73
- vec2 tc = vec2(tileCountX, tileCountY);
74
- float baseTile = floor(uvTile);
75
- float tileU = mod(baseTile, tc.x) / tc.x;
76
- float tileV = 1.0 - floor(baseTile / tc.x) / tc.y - 1.0 / tc.y;
77
- vUV = uv / tc + vec2(tileU, tileV);
78
- #ifdef TILE_BLEND
79
- float nextTile = ceil(uvTile);
80
- float nextU = mod(nextTile, tc.x) / tc.x;
81
- float nextV = 1.0 - floor(nextTile / tc.x) / tc.y - 1.0 / tc.y;
82
- vUV2 = uv / tc + vec2(nextU, nextV);
83
- vTileBlend = fract(uvTile);
84
- #endif
85
- #else
86
- vUV = uv;
87
- #endif
73
+ ${sheetVertexGLSL('uv')}
88
74
 
89
75
  #ifdef UV1
90
76
  #ifdef MESH_UV2