reze-engine 0.55.15 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/camera.d.ts +12 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +20 -3
- package/dist/engine.d.ts +210 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1013 -104
- package/dist/model.d.ts +12 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +47 -4
- package/dist/overlay.d.ts +0 -5
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +0 -5
- package/dist/reflection.d.ts +17 -4
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +93 -38
- package/dist/shaders/cast-api.d.ts +1 -1
- package/dist/shaders/cast-api.d.ts.map +1 -1
- package/dist/shaders/cast-api.js +7 -6
- package/dist/shaders/directives.d.ts +15 -0
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +5 -0
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +42 -6
- package/dist/shaders/passes/mirror.d.ts +32 -0
- package/dist/shaders/passes/mirror.d.ts.map +1 -0
- package/dist/shaders/passes/mirror.js +284 -0
- package/dist/shaders/passes/scene-contract.d.ts +6 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
- package/dist/shaders/passes/scene-contract.js +8 -0
- package/package.json +1 -1
- package/src/camera.ts +21 -4
- package/src/engine.ts +1081 -108
- package/src/model.ts +46 -4
- package/src/overlay.ts +0 -5
- package/src/reflection.ts +103 -37
- package/src/shaders/cast-api.ts +7 -6
- package/src/shaders/directives.ts +20 -0
- package/src/shaders/passes/ground.ts +42 -6
- package/src/shaders/passes/mirror.ts +289 -0
- package/src/shaders/passes/scene-contract.ts +14 -0
package/dist/engine.js
CHANGED
|
@@ -22,7 +22,8 @@ import { ID_DEBUG_SHADER_WGSL } from "./shaders/passes/id-debug";
|
|
|
22
22
|
import { paramChanged, sampleParamTrack } from "./param-track";
|
|
23
23
|
import { effectState } from "./effect-schedule";
|
|
24
24
|
import { SHADOW_CASCADES, buildShadowVP } from "./shadow-cascades";
|
|
25
|
-
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera } from "./reflection";
|
|
25
|
+
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection";
|
|
26
|
+
import { MIRROR_MASK_DOWNSAMPLE_WGSL, MIRROR_MAT_BYTES, mirrorShaderWgsl, mirrorShadowWgsl } from "./shaders/passes/mirror";
|
|
26
27
|
import { packHalf } from "./hdr";
|
|
27
28
|
import { evalIrradianceSH, projectIrradianceSH } from "./ibl";
|
|
28
29
|
import { LYRIC_ATLAS_MAX_H, LYRIC_ATLAS_MAX_W, LYRICS_FLOATS, packLyrics } from "./shaders/lyrics-api";
|
|
@@ -35,7 +36,7 @@ import { SELECTION_MASK_SHADER_WGSL, SELECTION_EDGE_SHADER_WGSL } from "./shader
|
|
|
35
36
|
import { GIZMO_SHADER_WGSL } from "./shaders/passes/gizmo";
|
|
36
37
|
import { OVERLAY_SHADER_WGSL, OVERLAY_COMPOSITE_SHADER_WGSL } from "./shaders/passes/overlay";
|
|
37
38
|
import { WIREFRAME_SHADER_WGSL } from "./shaders/passes/wireframe";
|
|
38
|
-
import { boneOverlay, boneMarkerPositions, buildOverlayShapes, jointOverlay, rigidbodyOverlay, writeOverlayInstance, OVERLAY_INSTANCE_FLOATS, OVERLAY_VERTEX_FLOATS, OVERLAY_SHAPES, OVERLAY_SOLID_SHAPES, DEFAULT_VERTEX_COLOR, OVERLAY_STYLE, } from "./overlay";
|
|
39
|
+
import { boneOverlay, boneMarkerPositions, buildOverlayShapes, jointOverlay, lineBetween, rigidbodyOverlay, writeOverlayInstance, OVERLAY_INSTANCE_FLOATS, OVERLAY_VERTEX_FLOATS, OVERLAY_SHAPES, OVERLAY_SOLID_SHAPES, DEFAULT_VERTEX_COLOR, OVERLAY_STYLE, } from "./overlay";
|
|
39
40
|
import { BLOOM_BLIT_SHADER_WGSL, BLOOM_DOWNSAMPLE_SHADER_WGSL, BLOOM_UPSAMPLE_SHADER_WGSL, } from "./shaders/passes/bloom";
|
|
40
41
|
import { AGX_LUT_GZ, AGX_LUT_SIZE } from "./shaders/agx-lut";
|
|
41
42
|
import { buildCompositeShader, EFFECT_SCENE_API, buildFieldShader, EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES, } from "./shaders/passes/composite";
|
|
@@ -693,7 +694,7 @@ export class Engine {
|
|
|
693
694
|
return this.reversedZ ? 0 : 1;
|
|
694
695
|
}
|
|
695
696
|
get reflectionActive() {
|
|
696
|
-
return this.reflectionDebug || this.groundMirror > 0;
|
|
697
|
+
return this.reflectionDebug || this.groundMirror > 0 || this.mirrorSurface !== null;
|
|
697
698
|
}
|
|
698
699
|
/** The first installed effect, for the many places that legitimately want
|
|
699
700
|
* "is anything installed" or the singleton API's one effect. */
|
|
@@ -730,6 +731,7 @@ export class Engine {
|
|
|
730
731
|
this.overlayJoints = null;
|
|
731
732
|
this.overlayVertices = null;
|
|
732
733
|
this.wireframeColorData = new Float32Array(8);
|
|
734
|
+
this.selectionFill = null;
|
|
733
735
|
/** Rebuilt every frame into these, grouped by shape so each shape is one draw. */
|
|
734
736
|
this.overlayByShape = new Map();
|
|
735
737
|
this.overlayScratch = [];
|
|
@@ -893,6 +895,7 @@ export class Engine {
|
|
|
893
895
|
this.cullCameraArgs = null;
|
|
894
896
|
this.cullShadowArgs = null;
|
|
895
897
|
this.cullMirrorArgs = null;
|
|
898
|
+
this.mirrorPlane = new Float32Array([0, 1, 0, 0]);
|
|
896
899
|
this.mirrorCameraData = new Float32Array(40);
|
|
897
900
|
// proj x mirrorView for the ground's projective sample and the cull planes,
|
|
898
901
|
// then (projA, projB, 0, 0) — the depth-linearisation pair, read off the
|
|
@@ -906,6 +909,19 @@ export class Engine {
|
|
|
906
909
|
this.mirrorBlurBindGroups = null;
|
|
907
910
|
this.groundMirrorBlur = 0;
|
|
908
911
|
this.mirrorMaskMsTexture = null;
|
|
912
|
+
/** The mirror pass's aux, RESOLVED and kept — `.g` is accumulated alpha, and
|
|
913
|
+
* it is the only coverage the reflection has. The HDR format is
|
|
914
|
+
* rg11b10ufloat wherever the device allows it, which has no alpha channel at
|
|
915
|
+
* all, so a mirror that read `refl.a` read 1 everywhere and painted the
|
|
916
|
+
* reflection target's empty black across the whole pane. */
|
|
917
|
+
this.mirrorMaskTexture = null;
|
|
918
|
+
this.mirrorMaskView = null;
|
|
919
|
+
/** Coverage carries the SAME mip chain as the colour — the two are one
|
|
920
|
+
* premultiplied quantity and a blur has to move them together. */
|
|
921
|
+
this.mirrorMaskMipViews = [];
|
|
922
|
+
this.mirrorMaskBlurBindGroups = null;
|
|
923
|
+
this.mirrorMaskDownsamplePipeline = null;
|
|
924
|
+
this.groundClipData = new Float32Array(8);
|
|
909
925
|
this.mirrorIdMsTexture = null;
|
|
910
926
|
this.mirrorDepthTexture = null;
|
|
911
927
|
this.mirrorDepthReadView = null;
|
|
@@ -914,6 +930,24 @@ export class Engine {
|
|
|
914
930
|
this.mirrorTransparentBundle = null;
|
|
915
931
|
/** setGroundMirror lands in step 7D; the debug dial is what exercises C. */
|
|
916
932
|
this.groundMirror = 0;
|
|
933
|
+
// ── The mirror SURFACE: the reflection as a plane you can put anywhere ──
|
|
934
|
+
// Null when there is none, which is also what keeps the reflection pass off.
|
|
935
|
+
// No vertex or index buffer: the quad is six generated vertices and the model
|
|
936
|
+
// matrix is the whole of the difference between one mirror and another.
|
|
937
|
+
this.mirrorSurface = null;
|
|
938
|
+
/** Position, rotation and the two extents, baked. Column 2 is the normal the
|
|
939
|
+
* reflection plane is built from, so this is the single source for both the
|
|
940
|
+
* draw and the fold — they cannot disagree about where the glass is. */
|
|
941
|
+
this.mirrorSurfaceModel = new Float32Array(16);
|
|
942
|
+
this.mirrorSurfaceMatData = new Float32Array(28);
|
|
943
|
+
this.mirrorSurfaceMatBuffer = null;
|
|
944
|
+
this.mirrorSurfaceBindGroupLayout = null;
|
|
945
|
+
this.mirrorSurfacePipeline = null;
|
|
946
|
+
this.mirrorSurfaceBindGroup = null;
|
|
947
|
+
/** The pane in the shadow pass — the frame throwing shade on the floor. One
|
|
948
|
+
* bind group per cascade, each carrying its own index. */
|
|
949
|
+
this.mirrorShadowPipeline = null;
|
|
950
|
+
this.mirrorShadowBindGroups = [];
|
|
917
951
|
this.reflectionDebug = false;
|
|
918
952
|
this.reflectionDebugPipeline = null;
|
|
919
953
|
this.reflectionDebugBindGroupLayout = null;
|
|
@@ -1042,6 +1076,14 @@ export class Engine {
|
|
|
1042
1076
|
this.shadowCascadeVPBuffers = [];
|
|
1043
1077
|
// All cascades' view-projections, 16 floats each, inner to outer.
|
|
1044
1078
|
this.shadowLightVPMatrix = new Float32Array(16 * SHADOW_CASCADES.length);
|
|
1079
|
+
// Stand-ins for the three mirror textures the ground's layout demands, bound
|
|
1080
|
+
// ONLY by the group that draws the floor INSIDE the mirror pass. Those
|
|
1081
|
+
// textures are that pass's own attachments, and WebGPU rejects a pass that
|
|
1082
|
+
// both writes a texture and binds it — regardless of whether the shader
|
|
1083
|
+
// reaches the sample. `material.mirror` is 0 in that pass (the draw is
|
|
1084
|
+
// skipped when the floor's own mirror is on), so nothing ever reads these.
|
|
1085
|
+
this.mirrorDummyColorView = null;
|
|
1086
|
+
this.mirrorDummyDepthView = null;
|
|
1045
1087
|
this.groundDrawCall = null;
|
|
1046
1088
|
this.lastTouchTime = 0;
|
|
1047
1089
|
this.DOUBLE_TAP_DELAY = 300;
|
|
@@ -1138,6 +1180,10 @@ export class Engine {
|
|
|
1138
1180
|
/** Skinned positions for picking, grown on demand. One click's worth of work
|
|
1139
1181
|
* reused across clicks — a model's vertex count does not change. */
|
|
1140
1182
|
this.materialPickScratch = null;
|
|
1183
|
+
/** Scratch for selectMaterialFaces — same shape as materialPickScratch,
|
|
1184
|
+
* its own buffer since a drag can end while a click is still in flight
|
|
1185
|
+
* from a different frame's hover. */
|
|
1186
|
+
this.materialSelectScratch = null;
|
|
1141
1187
|
// CPU frame-time breakdown (EMA-smoothed into getStats): where a frame's
|
|
1142
1188
|
// milliseconds actually go — animation/IK/blending vs physics vs everything
|
|
1143
1189
|
// else on the render thread. The first question of any perf report.
|
|
@@ -1156,6 +1202,8 @@ export class Engine {
|
|
|
1156
1202
|
this.groundMaterialData = null;
|
|
1157
1203
|
/** Set by the first getGpuTimings() call. See it for why asking is the switch. */
|
|
1158
1204
|
this.timestampsWanted = false;
|
|
1205
|
+
this.groundMirrorPipeline = null;
|
|
1206
|
+
this.groundMirrorSoftPipeline = null;
|
|
1159
1207
|
// Shadow is cast from the visible sun direction — same vector the shader lights with.
|
|
1160
1208
|
/** Whether the shadow map needs clearing — see the shadow pass in `render`.
|
|
1161
1209
|
*
|
|
@@ -1725,9 +1773,304 @@ export class Engine {
|
|
|
1725
1773
|
this.groundMirror = on ? 1 : 0;
|
|
1726
1774
|
if (blur !== undefined)
|
|
1727
1775
|
this.groundMirrorBlur = Math.min(Math.max(blur, 0), 1);
|
|
1728
|
-
this.
|
|
1776
|
+
this.writeGroundMirrorField();
|
|
1729
1777
|
return true;
|
|
1730
1778
|
}
|
|
1779
|
+
/**
|
|
1780
|
+
* The floor's mirror field, as the shader should see it.
|
|
1781
|
+
*
|
|
1782
|
+
* A PLACED mirror takes the plane — there is one reflection target and one
|
|
1783
|
+
* fold, and a floor still trying to reflect through a wall's plane shows the
|
|
1784
|
+
* scene lying on its side. So the field is forced to 0 while a mirror surface
|
|
1785
|
+
* exists, without touching what the caller set: remove the mirror and the
|
|
1786
|
+
* floor's own setting comes back.
|
|
1787
|
+
*
|
|
1788
|
+
* It is also what lets the reflection carry the FLOOR. The mirror pass draws
|
|
1789
|
+
* the ground now, and a ground whose own mirror is on would be sampling the
|
|
1790
|
+
* very texture that pass is writing.
|
|
1791
|
+
*/
|
|
1792
|
+
writeGroundMirrorField() {
|
|
1793
|
+
if (!this.groundShadowMaterialBuffer)
|
|
1794
|
+
return;
|
|
1795
|
+
const effective = this.mirrorSurface ? 0 : this.groundMirror;
|
|
1796
|
+
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 15 * 4, new Float32Array([effective, this.groundMirrorBlur]));
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Place a mirror in the scene, or remove it with null.
|
|
1800
|
+
*
|
|
1801
|
+
* PRIVATE, and the only caller is syncMirrorFromEffects. A mirror reaches a
|
|
1802
|
+
* scene by applying the effect that declares `#mirror`, which is what gives
|
|
1803
|
+
* it dials, a schedule, a library row and a publish — a second public door
|
|
1804
|
+
* straight to the plane would be the same feature with none of that, and two
|
|
1805
|
+
* ways to place one mirror when there can only be one.
|
|
1806
|
+
*
|
|
1807
|
+
* A real planar reflection: the scene is drawn a second time from the camera
|
|
1808
|
+
* folded through this plane, so what the glass shows is the SCENE — lit by
|
|
1809
|
+
* the same sun, carrying the same shadows, with particle and ribbon effects
|
|
1810
|
+
* in it — rather than a copy of what the camera already sees.
|
|
1811
|
+
*
|
|
1812
|
+
* ONE at a time. The reflection pass owns a single set of attachments and a
|
|
1813
|
+
* single camera block, and a second mirror is a second of each; placing one
|
|
1814
|
+
* also takes the plane away from the floor mirror, which is the same target.
|
|
1815
|
+
*
|
|
1816
|
+
* `width` and `height` are metres of glass. `rotation` orients a quad whose
|
|
1817
|
+
* face is +Z at identity, so a mirror dropped in with no rotation faces the
|
|
1818
|
+
* way the default camera looks from — what someone means by putting one in
|
|
1819
|
+
* front of her.
|
|
1820
|
+
*/
|
|
1821
|
+
setMirror(options) {
|
|
1822
|
+
if (!this.device)
|
|
1823
|
+
return;
|
|
1824
|
+
if (!options) {
|
|
1825
|
+
this.mirrorSurface = null;
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
// MMD UNITS, not metres. A character stands about 20 tall here — the
|
|
1829
|
+
// engine's own numbers say so: the camera targets y = 11, spawnOffsetX
|
|
1830
|
+
// stands the next cast member 9 aside, and the ground fades out at 80. A
|
|
1831
|
+
// mirror sized in metres is a postage stamp at her ankle.
|
|
1832
|
+
//
|
|
1833
|
+
// POSITION IS THE FOOT, not the centre: the quad runs 0..1 upward from it,
|
|
1834
|
+
// so shortening the mirror takes the height off the top and turning it
|
|
1835
|
+
// pivots on the floor. See the vertex shader.
|
|
1836
|
+
//
|
|
1837
|
+
// AND IT STANDS BEHIND HER, NOT IN FRONT. A mirror shows what is on its own
|
|
1838
|
+
// front side and nothing at all of what is behind it, so a plane dropped
|
|
1839
|
+
// between the camera and the cast is a WINDOW: the subject is on the far
|
|
1840
|
+
// side of the glass and what reflects is the empty room. The default camera
|
|
1841
|
+
// orbits to z = -31 (alpha = pi, distance 33), so the default here sits at
|
|
1842
|
+
// +z, off to one side and turned back toward both — which is the
|
|
1843
|
+
// arrangement every mirror shot uses, and the reason it is not centred is
|
|
1844
|
+
// that a mirror directly behind her reflects her into her own silhouette.
|
|
1845
|
+
const px = options.position?.x ?? -11;
|
|
1846
|
+
const py = options.position?.y ?? 0;
|
|
1847
|
+
const pz = options.position?.z ?? 2;
|
|
1848
|
+
const r = options.rotation ?? { x: 0, y: 0, z: 0, w: 1 };
|
|
1849
|
+
const width = options.width ?? 14;
|
|
1850
|
+
const height = options.height ?? 24;
|
|
1851
|
+
const tint = options.tint ?? { x: 1, y: 1, z: 1 };
|
|
1852
|
+
const blur = Math.min(Math.max(options.blur ?? 0, 0), 1);
|
|
1853
|
+
const frame = options.frame ?? 0.48;
|
|
1854
|
+
const frameColor = options.frameColor ?? { x: 0.631, y: 0.631, z: 0.667 };
|
|
1855
|
+
// Rotation, then a NON-UNIFORM scale on the two in-plane axes — which is
|
|
1856
|
+
// why this is not fromPositionRotationScaleInto: a mirror is 2 x 3, not a
|
|
1857
|
+
// square someone scaled. Column 2 is left unit length; it is the normal.
|
|
1858
|
+
const m = this.mirrorSurfaceModel;
|
|
1859
|
+
Mat4.fromQuatInto(r.x, r.y, r.z, r.w, m, 0);
|
|
1860
|
+
m[0] *= width;
|
|
1861
|
+
m[1] *= width;
|
|
1862
|
+
m[2] *= width;
|
|
1863
|
+
m[4] *= height;
|
|
1864
|
+
m[5] *= height;
|
|
1865
|
+
m[6] *= height;
|
|
1866
|
+
m[12] = px;
|
|
1867
|
+
m[13] = py;
|
|
1868
|
+
m[14] = pz;
|
|
1869
|
+
this.mirrorSurface = { tint: new Vec3(tint.x, tint.y, tint.z), blur };
|
|
1870
|
+
const d = this.mirrorSurfaceMatData;
|
|
1871
|
+
d.set(m, 0);
|
|
1872
|
+
d[16] = tint.x;
|
|
1873
|
+
d[17] = tint.y;
|
|
1874
|
+
d[18] = tint.z;
|
|
1875
|
+
d[19] = blur;
|
|
1876
|
+
d[20] = frameColor.x;
|
|
1877
|
+
d[21] = frameColor.y;
|
|
1878
|
+
d[22] = frameColor.z;
|
|
1879
|
+
// Clamped to under half the short side: a band wider than the glass leaves
|
|
1880
|
+
// no glass, and a mirror that is all moulding is a plank.
|
|
1881
|
+
d[23] = Math.min(Math.max(frame, 0), Math.min(width, height) * 0.45);
|
|
1882
|
+
d[24] = width;
|
|
1883
|
+
d[25] = height;
|
|
1884
|
+
this.ensureMirrorSurfaceResources();
|
|
1885
|
+
this.device.queue.writeBuffer(this.mirrorSurfaceMatBuffer, 0, this.mirrorSurfaceMatData);
|
|
1886
|
+
}
|
|
1887
|
+
/** Called by the sync once the surface exists, so the floor yields the plane
|
|
1888
|
+
* on the same frame the mirror appears. */
|
|
1889
|
+
mirrorSurfaceAppeared() {
|
|
1890
|
+
this.writeGroundMirrorField();
|
|
1891
|
+
}
|
|
1892
|
+
/** One dial's live value, or the fallback when the effect never declared it.
|
|
1893
|
+
* Reads the CPU mirror of the params buffer, which setEffectParam keeps
|
|
1894
|
+
* current — so dragging a slider moves the glass on the same frame. */
|
|
1895
|
+
effectDial(fx, name, fallback) {
|
|
1896
|
+
const slot = fx.paramLayout.get(name);
|
|
1897
|
+
return slot ? fx.paramsData[slot.offset] : fallback;
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Drive the mirror from whichever effect declares one.
|
|
1901
|
+
*
|
|
1902
|
+
* Runs before reflectionActive is read, because the answer to "is there a
|
|
1903
|
+
* mirror" is this function's output. FIRST match wins and the rest are
|
|
1904
|
+
* ignored: there is one reflection target, so a second mirror is not a second
|
|
1905
|
+
* mirror, it is a fight over the same plane — and a scene that quietly
|
|
1906
|
+
* reflected the wrong one would be far harder to understand than one where
|
|
1907
|
+
* the second mirror does nothing.
|
|
1908
|
+
*
|
|
1909
|
+
* Gated on `weight`, so a scheduled mirror appears and leaves with its strip
|
|
1910
|
+
* and a faded-out one costs no reflection pass at all.
|
|
1911
|
+
*/
|
|
1912
|
+
syncMirrorFromEffects() {
|
|
1913
|
+
let fx = null;
|
|
1914
|
+
for (const e of this.effects) {
|
|
1915
|
+
if (e.hasMirror && e.weight > 0) {
|
|
1916
|
+
fx = e;
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
if (!fx) {
|
|
1921
|
+
if (this.mirrorSurface) {
|
|
1922
|
+
this.mirrorSurface = null;
|
|
1923
|
+
this.writeGroundMirrorField();
|
|
1924
|
+
}
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
const had = this.mirrorSurface !== null;
|
|
1928
|
+
const D = Engine.MIRROR_DIALS;
|
|
1929
|
+
const deg = Math.PI / 180;
|
|
1930
|
+
const rot = Quat.fromEuler(this.effectDial(fx, D.rotX, 0) * deg, this.effectDial(fx, D.rotY, 0) * deg, this.effectDial(fx, D.rotZ, 0) * deg);
|
|
1931
|
+
const colour = (name, r, g, b) => {
|
|
1932
|
+
const slot = fx.paramLayout.get(name);
|
|
1933
|
+
return slot
|
|
1934
|
+
? { x: fx.paramsData[slot.offset], y: fx.paramsData[slot.offset + 1], z: fx.paramsData[slot.offset + 2] }
|
|
1935
|
+
: { x: r, y: g, z: b };
|
|
1936
|
+
};
|
|
1937
|
+
const tint = colour(D.tint, 1, 1, 1);
|
|
1938
|
+
this.setMirror({
|
|
1939
|
+
position: {
|
|
1940
|
+
x: this.effectDial(fx, D.posX, -11),
|
|
1941
|
+
y: this.effectDial(fx, D.posY, 0),
|
|
1942
|
+
z: this.effectDial(fx, D.posZ, 2),
|
|
1943
|
+
},
|
|
1944
|
+
rotation: rot,
|
|
1945
|
+
// Floored well above zero: a mirror scaled to nothing is a degenerate
|
|
1946
|
+
// quad whose normal is still read for the plane, and the reflection would
|
|
1947
|
+
// fold through a plane nobody can see.
|
|
1948
|
+
width: Math.max(this.effectDial(fx, D.width, 14), 0.1),
|
|
1949
|
+
height: Math.max(this.effectDial(fx, D.height, 24), 0.1),
|
|
1950
|
+
tint,
|
|
1951
|
+
blur: this.effectDial(fx, D.blur, 0),
|
|
1952
|
+
frame: this.effectDial(fx, D.frame, 0.48),
|
|
1953
|
+
frameColor: colour(D.frameColor, 0.631, 0.631, 0.667),
|
|
1954
|
+
});
|
|
1955
|
+
if (!had)
|
|
1956
|
+
this.mirrorSurfaceAppeared();
|
|
1957
|
+
}
|
|
1958
|
+
/** Built on the first setMirror rather than at init: a shader compile and a
|
|
1959
|
+
* pipeline cost load time, and most scenes never place one. */
|
|
1960
|
+
ensureMirrorSurfaceResources() {
|
|
1961
|
+
if (this.mirrorSurfacePipeline)
|
|
1962
|
+
return;
|
|
1963
|
+
this.mirrorSurfaceMatBuffer = this.device.createBuffer({
|
|
1964
|
+
label: "mirror surface material",
|
|
1965
|
+
size: MIRROR_MAT_BYTES,
|
|
1966
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1967
|
+
});
|
|
1968
|
+
this.mirrorSurfaceBindGroupLayout = this.device.createBindGroupLayout({
|
|
1969
|
+
label: "mirror surface layout",
|
|
1970
|
+
entries: [
|
|
1971
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1972
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1973
|
+
{ binding: 2, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1974
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
1975
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
1976
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
1977
|
+
],
|
|
1978
|
+
});
|
|
1979
|
+
this.mirrorSurfacePipeline = this.createRenderPipeline({
|
|
1980
|
+
label: "mirror surface pipeline",
|
|
1981
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.mirrorSurfaceBindGroupLayout] }),
|
|
1982
|
+
shaderModule: this.device.createShaderModule({ label: "mirror surface", code: mirrorShaderWgsl() }),
|
|
1983
|
+
// None: the quad is generated from the vertex index.
|
|
1984
|
+
vertexBuffers: [],
|
|
1985
|
+
fragmentTargets: sceneTargetsFor("mirror", this.sceneFormats),
|
|
1986
|
+
// Both faces. The shader answers for the back — see its side test. Culling
|
|
1987
|
+
// it instead would make a mirror rotated the wrong way INVISIBLE, which is
|
|
1988
|
+
// the one failure a transform panel cannot show you.
|
|
1989
|
+
cullMode: "none",
|
|
1990
|
+
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
1991
|
+
});
|
|
1992
|
+
this.buildMirrorSurfaceBindGroup();
|
|
1993
|
+
// ── The pane in the shadow pass ──
|
|
1994
|
+
const shadowLayout = this.device.createBindGroupLayout({
|
|
1995
|
+
label: "mirror shadow layout",
|
|
1996
|
+
entries: [
|
|
1997
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
1998
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
1999
|
+
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
2000
|
+
],
|
|
2001
|
+
});
|
|
2002
|
+
this.mirrorShadowPipeline = this.device.createRenderPipeline({
|
|
2003
|
+
label: "mirror shadow pipeline",
|
|
2004
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [shadowLayout] }),
|
|
2005
|
+
vertex: {
|
|
2006
|
+
module: this.device.createShaderModule({
|
|
2007
|
+
label: "mirror shadow",
|
|
2008
|
+
code: mirrorShadowWgsl(SHADOW_CASCADES.length),
|
|
2009
|
+
}),
|
|
2010
|
+
entryPoint: "vs",
|
|
2011
|
+
},
|
|
2012
|
+
// Depth only: no fragment stage at all, the pane being opaque across its
|
|
2013
|
+
// whole rectangle with nothing to alpha-test.
|
|
2014
|
+
primitive: { cullMode: "none" },
|
|
2015
|
+
depthStencil: {
|
|
2016
|
+
format: Engine.SHADOW_DEPTH_FORMAT,
|
|
2017
|
+
depthWriteEnabled: true,
|
|
2018
|
+
depthCompare: "less-equal",
|
|
2019
|
+
// The shadow map's own non-reversed convention, and the same bias the
|
|
2020
|
+
// model shadows take — see shadowDepthPipeline.
|
|
2021
|
+
depthBias: 2,
|
|
2022
|
+
depthBiasSlopeScale: 1.5,
|
|
2023
|
+
depthBiasClamp: 0,
|
|
2024
|
+
},
|
|
2025
|
+
});
|
|
2026
|
+
this.mirrorShadowBindGroups = SHADOW_CASCADES.map((_, ci) => {
|
|
2027
|
+
const index = this.device.createBuffer({
|
|
2028
|
+
label: `mirror shadow cascade ${ci}`,
|
|
2029
|
+
size: 16,
|
|
2030
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
2031
|
+
});
|
|
2032
|
+
this.device.queue.writeBuffer(index, 0, new Uint32Array([ci, 0, 0, 0]));
|
|
2033
|
+
return this.device.createBindGroup({
|
|
2034
|
+
label: `mirror shadow bind ${ci}`,
|
|
2035
|
+
layout: shadowLayout,
|
|
2036
|
+
entries: [
|
|
2037
|
+
{ binding: 0, resource: { buffer: this.shadowLightVPBuffer } },
|
|
2038
|
+
{ binding: 1, resource: { buffer: index } },
|
|
2039
|
+
{ binding: 2, resource: { buffer: this.mirrorSurfaceMatBuffer } },
|
|
2040
|
+
],
|
|
2041
|
+
});
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
/** Rebound whenever the reflection target is recreated, alongside the
|
|
2045
|
+
* ground's — the two read the same texture. */
|
|
2046
|
+
buildMirrorSurfaceBindGroup() {
|
|
2047
|
+
if (!this.mirrorSurfaceBindGroupLayout || !this.mirrorSurfaceMatBuffer)
|
|
2048
|
+
return;
|
|
2049
|
+
if (!this.mirrorColorView || !this.mirrorMaskView)
|
|
2050
|
+
return;
|
|
2051
|
+
this.mirrorSurfaceBindGroup = this.device.createBindGroup({
|
|
2052
|
+
label: "mirror surface bind",
|
|
2053
|
+
layout: this.mirrorSurfaceBindGroupLayout,
|
|
2054
|
+
entries: [
|
|
2055
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
2056
|
+
{ binding: 1, resource: { buffer: this.mirrorVPBuffer } },
|
|
2057
|
+
{ binding: 2, resource: { buffer: this.mirrorSurfaceMatBuffer } },
|
|
2058
|
+
{ binding: 3, resource: this.mirrorColorView },
|
|
2059
|
+
{ binding: 4, resource: this.materialSampler },
|
|
2060
|
+
{ binding: 5, resource: this.mirrorMaskView },
|
|
2061
|
+
],
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
/** The glass itself, in the camera pass. Never in the MIRROR pass: a mirror
|
|
2065
|
+
* drawn into its own reflection is a corridor, and with one target it is a
|
|
2066
|
+
* corridor of last frame's frame. */
|
|
2067
|
+
renderMirrorSurface(pass) {
|
|
2068
|
+
if (!this.mirrorSurface || !this.mirrorSurfacePipeline || !this.mirrorSurfaceBindGroup)
|
|
2069
|
+
return;
|
|
2070
|
+
pass.setPipeline(this.mirrorSurfacePipeline);
|
|
2071
|
+
pass.setBindGroup(0, this.mirrorSurfaceBindGroup);
|
|
2072
|
+
pass.draw(6);
|
|
2073
|
+
}
|
|
1731
2074
|
ensureReflectionDebugPipeline() {
|
|
1732
2075
|
if (!this.mirrorColorView)
|
|
1733
2076
|
return false;
|
|
@@ -1779,7 +2122,25 @@ export class Engine {
|
|
|
1779
2122
|
/** Refold the live camera with the reflection — a copy and a handful of
|
|
1780
2123
|
* sign flips; cheap enough to run every frame a mirror is on. */
|
|
1781
2124
|
updateMirrorCamera() {
|
|
1782
|
-
|
|
2125
|
+
// The placed surface wins over the floor — see the note on mirrorPlane.
|
|
2126
|
+
// Recomputed rather than cached because the mirror is the thing a gizmo
|
|
2127
|
+
// drags, and a plane that lagged the surface by a frame is a reflection
|
|
2128
|
+
// that slides on it.
|
|
2129
|
+
if (this.mirrorSurface) {
|
|
2130
|
+
const m = this.mirrorSurfaceModel;
|
|
2131
|
+
planeFromPointNormal(m[12], m[13], m[14], m[8], m[9], m[10], this.mirrorPlane);
|
|
2132
|
+
}
|
|
2133
|
+
else {
|
|
2134
|
+
this.mirrorPlane.set(Engine.GROUND_PLANE);
|
|
2135
|
+
}
|
|
2136
|
+
// The ground inside the mirror pass clips against this same plane — a
|
|
2137
|
+
// mirror shows nothing behind itself, and the floor is what is behind one.
|
|
2138
|
+
if (this.groundClipMirrorBuffer) {
|
|
2139
|
+
this.groundClipData.set(this.mirrorPlane, 0);
|
|
2140
|
+
this.groundClipData[4] = 1;
|
|
2141
|
+
this.device.queue.writeBuffer(this.groundClipMirrorBuffer, 0, this.groundClipData);
|
|
2142
|
+
}
|
|
2143
|
+
buildMirrorCamera(this.cameraMatrixData, this.mirrorPlane, this.mirrorCameraData);
|
|
1783
2144
|
this.device.queue.writeBuffer(this.mirrorCameraBuffer, 0, this.mirrorCameraData);
|
|
1784
2145
|
Mat4.multiplyArrays(this.cameraMatrixData, 16, this.mirrorCameraData, 0, this.mirrorVPData, 0);
|
|
1785
2146
|
// projA/projB ARE m[10] and m[14] of the projection (the dofU discipline:
|
|
@@ -1805,20 +2166,22 @@ export class Engine {
|
|
|
1805
2166
|
renderMirrorPass(encoder) {
|
|
1806
2167
|
if (!this.reflectionActive || !this.mirrorPassDescriptor)
|
|
1807
2168
|
return;
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
//
|
|
1813
|
-
//
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2169
|
+
// No early return on an empty cast. The CLEAR is the load-bearing half of
|
|
2170
|
+
// this pass: a mirror surface samples the target whether or not anything
|
|
2171
|
+
// was drawn into it, and a pass skipped for having no models leaves it
|
|
2172
|
+
// holding whatever the allocation held — undefined memory, sampled onto a
|
|
2173
|
+
// plane in the middle of the scene.
|
|
2174
|
+
// CLEARS TRANSPARENT, and the alpha is the whole point: it is 1 exactly
|
|
2175
|
+
// where the pass drew something and 0 where it drew nothing, so every
|
|
2176
|
+
// consumer can composite the reflection over its own surface instead of
|
|
2177
|
+
// over a guess.
|
|
2178
|
+
//
|
|
2179
|
+
// It used to clear to the scene's background, linearised, so that empty
|
|
2180
|
+
// regions read as backdrop rather than black. That colour then rode the
|
|
2181
|
+
// view transform a SECOND time — the real backdrop composites after it —
|
|
2182
|
+
// and the result was a panel paler and flatter than the backdrop beside it.
|
|
2183
|
+
// A mirror that fogs. Coverage costs nothing and lets the genuine backdrop
|
|
2184
|
+
// through, which is also what an empty mirror shows: the room.
|
|
1822
2185
|
// The descriptor is reused every frame, so the stamp is set on it rather
|
|
1823
2186
|
// than passed — same as the scene pass, which is built once too.
|
|
1824
2187
|
this.mirrorPassDescriptor.timestampWrites = this.stamps("mirror");
|
|
@@ -1829,12 +2192,26 @@ export class Engine {
|
|
|
1829
2192
|
bundles.push(this.mirrorOpaqueBundle);
|
|
1830
2193
|
if (this.mirrorTransparentBundle)
|
|
1831
2194
|
bundles.push(this.mirrorTransparentBundle);
|
|
1832
|
-
|
|
2195
|
+
if (bundles.length > 0)
|
|
2196
|
+
pass.executeBundles(bundles);
|
|
1833
2197
|
// Particles and ribbons are scene geometry, and a mirror that dropped them
|
|
1834
2198
|
// showed a dancer whose hand ribbon cast no reflection. Field effects stay
|
|
1835
2199
|
// out BY DESIGN: they are display-space overlays composited after the view
|
|
1836
2200
|
// transform, with no world position to mirror. executeBundles reset the
|
|
1837
2201
|
// pass state, so these draws bind everything themselves — which they do.
|
|
2202
|
+
// THE FLOOR, inside the reflection.
|
|
2203
|
+
//
|
|
2204
|
+
// Without it a standing mirror shows a figure hanging in empty colour, with
|
|
2205
|
+
// nothing under her feet to say where she is — which is most of what made
|
|
2206
|
+
// the pane read as a dark sheet rather than as glass. It never came up for
|
|
2207
|
+
// the floor mirror because a floor does not reflect itself.
|
|
2208
|
+
//
|
|
2209
|
+
// Skipped while the FLOOR's own mirror is on: that would be the floor
|
|
2210
|
+
// sampling a reflection it is currently drawing into, one frame stale, and
|
|
2211
|
+
// the two mirrors are already fighting over the single reflection plane.
|
|
2212
|
+
if (this.hasGround && (this.mirrorSurface !== null || this.groundMirror === 0) && this.groundMirrorViewBindGroup) {
|
|
2213
|
+
this.renderGroundWith(pass, this.groundMirrorViewBindGroup, true);
|
|
2214
|
+
}
|
|
1838
2215
|
this.renderParticles(pass, "mirror");
|
|
1839
2216
|
this.drawTrails(pass, "mirror");
|
|
1840
2217
|
pass.end();
|
|
@@ -1846,7 +2223,11 @@ export class Engine {
|
|
|
1846
2223
|
* samples level 0 exactly and the chain would be work nobody reads.
|
|
1847
2224
|
*/
|
|
1848
2225
|
renderMirrorBlurChain(encoder) {
|
|
1849
|
-
|
|
2226
|
+
// EITHER consumer's dial. Gated on the ground's alone, a mirror surface with
|
|
2227
|
+
// blur up sampled mip levels that had never been rendered into — undefined
|
|
2228
|
+
// memory, which read as the model going black the moment the slider moved.
|
|
2229
|
+
const blur = Math.max(this.groundMirrorBlur, this.mirrorSurface?.blur ?? 0);
|
|
2230
|
+
if (blur <= 0 || this.mirrorMipCount < 2)
|
|
1850
2231
|
return;
|
|
1851
2232
|
if (!this.mirrorBlurBindGroups) {
|
|
1852
2233
|
const layout = this.bloomDownsamplePipeline.getBindGroupLayout(0);
|
|
@@ -1872,6 +2253,47 @@ export class Engine {
|
|
|
1872
2253
|
p.draw(3);
|
|
1873
2254
|
p.end();
|
|
1874
2255
|
}
|
|
2256
|
+
// COVERAGE, down the same chain. Colour in the reflection target is
|
|
2257
|
+
// premultiplied by this, so the pair has to be blurred together: blur the
|
|
2258
|
+
// colour alone and the pane divides a colour that spread into the empty sky
|
|
2259
|
+
// by a coverage that did not, and the reflection darkens as the dial rises.
|
|
2260
|
+
if (!this.mirrorMaskDownsamplePipeline) {
|
|
2261
|
+
const module = this.device.createShaderModule({
|
|
2262
|
+
label: "mirror coverage downsample",
|
|
2263
|
+
code: MIRROR_MASK_DOWNSAMPLE_WGSL,
|
|
2264
|
+
});
|
|
2265
|
+
this.mirrorMaskDownsamplePipeline = this.device.createRenderPipeline({
|
|
2266
|
+
label: "mirror coverage downsample pipeline",
|
|
2267
|
+
layout: "auto",
|
|
2268
|
+
vertex: { module, entryPoint: "vs" },
|
|
2269
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: Engine.BLOOM_MASK_FORMAT }] },
|
|
2270
|
+
primitive: { topology: "triangle-list" },
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
if (!this.mirrorMaskBlurBindGroups) {
|
|
2274
|
+
const layout = this.mirrorMaskDownsamplePipeline.getBindGroupLayout(0);
|
|
2275
|
+
this.mirrorMaskBlurBindGroups = [];
|
|
2276
|
+
for (let i = 1; i < this.mirrorMipCount; i++) {
|
|
2277
|
+
this.mirrorMaskBlurBindGroups.push(this.device.createBindGroup({
|
|
2278
|
+
label: `mirror coverage blur ${i}`,
|
|
2279
|
+
layout,
|
|
2280
|
+
entries: [
|
|
2281
|
+
{ binding: 0, resource: this.mirrorMaskMipViews[i - 1] },
|
|
2282
|
+
{ binding: 1, resource: this.bloomSampler },
|
|
2283
|
+
],
|
|
2284
|
+
}));
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
for (let i = 1; i < this.mirrorMipCount; i++) {
|
|
2288
|
+
const p = encoder.beginRenderPass({
|
|
2289
|
+
label: `mirror coverage blur ${i}`,
|
|
2290
|
+
colorAttachments: [{ view: this.mirrorMaskMipViews[i], loadOp: "clear", storeOp: "store" }],
|
|
2291
|
+
});
|
|
2292
|
+
p.setPipeline(this.mirrorMaskDownsamplePipeline);
|
|
2293
|
+
p.setBindGroup(0, this.mirrorMaskBlurBindGroups[i - 1]);
|
|
2294
|
+
p.draw(3);
|
|
2295
|
+
p.end();
|
|
2296
|
+
}
|
|
1875
2297
|
}
|
|
1876
2298
|
/**
|
|
1877
2299
|
* Can this device multisample the id format at the pass's sample count?
|
|
@@ -2595,13 +3017,17 @@ export class Engine {
|
|
|
2595
3017
|
// lightEmit counts as a mount on its own: a pure lighting rig draws nothing
|
|
2596
3018
|
// and is still an effect — it is how a scene gets stage lights without also
|
|
2597
3019
|
// getting geometry it did not ask for.
|
|
2598
|
-
|
|
3020
|
+
// #mirror is a mount on the same footing, and for the same reason: a
|
|
3021
|
+
// reflection re-renders the scene from a folded camera, which is a pass and
|
|
3022
|
+
// not a shader, so the effect declares it and draws nothing itself.
|
|
3023
|
+
if (!hasBackground && !hasForeground && !wantsParticles && !wantsTrails && !hasLightEmit(wgsl) && !d.mirror) {
|
|
2599
3024
|
return { ok: false, diagnostics: [
|
|
2600
3025
|
"an effect must define fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f, " +
|
|
2601
3026
|
"fn foreground(ray: vec3f, uv: vec2f, time: f32, depth: f32) -> vec4f, " +
|
|
2602
3027
|
"the particle trio (particleInit/particleStep/particleShade), " +
|
|
2603
3028
|
"the ribbon pair (trailWidth/trailShade), " +
|
|
2604
|
-
"
|
|
3029
|
+
"fn lightEmit(i: u32) -> RzLight with #lights <n>, " +
|
|
3030
|
+
"or #mirror",
|
|
2605
3031
|
], mounts: noMounts, params: [], duration: 0 };
|
|
2606
3032
|
}
|
|
2607
3033
|
const mounts = { background: hasBackground, foreground: hasForeground };
|
|
@@ -2901,6 +3327,7 @@ export class Engine {
|
|
|
2901
3327
|
paramLayout: layout,
|
|
2902
3328
|
paramsBuffer,
|
|
2903
3329
|
paramsData,
|
|
3330
|
+
hasMirror: d.mirror,
|
|
2904
3331
|
hasBackground,
|
|
2905
3332
|
hasForeground,
|
|
2906
3333
|
// The author's OWN source, not the assembled module: the assembled one
|
|
@@ -5176,6 +5603,11 @@ export class Engine {
|
|
|
5176
5603
|
// The baked frost tile — see bakeGroundNoise. Sampled with binding 10's
|
|
5177
5604
|
// repeat sampler, so it brings no sampler of its own.
|
|
5178
5605
|
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
5606
|
+
// The mirror pass's resolved aux. .g is the reflection's COVERAGE, and
|
|
5607
|
+
// the colour target cannot carry it — rg11b10ufloat has no alpha.
|
|
5608
|
+
{ binding: 13, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
5609
|
+
// The mirror's plane, and whether this draw is inside the mirror pass.
|
|
5610
|
+
{ binding: 14, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
5179
5611
|
],
|
|
5180
5612
|
});
|
|
5181
5613
|
this.groundShadowPipelineDesc = {
|
|
@@ -5222,11 +5654,33 @@ export class Engine {
|
|
|
5222
5654
|
{ binding: 1, resource: this.materialSampler },
|
|
5223
5655
|
],
|
|
5224
5656
|
});
|
|
5657
|
+
// The same group on the MIRROR camera. An outline is geometry expanded
|
|
5658
|
+
// along its normals in CLIP space, so it has to be expanded through the
|
|
5659
|
+
// camera that is drawing it — handed the real camera it would trace a hull
|
|
5660
|
+
// around where she is standing, not around her reflection.
|
|
5661
|
+
this.outlineMirrorPerFrameBindGroup = this.device.createBindGroup({
|
|
5662
|
+
label: "outline per-frame bind group (mirror)",
|
|
5663
|
+
layout: this.outlinePerFrameBindGroupLayout,
|
|
5664
|
+
entries: [
|
|
5665
|
+
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
5666
|
+
{ binding: 1, resource: this.materialSampler },
|
|
5667
|
+
],
|
|
5668
|
+
});
|
|
5225
5669
|
const outlineShaderModule = this.device.createShaderModule({
|
|
5226
5670
|
label: "outline shaders",
|
|
5227
5671
|
code: outlineShaderWgsl(),
|
|
5228
5672
|
});
|
|
5229
|
-
|
|
5673
|
+
// FRONT, not back, and not "none". The outline is an inverted hull: the
|
|
5674
|
+
// model expanded along its normals, drawn with only its BACK faces showing
|
|
5675
|
+
// so the silhouette survives and the interior does not ink over the model.
|
|
5676
|
+
// A reflection has determinant -1 (reflection.ts), so those very triangles
|
|
5677
|
+
// arrive front-facing in the mirror pass and `back` culls every one of them
|
|
5678
|
+
// — which is why the mirror had no outline at all. `none` would draw the
|
|
5679
|
+
// hull's front faces too, painting the model over with ink.
|
|
5680
|
+
// Annotated so the string fields keep their literal types: pulled out of the
|
|
5681
|
+
// call into a const, "back" widens to string and the two pipelines below
|
|
5682
|
+
// stop type-checking.
|
|
5683
|
+
const outlineDesc = {
|
|
5230
5684
|
label: "outline pipeline",
|
|
5231
5685
|
layout: outlinePipelineLayout,
|
|
5232
5686
|
shaderModule: outlineShaderModule,
|
|
@@ -5263,6 +5717,12 @@ export class Engine {
|
|
|
5263
5717
|
stencilReadMask: 0xff,
|
|
5264
5718
|
stencilWriteMask: 0,
|
|
5265
5719
|
},
|
|
5720
|
+
};
|
|
5721
|
+
this.outlinePipeline = this.createRenderPipeline(outlineDesc);
|
|
5722
|
+
this.outlineMirrorPipeline = this.createRenderPipeline({
|
|
5723
|
+
...outlineDesc,
|
|
5724
|
+
label: "outline pipeline (mirror)",
|
|
5725
|
+
cullMode: "front",
|
|
5266
5726
|
});
|
|
5267
5727
|
// ─── Selection overlay (screen-space edge-detect on a per-material mask) ───
|
|
5268
5728
|
// Reuses outline camera + main skinMats bind group layouts. No group 2 (no per-mat uniform).
|
|
@@ -5820,12 +6280,25 @@ export class Engine {
|
|
|
5820
6280
|
}
|
|
5821
6281
|
this.mirrorBlurBindGroups = null;
|
|
5822
6282
|
this.mirrorMaskMsTexture = this.device.createTexture({
|
|
5823
|
-
label: "mirror aux (msaa
|
|
6283
|
+
label: "mirror aux (msaa)",
|
|
5824
6284
|
size: [mw, mh],
|
|
5825
6285
|
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
5826
6286
|
format: Engine.BLOOM_MASK_FORMAT,
|
|
5827
6287
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
5828
6288
|
});
|
|
6289
|
+
this.mirrorMaskTexture = this.device.createTexture({
|
|
6290
|
+
label: "mirror aux resolve (coverage)",
|
|
6291
|
+
size: [mw, mh],
|
|
6292
|
+
mipLevelCount: this.mirrorMipCount,
|
|
6293
|
+
format: Engine.BLOOM_MASK_FORMAT,
|
|
6294
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
6295
|
+
});
|
|
6296
|
+
this.mirrorMaskView = this.mirrorMaskTexture.createView();
|
|
6297
|
+
this.mirrorMaskMipViews = [];
|
|
6298
|
+
for (let i = 0; i < this.mirrorMipCount; i++) {
|
|
6299
|
+
this.mirrorMaskMipViews.push(this.mirrorMaskTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }));
|
|
6300
|
+
}
|
|
6301
|
+
this.mirrorMaskBlurBindGroups = null;
|
|
5829
6302
|
this.mirrorIdMsTexture = mrtIdsEnabled()
|
|
5830
6303
|
? this.device.createTexture({
|
|
5831
6304
|
label: "mirror id (msaa, discarded)",
|
|
@@ -5854,6 +6327,10 @@ export class Engine {
|
|
|
5854
6327
|
};
|
|
5855
6328
|
const mirrorMask = {
|
|
5856
6329
|
view: this.mirrorMaskMsTexture.createView(),
|
|
6330
|
+
// RESOLVED now rather than discarded: .g is the reflection's coverage.
|
|
6331
|
+
// Mip 0 of the chain, not the whole-chain view: a resolve target is a
|
|
6332
|
+
// single level.
|
|
6333
|
+
resolveTarget: this.mirrorMaskMipViews[0],
|
|
5857
6334
|
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
5858
6335
|
loadOp: "clear",
|
|
5859
6336
|
storeOp: "discard",
|
|
@@ -5881,8 +6358,10 @@ export class Engine {
|
|
|
5881
6358
|
stencilStoreOp: "discard",
|
|
5882
6359
|
},
|
|
5883
6360
|
};
|
|
5884
|
-
// The ground
|
|
6361
|
+
// The ground and the mirror surface both bind the reflection resolve;
|
|
6362
|
+
// rebind both against the new one.
|
|
5885
6363
|
this.buildGroundBindGroup();
|
|
6364
|
+
this.buildMirrorSurfaceBindGroup();
|
|
5886
6365
|
// Bloom pyramid: mip 0 is half-res, each subsequent mip halves again.
|
|
5887
6366
|
// Mip count chosen so the coarsest mip is ≥4 px on the short side, capped at BLOOM_MAX_LEVELS.
|
|
5888
6367
|
const bw = Math.max(1, Math.floor(width / 2));
|
|
@@ -6400,11 +6879,6 @@ export class Engine {
|
|
|
6400
6879
|
size: 32,
|
|
6401
6880
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6402
6881
|
});
|
|
6403
|
-
this.wireframeHoverUniformBuffer = this.device.createBuffer({
|
|
6404
|
-
label: "wireframe color (hovered material)",
|
|
6405
|
-
size: 32,
|
|
6406
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6407
|
-
});
|
|
6408
6882
|
const wireBg0 = this.device.createBindGroupLayout({
|
|
6409
6883
|
label: "wireframe group 0 layout (camera + wire)",
|
|
6410
6884
|
entries: [
|
|
@@ -6513,6 +6987,58 @@ export class Engine {
|
|
|
6513
6987
|
},
|
|
6514
6988
|
multisample: { count: Engine.OVERLAY_SAMPLE_COUNT },
|
|
6515
6989
|
});
|
|
6990
|
+
// The selection fill: vsDepth/fs again, no bias needed since it draws
|
|
6991
|
+
// ALWAYS-ahead like the rest of this layer's basic primitives (matching
|
|
6992
|
+
// the red edge lines selectMaterialFaces hands back — the two are meant
|
|
6993
|
+
// to read as one highlight, not fight each other's depth handling) —
|
|
6994
|
+
// never occluded, which a selection drawn over the far side of a body
|
|
6995
|
+
// the box also caught would otherwise need X-ray to explain.
|
|
6996
|
+
this.selectionFillUniformBuffer = this.device.createBuffer({
|
|
6997
|
+
label: "selection fill color",
|
|
6998
|
+
size: 32,
|
|
6999
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
7000
|
+
});
|
|
7001
|
+
this.device.queue.writeBuffer(this.selectionFillUniformBuffer, 0, new Float32Array([1, 0.15, 0.15, 0.45, 0, 0, 0, 0]));
|
|
7002
|
+
this.selectionFillBindGroup = this.device.createBindGroup({
|
|
7003
|
+
label: "selection fill bind group",
|
|
7004
|
+
layout: wireBg0,
|
|
7005
|
+
entries: [
|
|
7006
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
7007
|
+
{ binding: 1, resource: { buffer: this.selectionFillUniformBuffer } },
|
|
7008
|
+
],
|
|
7009
|
+
});
|
|
7010
|
+
this.selectionFillPipeline = this.device.createRenderPipeline({
|
|
7011
|
+
label: "selection fill pipeline",
|
|
7012
|
+
layout: this.device.createPipelineLayout({
|
|
7013
|
+
label: "selection fill pipeline layout",
|
|
7014
|
+
bindGroupLayouts: [wireBg0, this.wireframeSkinLayout],
|
|
7015
|
+
}),
|
|
7016
|
+
vertex: {
|
|
7017
|
+
module: wireShader,
|
|
7018
|
+
entryPoint: "vsDepth",
|
|
7019
|
+
buffers: [
|
|
7020
|
+
{ arrayStride: 8 * 4, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] },
|
|
7021
|
+
{ arrayStride: 4 * 2, attributes: [{ shaderLocation: 1, offset: 0, format: "uint16x4" }] },
|
|
7022
|
+
{ arrayStride: 4, attributes: [{ shaderLocation: 2, offset: 0, format: "unorm8x4" }] },
|
|
7023
|
+
],
|
|
7024
|
+
},
|
|
7025
|
+
fragment: {
|
|
7026
|
+
module: wireShader,
|
|
7027
|
+
entryPoint: "fs",
|
|
7028
|
+
targets: [
|
|
7029
|
+
{
|
|
7030
|
+
format: this.presentationFormat,
|
|
7031
|
+
blend: {
|
|
7032
|
+
color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
7033
|
+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
7034
|
+
},
|
|
7035
|
+
},
|
|
7036
|
+
],
|
|
7037
|
+
},
|
|
7038
|
+
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
7039
|
+
depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: "always" },
|
|
7040
|
+
multisample: { count: Engine.OVERLAY_SAMPLE_COUNT },
|
|
7041
|
+
});
|
|
6516
7042
|
this.wireframeBindGroup = this.device.createBindGroup({
|
|
6517
7043
|
label: "wireframe bind group",
|
|
6518
7044
|
layout: wireBg0,
|
|
@@ -6529,14 +7055,6 @@ export class Engine {
|
|
|
6529
7055
|
{ binding: 1, resource: { buffer: this.wireframeSeamUniformBuffer } },
|
|
6530
7056
|
],
|
|
6531
7057
|
});
|
|
6532
|
-
this.wireframeHoverBindGroup = this.device.createBindGroup({
|
|
6533
|
-
label: "wireframe bind group (hovered material)",
|
|
6534
|
-
layout: wireBg0,
|
|
6535
|
-
entries: [
|
|
6536
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
6537
|
-
{ binding: 1, resource: { buffer: this.wireframeHoverUniformBuffer } },
|
|
6538
|
-
],
|
|
6539
|
-
});
|
|
6540
7058
|
this.overlayPassDescriptor = {
|
|
6541
7059
|
label: "overlay pass",
|
|
6542
7060
|
timestampWrites: this.stamps("overlay"),
|
|
@@ -7047,6 +7565,12 @@ export class Engine {
|
|
|
7047
7565
|
setCameraDistance(d) {
|
|
7048
7566
|
this.camera.radius = d;
|
|
7049
7567
|
}
|
|
7568
|
+
/** Which button drags orbit the view — see Camera.setOrbitButton. Default
|
|
7569
|
+
* "left" (this engine's own, original binding, every existing consumer's
|
|
7570
|
+
* default); "middle" is Blender's, opt-in only. */
|
|
7571
|
+
setCameraOrbitButton(button) {
|
|
7572
|
+
this.camera?.setOrbitButton(button);
|
|
7573
|
+
}
|
|
7050
7574
|
getCameraAlpha() {
|
|
7051
7575
|
return this.camera.alpha;
|
|
7052
7576
|
}
|
|
@@ -7663,6 +8187,26 @@ export class Engine {
|
|
|
7663
8187
|
setGroundVisible(on) {
|
|
7664
8188
|
this.groundHidden = !on;
|
|
7665
8189
|
}
|
|
8190
|
+
/**
|
|
8191
|
+
* Moves a model to a new key. Nothing about it is rebuilt — same GPU
|
|
8192
|
+
* buffers, same style groups, same physics — only which string the rest
|
|
8193
|
+
* of the engine finds it under changes.
|
|
8194
|
+
*
|
|
8195
|
+
* What a document reload uses to swap in a freshly-loaded replacement
|
|
8196
|
+
* under the ORIGINAL id with no visible gap: the replacement loads under
|
|
8197
|
+
* a throwaway key while the current one keeps rendering untouched, and
|
|
8198
|
+
* only this call — a Map move, nothing async, nothing rebuilt — hands it
|
|
8199
|
+
* the name everything else in the app still expects.
|
|
8200
|
+
*/
|
|
8201
|
+
renameModel(oldName, newName) {
|
|
8202
|
+
const inst = this.modelInstances.get(oldName);
|
|
8203
|
+
if (!inst || oldName === newName || this.modelInstances.has(newName))
|
|
8204
|
+
return;
|
|
8205
|
+
this.modelInstances.delete(oldName);
|
|
8206
|
+
inst.name = newName;
|
|
8207
|
+
inst.model.setName(newName);
|
|
8208
|
+
this.modelInstances.set(newName, inst);
|
|
8209
|
+
}
|
|
7666
8210
|
removeModel(name) {
|
|
7667
8211
|
const inst = this.modelInstances.get(name);
|
|
7668
8212
|
if (!inst)
|
|
@@ -8112,6 +8656,210 @@ export class Engine {
|
|
|
8112
8656
|
}
|
|
8113
8657
|
return best;
|
|
8114
8658
|
}
|
|
8659
|
+
/**
|
|
8660
|
+
* Every face of one material fully inside a screen-space rectangle, on the
|
|
8661
|
+
* POSED mesh — same reason pickMaterial reads it posed: a drag selects
|
|
8662
|
+
* what a costume actually looks like, not the bind pose underneath it.
|
|
8663
|
+
*
|
|
8664
|
+
* A face counts as "inside" only when all three corners project into the
|
|
8665
|
+
* rect. A triangle straddling the edge stays out — splitting a material by
|
|
8666
|
+
* a box is a spatial cut, and a half-caught triangle would tear a seam
|
|
8667
|
+
* that started nowhere the box actually touched.
|
|
8668
|
+
*
|
|
8669
|
+
* `faceIndices` are LOCAL to the material's own face list (0 = its first
|
|
8670
|
+
* triangle) — what a document transform like splitMaterial needs, since it
|
|
8671
|
+
* never has to know where in the shared index buffer this material's run
|
|
8672
|
+
* starts. `lines` is every caught face's own three edges, in POSED world
|
|
8673
|
+
* space and ready to hand straight to setOverlay — a selection reads as the
|
|
8674
|
+
* FACES it covers, not a scatter of points at their corners, and a second
|
|
8675
|
+
* query back through skinning just to draw it would double the cost of
|
|
8676
|
+
* every drag.
|
|
8677
|
+
*
|
|
8678
|
+
* x0/y0/x1/y1 are CSS pixels relative to the canvas, same origin
|
|
8679
|
+
* pickMaterial's x/y use. Order does not matter — a drag can go in any
|
|
8680
|
+
* direction.
|
|
8681
|
+
*/
|
|
8682
|
+
selectMaterialFaces(modelName, materialName, x0, y0, x1, y1) {
|
|
8683
|
+
const empty = { faceIndices: [], lines: [] };
|
|
8684
|
+
const inst = this.modelInstances.get(modelName);
|
|
8685
|
+
if (!inst || !this.camera || inst.isStage || inst.isPlane || inst.isProp)
|
|
8686
|
+
return empty;
|
|
8687
|
+
const width = this.canvas.clientWidth;
|
|
8688
|
+
const height = this.canvas.clientHeight;
|
|
8689
|
+
if (width <= 0 || height <= 0)
|
|
8690
|
+
return empty;
|
|
8691
|
+
const left = Math.min(x0, x1);
|
|
8692
|
+
const right = Math.max(x0, x1);
|
|
8693
|
+
const top = Math.min(y0, y1);
|
|
8694
|
+
const bottom = Math.max(y0, y1);
|
|
8695
|
+
const model = inst.model;
|
|
8696
|
+
const materials = model.getMaterials();
|
|
8697
|
+
const mIndex = materials.findIndex((m) => m.name === materialName);
|
|
8698
|
+
if (mIndex < 0)
|
|
8699
|
+
return empty;
|
|
8700
|
+
const vp = this.camera.getProjectionMatrix().multiply(this.camera.getViewMatrix()).values;
|
|
8701
|
+
const { positions } = model.getGeometry();
|
|
8702
|
+
const count = positions.length / 3;
|
|
8703
|
+
const { joints, weights } = model.getSkinning();
|
|
8704
|
+
const skin = model.getSkinMatrices();
|
|
8705
|
+
// Skin once, same as pickMaterial: three slots per vertex, screen x/y and
|
|
8706
|
+
// clip w — cw doubles as "behind the camera" for the in-rect test below.
|
|
8707
|
+
if (!this.materialSelectScratch || this.materialSelectScratch.length !== count * 3) {
|
|
8708
|
+
this.materialSelectScratch = new Float32Array(count * 3);
|
|
8709
|
+
}
|
|
8710
|
+
const proj = this.materialSelectScratch;
|
|
8711
|
+
const skinnedWorld = (v) => {
|
|
8712
|
+
const bx = positions[v * 3];
|
|
8713
|
+
const by = positions[v * 3 + 1];
|
|
8714
|
+
const bz = positions[v * 3 + 2];
|
|
8715
|
+
let px = 0;
|
|
8716
|
+
let py = 0;
|
|
8717
|
+
let pz = 0;
|
|
8718
|
+
for (let k = 0; k < 4; k++) {
|
|
8719
|
+
const w = weights[v * 4 + k] / 255;
|
|
8720
|
+
if (w === 0)
|
|
8721
|
+
continue;
|
|
8722
|
+
const m = joints[v * 4 + k] * 16;
|
|
8723
|
+
px += w * (skin[m] * bx + skin[m + 4] * by + skin[m + 8] * bz + skin[m + 12]);
|
|
8724
|
+
py += w * (skin[m + 1] * bx + skin[m + 5] * by + skin[m + 9] * bz + skin[m + 13]);
|
|
8725
|
+
pz += w * (skin[m + 2] * bx + skin[m + 6] * by + skin[m + 10] * bz + skin[m + 14]);
|
|
8726
|
+
}
|
|
8727
|
+
return [px, py, pz];
|
|
8728
|
+
};
|
|
8729
|
+
for (let v = 0; v < count; v++) {
|
|
8730
|
+
const [px, py, pz] = skinnedWorld(v);
|
|
8731
|
+
const cw = vp[3] * px + vp[7] * py + vp[11] * pz + vp[15];
|
|
8732
|
+
proj[v * 3 + 2] = cw;
|
|
8733
|
+
if (cw <= 1e-6)
|
|
8734
|
+
continue;
|
|
8735
|
+
const cx = vp[0] * px + vp[4] * py + vp[8] * pz + vp[12];
|
|
8736
|
+
const cy = vp[1] * px + vp[5] * py + vp[9] * pz + vp[13];
|
|
8737
|
+
proj[v * 3] = ((cx / cw) * 0.5 + 0.5) * width;
|
|
8738
|
+
proj[v * 3 + 1] = (1 - ((cy / cw) * 0.5 + 0.5)) * height;
|
|
8739
|
+
}
|
|
8740
|
+
const inRect = (v) => {
|
|
8741
|
+
const cw = proj[v * 3 + 2];
|
|
8742
|
+
if (cw <= 1e-6)
|
|
8743
|
+
return false;
|
|
8744
|
+
const sx = proj[v * 3];
|
|
8745
|
+
const sy = proj[v * 3 + 1];
|
|
8746
|
+
return sx >= left && sx <= right && sy >= top && sy <= bottom;
|
|
8747
|
+
};
|
|
8748
|
+
// Walk to this material's own run, the same running sum pickMaterial's
|
|
8749
|
+
// matEnd walk uses — materials own a CONTIGUOUS run of the index buffer.
|
|
8750
|
+
const indices = model.getIndices();
|
|
8751
|
+
let start = 0;
|
|
8752
|
+
for (let i = 0; i < mIndex; i++)
|
|
8753
|
+
start += materials[i].vertexCount;
|
|
8754
|
+
const end = start + materials[mIndex].vertexCount;
|
|
8755
|
+
const faceIndices = [];
|
|
8756
|
+
const lines = [];
|
|
8757
|
+
let faceIndex = 0;
|
|
8758
|
+
for (let i = start; i < end; i += 3, faceIndex++) {
|
|
8759
|
+
const a = indices[i];
|
|
8760
|
+
const b = indices[i + 1];
|
|
8761
|
+
const c = indices[i + 2];
|
|
8762
|
+
if (!inRect(a) || !inRect(b) || !inRect(c))
|
|
8763
|
+
continue;
|
|
8764
|
+
faceIndices.push(faceIndex);
|
|
8765
|
+
const pa = skinnedWorld(a);
|
|
8766
|
+
const pb = skinnedWorld(b);
|
|
8767
|
+
const pc = skinnedWorld(c);
|
|
8768
|
+
// Every edge, including the ones shared with a neighbouring caught
|
|
8769
|
+
// face — drawn twice there, which costs nothing a triangle this size
|
|
8770
|
+
// notices and is simpler than tracking which edges are interior.
|
|
8771
|
+
for (const [p, q] of [
|
|
8772
|
+
[pa, pb],
|
|
8773
|
+
[pb, pc],
|
|
8774
|
+
[pc, pa],
|
|
8775
|
+
]) {
|
|
8776
|
+
const line = lineBetween(p, q, Engine.SELECTION_COLOR);
|
|
8777
|
+
if (line)
|
|
8778
|
+
lines.push(line);
|
|
8779
|
+
}
|
|
8780
|
+
}
|
|
8781
|
+
return { faceIndices, lines };
|
|
8782
|
+
}
|
|
8783
|
+
/**
|
|
8784
|
+
* Fill the given faces of one material solid red, live-skinned — unlike
|
|
8785
|
+
* the edge `lines` selectMaterialFaces hands back, which are a one-shot
|
|
8786
|
+
* snapshot at the moment of the drag, this reads the model's CURRENT skin
|
|
8787
|
+
* matrices every frame, so it keeps tracking the mesh through a pose
|
|
8788
|
+
* change the same way the wireframe overlay does.
|
|
8789
|
+
*
|
|
8790
|
+
* `faceIndices` are the same LOCAL (material-relative) numbers
|
|
8791
|
+
* selectMaterialFaces returns and splitMaterial takes. Pass an empty list
|
|
8792
|
+
* (or a null modelName/materialName) to clear it.
|
|
8793
|
+
*/
|
|
8794
|
+
setSelectionFill(modelName, materialName, faceIndices) {
|
|
8795
|
+
if (this.selectionFill) {
|
|
8796
|
+
this.selectionFill.buffer.destroy();
|
|
8797
|
+
this.selectionFill = null;
|
|
8798
|
+
}
|
|
8799
|
+
if (!modelName || !materialName || faceIndices.length === 0)
|
|
8800
|
+
return;
|
|
8801
|
+
const inst = this.modelInstances.get(modelName);
|
|
8802
|
+
if (!inst)
|
|
8803
|
+
return;
|
|
8804
|
+
const materials = inst.model.getMaterials();
|
|
8805
|
+
const mIndex = materials.findIndex((m) => m.name === materialName);
|
|
8806
|
+
if (mIndex < 0)
|
|
8807
|
+
return;
|
|
8808
|
+
let start = 0;
|
|
8809
|
+
for (let i = 0; i < mIndex; i++)
|
|
8810
|
+
start += materials[i].vertexCount;
|
|
8811
|
+
const faceCount = materials[mIndex].vertexCount / 3;
|
|
8812
|
+
const modelIndices = inst.model.getIndices();
|
|
8813
|
+
const data = new Uint32Array(faceIndices.length * 3);
|
|
8814
|
+
let n = 0;
|
|
8815
|
+
for (const f of faceIndices) {
|
|
8816
|
+
if (f < 0 || f >= faceCount)
|
|
8817
|
+
continue;
|
|
8818
|
+
const i = start + f * 3;
|
|
8819
|
+
data[n++] = modelIndices[i];
|
|
8820
|
+
data[n++] = modelIndices[i + 1];
|
|
8821
|
+
data[n++] = modelIndices[i + 2];
|
|
8822
|
+
}
|
|
8823
|
+
if (n === 0)
|
|
8824
|
+
return;
|
|
8825
|
+
const buffer = this.device.createBuffer({
|
|
8826
|
+
label: `selection fill ${modelName}/${materialName}`,
|
|
8827
|
+
size: n * 4,
|
|
8828
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
8829
|
+
});
|
|
8830
|
+
this.device.queue.writeBuffer(buffer, 0, data.buffer, 0, n * 4);
|
|
8831
|
+
// wireframeSkinLayout's binding 4 is the wireframe's own edge list, which
|
|
8832
|
+
// vsDepth never reads — inst.vertexBuffer (already bound as storage
|
|
8833
|
+
// elsewhere, for that same edge-drawing use) satisfies the layout's slot
|
|
8834
|
+
// as a harmless stand-in rather than a fifth buffer built only to sit unused.
|
|
8835
|
+
const skinBindGroup = this.device.createBindGroup({
|
|
8836
|
+
label: `selection fill skin ${modelName}`,
|
|
8837
|
+
layout: this.wireframeSkinLayout,
|
|
8838
|
+
entries: [
|
|
8839
|
+
{ binding: 0, resource: { buffer: inst.skinMatrixBuffer } },
|
|
8840
|
+
{ binding: 1, resource: { buffer: inst.vertexBuffer } },
|
|
8841
|
+
{ binding: 2, resource: { buffer: inst.jointsBuffer } },
|
|
8842
|
+
{ binding: 3, resource: { buffer: inst.weightsBuffer } },
|
|
8843
|
+
{ binding: 4, resource: { buffer: inst.vertexBuffer } },
|
|
8844
|
+
],
|
|
8845
|
+
});
|
|
8846
|
+
this.selectionFill = { modelName, buffer, count: n, skinBindGroup };
|
|
8847
|
+
}
|
|
8848
|
+
renderSelectionFill(pass) {
|
|
8849
|
+
if (!this.selectionFill)
|
|
8850
|
+
return;
|
|
8851
|
+
const inst = this.modelInstances.get(this.selectionFill.modelName);
|
|
8852
|
+
if (!inst)
|
|
8853
|
+
return;
|
|
8854
|
+
pass.setPipeline(this.selectionFillPipeline);
|
|
8855
|
+
pass.setBindGroup(0, this.selectionFillBindGroup);
|
|
8856
|
+
pass.setBindGroup(1, this.selectionFill.skinBindGroup);
|
|
8857
|
+
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
8858
|
+
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
8859
|
+
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
8860
|
+
pass.setIndexBuffer(this.selectionFill.buffer, "uint32");
|
|
8861
|
+
pass.drawIndexed(this.selectionFill.count);
|
|
8862
|
+
}
|
|
8115
8863
|
/** Draw an octahedron per bone of `modelName`, rebuilt each frame. Null off. */
|
|
8116
8864
|
setBoneOverlay(modelName, options = {}) {
|
|
8117
8865
|
this.overlayBones = modelName ? { modelName, options } : null;
|
|
@@ -9620,16 +10368,35 @@ export class Engine {
|
|
|
9620
10368
|
});
|
|
9621
10369
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
|
|
9622
10370
|
}
|
|
9623
|
-
buildGroundPipeline(soft) {
|
|
10371
|
+
buildGroundPipeline(soft, mirrored = false) {
|
|
9624
10372
|
return this.createRenderPipeline({
|
|
9625
10373
|
...this.groundShadowPipelineDesc,
|
|
9626
|
-
|
|
10374
|
+
// A REFLECTION FLIPS WINDING — determinant -1, see reflection.ts. The
|
|
10375
|
+
// ground culls back faces, so drawn into the mirror pass with the ordinary
|
|
10376
|
+
// pipeline every one of its triangles faces away and the floor vanishes
|
|
10377
|
+
// whole: no grid, and no received shadow either, since the shadow is a
|
|
10378
|
+
// layer of this shader. It is the same fact that sits the OUTLINE out of
|
|
10379
|
+
// the mirror, and the same answer the scene-pass pipelines already use.
|
|
10380
|
+
cullMode: mirrored ? "none" : this.groundShadowPipelineDesc.cullMode,
|
|
10381
|
+
label: `ground shadow pipeline${soft ? " (soft)" : ""}${mirrored ? " (mirror)" : ""}`,
|
|
9627
10382
|
shaderModule: this.device.createShaderModule({
|
|
9628
10383
|
label: soft ? "ground shadow (soft)" : "ground shadow",
|
|
9629
10384
|
code: groundShaderWgsl(soft),
|
|
9630
10385
|
}),
|
|
9631
10386
|
});
|
|
9632
10387
|
}
|
|
10388
|
+
/** The ground's pipeline for the mirror pass, built on the first frame a
|
|
10389
|
+
* mirror actually needs it. */
|
|
10390
|
+
ensureGroundMirrorPipeline(soft) {
|
|
10391
|
+
if (soft) {
|
|
10392
|
+
if (!this.groundMirrorSoftPipeline)
|
|
10393
|
+
this.groundMirrorSoftPipeline = this.buildGroundPipeline(true, true);
|
|
10394
|
+
return this.groundMirrorSoftPipeline;
|
|
10395
|
+
}
|
|
10396
|
+
if (!this.groundMirrorPipeline)
|
|
10397
|
+
this.groundMirrorPipeline = this.buildGroundPipeline(false, true);
|
|
10398
|
+
return this.groundMirrorPipeline;
|
|
10399
|
+
}
|
|
9633
10400
|
/** Built on the first frame that actually needs it. A shader compile costs
|
|
9634
10401
|
* load time, and the overwhelming majority of scenes never soften a shadow. */
|
|
9635
10402
|
ensureGroundSoftPipeline() {
|
|
@@ -9699,9 +10466,47 @@ export class Engine {
|
|
|
9699
10466
|
* and a bind group holding the old view would sample a destroyed texture on
|
|
9700
10467
|
* the first resized frame with a mirror on.
|
|
9701
10468
|
*/
|
|
10469
|
+
/** The stand-ins above, made once. 4x4 rather than 1x1: the depth one has to
|
|
10470
|
+
* be multisampled to match `texture_depth_multisampled_2d`, and a
|
|
10471
|
+
* multisampled attachment is the one kind of texture worth giving room. */
|
|
10472
|
+
ensureMirrorDummies() {
|
|
10473
|
+
if (this.mirrorDummyColorView && this.mirrorDummyDepthView)
|
|
10474
|
+
return;
|
|
10475
|
+
this.mirrorDummyColorView = this.device
|
|
10476
|
+
.createTexture({
|
|
10477
|
+
label: "mirror binding stand-in (colour)",
|
|
10478
|
+
size: [4, 4],
|
|
10479
|
+
format: this.hdrFormat,
|
|
10480
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
10481
|
+
})
|
|
10482
|
+
.createView();
|
|
10483
|
+
this.mirrorDummyDepthView = this.device
|
|
10484
|
+
.createTexture({
|
|
10485
|
+
label: "mirror binding stand-in (depth)",
|
|
10486
|
+
size: [4, 4],
|
|
10487
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
10488
|
+
format: this.depthFormat,
|
|
10489
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
10490
|
+
})
|
|
10491
|
+
.createView({ aspect: "depth-only" });
|
|
10492
|
+
}
|
|
9702
10493
|
buildGroundBindGroup() {
|
|
9703
10494
|
if (!this.groundShadowMaterialBuffer)
|
|
9704
10495
|
return;
|
|
10496
|
+
this.ensureMirrorDummies();
|
|
10497
|
+
if (!this.groundClipOffBuffer) {
|
|
10498
|
+
const make = (label, active) => {
|
|
10499
|
+
const b = this.device.createBuffer({
|
|
10500
|
+
label,
|
|
10501
|
+
size: 32,
|
|
10502
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
10503
|
+
});
|
|
10504
|
+
this.device.queue.writeBuffer(b, 0, new Float32Array([0, 1, 0, 0, active, 0, 0, 0]));
|
|
10505
|
+
return b;
|
|
10506
|
+
};
|
|
10507
|
+
this.groundClipOffBuffer = make("ground clip (camera: off)", 0);
|
|
10508
|
+
this.groundClipMirrorBuffer = make("ground clip (mirror)", 1);
|
|
10509
|
+
}
|
|
9705
10510
|
this.groundShadowBindGroup = this.device.createBindGroup({
|
|
9706
10511
|
label: "ground shadow bind",
|
|
9707
10512
|
layout: this.groundShadowBindGroupLayout,
|
|
@@ -9721,6 +10526,31 @@ export class Engine {
|
|
|
9721
10526
|
{ binding: 10, resource: this.materialSampler },
|
|
9722
10527
|
{ binding: 11, resource: this.mirrorDepthReadView },
|
|
9723
10528
|
{ binding: 12, resource: this.groundNoiseView },
|
|
10529
|
+
{ binding: 13, resource: this.mirrorMaskView },
|
|
10530
|
+
{ binding: 14, resource: { buffer: this.groundClipOffBuffer } },
|
|
10531
|
+
],
|
|
10532
|
+
});
|
|
10533
|
+
this.groundMirrorViewBindGroup = this.device.createBindGroup({
|
|
10534
|
+
label: "ground shadow bind (mirror view)",
|
|
10535
|
+
layout: this.groundShadowBindGroupLayout,
|
|
10536
|
+
entries: [
|
|
10537
|
+
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
10538
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
10539
|
+
{ binding: 2, resource: this.shadowMapDepthViews[0] },
|
|
10540
|
+
{ binding: 3, resource: this.shadowComparisonSampler },
|
|
10541
|
+
{ binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
|
|
10542
|
+
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
10543
|
+
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
10544
|
+
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
10545
|
+
{ binding: 8, resource: { buffer: this.mirrorVPBuffer } },
|
|
10546
|
+
// Stand-ins, not the live mirror textures: this group draws INTO the
|
|
10547
|
+
// mirror pass, where those are the attachments.
|
|
10548
|
+
{ binding: 9, resource: this.mirrorDummyColorView },
|
|
10549
|
+
{ binding: 10, resource: this.materialSampler },
|
|
10550
|
+
{ binding: 11, resource: this.mirrorDummyDepthView },
|
|
10551
|
+
{ binding: 12, resource: this.groundNoiseView },
|
|
10552
|
+
{ binding: 13, resource: this.mirrorDummyColorView },
|
|
10553
|
+
{ binding: 14, resource: { buffer: this.groundClipMirrorBuffer } },
|
|
9724
10554
|
],
|
|
9725
10555
|
});
|
|
9726
10556
|
if (this.groundDrawCall)
|
|
@@ -10297,6 +11127,23 @@ export class Engine {
|
|
|
10297
11127
|
}
|
|
10298
11128
|
this.device.queue.submit([encoder.finish()]);
|
|
10299
11129
|
}
|
|
11130
|
+
/** The ground draw, against a caller's bind group — the camera's, or the
|
|
11131
|
+
* mirror's. One body, so the reflected floor cannot drift from the real one. */
|
|
11132
|
+
renderGroundWith(pass, bindGroup, mirrored = false) {
|
|
11133
|
+
if (this.groundIsSuppressed())
|
|
11134
|
+
return;
|
|
11135
|
+
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
|
|
11136
|
+
return;
|
|
11137
|
+
pass.setPipeline(mirrored
|
|
11138
|
+
? this.ensureGroundMirrorPipeline(this.groundSoft)
|
|
11139
|
+
: this.groundSoft
|
|
11140
|
+
? this.ensureGroundSoftPipeline()
|
|
11141
|
+
: this.groundShadowPipeline);
|
|
11142
|
+
pass.setVertexBuffer(0, this.groundVertexBuffer);
|
|
11143
|
+
pass.setIndexBuffer(this.groundIndexBuffer, "uint16");
|
|
11144
|
+
pass.setBindGroup(0, bindGroup);
|
|
11145
|
+
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0);
|
|
11146
|
+
}
|
|
10300
11147
|
renderGround(pass) {
|
|
10301
11148
|
// A stage brings its own floor. Both sit at y=0, so drawing the built-in
|
|
10302
11149
|
// plane underneath produces z-fighting across the whole scene — enforced
|
|
@@ -10472,7 +11319,16 @@ export class Engine {
|
|
|
10472
11319
|
if (!this.overlayVertices)
|
|
10473
11320
|
return;
|
|
10474
11321
|
const inst = this.overlayModel(this.overlayVertices.modelName);
|
|
10475
|
-
const {
|
|
11322
|
+
const { xray } = this.overlayVertices;
|
|
11323
|
+
// A hovered material previews EXACTLY what clicking it would pick — the
|
|
11324
|
+
// same self-occluding, ISOLATED view, nothing else drawn. It substitutes
|
|
11325
|
+
// for `material` only while nothing is actually picked, so the section
|
|
11326
|
+
// opens on the whole mesh and narrows to one material the moment the
|
|
11327
|
+
// pointer (in the viewport OR the list — both just call
|
|
11328
|
+
// setHoveredMaterial) settles on it. A real pick always wins, and there
|
|
11329
|
+
// is nothing left to preview once one is made.
|
|
11330
|
+
const hm = this.hoverMaterial;
|
|
11331
|
+
const material = this.overlayVertices.material ?? (hm && hm.modelName === inst?.name ? hm.materialName : null);
|
|
10476
11332
|
if (!inst || !this.ensureEdgeBuffer(inst, material))
|
|
10477
11333
|
return;
|
|
10478
11334
|
const edges = inst.wireEdges.get(material ?? "");
|
|
@@ -10485,15 +11341,6 @@ export class Engine {
|
|
|
10485
11341
|
const seams = material === null && this.ensureEdgeBuffer(inst, null, true)
|
|
10486
11342
|
? (inst.wireEdges.get(Engine.SEAM_KEY) ?? null)
|
|
10487
11343
|
: null;
|
|
10488
|
-
// A pointer over a material previews EXACTLY what clicking it would pick —
|
|
10489
|
-
// the same self-occluding reveal, layered over the section-wide view rather
|
|
10490
|
-
// than replacing it, so the rest of the mesh stays legible while one
|
|
10491
|
-
// material calls attention to itself. Meaningless once something IS
|
|
10492
|
-
// picked, since only the picked material draws at all then.
|
|
10493
|
-
const hm = this.hoverMaterial;
|
|
10494
|
-
const hoverName = material === null && hm?.modelName === inst.name ? hm.materialName : null;
|
|
10495
|
-
const hoverRuns = hoverName ? this.materialIndexRanges(inst, hoverName) : [];
|
|
10496
|
-
const hover = hoverRuns.length > 0 && this.ensureEdgeBuffer(inst, hoverName) ? inst.wireEdges.get(hoverName) : null;
|
|
10497
11344
|
this.wireframeColorData.set(DEFAULT_VERTEX_COLOR);
|
|
10498
11345
|
this.wireframeColorData[4] = this.canvas.width;
|
|
10499
11346
|
this.wireframeColorData[5] = this.canvas.height;
|
|
@@ -10508,40 +11355,8 @@ export class Engine {
|
|
|
10508
11355
|
this.wireframeColorData[6] = OVERLAY_STYLE.seamStrokePx;
|
|
10509
11356
|
this.device.queue.writeBuffer(this.wireframeSeamUniformBuffer, 0, this.wireframeColorData);
|
|
10510
11357
|
}
|
|
10511
|
-
if (hover) {
|
|
10512
|
-
this.wireframeColorData[6] = OVERLAY_STYLE.hoverStrokePx;
|
|
10513
|
-
this.device.queue.writeBuffer(this.wireframeHoverUniformBuffer, 0, this.wireframeColorData);
|
|
10514
|
-
}
|
|
10515
|
-
// Both bind groups, before the FIRST draw call below, regardless of which
|
|
10516
|
-
// branch runs first — the depth prepass reads the camera from group 0 and
|
|
10517
|
-
// the skin matrices from group 1 same as the edge pass does, and every
|
|
10518
|
-
// draw in this function needs both set to SOMETHING before it runs. Each
|
|
10519
|
-
// block below is free to swap either one out for its own draws.
|
|
10520
11358
|
pass.setBindGroup(0, this.wireframeBindGroup);
|
|
10521
11359
|
pass.setBindGroup(1, edges.bindGroup);
|
|
10522
|
-
const bindMesh = () => {
|
|
10523
|
-
pass.setPipeline(this.wireframeDepthPipeline);
|
|
10524
|
-
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
10525
|
-
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
10526
|
-
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
10527
|
-
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
10528
|
-
};
|
|
10529
|
-
// The hover preview writes and draws against its OWN depth first, while the
|
|
10530
|
-
// shared depth buffer is still empty — the same trick a pick uses, run
|
|
10531
|
-
// before the base mesh below gets a chance to occlude it. The base mesh's
|
|
10532
|
-
// depth write further down repeats the SAME geometry for these faces
|
|
10533
|
-
// (identical z), so it neither disturbs this nor needs to skip them.
|
|
10534
|
-
if (hover && !xray) {
|
|
10535
|
-
bindMesh();
|
|
10536
|
-
for (const [start, end] of hoverRuns)
|
|
10537
|
-
pass.drawIndexed(end - start, 1, start);
|
|
10538
|
-
pass.setBindGroup(0, this.wireframeHoverBindGroup);
|
|
10539
|
-
pass.setBindGroup(1, hover.bindGroup);
|
|
10540
|
-
pass.setPipeline(this.wireframePipeline);
|
|
10541
|
-
pass.draw(6, hover.count / 2);
|
|
10542
|
-
pass.setBindGroup(0, this.wireframeBindGroup);
|
|
10543
|
-
pass.setBindGroup(1, edges.bindGroup);
|
|
10544
|
-
}
|
|
10545
11360
|
// What writes depth is what is allowed to hide the wireframe, and that
|
|
10546
11361
|
// differs between the two views.
|
|
10547
11362
|
//
|
|
@@ -10550,14 +11365,18 @@ export class Engine {
|
|
|
10550
11365
|
// mode and Maya included, and seeing both walls at once is moire rather than
|
|
10551
11366
|
// information.
|
|
10552
11367
|
//
|
|
10553
|
-
// A PICKED material writes only its OWN faces. The question
|
|
11368
|
+
// A PICKED (or hovered) material writes only its OWN faces. The question is
|
|
10554
11369
|
// where this material is, and half of it is usually under a coat; letting
|
|
10555
11370
|
// the coat hide it does not answer that. Its own depth still goes in, so its
|
|
10556
11371
|
// back faces stay hidden and it reads as an object instead of a haze —
|
|
10557
11372
|
// which is the difference between this and turning x-ray on.
|
|
10558
11373
|
const runs = material !== null ? this.materialIndexRanges(inst, material) : null;
|
|
10559
11374
|
if (!xray) {
|
|
10560
|
-
|
|
11375
|
+
pass.setPipeline(this.wireframeDepthPipeline);
|
|
11376
|
+
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
11377
|
+
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
11378
|
+
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
11379
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
10561
11380
|
if (runs)
|
|
10562
11381
|
for (const [start, end] of runs)
|
|
10563
11382
|
pass.drawIndexed(end - start, 1, start);
|
|
@@ -10590,7 +11409,8 @@ export class Engine {
|
|
|
10590
11409
|
this.overlayBones !== null ||
|
|
10591
11410
|
this.overlayBodies !== null ||
|
|
10592
11411
|
this.overlayJoints !== null ||
|
|
10593
|
-
this.overlayVertices !== null
|
|
11412
|
+
this.overlayVertices !== null ||
|
|
11413
|
+
this.selectionFill !== null);
|
|
10594
11414
|
}
|
|
10595
11415
|
overlayModel(name) {
|
|
10596
11416
|
return this.modelInstances.get(name) ?? null;
|
|
@@ -10709,7 +11529,7 @@ export class Engine {
|
|
|
10709
11529
|
if (inst)
|
|
10710
11530
|
collect(jointOverlay(inst.model, inst.physics, this.overlayJoints.options));
|
|
10711
11531
|
}
|
|
10712
|
-
if (total === 0 && !this.overlayVertices)
|
|
11532
|
+
if (total === 0 && !this.overlayVertices && !this.selectionFill)
|
|
10713
11533
|
return;
|
|
10714
11534
|
this.ensureOverlayInstanceCapacity(total);
|
|
10715
11535
|
const data = this.overlayInstanceData;
|
|
@@ -10736,6 +11556,10 @@ export class Engine {
|
|
|
10736
11556
|
const pass = encoder.beginRenderPass(this.overlayPassDescriptor);
|
|
10737
11557
|
// Under everything: the mesh is the haze the rig is read against.
|
|
10738
11558
|
this.renderWireframe(pass);
|
|
11559
|
+
// The selection wash, under its own edge lines (drawn below with the rest
|
|
11560
|
+
// of this layer's basic primitives) so the crisp outline still reads on
|
|
11561
|
+
// top of the fill rather than being washed out by it.
|
|
11562
|
+
this.renderSelectionFill(pass);
|
|
10739
11563
|
pass.setBindGroup(0, this.overlayBindGroup);
|
|
10740
11564
|
pass.setVertexBuffer(0, this.overlayVertexBuffer);
|
|
10741
11565
|
pass.setVertexBuffer(1, this.overlayInstanceBuffer);
|
|
@@ -11260,6 +12084,8 @@ export class Engine {
|
|
|
11260
12084
|
this.dispatchMorphCompute(encoder);
|
|
11261
12085
|
// Frustum cull into indirect arguments. After the camera and shadow matrices
|
|
11262
12086
|
// are settled, before the passes that draw from them.
|
|
12087
|
+
// Before reflectionActive is read: this is what decides it.
|
|
12088
|
+
this.syncMirrorFromEffects();
|
|
11263
12089
|
if (this.reflectionActive)
|
|
11264
12090
|
this.updateMirrorCamera();
|
|
11265
12091
|
if (hasModels)
|
|
@@ -11313,6 +12139,14 @@ export class Engine {
|
|
|
11313
12139
|
// instance count of an invisible model's draws.
|
|
11314
12140
|
if (wanted && this.shadowBundles[ci])
|
|
11315
12141
|
sp.executeBundles([this.shadowBundles[ci]]);
|
|
12142
|
+
// The mirror throws shade like anything else standing on the floor.
|
|
12143
|
+
// Direct rather than in the bundle: the bundles are recorded on scene
|
|
12144
|
+
// STRUCTURE, and a mirror comes and goes with an effect's weight.
|
|
12145
|
+
if (wanted && this.mirrorSurface && this.mirrorShadowPipeline) {
|
|
12146
|
+
sp.setPipeline(this.mirrorShadowPipeline);
|
|
12147
|
+
sp.setBindGroup(0, this.mirrorShadowBindGroups[ci]);
|
|
12148
|
+
sp.draw(6);
|
|
12149
|
+
}
|
|
11316
12150
|
sp.end();
|
|
11317
12151
|
this.shadowCascadeCleared[ci] = !wanted;
|
|
11318
12152
|
}
|
|
@@ -11364,6 +12198,10 @@ export class Engine {
|
|
|
11364
12198
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
11365
12199
|
if (this.hasGround)
|
|
11366
12200
|
this.renderGround(pass);
|
|
12201
|
+
// After the ground for the same early-z reason, and before the transparent
|
|
12202
|
+
// phase so sheer fabric blends over the glass rather than being depth
|
|
12203
|
+
// -rejected behind it.
|
|
12204
|
+
this.renderMirrorSurface(pass);
|
|
11367
12205
|
// The transparent phase is drawn DIRECTLY, and must stay that way. It is the
|
|
11368
12206
|
// one part of this pass that is not bundled, so the reason is worth keeping.
|
|
11369
12207
|
//
|
|
@@ -11578,13 +12416,21 @@ export class Engine {
|
|
|
11578
12416
|
claimed.set(m, g.id);
|
|
11579
12417
|
}
|
|
11580
12418
|
}
|
|
11581
|
-
//
|
|
12419
|
+
// Installs no longer present lose their CLAIM now (the generation bump
|
|
12420
|
+
// invalidates any in-flight compile for that id) but keep their BUFFER
|
|
12421
|
+
// until every draw call has stopped pointing at it. This was the actual
|
|
12422
|
+
// bug: destroying here, synchronously, left the render loop — which runs
|
|
12423
|
+
// independently on its own rAF and does not know this function is
|
|
12424
|
+
// mid-await — still submitting frames through a bind group whose buffer
|
|
12425
|
+
// no longer existed, for however many frames compileAndInstallGroup's own
|
|
12426
|
+
// shader compiles below took. "used in submit while destroyed" was that
|
|
12427
|
+
// race, not a one-off.
|
|
11582
12428
|
const nextIds = new Set(groups.map((g) => g.id));
|
|
12429
|
+
const outgoing = [];
|
|
11583
12430
|
for (const [id, install] of inst.styleGroups) {
|
|
11584
12431
|
if (!nextIds.has(id)) {
|
|
11585
12432
|
inst.styleGroupGen.set(id, (inst.styleGroupGen.get(id) ?? 0) + 1);
|
|
11586
|
-
|
|
11587
|
-
inst.styleGroups.delete(id);
|
|
12433
|
+
outgoing.push([id, install]);
|
|
11588
12434
|
}
|
|
11589
12435
|
}
|
|
11590
12436
|
const groupResults = [];
|
|
@@ -11592,7 +12438,13 @@ export class Engine {
|
|
|
11592
12438
|
const r = await this.compileAndInstallGroup(inst, g);
|
|
11593
12439
|
groupResults.push({ groupId: g.id, diagnostics: r.diagnostics, ok: r.ok });
|
|
11594
12440
|
}
|
|
12441
|
+
// Repoints every draw call away from the outgoing installs (among
|
|
12442
|
+
// everything else) — only past this line is destroying them safe.
|
|
11595
12443
|
this.assignDrawCallGroups(inst, claimed);
|
|
12444
|
+
for (const [id, install] of outgoing) {
|
|
12445
|
+
this.destroyInstall(install);
|
|
12446
|
+
inst.styleGroups.delete(id);
|
|
12447
|
+
}
|
|
11596
12448
|
return {
|
|
11597
12449
|
ok: groupResults.every((r) => r.ok),
|
|
11598
12450
|
groups: groupResults,
|
|
@@ -11880,6 +12732,7 @@ export class Engine {
|
|
|
11880
12732
|
let pipeline;
|
|
11881
12733
|
let pipelineNoDepthWrite;
|
|
11882
12734
|
let overEyesPipeline;
|
|
12735
|
+
let mirrorPipeline;
|
|
11883
12736
|
try {
|
|
11884
12737
|
pipeline = await this.createRenderClassPipeline(renderClass, module, false);
|
|
11885
12738
|
// The depth-write-off twin: stage transparency draws with it (see
|
|
@@ -11887,6 +12740,10 @@ export class Engine {
|
|
|
11887
12740
|
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false);
|
|
11888
12741
|
if (renderClass === "hair")
|
|
11889
12742
|
overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true);
|
|
12743
|
+
// Only the eye needs one: every other class culls "none", which a flipped
|
|
12744
|
+
// winding leaves alone.
|
|
12745
|
+
if (renderClass === "eye")
|
|
12746
|
+
mirrorPipeline = await this.createRenderClassPipeline(renderClass, module, false, true, true);
|
|
11890
12747
|
}
|
|
11891
12748
|
catch (e) {
|
|
11892
12749
|
diagnostics.push({ severity: "error", message: `pipeline creation failed: ${e.message}` });
|
|
@@ -11913,6 +12770,7 @@ export class Engine {
|
|
|
11913
12770
|
pipeline,
|
|
11914
12771
|
pipelineNoDepthWrite,
|
|
11915
12772
|
overEyesPipeline,
|
|
12773
|
+
mirrorPipeline,
|
|
11916
12774
|
uniformBuffer,
|
|
11917
12775
|
images: this.uploadGroupImages(group),
|
|
11918
12776
|
slotMap: result.slotMap,
|
|
@@ -12012,12 +12870,20 @@ export class Engine {
|
|
|
12012
12870
|
* render-class owns pass integration (stencil interplay, depth bias, cull). auto = plain;
|
|
12013
12871
|
* eye = stamp + front cull + bias; hair = stencil-test (+ the over-eyes variant).
|
|
12014
12872
|
*/
|
|
12015
|
-
createRenderClassPipeline(renderClass, module, overEyes, depthWrite = true) {
|
|
12873
|
+
createRenderClassPipeline(renderClass, module, overEyes, depthWrite = true, mirrored = false) {
|
|
12016
12874
|
const base = {
|
|
12017
12875
|
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
12018
12876
|
layout: this.mainPipelineLayout,
|
|
12019
12877
|
vertex: { module, buffers: this.fullVertexBufferLayouts },
|
|
12020
|
-
|
|
12878
|
+
// The eye front-culls — the see-through-hair look is draw order plus that
|
|
12879
|
+
// cull plus the stencil stamp, and it is the deployed appearance of every
|
|
12880
|
+
// published scene. In the MIRROR the winding is flipped (determinant -1,
|
|
12881
|
+
// reflection.ts), so the same cull keeps the back of the eyeball and
|
|
12882
|
+
// discards its front. `mirrored` flips it back rather than changing what
|
|
12883
|
+
// the camera sees.
|
|
12884
|
+
primitive: {
|
|
12885
|
+
cullMode: (renderClass === "eye" ? (mirrored ? "back" : "front") : "none"),
|
|
12886
|
+
},
|
|
12021
12887
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
12022
12888
|
};
|
|
12023
12889
|
const plainDepth = {
|
|
@@ -12081,12 +12947,15 @@ export class Engine {
|
|
|
12081
12947
|
// rain vanished the instant the camera crossed a glass dome or a curtain
|
|
12082
12948
|
// (reported: binary vanish/recover with camera angle, stage loaded). Stage
|
|
12083
12949
|
// transparency blends and leaves depth alone.
|
|
12084
|
-
pipelineForDrawCall(inst, dc) {
|
|
12950
|
+
pipelineForDrawCall(inst, dc, mirrored = false) {
|
|
12085
12951
|
const stageGlass = inst.isStage && dc.type === "transparent";
|
|
12086
12952
|
if (dc.groupId) {
|
|
12087
12953
|
const install = inst.styleGroups.get(dc.groupId);
|
|
12088
|
-
if (install)
|
|
12954
|
+
if (install) {
|
|
12955
|
+
if (mirrored && install.mirrorPipeline)
|
|
12956
|
+
return install.mirrorPipeline;
|
|
12089
12957
|
return stageGlass ? install.pipelineNoDepthWrite : install.pipeline;
|
|
12958
|
+
}
|
|
12090
12959
|
}
|
|
12091
12960
|
return stageGlass ? this.neutralPipelineNoDepthWrite : this.neutralPipeline;
|
|
12092
12961
|
}
|
|
@@ -12122,7 +12991,7 @@ export class Engine {
|
|
|
12122
12991
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
12123
12992
|
bound = true;
|
|
12124
12993
|
}
|
|
12125
|
-
const pipeline = this.pipelineForDrawCall(inst, draw);
|
|
12994
|
+
const pipeline = this.pipelineForDrawCall(inst, draw, view.args === "mirror");
|
|
12126
12995
|
if (pipeline !== currentPipeline) {
|
|
12127
12996
|
pass.setPipeline(pipeline);
|
|
12128
12997
|
currentPipeline = pipeline;
|
|
@@ -12133,8 +13002,9 @@ export class Engine {
|
|
|
12133
13002
|
// Same index range; own pipeline + groups 0/2. Group 1 (skinMats) is
|
|
12134
13003
|
// layout-identical between the main and outline pipelines and stays
|
|
12135
13004
|
// bound. Restore group 0 afterwards and force a pipeline re-set.
|
|
12136
|
-
|
|
12137
|
-
pass.
|
|
13005
|
+
const mirrored = view.args === "mirror";
|
|
13006
|
+
pass.setPipeline(mirrored ? this.outlineMirrorPipeline : this.outlinePipeline);
|
|
13007
|
+
pass.setBindGroup(0, mirrored ? this.outlineMirrorPerFrameBindGroup : this.outlinePerFrameBindGroup);
|
|
12138
13008
|
pass.setBindGroup(2, draw.outline.bindGroup);
|
|
12139
13009
|
this.issueDraw(pass, draw, view.args);
|
|
12140
13010
|
pass.setBindGroup(0, view.perFrame);
|
|
@@ -12168,10 +13038,13 @@ export class Engine {
|
|
|
12168
13038
|
* main camera's bind group.
|
|
12169
13039
|
*/
|
|
12170
13040
|
sceneView(kind) {
|
|
13041
|
+
// Outlines in BOTH now. They were off in the mirror because the hull culls
|
|
13042
|
+
// back faces and a reflection flips winding, so every hull triangle was
|
|
13043
|
+
// culled — a reflection of a toon character with no ink line beside the
|
|
13044
|
+
// real one reads as broken. The draw picks the flipped-cull pipeline and
|
|
13045
|
+
// the mirror camera's own group; see outlineMirrorPipeline.
|
|
12171
13046
|
return kind === "mirror"
|
|
12172
|
-
?
|
|
12173
|
-
// reflection flips winding, so the hull would ink over the model.
|
|
12174
|
-
{ perFrame: this.mirrorPerFrameBindGroup, args: "mirror", outlines: false }
|
|
13047
|
+
? { perFrame: this.mirrorPerFrameBindGroup, args: "mirror", outlines: true }
|
|
12175
13048
|
: { perFrame: this.perFrameBindGroup, args: "camera", outlines: true };
|
|
12176
13049
|
}
|
|
12177
13050
|
renderModelOpaquePhase(pass, inst, view) {
|
|
@@ -12956,11 +13829,16 @@ Engine.TIMED_PASSES = [
|
|
|
12956
13829
|
"composite",
|
|
12957
13830
|
"overlay",
|
|
12958
13831
|
];
|
|
12959
|
-
// ── The
|
|
13832
|
+
// ── The mirror (step 7C) ──
|
|
12960
13833
|
// Half-res scene-contract attachments a mirrored draw renders into, plus the
|
|
12961
|
-
// mirror's own camera block.
|
|
12962
|
-
//
|
|
12963
|
-
|
|
13834
|
+
// mirror's own camera block.
|
|
13835
|
+
//
|
|
13836
|
+
// ONE reflection target, so ONE plane at a time. The floor's is (0, 1, 0, 0)
|
|
13837
|
+
// — MMD floors live at y = 0 by convention and addGround builds its quad
|
|
13838
|
+
// there — and a placed mirror surface REPLACES it for as long as it exists,
|
|
13839
|
+
// because two planes would need two of every attachment below. Which one is
|
|
13840
|
+
// in force is mirrorPlane, refreshed by updateMirrorCamera.
|
|
13841
|
+
Engine.GROUND_PLANE = [0, 1, 0, 0];
|
|
12964
13842
|
Engine.BLOOM_MAX_LEVELS = 5;
|
|
12965
13843
|
// Width of the baked Filmic tone LUT (composite.ts FILMIC_LUT_W must match).
|
|
12966
13844
|
Engine.FILMIC_LUT_WIDTH = 256;
|
|
@@ -12968,6 +13846,37 @@ Engine.FILMIC_LUT_WIDTH = 256;
|
|
|
12968
13846
|
// (vsync-to-vsync), recomputed at STATS_REFRESH_MS so the readout doesn't flicker.
|
|
12969
13847
|
Engine.STATS_WINDOW = 120;
|
|
12970
13848
|
Engine.STATS_REFRESH_MS = 500;
|
|
13849
|
+
/**
|
|
13850
|
+
* The dials a `#mirror` effect is READ BY — the contract between an effect
|
|
13851
|
+
* file and the reflection.
|
|
13852
|
+
*
|
|
13853
|
+
* By name rather than by position, and with a fallback each, so that an
|
|
13854
|
+
* effect declaring only some of them still works: a mirror that wants to sit
|
|
13855
|
+
* at the origin facing the camera declares nothing but its size, and the ones
|
|
13856
|
+
* it leaves out are not missing, they are default.
|
|
13857
|
+
*
|
|
13858
|
+
* Names in the built-ins' own convention (SCREAMING_SNAKE), because these
|
|
13859
|
+
* appear in the params panel beside every other effect's.
|
|
13860
|
+
*/
|
|
13861
|
+
Engine.MIRROR_DIALS = {
|
|
13862
|
+
posX: "POS_X",
|
|
13863
|
+
posY: "POS_Y",
|
|
13864
|
+
posZ: "POS_Z",
|
|
13865
|
+
/** Degrees, the unit the slider shows — nothing converts on the way in. */
|
|
13866
|
+
rotX: "ROT_X",
|
|
13867
|
+
rotY: "ROT_Y",
|
|
13868
|
+
rotZ: "ROT_Z",
|
|
13869
|
+
width: "WIDTH",
|
|
13870
|
+
height: "HEIGHT",
|
|
13871
|
+
tint: "TINT",
|
|
13872
|
+
blur: "BLUR",
|
|
13873
|
+
frame: "FRAME",
|
|
13874
|
+
frameColor: "FRAME_COLOR",
|
|
13875
|
+
};
|
|
13876
|
+
/** Opaque red — a selection is a working state, not a material property,
|
|
13877
|
+
* so it reads as unmistakably distinct from anything a model's own
|
|
13878
|
+
* colours could be. */
|
|
13879
|
+
Engine.SELECTION_COLOR = [1, 0.15, 0.15, 1];
|
|
12971
13880
|
// ── GPU frustum cull ────────────────────────────────────────────────────────
|
|
12972
13881
|
//
|
|
12973
13882
|
// Sizes, once, so the arithmetic below is readable: a DrawMeta is 32 bytes
|