gaussian-splat-lite 1.4.1 → 1.4.2
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/CHANGELOG.md +9 -1
- package/dist/gaussian-splat-lite.cjs +44 -129
- package/dist/gaussian-splat-lite.cjs.map +1 -1
- package/dist/gaussian-splat-lite.min.cjs +62 -150
- package/dist/gaussian-splat-lite.min.cjs.map +1 -1
- package/dist/gaussian-splat-lite.module.js +44 -129
- package/dist/gaussian-splat-lite.module.js.map +1 -1
- package/dist/gaussian-splat-lite.module.min.js +497 -590
- package/dist/gaussian-splat-lite.module.min.js.map +1 -1
- package/dist/types/addons/neuralDenoiseWeights.d.cts +3 -21
- package/dist/types/addons/neuralDenoiseWeights.d.ts +3 -21
- package/dist/types/rendering/GaussianSplatRenderer.d.cts +1 -1
- package/dist/types/rendering/GaussianSplatRenderer.d.ts +1 -1
- package/dist/types/rendering/SplatGeometry.d.cts +2 -2
- package/dist/types/rendering/SplatGeometry.d.ts +2 -2
- package/package.json +1 -1
|
@@ -3895,19 +3895,22 @@ function hasViewDependentColor(mapping) {
|
|
|
3895
3895
|
}
|
|
3896
3896
|
const SPLATS_PER_INSTANCE = 128;
|
|
3897
3897
|
class SplatGeometry extends THREE.InstancedBufferGeometry {
|
|
3898
|
-
constructor() {
|
|
3898
|
+
constructor(triangle = false) {
|
|
3899
3899
|
super();
|
|
3900
|
-
const
|
|
3901
|
-
const
|
|
3900
|
+
const corners = triangle ? TRIANGLE_CORNERS : QUAD_CORNERS;
|
|
3901
|
+
const order = triangle ? TRIANGLE_INDICES : QUAD_INDICES;
|
|
3902
|
+
const vertexCount = corners.length / 2;
|
|
3903
|
+
const vertices = new Float32Array(SPLATS_PER_INSTANCE * vertexCount * 3);
|
|
3904
|
+
const indices = new Uint16Array(SPLATS_PER_INSTANCE * order.length);
|
|
3902
3905
|
for (let splat = 0; splat < SPLATS_PER_INSTANCE; splat++) {
|
|
3903
|
-
for (let vertex = 0; vertex <
|
|
3904
|
-
const offset = splat *
|
|
3905
|
-
vertices[offset] =
|
|
3906
|
-
vertices[offset + 1] =
|
|
3906
|
+
for (let vertex = 0; vertex < vertexCount; vertex++) {
|
|
3907
|
+
const offset = (splat * vertexCount + vertex) * 3;
|
|
3908
|
+
vertices[offset] = corners[vertex * 2];
|
|
3909
|
+
vertices[offset + 1] = corners[vertex * 2 + 1];
|
|
3907
3910
|
vertices[offset + 2] = splat;
|
|
3908
3911
|
}
|
|
3909
|
-
for (let index2 = 0; index2 <
|
|
3910
|
-
indices[splat *
|
|
3912
|
+
for (let index2 = 0; index2 < order.length; index2++)
|
|
3913
|
+
indices[splat * order.length + index2] = splat * vertexCount + order[index2];
|
|
3911
3914
|
}
|
|
3912
3915
|
this.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
|
|
3913
3916
|
this.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
@@ -3916,21 +3919,17 @@ class SplatGeometry extends THREE.InstancedBufferGeometry {
|
|
|
3916
3919
|
this.instanceCount = Math.ceil(count / SPLATS_PER_INSTANCE);
|
|
3917
3920
|
}
|
|
3918
3921
|
}
|
|
3919
|
-
const
|
|
3922
|
+
const QUAD_CORNERS = new Float32Array([-1, -1, 1, -1, 1, 1, -1, 1]);
|
|
3923
|
+
const TRIANGLE_CORNERS = new Float32Array([
|
|
3924
|
+
-Math.sqrt(3),
|
|
3920
3925
|
-1,
|
|
3926
|
+
Math.sqrt(3),
|
|
3921
3927
|
-1,
|
|
3922
3928
|
0,
|
|
3923
|
-
|
|
3924
|
-
-1,
|
|
3925
|
-
0,
|
|
3926
|
-
1,
|
|
3927
|
-
1,
|
|
3928
|
-
0,
|
|
3929
|
-
-1,
|
|
3930
|
-
1,
|
|
3931
|
-
0
|
|
3929
|
+
2
|
|
3932
3930
|
]);
|
|
3933
3931
|
const QUAD_INDICES = new Uint16Array([0, 2, 1, 0, 3, 2]);
|
|
3932
|
+
const TRIANGLE_INDICES = new Uint16Array([0, 2, 1]);
|
|
3934
3933
|
const variants = /* @__PURE__ */ new WeakMap();
|
|
3935
3934
|
function registerStochasticSplatMaterial(material, createVariant) {
|
|
3936
3935
|
variants.set(material, createVariant);
|
|
@@ -5601,10 +5600,7 @@ class ProjectedSplats {
|
|
|
5601
5600
|
constructor(renderer, uniforms) {
|
|
5602
5601
|
this.renderer = renderer;
|
|
5603
5602
|
this.uniforms = uniforms;
|
|
5604
|
-
this.indirect = new IndirectStorageBufferAttribute(
|
|
5605
|
-
new Uint32Array([SPLATS_PER_INSTANCE * 6, 0, 0, 0, 0]),
|
|
5606
|
-
1
|
|
5607
|
-
);
|
|
5603
|
+
this.indirect = new IndirectStorageBufferAttribute(new Uint32Array(5), 1);
|
|
5608
5604
|
this.error = null;
|
|
5609
5605
|
this.cache = new ProjectionCache();
|
|
5610
5606
|
this.visibleCount = new StorageBufferAttribute(new Uint32Array(1), 1);
|
|
@@ -5644,7 +5640,9 @@ class ProjectedSplats {
|
|
|
5644
5640
|
// RADIX_SORT_MODE_IDS value of the current key encoding.
|
|
5645
5641
|
sortMode: { value: 0 },
|
|
5646
5642
|
// NDC translation the draw applies to unjittered projections.
|
|
5647
|
-
projectionJitter: { value: new THREE.Vector2() }
|
|
5643
|
+
projectionJitter: { value: new THREE.Vector2() },
|
|
5644
|
+
// Indices per instance of the drawn geometry, quads or triangles.
|
|
5645
|
+
indexCount: { value: 0 }
|
|
5648
5646
|
};
|
|
5649
5647
|
const count = N.storage(this.visibleCount, "uint").setName("gslVisibleCount").toReadOnly().element(0);
|
|
5650
5648
|
this.sorter = new WebGPURadixSort(1, this.keys, {
|
|
@@ -5656,13 +5654,16 @@ class ProjectedSplats {
|
|
|
5656
5654
|
this.limits.maxBufferSize
|
|
5657
5655
|
)
|
|
5658
5656
|
});
|
|
5659
|
-
const
|
|
5657
|
+
const drawArgs = N.storage(this.indirect, "uint").setName(
|
|
5658
|
+
"gslDrawIndirect"
|
|
5659
|
+
);
|
|
5660
5660
|
const counter = N.storage(this.visibleCount, "uint").setName("gslVisibleCount").toAtomic();
|
|
5661
5661
|
this.resetCount = N.Fn(() => {
|
|
5662
5662
|
N.atomicStore(counter.element(0), N.uint(0));
|
|
5663
5663
|
})().compute(1, [1]).setName("Splat reset visible count");
|
|
5664
5664
|
this.finish = N.Fn(() => {
|
|
5665
|
-
|
|
5665
|
+
drawArgs.element(0).assign(uniformBinding(this.state, "indexCount", "uint"));
|
|
5666
|
+
drawArgs.element(1).assign(
|
|
5666
5667
|
count.add(SPLATS_PER_INSTANCE - 1).div(N.uint(SPLATS_PER_INSTANCE))
|
|
5667
5668
|
);
|
|
5668
5669
|
})().compute(1, [1]).setName("Splat visible draw arguments");
|
|
@@ -5914,6 +5915,7 @@ class ProjectedSplats {
|
|
|
5914
5915
|
const monoProjection = !multiView && unjittered && getProjectionJitter(camera.projectionMatrix, unjittered, jitter) ? unjittered : null;
|
|
5915
5916
|
geometry.setIndirect(this.indirect);
|
|
5916
5917
|
geometry.setSplatCount(Math.max(1, accumulator.numSplats));
|
|
5918
|
+
this.state.indexCount.value = geometry.index.count;
|
|
5917
5919
|
const inputs = this.pendingInputs;
|
|
5918
5920
|
let length = 0;
|
|
5919
5921
|
inputs[length++] = accumulator;
|
|
@@ -5922,6 +5924,7 @@ class ProjectedSplats {
|
|
|
5922
5924
|
inputs[length++] = accumulator.viewOrigin.y;
|
|
5923
5925
|
inputs[length++] = accumulator.viewOrigin.z;
|
|
5924
5926
|
inputs[length++] = cameras.length;
|
|
5927
|
+
inputs[length++] = geometry.index.count;
|
|
5925
5928
|
inputs[length++] = stochastic;
|
|
5926
5929
|
inputs[length++] = !stochastic && radial;
|
|
5927
5930
|
inputs[length++] = sortMode;
|
|
@@ -6108,9 +6111,11 @@ class GaussianSplatRenderer extends THREE.Mesh {
|
|
|
6108
6111
|
depthTest: options.depthTest ?? true,
|
|
6109
6112
|
depthWrite: options.depthWrite ?? false
|
|
6110
6113
|
});
|
|
6111
|
-
const
|
|
6114
|
+
const geometries = [new SplatGeometry(), new SplatGeometry(true)];
|
|
6115
|
+
const geometry = geometries[Number(stochastic)];
|
|
6112
6116
|
const material = backend.selectMaterial(stochastic);
|
|
6113
6117
|
super(geometry, material);
|
|
6118
|
+
this.geometries = geometries;
|
|
6114
6119
|
this.materialState = new MaterialState(material);
|
|
6115
6120
|
this.frameCallbacks = true;
|
|
6116
6121
|
this.renderSize = new THREE.Vector2();
|
|
@@ -6214,7 +6219,7 @@ class GaussianSplatRenderer extends THREE.Mesh {
|
|
|
6214
6219
|
accumulator.dispose();
|
|
6215
6220
|
}
|
|
6216
6221
|
this.accumulators.length = 0;
|
|
6217
|
-
this.geometry.dispose();
|
|
6222
|
+
for (const geometry of this.geometries) geometry.dispose();
|
|
6218
6223
|
}
|
|
6219
6224
|
/** Native WebGPU sorts on the GPU before drawing; WebGL sorts in a worker. */
|
|
6220
6225
|
get synchronousSort() {
|
|
@@ -6266,13 +6271,11 @@ class GaussianSplatRenderer extends THREE.Mesh {
|
|
|
6266
6271
|
const active = this.backend.kind === "webgpu" || this.display.numSplats === 0 ? this._stochastic : this.display.hasStochasticSeeds;
|
|
6267
6272
|
if (active === this.stochasticFrame) return;
|
|
6268
6273
|
this.stochasticFrame = active;
|
|
6269
|
-
this.
|
|
6270
|
-
this.
|
|
6271
|
-
|
|
6272
|
-
selectMaterial() {
|
|
6273
|
-
const material = this.backend.selectMaterial(this.stochasticFrame);
|
|
6274
|
-
this.applyMaterialState(this.stochasticFrame, material);
|
|
6274
|
+
this.geometry = this.geometries[Number(active)];
|
|
6275
|
+
const material = this.backend.selectMaterial(active);
|
|
6276
|
+
this.applyMaterialState(active, material);
|
|
6275
6277
|
this.material = material;
|
|
6278
|
+
this.setDirty();
|
|
6276
6279
|
}
|
|
6277
6280
|
applyMaterialState(stochasticActive, material = this.material) {
|
|
6278
6281
|
this.materialState.sync(material, this.material);
|
|
@@ -7196,74 +7199,6 @@ ${CATMULL_ROM_WEIGHTS}
|
|
|
7196
7199
|
fn catmullRom(image: texture_2d<f32>, uv: vec2f) -> vec4f {
|
|
7197
7200
|
return catmullRomSpan(image, uv).value;
|
|
7198
7201
|
}
|
|
7199
|
-
|
|
7200
|
-
// Lanczos-3 without its corner taps, from nine bilinear taps: the middle pair
|
|
7201
|
-
// of each axis is one tap and the other four pixels of an axis one each. A
|
|
7202
|
-
// history that is resampled once per frame loses fine detail with every pass,
|
|
7203
|
-
// and this kernel loses about half of what Catmull-Rom does. Its negative
|
|
7204
|
-
// lobes would ring over such a history, so the result is kept inside what the
|
|
7205
|
-
// four pixels around the position span. At a pixel's centre it returns the
|
|
7206
|
-
// pixel.
|
|
7207
|
-
struct LanczosWeights {
|
|
7208
|
-
low: vec3f,
|
|
7209
|
-
high: vec3f,
|
|
7210
|
-
};
|
|
7211
|
-
fn lanczosWeights(t: f32) -> LanczosWeights {
|
|
7212
|
-
// Integer-shifted sinc taps share their sine. Angle addition supplies the
|
|
7213
|
-
// other factor, reducing twelve sine evaluations per axis to three trig calls.
|
|
7214
|
-
let x: f32 = 3.14159265 * t;
|
|
7215
|
-
let s: f32 = sin(x / 3.0);
|
|
7216
|
-
let c: f32 = 0.866025404 * cos(x / 3.0);
|
|
7217
|
-
let scale: f32 = 0.303963551 * sin(x);
|
|
7218
|
-
let terms: vec3f = scale * vec3f(c - 0.5 * s, -c - 0.5 * s, s);
|
|
7219
|
-
let low: vec3f = vec3f(t + 2.0, t + 1.0, t);
|
|
7220
|
-
let high: vec3f = vec3f(t - 1.0, t - 2.0, t - 3.0);
|
|
7221
|
-
var w: LanczosWeights;
|
|
7222
|
-
w.low = terms / max(low * low, vec3f(1e-20));
|
|
7223
|
-
w.high = terms / max(high * high, vec3f(1e-20));
|
|
7224
|
-
if (abs(x) < 1e-5) { w.low.z = 1.0; }
|
|
7225
|
-
if (abs(3.14159265 * (t - 1.0)) < 1e-5) { w.high.x = 1.0; }
|
|
7226
|
-
return w;
|
|
7227
|
-
}
|
|
7228
|
-
struct LanczosKernel {
|
|
7229
|
-
base: vec2f,
|
|
7230
|
-
centre: vec2f,
|
|
7231
|
-
middle: f32,
|
|
7232
|
-
wx: vec4f,
|
|
7233
|
-
wy: vec4f,
|
|
7234
|
-
total: f32,
|
|
7235
|
-
}
|
|
7236
|
-
fn lanczosKernel(uv: vec2f) -> LanczosKernel {
|
|
7237
|
-
let position: vec2f = uv * P.size - 0.5;
|
|
7238
|
-
var k: LanczosKernel;
|
|
7239
|
-
k.base = floor(position);
|
|
7240
|
-
let t: vec2f = position - k.base;
|
|
7241
|
-
let x: LanczosWeights = lanczosWeights(t.x);
|
|
7242
|
-
let y: LanczosWeights = lanczosWeights(t.y);
|
|
7243
|
-
let middle: vec2f = vec2f(x.low.z + x.high.x, y.low.z + y.high.x);
|
|
7244
|
-
k.centre = (k.base + vec2f(x.high.x, y.high.x) / middle + 0.5) * P.invSize;
|
|
7245
|
-
k.middle = middle.x * middle.y;
|
|
7246
|
-
k.wx = vec4f(x.low.xy, x.high.yz) * middle.y;
|
|
7247
|
-
k.wy = vec4f(y.low.xy, y.high.yz) * middle.x;
|
|
7248
|
-
k.total = k.middle;
|
|
7249
|
-
for (var i: i32 = 0; i < 4; i++) { k.total += k.wx[i] + k.wy[i]; }
|
|
7250
|
-
return k;
|
|
7251
|
-
}
|
|
7252
|
-
fn lanczosCross(image: texture_2d<f32>, k: LanczosKernel) -> vec4f {
|
|
7253
|
-
var sum: vec4f = textureSampleLevel(image, linearSampler, k.centre, 0.0) * k.middle;
|
|
7254
|
-
const offsets = array<f32, 4>(-1.5, -0.5, 2.5, 3.5);
|
|
7255
|
-
for (var i: i32 = 0; i < 4; i++) {
|
|
7256
|
-
let along: vec2f = (k.base + offsets[i]) * P.invSize;
|
|
7257
|
-
sum += textureSampleLevel(image, linearSampler, vec2f(along.x, k.centre.y), 0.0) * k.wx[i];
|
|
7258
|
-
sum += textureSampleLevel(image, linearSampler, vec2f(k.centre.x, along.y), 0.0) * k.wy[i];
|
|
7259
|
-
}
|
|
7260
|
-
let p: vec2i = vec2i(k.base);
|
|
7261
|
-
let a: vec4f = textureLoad(image, bounded(p), 0);
|
|
7262
|
-
let b: vec4f = textureLoad(image, bounded(p + vec2i(1, 0)), 0);
|
|
7263
|
-
let c: vec4f = textureLoad(image, bounded(p + vec2i(0, 1)), 0);
|
|
7264
|
-
let d: vec4f = textureLoad(image, bounded(p + vec2i(1, 1)), 0);
|
|
7265
|
-
return clamp(sum / k.total, min(min(a, b), min(c, d)), max(max(a, b), max(c, d)));
|
|
7266
|
-
}
|
|
7267
7202
|
`
|
|
7268
7203
|
);
|
|
7269
7204
|
const TAPS = (
|
|
@@ -7617,7 +7552,7 @@ fn mean3x3(image: texture_2d<f32>, position: vec2f, offset: vec2f) -> vec4f {
|
|
|
7617
7552
|
const QUALITY_RESAMPLING = (
|
|
7618
7553
|
/* wgsl */
|
|
7619
7554
|
`
|
|
7620
|
-
// Full Catmull-Rom for
|
|
7555
|
+
// Full Catmull-Rom for Quality histories, retaining all four corner taps.
|
|
7621
7556
|
fn catmullRomNine(image: texture_2d<f32>, uv: vec2f) -> vec4f {
|
|
7622
7557
|
let position: vec2f = uv * P.size;
|
|
7623
7558
|
${CATMULL_ROM_WEIGHTS}
|
|
@@ -7935,7 +7870,7 @@ ${groups.map(
|
|
|
7935
7870
|
// half-resolution result.
|
|
7936
7871
|
@group(0) @binding(11) var wideTexture: texture_2d<f32>;
|
|
7937
7872
|
@group(0) @binding(12) var depthPrevious: texture_2d<f32>;
|
|
7938
|
-
${
|
|
7873
|
+
${weights.compact ? MEAN_3X3 : QUALITY_RESAMPLING}${BLOCKS}
|
|
7939
7874
|
// The frame's existing 4x4 mean, used only for rejected surfaces.
|
|
7940
7875
|
@group(0) @binding(13) var rejectedTexture: texture_2d<f32>;
|
|
7941
7876
|
${COARSE}
|
|
@@ -7983,13 +7918,10 @@ ${weights.compact ? "" : " let d: vec4f = textureLoad(filteredTexture, p, 0);"}
|
|
|
7983
7918
|
var same: f32 = 1.0;
|
|
7984
7919
|
let sparse: f32 = blockSparse(p);
|
|
7985
7920
|
var sparseMotion: f32 = 0.0;
|
|
7986
|
-
${weights.movingMean ? ` // Where the history was, for the accumulated path's mean below.
|
|
7987
|
-
var historyUv: vec2f = position.xy * P.invSize;` : ""}
|
|
7988
7921
|
if (P.historyValid > 0.5) {
|
|
7989
7922
|
let z: f32 = blockDepth(p);
|
|
7990
7923
|
let h: History = reproject(position.xy, z);
|
|
7991
7924
|
valid = h.valid;
|
|
7992
|
-
${weights.movingMean ? " historyUv = h.uv;" : ""}
|
|
7993
7925
|
// The pixel's own motion, or what a turn of the camera sweeps over half
|
|
7994
7926
|
// the view, whichever is more: at the centre of a turn the first is
|
|
7995
7927
|
// small while every surface there shows a new face. An orthographic
|
|
@@ -8020,9 +7952,9 @@ ${weights.movingMean ? " historyUv = h.uv;" : ""}
|
|
|
8020
7952
|
${weights.compact ? ` // The accumulated history keeps many frames while the camera moves
|
|
8021
7953
|
// slowly: read bilinearly, it lost detail with every one of them.
|
|
8022
7954
|
sr = max(catmullRom(previousAccumulated, h.uv), vec4f(0.0));
|
|
8023
|
-
dr = max(textureSampleLevel(previousDenoised, linearSampler, h.uv, 0.0), vec4f(0.0));` : `
|
|
8024
|
-
//
|
|
8025
|
-
|
|
7955
|
+
dr = max(textureSampleLevel(previousDenoised, linearSampler, h.uv, 0.0), vec4f(0.0));` : ` // Full Catmull-Rom avoids the Lanczos cross kernel's frequency gain
|
|
7956
|
+
// when periodic surface detail is reprojected over many frames.
|
|
7957
|
+
sr = max(catmullRomNine(previousAccumulated, h.uv), vec4f(0.0));
|
|
8026
7958
|
dr = max(catmullRomNine(previousDenoised, h.uv), vec4f(0.0));`}
|
|
8027
7959
|
a = previousStatistics(h.uv);
|
|
8028
7960
|
}
|
|
@@ -8099,24 +8031,7 @@ ${weights.compact ? ` let source: vec4f = shifted;
|
|
|
8099
8031
|
let error: f32 = sqrt(max(y2 - ys * ys, 0.0) / (ns + 1e-3) + 1e-10);
|
|
8100
8032
|
let ramp: f32 = 1.0 - exp(-ns / ${float(weights.gateFrames)});
|
|
8101
8033
|
let keep: f32 = ramp * (1.0 - clamp(${float(weights.gamma)} * error, 0.0, 1.0)) * trust.w;
|
|
8102
|
-
|
|
8103
|
-
// new view and its grain shows: it is shown from the 3x3 mean of its
|
|
8104
|
-
// history and this frame's filtered sample. What it keeps stays sharp, so
|
|
8105
|
-
// a camera that stops settles as before. This display-only smoothing does
|
|
8106
|
-
// not track moving objects: with a still camera they continue to use the
|
|
8107
|
-
// existing history rejection.
|
|
8108
|
-
var shownAccumulated: vec4f = s;
|
|
8109
|
-
if (P.historyValid > 0.5 && P.cameraStill < 0.5) {
|
|
8110
|
-
let held: vec4f = max(mean3x3(previousAccumulated, historyUv * P.size, vec2f(0.0)), vec4f(0.0));
|
|
8111
|
-
let moving: f32 = speed * speed / (speed * speed + ${float(weights.movingKnee * weights.movingKnee)});
|
|
8112
|
-
let smoothed: vec4f = (held * ws + filtered) / ds;
|
|
8113
|
-
// Only where the two differ by what the mean's noise explains: a larger
|
|
8114
|
-
// difference is detail, and stays.
|
|
8115
|
-
let differ: f32 = luma(abs(smoothed.rgb - s.rgb));
|
|
8116
|
-
let spread: f32 = ${float(weights.movingNoise)} * error * error;
|
|
8117
|
-
shownAccumulated = mix(s, smoothed, ${float(weights.movingMean)} * moving * spread / (spread + differ * differ + 1e-10));
|
|
8118
|
-
}
|
|
8119
|
-
let composite: vec4f = mix(dn, shownAccumulated, keep);` : " let composite: vec4f = mix(dn, s, keep);"}
|
|
8034
|
+
let composite: vec4f = mix(dn, s, keep);
|
|
8120
8035
|
// Stabilization: a predicted blend with the previous image, which may stay
|
|
8121
8036
|
// as far from this frame's result as half of what it spans around the
|
|
8122
8037
|
// pixel, plus a slack. Taking that span from this frame's result instead
|
|
@@ -10954,7 +10869,7 @@ const quality = {
|
|
|
10954
10869
|
}
|
|
10955
10870
|
};
|
|
10956
10871
|
const neuralDenoiseModels = {
|
|
10957
|
-
quality
|
|
10872
|
+
quality,
|
|
10958
10873
|
performance: { ...quality, compact: true, compactMix: [0.5, 0.75] }
|
|
10959
10874
|
};
|
|
10960
10875
|
class StochasticCoverage {
|