@driftengine/core 4.13.3 → 4.13.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/core",
3
- "version": "4.13.3",
3
+ "version": "4.13.4",
4
4
  "description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@driftengine/drft": "4.13.3",
33
- "@driftengine/physics": "4.13.3",
32
+ "@driftengine/drft": "4.13.4",
33
+ "@driftengine/physics": "4.13.4",
34
34
  "gl-matrix": "^3.4.3"
35
35
  },
36
36
  "author": "Drift Technologies",
@@ -67,8 +67,8 @@
67
67
  "prepack": "node ../../scripts/build.mjs @driftengine/core"
68
68
  },
69
69
  "devDependencies": {
70
- "@driftengine/nav": "4.13.3",
71
- "@driftengine/network": "4.13.3",
72
- "@driftengine/texture": "4.13.3"
70
+ "@driftengine/nav": "4.13.4",
71
+ "@driftengine/network": "4.13.4",
72
+ "@driftengine/texture": "4.13.4"
73
73
  }
74
74
  }
@@ -34,6 +34,7 @@ const SECTIONS: readonly (readonly [number, number])[] = [
34
34
  [7, 3],
35
35
  [10, 3],
36
36
  [13, 16],
37
+ [29, 1],
37
38
  ];
38
39
  /** Where the influences start, and where the garment's index sits, in particle counts. */
39
40
  const INFLUENCES = 13;
@@ -20,7 +20,7 @@ import { batchesOf } from '@driftengine/physics';
20
20
  import type { ClothBatches, SkinnedClothSetup } from '@driftengine/physics';
21
21
 
22
22
  /** Floats of statics a particle, across its sections. */
23
- export const CLOTH_STATIC_FLOATS = 29;
23
+ export const CLOTH_STATIC_FLOATS = 30;
24
24
 
25
25
  /** Bit flags of the limits a set-up has: a max distance, a backstop, a frontstop. */
26
26
  export const CLOTH_LIMIT_MAX = 1;
@@ -29,7 +29,10 @@ export const CLOTH_LIMIT_FRONT = 4;
29
29
 
