@plasius/gpu-renderer 0.2.47 → 0.2.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -556,12 +556,18 @@ struct Frame {
556
556
  @group(0) @binding(0) var<uniform> frame: Frame;
557
557
  @group(1) @binding(0) var shadowMap: texture_depth_2d;
558
558
  @group(1) @binding(1) var shadowSampler: sampler_comparison;
559
- @group(2) @binding(0) var reflectionMap: texture_2d<f32>;
560
- @group(2) @binding(1) var reflectionSampler: sampler;
559
+ @group(3) @binding(0) var reflectionMap: texture_2d<f32>;
560
+ @group(3) @binding(1) var reflectionSampler: sampler;
561
+ @group(2) @binding(0) var baseColourMap: texture_2d<f32>;
562
+ @group(2) @binding(1) var normalMap: texture_2d<f32>;
563
+ @group(2) @binding(2) var ormMap: texture_2d<f32>;
564
+ @group(2) @binding(3) var materialSampler: sampler;
565
+ @group(2) @binding(4) var<uniform> materialParameters: vec4f;
566
+ @group(2) @binding(5) var clearcoatMap: texture_2d<f32>;
561
567
  struct Surface {
562
568
  @builtin(position) clip: vec4f,
563
569
  @location(0) world: vec3f, @location(1) normal: vec3f,
564
- @location(2) colour: vec3f, @location(3) material: vec3f,
570
+ @location(2) colour: vec3f, @location(3) material: vec3f, @location(4) uv: vec2f,
565
571
  };
566
572
  fn project(p: vec3f) -> vec4f {
567
573
  let d = p - frame.eye.xyz;
@@ -575,8 +581,8 @@ fn lightProject(p: vec3f) -> vec4f {
575
581
  dot(d, frame.lightUp.xyz) / 34.0, dot(d, frame.lightForward.xyz) / 100.0, 1.0);
576
582
  }
577
583
  @vertex fn surfaceVertex(@location(0) p: vec3f, @location(1) n: vec3f,
578
- @location(2) c: vec3f, @location(3) m: vec3f) -> Surface {
579
- return Surface(project(p), p, n, c, m);
584
+ @location(2) c: vec3f, @location(3) m: vec3f, @location(4) uv: vec2f) -> Surface {
585
+ return Surface(project(p), p, n, c, m, uv);
580
586
  }
581
587
  @vertex fn shadowVertex(@location(0) p: vec3f) -> @builtin(position) vec4f {
582
588
  return lightProject(p);
@@ -629,17 +635,25 @@ fn visibility(p: vec3f, n: vec3f) -> f32 {
629
635
  }
630
636
  return result / 9.0;
631
637
  }
632
- fn brdf(c: vec3f, n: vec3f, v: vec3f, roughness: f32, metal: f32) -> vec3f {
638
+ // Dielectric coating is evaluated independently from the coloured substrate.
639
+ // This returns the microfacet lobe without Fresnel, including the cosine term.
640
+ fn specularLobe(n: vec3f, v: vec3f, roughness: f32) -> f32 {
633
641
  let l = frame.sun.xyz; let h = normalize(l + v);
634
642
  let nl = max(dot(n,l),0.0); let nv = max(dot(n,v),0.001);
635
- let nh = max(dot(n,h),0.0); let vh = max(dot(v,h),0.0);
643
+ let nh = max(dot(n,h),0.0);
636
644
  let a = max(0.035, roughness * roughness); let a2 = a * a;
637
645
  let d = a2 / max(0.00001, 3.14159265 * pow(nh * nh * (a2 - 1.0) + 1.0, 2.0));
638
646
  let k = pow(roughness + 1.0, 2.0) / 8.0;
639
647
  let g = (nl / (nl * (1.0-k) + k)) * (nv / (nv * (1.0-k) + k));
648
+ return d * g / max(0.001, 4.0*nl*nv) * nl;
649
+ }
650
+ fn brdf(c: vec3f, n: vec3f, v: vec3f, roughness: f32, metal: f32) -> vec3f {
651
+ let h = normalize(frame.sun.xyz + v);
652
+ let nl = max(dot(n,frame.sun.xyz),0.0);
653
+ let vh = max(dot(v,h),0.0);
640
654
  let f0 = mix(vec3f(0.04), c, metal);
641
655
  let f = f0 + (1.0-f0) * pow(1.0-vh, 5.0);
642
- return ((1.0-f) * (1.0-metal) * c / 3.14159265 + d * g * f / max(0.001, 4.0*nl*nv)) * nl;
656
+ return (1.0-f) * (1.0-metal) * c / 3.14159265 * nl + specularLobe(n,v,roughness) * f;
643
657
  }
644
658
  fn fog(c: vec3f, p: vec3f) -> vec3f {
645
659
  let amount = 1.0 - exp(-distance(frame.eye.xyz, p) * 0.0018);
@@ -647,23 +661,58 @@ fn fog(c: vec3f, p: vec3f) -> vec3f {
647
661
  }
648
662
  fn surfaceRadiance(s: Surface, front: bool) -> vec3f {
649
663
  let facing = select(select(-1.0,1.0,front),select(1.0,-1.0,front),frame.info.y>0.5);
650
- var n = normalize(s.normal) * facing;
664
+ let coatNormal = normalize(s.normal) * facing;
665
+ var n = coatNormal;
651
666
  let v = normalize(frame.eye.xyz - s.world);
652
- var c = s.colour;
653
- if (s.material.z > 2.5) {
667
+ // Derivatives and samples must be uniform across each fragment quad.
668
+ // Relative degeneracy: raw derivative magnitudes shrink with model scale and
669
+ // zoom. An absolute tangent cutoff silently disabled normal maps in close-ups.
670
+ let dpScale = max(length(dpdx(s.world)), length(dpdy(s.world)));
671
+ let uvScale = max(length(dpdx(s.uv)), length(dpdy(s.uv)));
672
+ let dx = dpdx(s.world) / max(dpScale, 1e-20);
673
+ let dy = dpdy(s.world) / max(dpScale, 1e-20);
674
+ let ux = dpdx(s.uv) / max(uvScale, 1e-20);
675
+ let uy = dpdy(s.uv) / max(uvScale, 1e-20);
676
+ let determinant = ux.x * uy.y - ux.y * uy.x;
677
+ let tangent = dx * uy.y - dy * ux.y;
678
+ let bitangent = dy * ux.x - dx * uy.x;
679
+ let sampledNormal = textureSample(normalMap, materialSampler, s.uv).xyz * 2.0 - 1.0;
680
+ var c = s.colour * textureSample(baseColourMap, materialSampler, s.uv).rgb;
681
+ let orm = textureSample(ormMap, materialSampler, s.uv).rgb;
682
+ let coatSample = textureSample(clearcoatMap, materialSampler, s.uv).rg;
683
+ let roughness = clamp(s.material.x * orm.g, 0.04, 1.0);
684
+ let metalness = clamp(s.material.y * orm.b, 0.0, 1.0);
685
+ let orthogonalTangent = tangent - n * dot(n,tangent);
686
+ let orthogonalBitangent = bitangent - n * dot(n,bitangent);
687
+ if (dpScale > 1e-20 && uvScale > 1e-20 && abs(determinant) > 1e-6 && dot(orthogonalTangent,orthogonalTangent) > 1e-12 && dot(orthogonalBitangent,orthogonalBitangent) > 1e-12) {
688
+ let t = normalize(orthogonalTangent) * sign(determinant);
689
+ let b = normalize(orthogonalBitangent) * sign(determinant);
690
+ n = normalize(t * sampledNormal.x * materialParameters.x + b * sampledNormal.y * materialParameters.x + n * max(0.001,sampledNormal.z));
691
+ }
692
+ if (materialParameters.y < 0.5 && s.material.z > 2.5) {
654
693
  let rock = smoothstep(0.45,0.68,noise(s.world*0.27));
655
694
  c = mix(c,vec3f(0.16,0.145,0.11),rock*0.7);
656
695
  c *= 0.75+0.45*noise(s.world*2.4);
657
696
  n = normalize(n+vec3f(noise(s.world*4.0)-0.5,0,noise(s.world*4.0+vec3f(13))-0.5)*0.28);
658
- } else if (s.material.z > 0.5 && s.material.z < 1.5) {
697
+ } else if (materialParameters.y < 0.5 && s.material.z > 0.5 && s.material.z < 1.5) {
659
698
  let grain = noise(s.world * vec3f(18, 3, 1.6));
660
699
  c *= 0.82 + 0.30 * grain;
661
- } else if (s.material.z > 1.5) { c *= 0.88 + 0.18 * noise(s.world * 12.0); }
700
+ } else if (materialParameters.y < 0.5 && s.material.z > 1.5) { c *= 0.88 + 0.18 * noise(s.world * 12.0); }
662
701
  let hemi = mix(vec3f(0.06,0.05,0.035), vec3f(0.16,0.22,0.29), n.y * 0.5 + 0.5);
663
- let direct = brdf(c, n, v, s.material.x, s.material.y) * vec3f(3.0, 2.65, 2.15) * visibility(s.world, n);
664
- let ambient = c * hemi * (1.0 - s.material.y * 0.7);
665
- let env = sky(reflect(-v,n)) * mix(vec3f(0.04), c, s.material.y) * (1.0-s.material.x) * 0.30;
666
- return fog(direct + ambient + env, s.world);
702
+ let direct = brdf(c, n, v, roughness, metalness) * vec3f(3.0, 2.65, 2.15) * visibility(s.world, n);
703
+ let ambient = c * hemi * (1.0 - metalness * 0.7) * orm.r;
704
+ let env = sky(reflect(-v,n)) * mix(vec3f(0.04), c, metalness) * (1.0-roughness) * orm.r * 0.30;
705
+ let baseRadiance = direct + ambient + env;
706
+ let coatCoverage = materialParameters.z * coatSample.r;
707
+ if (coatCoverage <= 0.0) { return fog(baseRadiance,s.world); }
708
+ // KHR_materials_clearcoat's simple Fresnel layering: varnish has its own
709
+ // smooth normal and roughness. Its reflection never inherits the wood colour.
710
+ let coatFresnel = 0.04 + 0.96 * pow(1.0-clamp(abs(dot(coatNormal,v)),0.0,1.0),5.0);
711
+ let coatWeight = coatCoverage * coatFresnel;
712
+ let coatRoughness = clamp(materialParameters.w * coatSample.g,0.04,1.0);
713
+ let coatDirect = vec3f(3.0,2.65,2.15) * specularLobe(coatNormal,v,coatRoughness) * visibility(s.world,coatNormal);
714
+ let coatEnvironment = sky(reflect(-v,coatNormal)) * (1.0-coatRoughness) * orm.r * 0.30;
715
+ return fog(baseRadiance * (1.0-coatWeight) + (coatDirect+coatEnvironment) * coatWeight, s.world);
667
716
  }
668
717
  @fragment fn surfaceFragment(s: Surface, @builtin(front_facing) front: bool) -> @location(0) vec4f {
669
718
  return vec4f(display(surfaceRadiance(s,front)),1);
@@ -717,12 +766,114 @@ struct WaterResult { @location(0) colour: vec4f, @builtin(frag_depth) depth: f32
717
766
  return vec4f(sky(cameraRay(pixel.xy)),1);
718
767
  }
719
768
  @fragment fn reflectionSurface(s: Surface, @builtin(front_facing) front: bool) -> @location(0) vec4f {
769
+ let radiance = surfaceRadiance(s,front);
720
770
  if (s.world.y < frame.viewport.z) { discard; }
721
- return vec4f(surfaceRadiance(s,front),1);
771
+ return vec4f(radiance,1);
722
772
  }
723
773
  `
724
774
  );
725
775
 
776
+ // src/native-surface-materials.js
777
+ var MAX_BYTES = 64 * 1024 * 1024;
778
+ var MAPS = ["baseColor", "normal", "orm", "clearcoatMap"];
779
+ var defaults = {
780
+ baseColor: [255, 255, 255, 255],
781
+ normal: [128, 128, 255, 255],
782
+ orm: [255, 255, 255, 255],
783
+ clearcoatMap: [255, 255, 255, 255]
784
+ };
785
+ var dimension = (n) => Number.isInteger(n) && n >= 1 && n <= 2048 && (n & n - 1) === 0;
786
+ function validateNativeMaterials(materials) {
787
+ if (!Array.isArray(materials) || materials.length > 16)
788
+ throw new Error("materials must contain at most 16 entries.");
789
+ let bytes = 0;
790
+ for (const material of materials) {
791
+ if (!material || typeof material !== "object")
792
+ throw new Error("Each material must be an object.");
793
+ if (!Number.isFinite(material.normalScale ?? 1) || (material.normalScale ?? 1) < 0 || (material.normalScale ?? 1) > 4) {
794
+ throw new Error("material normalScale must be between 0 and 4.");
795
+ }
796
+ for (const key of ["clearcoat", "clearcoatRoughness"]) {
797
+ const value = material[key] ?? 0;
798
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
799
+ throw new Error(`material ${key} must be between 0 and 1.`);
800
+ }
801
+ }
802
+ for (const key of MAPS) {
803
+ const map = material[key];
804
+ if (map === void 0) continue;
805
+ if (!map || !dimension(map.width) || !dimension(map.height) || !(map.data instanceof Uint8Array || map.data instanceof Uint8ClampedArray) || map.data.length !== map.width * map.height * 4) {
806
+ throw new Error(
807
+ `material ${key} texture must contain RGBA8 data with power-of-two dimensions between 1 and 2048.`
808
+ );
809
+ }
810
+ bytes += map.data.byteLength;
811
+ if (bytes > MAX_BYTES)
812
+ throw new Error("material textures exceed the 64 MiB source budget.");
813
+ }
814
+ }
815
+ }
816
+ var linear = (x) => x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
817
+ var srgb = (x) => x <= 31308e-7 ? x * 12.92 : 1.055 * x ** (1 / 2.4) - 0.055;
818
+ function nextMip(image, colour) {
819
+ const width = Math.max(1, image.width / 2), height = Math.max(1, image.height / 2);
820
+ const data = new Uint8Array(width * height * 4);
821
+ for (let y = 0; y < height; y++)
822
+ for (let x = 0; x < width; x++)
823
+ for (let c = 0; c < 4; c++) {
824
+ let sum = 0;
825
+ for (let dy = 0; dy < 2; dy++)
826
+ for (let dx = 0; dx < 2; dx++) {
827
+ const offset = (Math.min(image.height - 1, y * 2 + dy) * image.width + Math.min(image.width - 1, x * 2 + dx)) * 4 + c;
828
+ const value = image.data[offset] / 255;
829
+ sum += colour && c < 3 ? linear(value) : value;
830
+ }
831
+ data[(y * width + x) * 4 + c] = Math.round(
832
+ 255 * (colour && c < 3 ? srgb(sum / 4) : sum / 4)
833
+ );
834
+ }
835
+ return { width, height, data };
836
+ }
837
+ function createNativeMaterialTextures(device, materials, own) {
838
+ return [{}, ...materials].map((material, index) => {
839
+ const result = {
840
+ normalScale: material.normal ? material.normalScale ?? 1 : 0,
841
+ authored: ["baseColor", "normal", "orm"].some((key) => material[key]),
842
+ clearcoat: material.clearcoat ?? 0,
843
+ clearcoatRoughness: material.clearcoatRoughness ?? 0
844
+ };
845
+ for (const key of MAPS) {
846
+ let level = material[key] ?? {
847
+ width: 1,
848
+ height: 1,
849
+ data: new Uint8Array(defaults[key])
850
+ };
851
+ const mipLevelCount = 1 + Math.log2(Math.max(level.width, level.height));
852
+ const texture = own(
853
+ device.createTexture({
854
+ label: `native.material.${index}.${key}`,
855
+ size: { width: level.width, height: level.height },
856
+ format: key === "baseColor" ? "rgba8unorm-srgb" : "rgba8unorm",
857
+ mipLevelCount,
858
+ usage: 6
859
+ })
860
+ );
861
+ for (let mipLevel = 0; mipLevel < mipLevelCount; mipLevel++) {
862
+ device.queue.writeTexture(
863
+ { texture, mipLevel },
864
+ level.data,
865
+ { bytesPerRow: level.width * 4, rowsPerImage: level.height },
866
+ { width: level.width, height: level.height, depthOrArrayLayers: 1 }
867
+ );
868
+ if (mipLevel + 1 < mipLevelCount)
869
+ level = nextMip(level, key === "baseColor");
870
+ }
871
+ result[key] = texture.createView();
872
+ }
873
+ return result;
874
+ });
875
+ }
876
+
726
877
  // src/native-scene-renderer.js
727
878
  var STRIDE = 12;
728
879
  var UNIFORM_BYTES = 256;
@@ -738,7 +889,7 @@ var unit = (a) => {
738
889
  return a.map((v) => v / length);
739
890
  };
740
891
  var finiteVector = (a) => Array.isArray(a) && a.length === 3 && a.every(Number.isFinite);
741
- function validateFrame(frame, maxVertices) {
892
+ function validateFrame(frame, maxVertices, materialCount) {
742
893
  const { vertices, camera, time = 0 } = frame ?? {};
743
894
  if (!(vertices instanceof Float32Array) || vertices.length % (STRIDE * 3) !== 0 || vertices.length > maxVertices * STRIDE || !vertices.every(Number.isFinite)) {
744
895
  throw new Error(
@@ -760,6 +911,30 @@ function validateFrame(frame, maxVertices) {
760
911
  "wakes must contain at most four finite [x, z, heading] entries."
761
912
  );
762
913
  }
914
+ const count = vertices.length / STRIDE;
915
+ if (frame.texcoords !== void 0 && (!(frame.texcoords instanceof Float32Array) || frame.texcoords.length !== count * 2 || !frame.texcoords.every(Number.isFinite))) {
916
+ throw new Error(
917
+ "texcoords must contain two finite coordinates per vertex."
918
+ );
919
+ }
920
+ if (frame.surfaces !== void 0) {
921
+ if (!Array.isArray(frame.surfaces) || frame.surfaces.length > 256 || !frame.texcoords) {
922
+ throw new Error(
923
+ "surfaces require texcoords and at most 256 contiguous draw ranges."
924
+ );
925
+ }
926
+ let end = 0;
927
+ for (const range of frame.surfaces) {
928
+ if (!range || range.firstVertex !== end || !Number.isInteger(range.vertexCount) || range.vertexCount <= 0 || range.vertexCount % 3 !== 0 || !Number.isInteger(range.materialIndex) || range.materialIndex < 0 || range.materialIndex >= materialCount) {
929
+ throw new Error(
930
+ "surface ranges must cover triangles in order and reference a valid material."
931
+ );
932
+ }
933
+ end += range.vertexCount;
934
+ }
935
+ if (end !== count)
936
+ throw new Error("surface ranges must cover every vertex exactly once.");
937
+ }
763
938
  return { ...frame, time };
764
939
  }
765
940
  function frameUniforms(frame, width, height, reflected = false) {
@@ -807,6 +982,9 @@ function frameUniforms(frame, width, height, reflected = false) {
807
982
  }
808
983
  async function createNativeSceneRenderer(options = {}) {
809
984
  const maxVertices = options.maxVertices ?? 3e5;
985
+ const materials = options.materials ?? [];
986
+ validateNativeMaterials(materials);
987
+ const materialCount = materials.length;
810
988
  if (!Number.isInteger(maxVertices) || maxVertices < 3 || maxVertices > 6e5) {
811
989
  throw new Error("maxVertices must be an integer between 3 and 600000.");
812
990
  }
@@ -815,6 +993,7 @@ async function createNativeSceneRenderer(options = {}) {
815
993
  let available = true;
816
994
  let targets = null;
817
995
  let vertexBuffer = null;
996
+ let uvBuffer = null;
818
997
  let vertexCapacity = 0;
819
998
  let pendingFrame = null;
820
999
  let submittedFrames = 0;
@@ -921,7 +1100,20 @@ async function createNativeSceneRenderer(options = {}) {
921
1100
  if (p !== shadowPipeline) pass2.setBindGroup(1, shadowGroup);
922
1101
  if (vertexCount > 0) {
923
1102
  pass2.setVertexBuffer(0, vertexBuffer);
924
- pass2.draw(vertexCount);
1103
+ if (p === shadowPipeline) {
1104
+ pass2.draw(vertexCount);
1105
+ } else {
1106
+ pass2.setVertexBuffer(1, uvBuffer);
1107
+ if (pendingFrame.surfaces) {
1108
+ for (const range of pendingFrame.surfaces) {
1109
+ pass2.setBindGroup(2, materialGroups[range.materialIndex + 1]);
1110
+ pass2.draw(range.vertexCount, 1, range.firstVertex);
1111
+ }
1112
+ } else {
1113
+ pass2.setBindGroup(2, materialGroups[0]);
1114
+ pass2.draw(vertexCount);
1115
+ }
1116
+ }
925
1117
  }
926
1118
  };
927
1119
  const shadowPass = encoder.beginRenderPass({
@@ -967,7 +1159,8 @@ async function createNativeSceneRenderer(options = {}) {
967
1159
  if (pendingFrame.water !== false) {
968
1160
  pass.setPipeline(waterPipeline);
969
1161
  pass.setBindGroup(1, shadowGroup);
970
- pass.setBindGroup(2, targets.reflectionGroup);
1162
+ pass.setBindGroup(2, materialGroups[0]);
1163
+ pass.setBindGroup(3, targets.reflectionGroup);
971
1164
  pass.draw(3);
972
1165
  }
973
1166
  drawGeometry(pass, surfacePipeline, mainGroup);
@@ -1017,6 +1210,63 @@ async function createNativeSceneRenderer(options = {}) {
1017
1210
  { binding: 1, visibility: 2, sampler: { type: "filtering" } }
1018
1211
  ]
1019
1212
  });
1213
+ const materialLayout = device.createBindGroupLayout({
1214
+ entries: [
1215
+ ...[0, 1, 2, 5].map((binding) => ({
1216
+ binding,
1217
+ visibility: 2,
1218
+ texture: { sampleType: "float" }
1219
+ })),
1220
+ { binding: 3, visibility: 2, sampler: { type: "filtering" } },
1221
+ {
1222
+ binding: 4,
1223
+ visibility: 2,
1224
+ buffer: { type: "uniform", minBindingSize: 16 }
1225
+ }
1226
+ ]
1227
+ });
1228
+ const materialSampler = device.createSampler({
1229
+ addressModeU: "repeat",
1230
+ addressModeV: "repeat",
1231
+ minFilter: "linear",
1232
+ magFilter: "linear",
1233
+ mipmapFilter: "linear",
1234
+ maxAnisotropy: 4
1235
+ });
1236
+ const materialGroups = createNativeMaterialTextures(
1237
+ device,
1238
+ materials,
1239
+ own
1240
+ ).map((material) => {
1241
+ const params = own(
1242
+ device.createBuffer({
1243
+ size: 16,
1244
+ usage: 72,
1245
+ label: "native.material-parameters"
1246
+ })
1247
+ );
1248
+ device.queue.writeBuffer(
1249
+ params,
1250
+ 0,
1251
+ new Float32Array([
1252
+ material.normalScale,
1253
+ material.authored ? 1 : 0,
1254
+ material.clearcoat,
1255
+ material.clearcoatRoughness
1256
+ ])
1257
+ );
1258
+ return device.createBindGroup({
1259
+ layout: materialLayout,
1260
+ entries: [
1261
+ { binding: 0, resource: material.baseColor },
1262
+ { binding: 1, resource: material.normal },
1263
+ { binding: 2, resource: material.orm },
1264
+ { binding: 3, resource: materialSampler },
1265
+ { binding: 4, resource: { buffer: params } },
1266
+ { binding: 5, resource: material.clearcoatMap }
1267
+ ]
1268
+ });
1269
+ });
1020
1270
  const uniformBuffer = own(
1021
1271
  device.createBuffer({
1022
1272
  label: "native.frame",
@@ -1081,7 +1331,19 @@ async function createNativeSceneRenderer(options = {}) {
1081
1331
  {
1082
1332
  arrayStride: STRIDE * 4,
1083
1333
  attributes: vertex === "shadowVertex" ? attributes.slice(0, 1) : attributes
1084
- }
1334
+ },
1335
+ ...vertex === "shadowVertex" ? [] : [
1336
+ {
1337
+ arrayStride: 8,
1338
+ attributes: [
1339
+ {
1340
+ shaderLocation: 4,
1341
+ offset: 0,
1342
+ format: "float32x2"
1343
+ }
1344
+ ]
1345
+ }
1346
+ ]
1085
1347
  ]
1086
1348
  } : {}
1087
1349
  },
@@ -1115,7 +1377,7 @@ async function createNativeSceneRenderer(options = {}) {
1115
1377
  "surfaceFragment",
1116
1378
  format,
1117
1379
  4,
1118
- [uniformLayout, shadowLayout],
1380
+ [uniformLayout, shadowLayout, materialLayout],
1119
1381
  true
1120
1382
  );
1121
1383
  const skyPipeline = await pipeline(
@@ -1132,7 +1394,7 @@ async function createNativeSceneRenderer(options = {}) {
1132
1394
  "waterFragment",
1133
1395
  format,
1134
1396
  4,
1135
- [uniformLayout, shadowLayout, reflectionLayout]
1397
+ [uniformLayout, shadowLayout, materialLayout, reflectionLayout]
1136
1398
  );
1137
1399
  const reflectedSurfacePipeline = await pipeline(
1138
1400
  "reflection-surface",
@@ -1140,7 +1402,7 @@ async function createNativeSceneRenderer(options = {}) {
1140
1402
  "reflectionSurface",
1141
1403
  "rgba16float",
1142
1404
  1,
1143
- [uniformLayout, shadowLayout],
1405
+ [uniformLayout, shadowLayout, materialLayout],
1144
1406
  true
1145
1407
  );
1146
1408
  const reflectedSkyPipeline = await pipeline(
@@ -1155,15 +1417,23 @@ async function createNativeSceneRenderer(options = {}) {
1155
1417
  render(input) {
1156
1418
  if (destroyed) throw new Error("Native scene renderer was destroyed.");
1157
1419
  if (!available) throw new Error("WebGPU device was lost.");
1158
- pendingFrame = validateFrame(input, maxVertices);
1420
+ pendingFrame = validateFrame(input, maxVertices, materialCount);
1159
1421
  const data = pendingFrame.vertices;
1160
1422
  ensureTargets();
1161
1423
  if (data.byteLength > vertexCapacity) {
1162
1424
  if (vertexBuffer) release(vertexBuffer);
1425
+ if (uvBuffer) release(uvBuffer);
1163
1426
  vertexCapacity = Math.min(
1164
1427
  maxVertices * STRIDE * 4,
1165
1428
  Math.max(4096, 2 ** Math.ceil(Math.log2(data.byteLength)))
1166
1429
  );
1430
+ uvBuffer = own(
1431
+ device.createBuffer({
1432
+ label: "native.surface-uvs",
1433
+ size: Math.ceil(vertexCapacity / (STRIDE * 4)) * 8,
1434
+ usage: 40
1435
+ })
1436
+ );
1167
1437
  vertexBuffer = own(
1168
1438
  device.createBuffer({
1169
1439
  label: "native.surface-vertices",
@@ -1172,6 +1442,8 @@ async function createNativeSceneRenderer(options = {}) {
1172
1442
  })
1173
1443
  );
1174
1444
  }
1445
+ if (pendingFrame.texcoords?.byteLength)
1446
+ device.queue.writeBuffer(uvBuffer, 0, pendingFrame.texcoords);
1175
1447
  vertexCount = data.length / STRIDE;
1176
1448
  if (data.byteLength) device.queue.writeBuffer(vertexBuffer, 0, data);
1177
1449
  device.queue.writeBuffer(
@@ -3856,8 +4128,8 @@ function hashUint32(value) {
3856
4128
  x = ((x >>> 16 ^ x) >>> 0) * 73244475 >>> 0;
3857
4129
  return (x >>> 16 ^ x) >>> 0;
3858
4130
  }
3859
- function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension) {
3860
- let x = (pixelId >>> 0) * 747796405 ^ (sampleId >>> 0) * 2891336453 ^ (bounce >>> 0) * 277803737 ^ (frameIndex >>> 0) * 1442695041 ^ (dimension >>> 0) * 1597334677;
4131
+ function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension2) {
4132
+ let x = (pixelId >>> 0) * 747796405 ^ (sampleId >>> 0) * 2891336453 ^ (bounce >>> 0) * 277803737 ^ (frameIndex >>> 0) * 1442695041 ^ (dimension2 >>> 0) * 1597334677;
3861
4133
  x >>>= 0;
3862
4134
  x ^= x >>> 16;
3863
4135
  x = x * 2146121005 >>> 0;
@@ -9736,25 +10008,25 @@ function toWgslConstName(name) {
9736
10008
  return `SAMPLE_DIM_${name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase()}`;
9737
10009
  }
9738
10010
  var usedDimensions = /* @__PURE__ */ new Set();
9739
- for (const [, dimension] of sampleDimensionEntries) {
9740
- if (usedDimensions.has(dimension)) {
9741
- throw new Error(`Duplicate wavefront sample dimension ${dimension}.`);
10011
+ for (const [, dimension2] of sampleDimensionEntries) {
10012
+ if (usedDimensions.has(dimension2)) {
10013
+ throw new Error(`Duplicate wavefront sample dimension ${dimension2}.`);
9742
10014
  }
9743
- usedDimensions.add(dimension);
10015
+ usedDimensions.add(dimension2);
9744
10016
  }
9745
10017
  var WAVEFRONT_SAMPLE_DIMENSIONS = Object.freeze(
9746
10018
  Object.fromEntries(sampleDimensionEntries)
9747
10019
  );
9748
10020
  function listWavefrontSampleDimensions() {
9749
10021
  return sampleDimensionEntries.map(
9750
- ([name, dimension]) => Object.freeze({
10022
+ ([name, dimension2]) => Object.freeze({
9751
10023
  name,
9752
- dimension,
10024
+ dimension: dimension2,
9753
10025
  wgslName: toWgslConstName(name)
9754
10026
  })
9755
10027
  );
9756
10028
  }
9757
- var WAVEFRONT_SAMPLE_DIMENSIONS_WGSL = listWavefrontSampleDimensions().map(({ wgslName, dimension }) => `const ${wgslName}: u32 = ${dimension}u;`).join("\n");
10029
+ var WAVEFRONT_SAMPLE_DIMENSIONS_WGSL = listWavefrontSampleDimensions().map(({ wgslName, dimension: dimension2 }) => `const ${wgslName}: u32 = ${dimension2}u;`).join("\n");
9758
10030
  function fract(value) {
9759
10031
  return value - Math.floor(value);
9760
10032
  }
@@ -9767,19 +10039,19 @@ function radicalInverseVdc2(value) {
9767
10039
  bits = ((bits & 16711935) << 8 | (bits & 4278255360) >>> 8) >>> 0;
9768
10040
  return bits / 4294967296;
9769
10041
  }
9770
- function sampleWavefrontDimension1D(pixelId, sampleId, bounce, frameIndex, dimension) {
9771
- return random01FromSeed(mixSeed(pixelId, sampleId, bounce, frameIndex, dimension));
10042
+ function sampleWavefrontDimension1D(pixelId, sampleId, bounce, frameIndex, dimension2) {
10043
+ return random01FromSeed(mixSeed(pixelId, sampleId, bounce, frameIndex, dimension2));
9772
10044
  }
9773
- function sampleWavefrontDimension2D(pixelId, sampleId, bounce, frameIndex, dimension, strataCount) {
10045
+ function sampleWavefrontDimension2D(pixelId, sampleId, bounce, frameIndex, dimension2, strataCount) {
9774
10046
  const strata = Math.max(1, Number.isFinite(strataCount) ? Math.trunc(strataCount) : 1);
9775
10047
  const jitter = sampleWavefrontDimension1D(
9776
10048
  pixelId,
9777
10049
  sampleId,
9778
10050
  bounce,
9779
10051
  frameIndex,
9780
- dimension
10052
+ dimension2
9781
10053
  );
9782
- const scramble = hashUint32(mixSeed(pixelId, sampleId, bounce, frameIndex, dimension));
10054
+ const scramble = hashUint32(mixSeed(pixelId, sampleId, bounce, frameIndex, dimension2));
9783
10055
  const stratified = fract(((sampleId >>> 0) % strata + jitter) / strata);
9784
10056
  const lowDiscrepancy = fract(radicalInverseVdc2(sampleId >>> 0 ^ scramble) + jitter);
9785
10057
  return Object.freeze([stratified, lowDiscrepancy]);
@@ -14257,6 +14529,7 @@ export {
14257
14529
  supportsWebGpu,
14258
14530
  traceWavefrontReferenceTriangles,
14259
14531
  transitionMediumStack,
14532
+ validateNativeMaterials as validateNativeSurfaceMaterials,
14260
14533
  wavefrontMaterialKinds,
14261
14534
  wavefrontPathTracingComputeLimits,
14262
14535
  wavefrontSceneObjectKinds