poppygl 0.0.32 → 0.0.34
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 +6 -4
- package/dist/index.d.ts +13 -2
- package/dist/index.js +257 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,10 +86,12 @@ Realistic mode supports base-color textures and scalar glTF metallic/roughness
|
|
|
86
86
|
values. Normal maps, metallic/roughness textures, and custom HDR environments
|
|
87
87
|
are not yet supported. Transparent and alpha-cutout surfaces do not cast shadows.
|
|
88
88
|
|
|
89
|
-
Use realistic mode for saved images. At 600×600 with 2× supersampling,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
Use realistic mode for saved images. At 600×600 with 2× supersampling, the RP2040
|
|
90
|
+
motor-controller benchmark takes 2.26–5.49 seconds per warm render across three
|
|
91
|
+
views, a 2.11–2.76× speedup over the previous realistic renderer on the same
|
|
92
|
+
machine. Blender comparison error increases by at most 0.47%. See
|
|
93
|
+
[benchmark results and reproduction steps](BENCHMARK.md#rp2040-motor-controller-500-sample-blender-baseline-and-2-gate).
|
|
94
|
+
Render times vary with model complexity and hardware.
|
|
93
95
|
|
|
94
96
|
### Camera and background
|
|
95
97
|
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ interface Material {
|
|
|
15
15
|
baseColorTexture: BitmapLike | null;
|
|
16
16
|
metallicFactor?: number;
|
|
17
17
|
roughnessFactor?: number;
|
|
18
|
+
emissiveFactor?: [number, number, number];
|
|
19
|
+
emissiveTexture?: BitmapLike | null;
|
|
20
|
+
emissiveStrength?: number;
|
|
18
21
|
alphaMode?: "OPAQUE" | "MASK" | "BLEND";
|
|
19
22
|
alphaCutoff?: number;
|
|
20
23
|
}
|
|
@@ -178,9 +181,13 @@ declare class StudioLighting {
|
|
|
178
181
|
private eye;
|
|
179
182
|
private shadowDirections;
|
|
180
183
|
private diffuseCache;
|
|
184
|
+
private reflectionSamples;
|
|
185
|
+
private normalDiffuseCache;
|
|
186
|
+
private specularCache;
|
|
187
|
+
private blendedSpecular;
|
|
181
188
|
constructor(drawCalls: DrawCall[], camera: Camera);
|
|
182
189
|
private bounceRadiance;
|
|
183
|
-
shade(position: V3, n: V3, base: V3, material: Material,
|
|
190
|
+
shade(position: V3, n: V3, base: V3, material: Material, pixelX?: number, pixelY?: number): V3;
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
interface LightSettings {
|
|
@@ -209,7 +216,11 @@ declare class SoftwareRenderer {
|
|
|
209
216
|
drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean, options?: LineRenderOptions): void;
|
|
210
217
|
sampleTextureNearest(img: BitmapLike | null, u: number, v: number): MutableRGBA;
|
|
211
218
|
perspInterp(attrs: number[][], invWs: number[], lambdas: number[]): number[];
|
|
212
|
-
drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean, studio?: StudioLighting
|
|
219
|
+
drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean, studio?: StudioLighting, visibility?: {
|
|
220
|
+
winners: Int32Array;
|
|
221
|
+
triangleOffset: number;
|
|
222
|
+
depthOnly: boolean;
|
|
223
|
+
}): number;
|
|
213
224
|
}
|
|
214
225
|
|
|
215
226
|
/**
|
package/dist/index.js
CHANGED
|
@@ -598,14 +598,18 @@ var GeometryDenoiser = class {
|
|
|
598
598
|
this.albedo = new Float32Array(count * 3);
|
|
599
599
|
this.radiance = new Float32Array(count * 3);
|
|
600
600
|
this.distance = new Float32Array(count);
|
|
601
|
+
this.horizontal = new Float32Array(count * 3);
|
|
601
602
|
}
|
|
602
603
|
ids;
|
|
603
604
|
normals;
|
|
604
605
|
albedo;
|
|
605
606
|
radiance;
|
|
606
607
|
distance;
|
|
608
|
+
horizontal;
|
|
607
609
|
materials = /* @__PURE__ */ new WeakMap();
|
|
608
610
|
nextId = 1;
|
|
611
|
+
first = Infinity;
|
|
612
|
+
last = -1;
|
|
609
613
|
materialId(material) {
|
|
610
614
|
let id = this.materials.get(material);
|
|
611
615
|
if (!id) {
|
|
@@ -615,6 +619,8 @@ var GeometryDenoiser = class {
|
|
|
615
619
|
return id;
|
|
616
620
|
}
|
|
617
621
|
record(index, id, n, albedo, radiance, distance) {
|
|
622
|
+
if (index < this.first) this.first = index;
|
|
623
|
+
if (index > this.last) this.last = index;
|
|
618
624
|
this.ids[index] = id;
|
|
619
625
|
this.normals.set(n, index * 3);
|
|
620
626
|
this.albedo.set(albedo, index * 3);
|
|
@@ -622,40 +628,54 @@ var GeometryDenoiser = class {
|
|
|
622
628
|
this.distance[index] = distance;
|
|
623
629
|
}
|
|
624
630
|
apply(output, gamma) {
|
|
625
|
-
const kernel = [1,
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
631
|
+
const kernel = [1, 8, 28, 56, 70, 56, 28, 8, 1];
|
|
632
|
+
const radius = 4;
|
|
633
|
+
const n = this.normals, a = this.albedo, ids = this.ids;
|
|
634
|
+
const firstRow = Math.floor(this.first / this.width);
|
|
635
|
+
const lastRow = Math.floor(this.last / this.width);
|
|
636
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
637
|
+
const source = pass === 0 ? this.radiance : this.horizontal;
|
|
638
|
+
for (let y = firstRow; y <= lastRow; y++)
|
|
639
|
+
for (let x = 0; x < this.width; x++) {
|
|
640
|
+
const index = y * this.width + x, id = ids[index];
|
|
641
|
+
if (id === 0) continue;
|
|
642
|
+
const k = index * 3, distance = this.distance[index];
|
|
643
|
+
const nx = n[k], ny = n[k + 1], nz = n[k + 2];
|
|
644
|
+
const ar = a[k], ag = a[k + 1], ab = a[k + 2];
|
|
645
|
+
let r = 0, g = 0, b = 0, total = 0;
|
|
646
|
+
for (let delta = -radius; delta <= radius; delta++) {
|
|
647
|
+
const px = pass === 0 ? x + delta : x, py = pass === 0 ? y : y + delta;
|
|
635
648
|
if (px < 0 || py < 0 || px >= this.width || py >= this.height)
|
|
636
649
|
continue;
|
|
637
650
|
const neighbor = py * this.width + px, p = neighbor * 3;
|
|
638
|
-
if (
|
|
639
|
-
if (Math.abs(
|
|
651
|
+
if (ids[neighbor] !== id) continue;
|
|
652
|
+
if (Math.abs(distance - this.distance[neighbor]) > distance * 0.02)
|
|
640
653
|
continue;
|
|
641
|
-
if (
|
|
654
|
+
if (nx * n[p] + ny * n[p + 1] + nz * n[p + 2] < 0.98) continue;
|
|
655
|
+
if (Math.abs(ar - a[p]) + Math.abs(ag - a[p + 1]) + Math.abs(ab - a[p + 2]) > 0.03)
|
|
642
656
|
continue;
|
|
643
|
-
|
|
644
|
-
continue;
|
|
645
|
-
const weight = kernel[dy + 2] * kernel[dx + 2];
|
|
657
|
+
const weight = kernel[delta + radius];
|
|
646
658
|
total += weight;
|
|
647
|
-
r +=
|
|
648
|
-
g +=
|
|
649
|
-
b +=
|
|
659
|
+
r += source[p] * weight;
|
|
660
|
+
g += source[p + 1] * weight;
|
|
661
|
+
b += source[p + 2] * weight;
|
|
650
662
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
663
|
+
if (pass === 0) {
|
|
664
|
+
this.horizontal[k] = r / total;
|
|
665
|
+
this.horizontal[k + 1] = g / total;
|
|
666
|
+
this.horizontal[k + 2] = b / total;
|
|
667
|
+
} else {
|
|
668
|
+
const encode2 = (value) => Math.floor(
|
|
669
|
+
255 * (gamma ? srgbEncodeLinear01(
|
|
670
|
+
Math.max(0, Math.min(1, value / total))
|
|
671
|
+
) : Math.max(0, Math.min(1, value / total)))
|
|
672
|
+
);
|
|
673
|
+
output[index * 4] = encode2(r);
|
|
674
|
+
output[index * 4 + 1] = encode2(g);
|
|
675
|
+
output[index * 4 + 2] = encode2(b);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
659
679
|
}
|
|
660
680
|
};
|
|
661
681
|
|
|
@@ -835,9 +855,15 @@ var SoftwareRenderer = class {
|
|
|
835
855
|
}
|
|
836
856
|
return out;
|
|
837
857
|
}
|
|
838
|
-
drawMesh(mesh, camera, light, material, cullBackFaces = true, gammaOut = true, studio) {
|
|
858
|
+
drawMesh(mesh, camera, light, material, cullBackFaces = true, gammaOut = true, studio, visibility) {
|
|
839
859
|
let { positions, normals, uvs, indices, model, colors } = mesh;
|
|
840
860
|
const materialId = this.denoiser?.materialId(material) ?? 0;
|
|
861
|
+
const lightDir = light.dir ?? DEFAULT_LIGHT_DIR;
|
|
862
|
+
const ambient = clamp(light.ambient ?? DEFAULT_RENDER_OPTIONS.ambient, 0, 1);
|
|
863
|
+
const L = studio ? null : vec32.normalize(
|
|
864
|
+
vec32.create(),
|
|
865
|
+
vec32.fromValues(lightDir[0], lightDir[1], lightDir[2])
|
|
866
|
+
);
|
|
841
867
|
const view = camera.view;
|
|
842
868
|
const proj = camera.proj;
|
|
843
869
|
const mvp = mat43.create();
|
|
@@ -864,12 +890,17 @@ var SoftwareRenderer = class {
|
|
|
864
890
|
colors = clipped.colors;
|
|
865
891
|
idx = clipped.indices;
|
|
866
892
|
vertexCount = positions.length / 3;
|
|
893
|
+
const depthOnly = visibility?.depthOnly ?? false;
|
|
867
894
|
const vScreen = new Array(vertexCount);
|
|
868
895
|
const vInvW = new Float32Array(vertexCount);
|
|
869
896
|
const vNDCz = new Float32Array(vertexCount);
|
|
870
|
-
const vWorldN = new Array(
|
|
871
|
-
|
|
872
|
-
|
|
897
|
+
const vWorldN = new Array(
|
|
898
|
+
depthOnly ? 0 : vertexCount
|
|
899
|
+
);
|
|
900
|
+
const vWorldP = studio && !depthOnly ? new Array(vertexCount) : null;
|
|
901
|
+
const vColor = new Array(
|
|
902
|
+
depthOnly ? 0 : vertexCount
|
|
903
|
+
);
|
|
873
904
|
for (let i = 0; i < vertexCount; i++) {
|
|
874
905
|
const p = vec42.fromValues(
|
|
875
906
|
positions[i * 3 + 0],
|
|
@@ -892,6 +923,7 @@ var SoftwareRenderer = class {
|
|
|
892
923
|
vScreen[i] = [sx, sy];
|
|
893
924
|
vInvW[i] = invW;
|
|
894
925
|
vNDCz[i] = ndcZ;
|
|
926
|
+
if (depthOnly) continue;
|
|
895
927
|
const n = vec32.fromValues(
|
|
896
928
|
useNormals[i * 3 + 0],
|
|
897
929
|
useNormals[i * 3 + 1],
|
|
@@ -969,9 +1001,15 @@ var SoftwareRenderer = class {
|
|
|
969
1001
|
const z01 = zndc * 0.5 + 0.5;
|
|
970
1002
|
if (z01 < 0 || z01 > 1) continue;
|
|
971
1003
|
const di = y * this.width + x;
|
|
1004
|
+
if (visibility && !visibility.depthOnly && visibility.winners[di] !== visibility.triangleOffset + i / 3)
|
|
1005
|
+
continue;
|
|
972
1006
|
const depth = this.depth;
|
|
973
1007
|
if (z01 >= depth[di]) continue;
|
|
974
1008
|
depth[di] = z01;
|
|
1009
|
+
if (visibility?.depthOnly) {
|
|
1010
|
+
visibility.winners[di] = visibility.triangleOffset + i / 3;
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
975
1013
|
let baseColor = [
|
|
976
1014
|
material.baseColorFactor[0],
|
|
977
1015
|
material.baseColorFactor[1],
|
|
@@ -1006,24 +1044,7 @@ var SoftwareRenderer = class {
|
|
|
1006
1044
|
np1 / nlen,
|
|
1007
1045
|
np2 / nlen
|
|
1008
1046
|
];
|
|
1009
|
-
|
|
1010
|
-
const ambient = clamp(
|
|
1011
|
-
light.ambient ?? DEFAULT_RENDER_OPTIONS.ambient,
|
|
1012
|
-
0,
|
|
1013
|
-
1
|
|
1014
|
-
);
|
|
1015
|
-
const L = vec32.normalize(
|
|
1016
|
-
vec32.create(),
|
|
1017
|
-
vec32.fromValues(lightDir[0], lightDir[1], lightDir[2])
|
|
1018
|
-
);
|
|
1019
|
-
const ndotl = Math.max(
|
|
1020
|
-
0,
|
|
1021
|
-
nrm[0] * -L[0] + nrm[1] * -L[1] + nrm[2] * -L[2]
|
|
1022
|
-
);
|
|
1023
|
-
const lit = ambient + (1 - ambient) * ndotl;
|
|
1024
|
-
let r = baseColor[0] * lit;
|
|
1025
|
-
let g = baseColor[1] * lit;
|
|
1026
|
-
let b = baseColor[2] * lit;
|
|
1047
|
+
let r = 0, g = 0, b = 0;
|
|
1027
1048
|
if (studio && worldPositions) {
|
|
1028
1049
|
const position = this.perspInterp(worldPositions, invW, [
|
|
1029
1050
|
l0,
|
|
@@ -1035,9 +1056,36 @@ var SoftwareRenderer = class {
|
|
|
1035
1056
|
nrm,
|
|
1036
1057
|
[baseColor[0], baseColor[1], baseColor[2]],
|
|
1037
1058
|
material,
|
|
1038
|
-
|
|
1059
|
+
x,
|
|
1060
|
+
y
|
|
1061
|
+
);
|
|
1062
|
+
} else {
|
|
1063
|
+
const ndotl = Math.max(
|
|
1064
|
+
0,
|
|
1065
|
+
nrm[0] * -L[0] + nrm[1] * -L[1] + nrm[2] * -L[2]
|
|
1066
|
+
);
|
|
1067
|
+
const lit = ambient + (1 - ambient) * ndotl;
|
|
1068
|
+
r = baseColor[0] * lit;
|
|
1069
|
+
g = baseColor[1] * lit;
|
|
1070
|
+
b = baseColor[2] * lit;
|
|
1071
|
+
}
|
|
1072
|
+
const emission = material.emissiveFactor ?? [0, 0, 0];
|
|
1073
|
+
const strength = material.emissiveStrength ?? 1;
|
|
1074
|
+
let er = emission[0], eg = emission[1], eb = emission[2];
|
|
1075
|
+
if (uv && material.emissiveTexture) {
|
|
1076
|
+
const uvp = this.perspInterp(uv, invW, [l0, l1, l2]);
|
|
1077
|
+
const texel = this.sampleTextureNearest(
|
|
1078
|
+
material.emissiveTexture,
|
|
1079
|
+
uvp[0],
|
|
1080
|
+
uvp[1]
|
|
1039
1081
|
);
|
|
1082
|
+
er *= srgbDecodeToLinear01(texel[0]);
|
|
1083
|
+
eg *= srgbDecodeToLinear01(texel[1]);
|
|
1084
|
+
eb *= srgbDecodeToLinear01(texel[2]);
|
|
1040
1085
|
}
|
|
1086
|
+
r += er * strength;
|
|
1087
|
+
g += eg * strength;
|
|
1088
|
+
b += eb * strength;
|
|
1041
1089
|
let a = baseColor[3];
|
|
1042
1090
|
const linearR = r, linearG = g, linearB = b;
|
|
1043
1091
|
const alphaMode = material.alphaMode ?? "OPAQUE";
|
|
@@ -1100,6 +1148,7 @@ var SoftwareRenderer = class {
|
|
|
1100
1148
|
}
|
|
1101
1149
|
}
|
|
1102
1150
|
}
|
|
1151
|
+
return idx.length / 3;
|
|
1103
1152
|
}
|
|
1104
1153
|
};
|
|
1105
1154
|
function edge(a, b, p) {
|
|
@@ -1485,15 +1534,16 @@ var RayOcclusion = class {
|
|
|
1485
1534
|
if ((dc.mode ?? 4) !== 4 || dc.material.alphaMode === "BLEND") continue;
|
|
1486
1535
|
if (dc.material.alphaMode === "MASK") continue;
|
|
1487
1536
|
const count = dc.indices?.length ?? dc.positions.length / 3;
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1537
|
+
const worldVertices = [];
|
|
1538
|
+
for (let i = 0; i < dc.positions.length; i += 3) {
|
|
1490
1539
|
const p = vec33.transformMat4(
|
|
1491
1540
|
vec33.create(),
|
|
1492
1541
|
[dc.positions[i], dc.positions[i + 1], dc.positions[i + 2]],
|
|
1493
1542
|
dc.model
|
|
1494
1543
|
);
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1544
|
+
worldVertices.push([p[0], p[1], p[2]]);
|
|
1545
|
+
}
|
|
1546
|
+
const vertex = (index) => worldVertices[dc.indices?.[index] ?? index];
|
|
1497
1547
|
for (let i = 0; i < count; i += 3) {
|
|
1498
1548
|
const p = vertex(i), q = vertex(i + 1), r = vertex(i + 2);
|
|
1499
1549
|
const min = [0, 1, 2].map(
|
|
@@ -1569,52 +1619,97 @@ var RayOcclusion = class {
|
|
|
1569
1619
|
}
|
|
1570
1620
|
}
|
|
1571
1621
|
build(triangles) {
|
|
1572
|
-
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
1622
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity], centerMin = [Infinity, Infinity, Infinity], centerMax = [-Infinity, -Infinity, -Infinity];
|
|
1573
1623
|
for (const t of triangles)
|
|
1574
1624
|
for (let a = 0; a < 3; a++) {
|
|
1575
1625
|
min[a] = Math.min(min[a], t.min[a]);
|
|
1576
1626
|
max[a] = Math.max(max[a], t.max[a]);
|
|
1627
|
+
centerMin[a] = Math.min(centerMin[a], t.center[a]);
|
|
1628
|
+
centerMax[a] = Math.max(centerMax[a], t.center[a]);
|
|
1577
1629
|
}
|
|
1578
1630
|
if (triangles.length <= 8) return { min, max, triangles };
|
|
1631
|
+
const binCount = 16;
|
|
1579
1632
|
const area = (lo, hi) => {
|
|
1580
1633
|
const x = hi[0] - lo[0], y = hi[1] - lo[1], z = hi[2] - lo[2];
|
|
1581
1634
|
return 2 * (x * y + y * z + z * x);
|
|
1582
1635
|
};
|
|
1583
|
-
let bestCost = Infinity,
|
|
1636
|
+
let bestCost = Infinity, bestAxis = -1, bestSplit = 0, bestMin = 0, bestScale = 0;
|
|
1637
|
+
let longestAxis = 0;
|
|
1638
|
+
for (let a = 1; a < 3; a++)
|
|
1639
|
+
if (centerMax[a] - centerMin[a] > centerMax[longestAxis] - centerMin[longestAxis])
|
|
1640
|
+
longestAxis = a;
|
|
1584
1641
|
for (let axis = 0; axis < 3; axis++) {
|
|
1585
|
-
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1588
|
-
|
|
1642
|
+
if (triangles.length <= 4096 && axis !== longestAxis) continue;
|
|
1643
|
+
const loCenter = centerMin[axis], hiCenter = centerMax[axis];
|
|
1644
|
+
if (loCenter === hiCenter) continue;
|
|
1645
|
+
const scale = binCount / (hiCenter - loCenter);
|
|
1646
|
+
const counts = new Uint32Array(binCount);
|
|
1647
|
+
const bounds = new Float64Array(binCount * 6);
|
|
1648
|
+
for (let i = 0; i < binCount; i++) {
|
|
1649
|
+
bounds.fill(Infinity, i * 6, i * 6 + 3);
|
|
1650
|
+
bounds.fill(-Infinity, i * 6 + 3, i * 6 + 6);
|
|
1651
|
+
}
|
|
1652
|
+
for (const t of triangles) {
|
|
1653
|
+
const bin = Math.min(
|
|
1654
|
+
binCount - 1,
|
|
1655
|
+
Math.floor((t.center[axis] - loCenter) * scale)
|
|
1656
|
+
);
|
|
1657
|
+
counts[bin]++;
|
|
1658
|
+
const offset = bin * 6;
|
|
1589
1659
|
for (let a = 0; a < 3; a++) {
|
|
1590
|
-
|
|
1591
|
-
|
|
1660
|
+
bounds[offset + a] = Math.min(bounds[offset + a], t.min[a]);
|
|
1661
|
+
bounds[offset + a + 3] = Math.max(bounds[offset + a + 3], t.max[a]);
|
|
1592
1662
|
}
|
|
1593
|
-
|
|
1663
|
+
}
|
|
1664
|
+
const suffixCost = new Float64Array(binCount);
|
|
1665
|
+
let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity], count = 0;
|
|
1666
|
+
for (let i = binCount - 1; i >= 0; i--) {
|
|
1667
|
+
count += counts[i];
|
|
1668
|
+
if (counts[i])
|
|
1669
|
+
for (let a = 0; a < 3; a++) {
|
|
1670
|
+
lo[a] = Math.min(lo[a], bounds[i * 6 + a]);
|
|
1671
|
+
hi[a] = Math.max(hi[a], bounds[i * 6 + a + 3]);
|
|
1672
|
+
}
|
|
1673
|
+
suffixCost[i] = count ? area(lo, hi) * count : 0;
|
|
1594
1674
|
}
|
|
1595
1675
|
lo = [Infinity, Infinity, Infinity];
|
|
1596
1676
|
hi = [-Infinity, -Infinity, -Infinity];
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1677
|
+
count = 0;
|
|
1678
|
+
for (let i = 0; i < binCount - 1; i++) {
|
|
1679
|
+
count += counts[i];
|
|
1680
|
+
if (counts[i])
|
|
1681
|
+
for (let a = 0; a < 3; a++) {
|
|
1682
|
+
lo[a] = Math.min(lo[a], bounds[i * 6 + a]);
|
|
1683
|
+
hi[a] = Math.max(hi[a], bounds[i * 6 + a + 3]);
|
|
1684
|
+
}
|
|
1685
|
+
if (!count || count === triangles.length) continue;
|
|
1686
|
+
const cost = area(lo, hi) * count + suffixCost[i + 1];
|
|
1605
1687
|
if (cost < bestCost) {
|
|
1606
1688
|
bestCost = cost;
|
|
1607
|
-
|
|
1608
|
-
|
|
1689
|
+
bestAxis = axis;
|
|
1690
|
+
bestSplit = i;
|
|
1691
|
+
bestMin = loCenter;
|
|
1692
|
+
bestScale = scale;
|
|
1609
1693
|
}
|
|
1610
1694
|
}
|
|
1611
1695
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
left
|
|
1616
|
-
right
|
|
1617
|
-
}
|
|
1696
|
+
let left, right;
|
|
1697
|
+
if (bestAxis < 0) {
|
|
1698
|
+
const middle = triangles.length >> 1;
|
|
1699
|
+
left = triangles.slice(0, middle);
|
|
1700
|
+
right = triangles.slice(middle);
|
|
1701
|
+
} else {
|
|
1702
|
+
left = [];
|
|
1703
|
+
right = [];
|
|
1704
|
+
for (const t of triangles) {
|
|
1705
|
+
const bin = Math.min(
|
|
1706
|
+
binCount - 1,
|
|
1707
|
+
Math.floor((t.center[bestAxis] - bestMin) * bestScale)
|
|
1708
|
+
);
|
|
1709
|
+
(bin <= bestSplit ? left : right).push(t);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return { min, max, left: this.build(left), right: this.build(right) };
|
|
1618
1713
|
}
|
|
1619
1714
|
prepareRay(direction) {
|
|
1620
1715
|
this.parallelAxes = 0;
|
|
@@ -1749,6 +1844,9 @@ var W = 96;
|
|
|
1749
1844
|
var H = 48;
|
|
1750
1845
|
var SAMPLES = 128;
|
|
1751
1846
|
var ROUGHNESSES = [0.04, 0.1, 0.18, 0.28, 0.4, 0.55, 0.7, 0.85, 1];
|
|
1847
|
+
var SHADOW_SAMPLES = 4;
|
|
1848
|
+
var REFLECTION_SAMPLES = 4;
|
|
1849
|
+
var DIFFUSE_SAMPLES = 2;
|
|
1752
1850
|
var lobes = studio_environment_default.lobes.map((l) => ({
|
|
1753
1851
|
...l,
|
|
1754
1852
|
direction: normalize(l.direction),
|
|
@@ -1881,6 +1979,22 @@ var StudioLighting = class {
|
|
|
1881
1979
|
eye;
|
|
1882
1980
|
shadowDirections;
|
|
1883
1981
|
diffuseCache = /* @__PURE__ */ new WeakMap();
|
|
1982
|
+
reflectionSamples = /* @__PURE__ */ new Map();
|
|
1983
|
+
normalDiffuseCache = /* @__PURE__ */ new WeakMap();
|
|
1984
|
+
specularCache = /* @__PURE__ */ new Map();
|
|
1985
|
+
blendedSpecular(rough, level, blend) {
|
|
1986
|
+
const cached = this.specularCache.get(rough);
|
|
1987
|
+
if (cached) return cached;
|
|
1988
|
+
if (this.specularCache.size >= 16) return null;
|
|
1989
|
+
const tables = lobes.map((_, j) => {
|
|
1990
|
+
const lo = this.tables.specular[level][j], hi = this.tables.specular[level + 1][j], table = new Float64Array(W * H);
|
|
1991
|
+
for (let i = 0; i < table.length; i++)
|
|
1992
|
+
table[i] = lo[i] * (1 - blend) + hi[i] * blend;
|
|
1993
|
+
return table;
|
|
1994
|
+
});
|
|
1995
|
+
this.specularCache.set(rough, tables);
|
|
1996
|
+
return tables;
|
|
1997
|
+
}
|
|
1884
1998
|
constructor(drawCalls, camera) {
|
|
1885
1999
|
this.occlusion = new RayOcclusion(drawCalls);
|
|
1886
2000
|
const world = mat46.invert(mat46.create(), camera.view);
|
|
@@ -1889,8 +2003,8 @@ var StudioLighting = class {
|
|
|
1889
2003
|
this.eye = [world[12], world[13], world[14]];
|
|
1890
2004
|
this.shadowDirections = lobes.map((l) => {
|
|
1891
2005
|
const [t, b] = basis(l.direction);
|
|
1892
|
-
return Array.from({ length:
|
|
1893
|
-
const u = (i + 0.5) /
|
|
2006
|
+
return Array.from({ length: SHADOW_SAMPLES }, (_, i) => {
|
|
2007
|
+
const u = (i + 0.5) / SHADOW_SAMPLES, cos = 1 + Math.log(1 - u) / l.sharpness, sin = Math.sqrt(Math.max(0, 1 - cos * cos)), phi = 2 * Math.PI * radicalInverse(i);
|
|
1894
2008
|
const direction = orient(
|
|
1895
2009
|
sin * Math.cos(phi),
|
|
1896
2010
|
sin * Math.sin(phi),
|
|
@@ -1948,15 +2062,16 @@ var StudioLighting = class {
|
|
|
1948
2062
|
);
|
|
1949
2063
|
const pref = [...studio_environment_default.ambient];
|
|
1950
2064
|
const reflectedLookup = tableLookup(reflected);
|
|
2065
|
+
const specularTables = this.blendedSpecular(rough, level, blend);
|
|
1951
2066
|
for (let j = 0; j < lobes.length; j++) {
|
|
1952
|
-
const value = sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend;
|
|
2067
|
+
const value = specularTables ? sampleTable(specularTables[j], reflectedLookup) : sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend;
|
|
1953
2068
|
for (let k = 0; k < 3; k++) pref[k] += value * lobes[j].color[k];
|
|
1954
2069
|
}
|
|
1955
2070
|
for (let k = 0; k < 3; k++)
|
|
1956
2071
|
radiance[k] += pref[k] * ((0.04 * (1 - metal) + base[k] * metal) * A + B);
|
|
1957
2072
|
return radiance;
|
|
1958
2073
|
}
|
|
1959
|
-
shade(position, n, base, material,
|
|
2074
|
+
shade(position, n, base, material, pixelX = 0, pixelY = 0) {
|
|
1960
2075
|
const v = normalize([
|
|
1961
2076
|
this.eye[0] - position[0],
|
|
1962
2077
|
this.eye[1] - position[1],
|
|
@@ -1988,12 +2103,20 @@ var StudioLighting = class {
|
|
|
1988
2103
|
const specColor = f0.map(
|
|
1989
2104
|
(c, j) => studio_environment_default.ambient[j] * (c * A + B)
|
|
1990
2105
|
);
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
bits = Math.imul(bits ^ bits >>> 15, 2221713035);
|
|
1994
|
-
const hash = ((bits ^ bits >>> 16) >>> 0) / 4294967296;
|
|
2106
|
+
const gradient = (0.06711056 * pixelX + 583715e-8 * pixelY) % 1;
|
|
2107
|
+
const hash = 52.9829189 * gradient % 1;
|
|
1995
2108
|
const phase = hash * 2 * Math.PI, cosPhase = Math.cos(phase), sinPhase = Math.sin(phase);
|
|
1996
|
-
|
|
2109
|
+
let normalDiffuse = this.normalDiffuseCache.get(material);
|
|
2110
|
+
if (!normalDiffuse || normalDiffuse.normal.some((value, k) => !Object.is(value, n[k]))) {
|
|
2111
|
+
const lookup = tableLookup(n);
|
|
2112
|
+
normalDiffuse = {
|
|
2113
|
+
normal: [...n],
|
|
2114
|
+
values: this.tables.diffuse.map((table) => sampleTable(table, lookup))
|
|
2115
|
+
};
|
|
2116
|
+
this.normalDiffuseCache.set(material, normalDiffuse);
|
|
2117
|
+
}
|
|
2118
|
+
const reflectedLookup = tableLookup(reflected);
|
|
2119
|
+
const specularTables = this.blendedSpecular(rough, level, blend);
|
|
1997
2120
|
for (let j = 0; j < lobes.length; j++) {
|
|
1998
2121
|
const dirs = this.shadowDirections[j];
|
|
1999
2122
|
let visible = 0, total = 0;
|
|
@@ -2008,8 +2131,8 @@ var StudioLighting = class {
|
|
|
2008
2131
|
if (weight > 0 && !this.occlusion.occluded(origin, l)) visible += weight;
|
|
2009
2132
|
}
|
|
2010
2133
|
const visibility = total > 0 ? visible / total : 1;
|
|
2011
|
-
const diffuse =
|
|
2012
|
-
const specular = (sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend) * visibility;
|
|
2134
|
+
const diffuse = normalDiffuse.values[j] * visibility;
|
|
2135
|
+
const specular = (specularTables ? sampleTable(specularTables[j], reflectedLookup) : sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend) * visibility;
|
|
2013
2136
|
for (let k = 0; k < 3; k++) {
|
|
2014
2137
|
color[k] += lobes[j].color[k] * base[k] * (1 - metal) * 0.96 * diffuse;
|
|
2015
2138
|
specColor[k] += lobes[j].color[k] * (f0[k] * A + B) * specular;
|
|
@@ -2017,8 +2140,8 @@ var StudioLighting = class {
|
|
|
2017
2140
|
}
|
|
2018
2141
|
const [t, b] = basis(n);
|
|
2019
2142
|
let blocked = 0;
|
|
2020
|
-
for (let i = 0; metal < 1 && i <
|
|
2021
|
-
const phi = i * Math.PI /
|
|
2143
|
+
for (let i = 0; metal < 1 && i < DIFFUSE_SAMPLES; i++) {
|
|
2144
|
+
const phi = i * 2 * Math.PI / DIFFUSE_SAMPLES + phase, l = orient(
|
|
2022
2145
|
0.7 * Math.cos(phi),
|
|
2023
2146
|
0.7 * Math.sin(phi),
|
|
2024
2147
|
Math.sqrt(0.51),
|
|
@@ -2031,20 +2154,26 @@ var StudioLighting = class {
|
|
|
2031
2154
|
blocked++;
|
|
2032
2155
|
const bounce2 = this.bounceRadiance(hit, l);
|
|
2033
2156
|
for (let k = 0; k < 3; k++)
|
|
2034
|
-
color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 /
|
|
2157
|
+
color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 / DIFFUSE_SAMPLES;
|
|
2035
2158
|
}
|
|
2036
2159
|
}
|
|
2037
|
-
const ao = 1 - blocked /
|
|
2160
|
+
const ao = 1 - blocked / DIFFUSE_SAMPLES;
|
|
2038
2161
|
for (let k = 0; k < 3; k++)
|
|
2039
2162
|
color[k] -= studio_environment_default.ambient[k] * (1 - ao) * base[k] * (1 - metal) * 0.96;
|
|
2040
2163
|
const [rt, rb] = basis(reflected), bounce = [0, 0, 0];
|
|
2041
2164
|
let hitWeight = 0, totalWeight = 0;
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2165
|
+
let samples = this.reflectionSamples.get(rough);
|
|
2166
|
+
if (!samples) {
|
|
2167
|
+
samples = Array.from({ length: REFLECTION_SAMPLES }, (_, i) => {
|
|
2168
|
+
const u = (i + 0.5) / REFLECTION_SAMPLES, phi = 2 * Math.PI * radicalInverse(i), a = rough * rough, nh = Math.sqrt((1 - u) / (1 + (a * a - 1) * u)), sin = Math.sqrt(1 - nh * nh);
|
|
2169
|
+
return [sin * Math.cos(phi), sin * Math.sin(phi), nh];
|
|
2170
|
+
});
|
|
2171
|
+
this.reflectionSamples.set(rough, samples);
|
|
2172
|
+
}
|
|
2173
|
+
for (const [sx, sy, nh] of samples) {
|
|
2045
2174
|
const h = orient(
|
|
2046
|
-
|
|
2047
|
-
|
|
2175
|
+
sx * cosPhase - sy * sinPhase,
|
|
2176
|
+
sx * sinPhase + sy * cosPhase,
|
|
2048
2177
|
nh,
|
|
2049
2178
|
reflected,
|
|
2050
2179
|
rt,
|
|
@@ -2153,8 +2282,29 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
2153
2282
|
(dc) => (dc.material.alphaMode ?? "OPAQUE") === "BLEND"
|
|
2154
2283
|
);
|
|
2155
2284
|
const studio = options.realistic ? new StudioLighting(allDrawCalls, camera) : void 0;
|
|
2156
|
-
|
|
2157
|
-
|
|
2285
|
+
let winners;
|
|
2286
|
+
const triangleOffsets = [];
|
|
2287
|
+
if (studio && opaqueDraws.length > 1 && opaqueDraws.every((dc) => dc.mode !== 1)) {
|
|
2288
|
+
winners = new Int32Array(renderWidth * renderHeight).fill(-1);
|
|
2289
|
+
let triangleOffset = 0;
|
|
2290
|
+
for (const dc of opaqueDraws) {
|
|
2291
|
+
triangleOffsets.push(triangleOffset);
|
|
2292
|
+
triangleOffset += renderer.drawMesh(
|
|
2293
|
+
dc,
|
|
2294
|
+
camera,
|
|
2295
|
+
{ dir: options.lightDir, ambient: options.ambient },
|
|
2296
|
+
dc.material,
|
|
2297
|
+
options.cull,
|
|
2298
|
+
options.gamma,
|
|
2299
|
+
studio,
|
|
2300
|
+
{ winners, triangleOffset, depthOnly: true }
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
renderer.depth.fill(Infinity);
|
|
2304
|
+
}
|
|
2305
|
+
const renderGroup = (dcs, opaque = false) => {
|
|
2306
|
+
for (let i = 0; i < dcs.length; i++) {
|
|
2307
|
+
const dc = dcs[i];
|
|
2158
2308
|
if (dc.mode === 1) {
|
|
2159
2309
|
renderer.drawLines(dc, camera, options.gamma);
|
|
2160
2310
|
} else {
|
|
@@ -2165,12 +2315,13 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
2165
2315
|
dc.material,
|
|
2166
2316
|
options.cull,
|
|
2167
2317
|
options.gamma,
|
|
2168
|
-
studio
|
|
2318
|
+
studio,
|
|
2319
|
+
opaque && winners ? { winners, triangleOffset: triangleOffsets[i], depthOnly: false } : void 0
|
|
2169
2320
|
);
|
|
2170
2321
|
}
|
|
2171
2322
|
}
|
|
2172
2323
|
};
|
|
2173
|
-
renderGroup(opaqueDraws);
|
|
2324
|
+
renderGroup(opaqueDraws, true);
|
|
2174
2325
|
renderGroup(maskDraws);
|
|
2175
2326
|
if (infiniteGridOptions) {
|
|
2176
2327
|
const aabb = computeWorldAABB(drawCalls);
|
|
@@ -2519,6 +2670,9 @@ function getMaterial(gltf, textures, images, materialIndex) {
|
|
|
2519
2670
|
baseColorTexture: texImg,
|
|
2520
2671
|
metallicFactor: pbr.metallicFactor ?? 1,
|
|
2521
2672
|
roughnessFactor: pbr.roughnessFactor ?? 1,
|
|
2673
|
+
emissiveFactor: material.emissiveFactor ?? [0, 0, 0],
|
|
2674
|
+
emissiveTexture: images[textures[material.emissiveTexture?.index]?.source] ?? null,
|
|
2675
|
+
emissiveStrength: material.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1,
|
|
2522
2676
|
alphaMode,
|
|
2523
2677
|
alphaCutoff
|
|
2524
2678
|
};
|