30
30
  export interface PackedCloth {
31
31
  readonly count: number;
32
- /** inverse mass, max distance, backstop ×2, frontstop ×2, thickness, rest ×3, normal ×3, influences ×16. */
32
+ /**
33
+ * inverse mass, max distance, backstop ×2, frontstop ×2, thickness, rest ×3, normal ×3,
34
+ * influences ×16, pull weight.
35
+ */
33
36
  readonly statics: Float32Array;
34
37
  /** Distance records (a, b, rest, compliance), bending (four ids, rest, compliance), tethers (particle, anchor, length). */
35
38
  readonly constraints: Uint32Array;
@@ -77,6 +80,7 @@ export function packCloth(setup: SkinnedClothSetup): PackedCloth {
77
80
  statics[at + i * 2 + 1] = real ? radius : 0;
78
81
  }
79
82
  statics[6 * n + i] = limits.thickness?.[i] ?? 0;
83
+ statics[29 * n + i] = setup.pullWeights?.[i] ?? 1;
80
84
  for (let k = 0; k < 3; k++) {
81
85
  statics[7 * n + i * 3 + k] = setup.positions[i * 3 + k] as number;
82
86
  statics[10 * n + i * 3 + k] = setup.normals?.[i * 3 + k] ?? (k === 2 ? 1 : 0);
@@ -338,7 +338,7 @@ export class GpuClothSet {
338
338
  u[base + 32] = garment.range.colliderBase;
339
339
  u[base + 33] = garment.range.colliders;
340
340
  u[base + 34] = garment.particles.width;
341
- f[base + 35] = 1 / (1 + p.pull * h * h);
341
+ f[base + 35] = p.pull * h * h;
342
342
  f[base + 36] = exactExp(p.damping * h);
343
343
  f[base + 37] = exactExp(p.drag * h);
344
344
  f[base + 38] = p.maxDistanceScale;
@@ -88,8 +88,9 @@ struct Pace {
88
88
  colliderBase: u32,
89
89
  colliders: u32,
90
90
  width: u32,
91
- /* The share of its offset from its skinned position a particle keeps a substep: the pull's. */
92
- pullKeep: f32,
91
+ /* The garment's pull as a substep's stiffness, pull · h²: a particle weighted w keeps
92
+ 1 / (1 + w · pullStep) of its offset from its skinned position. */
93
+ pullStep: f32,
93
94
  keep: f32,
94
95
  dragKeep: f32,
95
96
  maxDistanceScale: f32,
@@ -119,7 +120,7 @@ struct Batch {
119
120
 
120
121
  /* The record of particle i's garment this round: which garment a particle is, in statics. */
121
122
  fn paceAt(i: u32) -> u32 {
122
- return round.at + u32(statics[29u * c.count + i]);
123
+ return round.at + u32(statics[30u * c.count + i]);
123
124
  }
124
125
 
125
126
  fn stateAt(section: u32, i: u32) -> u32 {
@@ -446,8 +447,9 @@ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
446
447
  var p = load(at);
447
448
  let t = skinnedAt(i, g);
448
449
  /* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */
449
- if (paces[g].pullKeep < 1.0) {
450
- p = t + (p - t) * paces[g].pullKeep;
450
+ let pull = statics[29u * n + i] * paces[g].pullStep;
451
+ if (pull > 0.0) {
452
+ p = t + (p - t) * (1.0 / (1.0 + pull));
451
453
  }
452
454
  /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */
453
455
  let maxDistance = statics[n + i];
@@ -50,6 +50,17 @@ export const HAIR_MIN_BETA = 0.05;
50
50
  */
51
51
  export const HAIR_SCATTER_SPHERE = 1.33;
52
52
 
53
+ /**
54
+ * How far every lobe is widened for light from every side, in radians: the angle a source would
55
+ * subtend to widen them as much, `sourceRadius / 2·dist` in `hairLight`'s terms.
56
+ *
57
+ * **0.2, the real-time form's own value**, which stands one direction in for the sphere: a lobe as
58
+ * narrow as a lamp's would put the whole sky's light into one highlight wherever the eye's
59
+ * direction happens to meet it. What it gives up is the sky's shape: a sun low in a clear sky and
60
+ * an even overcast light hair through the same widened lobes.
61
+ */
62
+ export const HAIR_AREA = 0.2;
63
+
53
64
  /** How much the scatter term leans toward the strand's own direction rather than the pseudo-normal. */
54
65
  const KAJIYA_SHARE = 0.33;
55
66
 
@@ -98,6 +109,71 @@ export function hairLight(
98
109
  sourceRadius: number,
99
110
  dist: number,
100
111
  out: Float64Array,
112
+ ): void {
113
+ const grow = sourceRadius / Math.max(2 * dist, 1e-3);
114
+ hairLobes(tangent, toLight, toEye, fibre, roughness, shift, scatter, backlit, grow, out);
115
+ }
116
+
117
+ /** The flattened eye `hairIndirect` lights a strand along, as the shader finds it. */
118
+ const flattened = new Float64Array(3);
119
+
120
+ /**
121
+ * The share of light from every side a strand returns, a colour into `out`, with the one
122
+ * direction's lobes it is made of into `lobes` (`HAIR_OUT`'s layout). `normal` is the surface's,
123
+ * which stands in for the flattened eye where the eye runs along the strand and there is none.
124
+ *
125
+ * **One direction for the sphere**: the eye flattened across the strand, `V − T(V·T)`, unshadowed,
126
+ * with no light from behind and so no transmitted lobe, every lobe widened by `HAIR_AREA`. Twice
127
+ * the sum, and at most all of what arrives. **Why twice**: the scatter's half is then
128
+ * `HAIR_SCATTER_SPHERE`, its own answer to an even sky — along the flattened eye its shape is
129
+ * `mix(1/2, 1, 0.33)`, 0.665, which doubled is that constant's 1.33 — so the lobes ride on a
130
+ * scatter the sphere's quadrature already holds to, and R and TRT are what the direction adds.
131
+ */
132
+ export function hairIndirect(
133
+ tangent: ArrayLike<number>,
134
+ toEye: ArrayLike<number>,
135
+ normal: ArrayLike<number>,
136
+ fibre: Vec3,
137
+ roughness: number,
138
+ shift: number,
139
+ scatter: number,
140
+ lobes: Float64Array,
141
+ out: Float64Array,
142
+ ): void {
143
+ const along = dot(tangent, toEye);
144
+ let length2 = 0;
145
+ for (let c = 0; c < 3; c++) {
146
+ const across = (toEye[c] as number) - (tangent[c] as number) * along;
147
+ flattened[c] = across;
148
+ length2 += across * across;
149
+ }
150
+ for (let c = 0; c < 3; c++) {
151
+ flattened[c] =
152
+ length2 > 1e-8 ? (flattened[c] as number) / Math.sqrt(length2) : (normal[c] as number);
153
+ }
154
+ hairLobes(tangent, flattened, toEye, fibre, roughness, shift, scatter, 0, HAIR_AREA, lobes);
155
+ for (let c = 0; c < 3; c++) {
156
+ const sum =
157
+ (lobes[HAIR_OUT.r] as number) +
158
+ (lobes[HAIR_OUT.tt + c] as number) +
159
+ (lobes[HAIR_OUT.trt + c] as number) +
160
+ (lobes[HAIR_OUT.scatter + c] as number);
161
+ out[c] = Math.min(1, 2 * sum);
162
+ }
163
+ }
164
+
165
+ /** `hairLight`'s arithmetic, every lobe widened by `grow` radians. */
166
+ function hairLobes(
167
+ tangent: ArrayLike<number>,
168
+ toLight: ArrayLike<number>,
169
+ toEye: ArrayLike<number>,
170
+ fibre: Vec3,
171
+ roughness: number,
172
+ shift: number,
173
+ scatter: number,
174
+ backlit: number,
175
+ grow: number,
176
+ out: Float64Array,
101
177
  ): void {
102
178
  const sinI = clamp(dot(tangent, toLight), -1, 1);
103
179
  const sinO = clamp(dot(tangent, toEye), -1, 1);
@@ -124,7 +200,6 @@ export function hairLight(
124
200
  const cosPhi = lpvp / Math.sqrt(lp2 * vp2 + 1e-4);
125
201
  const cosHalfPhi = Math.sqrt(clamp(0.5 + 0.5 * cosPhi, 0, 1));
126
202
  const beta = Math.max(roughness * roughness, HAIR_MIN_BETA);
127
- const grow = sourceRadius / Math.max(2 * dist, 1e-3);
128
203
  const x = sinI + sinO;
129
204
 
130
205
  const r =
@@ -4,29 +4,35 @@
4
4
  * receives, and every term to a finite answer where the geometry degenerates.
5
5
  *
6
6
  * **The strand is the tangent, root to tip.** The model map's green turns each strand's tilt by up
7
- * to half of it either way (0.5 is none) and its blue is occlusion, which the sky, DriftLight and
8
- * the environment's reflection take and the lamps do not — their shadows are theirs to cast. The
9
- * red is not read: a colour from root to tip is the albedo map's to carry.
7
+ * to half of it either way (0.5 is none) and its blue is occlusion, which light from every side
8
+ * takes and the lamps do not — their shadows are theirs to cast. The red is not read: a colour from
9
+ * root to tip is the albedo map's to carry.
10
10
  *
11
- * **The environment** is the reflected lobe's: mirrored about a normal turned toward the eye across
12
- * the strand, which is the cone R lies on, at the strand's own Fresnel — the lobe integrates to it
13
- * over every direction of arrival. **What it gives up** is the two transmitted lobes against the
14
- * sky, which a head of hair lit from behind by an open sky would show as a rim; the lamps carry
15
- * them.
11
+ * **Light from every side** — the sky, the probes, DriftLight — goes through the lobes rather than
12
+ * past them: they are evaluated once, for a light along the eye flattened across the strand, every
13
+ * lobe widened by \`HAIR_AREA\` as a source that large would widen it, and their sum taken as the
14
+ * share of that light the strand returns (\`hairIndirect\`). That is how a real-time hair model
15
+ * stands one direction in for the sphere: a strand returns most toward the eye what arrives from
16
+ * the eye's side of it, and the R and TRT lobes are what make a fibre gold rather than a wash of
17
+ * its scatter. The environment's reflection leaves hair out, because R already took its share.
18
+ *
19
+ * **What it gives up**: light from behind. The direction stands for the eye's side only, so there
20
+ * is no transmitted lobe in it, and a head of hair lit from behind by an open sky shows no rim
21
+ * from the sky; the lamps carry one. And the share is one direction's, not the sphere's integral,
22
+ * so it moves with the eye as the lobes do rather than holding still as an average would. **What
23
+ * would make it wrong** is hair seen mostly against its light, a head against a bright window,
24
+ * whose sky arrives through the strands rather than off them and wants the transmitted lobe back.
16
25
  */
17
- import { HAIR_F0, HAIR_MIN_BETA, HAIR_SCATTER_SPHERE } from '../../hairLobes.ts';
26
+ import { HAIR_AREA, HAIR_F0, HAIR_MIN_BETA } from '../../hairLobes.ts';
18
27
 
19
28
  export const HAIR_GLSL = /* glsl */ `
20
29
  const float HAIR_F0 = ${HAIR_F0.toFixed(9)};
21
30
  const float HAIR_MIN_BETA = ${HAIR_MIN_BETA.toFixed(3)};
22
- const float HAIR_SCATTER_SPHERE = ${HAIR_SCATTER_SPHERE.toFixed(3)};
31
+ const float HAIR_AREA = ${HAIR_AREA.toFixed(3)};
23
32
 
24
- /* This texel's tilt, gathered once with the surface. */
33
+ /* This texel's tilt, and the share of light from every side it returns: gathered with the surface. */
25
34
  float hShift;
26
-
27
- void hairSurface() {
28
- hShift = uModelParams[0].x * (0.5 + mMap.g);
29
- }
35
+ vec3 hAround;
30
36
 
31
37
  /** A longitudinal lobe: a normalised Gaussian of width \`beta\` at \`x\` from its centre. */
32
38
  float hairLongitudinal(float beta, float x) {
@@ -38,14 +44,18 @@ float hairFresnel(float cosine) {
38
44
  return HAIR_F0 + (1.0 - HAIR_F0) * c * c * c * c * c;
39
45
  }
40
46
 
41
- /** The normal turned toward the eye across the strand: the scatter's and the environment's. */
47
+ /** The normal turned toward the eye across the strand: the scatter's, and light from every side's. */
42
48
  vec3 hairAcross() {
43
49
  vec3 across = mToEye - mTangent * dot(mTangent, mToEye);
44
50
  float length2 = dot(across, across);
45
51
  return length2 > 1e-8 ? across * inversesqrt(length2) : mNormal;
46
52
  }
47
53
 
48
- void hairLight(vec3 l, float sourceRadius, float dist) {
54
+ /**
55
+ * The lobes for light arriving along \`l\`, into \`mSpecular\` and \`mDiffuse\`: every lobe widened by
56
+ * \`grow\` radians, and the transmitted one scaled by \`backlit\`.
57
+ */
58
+ void hairLobes(vec3 l, float grow, float backlit) {
49
59
  vec3 t = mTangent;
50
60
  float sinI = clamp(dot(t, l), -1.0, 1.0);
51
61
  float sinO = clamp(dot(t, mToEye), -1.0, 1.0);
@@ -56,7 +66,6 @@ void hairLight(vec3 l, float sourceRadius, float dist) {
56
66
  float cosPhi = dot(lp, vp) * inversesqrt(dot(lp, lp) * dot(vp, vp) + 1e-4);
57
67
  float cosHalfPhi = sqrt(clamp(0.5 + 0.5 * cosPhi, 0.0, 1.0));
58
68
  float beta = max(mRoughness * mRoughness, HAIR_MIN_BETA);
59
- float grow = sourceRadius / max(2.0 * dist, 1e-3);
60
69
  float x = sinI + sinO;
61
70
 
62
71
  float r = hairLongitudinal(beta + grow, x + 2.0 * hShift) * 0.25 * cosHalfPhi
@@ -66,7 +75,7 @@ void hairLight(vec3 l, float sourceRadius, float dist) {
66
75
  float h = cosHalfPhi * (1.0 + a * (0.6 - 0.8 * cosPhi));
67
76
  float fTT = hairFresnel(cosD * sqrt(clamp(1.0 - h * h, 0.0, 1.0)));
68
77
  float tt = hairLongitudinal(0.5 * beta + grow, x - hShift) * exp(-3.65 * cosPhi - 3.98)
69
- * (1.0 - fTT) * (1.0 - fTT) * uModelParams[0].z;
78
+ * (1.0 - fTT) * (1.0 - fTT) * backlit;
70
79
  float ttPower = 0.5 * sqrt(clamp(1.0 - h * h * a * a, 0.0, 1.0)) / cosD;
71
80
 
72
81
  float fTRT = hairFresnel(0.5 * cosD);
@@ -81,6 +90,11 @@ void hairLight(vec3 l, float sourceRadius, float dist) {
81
90
  mDiffuse = sqrt(mAlbedo) * uModelParams[0].y * mix(wrapped, 1.0 - abs(sinI), 0.33);
82
91
  }
83
92
 
93
+ /** A lamp, or the sun at no size: widened by the angle it subtends, as \`sphereLobe\` takes it. */
94
+ void hairLight(vec3 l, float sourceRadius, float dist) {
95
+ hairLobes(l, sourceRadius / max(2.0 * dist, 1e-3), uModelParams[0].z);
96
+ }
97
+
84
98
  /**
85
99
  * A shadow on the scatter: light that reaches a strand in another's shadow has passed through
86
100
  * hair to get there, so a penumbra deepens toward the fibre's own hue.
@@ -90,13 +104,25 @@ vec3 hairShade(float shadow) {
90
104
  return shadow * pow(max(hue, vec3(1e-4)), vec3(1.0 - shadow));
91
105
  }
92
106
 
93
- /** Light from every side, through the scatter term: \`HAIR_SCATTER_SPHERE\` says why that number. */
94
- vec3 hairAround(vec3 light) {
95
- return sqrt(mAlbedo) * (uModelParams[0].y * HAIR_SCATTER_SPHERE * mMap.b) * light;
107
+ /**
108
+ * The share of light from every side a strand returns: the lobes for a light along the eye
109
+ * flattened across the strand, unshadowed, with nothing from behind and every lobe widened by
110
+ * \`HAIR_AREA\`, twice their sum and at most all of it. Twice, in this engine's units, because the
111
+ * lobes are per unit irradiance and the scatter's half is then exactly \`HAIR_SCATTER_SPHERE\`, its
112
+ * own average over the sphere: \`hairLobes.ts\` says why, and holds the arithmetic to it.
113
+ */
114
+ vec3 hairIndirect() {
115
+ hairLobes(hairAcross(), HAIR_AREA, 0.0);
116
+ return min(vec3(1.0), 2.0 * (mSpecular + mDiffuse));
96
117
  }
97
118
 
98
- /** The reflected lobe's share of the environment: its Fresnel, which is what it integrates to. */
99
- float hairReflectance() {
100
- return hairFresnel(dot(hairAcross(), mToEye)) * mMap.b;
119
+ void hairSurface() {
120
+ hShift = uModelParams[0].x * (0.5 + mMap.g);
121
+ hAround = hairIndirect();
122
+ }
123
+
124
+ /** Light from every side, through the lobes, under the map's occlusion. */
125
+ vec3 hairAround(vec3 light) {
126
+ return hAround * (mMap.b * light);
101
127
  }
102
128
  `;
@@ -695,8 +695,9 @@ export const MAIN_GLSL = `void main() {
695
695
  * branch it did not take before.
696
696
  */
697
697
  /* A model reflects its environment whatever the pass asked of the standard model: see
698
- modelReflectance. On the switches alone, so the derivatives below stay in uniform flow. */
699
- if (uEnvironmentDials.y > 0.0 || uMaterialFlags.y != 0 || modelled) {
698
+ modelReflectance. Hair takes none: modelReflects. On the switches alone, so the derivatives
699
+ below stay in uniform flow. */
700
+ if ((uEnvironmentDials.y > 0.0 || uMaterialFlags.y != 0 || modelled) && modelReflects()) {
700
701
  vec3 toEye = normalize(uCameraPos - vWorldPos);
701
702
  vec3 mirrored = reflect(-toEye, modelled ? modelReflectNormal(n) : n);
702
703
  vec3 environment = mix(uAmbientGround, uAmbient, mirrored.y * 0.5 + 0.5);
@@ -12,8 +12,8 @@
12
12
  * **Where the models plug in.** `main` gathers the surface once (`modelSurface`), then asks at each
13
13
  * place light arrives: `modelLight` for the sun, each lamp and each rectangle, `modelAmbient` for
14
14
  * the sky and the probe's irradiance, `modelIrradiance` for DriftLight, `modelReflectNormal` and
15
- * `modelReflectance` for the environment's reflection. Each returns today's term for the standard
16
- * model, and each model's file is where its own answers are.
15
+ * `modelReflectance` for the environment's reflection, which `modelReflects` keeps from hair. Each
16
+ * returns today's term for the standard model, and each model's file is where its own answers are.
17
17
  *
18
18
  * **What a model returns from a light** is the two halves the standard terms are: a diffuse already
19
19
  * weighted by the angle of arrival, and a specular lobe in the engine's peak-normalised units (see
@@ -95,6 +95,15 @@ bool modelSeesBehind() {
95
95
  return false;
96
96
  }
97
97
 
98
+ /**
99
+ * Whether the environment's reflection reaches this surface: every model's but hair's, whose share
100
+ * of it is in its light from every side (hair.ts).
101
+ */
102
+ bool modelReflects() {
103
+ if (MODEL_HAIR) return false;
104
+ return true;
105
+ }
106
+
98
107
  /* The surface a model shades, gathered once in main before any light reaches it. */
99
108
  vec3 mNormal;
100
109
  vec3 mToEye;
@@ -285,7 +294,6 @@ vec3 modelIrradiance(vec3 irradiance) {
285
294
  /** The normal the environment is reflected about: the surface's own, but for a stretched lobe. */
286
295
  vec3 modelReflectNormal(vec3 n) {
287
296
  if (MODEL_ANISOTROPIC) return anisotropicReflectNormal(n);
288
- if (MODEL_HAIR) return hairAcross();
289
297
  return n;
290
298
  }
291
299
 
@@ -361,7 +369,6 @@ vec3 modelDiffuseOut(vec3 diffuse, float fog) {
361
369
  * what the standard model would.
362
370
  */
363
371
  float modelReflectance(float standard) {
364
- if (MODEL_HAIR) return hairReflectance();
365
372
  if (MODEL_SKIN) return skinReflectance();
366
373
  if (MODEL_EYE) return eyeReflectance();
367
374
  return standard;