reze-engine 0.55.16 → 0.56.1
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 +253 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1078 -104
- package/dist/model.d.ts +10 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +16 -0
- 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/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/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 +1158 -108
- package/src/model.ts +16 -0
- package/src/overlay.ts +0 -5
- package/src/reflection.ts +103 -37
- 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,15 @@ 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 the small per-vertex writeBuffer calls setVertexPositions
|
|
1184
|
+
* and setBoneBindPositions make — one float32x3, reused so a live drag
|
|
1185
|
+
* calling either hundreds of times a frame does not also allocate hundreds
|
|
1186
|
+
* of throwaway arrays. */
|
|
1187
|
+
this.vec3Scratch = new Float32Array(3);
|
|
1188
|
+
/** Scratch for selectMaterialFaces — same shape as materialPickScratch,
|
|
1189
|
+
* its own buffer since a drag can end while a click is still in flight
|
|
1190
|
+
* from a different frame's hover. */
|
|
1191
|
+
this.materialSelectScratch = null;
|
|
1141
1192
|
// CPU frame-time breakdown (EMA-smoothed into getStats): where a frame's
|
|
1142
1193
|
// milliseconds actually go — animation/IK/blending vs physics vs everything
|
|
1143
1194
|
// else on the render thread. The first question of any perf report.
|
|
@@ -1156,6 +1207,8 @@ export class Engine {
|
|
|
1156
1207
|
this.groundMaterialData = null;
|
|
1157
1208
|
/** Set by the first getGpuTimings() call. See it for why asking is the switch. */
|
|
1158
1209
|
this.timestampsWanted = false;
|
|
1210
|
+
this.groundMirrorPipeline = null;
|
|
1211
|
+
this.groundMirrorSoftPipeline = null;
|
|
1159
1212
|
// Shadow is cast from the visible sun direction — same vector the shader lights with.
|
|
1160
1213
|
/** Whether the shadow map needs clearing — see the shadow pass in `render`.
|
|
1161
1214
|
*
|
|
@@ -1725,9 +1778,304 @@ export class Engine {
|
|
|
1725
1778
|
this.groundMirror = on ? 1 : 0;
|
|
1726
1779
|
if (blur !== undefined)
|
|
1727
1780
|
this.groundMirrorBlur = Math.min(Math.max(blur, 0), 1);
|
|
1728
|
-
this.
|
|
1781
|
+
this.writeGroundMirrorField();
|
|
1729
1782
|
return true;
|
|
1730
1783
|
}
|
|
1784
|
+
/**
|
|
1785
|
+
* The floor's mirror field, as the shader should see it.
|
|
1786
|
+
*
|
|
1787
|
+
* A PLACED mirror takes the plane — there is one reflection target and one
|
|
1788
|
+
* fold, and a floor still trying to reflect through a wall's plane shows the
|
|
1789
|
+
* scene lying on its side. So the field is forced to 0 while a mirror surface
|
|
1790
|
+
* exists, without touching what the caller set: remove the mirror and the
|
|
1791
|
+
* floor's own setting comes back.
|
|
1792
|
+
*
|
|
1793
|
+
* It is also what lets the reflection carry the FLOOR. The mirror pass draws
|
|
1794
|
+
* the ground now, and a ground whose own mirror is on would be sampling the
|
|
1795
|
+
* very texture that pass is writing.
|
|
1796
|
+
*/
|
|
1797
|
+
writeGroundMirrorField() {
|
|
1798
|
+
if (!this.groundShadowMaterialBuffer)
|
|
1799
|
+
return;
|
|
1800
|
+
const effective = this.mirrorSurface ? 0 : this.groundMirror;
|
|
1801
|
+
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 15 * 4, new Float32Array([effective, this.groundMirrorBlur]));
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Place a mirror in the scene, or remove it with null.
|
|
1805
|
+
*
|
|
1806
|
+
* PRIVATE, and the only caller is syncMirrorFromEffects. A mirror reaches a
|
|
1807
|
+
* scene by applying the effect that declares `#mirror`, which is what gives
|
|
1808
|
+
* it dials, a schedule, a library row and a publish — a second public door
|
|
1809
|
+
* straight to the plane would be the same feature with none of that, and two
|
|
1810
|
+
* ways to place one mirror when there can only be one.
|
|
1811
|
+
*
|
|
1812
|
+
* A real planar reflection: the scene is drawn a second time from the camera
|
|
1813
|
+
* folded through this plane, so what the glass shows is the SCENE — lit by
|
|
1814
|
+
* the same sun, carrying the same shadows, with particle and ribbon effects
|
|
1815
|
+
* in it — rather than a copy of what the camera already sees.
|
|
1816
|
+
*
|
|
1817
|
+
* ONE at a time. The reflection pass owns a single set of attachments and a
|
|
1818
|
+
* single camera block, and a second mirror is a second of each; placing one
|
|
1819
|
+
* also takes the plane away from the floor mirror, which is the same target.
|
|
1820
|
+
*
|
|
1821
|
+
* `width` and `height` are metres of glass. `rotation` orients a quad whose
|
|
1822
|
+
* face is +Z at identity, so a mirror dropped in with no rotation faces the
|
|
1823
|
+
* way the default camera looks from — what someone means by putting one in
|
|
1824
|
+
* front of her.
|
|
1825
|
+
*/
|
|
1826
|
+
setMirror(options) {
|
|
1827
|
+
if (!this.device)
|
|
1828
|
+
return;
|
|
1829
|
+
if (!options) {
|
|
1830
|
+
this.mirrorSurface = null;
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
// MMD UNITS, not metres. A character stands about 20 tall here — the
|
|
1834
|
+
// engine's own numbers say so: the camera targets y = 11, spawnOffsetX
|
|
1835
|
+
// stands the next cast member 9 aside, and the ground fades out at 80. A
|
|
1836
|
+
// mirror sized in metres is a postage stamp at her ankle.
|
|
1837
|
+
//
|
|
1838
|
+
// POSITION IS THE FOOT, not the centre: the quad runs 0..1 upward from it,
|
|
1839
|
+
// so shortening the mirror takes the height off the top and turning it
|
|
1840
|
+
// pivots on the floor. See the vertex shader.
|
|
1841
|
+
//
|
|
1842
|
+
// AND IT STANDS BEHIND HER, NOT IN FRONT. A mirror shows what is on its own
|
|
1843
|
+
// front side and nothing at all of what is behind it, so a plane dropped
|
|
1844
|
+
// between the camera and the cast is a WINDOW: the subject is on the far
|
|
1845
|
+
// side of the glass and what reflects is the empty room. The default camera
|
|
1846
|
+
// orbits to z = -31 (alpha = pi, distance 33), so the default here sits at
|
|
1847
|
+
// +z, off to one side and turned back toward both — which is the
|
|
1848
|
+
// arrangement every mirror shot uses, and the reason it is not centred is
|
|
1849
|
+
// that a mirror directly behind her reflects her into her own silhouette.
|
|
1850
|
+
const px = options.position?.x ?? -11;
|
|
1851
|
+
const py = options.position?.y ?? 0;
|
|
1852
|
+
const pz = options.position?.z ?? 2;
|
|
1853
|
+
const r = options.rotation ?? { x: 0, y: 0, z: 0, w: 1 };
|
|
1854
|
+
const width = options.width ?? 14;
|
|
1855
|
+
const height = options.height ?? 24;
|
|
1856
|
+
const tint = options.tint ?? { x: 1, y: 1, z: 1 };
|
|
1857
|
+
const blur = Math.min(Math.max(options.blur ?? 0, 0), 1);
|
|
1858
|
+
const frame = options.frame ?? 0.48;
|
|
1859
|
+
const frameColor = options.frameColor ?? { x: 0.631, y: 0.631, z: 0.667 };
|
|
1860
|
+
// Rotation, then a NON-UNIFORM scale on the two in-plane axes — which is
|
|
1861
|
+
// why this is not fromPositionRotationScaleInto: a mirror is 2 x 3, not a
|
|
1862
|
+
// square someone scaled. Column 2 is left unit length; it is the normal.
|
|
1863
|
+
const m = this.mirrorSurfaceModel;
|
|
1864
|
+
Mat4.fromQuatInto(r.x, r.y, r.z, r.w, m, 0);
|
|
1865
|
+
m[0] *= width;
|
|
1866
|
+
m[1] *= width;
|
|
1867
|
+
m[2] *= width;
|
|
1868
|
+
m[4] *= height;
|
|
1869
|
+
m[5] *= height;
|
|
1870
|
+
m[6] *= height;
|
|
1871
|
+
m[12] = px;
|
|
1872
|
+
m[13] = py;
|
|
1873
|
+
m[14] = pz;
|
|
1874
|
+
this.mirrorSurface = { tint: new Vec3(tint.x, tint.y, tint.z), blur };
|
|
1875
|
+
const d = this.mirrorSurfaceMatData;
|
|
1876
|
+
d.set(m, 0);
|
|
1877
|
+
d[16] = tint.x;
|
|
1878
|
+
d[17] = tint.y;
|
|
1879
|
+
d[18] = tint.z;
|
|
1880
|
+
d[19] = blur;
|
|
1881
|
+
d[20] = frameColor.x;
|
|
1882
|
+
d[21] = frameColor.y;
|
|
1883
|
+
d[22] = frameColor.z;
|
|
1884
|
+
// Clamped to under half the short side: a band wider than the glass leaves
|
|
1885
|
+
// no glass, and a mirror that is all moulding is a plank.
|
|
1886
|
+
d[23] = Math.min(Math.max(frame, 0), Math.min(width, height) * 0.45);
|
|
1887
|
+
d[24] = width;
|
|
1888
|
+
d[25] = height;
|
|
1889
|
+
this.ensureMirrorSurfaceResources();
|
|
1890
|
+
this.device.queue.writeBuffer(this.mirrorSurfaceMatBuffer, 0, this.mirrorSurfaceMatData);
|
|
1891
|
+
}
|
|
1892
|
+
/** Called by the sync once the surface exists, so the floor yields the plane
|
|
1893
|
+
* on the same frame the mirror appears. */
|
|
1894
|
+
mirrorSurfaceAppeared() {
|
|
1895
|
+
this.writeGroundMirrorField();
|
|
1896
|
+
}
|
|
1897
|
+
/** One dial's live value, or the fallback when the effect never declared it.
|
|
1898
|
+
* Reads the CPU mirror of the params buffer, which setEffectParam keeps
|
|
1899
|
+
* current — so dragging a slider moves the glass on the same frame. */
|
|
1900
|
+
effectDial(fx, name, fallback) {
|
|
1901
|
+
const slot = fx.paramLayout.get(name);
|
|
1902
|
+
return slot ? fx.paramsData[slot.offset] : fallback;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Drive the mirror from whichever effect declares one.
|
|
1906
|
+
*
|
|
1907
|
+
* Runs before reflectionActive is read, because the answer to "is there a
|
|
1908
|
+
* mirror" is this function's output. FIRST match wins and the rest are
|
|
1909
|
+
* ignored: there is one reflection target, so a second mirror is not a second
|
|
1910
|
+
* mirror, it is a fight over the same plane — and a scene that quietly
|
|
1911
|
+
* reflected the wrong one would be far harder to understand than one where
|
|
1912
|
+
* the second mirror does nothing.
|
|
1913
|
+
*
|
|
1914
|
+
* Gated on `weight`, so a scheduled mirror appears and leaves with its strip
|
|
1915
|
+
* and a faded-out one costs no reflection pass at all.
|
|
1916
|
+
*/
|
|
1917
|
+
syncMirrorFromEffects() {
|
|
1918
|
+
let fx = null;
|
|
1919
|
+
for (const e of this.effects) {
|
|
1920
|
+
if (e.hasMirror && e.weight > 0) {
|
|
1921
|
+
fx = e;
|
|
1922
|
+
break;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (!fx) {
|
|
1926
|
+
if (this.mirrorSurface) {
|
|
1927
|
+
this.mirrorSurface = null;
|
|
1928
|
+
this.writeGroundMirrorField();
|
|
1929
|
+
}
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
const had = this.mirrorSurface !== null;
|
|
1933
|
+
const D = Engine.MIRROR_DIALS;
|
|
1934
|
+
const deg = Math.PI / 180;
|
|
1935
|
+
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);
|
|
1936
|
+
const colour = (name, r, g, b) => {
|
|
1937
|
+
const slot = fx.paramLayout.get(name);
|
|
1938
|
+
return slot
|
|
1939
|
+
? { x: fx.paramsData[slot.offset], y: fx.paramsData[slot.offset + 1], z: fx.paramsData[slot.offset + 2] }
|
|
1940
|
+
: { x: r, y: g, z: b };
|
|
1941
|
+
};
|
|
1942
|
+
const tint = colour(D.tint, 1, 1, 1);
|
|
1943
|
+
this.setMirror({
|
|
1944
|
+
position: {
|
|
1945
|
+
x: this.effectDial(fx, D.posX, -11),
|
|
1946
|
+
y: this.effectDial(fx, D.posY, 0),
|
|
1947
|
+
z: this.effectDial(fx, D.posZ, 2),
|
|
1948
|
+
},
|
|
1949
|
+
rotation: rot,
|
|
1950
|
+
// Floored well above zero: a mirror scaled to nothing is a degenerate
|
|
1951
|
+
// quad whose normal is still read for the plane, and the reflection would
|
|
1952
|
+
// fold through a plane nobody can see.
|
|
1953
|
+
width: Math.max(this.effectDial(fx, D.width, 14), 0.1),
|
|
1954
|
+
height: Math.max(this.effectDial(fx, D.height, 24), 0.1),
|
|
1955
|
+
tint,
|
|
1956
|
+
blur: this.effectDial(fx, D.blur, 0),
|
|
1957
|
+
frame: this.effectDial(fx, D.frame, 0.48),
|
|
1958
|
+
frameColor: colour(D.frameColor, 0.631, 0.631, 0.667),
|
|
1959
|
+
});
|
|
1960
|
+
if (!had)
|
|
1961
|
+
this.mirrorSurfaceAppeared();
|
|
1962
|
+
}
|
|
1963
|
+
/** Built on the first setMirror rather than at init: a shader compile and a
|
|
1964
|
+
* pipeline cost load time, and most scenes never place one. */
|
|
1965
|
+
ensureMirrorSurfaceResources() {
|
|
1966
|
+
if (this.mirrorSurfacePipeline)
|
|
1967
|
+
return;
|
|
1968
|
+
this.mirrorSurfaceMatBuffer = this.device.createBuffer({
|
|
1969
|
+
label: "mirror surface material",
|
|
1970
|
+
size: MIRROR_MAT_BYTES,
|
|
1971
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1972
|
+
});
|
|
1973
|
+
this.mirrorSurfaceBindGroupLayout = this.device.createBindGroupLayout({
|
|
1974
|
+
label: "mirror surface layout",
|
|
1975
|
+
entries: [
|
|
1976
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1977
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1978
|
+
{ binding: 2, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1979
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
1980
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
1981
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
1982
|
+
],
|
|
1983
|
+
});
|
|
1984
|
+
this.mirrorSurfacePipeline = this.createRenderPipeline({
|
|
1985
|
+
label: "mirror surface pipeline",
|
|
1986
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.mirrorSurfaceBindGroupLayout] }),
|
|
1987
|
+
shaderModule: this.device.createShaderModule({ label: "mirror surface", code: mirrorShaderWgsl() }),
|
|
1988
|
+
// None: the quad is generated from the vertex index.
|
|
1989
|
+
vertexBuffers: [],
|
|
1990
|
+
fragmentTargets: sceneTargetsFor("mirror", this.sceneFormats),
|
|
1991
|
+
// Both faces. The shader answers for the back — see its side test. Culling
|
|
1992
|
+
// it instead would make a mirror rotated the wrong way INVISIBLE, which is
|
|
1993
|
+
// the one failure a transform panel cannot show you.
|
|
1994
|
+
cullMode: "none",
|
|
1995
|
+
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
1996
|
+
});
|
|
1997
|
+
this.buildMirrorSurfaceBindGroup();
|
|
1998
|
+
// ── The pane in the shadow pass ──
|
|
1999
|
+
const shadowLayout = this.device.createBindGroupLayout({
|
|
2000
|
+
label: "mirror shadow layout",
|
|
2001
|
+
entries: [
|
|
2002
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
2003
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
2004
|
+
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
2005
|
+
],
|
|
2006
|
+
});
|
|
2007
|
+
this.mirrorShadowPipeline = this.device.createRenderPipeline({
|
|
2008
|
+
label: "mirror shadow pipeline",
|
|
2009
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [shadowLayout] }),
|
|
2010
|
+
vertex: {
|
|
2011
|
+
module: this.device.createShaderModule({
|
|
2012
|
+
label: "mirror shadow",
|
|
2013
|
+
code: mirrorShadowWgsl(SHADOW_CASCADES.length),
|
|
2014
|
+
}),
|
|
2015
|
+
entryPoint: "vs",
|
|
2016
|
+
},
|
|
2017
|
+
// Depth only: no fragment stage at all, the pane being opaque across its
|
|
2018
|
+
// whole rectangle with nothing to alpha-test.
|
|
2019
|
+
primitive: { cullMode: "none" },
|
|
2020
|
+
depthStencil: {
|
|
2021
|
+
format: Engine.SHADOW_DEPTH_FORMAT,
|
|
2022
|
+
depthWriteEnabled: true,
|
|
2023
|
+
depthCompare: "less-equal",
|
|
2024
|
+
// The shadow map's own non-reversed convention, and the same bias the
|
|
2025
|
+
// model shadows take — see shadowDepthPipeline.
|
|
2026
|
+
depthBias: 2,
|
|
2027
|
+
depthBiasSlopeScale: 1.5,
|
|
2028
|
+
depthBiasClamp: 0,
|
|
2029
|
+
},
|
|
2030
|
+
});
|
|
2031
|
+
this.mirrorShadowBindGroups = SHADOW_CASCADES.map((_, ci) => {
|
|
2032
|
+
const index = this.device.createBuffer({
|
|
2033
|
+
label: `mirror shadow cascade ${ci}`,
|
|
2034
|
+
size: 16,
|
|
2035
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
2036
|
+
});
|
|
2037
|
+
this.device.queue.writeBuffer(index, 0, new Uint32Array([ci, 0, 0, 0]));
|
|
2038
|
+
return this.device.createBindGroup({
|
|
2039
|
+
label: `mirror shadow bind ${ci}`,
|
|
2040
|
+
layout: shadowLayout,
|
|
2041
|
+
entries: [
|
|
2042
|
+
{ binding: 0, resource: { buffer: this.shadowLightVPBuffer } },
|
|
2043
|
+
{ binding: 1, resource: { buffer: index } },
|
|
2044
|
+
{ binding: 2, resource: { buffer: this.mirrorSurfaceMatBuffer } },
|
|
2045
|
+
],
|
|
2046
|
+
});
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
/** Rebound whenever the reflection target is recreated, alongside the
|
|
2050
|
+
* ground's — the two read the same texture. */
|
|
2051
|
+
buildMirrorSurfaceBindGroup() {
|
|
2052
|
+
if (!this.mirrorSurfaceBindGroupLayout || !this.mirrorSurfaceMatBuffer)
|
|
2053
|
+
return;
|
|
2054
|
+
if (!this.mirrorColorView || !this.mirrorMaskView)
|
|
2055
|
+
return;
|
|
2056
|
+
this.mirrorSurfaceBindGroup = this.device.createBindGroup({
|
|
2057
|
+
label: "mirror surface bind",
|
|
2058
|
+
layout: this.mirrorSurfaceBindGroupLayout,
|
|
2059
|
+
entries: [
|
|
2060
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
2061
|
+
{ binding: 1, resource: { buffer: this.mirrorVPBuffer } },
|
|
2062
|
+
{ binding: 2, resource: { buffer: this.mirrorSurfaceMatBuffer } },
|
|
2063
|
+
{ binding: 3, resource: this.mirrorColorView },
|
|
2064
|
+
{ binding: 4, resource: this.materialSampler },
|
|
2065
|
+
{ binding: 5, resource: this.mirrorMaskView },
|
|
2066
|
+
],
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
/** The glass itself, in the camera pass. Never in the MIRROR pass: a mirror
|
|
2070
|
+
* drawn into its own reflection is a corridor, and with one target it is a
|
|
2071
|
+
* corridor of last frame's frame. */
|
|
2072
|
+
renderMirrorSurface(pass) {
|
|
2073
|
+
if (!this.mirrorSurface || !this.mirrorSurfacePipeline || !this.mirrorSurfaceBindGroup)
|
|
2074
|
+
return;
|
|
2075
|
+
pass.setPipeline(this.mirrorSurfacePipeline);
|
|
2076
|
+
pass.setBindGroup(0, this.mirrorSurfaceBindGroup);
|
|
2077
|
+
pass.draw(6);
|
|
2078
|
+
}
|
|
1731
2079
|
ensureReflectionDebugPipeline() {
|
|
1732
2080
|
if (!this.mirrorColorView)
|
|
1733
2081
|
return false;
|
|
@@ -1779,7 +2127,25 @@ export class Engine {
|
|
|
1779
2127
|
/** Refold the live camera with the reflection — a copy and a handful of
|
|
1780
2128
|
* sign flips; cheap enough to run every frame a mirror is on. */
|
|
1781
2129
|
updateMirrorCamera() {
|
|
1782
|
-
|
|
2130
|
+
// The placed surface wins over the floor — see the note on mirrorPlane.
|
|
2131
|
+
// Recomputed rather than cached because the mirror is the thing a gizmo
|
|
2132
|
+
// drags, and a plane that lagged the surface by a frame is a reflection
|
|
2133
|
+
// that slides on it.
|
|
2134
|
+
if (this.mirrorSurface) {
|
|
2135
|
+
const m = this.mirrorSurfaceModel;
|
|
2136
|
+
planeFromPointNormal(m[12], m[13], m[14], m[8], m[9], m[10], this.mirrorPlane);
|
|
2137
|
+
}
|
|
2138
|
+
else {
|
|
2139
|
+
this.mirrorPlane.set(Engine.GROUND_PLANE);
|
|
2140
|
+
}
|
|
2141
|
+
// The ground inside the mirror pass clips against this same plane — a
|
|
2142
|
+
// mirror shows nothing behind itself, and the floor is what is behind one.
|
|
2143
|
+
if (this.groundClipMirrorBuffer) {
|
|
2144
|
+
this.groundClipData.set(this.mirrorPlane, 0);
|
|
2145
|
+
this.groundClipData[4] = 1;
|
|
2146
|
+
this.device.queue.writeBuffer(this.groundClipMirrorBuffer, 0, this.groundClipData);
|
|
2147
|
+
}
|
|
2148
|
+
buildMirrorCamera(this.cameraMatrixData, this.mirrorPlane, this.mirrorCameraData);
|
|
1783
2149
|
this.device.queue.writeBuffer(this.mirrorCameraBuffer, 0, this.mirrorCameraData);
|
|
1784
2150
|
Mat4.multiplyArrays(this.cameraMatrixData, 16, this.mirrorCameraData, 0, this.mirrorVPData, 0);
|
|
1785
2151
|
// projA/projB ARE m[10] and m[14] of the projection (the dofU discipline:
|
|
@@ -1805,20 +2171,22 @@ export class Engine {
|
|
|
1805
2171
|
renderMirrorPass(encoder) {
|
|
1806
2172
|
if (!this.reflectionActive || !this.mirrorPassDescriptor)
|
|
1807
2173
|
return;
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
//
|
|
1813
|
-
//
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2174
|
+
// No early return on an empty cast. The CLEAR is the load-bearing half of
|
|
2175
|
+
// this pass: a mirror surface samples the target whether or not anything
|
|
2176
|
+
// was drawn into it, and a pass skipped for having no models leaves it
|
|
2177
|
+
// holding whatever the allocation held — undefined memory, sampled onto a
|
|
2178
|
+
// plane in the middle of the scene.
|
|
2179
|
+
// CLEARS TRANSPARENT, and the alpha is the whole point: it is 1 exactly
|
|
2180
|
+
// where the pass drew something and 0 where it drew nothing, so every
|
|
2181
|
+
// consumer can composite the reflection over its own surface instead of
|
|
2182
|
+
// over a guess.
|
|
2183
|
+
//
|
|
2184
|
+
// It used to clear to the scene's background, linearised, so that empty
|
|
2185
|
+
// regions read as backdrop rather than black. That colour then rode the
|
|
2186
|
+
// view transform a SECOND time — the real backdrop composites after it —
|
|
2187
|
+
// and the result was a panel paler and flatter than the backdrop beside it.
|
|
2188
|
+
// A mirror that fogs. Coverage costs nothing and lets the genuine backdrop
|
|
2189
|
+
// through, which is also what an empty mirror shows: the room.
|
|
1822
2190
|
// The descriptor is reused every frame, so the stamp is set on it rather
|
|
1823
2191
|
// than passed — same as the scene pass, which is built once too.
|
|
1824
2192
|
this.mirrorPassDescriptor.timestampWrites = this.stamps("mirror");
|
|
@@ -1829,12 +2197,26 @@ export class Engine {
|
|
|
1829
2197
|
bundles.push(this.mirrorOpaqueBundle);
|
|
1830
2198
|
if (this.mirrorTransparentBundle)
|
|
1831
2199
|
bundles.push(this.mirrorTransparentBundle);
|
|
1832
|
-
|
|
2200
|
+
if (bundles.length > 0)
|
|
2201
|
+
pass.executeBundles(bundles);
|
|
1833
2202
|
// Particles and ribbons are scene geometry, and a mirror that dropped them
|
|
1834
2203
|
// showed a dancer whose hand ribbon cast no reflection. Field effects stay
|
|
1835
2204
|
// out BY DESIGN: they are display-space overlays composited after the view
|
|
1836
2205
|
// transform, with no world position to mirror. executeBundles reset the
|
|
1837
2206
|
// pass state, so these draws bind everything themselves — which they do.
|
|
2207
|
+
// THE FLOOR, inside the reflection.
|
|
2208
|
+
//
|
|
2209
|
+
// Without it a standing mirror shows a figure hanging in empty colour, with
|
|
2210
|
+
// nothing under her feet to say where she is — which is most of what made
|
|
2211
|
+
// the pane read as a dark sheet rather than as glass. It never came up for
|
|
2212
|
+
// the floor mirror because a floor does not reflect itself.
|
|
2213
|
+
//
|
|
2214
|
+
// Skipped while the FLOOR's own mirror is on: that would be the floor
|
|
2215
|
+
// sampling a reflection it is currently drawing into, one frame stale, and
|
|
2216
|
+
// the two mirrors are already fighting over the single reflection plane.
|
|
2217
|
+
if (this.hasGround && (this.mirrorSurface !== null || this.groundMirror === 0) && this.groundMirrorViewBindGroup) {
|
|
2218
|
+
this.renderGroundWith(pass, this.groundMirrorViewBindGroup, true);
|
|
2219
|
+
}
|
|
1838
2220
|
this.renderParticles(pass, "mirror");
|
|
1839
2221
|
this.drawTrails(pass, "mirror");
|
|
1840
2222
|
pass.end();
|
|
@@ -1846,7 +2228,11 @@ export class Engine {
|
|
|
1846
2228
|
* samples level 0 exactly and the chain would be work nobody reads.
|
|
1847
2229
|
*/
|
|
1848
2230
|
renderMirrorBlurChain(encoder) {
|
|
1849
|
-
|
|
2231
|
+
// EITHER consumer's dial. Gated on the ground's alone, a mirror surface with
|
|
2232
|
+
// blur up sampled mip levels that had never been rendered into — undefined
|
|
2233
|
+
// memory, which read as the model going black the moment the slider moved.
|
|
2234
|
+
const blur = Math.max(this.groundMirrorBlur, this.mirrorSurface?.blur ?? 0);
|
|
2235
|
+
if (blur <= 0 || this.mirrorMipCount < 2)
|
|
1850
2236
|
return;
|
|
1851
2237
|
if (!this.mirrorBlurBindGroups) {
|
|
1852
2238
|
const layout = this.bloomDownsamplePipeline.getBindGroupLayout(0);
|
|
@@ -1872,6 +2258,47 @@ export class Engine {
|
|
|
1872
2258
|
p.draw(3);
|
|
1873
2259
|
p.end();
|
|
1874
2260
|
}
|
|
2261
|
+
// COVERAGE, down the same chain. Colour in the reflection target is
|
|
2262
|
+
// premultiplied by this, so the pair has to be blurred together: blur the
|
|
2263
|
+
// colour alone and the pane divides a colour that spread into the empty sky
|
|
2264
|
+
// by a coverage that did not, and the reflection darkens as the dial rises.
|
|
2265
|
+
if (!this.mirrorMaskDownsamplePipeline) {
|
|
2266
|
+
const module = this.device.createShaderModule({
|
|
2267
|
+
label: "mirror coverage downsample",
|
|
2268
|
+
code: MIRROR_MASK_DOWNSAMPLE_WGSL,
|
|
2269
|
+
});
|
|
2270
|
+
this.mirrorMaskDownsamplePipeline = this.device.createRenderPipeline({
|
|
2271
|
+
label: "mirror coverage downsample pipeline",
|
|
2272
|
+
layout: "auto",
|
|
2273
|
+
vertex: { module, entryPoint: "vs" },
|
|
2274
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: Engine.BLOOM_MASK_FORMAT }] },
|
|
2275
|
+
primitive: { topology: "triangle-list" },
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
if (!this.mirrorMaskBlurBindGroups) {
|
|
2279
|
+
const layout = this.mirrorMaskDownsamplePipeline.getBindGroupLayout(0);
|
|
2280
|
+
this.mirrorMaskBlurBindGroups = [];
|
|
2281
|
+
for (let i = 1; i < this.mirrorMipCount; i++) {
|
|
2282
|
+
this.mirrorMaskBlurBindGroups.push(this.device.createBindGroup({
|
|
2283
|
+
label: `mirror coverage blur ${i}`,
|
|
2284
|
+
layout,
|
|
2285
|
+
entries: [
|
|
2286
|
+
{ binding: 0, resource: this.mirrorMaskMipViews[i - 1] },
|
|
2287
|
+
{ binding: 1, resource: this.bloomSampler },
|
|
2288
|
+
],
|
|
2289
|
+
}));
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
for (let i = 1; i < this.mirrorMipCount; i++) {
|
|
2293
|
+
const p = encoder.beginRenderPass({
|
|
2294
|
+
label: `mirror coverage blur ${i}`,
|
|
2295
|
+
colorAttachments: [{ view: this.mirrorMaskMipViews[i], loadOp: "clear", storeOp: "store" }],
|
|
2296
|
+
});
|
|
2297
|
+
p.setPipeline(this.mirrorMaskDownsamplePipeline);
|
|
2298
|
+
p.setBindGroup(0, this.mirrorMaskBlurBindGroups[i - 1]);
|
|
2299
|
+
p.draw(3);
|
|
2300
|
+
p.end();
|
|
2301
|
+
}
|
|
1875
2302
|
}
|
|
1876
2303
|
/**
|
|
1877
2304
|
* Can this device multisample the id format at the pass's sample count?
|
|
@@ -2595,13 +3022,17 @@ export class Engine {
|
|
|
2595
3022
|
// lightEmit counts as a mount on its own: a pure lighting rig draws nothing
|
|
2596
3023
|
// and is still an effect — it is how a scene gets stage lights without also
|
|
2597
3024
|
// getting geometry it did not ask for.
|
|
2598
|
-
|
|
3025
|
+
// #mirror is a mount on the same footing, and for the same reason: a
|
|
3026
|
+
// reflection re-renders the scene from a folded camera, which is a pass and
|
|
3027
|
+
// not a shader, so the effect declares it and draws nothing itself.
|
|
3028
|
+
if (!hasBackground && !hasForeground && !wantsParticles && !wantsTrails && !hasLightEmit(wgsl) && !d.mirror) {
|
|
2599
3029
|
return { ok: false, diagnostics: [
|
|
2600
3030
|
"an effect must define fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f, " +
|
|
2601
3031
|
"fn foreground(ray: vec3f, uv: vec2f, time: f32, depth: f32) -> vec4f, " +
|
|
2602
3032
|
"the particle trio (particleInit/particleStep/particleShade), " +
|
|
2603
3033
|
"the ribbon pair (trailWidth/trailShade), " +
|
|
2604
|
-
"
|
|
3034
|
+
"fn lightEmit(i: u32) -> RzLight with #lights <n>, " +
|
|
3035
|
+
"or #mirror",
|
|
2605
3036
|
], mounts: noMounts, params: [], duration: 0 };
|
|
2606
3037
|
}
|
|
2607
3038
|
const mounts = { background: hasBackground, foreground: hasForeground };
|
|
@@ -2901,6 +3332,7 @@ export class Engine {
|
|
|
2901
3332
|
paramLayout: layout,
|
|
2902
3333
|
paramsBuffer,
|
|
2903
3334
|
paramsData,
|
|
3335
|
+
hasMirror: d.mirror,
|
|
2904
3336
|
hasBackground,
|
|
2905
3337
|
hasForeground,
|
|
2906
3338
|
// The author's OWN source, not the assembled module: the assembled one
|
|
@@ -5176,6 +5608,11 @@ export class Engine {
|
|
|
5176
5608
|
// The baked frost tile — see bakeGroundNoise. Sampled with binding 10's
|
|
5177
5609
|
// repeat sampler, so it brings no sampler of its own.
|
|
5178
5610
|
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
5611
|
+
// The mirror pass's resolved aux. .g is the reflection's COVERAGE, and
|
|
5612
|
+
// the colour target cannot carry it — rg11b10ufloat has no alpha.
|
|
5613
|
+
{ binding: 13, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
5614
|
+
// The mirror's plane, and whether this draw is inside the mirror pass.
|
|
5615
|
+
{ binding: 14, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
5179
5616
|
],
|
|
5180
5617
|
});
|
|
5181
5618
|
this.groundShadowPipelineDesc = {
|
|
@@ -5222,11 +5659,33 @@ export class Engine {
|
|
|
5222
5659
|
{ binding: 1, resource: this.materialSampler },
|
|
5223
5660
|
],
|
|
5224
5661
|
});
|
|
5662
|
+
// The same group on the MIRROR camera. An outline is geometry expanded
|
|
5663
|
+
// along its normals in CLIP space, so it has to be expanded through the
|
|
5664
|
+
// camera that is drawing it — handed the real camera it would trace a hull
|
|
5665
|
+
// around where she is standing, not around her reflection.
|
|
5666
|
+
this.outlineMirrorPerFrameBindGroup = this.device.createBindGroup({
|
|
5667
|
+
label: "outline per-frame bind group (mirror)",
|
|
5668
|
+
layout: this.outlinePerFrameBindGroupLayout,
|
|
5669
|
+
entries: [
|
|
5670
|
+
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
5671
|
+
{ binding: 1, resource: this.materialSampler },
|
|
5672
|
+
],
|
|
5673
|
+
});
|
|
5225
5674
|
const outlineShaderModule = this.device.createShaderModule({
|
|
5226
5675
|
label: "outline shaders",
|
|
5227
5676
|
code: outlineShaderWgsl(),
|
|
5228
5677
|
});
|
|
5229
|
-
|
|
5678
|
+
// FRONT, not back, and not "none". The outline is an inverted hull: the
|
|
5679
|
+
// model expanded along its normals, drawn with only its BACK faces showing
|
|
5680
|
+
// so the silhouette survives and the interior does not ink over the model.
|
|
5681
|
+
// A reflection has determinant -1 (reflection.ts), so those very triangles
|
|
5682
|
+
// arrive front-facing in the mirror pass and `back` culls every one of them
|
|
5683
|
+
// — which is why the mirror had no outline at all. `none` would draw the
|
|
5684
|
+
// hull's front faces too, painting the model over with ink.
|
|
5685
|
+
// Annotated so the string fields keep their literal types: pulled out of the
|
|
5686
|
+
// call into a const, "back" widens to string and the two pipelines below
|
|
5687
|
+
// stop type-checking.
|
|
5688
|
+
const outlineDesc = {
|
|
5230
5689
|
label: "outline pipeline",
|
|
5231
5690
|
layout: outlinePipelineLayout,
|
|
5232
5691
|
shaderModule: outlineShaderModule,
|
|
@@ -5263,6 +5722,12 @@ export class Engine {
|
|
|
5263
5722
|
stencilReadMask: 0xff,
|
|
5264
5723
|
stencilWriteMask: 0,
|
|
5265
5724
|
},
|
|
5725
|
+
};
|
|
5726
|
+
this.outlinePipeline = this.createRenderPipeline(outlineDesc);
|
|
5727
|
+
this.outlineMirrorPipeline = this.createRenderPipeline({
|
|
5728
|
+
...outlineDesc,
|
|
5729
|
+
label: "outline pipeline (mirror)",
|
|
5730
|
+
cullMode: "front",
|
|
5266
5731
|
});
|
|
5267
5732
|
// ─── Selection overlay (screen-space edge-detect on a per-material mask) ───
|
|
5268
5733
|
// Reuses outline camera + main skinMats bind group layouts. No group 2 (no per-mat uniform).
|
|
@@ -5820,12 +6285,25 @@ export class Engine {
|
|
|
5820
6285
|
}
|
|
5821
6286
|
this.mirrorBlurBindGroups = null;
|
|
5822
6287
|
this.mirrorMaskMsTexture = this.device.createTexture({
|
|
5823
|
-
label: "mirror aux (msaa
|
|
6288
|
+
label: "mirror aux (msaa)",
|
|
5824
6289
|
size: [mw, mh],
|
|
5825
6290
|
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
5826
6291
|
format: Engine.BLOOM_MASK_FORMAT,
|
|
5827
6292
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
5828
6293
|
});
|
|
6294
|
+
this.mirrorMaskTexture = this.device.createTexture({
|
|
6295
|
+
label: "mirror aux resolve (coverage)",
|
|
6296
|
+
size: [mw, mh],
|
|
6297
|
+
mipLevelCount: this.mirrorMipCount,
|
|
6298
|
+
format: Engine.BLOOM_MASK_FORMAT,
|
|
6299
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
6300
|
+
});
|
|
6301
|
+
this.mirrorMaskView = this.mirrorMaskTexture.createView();
|
|
6302
|
+
this.mirrorMaskMipViews = [];
|
|
6303
|
+
for (let i = 0; i < this.mirrorMipCount; i++) {
|
|
6304
|
+
this.mirrorMaskMipViews.push(this.mirrorMaskTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }));
|
|
6305
|
+
}
|
|
6306
|
+
this.mirrorMaskBlurBindGroups = null;
|
|
5829
6307
|
this.mirrorIdMsTexture = mrtIdsEnabled()
|
|
5830
6308
|
? this.device.createTexture({
|
|
5831
6309
|
label: "mirror id (msaa, discarded)",
|
|
@@ -5854,6 +6332,10 @@ export class Engine {
|
|
|
5854
6332
|
};
|
|
5855
6333
|
const mirrorMask = {
|
|
5856
6334
|
view: this.mirrorMaskMsTexture.createView(),
|
|
6335
|
+
// RESOLVED now rather than discarded: .g is the reflection's coverage.
|
|
6336
|
+
// Mip 0 of the chain, not the whole-chain view: a resolve target is a
|
|
6337
|
+
// single level.
|
|
6338
|
+
resolveTarget: this.mirrorMaskMipViews[0],
|
|
5857
6339
|
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
5858
6340
|
loadOp: "clear",
|
|
5859
6341
|
storeOp: "discard",
|
|
@@ -5881,8 +6363,10 @@ export class Engine {
|
|
|
5881
6363
|
stencilStoreOp: "discard",
|
|
5882
6364
|
},
|
|
5883
6365
|
};
|
|
5884
|
-
// The ground
|
|
6366
|
+
// The ground and the mirror surface both bind the reflection resolve;
|
|
6367
|
+
// rebind both against the new one.
|
|
5885
6368
|
this.buildGroundBindGroup();
|
|
6369
|
+
this.buildMirrorSurfaceBindGroup();
|
|
5886
6370
|
// Bloom pyramid: mip 0 is half-res, each subsequent mip halves again.
|
|
5887
6371
|
// Mip count chosen so the coarsest mip is ≥4 px on the short side, capped at BLOOM_MAX_LEVELS.
|
|
5888
6372
|
const bw = Math.max(1, Math.floor(width / 2));
|
|
@@ -6400,11 +6884,6 @@ export class Engine {
|
|
|
6400
6884
|
size: 32,
|
|
6401
6885
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6402
6886
|
});
|
|
6403
|
-
this.wireframeHoverUniformBuffer = this.device.createBuffer({
|
|
6404
|
-
label: "wireframe color (hovered material)",
|
|
6405
|
-
size: 32,
|
|
6406
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6407
|
-
});
|
|
6408
6887
|
const wireBg0 = this.device.createBindGroupLayout({
|
|
6409
6888
|
label: "wireframe group 0 layout (camera + wire)",
|
|
6410
6889
|
entries: [
|
|
@@ -6513,6 +6992,58 @@ export class Engine {
|
|
|
6513
6992
|
},
|
|
6514
6993
|
multisample: { count: Engine.OVERLAY_SAMPLE_COUNT },
|
|
6515
6994
|
});
|
|
6995
|
+
// The selection fill: vsDepth/fs again, no bias needed since it draws
|
|
6996
|
+
// ALWAYS-ahead like the rest of this layer's basic primitives (matching
|
|
6997
|
+
// the red edge lines selectMaterialFaces hands back — the two are meant
|
|
6998
|
+
// to read as one highlight, not fight each other's depth handling) —
|
|
6999
|
+
// never occluded, which a selection drawn over the far side of a body
|
|
7000
|
+
// the box also caught would otherwise need X-ray to explain.
|
|
7001
|
+
this.selectionFillUniformBuffer = this.device.createBuffer({
|
|
7002
|
+
label: "selection fill color",
|
|
7003
|
+
size: 32,
|
|
7004
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
7005
|
+
});
|
|
7006
|
+
this.device.queue.writeBuffer(this.selectionFillUniformBuffer, 0, new Float32Array([1, 0.15, 0.15, 0.45, 0, 0, 0, 0]));
|
|
7007
|
+
this.selectionFillBindGroup = this.device.createBindGroup({
|
|
7008
|
+
label: "selection fill bind group",
|
|
7009
|
+
layout: wireBg0,
|
|
7010
|
+
entries: [
|
|
7011
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
7012
|
+
{ binding: 1, resource: { buffer: this.selectionFillUniformBuffer } },
|
|
7013
|
+
],
|
|
7014
|
+
});
|
|
7015
|
+
this.selectionFillPipeline = this.device.createRenderPipeline({
|
|
7016
|
+
label: "selection fill pipeline",
|
|
7017
|
+
layout: this.device.createPipelineLayout({
|
|
7018
|
+
label: "selection fill pipeline layout",
|
|
7019
|
+
bindGroupLayouts: [wireBg0, this.wireframeSkinLayout],
|
|
7020
|
+
}),
|
|
7021
|
+
vertex: {
|
|
7022
|
+
module: wireShader,
|
|
7023
|
+
entryPoint: "vsDepth",
|
|
7024
|
+
buffers: [
|
|
7025
|
+
{ arrayStride: 8 * 4, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] },
|
|
7026
|
+
{ arrayStride: 4 * 2, attributes: [{ shaderLocation: 1, offset: 0, format: "uint16x4" }] },
|
|
7027
|
+
{ arrayStride: 4, attributes: [{ shaderLocation: 2, offset: 0, format: "unorm8x4" }] },
|
|
7028
|
+
],
|
|
7029
|
+
},
|
|
7030
|
+
fragment: {
|
|
7031
|
+
module: wireShader,
|
|
7032
|
+
entryPoint: "fs",
|
|
7033
|
+
targets: [
|
|
7034
|
+
{
|
|
7035
|
+
format: this.presentationFormat,
|
|
7036
|
+
blend: {
|
|
7037
|
+
color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
7038
|
+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
7039
|
+
},
|
|
7040
|
+
},
|
|
7041
|
+
],
|
|
7042
|
+
},
|
|
7043
|
+
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
7044
|
+
depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: "always" },
|
|
7045
|
+
multisample: { count: Engine.OVERLAY_SAMPLE_COUNT },
|
|
7046
|
+
});
|
|
6516
7047
|
this.wireframeBindGroup = this.device.createBindGroup({
|
|
6517
7048
|
label: "wireframe bind group",
|
|
6518
7049
|
layout: wireBg0,
|
|
@@ -6529,14 +7060,6 @@ export class Engine {
|
|
|
6529
7060
|
{ binding: 1, resource: { buffer: this.wireframeSeamUniformBuffer } },
|
|
6530
7061
|
],
|
|
6531
7062
|
});
|
|
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
7063
|
this.overlayPassDescriptor = {
|
|
6541
7064
|
label: "overlay pass",
|
|
6542
7065
|
timestampWrites: this.stamps("overlay"),
|
|
@@ -7047,6 +7570,12 @@ export class Engine {
|
|
|
7047
7570
|
setCameraDistance(d) {
|
|
7048
7571
|
this.camera.radius = d;
|
|
7049
7572
|
}
|
|
7573
|
+
/** Which button drags orbit the view — see Camera.setOrbitButton. Default
|
|
7574
|
+
* "left" (this engine's own, original binding, every existing consumer's
|
|
7575
|
+
* default); "middle" is Blender's, opt-in only. */
|
|
7576
|
+
setCameraOrbitButton(button) {
|
|
7577
|
+
this.camera?.setOrbitButton(button);
|
|
7578
|
+
}
|
|
7050
7579
|
getCameraAlpha() {
|
|
7051
7580
|
return this.camera.alpha;
|
|
7052
7581
|
}
|
|
@@ -7663,6 +8192,26 @@ export class Engine {
|
|
|
7663
8192
|
setGroundVisible(on) {
|
|
7664
8193
|
this.groundHidden = !on;
|
|
7665
8194
|
}
|
|
8195
|
+
/**
|
|
8196
|
+
* Moves a model to a new key. Nothing about it is rebuilt — same GPU
|
|
8197
|
+
* buffers, same style groups, same physics — only which string the rest
|
|
8198
|
+
* of the engine finds it under changes.
|
|
8199
|
+
*
|
|
8200
|
+
* What a document reload uses to swap in a freshly-loaded replacement
|
|
8201
|
+
* under the ORIGINAL id with no visible gap: the replacement loads under
|
|
8202
|
+
* a throwaway key while the current one keeps rendering untouched, and
|
|
8203
|
+
* only this call — a Map move, nothing async, nothing rebuilt — hands it
|
|
8204
|
+
* the name everything else in the app still expects.
|
|
8205
|
+
*/
|
|
8206
|
+
renameModel(oldName, newName) {
|
|
8207
|
+
const inst = this.modelInstances.get(oldName);
|
|
8208
|
+
if (!inst || oldName === newName || this.modelInstances.has(newName))
|
|
8209
|
+
return;
|
|
8210
|
+
this.modelInstances.delete(oldName);
|
|
8211
|
+
inst.name = newName;
|
|
8212
|
+
inst.model.setName(newName);
|
|
8213
|
+
this.modelInstances.set(newName, inst);
|
|
8214
|
+
}
|
|
7666
8215
|
removeModel(name) {
|
|
7667
8216
|
const inst = this.modelInstances.get(name);
|
|
7668
8217
|
if (!inst)
|
|
@@ -8112,6 +8661,269 @@ export class Engine {
|
|
|
8112
8661
|
}
|
|
8113
8662
|
return best;
|
|
8114
8663
|
}
|
|
8664
|
+
/**
|
|
8665
|
+
* Overwrites BASE (pre-morph) vertex positions — a PERMANENT geometry edit
|
|
8666
|
+
* (a bone scale's own vertex half, say), not a transient morph: nothing
|
|
8667
|
+
* here is undone by a weight going back to 0.
|
|
8668
|
+
*
|
|
8669
|
+
* Three places have to agree, or the very next frame — or the next morph
|
|
8670
|
+
* weight change — quietly reverts this: the model's own CPU copy
|
|
8671
|
+
* (Model.setVertexPositions), the live render buffer, and, for a model
|
|
8672
|
+
* whose morphs run on the GPU, the compute pass's OWN base-positions
|
|
8673
|
+
* buffer, which it recomputes from on its next dispatch regardless of
|
|
8674
|
+
* whatever a direct vertex-buffer write just put there.
|
|
8675
|
+
*/
|
|
8676
|
+
setVertexPositions(modelName, updates) {
|
|
8677
|
+
const inst = this.modelInstances.get(modelName);
|
|
8678
|
+
if (!inst || updates.length === 0)
|
|
8679
|
+
return;
|
|
8680
|
+
inst.model.setVertexPositions(updates);
|
|
8681
|
+
const scratch = this.vec3Scratch;
|
|
8682
|
+
for (const { index, position } of updates) {
|
|
8683
|
+
scratch[0] = position[0];
|
|
8684
|
+
scratch[1] = position[1];
|
|
8685
|
+
scratch[2] = position[2];
|
|
8686
|
+
this.device.queue.writeBuffer(inst.vertexBuffer, index * 32, scratch);
|
|
8687
|
+
if (inst.gpuMorph)
|
|
8688
|
+
this.device.queue.writeBuffer(inst.gpuMorph.baseBuf, index * 12, scratch);
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
8691
|
+
/**
|
|
8692
|
+
* Moves bones to new WORLD bind (rest) positions — a permanent rig edit
|
|
8693
|
+
* (a bone scale's own bone half), never a pose. Patches bindTranslation
|
|
8694
|
+
* (relative to the bone's CURRENT parent world position, read fresh off
|
|
8695
|
+
* its own inverseBindMatrix — pass parents before children in one call if
|
|
8696
|
+
* a bone's OWN local offset needs to stay exactly consistent, though nothing
|
|
8697
|
+
* downstream of this actually reads bindTranslation live: skinning, the
|
|
8698
|
+
* bone overlay and picking all read inverseBindMatrices directly, which
|
|
8699
|
+
* this always writes correctly regardless of order) and the cached
|
|
8700
|
+
* inverse-bind matrix directly, so every one of those picks the new
|
|
8701
|
+
* position up on the very next frame with nothing else to recompute.
|
|
8702
|
+
*/
|
|
8703
|
+
setBoneBindPositions(modelName, updates) {
|
|
8704
|
+
const inst = this.modelInstances.get(modelName);
|
|
8705
|
+
if (!inst || updates.length === 0)
|
|
8706
|
+
return;
|
|
8707
|
+
const skeleton = inst.model.getSkeleton();
|
|
8708
|
+
const invBind = skeleton.inverseBindMatrices;
|
|
8709
|
+
for (const { index, position } of updates) {
|
|
8710
|
+
const bone = skeleton.bones[index];
|
|
8711
|
+
if (!bone)
|
|
8712
|
+
continue;
|
|
8713
|
+
const parent = bone.parentIndex >= 0 ? bone.parentIndex : -1;
|
|
8714
|
+
const parentPos = parent >= 0
|
|
8715
|
+
? [-invBind[parent * 16 + 12], -invBind[parent * 16 + 13], -invBind[parent * 16 + 14]]
|
|
8716
|
+
: [0, 0, 0];
|
|
8717
|
+
bone.bindTranslation = [position[0] - parentPos[0], position[1] - parentPos[1], position[2] - parentPos[2]];
|
|
8718
|
+
invBind[index * 16 + 12] = -position[0];
|
|
8719
|
+
invBind[index * 16 + 13] = -position[1];
|
|
8720
|
+
invBind[index * 16 + 14] = -position[2];
|
|
8721
|
+
}
|
|
8722
|
+
}
|
|
8723
|
+
/**
|
|
8724
|
+
* Every face of one material fully inside a screen-space rectangle, on the
|
|
8725
|
+
* POSED mesh — same reason pickMaterial reads it posed: a drag selects
|
|
8726
|
+
* what a costume actually looks like, not the bind pose underneath it.
|
|
8727
|
+
*
|
|
8728
|
+
* A face counts as "inside" only when all three corners project into the
|
|
8729
|
+
* rect. A triangle straddling the edge stays out — splitting a material by
|
|
8730
|
+
* a box is a spatial cut, and a half-caught triangle would tear a seam
|
|
8731
|
+
* that started nowhere the box actually touched.
|
|
8732
|
+
*
|
|
8733
|
+
* `faceIndices` are LOCAL to the material's own face list (0 = its first
|
|
8734
|
+
* triangle) — what a document transform like splitMaterial needs, since it
|
|
8735
|
+
* never has to know where in the shared index buffer this material's run
|
|
8736
|
+
* starts. `lines` is every caught face's own three edges, in POSED world
|
|
8737
|
+
* space and ready to hand straight to setOverlay — a selection reads as the
|
|
8738
|
+
* FACES it covers, not a scatter of points at their corners, and a second
|
|
8739
|
+
* query back through skinning just to draw it would double the cost of
|
|
8740
|
+
* every drag.
|
|
8741
|
+
*
|
|
8742
|
+
* x0/y0/x1/y1 are CSS pixels relative to the canvas, same origin
|
|
8743
|
+
* pickMaterial's x/y use. Order does not matter — a drag can go in any
|
|
8744
|
+
* direction.
|
|
8745
|
+
*/
|
|
8746
|
+
selectMaterialFaces(modelName, materialName, x0, y0, x1, y1) {
|
|
8747
|
+
const empty = { faceIndices: [], lines: [] };
|
|
8748
|
+
const inst = this.modelInstances.get(modelName);
|
|
8749
|
+
if (!inst || !this.camera || inst.isStage || inst.isPlane || inst.isProp)
|
|
8750
|
+
return empty;
|
|
8751
|
+
const width = this.canvas.clientWidth;
|
|
8752
|
+
const height = this.canvas.clientHeight;
|
|
8753
|
+
if (width <= 0 || height <= 0)
|
|
8754
|
+
return empty;
|
|
8755
|
+
const left = Math.min(x0, x1);
|
|
8756
|
+
const right = Math.max(x0, x1);
|
|
8757
|
+
const top = Math.min(y0, y1);
|
|
8758
|
+
const bottom = Math.max(y0, y1);
|
|
8759
|
+
const model = inst.model;
|
|
8760
|
+
const materials = model.getMaterials();
|
|
8761
|
+
const mIndex = materials.findIndex((m) => m.name === materialName);
|
|
8762
|
+
if (mIndex < 0)
|
|
8763
|
+
return empty;
|
|
8764
|
+
const vp = this.camera.getProjectionMatrix().multiply(this.camera.getViewMatrix()).values;
|
|
8765
|
+
const { positions } = model.getGeometry();
|
|
8766
|
+
const count = positions.length / 3;
|
|
8767
|
+
const { joints, weights } = model.getSkinning();
|
|
8768
|
+
const skin = model.getSkinMatrices();
|
|
8769
|
+
// Skin once, same as pickMaterial: three slots per vertex, screen x/y and
|
|
8770
|
+
// clip w — cw doubles as "behind the camera" for the in-rect test below.
|
|
8771
|
+
if (!this.materialSelectScratch || this.materialSelectScratch.length !== count * 3) {
|
|
8772
|
+
this.materialSelectScratch = new Float32Array(count * 3);
|
|
8773
|
+
}
|
|
8774
|
+
const proj = this.materialSelectScratch;
|
|
8775
|
+
const skinnedWorld = (v) => {
|
|
8776
|
+
const bx = positions[v * 3];
|
|
8777
|
+
const by = positions[v * 3 + 1];
|
|
8778
|
+
const bz = positions[v * 3 + 2];
|
|
8779
|
+
let px = 0;
|
|
8780
|
+
let py = 0;
|
|
8781
|
+
let pz = 0;
|
|
8782
|
+
for (let k = 0; k < 4; k++) {
|
|
8783
|
+
const w = weights[v * 4 + k] / 255;
|
|
8784
|
+
if (w === 0)
|
|
8785
|
+
continue;
|
|
8786
|
+
const m = joints[v * 4 + k] * 16;
|
|
8787
|
+
px += w * (skin[m] * bx + skin[m + 4] * by + skin[m + 8] * bz + skin[m + 12]);
|
|
8788
|
+
py += w * (skin[m + 1] * bx + skin[m + 5] * by + skin[m + 9] * bz + skin[m + 13]);
|
|
8789
|
+
pz += w * (skin[m + 2] * bx + skin[m + 6] * by + skin[m + 10] * bz + skin[m + 14]);
|
|
8790
|
+
}
|
|
8791
|
+
return [px, py, pz];
|
|
8792
|
+
};
|
|
8793
|
+
for (let v = 0; v < count; v++) {
|
|
8794
|
+
const [px, py, pz] = skinnedWorld(v);
|
|
8795
|
+
const cw = vp[3] * px + vp[7] * py + vp[11] * pz + vp[15];
|
|
8796
|
+
proj[v * 3 + 2] = cw;
|
|
8797
|
+
if (cw <= 1e-6)
|
|
8798
|
+
continue;
|
|
8799
|
+
const cx = vp[0] * px + vp[4] * py + vp[8] * pz + vp[12];
|
|
8800
|
+
const cy = vp[1] * px + vp[5] * py + vp[9] * pz + vp[13];
|
|
8801
|
+
proj[v * 3] = ((cx / cw) * 0.5 + 0.5) * width;
|
|
8802
|
+
proj[v * 3 + 1] = (1 - ((cy / cw) * 0.5 + 0.5)) * height;
|
|
8803
|
+
}
|
|
8804
|
+
const inRect = (v) => {
|
|
8805
|
+
const cw = proj[v * 3 + 2];
|
|
8806
|
+
if (cw <= 1e-6)
|
|
8807
|
+
return false;
|
|
8808
|
+
const sx = proj[v * 3];
|
|
8809
|
+
const sy = proj[v * 3 + 1];
|
|
8810
|
+
return sx >= left && sx <= right && sy >= top && sy <= bottom;
|
|
8811
|
+
};
|
|
8812
|
+
// Walk to this material's own run, the same running sum pickMaterial's
|
|
8813
|
+
// matEnd walk uses — materials own a CONTIGUOUS run of the index buffer.
|
|
8814
|
+
const indices = model.getIndices();
|
|
8815
|
+
let start = 0;
|
|
8816
|
+
for (let i = 0; i < mIndex; i++)
|
|
8817
|
+
start += materials[i].vertexCount;
|
|
8818
|
+
const end = start + materials[mIndex].vertexCount;
|
|
8819
|
+
const faceIndices = [];
|
|
8820
|
+
const lines = [];
|
|
8821
|
+
let faceIndex = 0;
|
|
8822
|
+
for (let i = start; i < end; i += 3, faceIndex++) {
|
|
8823
|
+
const a = indices[i];
|
|
8824
|
+
const b = indices[i + 1];
|
|
8825
|
+
const c = indices[i + 2];
|
|
8826
|
+
if (!inRect(a) || !inRect(b) || !inRect(c))
|
|
8827
|
+
continue;
|
|
8828
|
+
faceIndices.push(faceIndex);
|
|
8829
|
+
const pa = skinnedWorld(a);
|
|
8830
|
+
const pb = skinnedWorld(b);
|
|
8831
|
+
const pc = skinnedWorld(c);
|
|
8832
|
+
// Every edge, including the ones shared with a neighbouring caught
|
|
8833
|
+
// face — drawn twice there, which costs nothing a triangle this size
|
|
8834
|
+
// notices and is simpler than tracking which edges are interior.
|
|
8835
|
+
for (const [p, q] of [
|
|
8836
|
+
[pa, pb],
|
|
8837
|
+
[pb, pc],
|
|
8838
|
+
[pc, pa],
|
|
8839
|
+
]) {
|
|
8840
|
+
const line = lineBetween(p, q, Engine.SELECTION_COLOR);
|
|
8841
|
+
if (line)
|
|
8842
|
+
lines.push(line);
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
return { faceIndices, lines };
|
|
8846
|
+
}
|
|
8847
|
+
/**
|
|
8848
|
+
* Fill the given faces of one material solid red, live-skinned — unlike
|
|
8849
|
+
* the edge `lines` selectMaterialFaces hands back, which are a one-shot
|
|
8850
|
+
* snapshot at the moment of the drag, this reads the model's CURRENT skin
|
|
8851
|
+
* matrices every frame, so it keeps tracking the mesh through a pose
|
|
8852
|
+
* change the same way the wireframe overlay does.
|
|
8853
|
+
*
|
|
8854
|
+
* `faceIndices` are the same LOCAL (material-relative) numbers
|
|
8855
|
+
* selectMaterialFaces returns and splitMaterial takes. Pass an empty list
|
|
8856
|
+
* (or a null modelName/materialName) to clear it.
|
|
8857
|
+
*/
|
|
8858
|
+
setSelectionFill(modelName, materialName, faceIndices) {
|
|
8859
|
+
if (this.selectionFill) {
|
|
8860
|
+
this.selectionFill.buffer.destroy();
|
|
8861
|
+
this.selectionFill = null;
|
|
8862
|
+
}
|
|
8863
|
+
if (!modelName || !materialName || faceIndices.length === 0)
|
|
8864
|
+
return;
|
|
8865
|
+
const inst = this.modelInstances.get(modelName);
|
|
8866
|
+
if (!inst)
|
|
8867
|
+
return;
|
|
8868
|
+
const materials = inst.model.getMaterials();
|
|
8869
|
+
const mIndex = materials.findIndex((m) => m.name === materialName);
|
|
8870
|
+
if (mIndex < 0)
|
|
8871
|
+
return;
|
|
8872
|
+
let start = 0;
|
|
8873
|
+
for (let i = 0; i < mIndex; i++)
|
|
8874
|
+
start += materials[i].vertexCount;
|
|
8875
|
+
const faceCount = materials[mIndex].vertexCount / 3;
|
|
8876
|
+
const modelIndices = inst.model.getIndices();
|
|
8877
|
+
const data = new Uint32Array(faceIndices.length * 3);
|
|
8878
|
+
let n = 0;
|
|
8879
|
+
for (const f of faceIndices) {
|
|
8880
|
+
if (f < 0 || f >= faceCount)
|
|
8881
|
+
continue;
|
|
8882
|
+
const i = start + f * 3;
|
|
8883
|
+
data[n++] = modelIndices[i];
|
|
8884
|
+
data[n++] = modelIndices[i + 1];
|
|
8885
|
+
data[n++] = modelIndices[i + 2];
|
|
8886
|
+
}
|
|
8887
|
+
if (n === 0)
|
|
8888
|
+
return;
|
|
8889
|
+
const buffer = this.device.createBuffer({
|
|
8890
|
+
label: `selection fill ${modelName}/${materialName}`,
|
|
8891
|
+
size: n * 4,
|
|
8892
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
8893
|
+
});
|
|
8894
|
+
this.device.queue.writeBuffer(buffer, 0, data.buffer, 0, n * 4);
|
|
8895
|
+
// wireframeSkinLayout's binding 4 is the wireframe's own edge list, which
|
|
8896
|
+
// vsDepth never reads — inst.vertexBuffer (already bound as storage
|
|
8897
|
+
// elsewhere, for that same edge-drawing use) satisfies the layout's slot
|
|
8898
|
+
// as a harmless stand-in rather than a fifth buffer built only to sit unused.
|
|
8899
|
+
const skinBindGroup = this.device.createBindGroup({
|
|
8900
|
+
label: `selection fill skin ${modelName}`,
|
|
8901
|
+
layout: this.wireframeSkinLayout,
|
|
8902
|
+
entries: [
|
|
8903
|
+
{ binding: 0, resource: { buffer: inst.skinMatrixBuffer } },
|
|
8904
|
+
{ binding: 1, resource: { buffer: inst.vertexBuffer } },
|
|
8905
|
+
{ binding: 2, resource: { buffer: inst.jointsBuffer } },
|
|
8906
|
+
{ binding: 3, resource: { buffer: inst.weightsBuffer } },
|
|
8907
|
+
{ binding: 4, resource: { buffer: inst.vertexBuffer } },
|
|
8908
|
+
],
|
|
8909
|
+
});
|
|
8910
|
+
this.selectionFill = { modelName, buffer, count: n, skinBindGroup };
|
|
8911
|
+
}
|
|
8912
|
+
renderSelectionFill(pass) {
|
|
8913
|
+
if (!this.selectionFill)
|
|
8914
|
+
return;
|
|
8915
|
+
const inst = this.modelInstances.get(this.selectionFill.modelName);
|
|
8916
|
+
if (!inst)
|
|
8917
|
+
return;
|
|
8918
|
+
pass.setPipeline(this.selectionFillPipeline);
|
|
8919
|
+
pass.setBindGroup(0, this.selectionFillBindGroup);
|
|
8920
|
+
pass.setBindGroup(1, this.selectionFill.skinBindGroup);
|
|
8921
|
+
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
8922
|
+
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
8923
|
+
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
8924
|
+
pass.setIndexBuffer(this.selectionFill.buffer, "uint32");
|
|
8925
|
+
pass.drawIndexed(this.selectionFill.count);
|
|
8926
|
+
}
|
|
8115
8927
|
/** Draw an octahedron per bone of `modelName`, rebuilt each frame. Null off. */
|
|
8116
8928
|
setBoneOverlay(modelName, options = {}) {
|
|
8117
8929
|
this.overlayBones = modelName ? { modelName, options } : null;
|
|
@@ -9554,6 +10366,7 @@ export class Engine {
|
|
|
9554
10366
|
weightsData: new Float32Array(data.morphCount),
|
|
9555
10367
|
workgroups: Math.ceil(data.vertexCount / 64),
|
|
9556
10368
|
dispatchNeeded: false, // vertex buffer already holds base; dispatch on first weight change
|
|
10369
|
+
baseBuf,
|
|
9557
10370
|
};
|
|
9558
10371
|
}
|
|
9559
10372
|
createGroundGeometry(width = 100, height = 100) {
|
|
@@ -9620,16 +10433,35 @@ export class Engine {
|
|
|
9620
10433
|
});
|
|
9621
10434
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
|
|
9622
10435
|
}
|
|
9623
|
-
buildGroundPipeline(soft) {
|
|
10436
|
+
buildGroundPipeline(soft, mirrored = false) {
|
|
9624
10437
|
return this.createRenderPipeline({
|
|
9625
10438
|
...this.groundShadowPipelineDesc,
|
|
9626
|
-
|
|
10439
|
+
// A REFLECTION FLIPS WINDING — determinant -1, see reflection.ts. The
|
|
10440
|
+
// ground culls back faces, so drawn into the mirror pass with the ordinary
|
|
10441
|
+
// pipeline every one of its triangles faces away and the floor vanishes
|
|
10442
|
+
// whole: no grid, and no received shadow either, since the shadow is a
|
|
10443
|
+
// layer of this shader. It is the same fact that sits the OUTLINE out of
|
|
10444
|
+
// the mirror, and the same answer the scene-pass pipelines already use.
|
|
10445
|
+
cullMode: mirrored ? "none" : this.groundShadowPipelineDesc.cullMode,
|
|
10446
|
+
label: `ground shadow pipeline${soft ? " (soft)" : ""}${mirrored ? " (mirror)" : ""}`,
|
|
9627
10447
|
shaderModule: this.device.createShaderModule({
|
|
9628
10448
|
label: soft ? "ground shadow (soft)" : "ground shadow",
|
|
9629
10449
|
code: groundShaderWgsl(soft),
|
|
9630
10450
|
}),
|
|
9631
10451
|
});
|
|
9632
10452
|
}
|
|
10453
|
+
/** The ground's pipeline for the mirror pass, built on the first frame a
|
|
10454
|
+
* mirror actually needs it. */
|
|
10455
|
+
ensureGroundMirrorPipeline(soft) {
|
|
10456
|
+
if (soft) {
|
|
10457
|
+
if (!this.groundMirrorSoftPipeline)
|
|
10458
|
+
this.groundMirrorSoftPipeline = this.buildGroundPipeline(true, true);
|
|
10459
|
+
return this.groundMirrorSoftPipeline;
|
|
10460
|
+
}
|
|
10461
|
+
if (!this.groundMirrorPipeline)
|
|
10462
|
+
this.groundMirrorPipeline = this.buildGroundPipeline(false, true);
|
|
10463
|
+
return this.groundMirrorPipeline;
|
|
10464
|
+
}
|
|
9633
10465
|
/** Built on the first frame that actually needs it. A shader compile costs
|
|
9634
10466
|
* load time, and the overwhelming majority of scenes never soften a shadow. */
|
|
9635
10467
|
ensureGroundSoftPipeline() {
|
|
@@ -9699,9 +10531,47 @@ export class Engine {
|
|
|
9699
10531
|
* and a bind group holding the old view would sample a destroyed texture on
|
|
9700
10532
|
* the first resized frame with a mirror on.
|
|
9701
10533
|
*/
|
|
10534
|
+
/** The stand-ins above, made once. 4x4 rather than 1x1: the depth one has to
|
|
10535
|
+
* be multisampled to match `texture_depth_multisampled_2d`, and a
|
|
10536
|
+
* multisampled attachment is the one kind of texture worth giving room. */
|
|
10537
|
+
ensureMirrorDummies() {
|
|
10538
|
+
if (this.mirrorDummyColorView && this.mirrorDummyDepthView)
|
|
10539
|
+
return;
|
|
10540
|
+
this.mirrorDummyColorView = this.device
|
|
10541
|
+
.createTexture({
|
|
10542
|
+
label: "mirror binding stand-in (colour)",
|
|
10543
|
+
size: [4, 4],
|
|
10544
|
+
format: this.hdrFormat,
|
|
10545
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
10546
|
+
})
|
|
10547
|
+
.createView();
|
|
10548
|
+
this.mirrorDummyDepthView = this.device
|
|
10549
|
+
.createTexture({
|
|
10550
|
+
label: "mirror binding stand-in (depth)",
|
|
10551
|
+
size: [4, 4],
|
|
10552
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
10553
|
+
format: this.depthFormat,
|
|
10554
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
10555
|
+
})
|
|
10556
|
+
.createView({ aspect: "depth-only" });
|
|
10557
|
+
}
|
|
9702
10558
|
buildGroundBindGroup() {
|
|
9703
10559
|
if (!this.groundShadowMaterialBuffer)
|
|
9704
10560
|
return;
|
|
10561
|
+
this.ensureMirrorDummies();
|
|
10562
|
+
if (!this.groundClipOffBuffer) {
|
|
10563
|
+
const make = (label, active) => {
|
|
10564
|
+
const b = this.device.createBuffer({
|
|
10565
|
+
label,
|
|
10566
|
+
size: 32,
|
|
10567
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
10568
|
+
});
|
|
10569
|
+
this.device.queue.writeBuffer(b, 0, new Float32Array([0, 1, 0, 0, active, 0, 0, 0]));
|
|
10570
|
+
return b;
|
|
10571
|
+
};
|
|
10572
|
+
this.groundClipOffBuffer = make("ground clip (camera: off)", 0);
|
|
10573
|
+
this.groundClipMirrorBuffer = make("ground clip (mirror)", 1);
|
|
10574
|
+
}
|
|
9705
10575
|
this.groundShadowBindGroup = this.device.createBindGroup({
|
|
9706
10576
|
label: "ground shadow bind",
|
|
9707
10577
|
layout: this.groundShadowBindGroupLayout,
|
|
@@ -9721,6 +10591,31 @@ export class Engine {
|
|
|
9721
10591
|
{ binding: 10, resource: this.materialSampler },
|
|
9722
10592
|
{ binding: 11, resource: this.mirrorDepthReadView },
|
|
9723
10593
|
{ binding: 12, resource: this.groundNoiseView },
|
|
10594
|
+
{ binding: 13, resource: this.mirrorMaskView },
|
|
10595
|
+
{ binding: 14, resource: { buffer: this.groundClipOffBuffer } },
|
|
10596
|
+
],
|
|
10597
|
+
});
|
|
10598
|
+
this.groundMirrorViewBindGroup = this.device.createBindGroup({
|
|
10599
|
+
label: "ground shadow bind (mirror view)",
|
|
10600
|
+
layout: this.groundShadowBindGroupLayout,
|
|
10601
|
+
entries: [
|
|
10602
|
+
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
10603
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
10604
|
+
{ binding: 2, resource: this.shadowMapDepthViews[0] },
|
|
10605
|
+
{ binding: 3, resource: this.shadowComparisonSampler },
|
|
10606
|
+
{ binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
|
|
10607
|
+
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
10608
|
+
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
10609
|
+
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
10610
|
+
{ binding: 8, resource: { buffer: this.mirrorVPBuffer } },
|
|
10611
|
+
// Stand-ins, not the live mirror textures: this group draws INTO the
|
|
10612
|
+
// mirror pass, where those are the attachments.
|
|
10613
|
+
{ binding: 9, resource: this.mirrorDummyColorView },
|
|
10614
|
+
{ binding: 10, resource: this.materialSampler },
|
|
10615
|
+
{ binding: 11, resource: this.mirrorDummyDepthView },
|
|
10616
|
+
{ binding: 12, resource: this.groundNoiseView },
|
|
10617
|
+
{ binding: 13, resource: this.mirrorDummyColorView },
|
|
10618
|
+
{ binding: 14, resource: { buffer: this.groundClipMirrorBuffer } },
|
|
9724
10619
|
],
|
|
9725
10620
|
});
|
|
9726
10621
|
if (this.groundDrawCall)
|
|
@@ -10297,6 +11192,23 @@ export class Engine {
|
|
|
10297
11192
|
}
|
|
10298
11193
|
this.device.queue.submit([encoder.finish()]);
|
|
10299
11194
|
}
|
|
11195
|
+
/** The ground draw, against a caller's bind group — the camera's, or the
|
|
11196
|
+
* mirror's. One body, so the reflected floor cannot drift from the real one. */
|
|
11197
|
+
renderGroundWith(pass, bindGroup, mirrored = false) {
|
|
11198
|
+
if (this.groundIsSuppressed())
|
|
11199
|
+
return;
|
|
11200
|
+
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
|
|
11201
|
+
return;
|
|
11202
|
+
pass.setPipeline(mirrored
|
|
11203
|
+
? this.ensureGroundMirrorPipeline(this.groundSoft)
|
|
11204
|
+
: this.groundSoft
|
|
11205
|
+
? this.ensureGroundSoftPipeline()
|
|
11206
|
+
: this.groundShadowPipeline);
|
|
11207
|
+
pass.setVertexBuffer(0, this.groundVertexBuffer);
|
|
11208
|
+
pass.setIndexBuffer(this.groundIndexBuffer, "uint16");
|
|
11209
|
+
pass.setBindGroup(0, bindGroup);
|
|
11210
|
+
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0);
|
|
11211
|
+
}
|
|
10300
11212
|
renderGround(pass) {
|
|
10301
11213
|
// A stage brings its own floor. Both sit at y=0, so drawing the built-in
|
|
10302
11214
|
// plane underneath produces z-fighting across the whole scene — enforced
|
|
@@ -10472,7 +11384,16 @@ export class Engine {
|
|
|
10472
11384
|
if (!this.overlayVertices)
|
|
10473
11385
|
return;
|
|
10474
11386
|
const inst = this.overlayModel(this.overlayVertices.modelName);
|
|
10475
|
-
const {
|
|
11387
|
+
const { xray } = this.overlayVertices;
|
|
11388
|
+
// A hovered material previews EXACTLY what clicking it would pick — the
|
|
11389
|
+
// same self-occluding, ISOLATED view, nothing else drawn. It substitutes
|
|
11390
|
+
// for `material` only while nothing is actually picked, so the section
|
|
11391
|
+
// opens on the whole mesh and narrows to one material the moment the
|
|
11392
|
+
// pointer (in the viewport OR the list — both just call
|
|
11393
|
+
// setHoveredMaterial) settles on it. A real pick always wins, and there
|
|
11394
|
+
// is nothing left to preview once one is made.
|
|
11395
|
+
const hm = this.hoverMaterial;
|
|
11396
|
+
const material = this.overlayVertices.material ?? (hm && hm.modelName === inst?.name ? hm.materialName : null);
|
|
10476
11397
|
if (!inst || !this.ensureEdgeBuffer(inst, material))
|
|
10477
11398
|
return;
|
|
10478
11399
|
const edges = inst.wireEdges.get(material ?? "");
|
|
@@ -10485,15 +11406,6 @@ export class Engine {
|
|
|
10485
11406
|
const seams = material === null && this.ensureEdgeBuffer(inst, null, true)
|
|
10486
11407
|
? (inst.wireEdges.get(Engine.SEAM_KEY) ?? null)
|
|
10487
11408
|
: 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
11409
|
this.wireframeColorData.set(DEFAULT_VERTEX_COLOR);
|
|
10498
11410
|
this.wireframeColorData[4] = this.canvas.width;
|
|
10499
11411
|
this.wireframeColorData[5] = this.canvas.height;
|
|
@@ -10508,40 +11420,8 @@ export class Engine {
|
|
|
10508
11420
|
this.wireframeColorData[6] = OVERLAY_STYLE.seamStrokePx;
|
|
10509
11421
|
this.device.queue.writeBuffer(this.wireframeSeamUniformBuffer, 0, this.wireframeColorData);
|
|
10510
11422
|
}
|
|
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
11423
|
pass.setBindGroup(0, this.wireframeBindGroup);
|
|
10521
11424
|
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
11425
|
// What writes depth is what is allowed to hide the wireframe, and that
|
|
10546
11426
|
// differs between the two views.
|
|
10547
11427
|
//
|
|
@@ -10550,14 +11430,18 @@ export class Engine {
|
|
|
10550
11430
|
// mode and Maya included, and seeing both walls at once is moire rather than
|
|
10551
11431
|
// information.
|
|
10552
11432
|
//
|
|
10553
|
-
// A PICKED material writes only its OWN faces. The question
|
|
11433
|
+
// A PICKED (or hovered) material writes only its OWN faces. The question is
|
|
10554
11434
|
// where this material is, and half of it is usually under a coat; letting
|
|
10555
11435
|
// the coat hide it does not answer that. Its own depth still goes in, so its
|
|
10556
11436
|
// back faces stay hidden and it reads as an object instead of a haze —
|
|
10557
11437
|
// which is the difference between this and turning x-ray on.
|
|
10558
11438
|
const runs = material !== null ? this.materialIndexRanges(inst, material) : null;
|
|
10559
11439
|
if (!xray) {
|
|
10560
|
-
|
|
11440
|
+
pass.setPipeline(this.wireframeDepthPipeline);
|
|
11441
|
+
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
11442
|
+
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
11443
|
+
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
11444
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
10561
11445
|
if (runs)
|
|
10562
11446
|
for (const [start, end] of runs)
|
|
10563
11447
|
pass.drawIndexed(end - start, 1, start);
|
|
@@ -10590,7 +11474,8 @@ export class Engine {
|
|
|
10590
11474
|
this.overlayBones !== null ||
|
|
10591
11475
|
this.overlayBodies !== null ||
|
|
10592
11476
|
this.overlayJoints !== null ||
|
|
10593
|
-
this.overlayVertices !== null
|
|
11477
|
+
this.overlayVertices !== null ||
|
|
11478
|
+
this.selectionFill !== null);
|
|
10594
11479
|
}
|
|
10595
11480
|
overlayModel(name) {
|
|
10596
11481
|
return this.modelInstances.get(name) ?? null;
|
|
@@ -10709,7 +11594,7 @@ export class Engine {
|
|
|
10709
11594
|
if (inst)
|
|
10710
11595
|
collect(jointOverlay(inst.model, inst.physics, this.overlayJoints.options));
|
|
10711
11596
|
}
|
|
10712
|
-
if (total === 0 && !this.overlayVertices)
|
|
11597
|
+
if (total === 0 && !this.overlayVertices && !this.selectionFill)
|
|
10713
11598
|
return;
|
|
10714
11599
|
this.ensureOverlayInstanceCapacity(total);
|
|
10715
11600
|
const data = this.overlayInstanceData;
|
|
@@ -10736,6 +11621,10 @@ export class Engine {
|
|
|
10736
11621
|
const pass = encoder.beginRenderPass(this.overlayPassDescriptor);
|
|
10737
11622
|
// Under everything: the mesh is the haze the rig is read against.
|
|
10738
11623
|
this.renderWireframe(pass);
|
|
11624
|
+
// The selection wash, under its own edge lines (drawn below with the rest
|
|
11625
|
+
// of this layer's basic primitives) so the crisp outline still reads on
|
|
11626
|
+
// top of the fill rather than being washed out by it.
|
|
11627
|
+
this.renderSelectionFill(pass);
|
|
10739
11628
|
pass.setBindGroup(0, this.overlayBindGroup);
|
|
10740
11629
|
pass.setVertexBuffer(0, this.overlayVertexBuffer);
|
|
10741
11630
|
pass.setVertexBuffer(1, this.overlayInstanceBuffer);
|
|
@@ -11260,6 +12149,8 @@ export class Engine {
|
|
|
11260
12149
|
this.dispatchMorphCompute(encoder);
|
|
11261
12150
|
// Frustum cull into indirect arguments. After the camera and shadow matrices
|
|
11262
12151
|
// are settled, before the passes that draw from them.
|
|
12152
|
+
// Before reflectionActive is read: this is what decides it.
|
|
12153
|
+
this.syncMirrorFromEffects();
|
|
11263
12154
|
if (this.reflectionActive)
|
|
11264
12155
|
this.updateMirrorCamera();
|
|
11265
12156
|
if (hasModels)
|
|
@@ -11313,6 +12204,14 @@ export class Engine {
|
|
|
11313
12204
|
// instance count of an invisible model's draws.
|
|
11314
12205
|
if (wanted && this.shadowBundles[ci])
|
|
11315
12206
|
sp.executeBundles([this.shadowBundles[ci]]);
|
|
12207
|
+
// The mirror throws shade like anything else standing on the floor.
|
|
12208
|
+
// Direct rather than in the bundle: the bundles are recorded on scene
|
|
12209
|
+
// STRUCTURE, and a mirror comes and goes with an effect's weight.
|
|
12210
|
+
if (wanted && this.mirrorSurface && this.mirrorShadowPipeline) {
|
|
12211
|
+
sp.setPipeline(this.mirrorShadowPipeline);
|
|
12212
|
+
sp.setBindGroup(0, this.mirrorShadowBindGroups[ci]);
|
|
12213
|
+
sp.draw(6);
|
|
12214
|
+
}
|
|
11316
12215
|
sp.end();
|
|
11317
12216
|
this.shadowCascadeCleared[ci] = !wanted;
|
|
11318
12217
|
}
|
|
@@ -11364,6 +12263,10 @@ export class Engine {
|
|
|
11364
12263
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
11365
12264
|
if (this.hasGround)
|
|
11366
12265
|
this.renderGround(pass);
|
|
12266
|
+
// After the ground for the same early-z reason, and before the transparent
|
|
12267
|
+
// phase so sheer fabric blends over the glass rather than being depth
|
|
12268
|
+
// -rejected behind it.
|
|
12269
|
+
this.renderMirrorSurface(pass);
|
|
11367
12270
|
// The transparent phase is drawn DIRECTLY, and must stay that way. It is the
|
|
11368
12271
|
// one part of this pass that is not bundled, so the reason is worth keeping.
|
|
11369
12272
|
//
|
|
@@ -11578,13 +12481,21 @@ export class Engine {
|
|
|
11578
12481
|
claimed.set(m, g.id);
|
|
11579
12482
|
}
|
|
11580
12483
|
}
|
|
11581
|
-
//
|
|
12484
|
+
// Installs no longer present lose their CLAIM now (the generation bump
|
|
12485
|
+
// invalidates any in-flight compile for that id) but keep their BUFFER
|
|
12486
|
+
// until every draw call has stopped pointing at it. This was the actual
|
|
12487
|
+
// bug: destroying here, synchronously, left the render loop — which runs
|
|
12488
|
+
// independently on its own rAF and does not know this function is
|
|
12489
|
+
// mid-await — still submitting frames through a bind group whose buffer
|
|
12490
|
+
// no longer existed, for however many frames compileAndInstallGroup's own
|
|
12491
|
+
// shader compiles below took. "used in submit while destroyed" was that
|
|
12492
|
+
// race, not a one-off.
|
|
11582
12493
|
const nextIds = new Set(groups.map((g) => g.id));
|
|
12494
|
+
const outgoing = [];
|
|
11583
12495
|
for (const [id, install] of inst.styleGroups) {
|
|
11584
12496
|
if (!nextIds.has(id)) {
|
|
11585
12497
|
inst.styleGroupGen.set(id, (inst.styleGroupGen.get(id) ?? 0) + 1);
|
|
11586
|
-
|
|
11587
|
-
inst.styleGroups.delete(id);
|
|
12498
|
+
outgoing.push([id, install]);
|
|
11588
12499
|
}
|
|
11589
12500
|
}
|
|
11590
12501
|
const groupResults = [];
|
|
@@ -11592,7 +12503,13 @@ export class Engine {
|
|
|
11592
12503
|
const r = await this.compileAndInstallGroup(inst, g);
|
|
11593
12504
|
groupResults.push({ groupId: g.id, diagnostics: r.diagnostics, ok: r.ok });
|
|
11594
12505
|
}
|
|
12506
|
+
// Repoints every draw call away from the outgoing installs (among
|
|
12507
|
+
// everything else) — only past this line is destroying them safe.
|
|
11595
12508
|
this.assignDrawCallGroups(inst, claimed);
|
|
12509
|
+
for (const [id, install] of outgoing) {
|
|
12510
|
+
this.destroyInstall(install);
|
|
12511
|
+
inst.styleGroups.delete(id);
|
|
12512
|
+
}
|
|
11596
12513
|
return {
|
|
11597
12514
|
ok: groupResults.every((r) => r.ok),
|
|
11598
12515
|
groups: groupResults,
|
|
@@ -11880,6 +12797,7 @@ export class Engine {
|
|
|
11880
12797
|
let pipeline;
|
|
11881
12798
|
let pipelineNoDepthWrite;
|
|
11882
12799
|
let overEyesPipeline;
|
|
12800
|
+
let mirrorPipeline;
|
|
11883
12801
|
try {
|
|
11884
12802
|
pipeline = await this.createRenderClassPipeline(renderClass, module, false);
|
|
11885
12803
|
// The depth-write-off twin: stage transparency draws with it (see
|
|
@@ -11887,6 +12805,10 @@ export class Engine {
|
|
|
11887
12805
|
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false);
|
|
11888
12806
|
if (renderClass === "hair")
|
|
11889
12807
|
overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true);
|
|
12808
|
+
// Only the eye needs one: every other class culls "none", which a flipped
|
|
12809
|
+
// winding leaves alone.
|
|
12810
|
+
if (renderClass === "eye")
|
|
12811
|
+
mirrorPipeline = await this.createRenderClassPipeline(renderClass, module, false, true, true);
|
|
11890
12812
|
}
|
|
11891
12813
|
catch (e) {
|
|
11892
12814
|
diagnostics.push({ severity: "error", message: `pipeline creation failed: ${e.message}` });
|
|
@@ -11913,6 +12835,7 @@ export class Engine {
|
|
|
11913
12835
|
pipeline,
|
|
11914
12836
|
pipelineNoDepthWrite,
|
|
11915
12837
|
overEyesPipeline,
|
|
12838
|
+
mirrorPipeline,
|
|
11916
12839
|
uniformBuffer,
|
|
11917
12840
|
images: this.uploadGroupImages(group),
|
|
11918
12841
|
slotMap: result.slotMap,
|
|
@@ -12012,12 +12935,20 @@ export class Engine {
|
|
|
12012
12935
|
* render-class owns pass integration (stencil interplay, depth bias, cull). auto = plain;
|
|
12013
12936
|
* eye = stamp + front cull + bias; hair = stencil-test (+ the over-eyes variant).
|
|
12014
12937
|
*/
|
|
12015
|
-
createRenderClassPipeline(renderClass, module, overEyes, depthWrite = true) {
|
|
12938
|
+
createRenderClassPipeline(renderClass, module, overEyes, depthWrite = true, mirrored = false) {
|
|
12016
12939
|
const base = {
|
|
12017
12940
|
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
12018
12941
|
layout: this.mainPipelineLayout,
|
|
12019
12942
|
vertex: { module, buffers: this.fullVertexBufferLayouts },
|
|
12020
|
-
|
|
12943
|
+
// The eye front-culls — the see-through-hair look is draw order plus that
|
|
12944
|
+
// cull plus the stencil stamp, and it is the deployed appearance of every
|
|
12945
|
+
// published scene. In the MIRROR the winding is flipped (determinant -1,
|
|
12946
|
+
// reflection.ts), so the same cull keeps the back of the eyeball and
|
|
12947
|
+
// discards its front. `mirrored` flips it back rather than changing what
|
|
12948
|
+
// the camera sees.
|
|
12949
|
+
primitive: {
|
|
12950
|
+
cullMode: (renderClass === "eye" ? (mirrored ? "back" : "front") : "none"),
|
|
12951
|
+
},
|
|
12021
12952
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
12022
12953
|
};
|
|
12023
12954
|
const plainDepth = {
|
|
@@ -12081,12 +13012,15 @@ export class Engine {
|
|
|
12081
13012
|
// rain vanished the instant the camera crossed a glass dome or a curtain
|
|
12082
13013
|
// (reported: binary vanish/recover with camera angle, stage loaded). Stage
|
|
12083
13014
|
// transparency blends and leaves depth alone.
|
|
12084
|
-
pipelineForDrawCall(inst, dc) {
|
|
13015
|
+
pipelineForDrawCall(inst, dc, mirrored = false) {
|
|
12085
13016
|
const stageGlass = inst.isStage && dc.type === "transparent";
|
|
12086
13017
|
if (dc.groupId) {
|
|
12087
13018
|
const install = inst.styleGroups.get(dc.groupId);
|
|
12088
|
-
if (install)
|
|
13019
|
+
if (install) {
|
|
13020
|
+
if (mirrored && install.mirrorPipeline)
|
|
13021
|
+
return install.mirrorPipeline;
|
|
12089
13022
|
return stageGlass ? install.pipelineNoDepthWrite : install.pipeline;
|
|
13023
|
+
}
|
|
12090
13024
|
}
|
|
12091
13025
|
return stageGlass ? this.neutralPipelineNoDepthWrite : this.neutralPipeline;
|
|
12092
13026
|
}
|
|
@@ -12122,7 +13056,7 @@ export class Engine {
|
|
|
12122
13056
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
12123
13057
|
bound = true;
|
|
12124
13058
|
}
|
|
12125
|
-
const pipeline = this.pipelineForDrawCall(inst, draw);
|
|
13059
|
+
const pipeline = this.pipelineForDrawCall(inst, draw, view.args === "mirror");
|
|
12126
13060
|
if (pipeline !== currentPipeline) {
|
|
12127
13061
|
pass.setPipeline(pipeline);
|
|
12128
13062
|
currentPipeline = pipeline;
|
|
@@ -12133,8 +13067,9 @@ export class Engine {
|
|
|
12133
13067
|
// Same index range; own pipeline + groups 0/2. Group 1 (skinMats) is
|
|
12134
13068
|
// layout-identical between the main and outline pipelines and stays
|
|
12135
13069
|
// bound. Restore group 0 afterwards and force a pipeline re-set.
|
|
12136
|
-
|
|
12137
|
-
pass.
|
|
13070
|
+
const mirrored = view.args === "mirror";
|
|
13071
|
+
pass.setPipeline(mirrored ? this.outlineMirrorPipeline : this.outlinePipeline);
|
|
13072
|
+
pass.setBindGroup(0, mirrored ? this.outlineMirrorPerFrameBindGroup : this.outlinePerFrameBindGroup);
|
|
12138
13073
|
pass.setBindGroup(2, draw.outline.bindGroup);
|
|
12139
13074
|
this.issueDraw(pass, draw, view.args);
|
|
12140
13075
|
pass.setBindGroup(0, view.perFrame);
|
|
@@ -12168,10 +13103,13 @@ export class Engine {
|
|
|
12168
13103
|
* main camera's bind group.
|
|
12169
13104
|
*/
|
|
12170
13105
|
sceneView(kind) {
|
|
13106
|
+
// Outlines in BOTH now. They were off in the mirror because the hull culls
|
|
13107
|
+
// back faces and a reflection flips winding, so every hull triangle was
|
|
13108
|
+
// culled — a reflection of a toon character with no ink line beside the
|
|
13109
|
+
// real one reads as broken. The draw picks the flipped-cull pipeline and
|
|
13110
|
+
// the mirror camera's own group; see outlineMirrorPipeline.
|
|
12171
13111
|
return kind === "mirror"
|
|
12172
|
-
?
|
|
12173
|
-
// reflection flips winding, so the hull would ink over the model.
|
|
12174
|
-
{ perFrame: this.mirrorPerFrameBindGroup, args: "mirror", outlines: false }
|
|
13112
|
+
? { perFrame: this.mirrorPerFrameBindGroup, args: "mirror", outlines: true }
|
|
12175
13113
|
: { perFrame: this.perFrameBindGroup, args: "camera", outlines: true };
|
|
12176
13114
|
}
|
|
12177
13115
|
renderModelOpaquePhase(pass, inst, view) {
|
|
@@ -12956,11 +13894,16 @@ Engine.TIMED_PASSES = [
|
|
|
12956
13894
|
"composite",
|
|
12957
13895
|
"overlay",
|
|
12958
13896
|
];
|
|
12959
|
-
// ── The
|
|
13897
|
+
// ── The mirror (step 7C) ──
|
|
12960
13898
|
// Half-res scene-contract attachments a mirrored draw renders into, plus the
|
|
12961
|
-
// mirror's own camera block.
|
|
12962
|
-
//
|
|
12963
|
-
|
|
13899
|
+
// mirror's own camera block.
|
|
13900
|
+
//
|
|
13901
|
+
// ONE reflection target, so ONE plane at a time. The floor's is (0, 1, 0, 0)
|
|
13902
|
+
// — MMD floors live at y = 0 by convention and addGround builds its quad
|
|
13903
|
+
// there — and a placed mirror surface REPLACES it for as long as it exists,
|
|
13904
|
+
// because two planes would need two of every attachment below. Which one is
|
|
13905
|
+
// in force is mirrorPlane, refreshed by updateMirrorCamera.
|
|
13906
|
+
Engine.GROUND_PLANE = [0, 1, 0, 0];
|
|
12964
13907
|
Engine.BLOOM_MAX_LEVELS = 5;
|
|
12965
13908
|
// Width of the baked Filmic tone LUT (composite.ts FILMIC_LUT_W must match).
|
|
12966
13909
|
Engine.FILMIC_LUT_WIDTH = 256;
|
|
@@ -12968,6 +13911,37 @@ Engine.FILMIC_LUT_WIDTH = 256;
|
|
|
12968
13911
|
// (vsync-to-vsync), recomputed at STATS_REFRESH_MS so the readout doesn't flicker.
|
|
12969
13912
|
Engine.STATS_WINDOW = 120;
|
|
12970
13913
|
Engine.STATS_REFRESH_MS = 500;
|
|
13914
|
+
/**
|
|
13915
|
+
* The dials a `#mirror` effect is READ BY — the contract between an effect
|
|
13916
|
+
* file and the reflection.
|
|
13917
|
+
*
|
|
13918
|
+
* By name rather than by position, and with a fallback each, so that an
|
|
13919
|
+
* effect declaring only some of them still works: a mirror that wants to sit
|
|
13920
|
+
* at the origin facing the camera declares nothing but its size, and the ones
|
|
13921
|
+
* it leaves out are not missing, they are default.
|
|
13922
|
+
*
|
|
13923
|
+
* Names in the built-ins' own convention (SCREAMING_SNAKE), because these
|
|
13924
|
+
* appear in the params panel beside every other effect's.
|
|
13925
|
+
*/
|
|
13926
|
+
Engine.MIRROR_DIALS = {
|
|
13927
|
+
posX: "POS_X",
|
|
13928
|
+
posY: "POS_Y",
|
|
13929
|
+
posZ: "POS_Z",
|
|
13930
|
+
/** Degrees, the unit the slider shows — nothing converts on the way in. */
|
|
13931
|
+
rotX: "ROT_X",
|
|
13932
|
+
rotY: "ROT_Y",
|
|
13933
|
+
rotZ: "ROT_Z",
|
|
13934
|
+
width: "WIDTH",
|
|
13935
|
+
height: "HEIGHT",
|
|
13936
|
+
tint: "TINT",
|
|
13937
|
+
blur: "BLUR",
|
|
13938
|
+
frame: "FRAME",
|
|
13939
|
+
frameColor: "FRAME_COLOR",
|
|
13940
|
+
};
|
|
13941
|
+
/** Opaque red — a selection is a working state, not a material property,
|
|
13942
|
+
* so it reads as unmistakably distinct from anything a model's own
|
|
13943
|
+
* colours could be. */
|
|
13944
|
+
Engine.SELECTION_COLOR = [1, 0.15, 0.15, 1];
|
|
12971
13945
|
// ── GPU frustum cull ────────────────────────────────────────────────────────
|
|
12972
13946
|
//
|
|
12973
13947
|
// Sizes, once, so the arithmetic below is readable: a DrawMeta is 32 bytes
|