@viggle/splat-engine-three 0.1.0 → 0.1.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/README.md +174 -14
- package/dist/chunks/{chunk-MM6DJU6S.js → chunk-WN5WBKM4.js} +848 -43
- package/dist/chunks/chunk-WN5WBKM4.js.map +7 -0
- package/dist/index.js +7 -3
- package/dist/types/character.d.ts +43 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/splat/bone-binding.d.ts +1 -1
- package/dist/types/splat/depth-sort.d.ts +23 -5
- package/dist/types/splat/importance.d.ts +60 -0
- package/dist/types/splat/prune.d.ts +60 -0
- package/dist/types/splat/quality.d.ts +175 -0
- package/dist/types/splat/splat.d.ts +66 -0
- package/dist/types/splat/webgl/shaders.d.ts +1 -1
- package/dist/types/splat/webgl/splat-mesh.d.ts +2 -0
- package/dist/types/splat/webgpu/splat-mesh.d.ts +2 -0
- package/dist/types/three/splat-character.d.ts +28 -0
- package/dist/webgpu.js +36 -6
- package/dist/webgpu.js.map +2 -2
- package/package.json +1 -1
- package/dist/chunks/chunk-MM6DJU6S.js.map +0 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/three/splat-character.ts
|
|
2
|
-
import { Color, Matrix4 as Matrix42, Object3D, SRGBColorSpace, Vector3 as Vector35 } from "three";
|
|
2
|
+
import { Color, Matrix3, Matrix4 as Matrix42, Object3D, SRGBColorSpace, Vector3 as Vector35 } from "three";
|
|
3
3
|
|
|
4
4
|
// src/character.ts
|
|
5
5
|
import { Euler, MathUtils, Vector3 as Vector34 } from "three";
|
|
@@ -2341,6 +2341,14 @@ function buildSourceBoneRemap(splat, numBones, skeletonLibrary) {
|
|
|
2341
2341
|
const sourceCount = splat.sourceBoneCount;
|
|
2342
2342
|
if (!sourceCount || sourceCount === numBones) return null;
|
|
2343
2343
|
if (!skeletonLibrary) return null;
|
|
2344
|
+
if (sourceCount === 119 && numBones === 86) {
|
|
2345
|
+
const head = skeletonLibrary.boneNames?.indexOf("head") ?? -1;
|
|
2346
|
+
if (head < 0) return null;
|
|
2347
|
+
const remap2 = new Int32Array(sourceCount);
|
|
2348
|
+
for (let i = 0; i < 86; i++) remap2[i] = i;
|
|
2349
|
+
remap2.fill(head, 86);
|
|
2350
|
+
return remap2;
|
|
2351
|
+
}
|
|
2344
2352
|
let targetNames;
|
|
2345
2353
|
if (skeletonLibrary.boneNames?.length === numBones) targetNames = skeletonLibrary.boneNames;
|
|
2346
2354
|
else if (skeletonLibrary.boneNames441?.length === numBones) targetNames = skeletonLibrary.boneNames441;
|
|
@@ -2404,15 +2412,20 @@ function initializeSplatBoneMapping(splat, numBones, skeletonLibrary) {
|
|
|
2404
2412
|
}
|
|
2405
2413
|
}
|
|
2406
2414
|
}
|
|
2415
|
+
splat.syncMappedCpuInfluences(firstIdx);
|
|
2407
2416
|
splat.markBoneTexturesDirty();
|
|
2408
2417
|
return firstIdx;
|
|
2409
2418
|
}
|
|
2410
|
-
function buildSplatBoneIndexList(splat, numBones) {
|
|
2419
|
+
function buildSplatBoneIndexList(splat, numBones, firstPaletteIdx) {
|
|
2411
2420
|
const weights = splat.splatWeights;
|
|
2412
2421
|
if (!weights) return new Int32Array(0);
|
|
2413
2422
|
const used = new Uint8Array(numBones);
|
|
2423
|
+
const indices = splat.boneIndicesData;
|
|
2424
|
+
const mappedWeights = splat.boneWeightsData;
|
|
2425
|
+
if (!indices || !mappedWeights) return new Int32Array(0);
|
|
2414
2426
|
for (let i = 0; i < weights.indices.length; i++) {
|
|
2415
|
-
|
|
2427
|
+
if (mappedWeights[i] <= 1e-3) continue;
|
|
2428
|
+
const idx2 = indices[i] - firstPaletteIdx | 0;
|
|
2416
2429
|
if (idx2 >= 0 && idx2 < numBones) used[idx2] = 1;
|
|
2417
2430
|
}
|
|
2418
2431
|
let count = 0;
|
|
@@ -2561,7 +2574,7 @@ var SplatSkinning = class {
|
|
|
2561
2574
|
}
|
|
2562
2575
|
let boneIndexList = this._boneIndexLists.get(splat);
|
|
2563
2576
|
if (!boneIndexList) {
|
|
2564
|
-
boneIndexList = buildSplatBoneIndexList(splat, numBones);
|
|
2577
|
+
boneIndexList = buildSplatBoneIndexList(splat, numBones, firstPaletteIdx);
|
|
2565
2578
|
this._boneIndexLists.set(splat, boneIndexList);
|
|
2566
2579
|
}
|
|
2567
2580
|
const palette = splat.transformPalette;
|
|
@@ -3741,6 +3754,490 @@ function packSplats(numSplats, s) {
|
|
|
3741
3754
|
};
|
|
3742
3755
|
}
|
|
3743
3756
|
|
|
3757
|
+
// src/splat/importance.ts
|
|
3758
|
+
import { DataUtils as DataUtils2 } from "three";
|
|
3759
|
+
function bucketByCell(cellKey, n) {
|
|
3760
|
+
const id = /* @__PURE__ */ new Map();
|
|
3761
|
+
const dense = new Uint32Array(n);
|
|
3762
|
+
let cells = 0;
|
|
3763
|
+
for (let i = 0; i < n; i++) {
|
|
3764
|
+
const key = cellKey[i];
|
|
3765
|
+
let d = id.get(key);
|
|
3766
|
+
if (d === void 0) {
|
|
3767
|
+
d = cells++;
|
|
3768
|
+
id.set(key, d);
|
|
3769
|
+
}
|
|
3770
|
+
dense[i] = d;
|
|
3771
|
+
}
|
|
3772
|
+
const start = new Uint32Array(cells + 1);
|
|
3773
|
+
for (let i = 0; i < n; i++) start[dense[i] + 1]++;
|
|
3774
|
+
for (let c = 0; c < cells; c++) start[c + 1] += start[c];
|
|
3775
|
+
const order = new Uint32Array(n);
|
|
3776
|
+
const cursor = new Uint32Array(cells);
|
|
3777
|
+
for (let i = 0; i < n; i++) {
|
|
3778
|
+
const d = dense[i];
|
|
3779
|
+
order[start[d] + cursor[d]++] = i;
|
|
3780
|
+
}
|
|
3781
|
+
return { order, start, cells };
|
|
3782
|
+
}
|
|
3783
|
+
var CELL_STRIDE = 1024;
|
|
3784
|
+
function cellKeyOf(x, y, z, minX, minY, minZ, invVoxel) {
|
|
3785
|
+
const cx = Math.floor((x - minX) * invVoxel);
|
|
3786
|
+
const cy = Math.floor((y - minY) * invVoxel);
|
|
3787
|
+
const cz = Math.floor((z - minZ) * invVoxel);
|
|
3788
|
+
return (cx * CELL_STRIDE + cy) * CELL_STRIDE + cz;
|
|
3789
|
+
}
|
|
3790
|
+
function cellKeysFor(centers, count, voxelSize) {
|
|
3791
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
3792
|
+
for (let i = 0, c = 0; i < count; i++, c += 3) {
|
|
3793
|
+
if (centers[c] < minX) minX = centers[c];
|
|
3794
|
+
if (centers[c + 1] < minY) minY = centers[c + 1];
|
|
3795
|
+
if (centers[c + 2] < minZ) minZ = centers[c + 2];
|
|
3796
|
+
}
|
|
3797
|
+
const inv = 1 / voxelSize;
|
|
3798
|
+
const keys = new Float64Array(count);
|
|
3799
|
+
for (let i = 0, c = 0; i < count; i++, c += 3) {
|
|
3800
|
+
keys[i] = cellKeyOf(centers[c], centers[c + 1], centers[c + 2], minX, minY, minZ, inv);
|
|
3801
|
+
}
|
|
3802
|
+
return keys;
|
|
3803
|
+
}
|
|
3804
|
+
function buildImportanceOrder(packed, voxelSize) {
|
|
3805
|
+
const n = packed.numSplats;
|
|
3806
|
+
const out = new Uint32Array(n);
|
|
3807
|
+
if (n === 0) return out;
|
|
3808
|
+
const { color, transformB, centers } = packed;
|
|
3809
|
+
const half = DataUtils2.fromHalfFloat;
|
|
3810
|
+
const score = new Float32Array(n);
|
|
3811
|
+
for (let i = 0; i < n; i++) {
|
|
3812
|
+
const o = i * 4;
|
|
3813
|
+
const alpha = half(color[o + 3]);
|
|
3814
|
+
const s0 = half(transformB[o]);
|
|
3815
|
+
const s1 = half(transformB[o + 1]);
|
|
3816
|
+
const s2 = half(transformB[o + 2]);
|
|
3817
|
+
const mx = s0 > s1 ? s0 > s2 ? s0 : s2 : s1 > s2 ? s1 : s2;
|
|
3818
|
+
const mn = s0 < s1 ? s0 < s2 ? s0 : s2 : s1 < s2 ? s1 : s2;
|
|
3819
|
+
score[i] = alpha * mx * (s0 + s1 + s2 - mx - mn);
|
|
3820
|
+
}
|
|
3821
|
+
const { order, start, cells } = bucketByCell(cellKeysFor(centers, n, voxelSize), n);
|
|
3822
|
+
let widest = 0;
|
|
3823
|
+
const scratch = [];
|
|
3824
|
+
for (let c = 0; c < cells; c++) {
|
|
3825
|
+
const lo = start[c];
|
|
3826
|
+
const hi = start[c + 1];
|
|
3827
|
+
const size = hi - lo;
|
|
3828
|
+
if (size > widest) widest = size;
|
|
3829
|
+
if (size < 2) continue;
|
|
3830
|
+
scratch.length = 0;
|
|
3831
|
+
for (let k = lo; k < hi; k++) scratch.push(order[k]);
|
|
3832
|
+
scratch.sort((a, b) => score[b] - score[a]);
|
|
3833
|
+
for (let k = 0; k < size; k++) order[lo + k] = scratch[k];
|
|
3834
|
+
}
|
|
3835
|
+
let w = 0;
|
|
3836
|
+
for (let rank = 0; rank < widest; rank++) {
|
|
3837
|
+
for (let c = 0; c < cells; c++) {
|
|
3838
|
+
const at = start[c] + rank;
|
|
3839
|
+
if (at < start[c + 1]) out[w++] = order[at];
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
return out;
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
// src/splat/quality.ts
|
|
3846
|
+
var MAX_MIN_OPACITY = 0.1;
|
|
3847
|
+
var _quadAlphaCutoff = 8 / 255;
|
|
3848
|
+
var _screenSizeCull = 0;
|
|
3849
|
+
var _minOpacity = 0.05;
|
|
3850
|
+
var _pruneRatio = 0;
|
|
3851
|
+
var _pruneVoxelSize = 0.04;
|
|
3852
|
+
var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
|
|
3853
|
+
var warnedAboutCap = false;
|
|
3854
|
+
var SplatQuality = {
|
|
3855
|
+
/**
|
|
3856
|
+
* Alpha at which a splat's quad is allowed to end, so each one is sized to
|
|
3857
|
+
* the radius its own opacity actually reaches instead of to the worst case.
|
|
3858
|
+
* **The first knob to reach for on a weak GPU.**
|
|
3859
|
+
*
|
|
3860
|
+
* The fragment stage multiplies a gaussian falloff by the splat's opacity
|
|
3861
|
+
* and discards anything below 1/255, so a quad drawn out to where that
|
|
3862
|
+
* product is already below the cutoff spends fragments on a ring that
|
|
3863
|
+
* cannot put ink on the screen. Raising the cutoff shrinks every quad. What
|
|
3864
|
+
* it costs is a harder edge on each splat, which shows first on soft things
|
|
3865
|
+
* - hair, fabric, anything semi-transparent.
|
|
3866
|
+
*
|
|
3867
|
+
* Takes effect on the next frame; nothing is reloaded and nothing is lost,
|
|
3868
|
+
* so it is safe to drive from a frame-rate governor.
|
|
3869
|
+
*
|
|
3870
|
+
* | Value | What it is for |
|
|
3871
|
+
* |---|---|
|
|
3872
|
+
* | `1 / 255` | Exactly the behaviour before this knob existed. Changes nothing at all: below it a fragment is discarded anyway, so the quad it trims was drawing nothing. |
|
|
3873
|
+
* | `8 / 255` | What the Unity build ships. The conservative end. |
|
|
3874
|
+
* | `12 / 255` | **The default here.** |
|
|
3875
|
+
* | `16 / 255` | **Low-end phones, and the floor.** Still 43.7 dB overall and 34.8 dB on the face, which the Unity build calls invisible at arm's length. |
|
|
3876
|
+
* | above `16 / 255` | Not on this project. The Unity build measures a further -7% at 24/255, but the edges are visibly harder by then; a device that needs more should spend it on {@link SplatQuality.pruneRatio}, the renderer's pixel ratio, or a distance LOD, none of which harden every silhouette in the frame. |
|
|
3877
|
+
*
|
|
3878
|
+
* Measured on a Mali-G57 with three characters, against 8/255: 12/255 -7%,
|
|
3879
|
+
* 16/255 -12%, 24/255 -19%. Measured here on an Apple GPU where fragments
|
|
3880
|
+
* are the bottleneck, against 1/255: 8/255 -29%, 16/255 -42%, 24/255 -50%.
|
|
3881
|
+
* Where the main thread is the bottleneck instead - the WebGL backend with
|
|
3882
|
+
* animated characters - it is free but buys nothing; see the README's
|
|
3883
|
+
* Tuning section for which case a scene is in.
|
|
3884
|
+
*/
|
|
3885
|
+
get quadAlphaCutoff() {
|
|
3886
|
+
return _quadAlphaCutoff;
|
|
3887
|
+
},
|
|
3888
|
+
set quadAlphaCutoff(value) {
|
|
3889
|
+
_quadAlphaCutoff = clamp(value, 0, 1);
|
|
3890
|
+
},
|
|
3891
|
+
/**
|
|
3892
|
+
* Drop splats whose projected size falls below this radius in pixels, for
|
|
3893
|
+
* every splat at once. `0` (the default) draws everything.
|
|
3894
|
+
*
|
|
3895
|
+
* The same test as the per-splat {@link Splat.minPixelSize}; the larger of
|
|
3896
|
+
* the two wins, so a global floor cannot un-cull a splat an application
|
|
3897
|
+
* already asked to cull. Removes distant crowd splats that cannot contribute
|
|
3898
|
+
* more than a fraction of a pixel.
|
|
3899
|
+
*/
|
|
3900
|
+
get screenSizeCull() {
|
|
3901
|
+
return _screenSizeCull;
|
|
3902
|
+
},
|
|
3903
|
+
set screenSizeCull(value) {
|
|
3904
|
+
_screenSizeCull = Math.max(0, value);
|
|
3905
|
+
},
|
|
3906
|
+
/**
|
|
3907
|
+
* Peak opacity a splat must reach to be kept when a character or scene is
|
|
3908
|
+
* loaded. `0` keeps everything.
|
|
3909
|
+
*
|
|
3910
|
+
* A splat's alpha at its centre IS its opacity - the gaussian only falls
|
|
3911
|
+
* off from there - so the faintest splats in a bundle are ones that paint
|
|
3912
|
+
* almost nothing from any camera while still costing memory, a skinning
|
|
3913
|
+
* pass, a sort slot and a quad. This removes them outright, which is what
|
|
3914
|
+
* separates it from {@link SplatQuality.quadAlphaCutoff}: the cutoff
|
|
3915
|
+
* shrinks a splat's quad and saves rasterisation alone, this saves
|
|
3916
|
+
* everything, but it cannot half-remove one.
|
|
3917
|
+
*
|
|
3918
|
+
* **Applied during the decode of a CHARACTER bundle**, so it only affects
|
|
3919
|
+
* what is loaded afterwards - set it before the first `load()`, and changing
|
|
3920
|
+
* it later does nothing to characters already in the scene. It does not
|
|
3921
|
+
* touch `.ply` scenes, matching the Unity build, which applies it in the
|
|
3922
|
+
* skinned renderer and nowhere else: a scene's opacity distribution is a
|
|
3923
|
+
* different thing, and 0.05 measured as 8.7% of a character against 41.8%
|
|
3924
|
+
* of a 1M-splat room. {@link pruneSplats} is exported for an application
|
|
3925
|
+
* that wants to make the trade on a scene of its own. It also changes
|
|
3926
|
+
* {@link Splat.numSplats}, so an application that hands back a per-splat
|
|
3927
|
+
* array ({@link Splat.setSplatGroups}) must size that array from
|
|
3928
|
+
* `numSplats` after loading rather than from the file.
|
|
3929
|
+
*
|
|
3930
|
+
* | Value | What it is for |
|
|
3931
|
+
* |---|---|
|
|
3932
|
+
* | `0` | Keep every splat. The behaviour before this knob existed. |
|
|
3933
|
+
* | `1 / 255` | Free in the strict sense: provably pixel-identical, because a splat under the fragment shader's own discard cannot produce a pixel. Worth about 2% of a character. |
|
|
3934
|
+
* | `0.05` | **The default here, and what the Unity build ships.** Removes ~13% of a typical character for a difference of at most 11/255 on 1.3% of it - 58-62 dB, which does not read as a quality loss. |
|
|
3935
|
+
* | above `0.05` | Untested. Prefer {@link SplatQuality.pruneRatio} to go further: it is measured, and it thins every region by the same share instead of taking the faintest splats wherever they happen to be - which is disproportionately the face. |
|
|
3936
|
+
*/
|
|
3937
|
+
get minOpacity() {
|
|
3938
|
+
return _minOpacity;
|
|
3939
|
+
},
|
|
3940
|
+
set minOpacity(value) {
|
|
3941
|
+
if (value > MAX_MIN_OPACITY && !warnedAboutCap) {
|
|
3942
|
+
warnedAboutCap = true;
|
|
3943
|
+
console.warn(
|
|
3944
|
+
`[splat-engine-three] SplatQuality.minOpacity is capped at ${MAX_MIN_OPACITY}; ${value} was requested and ${MAX_MIN_OPACITY} applied. Past the cap a character loses the faint splats its soft edges are made of. To go further, use pruneRatio, the renderer's pixel ratio, or a distance LOD.`
|
|
3945
|
+
);
|
|
3946
|
+
}
|
|
3947
|
+
_minOpacity = clamp(value, 0, MAX_MIN_OPACITY);
|
|
3948
|
+
},
|
|
3949
|
+
/**
|
|
3950
|
+
* Fraction of splats removed at load by voxel-local importance, on top of
|
|
3951
|
+
* {@link SplatQuality.minOpacity}. `0` (the default) keeps everything, `0.5`
|
|
3952
|
+
* drops the least-important half of every cell.
|
|
3953
|
+
*
|
|
3954
|
+
* Importance is opacity times a splat's two largest axes - the area it
|
|
3955
|
+
* actually paints - ranked separately inside each spatial cell so a dense
|
|
3956
|
+
* region loses the same share as a sparse one rather than being deleted
|
|
3957
|
+
* wholesale. That per-cell quota is what keeps the face: a global ranking
|
|
3958
|
+
* puts the many faint facial splats at the bottom and smears it. Measured on
|
|
3959
|
+
* the Unity build's reference character at 0.5: 25,826 of 50,267 splats
|
|
3960
|
+
* remain, the frame falls 24.5% on a Mali-G57, and the face holds 43.5 dB
|
|
3961
|
+
* where a global rank gives 37.6.
|
|
3962
|
+
*
|
|
3963
|
+
* Applied during the decode of a character bundle, like
|
|
3964
|
+
* {@link SplatQuality.minOpacity}, with the same scope and the same reason
|
|
3965
|
+
* for being off by default.
|
|
3966
|
+
*/
|
|
3967
|
+
get pruneRatio() {
|
|
3968
|
+
return _pruneRatio;
|
|
3969
|
+
},
|
|
3970
|
+
set pruneRatio(value) {
|
|
3971
|
+
_pruneRatio = clamp(value, 0, 1);
|
|
3972
|
+
},
|
|
3973
|
+
/**
|
|
3974
|
+
* Cell size, in metres, for {@link SplatQuality.pruneRatio}'s per-voxel
|
|
3975
|
+
* quota.
|
|
3976
|
+
*
|
|
3977
|
+
* 4 cm gives ~19 splats per occupied cell on the Unity build's reference
|
|
3978
|
+
* character - small enough that the face and the jacket are ranked apart,
|
|
3979
|
+
* large enough that a cell has a population to rank. A tuning constant
|
|
3980
|
+
* rather than a per-title knob, exposed because a character authored at a
|
|
3981
|
+
* different scale needs a different one.
|
|
3982
|
+
*/
|
|
3983
|
+
get pruneVoxelSize() {
|
|
3984
|
+
return _pruneVoxelSize;
|
|
3985
|
+
},
|
|
3986
|
+
set pruneVoxelSize(value) {
|
|
3987
|
+
_pruneVoxelSize = Math.max(1e-4, value);
|
|
3988
|
+
},
|
|
3989
|
+
/**
|
|
3990
|
+
* Test each splat against the depth buffer, so opaque geometry hides the
|
|
3991
|
+
* splats behind it. On by default.
|
|
3992
|
+
*
|
|
3993
|
+
* Unlike the Unity build, where turning this off was the single largest
|
|
3994
|
+
* mobile saving, this is a correctness switch here and not a performance
|
|
3995
|
+
* one: that build renders splats into an offscreen target and pays for the
|
|
3996
|
+
* test by sampling a depth texture per fragment, where this package draws
|
|
3997
|
+
* straight into the camera's target and gets the hardware depth test for
|
|
3998
|
+
* free. Turning it off makes characters draw through walls and saves
|
|
3999
|
+
* nothing. It is here because the behaviour is occasionally what an
|
|
4000
|
+
* application wants - a character deliberately drawn over the scene.
|
|
4001
|
+
*/
|
|
4002
|
+
depthOcclusion: true,
|
|
4003
|
+
/**
|
|
4004
|
+
* Collect {@link SplatQuality.timings}. Off by default.
|
|
4005
|
+
*
|
|
4006
|
+
* The CPU stages are timed with `performance.now()`, which is cheap but not
|
|
4007
|
+
* free at a few calls per splat per frame, and a shipping application that
|
|
4008
|
+
* never reads the numbers should not pay for them.
|
|
4009
|
+
*/
|
|
4010
|
+
passTimings: false,
|
|
4011
|
+
/**
|
|
4012
|
+
* Per-stage cost of the last frame in which {@link SplatQuality.passTimings}
|
|
4013
|
+
* was on, in milliseconds, summed over every splat that ran that stage.
|
|
4014
|
+
*
|
|
4015
|
+
* These are main-thread times, which is the measurement that matters for
|
|
4016
|
+
* this package: under `WebGLRenderer` the per-character CPU cost - skinning
|
|
4017
|
+
* every centre to feed the sort, then the sort - is what limits how many
|
|
4018
|
+
* characters a scene can carry, and neither stage exists on the GPU
|
|
4019
|
+
* timeline. `skin` and `sort` are zero under `WebGPURenderer`, where both
|
|
4020
|
+
* happen in compute; `splatsDrawn` and `splatsTotal` are meaningful on both,
|
|
4021
|
+
* and differ when {@link SplatQuality.screenSizeCull} or a count LOD is
|
|
4022
|
+
* reducing what is submitted.
|
|
4023
|
+
*
|
|
4024
|
+
* These describe the most recent pass in which any splat drew, so read them
|
|
4025
|
+
* after your render call. A pass that drew no splats leaves the previous
|
|
4026
|
+
* numbers in place rather than zeroing them - a zero and "nothing happened"
|
|
4027
|
+
* would otherwise read the same, which is the mistake the Unity build's
|
|
4028
|
+
* equivalent made and spent a long time chasing.
|
|
4029
|
+
*/
|
|
4030
|
+
timings: {
|
|
4031
|
+
/** CPU skinning of splat centres, i.e. `Splat.ensureCenters`. */
|
|
4032
|
+
skin: 0,
|
|
4033
|
+
/** The depth sort, where it runs on the main thread. */
|
|
4034
|
+
sort: 0,
|
|
4035
|
+
/** Uniform and buffer updates in `beforeRender`, excluding skin and sort. */
|
|
4036
|
+
prepare: 0,
|
|
4037
|
+
/** Splat instances submitted to the GPU this frame. */
|
|
4038
|
+
splatsDrawn: 0,
|
|
4039
|
+
/** Splat instances those draws could have covered at full detail. */
|
|
4040
|
+
splatsTotal: 0
|
|
4041
|
+
},
|
|
4042
|
+
/** Restore every knob to its default. Call when tearing the engine down. */
|
|
4043
|
+
resetStatics() {
|
|
4044
|
+
SplatQuality.quadAlphaCutoff = 8 / 255;
|
|
4045
|
+
SplatQuality.screenSizeCull = 0;
|
|
4046
|
+
SplatQuality.minOpacity = 0.05;
|
|
4047
|
+
SplatQuality.pruneRatio = 0;
|
|
4048
|
+
SplatQuality.pruneVoxelSize = 0.04;
|
|
4049
|
+
SplatQuality.depthOcclusion = true;
|
|
4050
|
+
SplatQuality.passTimings = false;
|
|
4051
|
+
resetTimings();
|
|
4052
|
+
}
|
|
4053
|
+
};
|
|
4054
|
+
function effectiveMinPixelSize(splatMinPixelSize) {
|
|
4055
|
+
const global = SplatQuality.screenSizeCull;
|
|
4056
|
+
return global > splatMinPixelSize ? global : splatMinPixelSize;
|
|
4057
|
+
}
|
|
4058
|
+
var pendingSkin = 0;
|
|
4059
|
+
var contributors = /* @__PURE__ */ new Set();
|
|
4060
|
+
function addSkinTime(ms) {
|
|
4061
|
+
pendingSkin += ms;
|
|
4062
|
+
}
|
|
4063
|
+
function resetTimings() {
|
|
4064
|
+
const t = SplatQuality.timings;
|
|
4065
|
+
t.skin = pendingSkin;
|
|
4066
|
+
pendingSkin = 0;
|
|
4067
|
+
t.sort = 0;
|
|
4068
|
+
t.prepare = 0;
|
|
4069
|
+
t.splatsDrawn = 0;
|
|
4070
|
+
t.splatsTotal = 0;
|
|
4071
|
+
}
|
|
4072
|
+
function beginFrameTimings(source) {
|
|
4073
|
+
if (contributors.has(source)) {
|
|
4074
|
+
contributors.clear();
|
|
4075
|
+
resetTimings();
|
|
4076
|
+
}
|
|
4077
|
+
contributors.add(source);
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
// src/splat/prune.ts
|
|
4081
|
+
var COLUMNS = [
|
|
4082
|
+
"x",
|
|
4083
|
+
"y",
|
|
4084
|
+
"z",
|
|
4085
|
+
"scale_0",
|
|
4086
|
+
"scale_1",
|
|
4087
|
+
"scale_2",
|
|
4088
|
+
"rot_0",
|
|
4089
|
+
"rot_1",
|
|
4090
|
+
"rot_2",
|
|
4091
|
+
"rot_3",
|
|
4092
|
+
"f_dc_0",
|
|
4093
|
+
"f_dc_1",
|
|
4094
|
+
"f_dc_2",
|
|
4095
|
+
"opacity"
|
|
4096
|
+
];
|
|
4097
|
+
function pruneSplats(numSplats, attributes, weights, opts) {
|
|
4098
|
+
const minOpacity = opts?.minOpacity ?? SplatQuality.minOpacity;
|
|
4099
|
+
const pruneRatio = opts?.pruneRatio ?? SplatQuality.pruneRatio;
|
|
4100
|
+
const voxelSize = opts?.voxelSize ?? SplatQuality.pruneVoxelSize;
|
|
4101
|
+
const report = {
|
|
4102
|
+
numSplatsIn: numSplats,
|
|
4103
|
+
numSplatsOut: numSplats,
|
|
4104
|
+
droppedByOpacity: 0,
|
|
4105
|
+
droppedByImportance: 0,
|
|
4106
|
+
weights
|
|
4107
|
+
};
|
|
4108
|
+
if (numSplats === 0) return report;
|
|
4109
|
+
const k = influencesPerSplat(numSplats, weights);
|
|
4110
|
+
let count = numSplats;
|
|
4111
|
+
if (minOpacity > 0) {
|
|
4112
|
+
const kept = cullByOpacity(count, attributes, weights, k, minOpacity);
|
|
4113
|
+
report.droppedByOpacity = count - kept;
|
|
4114
|
+
count = kept;
|
|
4115
|
+
}
|
|
4116
|
+
if (pruneRatio > 0 && voxelSize > 0) {
|
|
4117
|
+
const kept = pruneByImportance(count, attributes, weights, k, Math.min(pruneRatio, 1), voxelSize);
|
|
4118
|
+
report.droppedByImportance = count - kept;
|
|
4119
|
+
count = kept;
|
|
4120
|
+
}
|
|
4121
|
+
report.numSplatsOut = count;
|
|
4122
|
+
if (weights && count !== numSplats) {
|
|
4123
|
+
report.weights = {
|
|
4124
|
+
indices: weights.indices.subarray(0, count * k),
|
|
4125
|
+
weights: weights.weights.subarray(0, count * k)
|
|
4126
|
+
};
|
|
4127
|
+
}
|
|
4128
|
+
return report;
|
|
4129
|
+
}
|
|
4130
|
+
function influencesPerSplat(numSplats, weights) {
|
|
4131
|
+
if (!weights || numSplats === 0) return 0;
|
|
4132
|
+
return Math.max(1, Math.floor(weights.indices.length / numSplats));
|
|
4133
|
+
}
|
|
4134
|
+
function cullByOpacity(numSplats, attributes, weights, influences, minOpacity) {
|
|
4135
|
+
if (minOpacity >= 1) return 0;
|
|
4136
|
+
const threshold = Math.log(minOpacity / (1 - minOpacity));
|
|
4137
|
+
const opacity = attributes.opacity;
|
|
4138
|
+
const keep = new Uint8Array(numSplats);
|
|
4139
|
+
let kept = 0;
|
|
4140
|
+
for (let i = 0; i < numSplats; i++) {
|
|
4141
|
+
if (opacity[i] >= threshold) {
|
|
4142
|
+
keep[i] = 1;
|
|
4143
|
+
kept++;
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
if (kept === numSplats) return kept;
|
|
4147
|
+
compact(numSplats, keep, attributes, weights, influences);
|
|
4148
|
+
return kept;
|
|
4149
|
+
}
|
|
4150
|
+
function pruneByImportance(numSplats, attributes, weights, influences, ratio, voxelSize) {
|
|
4151
|
+
const { x, y, z, scale_0, scale_1, scale_2, opacity } = attributes;
|
|
4152
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
4153
|
+
for (let i = 0; i < numSplats; i++) {
|
|
4154
|
+
if (x[i] < minX) minX = x[i];
|
|
4155
|
+
if (y[i] < minY) minY = y[i];
|
|
4156
|
+
if (z[i] < minZ) minZ = z[i];
|
|
4157
|
+
}
|
|
4158
|
+
const invVoxel = 1 / voxelSize;
|
|
4159
|
+
const score = new Float32Array(numSplats);
|
|
4160
|
+
const cellKey = new Float64Array(numSplats);
|
|
4161
|
+
for (let i = 0; i < numSplats; i++) {
|
|
4162
|
+
const s0 = Math.exp(scale_0[i]);
|
|
4163
|
+
const s1 = Math.exp(scale_1[i]);
|
|
4164
|
+
const s2 = Math.exp(scale_2[i]);
|
|
4165
|
+
const mx = s0 > s1 ? s0 > s2 ? s0 : s2 : s1 > s2 ? s1 : s2;
|
|
4166
|
+
const mn = s0 < s1 ? s0 < s2 ? s0 : s2 : s1 < s2 ? s1 : s2;
|
|
4167
|
+
const md = s0 + s1 + s2 - mx - mn;
|
|
4168
|
+
const alpha = 1 / (1 + Math.exp(-opacity[i]));
|
|
4169
|
+
score[i] = alpha * mx * md;
|
|
4170
|
+
cellKey[i] = cellKeyOf(x[i], y[i], z[i], minX, minY, minZ, invVoxel);
|
|
4171
|
+
}
|
|
4172
|
+
const { order, start, cells } = bucketByCell(cellKey, numSplats);
|
|
4173
|
+
const keep = new Uint8Array(numSplats).fill(1);
|
|
4174
|
+
let dropped = 0;
|
|
4175
|
+
for (let c = 0; c < cells; c++) {
|
|
4176
|
+
const lo = start[c];
|
|
4177
|
+
const hi = start[c + 1];
|
|
4178
|
+
const size = hi - lo;
|
|
4179
|
+
const dropN = Math.floor(size * ratio);
|
|
4180
|
+
if (dropN <= 0) continue;
|
|
4181
|
+
if (dropN >= size) {
|
|
4182
|
+
for (let k = lo; k < hi; k++) keep[order[k]] = 0;
|
|
4183
|
+
dropped += size;
|
|
4184
|
+
continue;
|
|
4185
|
+
}
|
|
4186
|
+
selectWeakest(order, score, lo, hi, dropN);
|
|
4187
|
+
for (let k = 0; k < dropN; k++) keep[order[lo + k]] = 0;
|
|
4188
|
+
dropped += dropN;
|
|
4189
|
+
}
|
|
4190
|
+
if (dropped === 0) return numSplats;
|
|
4191
|
+
compact(numSplats, keep, attributes, weights, influences);
|
|
4192
|
+
return numSplats - dropped;
|
|
4193
|
+
}
|
|
4194
|
+
function selectWeakest(order, score, lo, hi, k) {
|
|
4195
|
+
while (hi - lo > 1) {
|
|
4196
|
+
const mid = lo + (hi - lo >> 1);
|
|
4197
|
+
const a = score[order[lo]];
|
|
4198
|
+
const b = score[order[mid]];
|
|
4199
|
+
const c = score[order[hi - 1]];
|
|
4200
|
+
const pivot = a < b ? b < c ? b : a < c ? c : a : a < c ? a : b < c ? c : b;
|
|
4201
|
+
let i = lo;
|
|
4202
|
+
let j = hi - 1;
|
|
4203
|
+
while (i <= j) {
|
|
4204
|
+
while (score[order[i]] < pivot) i++;
|
|
4205
|
+
while (score[order[j]] > pivot) j--;
|
|
4206
|
+
if (i <= j) {
|
|
4207
|
+
const t = order[i];
|
|
4208
|
+
order[i] = order[j];
|
|
4209
|
+
order[j] = t;
|
|
4210
|
+
i++;
|
|
4211
|
+
j--;
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
if (lo + k <= i && lo + k > j + 1) return;
|
|
4215
|
+
if (lo + k <= j + 1) hi = j + 1;
|
|
4216
|
+
else {
|
|
4217
|
+
k -= i - lo;
|
|
4218
|
+
lo = i;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
function compact(numSplats, keep, attributes, weights, k) {
|
|
4223
|
+
const indices = weights?.indices;
|
|
4224
|
+
const values = weights?.weights;
|
|
4225
|
+
let w = 0;
|
|
4226
|
+
for (let r = 0; r < numSplats; r++) {
|
|
4227
|
+
if (!keep[r]) continue;
|
|
4228
|
+
if (w !== r) {
|
|
4229
|
+
for (const column of COLUMNS) attributes[column][w] = attributes[column][r];
|
|
4230
|
+
if (indices && values) {
|
|
4231
|
+
for (let j = 0; j < k; j++) {
|
|
4232
|
+
indices[w * k + j] = indices[r * k + j];
|
|
4233
|
+
values[w * k + j] = values[r * k + j];
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
w++;
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
|
|
3744
4241
|
// src/loader/character/types.ts
|
|
3745
4242
|
var CharacterBundleError = class extends Error {
|
|
3746
4243
|
category;
|
|
@@ -4854,10 +5351,16 @@ async function decodeCharacterBundle(file) {
|
|
|
4854
5351
|
};
|
|
4855
5352
|
}
|
|
4856
5353
|
const header = result.header;
|
|
5354
|
+
const decodedWeights = result.boneIndices && result.boneWeights ? { indices: result.boneIndices, weights: result.boneWeights } : void 0;
|
|
5355
|
+
const { numSplatsOut, weights: splatWeights } = pruneSplats(
|
|
5356
|
+
result.numSplats,
|
|
5357
|
+
result.splats,
|
|
5358
|
+
decodedWeights
|
|
5359
|
+
);
|
|
4857
5360
|
return {
|
|
4858
5361
|
source: {
|
|
4859
|
-
packed: packSplats(
|
|
4860
|
-
splatWeights
|
|
5362
|
+
packed: packSplats(numSplatsOut, result.splats),
|
|
5363
|
+
splatWeights,
|
|
4861
5364
|
sourceBoneCount: header?.numBones ?? armatureData?.numBones
|
|
4862
5365
|
},
|
|
4863
5366
|
armatureData,
|
|
@@ -5019,7 +5522,7 @@ import {
|
|
|
5019
5522
|
} from "three";
|
|
5020
5523
|
|
|
5021
5524
|
// src/splat/depth-sort.ts
|
|
5022
|
-
function countingSortByDepth(centers, count, dx, dy, dz, depthMin, depthMax, order, scratch) {
|
|
5525
|
+
function countingSortByDepth(centers, count, dx, dy, dz, depthMin, depthMax, order, scratch, subset) {
|
|
5023
5526
|
if (count === 0) return;
|
|
5024
5527
|
const bits = Math.max(10, Math.min(20, Math.round(Math.log2(count / 4))));
|
|
5025
5528
|
const bucketCount = 1 << bits;
|
|
@@ -5031,17 +5534,31 @@ function countingSortByDepth(centers, count, dx, dy, dz, depthMin, depthMax, ord
|
|
|
5031
5534
|
const range = depthMax - depthMin;
|
|
5032
5535
|
const scale = range > 1e-12 ? (bucketCount - 1) / range : 0;
|
|
5033
5536
|
const last = bucketCount - 1;
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5537
|
+
if (subset) {
|
|
5538
|
+
for (let i = 0; i < count; i++) {
|
|
5539
|
+
const c = subset[i] * 3;
|
|
5540
|
+
const depth = centers[c] * dx + centers[c + 1] * dy + centers[c + 2] * dz;
|
|
5541
|
+
let key = (depthMax - depth) * scale | 0;
|
|
5542
|
+
if (key < 0) key = 0;
|
|
5543
|
+
else if (key > last) key = last;
|
|
5544
|
+
keys[i] = key;
|
|
5545
|
+
counts[key]++;
|
|
5546
|
+
}
|
|
5547
|
+
} else {
|
|
5548
|
+
for (let i = 0, c = 0; i < count; i++, c += 3) {
|
|
5549
|
+
const depth = centers[c] * dx + centers[c + 1] * dy + centers[c + 2] * dz;
|
|
5550
|
+
let key = (depthMax - depth) * scale | 0;
|
|
5551
|
+
if (key < 0) key = 0;
|
|
5552
|
+
else if (key > last) key = last;
|
|
5553
|
+
keys[i] = key;
|
|
5554
|
+
counts[key]++;
|
|
5555
|
+
}
|
|
5041
5556
|
}
|
|
5042
5557
|
for (let i = 1; i < bucketCount; i++) counts[i] += counts[i - 1];
|
|
5043
|
-
|
|
5044
|
-
order[--counts[keys[i]]] = i;
|
|
5558
|
+
if (subset) {
|
|
5559
|
+
for (let i = count - 1; i >= 0; i--) order[--counts[keys[i]]] = subset[i];
|
|
5560
|
+
} else {
|
|
5561
|
+
for (let i = count - 1; i >= 0; i--) order[--counts[keys[i]]] = i;
|
|
5045
5562
|
}
|
|
5046
5563
|
}
|
|
5047
5564
|
function projectBoundsOnDirection(min, max, dx, dy, dz) {
|
|
@@ -5051,8 +5568,14 @@ function projectBoundsOnDirection(min, max, dx, dy, dz) {
|
|
|
5051
5568
|
}
|
|
5052
5569
|
var DepthSorter = class {
|
|
5053
5570
|
_scratch = { keys: null, counts: null };
|
|
5054
|
-
|
|
5055
|
-
|
|
5571
|
+
/**
|
|
5572
|
+
* @param count - How many splats take part. With `subset`, the length of
|
|
5573
|
+
* the prefix of it to use; without, the splats are `[0, count)`.
|
|
5574
|
+
* @param subset - Splat indices to sort, for a count LOD. See
|
|
5575
|
+
* {@link Splat.setCountLod}.
|
|
5576
|
+
*/
|
|
5577
|
+
sort(centers, count, dx, dy, dz, depthMin, depthMax, order, subset) {
|
|
5578
|
+
countingSortByDepth(centers, count, dx, dy, dz, depthMin, depthMax, order, this._scratch, subset);
|
|
5056
5579
|
}
|
|
5057
5580
|
};
|
|
5058
5581
|
var AsyncDepthSorter = class {
|
|
@@ -5062,22 +5585,26 @@ var AsyncDepthSorter = class {
|
|
|
5062
5585
|
_order;
|
|
5063
5586
|
/**
|
|
5064
5587
|
* @param centers - Flat `[x,y,z] * count`. Copied into the worker once.
|
|
5065
|
-
* @param onSorted - Called with each finished order
|
|
5066
|
-
*
|
|
5067
|
-
* request - so copy out of it
|
|
5588
|
+
* @param onSorted - Called with each finished order and how many of its
|
|
5589
|
+
* entries are current. The array is only valid for the duration of the call
|
|
5590
|
+
* - it goes back to the worker on the next request - so copy out of it
|
|
5591
|
+
* before returning.
|
|
5068
5592
|
*/
|
|
5069
5593
|
constructor(centers, count, onSorted) {
|
|
5070
5594
|
this._order = new Uint32Array(count);
|
|
5071
5595
|
const source = `
|
|
5072
5596
|
const sortByDepth = (${countingSortByDepth.toString()});
|
|
5073
5597
|
let centers = null;
|
|
5598
|
+
let subset = null;
|
|
5074
5599
|
const scratch = { keys: null, counts: null };
|
|
5075
5600
|
self.onmessage = (e) => {
|
|
5076
5601
|
const m = e.data;
|
|
5077
5602
|
if (m.centers) { centers = m.centers; return; }
|
|
5603
|
+
if (m.subset) { subset = m.subset; return; }
|
|
5078
5604
|
const order = m.order;
|
|
5079
|
-
|
|
5080
|
-
|
|
5605
|
+
const count = m.count === undefined ? order.length : m.count;
|
|
5606
|
+
sortByDepth(centers, count, m.dx, m.dy, m.dz, m.depthMin, m.depthMax, order, scratch, subset);
|
|
5607
|
+
self.postMessage({ order, count }, [order.buffer]);
|
|
5081
5608
|
};
|
|
5082
5609
|
`;
|
|
5083
5610
|
const url = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
|
|
@@ -5085,7 +5612,7 @@ var AsyncDepthSorter = class {
|
|
|
5085
5612
|
URL.revokeObjectURL(url);
|
|
5086
5613
|
this._worker.onmessage = (e) => {
|
|
5087
5614
|
this._order = e.data.order;
|
|
5088
|
-
onSorted(this._order);
|
|
5615
|
+
onSorted(this._order, e.data.count);
|
|
5089
5616
|
if (this._pending) {
|
|
5090
5617
|
const next = this._pending;
|
|
5091
5618
|
this._pending = null;
|
|
@@ -5095,6 +5622,16 @@ var AsyncDepthSorter = class {
|
|
|
5095
5622
|
const copy = centers.slice(0, count * 3);
|
|
5096
5623
|
this._worker.postMessage({ centers: copy }, [copy.buffer]);
|
|
5097
5624
|
}
|
|
5625
|
+
/**
|
|
5626
|
+
* Give the worker the importance ordering a count LOD selects from. Sent
|
|
5627
|
+
* once and kept there, like the centres; after this a request carries only
|
|
5628
|
+
* how many of its entries to use.
|
|
5629
|
+
*/
|
|
5630
|
+
setSubset(subset) {
|
|
5631
|
+
if (!this._worker) return;
|
|
5632
|
+
const copy = subset.slice();
|
|
5633
|
+
this._worker.postMessage({ subset: copy }, [copy.buffer]);
|
|
5634
|
+
}
|
|
5098
5635
|
/** True while a sort is running in the worker. */
|
|
5099
5636
|
get busy() {
|
|
5100
5637
|
return this._order === null;
|
|
@@ -5382,6 +5919,11 @@ uniform int uTexWidth;
|
|
|
5382
5919
|
// Drawing-buffer size in pixels.
|
|
5383
5920
|
uniform vec2 uViewport;
|
|
5384
5921
|
uniform float uMinPixelSize;
|
|
5922
|
+
// 1 / the alpha a splat's quad is allowed to end at. Carried as the reciprocal
|
|
5923
|
+
// so the historic 1/255 cutoff arrives as exactly 255.0 and the two
|
|
5924
|
+
// expressions below reduce, bit for bit, to what they were before the cutoff
|
|
5925
|
+
// was a setting at all. See SplatQuality.quadAlphaCutoff.
|
|
5926
|
+
uniform float uAlphaCutoffScale;
|
|
5385
5927
|
// World rotation (xyzw) and scale of the splat object.
|
|
5386
5928
|
uniform vec4 uModelRotation;
|
|
5387
5929
|
uniform vec3 uModelScale;
|
|
@@ -5572,14 +6114,19 @@ void main(void) {
|
|
|
5572
6114
|
clr.rgb = clamp(clr.rgb, 0.0, 1.0);
|
|
5573
6115
|
}
|
|
5574
6116
|
|
|
5575
|
-
|
|
6117
|
+
// A splat whose peak alpha is already under the cutoff cannot put a
|
|
6118
|
+
// fragment on screen: its own quad would be empty.
|
|
6119
|
+
float alphaOverCutoff = clr.a * uAlphaCutoffScale;
|
|
6120
|
+
if (alphaOverCutoff <= 1.0) {
|
|
5576
6121
|
gl_Position = discardVec;
|
|
5577
6122
|
return;
|
|
5578
6123
|
}
|
|
5579
6124
|
|
|
5580
|
-
// Shrink the quad to where the gaussian is still above
|
|
5581
|
-
// fragments
|
|
5582
|
-
|
|
6125
|
+
// Shrink the quad to where the gaussian is still above the cutoff: fewer
|
|
6126
|
+
// fragments. At a cutoff of 1/255 the pixels are identical, since below
|
|
6127
|
+
// that frag() discards anyway; above it, this is the trade the setting
|
|
6128
|
+
// exists to make.
|
|
6129
|
+
float clip = min(1.0, sqrt(log(alphaOverCutoff)) * 0.5);
|
|
5583
6130
|
offset *= clip;
|
|
5584
6131
|
|
|
5585
6132
|
gl_Position = centerProj + vec4(offset, 0.0, 0.0);
|
|
@@ -5645,6 +6192,8 @@ var WebGLSplatImpl = class {
|
|
|
5645
6192
|
_lastDx = NaN;
|
|
5646
6193
|
_lastDy = NaN;
|
|
5647
6194
|
_lastDz = NaN;
|
|
6195
|
+
/** The importance ordering the worker already holds, if any. */
|
|
6196
|
+
_sentSubset = null;
|
|
5648
6197
|
/**
|
|
5649
6198
|
* @param asyncSort - Sort in a worker and keep drawing the previous order
|
|
5650
6199
|
* meanwhile. Right for large static splats; wrong for skinned ones, whose
|
|
@@ -5674,6 +6223,7 @@ var WebGLSplatImpl = class {
|
|
|
5674
6223
|
uTexWidth: { value: splat.packed.width },
|
|
5675
6224
|
uViewport: { value: new Vector2(1, 1) },
|
|
5676
6225
|
uMinPixelSize: { value: 0 },
|
|
6226
|
+
uAlphaCutoffScale: { value: 255 },
|
|
5677
6227
|
uModelRotation: { value: new Vector4(0, 0, 0, 1) },
|
|
5678
6228
|
uModelScale: { value: new Vector32(1, 1, 1) },
|
|
5679
6229
|
uTint: { value: splat.tint },
|
|
@@ -5712,10 +6262,10 @@ var WebGLSplatImpl = class {
|
|
|
5712
6262
|
if (asyncSort) {
|
|
5713
6263
|
this._syncSorter = null;
|
|
5714
6264
|
geometry.instanceCount = 0;
|
|
5715
|
-
this._asyncSorter = new AsyncDepthSorter(splat.centers, splat.numSplats, (sorted) => {
|
|
6265
|
+
this._asyncSorter = new AsyncDepthSorter(splat.centers, splat.numSplats, (sorted, count) => {
|
|
5716
6266
|
order.array.set(sorted);
|
|
5717
6267
|
order.needsUpdate = true;
|
|
5718
|
-
geometry.instanceCount =
|
|
6268
|
+
geometry.instanceCount = count;
|
|
5719
6269
|
});
|
|
5720
6270
|
} else {
|
|
5721
6271
|
this._asyncSorter = null;
|
|
@@ -5759,6 +6309,7 @@ var WebGLSplatImpl = class {
|
|
|
5759
6309
|
box.getBoundingSphere(this.geometry.boundingSphere);
|
|
5760
6310
|
}
|
|
5761
6311
|
beforeRender(renderer, camera) {
|
|
6312
|
+
const tEntered = SplatQuality.passTimings ? performance.now() : 0;
|
|
5762
6313
|
const gl = renderer;
|
|
5763
6314
|
const splat = this._splat;
|
|
5764
6315
|
const u = this.material.uniforms;
|
|
@@ -5770,16 +6321,31 @@ var WebGLSplatImpl = class {
|
|
|
5770
6321
|
this._host.matrixWorld.decompose(_position, _quaternion, _scale);
|
|
5771
6322
|
u.uModelRotation.value.set(_quaternion.x, _quaternion.y, _quaternion.z, _quaternion.w);
|
|
5772
6323
|
u.uModelScale.value.copy(_scale);
|
|
5773
|
-
u.uMinPixelSize.value = splat.minPixelSize;
|
|
6324
|
+
u.uMinPixelSize.value = effectiveMinPixelSize(splat.minPixelSize);
|
|
6325
|
+
u.uAlphaCutoffScale.value = 1 / Math.max(1 / 65535, SplatQuality.quadAlphaCutoff);
|
|
5774
6326
|
u.transformPalette.value = splat.transformPalette.texture;
|
|
5775
6327
|
u.splatGroupIds.value = splat.groupIdsTexture ?? null;
|
|
5776
6328
|
u.splatVariantMask.value = splat.variantMaskTexture ?? null;
|
|
6329
|
+
if (this.material.depthTest !== SplatQuality.depthOcclusion) {
|
|
6330
|
+
this.material.depthTest = SplatQuality.depthOcclusion;
|
|
6331
|
+
}
|
|
5777
6332
|
this._engine.syncLighting();
|
|
6333
|
+
if (!SplatQuality.passTimings) {
|
|
6334
|
+
this._sortFor(camera);
|
|
6335
|
+
return;
|
|
6336
|
+
}
|
|
6337
|
+
beginFrameTimings(this);
|
|
6338
|
+
const t = SplatQuality.timings;
|
|
6339
|
+
const tPrepared = performance.now();
|
|
6340
|
+
t.prepare += tPrepared - tEntered;
|
|
5778
6341
|
this._sortFor(camera);
|
|
6342
|
+
t.sort += performance.now() - tPrepared;
|
|
6343
|
+
t.splatsDrawn += this.geometry.instanceCount;
|
|
6344
|
+
t.splatsTotal += splat.numSplats;
|
|
5779
6345
|
}
|
|
5780
6346
|
_sortFor(camera) {
|
|
5781
6347
|
const splat = this._splat;
|
|
5782
|
-
const count = splat.
|
|
6348
|
+
const count = splat.activeSplatCount;
|
|
5783
6349
|
if (count === 0) return;
|
|
5784
6350
|
splat.ensureCenters();
|
|
5785
6351
|
const ce = camera.matrixWorld.elements;
|
|
@@ -5804,11 +6370,27 @@ var WebGLSplatImpl = class {
|
|
|
5804
6370
|
dy,
|
|
5805
6371
|
dz
|
|
5806
6372
|
);
|
|
6373
|
+
const subset = splat.countLodActive ? splat.importanceOrder : null;
|
|
5807
6374
|
if (this._asyncSorter) {
|
|
5808
|
-
|
|
6375
|
+
if (subset && subset !== this._sentSubset) {
|
|
6376
|
+
this._sentSubset = subset;
|
|
6377
|
+
this._asyncSorter.setSubset(subset);
|
|
6378
|
+
}
|
|
6379
|
+
this._asyncSorter.request({ dx, dy, dz, depthMin, depthMax, count });
|
|
5809
6380
|
} else {
|
|
5810
|
-
this._syncSorter.sort(
|
|
6381
|
+
this._syncSorter.sort(
|
|
6382
|
+
splat.centers,
|
|
6383
|
+
count,
|
|
6384
|
+
dx,
|
|
6385
|
+
dy,
|
|
6386
|
+
dz,
|
|
6387
|
+
depthMin,
|
|
6388
|
+
depthMax,
|
|
6389
|
+
this._order.array,
|
|
6390
|
+
subset
|
|
6391
|
+
);
|
|
5811
6392
|
this._order.needsUpdate = true;
|
|
6393
|
+
this.geometry.instanceCount = count;
|
|
5812
6394
|
}
|
|
5813
6395
|
}
|
|
5814
6396
|
dispose() {
|
|
@@ -6026,6 +6608,10 @@ var Splat = class _Splat {
|
|
|
6026
6608
|
localBounds = new Box3();
|
|
6027
6609
|
/** Discard splats smaller than this many pixels on screen. `0` draws everything. */
|
|
6028
6610
|
minPixelSize = 0;
|
|
6611
|
+
_activeSplatCount;
|
|
6612
|
+
_importanceOrder = null;
|
|
6613
|
+
/** `_importanceOrder` inverted: each splat's place in it. @internal */
|
|
6614
|
+
_importanceRank = null;
|
|
6029
6615
|
_mesh;
|
|
6030
6616
|
_cpuInfluences = null;
|
|
6031
6617
|
_pendingBoneTransforms = null;
|
|
@@ -6069,6 +6655,7 @@ var Splat = class _Splat {
|
|
|
6069
6655
|
const size = this.localBounds.getSize(_size);
|
|
6070
6656
|
this.localBounds.expandByScalar(0.5 * Math.max(size.x, size.y, size.z));
|
|
6071
6657
|
}
|
|
6658
|
+
this._activeSplatCount = this.numSplats;
|
|
6072
6659
|
const asyncSort = opts?.asyncSort ?? !this.splatWeights;
|
|
6073
6660
|
this._mesh = new SplatMesh(this, engine, asyncSort);
|
|
6074
6661
|
this.object3D = this._mesh;
|
|
@@ -6103,10 +6690,101 @@ var Splat = class _Splat {
|
|
|
6103
6690
|
}
|
|
6104
6691
|
return { indices, weights };
|
|
6105
6692
|
}
|
|
6693
|
+
/**
|
|
6694
|
+
* How many splats are skinned, sorted and drawn this frame: the whole set,
|
|
6695
|
+
* or the first `n` of {@link Splat.importanceOrder} after
|
|
6696
|
+
* {@link Splat.setCountLod}.
|
|
6697
|
+
*/
|
|
6698
|
+
get activeSplatCount() {
|
|
6699
|
+
return this._activeSplatCount;
|
|
6700
|
+
}
|
|
6701
|
+
/** Whether a count LOD is currently reducing this splat. */
|
|
6702
|
+
get countLodActive() {
|
|
6703
|
+
return this._activeSplatCount < this.numSplats;
|
|
6704
|
+
}
|
|
6705
|
+
/**
|
|
6706
|
+
* Process only the `n` most important splats this frame, or the whole set
|
|
6707
|
+
* with `null`.
|
|
6708
|
+
*
|
|
6709
|
+
* The dynamic counterpart of {@link SplatQuality.pruneRatio}: the prune
|
|
6710
|
+
* decides once, at load, that some splats are not worth carrying at all,
|
|
6711
|
+
* while this decides per frame how many of them are worth the work right
|
|
6712
|
+
* now - which is the trade a character makes as it walks away from the
|
|
6713
|
+
* camera. Nothing is freed, so it reverses the moment `n` goes back up.
|
|
6714
|
+
*
|
|
6715
|
+
* Which splats the first `n` are is {@link Splat.importanceOrder}'s answer:
|
|
6716
|
+
* the strongest splat of every 4 cm cell, then the second of every cell, and
|
|
6717
|
+
* so on, so the character thins evenly rather than losing a region.
|
|
6718
|
+
*
|
|
6719
|
+
* `WebGLRenderer` only, where it is worth the most: that backend skins every
|
|
6720
|
+
* splat centre on the CPU each frame to feed its sort, and this cuts that
|
|
6721
|
+
* pass, the sort and the draw together. Under `WebGPURenderer` all of that
|
|
6722
|
+
* already happens in compute, and the call is accepted and ignored rather
|
|
6723
|
+
* than silently changing which splats a GPU sort can see.
|
|
6724
|
+
*
|
|
6725
|
+
* {@link Character.setSplatCountLod} drives this from camera distance; call
|
|
6726
|
+
* it directly for a policy of your own.
|
|
6727
|
+
*/
|
|
6728
|
+
setCountLod(n) {
|
|
6729
|
+
const next = n === null ? this.numSplats : Math.max(1, Math.min(this.numSplats, Math.floor(n)));
|
|
6730
|
+
if (next === this._activeSplatCount) return;
|
|
6731
|
+
if (next < this.numSplats) this.prepareCountLod();
|
|
6732
|
+
this._activeSplatCount = next;
|
|
6733
|
+
this.centersVersion++;
|
|
6734
|
+
this._mesh.requestSort();
|
|
6735
|
+
}
|
|
6736
|
+
/**
|
|
6737
|
+
* Build {@link Splat.importanceOrder} now, if it does not exist yet.
|
|
6738
|
+
*
|
|
6739
|
+
* One pass over the splats plus a small sort inside each 4 cm cell - single
|
|
6740
|
+
* digit milliseconds for a character, but milliseconds that would otherwise
|
|
6741
|
+
* land on the frame where the LOD first engages, which is a frame where the
|
|
6742
|
+
* camera is already moving. {@link Character.setSplatCountLod} calls this
|
|
6743
|
+
* when the LOD is configured, so the cost lands there instead.
|
|
6744
|
+
*/
|
|
6745
|
+
prepareCountLod() {
|
|
6746
|
+
if (this._importanceRank) return;
|
|
6747
|
+
const order = this._importanceOrder ??= buildImportanceOrder(this.packed, SplatQuality.pruneVoxelSize);
|
|
6748
|
+
const rank = new Uint32Array(order.length);
|
|
6749
|
+
for (let i = 0; i < order.length; i++) rank[order[i]] = i;
|
|
6750
|
+
this._importanceRank = rank;
|
|
6751
|
+
}
|
|
6752
|
+
/**
|
|
6753
|
+
* Each splat's place in {@link Splat.importanceOrder}, which is how the
|
|
6754
|
+
* per-frame passes test membership of the active prefix. `null` until
|
|
6755
|
+
* {@link Splat.prepareCountLod}.
|
|
6756
|
+
* @internal
|
|
6757
|
+
*/
|
|
6758
|
+
get importanceRank() {
|
|
6759
|
+
return this._importanceRank;
|
|
6760
|
+
}
|
|
6761
|
+
/**
|
|
6762
|
+
* Splat indices ordered by importance for {@link Splat.setCountLod}. `null`
|
|
6763
|
+
* until {@link Splat.prepareCountLod} or a count LOD has asked for one.
|
|
6764
|
+
*/
|
|
6765
|
+
get importanceOrder() {
|
|
6766
|
+
return this._importanceOrder;
|
|
6767
|
+
}
|
|
6106
6768
|
/** Re-upload the bone index/weight textures after editing their data arrays. */
|
|
6107
6769
|
markBoneTexturesDirty() {
|
|
6108
6770
|
this._mesh.syncBoneData();
|
|
6109
6771
|
}
|
|
6772
|
+
/** Keep CPU-skinned centres (sorting and ground probes) on the same bone
|
|
6773
|
+
* mapping as the GPU shader. Called once when an armature binds this splat. */
|
|
6774
|
+
syncMappedCpuInfluences(firstPaletteIdx) {
|
|
6775
|
+
const packed = this._cpuInfluences;
|
|
6776
|
+
const indices = this.boneIndicesData;
|
|
6777
|
+
const weights = this.boneWeightsData;
|
|
6778
|
+
if (!packed || !indices || !weights) return;
|
|
6779
|
+
for (let s = 0; s < this.numSplats; s++) {
|
|
6780
|
+
const base = s * 11;
|
|
6781
|
+
const src = s * 4;
|
|
6782
|
+
for (let j = 0; j < 4; j++) {
|
|
6783
|
+
packed[base + 3 + j * 2] = weights[src + j];
|
|
6784
|
+
packed[base + 4 + j * 2] = indices[src + j] - firstPaletteIdx;
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6110
6788
|
get variantMaskEnabled() {
|
|
6111
6789
|
return this._variantMaskEnabled;
|
|
6112
6790
|
}
|
|
@@ -6152,11 +6830,16 @@ var Splat = class _Splat {
|
|
|
6152
6830
|
/**
|
|
6153
6831
|
* Assign each splat to a group and enable per-group visibility. All groups
|
|
6154
6832
|
* start visible; toggle them with {@link Splat.setVariantMask}.
|
|
6833
|
+
*
|
|
6834
|
+
* `groupIds` is indexed by splat, so it has to be built against
|
|
6835
|
+
* {@link Splat.numSplats} as loaded - which {@link SplatQuality.minOpacity}
|
|
6836
|
+
* makes smaller than the count in the file. An array authored offline
|
|
6837
|
+
* against the file cannot simply be shortened: see the error this throws.
|
|
6155
6838
|
*/
|
|
6156
6839
|
setSplatGroups(groupIds, numGroups) {
|
|
6157
6840
|
if (groupIds.length !== this.numSplats) {
|
|
6158
6841
|
throw new Error(
|
|
6159
|
-
`Splat.setSplatGroups: groupIds
|
|
6842
|
+
`Splat.setSplatGroups: groupIds has ${groupIds.length} entries but this splat has ${this.numSplats}. One id per loaded splat is required. If the array was authored against the source file, do not truncate it - its indices no longer line up, because SplatQuality.minOpacity drops splats during the decode. Build it from the loaded splats, or set SplatQuality.minOpacity = 0 before loading this character.`
|
|
6160
6843
|
);
|
|
6161
6844
|
}
|
|
6162
6845
|
if (numGroups < 1) throw new Error("Splat.setSplatGroups: numGroups must be \u22651");
|
|
@@ -6217,21 +6900,22 @@ var Splat = class _Splat {
|
|
|
6217
6900
|
if (n === 0 || !this.splatWeights) return this.getWorldMinY();
|
|
6218
6901
|
this.ensureCenters();
|
|
6219
6902
|
const centers = this.centers;
|
|
6903
|
+
this.object3D.updateWorldMatrix(true, false);
|
|
6904
|
+
const m = this.object3D.matrixWorld.elements;
|
|
6905
|
+
const worldY = (i) => m[1] * centers[i * 3] + m[5] * centers[i * 3 + 1] + m[9] * centers[i * 3 + 2] + m[13];
|
|
6220
6906
|
let lo = Infinity, hi = -Infinity;
|
|
6221
6907
|
for (let i = 0; i < n; i++) {
|
|
6222
|
-
const y =
|
|
6908
|
+
const y = worldY(i);
|
|
6223
6909
|
if (y < lo) lo = y;
|
|
6224
6910
|
if (y > hi) hi = y;
|
|
6225
6911
|
}
|
|
6226
|
-
|
|
6227
|
-
const worldOffsetY = this.object3D.matrixWorld.elements[13];
|
|
6228
|
-
if (!(hi > lo)) return lo + worldOffsetY;
|
|
6912
|
+
if (!(hi > lo)) return lo;
|
|
6229
6913
|
const BUCKETS = 256;
|
|
6230
6914
|
const counts = this._groundHist;
|
|
6231
6915
|
counts.fill(0);
|
|
6232
6916
|
const scale = BUCKETS / (hi - lo);
|
|
6233
6917
|
for (let i = 0; i < n; i++) {
|
|
6234
|
-
const y =
|
|
6918
|
+
const y = worldY(i);
|
|
6235
6919
|
let b = (y - lo) * scale | 0;
|
|
6236
6920
|
if (b >= BUCKETS) b = BUCKETS - 1;
|
|
6237
6921
|
if (b < 0) b = 0;
|
|
@@ -6247,7 +6931,7 @@ var Splat = class _Splat {
|
|
|
6247
6931
|
break;
|
|
6248
6932
|
}
|
|
6249
6933
|
}
|
|
6250
|
-
return boundary
|
|
6934
|
+
return boundary;
|
|
6251
6935
|
}
|
|
6252
6936
|
/**
|
|
6253
6937
|
* Flatten rest centres and influences into one interleaved buffer for the
|
|
@@ -6286,7 +6970,13 @@ var Splat = class _Splat {
|
|
|
6286
6970
|
const boneTransforms = this._pendingBoneTransforms;
|
|
6287
6971
|
if (!boneTransforms) return;
|
|
6288
6972
|
this._pendingBoneTransforms = null;
|
|
6973
|
+
if (!SplatQuality.passTimings) {
|
|
6974
|
+
this._skinCenters(boneTransforms);
|
|
6975
|
+
return;
|
|
6976
|
+
}
|
|
6977
|
+
const t0 = performance.now();
|
|
6289
6978
|
this._skinCenters(boneTransforms);
|
|
6979
|
+
addSkinTime(performance.now() - t0);
|
|
6290
6980
|
}
|
|
6291
6981
|
/**
|
|
6292
6982
|
* Skin the splat centres on the CPU with the same matrices the GPU uses.
|
|
@@ -6318,9 +7008,12 @@ var Splat = class _Splat {
|
|
|
6318
7008
|
}
|
|
6319
7009
|
const centers = this.centers;
|
|
6320
7010
|
const count = this.numSplats;
|
|
7011
|
+
const active = this._activeSplatCount;
|
|
7012
|
+
const rank = active < count ? this._importanceRank : null;
|
|
6321
7013
|
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
6322
7014
|
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
6323
7015
|
for (let i = 0, base = 0, ci = 0; i < count; i++, base += 11, ci += 3) {
|
|
7016
|
+
if (rank !== null && rank[i] >= active) continue;
|
|
6324
7017
|
const rx = packed[base];
|
|
6325
7018
|
const ry = packed[base + 1];
|
|
6326
7019
|
const rz = packed[base + 2];
|
|
@@ -6428,6 +7121,12 @@ var Character = class _Character {
|
|
|
6428
7121
|
get splat() {
|
|
6429
7122
|
return this.splats[this._activeLevel];
|
|
6430
7123
|
}
|
|
7124
|
+
// Splat-count LOD configuration (see setSplatCountLod). Off by default.
|
|
7125
|
+
_countLodEnabled = false;
|
|
7126
|
+
_countLodNearDist = 0;
|
|
7127
|
+
_countLodFarDist = Infinity;
|
|
7128
|
+
_countLodNearFraction = 1;
|
|
7129
|
+
_countLodFarFraction = 0.3;
|
|
6431
7130
|
// Render LOD configuration.
|
|
6432
7131
|
_renderLodEnabled = false;
|
|
6433
7132
|
_renderLodNearDist = Infinity;
|
|
@@ -6653,7 +7352,9 @@ var Character = class _Character {
|
|
|
6653
7352
|
const boneNames = use441 ? library.boneNames441 : library.boneNames;
|
|
6654
7353
|
const restSource = opts?.restSource ?? "auto";
|
|
6655
7354
|
const wantsBundled = restSource === "auto" ? primary.armatureData?.corrected === true : restSource === "bundled";
|
|
6656
|
-
const
|
|
7355
|
+
const bodyRestCount = builtinRest.length;
|
|
7356
|
+
const expressionOnBody = binBoneCount === 119 && !use441 && bodyRestCount === 86 * 3;
|
|
7357
|
+
const canBundle = wantsBundled && bundledJoints instanceof Float32Array && (bundledJoints.length === bodyRestCount || expressionOnBody && bundledJoints.length === 119 * 3);
|
|
6657
7358
|
let restTranslations = builtinRest;
|
|
6658
7359
|
if (canBundle) {
|
|
6659
7360
|
const joints = bundledJoints;
|
|
@@ -6709,6 +7410,7 @@ var Character = class _Character {
|
|
|
6709
7410
|
this._activeLevel = next;
|
|
6710
7411
|
}
|
|
6711
7412
|
}
|
|
7413
|
+
if (this._countLodEnabled) this._applyCountLod();
|
|
6712
7414
|
this.armature.update(dt);
|
|
6713
7415
|
};
|
|
6714
7416
|
this._engine.addUpdater(this._updateHandler);
|
|
@@ -6774,6 +7476,60 @@ var Character = class _Character {
|
|
|
6774
7476
|
this.splats[clamped].setRenderingEnabled(true);
|
|
6775
7477
|
this._activeLevel = clamped;
|
|
6776
7478
|
}
|
|
7479
|
+
/**
|
|
7480
|
+
* Thin the character with distance by processing fewer of its splats, rather
|
|
7481
|
+
* than by switching to a different file.
|
|
7482
|
+
*
|
|
7483
|
+
* At `nearDist` and closer, `nearFraction` of the splats are skinned, sorted
|
|
7484
|
+
* and drawn; at `farDist` and beyond, `farFraction` of them; in between, the
|
|
7485
|
+
* fraction is interpolated. Which splats those are is decided by importance
|
|
7486
|
+
* inside each 4 cm cell (see {@link Splat.setCountLod}), so the character
|
|
7487
|
+
* thins evenly instead of losing a region.
|
|
7488
|
+
*
|
|
7489
|
+
* The dynamic counterpart of {@link SplatQuality.pruneRatio}, and the one to
|
|
7490
|
+
* reach for when the splats are still wanted close up. It composes with
|
|
7491
|
+
* everything else: {@link Character.setRenderLod} switches between loaded
|
|
7492
|
+
* files, {@link Armature.setAnimationLod} and {@link Armature.setSortLod}
|
|
7493
|
+
* drop frames of pose and sort work, and this one changes how much of the
|
|
7494
|
+
* character each of those frames covers.
|
|
7495
|
+
*
|
|
7496
|
+
* `WebGLRenderer` only - see {@link Splat.setCountLod} for why.
|
|
7497
|
+
*
|
|
7498
|
+
* ```ts
|
|
7499
|
+
* character.setSplatCountLod({ nearDist: 4, farDist: 16, farFraction: 0.25 });
|
|
7500
|
+
* ```
|
|
7501
|
+
*/
|
|
7502
|
+
setSplatCountLod(opts) {
|
|
7503
|
+
this._countLodNearDist = Math.max(0, opts.nearDist);
|
|
7504
|
+
this._countLodFarDist = Math.max(this._countLodNearDist + 1e-6, opts.farDist);
|
|
7505
|
+
this._countLodNearFraction = Math.min(1, Math.max(0.01, opts.nearFraction ?? 1));
|
|
7506
|
+
this._countLodFarFraction = Math.min(1, Math.max(0.01, opts.farFraction ?? 0.3));
|
|
7507
|
+
this._countLodEnabled = true;
|
|
7508
|
+
for (const splat of this.splats) splat.prepareCountLod();
|
|
7509
|
+
}
|
|
7510
|
+
/** Stop thinning: every splat is processed again, on every level. */
|
|
7511
|
+
disableSplatCountLod() {
|
|
7512
|
+
if (!this._countLodEnabled) return;
|
|
7513
|
+
this._countLodEnabled = false;
|
|
7514
|
+
for (const splat of this.splats) splat.setCountLod(null);
|
|
7515
|
+
}
|
|
7516
|
+
/** Pick this frame's splat fraction from camera distance and apply it. */
|
|
7517
|
+
_applyCountLod() {
|
|
7518
|
+
const distSq = this.armature.cameraDistanceSq();
|
|
7519
|
+
if (distSq === null) return;
|
|
7520
|
+
const near = this._countLodNearDist;
|
|
7521
|
+
const far = this._countLodFarDist;
|
|
7522
|
+
const nearF = this._countLodNearFraction;
|
|
7523
|
+
const farF = this._countLodFarFraction;
|
|
7524
|
+
const dist = Math.sqrt(distSq);
|
|
7525
|
+
let fraction;
|
|
7526
|
+
if (dist <= near) fraction = nearF;
|
|
7527
|
+
else if (dist >= far) fraction = farF;
|
|
7528
|
+
else fraction = nearF + (farF - nearF) * (dist - near) / (far - near);
|
|
7529
|
+
const steps = Math.max(1, Math.round(fraction * 64));
|
|
7530
|
+
const splat = this.splat;
|
|
7531
|
+
splat.setCountLod(Math.round(steps / 64 * splat.numSplats));
|
|
7532
|
+
}
|
|
6777
7533
|
/** Number of LOD levels loaded for this character (>=1). */
|
|
6778
7534
|
get renderLodLevels() {
|
|
6779
7535
|
return this.splats.length;
|
|
@@ -7172,6 +7928,8 @@ var _euler = new Euler();
|
|
|
7172
7928
|
// src/three/splat-character.ts
|
|
7173
7929
|
var _color = new Color();
|
|
7174
7930
|
var _rgb = { r: 0, g: 0, b: 0 };
|
|
7931
|
+
var _groundDelta = new Vector35();
|
|
7932
|
+
var _parentInverseLinear = new Matrix3();
|
|
7175
7933
|
var _clipSerial = 0;
|
|
7176
7934
|
var SplatCharacter = class _SplatCharacter extends Object3D {
|
|
7177
7935
|
/** Type flag, in the three.js manner. */
|
|
@@ -7189,6 +7947,10 @@ var SplatCharacter = class _SplatCharacter extends Object3D {
|
|
|
7189
7947
|
_loop = true;
|
|
7190
7948
|
_finishedSent = false;
|
|
7191
7949
|
_rootMotion = "off";
|
|
7950
|
+
/** Optional world-space floor height. When set, the lowest visible splats
|
|
7951
|
+
* are aligned to it after each pose. Useful for dances with no airborne
|
|
7952
|
+
* phase; leave null for clips with intentional jumps or flight. */
|
|
7953
|
+
groundLockY = null;
|
|
7192
7954
|
_disposed = false;
|
|
7193
7955
|
/**
|
|
7194
7956
|
* @param bundles - Decoded character bundles, finest level of detail first.
|
|
@@ -7226,6 +7988,18 @@ var SplatCharacter = class _SplatCharacter extends Object3D {
|
|
|
7226
7988
|
const armature = this.character.armature;
|
|
7227
7989
|
const before = armature.animationTime;
|
|
7228
7990
|
this.character.update(delta);
|
|
7991
|
+
if (this.groundLockY !== null) {
|
|
7992
|
+
const low = this.character.getLowestWorldY();
|
|
7993
|
+
if (low !== null && Number.isFinite(low)) {
|
|
7994
|
+
_groundDelta.set(0, this.groundLockY - low, 0);
|
|
7995
|
+
if (this.parent) {
|
|
7996
|
+
this.parent.updateWorldMatrix(true, false);
|
|
7997
|
+
_parentInverseLinear.setFromMatrix4(this.parent.matrixWorld).invert();
|
|
7998
|
+
_groundDelta.applyMatrix3(_parentInverseLinear);
|
|
7999
|
+
}
|
|
8000
|
+
this.position.add(_groundDelta);
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
7229
8003
|
const clip = this._clip;
|
|
7230
8004
|
if (!clip || !armature.isPlaying) return;
|
|
7231
8005
|
if (this._loop) {
|
|
@@ -7358,6 +8132,32 @@ var SplatCharacter = class _SplatCharacter extends Object3D {
|
|
|
7358
8132
|
for (const splat of this.character.splats) splat.setGrade(contrast, saturation);
|
|
7359
8133
|
return this;
|
|
7360
8134
|
}
|
|
8135
|
+
// ── Performance ──────────────────────────────────────────────────────────
|
|
8136
|
+
/**
|
|
8137
|
+
* Draw fewer of this character's splats as it gets further away.
|
|
8138
|
+
*
|
|
8139
|
+
* `nearFraction` of them at `nearDist` and closer, `farFraction` at
|
|
8140
|
+
* `farDist` and beyond, interpolated in between. Which splats those are is
|
|
8141
|
+
* decided by importance within each 4 cm cell, so the character thins evenly
|
|
8142
|
+
* rather than losing a region of itself. See {@link Character.setSplatCountLod}.
|
|
8143
|
+
*
|
|
8144
|
+
* Needs a camera on the engine to measure distance from - the same one the
|
|
8145
|
+
* other distance throttles use:
|
|
8146
|
+
*
|
|
8147
|
+
* ```ts
|
|
8148
|
+
* SplatEngine.shared.camera = camera;
|
|
8149
|
+
* character.setSplatCountLod({ nearDist: 4, farDist: 16, farFraction: 0.25 });
|
|
8150
|
+
* ```
|
|
8151
|
+
*/
|
|
8152
|
+
setSplatCountLod(opts) {
|
|
8153
|
+
this.character.setSplatCountLod(opts);
|
|
8154
|
+
return this;
|
|
8155
|
+
}
|
|
8156
|
+
/** Stop thinning: every splat is drawn again, at any distance. */
|
|
8157
|
+
disableSplatCountLod() {
|
|
8158
|
+
this.character.disableSplatCountLod();
|
|
8159
|
+
return this;
|
|
8160
|
+
}
|
|
7361
8161
|
// ── Rig ──────────────────────────────────────────────────────────────────
|
|
7362
8162
|
/** Bone names, in rig order. */
|
|
7363
8163
|
get boneNames() {
|
|
@@ -7391,6 +8191,7 @@ var SplatCharacter = class _SplatCharacter extends Object3D {
|
|
|
7391
8191
|
copy.layers.mask = this.layers.mask;
|
|
7392
8192
|
copy.userData = { ...this.userData };
|
|
7393
8193
|
copy.rootMotion = this._rootMotion;
|
|
8194
|
+
copy.groundLockY = this.groundLockY;
|
|
7394
8195
|
return copy;
|
|
7395
8196
|
}
|
|
7396
8197
|
/**
|
|
@@ -7778,6 +8579,10 @@ export {
|
|
|
7778
8579
|
SplatEngine,
|
|
7779
8580
|
EngineDisposedError,
|
|
7780
8581
|
packSplats,
|
|
8582
|
+
SplatQuality,
|
|
8583
|
+
effectiveMinPixelSize,
|
|
8584
|
+
beginFrameTimings,
|
|
8585
|
+
pruneSplats,
|
|
7781
8586
|
CharacterBundleError,
|
|
7782
8587
|
decodeCharacterBundle,
|
|
7783
8588
|
loadCharacterBundle,
|
|
@@ -7798,4 +8603,4 @@ export {
|
|
|
7798
8603
|
SkyLight,
|
|
7799
8604
|
PointLight
|
|
7800
8605
|
};
|
|
7801
|
-
//# sourceMappingURL=chunk-
|
|
8606
|
+
//# sourceMappingURL=chunk-WN5WBKM4.js.map
|