babylon.quarks 0.20.0 → 0.23.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/src/TrailBatch.ts CHANGED
@@ -56,6 +56,12 @@ export class TrailBatch extends VFXBatch {
56
56
  this.widthBuffer = new Float32Array(this.maxParticles * 2);
57
57
  this.colorBuffer = new Float32Array(this.maxParticles * 8);
58
58
  this.indexBuffer = new Uint32Array(this.maxParticles * 6);
59
+ // Vertices come in pairs, one per side of the ribbon, so the side never
60
+ // changes: written once here and never uploaded again.
61
+ for (let i = 0; i < this.sideBuffer.length; i += 2) {
62
+ this.sideBuffer[i] = 1;
63
+ this.sideBuffer[i + 1] = -1;
64
+ }
59
65
 
60
66
  // Initialize with full-capacity buffers (updatable) and a dummy triangle
61
67
  this.positionBuffer[0] = 0;
@@ -216,10 +222,21 @@ export class TrailBatch extends VFXBatch {
216
222
  private vector3_ = new Vector3();
217
223
  private quaternion_ = new Quaternion();
218
224
 
219
- /** Forces the emitter's world matrix to be current before sampling it. */
225
+ /**
226
+ * Brings the emitter's world matrix up to date when its transform changed.
227
+ *
228
+ * Babylon only recomputes a world matrix once per render id, so a frame that
229
+ * moves the emitter after that (or a step driven without rendering) would
230
+ * draw the ribbon against the old one. Recomputing unconditionally every
231
+ * frame costs a matrix compose per system; checking the transform cache
232
+ * first makes a still emitter free.
233
+ */
220
234
  private static ensureWorldMatrix(system: IParticleSystem): void {
221
- const emitter = system.emitter as {computeWorldMatrix?: (force: boolean) => void};
222
- if (emitter.computeWorldMatrix) {
235
+ const emitter = system.emitter as {
236
+ computeWorldMatrix?: (force: boolean) => void;
237
+ isSynchronized?: () => boolean;
238
+ };
239
+ if (emitter.computeWorldMatrix && (!emitter.isSynchronized || !emitter.isSynchronized())) {
223
240
  emitter.computeWorldMatrix(true);
224
241
  }
225
242
  }
@@ -327,31 +344,46 @@ export class TrailBatch extends VFXBatch {
327
344
  1 / (stepFraction !== 0 ? particleHistoryLength : Math.max(1, particleHistoryLength - 1));
328
345
 
329
346
  // Walk the ring buffer oldest-first with hand-rolled wrapping so the
330
- // inner loop stays free of modulo operations.
347
+ // inner loop stays free of modulo operations. Each sample is brought
348
+ // into world space once: the point written as `next` on one
349
+ // iteration is `current` on the following one and `previous` on
350
+ // the one after that.
331
351
  let currentSlot = particle.getHistoryIndex(0);
332
- let previousSlot = currentSlot;
352
+ let currentX = historyPositions[currentSlot * 3];
353
+ let currentY = historyPositions[currentSlot * 3 + 1];
354
+ let currentZ = historyPositions[currentSlot * 3 + 2];
355
+ if (!samplesInWorld) {
356
+ const w = 1 / (m03 * currentX + m13 * currentY + m23 * currentZ + m33);
357
+ const tx = (m00 * currentX + m10 * currentY + m20 * currentZ + m30) * w;
358
+ const ty = (m01 * currentX + m11 * currentY + m21 * currentZ + m31) * w;
359
+ currentZ = (m02 * currentX + m12 * currentY + m22 * currentZ + m32) * w;
360
+ currentX = tx;
361
+ currentY = ty;
362
+ }
363
+ let previousX = currentX;
364
+ let previousY = currentY;
365
+ let previousZ = currentZ;
333
366
 
334
367
  for (let i = 0; i < particleHistoryLength; i++, index += 2) {
335
- const nextSlot =
336
- i + 1 < particleHistoryLength
337
- ? currentSlot + 1 === historyCapacity
338
- ? 0
339
- : currentSlot + 1
340
- : currentSlot;
341
-
342
- const currentPosIndex = currentSlot * 3;
343
- const previousPosIndex = previousSlot * 3;
344
- const nextPosIndex = nextSlot * 3;
345
-
346
- let currentX = historyPositions[currentPosIndex];
347
- let currentY = historyPositions[currentPosIndex + 1];
348
- let currentZ = historyPositions[currentPosIndex + 2];
349
- let previousX = historyPositions[previousPosIndex];
350
- let previousY = historyPositions[previousPosIndex + 1];
351
- let previousZ = historyPositions[previousPosIndex + 2];
352
- let nextX = historyPositions[nextPosIndex];
353
- let nextY = historyPositions[nextPosIndex + 1];
354
- let nextZ = historyPositions[nextPosIndex + 2];
368
+ let nextX = currentX;
369
+ let nextY = currentY;
370
+ let nextZ = currentZ;
371
+ let nextSlot = currentSlot;
372
+ if (i + 1 < particleHistoryLength) {
373
+ nextSlot = currentSlot + 1 === historyCapacity ? 0 : currentSlot + 1;
374
+ const nextPosIndex = nextSlot * 3;
375
+ nextX = historyPositions[nextPosIndex];
376
+ nextY = historyPositions[nextPosIndex + 1];
377
+ nextZ = historyPositions[nextPosIndex + 2];
378
+ if (!samplesInWorld) {
379
+ const w = 1 / (m03 * nextX + m13 * nextY + m23 * nextZ + m33);
380
+ const tx = (m00 * nextX + m10 * nextY + m20 * nextZ + m30) * w;
381
+ const ty = (m01 * nextX + m11 * nextY + m21 * nextZ + m31) * w;
382
+ nextZ = (m02 * nextX + m12 * nextY + m22 * nextZ + m32) * w;
383
+ nextX = tx;
384
+ nextY = ty;
385
+ }
386
+ }
355
387
 
356
388
  const currentSize = historySizes[currentSlot];
357
389
  const currentColorIndex = colorFromParticle ? particleColorIndex : currentSlot * 4;
@@ -360,33 +392,6 @@ export class TrailBatch extends VFXBatch {
360
392
  const currentColorZ = colors[currentColorIndex + 2];
361
393
  const currentColorW = colors[currentColorIndex + 3];
362
394
 
363
- previousSlot = currentSlot;
364
- currentSlot = nextSlot;
365
-
366
- if (!samplesInWorld) {
367
- // Inlined point transforms — three per trail sample.
368
- let w = 1 / (m03 * currentX + m13 * currentY + m23 * currentZ + m33);
369
- let tx = (m00 * currentX + m10 * currentY + m20 * currentZ + m30) * w;
370
- let ty = (m01 * currentX + m11 * currentY + m21 * currentZ + m31) * w;
371
- currentZ = (m02 * currentX + m12 * currentY + m22 * currentZ + m32) * w;
372
- currentX = tx;
373
- currentY = ty;
374
-
375
- w = 1 / (m03 * previousX + m13 * previousY + m23 * previousZ + m33);
376
- tx = (m00 * previousX + m10 * previousY + m20 * previousZ + m30) * w;
377
- ty = (m01 * previousX + m11 * previousY + m21 * previousZ + m31) * w;
378
- previousZ = (m02 * previousX + m12 * previousY + m22 * previousZ + m32) * w;
379
- previousX = tx;
380
- previousY = ty;
381
-
382
- w = 1 / (m03 * nextX + m13 * nextY + m23 * nextZ + m33);
383
- tx = (m00 * nextX + m10 * nextY + m20 * nextZ + m30) * w;
384
- ty = (m01 * nextX + m11 * nextY + m21 * nextZ + m31) * w;
385
- nextZ = (m02 * nextX + m12 * nextY + m22 * nextZ + m32) * w;
386
- nextX = tx;
387
- nextY = ty;
388
- }
389
-
390
395
  const pi = index * 3;
391
396
  this.positionBuffer[pi] = currentX;
392
397
  this.positionBuffer[pi + 1] = currentY;
@@ -409,9 +414,6 @@ export class TrailBatch extends VFXBatch {
409
414
  this.nextBuffer[pi + 4] = nextY;
410
415
  this.nextBuffer[pi + 5] = nextZ;
411
416
 
412
- this.sideBuffer[index] = 1;
413
- this.sideBuffer[index + 1] = -1;
414
-
415
417
  if (systemWorldSpace || particle.parentMatrix) {
416
418
  this.widthBuffer[index] = currentSize;
417
419
  this.widthBuffer[index + 1] = currentSize;
@@ -448,6 +450,14 @@ export class TrailBatch extends VFXBatch {
448
450
  this.indexBuffer[triangles * 3 + 2] = index + 3;
449
451
  triangles++;
450
452
  }
453
+
454
+ previousX = currentX;
455
+ previousY = currentY;
456
+ previousZ = currentZ;
457
+ currentX = nextX;
458
+ currentY = nextY;
459
+ currentZ = nextZ;
460
+ currentSlot = nextSlot;
451
461
  }
452
462
 
453
463
  // Live tip: same place a sprite would be drawn this frame, past
@@ -505,9 +515,6 @@ export class TrailBatch extends VFXBatch {
505
515
  this.nextBuffer[headPi + 4] = tipY;
506
516
  this.nextBuffer[headPi + 5] = tipZ;
507
517
 
508
- this.sideBuffer[index] = 1;
509
- this.sideBuffer[index + 1] = -1;
510
-
511
518
  const tipSize = particle.size.x;
512
519
  if (systemWorldSpace || particle.parentMatrix) {
513
520
  this.widthBuffer[index] = tipSize;
@@ -560,22 +567,30 @@ export class TrailBatch extends VFXBatch {
560
567
  * costs that loop its optimised code.
561
568
  */
562
569
  private uploadGeometry(index: number, triangles: number): void {
570
+ const geometry = this.mesh.geometry;
571
+ const indexBuffer = geometry?.getIndexBuffer();
563
572
  if (index > 0 && triangles > 0) {
564
573
  // Upload only the vertices written this frame; the buffers are sized for
565
574
  // the high-water mark of every system in the batch.
566
575
  this.mesh.updateVerticesData(VertexBuffer.PositionKind, this.positionBuffer.subarray(0, index * 3));
567
576
  this.mesh.updateVerticesData(VertexBuffer.UVKind, this.uvBuffer.subarray(0, index * 2));
568
- this.mesh.updateIndices(this.indexBuffer.subarray(0, triangles * 3));
577
+ // Straight to the GPU buffer. Going through `mesh.updateIndices` with a
578
+ // range whose length changes every frame makes Babylon copy the
579
+ // indices, rebuild the sub mesh and recompute its bounds from the
580
+ // whole position buffer, all of which this batch then undoes.
581
+ if (indexBuffer) {
582
+ this.scene
583
+ .getEngine()
584
+ .updateDynamicIndexBuffer(indexBuffer, this.indexBuffer.subarray(0, triangles * 3), 0);
585
+ }
569
586
  this.previousVB.update(this.previousBuffer.subarray(0, index * 3));
570
587
  this.nextVB.update(this.nextBuffer.subarray(0, index * 3));
571
- this.sideVB.update(this.sideBuffer.subarray(0, index));
572
588
  this.widthVB.update(this.widthBuffer.subarray(0, index));
573
589
 
574
590
  const colorVB = this.mesh.getVertexBuffer(VertexBuffer.ColorKind);
575
591
  if (colorVB) colorVB.update(this.colorBuffer.subarray(0, index * 4));
576
592
 
577
593
  if (this.mesh.subMeshes && this.mesh.subMeshes.length > 0) {
578
- this.adoptMeshBounds();
579
594
  this.mesh.subMeshes[0].indexCount = triangles * 3;
580
595
  this.mesh.subMeshes[0].verticesCount = index;
581
596
  }
@@ -593,8 +608,9 @@ export class TrailBatch extends VFXBatch {
593
608
  // every empty frame.
594
609
  if (!this.drawsNothing) {
595
610
  this.indexBuffer.fill(0, 0, 6);
596
- this.mesh.updateIndices(this.indexBuffer.subarray(0, 6));
597
- this.adoptMeshBounds();
611
+ if (indexBuffer) {
612
+ this.scene.getEngine().updateDynamicIndexBuffer(indexBuffer, this.indexBuffer.subarray(0, 6), 0);
613
+ }
598
614
  this.drawsNothing = true;
599
615
  }
600
616
  this.mesh.subMeshes[0].indexCount = 6;
package/src/VFXBatch.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
2
2
  import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
3
+ import {Color3} from '@babylonjs/core/Maths/math.color';
4
+ import {Vector3 as BVector3} from '@babylonjs/core/Maths/math.vector';
3
5
  import {Mesh} from '@babylonjs/core/Meshes/mesh';
4
6
  import {Scene} from '@babylonjs/core/scene';
5
7
  import {IParticleSystem, Vector3} from 'quarks.core';
@@ -14,6 +16,96 @@ export enum RenderMode {
14
16
  VerticalBillBoard = 5,
15
17
  }
16
18
 
19
+ /**
20
+ * How mesh particles are lit.
21
+ *
22
+ * - `'fixed'` (default): one directional light plus ambient, the same for every
23
+ * mesh batch of a renderer, set with {@link BatchedRenderer.setMeshLighting}.
24
+ * - `'unlit'`: no lighting term — the texture times the particle colour, for
25
+ * emissive meshes (fire, energy, glowing blades).
26
+ * - `'scene'`: follows the scene's first enabled `DirectionalLight` or
27
+ * `HemisphericLight` every frame, falling back to the fixed light when there
28
+ * is none.
29
+ *
30
+ * Only mesh particles are lit; other render modes ignore it.
31
+ */
32
+ export type MeshLighting = 'unlit' | 'fixed' | 'scene';
33
+
34
+ /** The light {@link MeshLighting} `'fixed'` mesh particles are lit by. */
35
+ export interface MeshLightingSettings {
36
+ /** Direction the light travels in, world space. Default `(0.4, -1, 0.6)`. */
37
+ direction: BVector3;
38
+ /** Light colour, intensity folded in. Default white. */
39
+ color: Color3;
40
+ /** Light every face gets regardless of its direction. Default `(0.35, 0.35, 0.35)`. */
41
+ ambient: Color3;
42
+ }
43
+
44
+ /** The light mesh particles have always been lit by. */
45
+ export function defaultMeshLighting(): MeshLightingSettings {
46
+ return {
47
+ direction: new BVector3(0.4, -1, 0.6),
48
+ color: new Color3(1, 1, 1),
49
+ ambient: new Color3(0.35, 0.35, 0.35),
50
+ };
51
+ }
52
+
53
+ /**
54
+ * A rim (Fresnel) term on mesh particles: a colour that grows where the surface
55
+ * turns away from the camera, `pow(1 - |N·V|, power) * strength`. Unset: no rim.
56
+ * Only mesh particles draw it.
57
+ */
58
+ export interface MeshRim {
59
+ /** Edge colour, linear RGB. Default `[1, 1, 1]`. */
60
+ color: [number, number, number];
61
+ /** Falloff exponent; higher keeps the rim to a thinner edge. Default 2. */
62
+ power: number;
63
+ /** How much of the rim is applied, usually 0…1. Default 1. */
64
+ strength: number;
65
+ /**
66
+ * `'add'` (default) adds the colour, for a glowing edge; `'mix'` blends the
67
+ * surface toward it, for an outline (a dark ink line with a dark colour).
68
+ */
69
+ mode: 'add' | 'mix';
70
+ }
71
+
72
+ /**
73
+ * A complete rim from a partial one, every field defaulted and kept in range,
74
+ * or undefined when `rim` is unset.
75
+ */
76
+ export function normalizeMeshRim(rim: Partial<MeshRim> | null | undefined): MeshRim | undefined {
77
+ if (!rim || typeof rim !== 'object') {
78
+ return undefined;
79
+ }
80
+ const color = Array.isArray(rim.color) ? rim.color : [1, 1, 1];
81
+ const channel = (value: unknown, fallback: number) =>
82
+ typeof value === 'number' && Number.isFinite(value) ? value : fallback;
83
+ return {
84
+ color: [channel(color[0], 1), channel(color[1], 1), channel(color[2], 1)],
85
+ power: Math.max(0.01, channel(rim.power, 2)),
86
+ strength: Math.max(0, channel(rim.strength, 1)),
87
+ mode: rim.mode === 'mix' ? 'mix' : 'add',
88
+ };
89
+ }
90
+
91
+ /** True when both describe the same rim (or both none). */
92
+ export function meshRimEqual(a: MeshRim | undefined, b: MeshRim | undefined): boolean {
93
+ if (a === b) {
94
+ return true;
95
+ }
96
+ if (!a || !b) {
97
+ return false;
98
+ }
99
+ return (
100
+ a.mode === b.mode &&
101
+ a.power === b.power &&
102
+ a.strength === b.strength &&
103
+ a.color[0] === b.color[0] &&
104
+ a.color[1] === b.color[1] &&
105
+ a.color[2] === b.color[2]
106
+ );
107
+ }
108
+
17
109
  export interface StoredBatchSettings {
18
110
  instancingGeometry: Float32Array;
19
111
  instancingIndices: Uint32Array | Uint16Array;
@@ -40,6 +132,10 @@ export interface StoredBatchSettings {
40
132
  layerMask: number;
41
133
  /** Billboard pivot in particle-size units, see {@link VFXBatchSettings.pivot}. */
42
134
  pivot?: Vector3;
135
+ /** Mesh particle lighting, see {@link VFXBatchSettings.lighting}. */
136
+ lighting?: MeshLighting;
137
+ /** Mesh particle rim, see {@link VFXBatchSettings.rim}. */
138
+ rim?: MeshRim;
43
139
  }
44
140
 
45
141
  export abstract class VFXBatch {
@@ -79,6 +175,8 @@ export abstract class VFXBatch {
79
175
  reflectionAtlas: settings.reflectionAtlas ?? null,
80
176
  pivot: settings.pivot?.clone(),
81
177
  layerMask: settings.layerMask,
178
+ lighting: settings.lighting,
179
+ rim: normalizeMeshRim(settings.rim),
82
180
  };
83
181
  this.mesh = new Mesh('vfxBatch', scene);
84
182
  this.mesh.alwaysSelectAsActiveMesh = true;
@@ -116,6 +214,9 @@ export abstract class VFXBatch {
116
214
  }
117
215
  }
118
216
 
217
+ /** Sets the light `'fixed'` mesh particles are lit by. Batches that draw no meshes ignore it. */
218
+ applyMeshLighting(_lighting: MeshLightingSettings): void {}
219
+
119
220
  abstract setupBuffers(): void;
120
221
  abstract expandBuffers(target: number): void;
121
222
  abstract rebuildMaterial(): void;
package/src/index.ts CHANGED
@@ -25,15 +25,21 @@ export type {
25
25
  ParticleSystemParameters,
26
26
  } from './ParticleSystem';
27
27
  export {QuarksLoader} from './QuarksLoader';
28
- export type {QuarksLoaderOptions} from './QuarksLoader';
28
+ export type {
29
+ QuarksBatchLoadOptions,
30
+ QuarksJSONLoadEntry,
31
+ QuarksLoaderOptions,
32
+ QuarksResourceKind,
33
+ QuarksResourceResolver,
34
+ } from './QuarksLoader';
29
35
  export {QuarksPrefab} from './QuarksPrefab';
30
36
  export type {AnimationData, QuarksTimelineClip} from './QuarksPrefab';
31
37
  export {QuarksUtil} from './QuarksUtil';
32
38
  export * from './shaders/';
33
39
  export {SpriteBatch} from './SpriteBatch';
34
40
  export {TrailBatch} from './TrailBatch';
35
- export {RenderMode, VFXBatch} from './VFXBatch';
36
- export type {StoredBatchSettings} from './VFXBatch';
41
+ export {defaultMeshLighting, meshRimEqual, normalizeMeshRim, RenderMode, VFXBatch} from './VFXBatch';
42
+ export type {MeshLighting, MeshLightingSettings, MeshRim, StoredBatchSettings} from './VFXBatch';
37
43
 
38
44
  // Re-export quarks.core types (interfaces - type-only)
39
45
  export type {
@@ -4,9 +4,11 @@ varying vec4 vColor;
4
4
  varying vec3 vNormal;
5
5
  varying vec3 vWorldPos;
6
6
 
7
+ #ifndef MESH_UNLIT
7
8
  uniform vec3 lightDirection;
8
9
  uniform vec3 lightColor;
9
10
  uniform vec3 ambientColor;
11
+ #endif
10
12
 
11
13
  #ifdef TILE_BLEND
12
14
  varying vec2 vUV2;
@@ -17,11 +19,19 @@ varying float vTileBlend;
17
19
  uniform sampler2D map;
18
20
  #endif
19
21
 
22
+ #if defined(USE_ENVMAP_ATLAS) || defined(MESH_RIM)
23
+ uniform vec3 eyePosition;
24
+ #endif
25
+
26
+ #ifdef MESH_RIM
27
+ uniform vec3 rimColor;
28
+ uniform vec2 rimParams;
29
+ #endif
30
+
20
31
  // One 3×2 atlas (px py pz / nx ny nz). Avoids samplerCube and multi-face
21
32
  // sampler bindings that raise GL_INVALID_OPERATION on iOS WebKit.
22
33
  #ifdef USE_ENVMAP_ATLAS
23
34
  uniform sampler2D envAtlas;
24
- uniform vec3 eyePosition;
25
35
  uniform float reflectionLevel;
26
36
 
27
37
  vec3 sampleEnvAtlas(vec3 r) {
@@ -94,11 +104,15 @@ void main() {
94
104
  #endif
95
105
 
96
106
  vec3 N = normalize(vNormal);
107
+ #ifdef MESH_UNLIT
108
+ vec3 color = baseColor.rgb;
109
+ #else
97
110
  vec3 L = normalize(-lightDirection);
98
111
  float NdotL = max(dot(N, L), 0.0);
99
112
  vec3 litColor = ambientColor + lightColor * NdotL;
100
113
 
101
114
  vec3 color = baseColor.rgb * litColor;
115
+ #endif
102
116
 
103
117
  #ifdef USE_ENVMAP_ATLAS
104
118
  vec3 viewDir = normalize(vWorldPos - eyePosition);
@@ -106,6 +120,17 @@ void main() {
106
120
  color += sampleEnvAtlas(reflectionCoords) * reflectionLevel;
107
121
  #endif
108
122
 
123
+ #ifdef MESH_RIM
124
+ // Fresnel: strongest where the surface turns away from the camera.
125
+ vec3 V = normalize(eyePosition - vWorldPos);
126
+ float rim = pow(1.0 - abs(dot(N, V)), rimParams.x) * rimParams.y;
127
+ #ifdef MESH_RIM_MIX
128
+ color = mix(color, rimColor, clamp(rim, 0.0, 1.0));
129
+ #else
130
+ color += rimColor * rim;
131
+ #endif
132
+ #endif
133
+
109
134
  gl_FragColor = vec4(color, baseColor.a);
110
135
 
111
136
  #ifdef SOFT_PARTICLES
@@ -4,9 +4,11 @@ varying vColor: vec4f;
4
4
  varying vNormal: vec3f;
5
5
  varying vWorldPos: vec3f;
6
6
 
7
+ #ifndef MESH_UNLIT
7
8
  uniform lightDirection: vec3f;
8
9
  uniform lightColor: vec3f;
9
10
  uniform ambientColor: vec3f;
11
+ #endif
10
12
 
11
13
  #ifdef TILE_BLEND
12
14
  varying vUV2: vec2f;
@@ -18,10 +20,18 @@ var mapSampler: sampler;
18
20
  var map: texture_2d<f32>;
19
21
  #endif
20
22
 
23
+ #if defined(USE_ENVMAP_ATLAS) || defined(MESH_RIM)
24
+ uniform eyePosition: vec3f;
25
+ #endif
26
+
27
+ #ifdef MESH_RIM
28
+ uniform rimColor: vec3f;
29
+ uniform rimParams: vec2f;
30
+ #endif
31
+
21
32
  #ifdef USE_ENVMAP_ATLAS
22
33
  var envAtlasSampler: sampler;
23
34
  var envAtlas: texture_2d<f32>;
24
- uniform eyePosition: vec3f;
25
35
  uniform reflectionLevel: f32;
26
36
 
27
37
  fn sampleEnvAtlas(r: vec3f) -> vec3f {
@@ -95,11 +105,15 @@ fn main(input: FragmentInputs) -> FragmentOutputs {
95
105
  #endif
96
106
 
97
107
  let N = normalize(fragmentInputs.vNormal);
108
+ #ifdef MESH_UNLIT
109
+ var color = baseColor.rgb;
110
+ #else
98
111
  let L = normalize(-uniforms.lightDirection);
99
112
  let NdotL = max(dot(N, L), 0.0);
100
113
  let litColor = uniforms.ambientColor + uniforms.lightColor * NdotL;
101
114
 
102
115
  var color = baseColor.rgb * litColor;
116
+ #endif
103
117
 
104
118
  #ifdef USE_ENVMAP_ATLAS
105
119
  let viewDir = normalize(fragmentInputs.vWorldPos - uniforms.eyePosition);
@@ -107,6 +121,17 @@ fn main(input: FragmentInputs) -> FragmentOutputs {
107
121
  color += sampleEnvAtlas(reflectionCoords) * uniforms.reflectionLevel;
108
122
  #endif
109
123
 
124
+ #ifdef MESH_RIM
125
+ // Fresnel: strongest where the surface turns away from the camera.
126
+ let V = normalize(uniforms.eyePosition - fragmentInputs.vWorldPos);
127
+ let rimTerm = pow(1.0 - abs(dot(N, V)), uniforms.rimParams.x) * uniforms.rimParams.y;
128
+ #ifdef MESH_RIM_MIX
129
+ color = mix(color, uniforms.rimColor, clamp(rimTerm, 0.0, 1.0));
130
+ #else
131
+ color += uniforms.rimColor * rimTerm;
132
+ #endif
133
+ #endif
134
+
110
135
  var finalColor = vec4f(color, baseColor.a);
111
136
 
112
137
  #ifdef SOFT_PARTICLES