@plasius/gpu-shared 0.1.1 → 0.1.3
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 +35 -0
- package/README.md +19 -1
- package/dist/index.cjs +469 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{showcase-runtime-5H44EZXD.js → showcase-runtime-V72XV55N.js} +469 -49
- package/dist/showcase-runtime-V72XV55N.js.map +1 -0
- package/package.json +7 -7
- package/src/index.d.ts +1 -0
- package/src/showcase-runtime.js +420 -52
- package/dist/showcase-runtime-5H44EZXD.js.map +0 -1
package/src/showcase-runtime.js
CHANGED
|
@@ -597,7 +597,163 @@ function buildDemoDom(root, options) {
|
|
|
597
597
|
};
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
-
function
|
|
600
|
+
function buildSceneSnapshot(state, shipModel) {
|
|
601
|
+
return Object.freeze({
|
|
602
|
+
focus: state.focus,
|
|
603
|
+
frame: state.frame,
|
|
604
|
+
time: state.time,
|
|
605
|
+
stress: state.stress,
|
|
606
|
+
collisions: state.contactCount,
|
|
607
|
+
collisionCount: state.collisionCount,
|
|
608
|
+
collisionFlash: state.collisionFlash,
|
|
609
|
+
sprays: Object.freeze(
|
|
610
|
+
state.sprays.map((spray) =>
|
|
611
|
+
Object.freeze({
|
|
612
|
+
life: spray.life,
|
|
613
|
+
position: Object.freeze({ ...spray.position }),
|
|
614
|
+
velocity: Object.freeze({ ...spray.velocity }),
|
|
615
|
+
})
|
|
616
|
+
)
|
|
617
|
+
),
|
|
618
|
+
ships: Object.freeze(
|
|
619
|
+
state.ships.map((ship) =>
|
|
620
|
+
Object.freeze({
|
|
621
|
+
id: ship.id,
|
|
622
|
+
position: Object.freeze({ ...ship.position }),
|
|
623
|
+
velocity: Object.freeze({ ...ship.velocity }),
|
|
624
|
+
rotationY: ship.rotationY,
|
|
625
|
+
angularVelocity: ship.angularVelocity,
|
|
626
|
+
tint: Object.freeze({ ...ship.tint }),
|
|
627
|
+
})
|
|
628
|
+
)
|
|
629
|
+
),
|
|
630
|
+
waveImpulses: Object.freeze(
|
|
631
|
+
state.waveImpulses.map((impulse) =>
|
|
632
|
+
Object.freeze({
|
|
633
|
+
x: impulse.x,
|
|
634
|
+
z: impulse.z,
|
|
635
|
+
strength: impulse.strength,
|
|
636
|
+
radius: impulse.radius,
|
|
637
|
+
life: impulse.life,
|
|
638
|
+
})
|
|
639
|
+
)
|
|
640
|
+
),
|
|
641
|
+
physics: Object.freeze({
|
|
642
|
+
profile: state.physics.profile,
|
|
643
|
+
plan: state.physics.plan,
|
|
644
|
+
manifest: state.physics.manifest,
|
|
645
|
+
snapshot: state.physics.snapshot,
|
|
646
|
+
shipPhysics: shipModel?.physics ?? null,
|
|
647
|
+
}),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function resolveSceneDescription(state, options, shipModel) {
|
|
652
|
+
const scene = buildSceneSnapshot(state, shipModel);
|
|
653
|
+
if (typeof options.describeState !== "function") {
|
|
654
|
+
return { scene, description: null };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const description = options.describeState(state.packageState, scene) ?? null;
|
|
658
|
+
return { scene, description };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function updatePackageState(state, options, shipModel, dt) {
|
|
662
|
+
if (typeof options.updateState !== "function") {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const scene = buildSceneSnapshot(state, shipModel);
|
|
667
|
+
const nextState = options.updateState(state.packageState, scene, dt);
|
|
668
|
+
if (typeof nextState !== "undefined") {
|
|
669
|
+
state.packageState = nextState;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function normalizeColorOverride(color, fallback) {
|
|
674
|
+
if (!color || typeof color !== "object") {
|
|
675
|
+
return fallback;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
r: typeof color.r === "number" ? color.r : fallback.r,
|
|
680
|
+
g: typeof color.g === "number" ? color.g : fallback.g,
|
|
681
|
+
b: typeof color.b === "number" ? color.b : fallback.b,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function readVisualNumber(value, fallback) {
|
|
686
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
|
|
690
|
+
const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
|
|
691
|
+
const defaults = {
|
|
692
|
+
skyTop: premiumShadows ? "#f0f7fb" : "#e8f1f7",
|
|
693
|
+
skyMid: premiumShadows ? "#c7d9e5" : "#b9ceda",
|
|
694
|
+
skyBottom: premiumShadows ? "#84a7bd" : "#7b9bb0",
|
|
695
|
+
seaTop: premiumShadows ? "#235064" : "#264c5f",
|
|
696
|
+
seaMid: premiumShadows ? "#153e53" : "#173d4f",
|
|
697
|
+
seaBottom: "#0b2433",
|
|
698
|
+
sunCore: "rgba(255, 244, 210, 0.9)",
|
|
699
|
+
reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
|
|
700
|
+
shadowAccent: lightingSnapshot.currentLevel.config.shadowStrength,
|
|
701
|
+
waveAmplitude: 1,
|
|
702
|
+
waveDirection: { x: 0.86, z: 0.34 },
|
|
703
|
+
wavePhaseSpeed: 1,
|
|
704
|
+
wakeStrength: 0.24,
|
|
705
|
+
wakeLength: 15,
|
|
706
|
+
collisionRippleStrength: 0.34,
|
|
707
|
+
waterNear: { r: 0.12, g: 0.36, b: 0.46 },
|
|
708
|
+
waterFar: { r: 0.28, g: 0.56, b: 0.68 },
|
|
709
|
+
harborWall: { r: 0.48, g: 0.4, b: 0.32 },
|
|
710
|
+
harborDeck: { r: 0.5, g: 0.34, b: 0.22 },
|
|
711
|
+
harborTower: { r: 0.34, g: 0.32, b: 0.36 },
|
|
712
|
+
flagColor: { r: 0.76, g: 0.24, b: 0.18 },
|
|
713
|
+
flagMotion: 1,
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
skyTop: typeof customVisuals.skyTop === "string" ? customVisuals.skyTop : defaults.skyTop,
|
|
718
|
+
skyMid: typeof customVisuals.skyMid === "string" ? customVisuals.skyMid : defaults.skyMid,
|
|
719
|
+
skyBottom:
|
|
720
|
+
typeof customVisuals.skyBottom === "string" ? customVisuals.skyBottom : defaults.skyBottom,
|
|
721
|
+
seaTop: typeof customVisuals.seaTop === "string" ? customVisuals.seaTop : defaults.seaTop,
|
|
722
|
+
seaMid: typeof customVisuals.seaMid === "string" ? customVisuals.seaMid : defaults.seaMid,
|
|
723
|
+
seaBottom:
|
|
724
|
+
typeof customVisuals.seaBottom === "string" ? customVisuals.seaBottom : defaults.seaBottom,
|
|
725
|
+
sunCore:
|
|
726
|
+
typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.sunCore,
|
|
727
|
+
reflectionStrength: readVisualNumber(
|
|
728
|
+
customVisuals.reflectionStrength,
|
|
729
|
+
defaults.reflectionStrength
|
|
730
|
+
),
|
|
731
|
+
shadowAccent: readVisualNumber(customVisuals.shadowAccent, defaults.shadowAccent),
|
|
732
|
+
waveAmplitude: readVisualNumber(customVisuals.waveAmplitude, defaults.waveAmplitude),
|
|
733
|
+
waveDirection:
|
|
734
|
+
customVisuals.waveDirection &&
|
|
735
|
+
typeof customVisuals.waveDirection.x === "number" &&
|
|
736
|
+
typeof customVisuals.waveDirection.z === "number"
|
|
737
|
+
? { x: customVisuals.waveDirection.x, z: customVisuals.waveDirection.z }
|
|
738
|
+
: defaults.waveDirection,
|
|
739
|
+
wavePhaseSpeed: readVisualNumber(customVisuals.wavePhaseSpeed, defaults.wavePhaseSpeed),
|
|
740
|
+
wakeStrength: readVisualNumber(customVisuals.wakeStrength, defaults.wakeStrength),
|
|
741
|
+
wakeLength: readVisualNumber(customVisuals.wakeLength, defaults.wakeLength),
|
|
742
|
+
collisionRippleStrength: readVisualNumber(
|
|
743
|
+
customVisuals.collisionRippleStrength,
|
|
744
|
+
defaults.collisionRippleStrength
|
|
745
|
+
),
|
|
746
|
+
waterNear: normalizeColorOverride(customVisuals.waterNear, defaults.waterNear),
|
|
747
|
+
waterFar: normalizeColorOverride(customVisuals.waterFar, defaults.waterFar),
|
|
748
|
+
harborWall: normalizeColorOverride(customVisuals.harborWall, defaults.harborWall),
|
|
749
|
+
harborDeck: normalizeColorOverride(customVisuals.harborDeck, defaults.harborDeck),
|
|
750
|
+
harborTower: normalizeColorOverride(customVisuals.harborTower, defaults.harborTower),
|
|
751
|
+
flagColor: normalizeColorOverride(customVisuals.flagColor, defaults.flagColor),
|
|
752
|
+
flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function buildClothSurface(model, state, meshDetail, visuals) {
|
|
601
757
|
const clothPlan = createClothRepresentationPlan({
|
|
602
758
|
garmentId: "shore-flag",
|
|
603
759
|
kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
|
|
@@ -626,12 +782,23 @@ function buildClothSurface(model, state, meshDetail) {
|
|
|
626
782
|
for (let column = 0; column < cols; column += 1) {
|
|
627
783
|
const u = column / (cols - 1);
|
|
628
784
|
const v = row / (rows - 1);
|
|
629
|
-
const gust =
|
|
630
|
-
|
|
785
|
+
const gust =
|
|
786
|
+
Math.sin(time * 1.9 + v * 3.2 + u * 2.1) *
|
|
787
|
+
continuity.broadMotionFloor *
|
|
788
|
+
visuals.flagMotion;
|
|
789
|
+
const wrinkle =
|
|
790
|
+
Math.sin(time * 4.4 + u * 9.2 + v * 5.6) *
|
|
791
|
+
continuity.wrinkleFloor *
|
|
792
|
+
0.22 *
|
|
793
|
+
Math.max(0.55, visuals.flagMotion);
|
|
631
794
|
const x = origin.x + u * 1.8 + gust * 0.55 * (u * 0.9);
|
|
632
795
|
const y = origin.y - height * v + wrinkle * 0.2;
|
|
633
796
|
const z = origin.z + width * u + gust * 0.72 * (u * 0.85);
|
|
634
|
-
const flap =
|
|
797
|
+
const flap =
|
|
798
|
+
Math.cos(time * 2.7 + u * 7.4 + v * 3.8) *
|
|
799
|
+
continuity.broadMotionFloor *
|
|
800
|
+
0.28 *
|
|
801
|
+
visuals.flagMotion;
|
|
635
802
|
positions.push(vec3(x + flap, y, z));
|
|
636
803
|
}
|
|
637
804
|
}
|
|
@@ -651,21 +818,102 @@ function buildClothSurface(model, state, meshDetail) {
|
|
|
651
818
|
band,
|
|
652
819
|
representation,
|
|
653
820
|
continuity,
|
|
821
|
+
color: visuals.flagColor,
|
|
654
822
|
positions,
|
|
655
823
|
indices,
|
|
656
824
|
grid: { rows, cols },
|
|
657
825
|
};
|
|
658
826
|
}
|
|
659
827
|
|
|
660
|
-
function
|
|
828
|
+
function resolveWaveDirection(state) {
|
|
829
|
+
const direction = state.demoVisuals?.waveDirection;
|
|
830
|
+
if (
|
|
831
|
+
direction &&
|
|
832
|
+
typeof direction === "object" &&
|
|
833
|
+
typeof direction.x === "number" &&
|
|
834
|
+
typeof direction.z === "number"
|
|
835
|
+
) {
|
|
836
|
+
return normalizeVec3(vec3(direction.x, 0, direction.z));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return normalizeVec3(vec3(0.86, 0, 0.34));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function sampleShipWake(state, x, z, time) {
|
|
843
|
+
const wakeStrength = readVisualNumber(state.demoVisuals?.wakeStrength, 0.24);
|
|
844
|
+
const wakeLength = readVisualNumber(state.demoVisuals?.wakeLength, 15);
|
|
845
|
+
let total = 0;
|
|
846
|
+
|
|
847
|
+
for (const ship of state.ships) {
|
|
848
|
+
const speed = Math.hypot(ship.velocity.x, ship.velocity.z);
|
|
849
|
+
if (speed <= 0.05) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const direction = normalizeVec3(vec3(ship.velocity.x, 0, ship.velocity.z));
|
|
854
|
+
const behind = scaleVec3(direction, -1);
|
|
855
|
+
const lateral = vec3(-direction.z, 0, direction.x);
|
|
856
|
+
const delta = vec3(x - ship.position.x, 0, z - ship.position.z);
|
|
857
|
+
const along = dotVec3(delta, behind);
|
|
858
|
+
if (along < 0 || along > wakeLength) {
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const cross = Math.abs(dotVec3(delta, lateral));
|
|
863
|
+
const width = 0.9 + along * 0.2;
|
|
864
|
+
if (cross > width * 3.2) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const envelope =
|
|
869
|
+
Math.exp(-along * 0.14) * Math.exp(-((cross * cross) / Math.max(0.4, width * width * 2.4)));
|
|
870
|
+
total += Math.sin(along * 1.6 - time * 4.2) * speed * wakeStrength * envelope;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return total;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function sampleWaveImpulses(state, x, z, time) {
|
|
877
|
+
const rippleStrength = readVisualNumber(state.demoVisuals?.collisionRippleStrength, 0.34);
|
|
878
|
+
let total = 0;
|
|
879
|
+
|
|
880
|
+
for (const impulse of state.waveImpulses) {
|
|
881
|
+
const dx = x - impulse.x;
|
|
882
|
+
const dz = z - impulse.z;
|
|
883
|
+
const distance = Math.hypot(dx, dz);
|
|
884
|
+
const radius = impulse.radius + (1 - impulse.life) * 4.8;
|
|
885
|
+
if (distance > radius * 2.8) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const phase = distance * 1.8 - (1 - impulse.life) * 10 - time * 0.4;
|
|
890
|
+
const envelope = Math.exp(-distance / Math.max(0.1, radius)) * impulse.life;
|
|
891
|
+
total += Math.sin(phase) * impulse.strength * rippleStrength * envelope * 0.18;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return total;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function sampleWave(state, x, z, time) {
|
|
898
|
+
const direction = resolveWaveDirection(state);
|
|
899
|
+
const lateral = vec3(-direction.z, 0, direction.x);
|
|
900
|
+
const along = x * direction.x + z * direction.z;
|
|
901
|
+
const cross = x * lateral.x + z * lateral.z;
|
|
902
|
+
const phaseSpeed = readVisualNumber(state.demoVisuals?.wavePhaseSpeed, 1);
|
|
903
|
+
const amplitude = readVisualNumber(state.demoVisuals?.waveAmplitude, 1);
|
|
904
|
+
const base =
|
|
905
|
+
Math.sin(along * 0.22 - time * 1.12 * phaseSpeed) * 0.42 +
|
|
906
|
+
Math.cos(along * 0.11 + cross * 0.07 - time * 0.78 * phaseSpeed) * 0.26 +
|
|
907
|
+
Math.sin(cross * 0.19 - time * 1.34 * phaseSpeed) * 0.16;
|
|
908
|
+
|
|
661
909
|
return (
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
910
|
+
base * amplitude +
|
|
911
|
+
sampleShipWake(state, x, z, time) +
|
|
912
|
+
sampleWaveImpulses(state, x, z, time)
|
|
665
913
|
);
|
|
666
914
|
}
|
|
667
915
|
|
|
668
|
-
function buildWaterBands(state, fluidDetail) {
|
|
916
|
+
function buildWaterBands(state, fluidDetail, visuals) {
|
|
669
917
|
const fluidPlan = createFluidRepresentationPlan({
|
|
670
918
|
fluidBodyId: "harbor",
|
|
671
919
|
kind: state.focus === "fluid" ? "ocean" : fluidBodyKinds[0],
|
|
@@ -711,7 +959,7 @@ function buildWaterBands(state, fluidDetail) {
|
|
|
711
959
|
const z = originZ + bandSpec.depth * v;
|
|
712
960
|
const y =
|
|
713
961
|
bandSpec.y +
|
|
714
|
-
sampleWave(x, z, state.time) *
|
|
962
|
+
sampleWave(state, x, z, state.time) *
|
|
715
963
|
continuity.amplitudeFloor *
|
|
716
964
|
(bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
|
|
717
965
|
positions.push(vec3(x, y, z));
|
|
@@ -737,12 +985,20 @@ function buildWaterBands(state, fluidDetail) {
|
|
|
737
985
|
indices,
|
|
738
986
|
color:
|
|
739
987
|
bandSpec.band === "near"
|
|
740
|
-
?
|
|
988
|
+
? visuals.waterNear
|
|
741
989
|
: bandSpec.band === "mid"
|
|
742
|
-
? {
|
|
990
|
+
? {
|
|
991
|
+
r: mix(visuals.waterNear.r, visuals.waterFar.r, 0.4),
|
|
992
|
+
g: mix(visuals.waterNear.g, visuals.waterFar.g, 0.4),
|
|
993
|
+
b: mix(visuals.waterNear.b, visuals.waterFar.b, 0.4),
|
|
994
|
+
}
|
|
743
995
|
: bandSpec.band === "far"
|
|
744
|
-
?
|
|
745
|
-
: {
|
|
996
|
+
? visuals.waterFar
|
|
997
|
+
: {
|
|
998
|
+
r: mix(visuals.waterFar.r, 0.76, 0.2),
|
|
999
|
+
g: mix(visuals.waterFar.g, 0.78, 0.2),
|
|
1000
|
+
b: mix(visuals.waterFar.b, 0.82, 0.2),
|
|
1001
|
+
},
|
|
746
1002
|
});
|
|
747
1003
|
}
|
|
748
1004
|
|
|
@@ -784,6 +1040,9 @@ function createSceneState(options) {
|
|
|
784
1040
|
clothDetail,
|
|
785
1041
|
lightingDetail,
|
|
786
1042
|
debugSession,
|
|
1043
|
+
packageState: undefined,
|
|
1044
|
+
demoDescription: null,
|
|
1045
|
+
demoVisuals: null,
|
|
787
1046
|
time: 0,
|
|
788
1047
|
lastTimeMs: null,
|
|
789
1048
|
paused: false,
|
|
@@ -811,7 +1070,9 @@ function createSceneState(options) {
|
|
|
811
1070
|
},
|
|
812
1071
|
],
|
|
813
1072
|
sprays: [],
|
|
1073
|
+
waveImpulses: [],
|
|
814
1074
|
frame: 0,
|
|
1075
|
+
contactCount: 0,
|
|
815
1076
|
collisionCount: 0,
|
|
816
1077
|
collisionFlash: 0,
|
|
817
1078
|
physics: {
|
|
@@ -828,26 +1089,26 @@ function setListContent(element, values) {
|
|
|
828
1089
|
element.innerHTML = values.map((value) => `<li>${value}</li>`).join("");
|
|
829
1090
|
}
|
|
830
1091
|
|
|
831
|
-
function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength) {
|
|
1092
|
+
function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength, visuals) {
|
|
832
1093
|
const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
|
|
833
1094
|
const sky = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.5);
|
|
834
|
-
sky.addColorStop(0,
|
|
835
|
-
sky.addColorStop(0.6,
|
|
836
|
-
sky.addColorStop(1,
|
|
1095
|
+
sky.addColorStop(0, visuals.skyTop);
|
|
1096
|
+
sky.addColorStop(0.6, visuals.skyMid);
|
|
1097
|
+
sky.addColorStop(1, visuals.skyBottom);
|
|
837
1098
|
ctx.fillStyle = sky;
|
|
838
1099
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
839
1100
|
|
|
840
1101
|
const shoreline = ctx.createLinearGradient(0, canvas.height * 0.45, 0, canvas.height);
|
|
841
|
-
shoreline.addColorStop(0,
|
|
842
|
-
shoreline.addColorStop(0.52,
|
|
843
|
-
shoreline.addColorStop(1,
|
|
1102
|
+
shoreline.addColorStop(0, visuals.seaTop);
|
|
1103
|
+
shoreline.addColorStop(0.52, visuals.seaMid);
|
|
1104
|
+
shoreline.addColorStop(1, visuals.seaBottom);
|
|
844
1105
|
ctx.fillStyle = shoreline;
|
|
845
1106
|
ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
|
|
846
1107
|
|
|
847
1108
|
const sunX = mix(canvas.width * 0.16, canvas.width * 0.84, (Math.sin(state.time * 0.12) + 1) * 0.5);
|
|
848
1109
|
const sunY = canvas.height * 0.14 + Math.cos(state.time * 0.12) * 22;
|
|
849
1110
|
const sun = ctx.createRadialGradient(sunX, sunY, 10, sunX, sunY, 90);
|
|
850
|
-
sun.addColorStop(0,
|
|
1111
|
+
sun.addColorStop(0, visuals.sunCore);
|
|
851
1112
|
sun.addColorStop(1, "rgba(255, 244, 210, 0)");
|
|
852
1113
|
ctx.fillStyle = sun;
|
|
853
1114
|
ctx.beginPath();
|
|
@@ -934,27 +1195,27 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
|
|
|
934
1195
|
ctx.restore();
|
|
935
1196
|
}
|
|
936
1197
|
|
|
937
|
-
function pushHarborGeometry(camera, viewport, triangles) {
|
|
1198
|
+
function pushHarborGeometry(camera, viewport, triangles, visuals) {
|
|
938
1199
|
const harborObjects = [
|
|
939
1200
|
{
|
|
940
1201
|
position: vec3(-8.2, 1.1, -0.9),
|
|
941
1202
|
rotationY: -0.16,
|
|
942
1203
|
scale: { x: 5.4, y: 2.4, z: 4.2 },
|
|
943
|
-
color:
|
|
1204
|
+
color: visuals.harborWall,
|
|
944
1205
|
accent: 0.06,
|
|
945
1206
|
},
|
|
946
1207
|
{
|
|
947
1208
|
position: vec3(-5.7, 0.45, 1.4),
|
|
948
1209
|
rotationY: -0.08,
|
|
949
1210
|
scale: { x: 6.8, y: 0.3, z: 2.1 },
|
|
950
|
-
color:
|
|
1211
|
+
color: visuals.harborDeck,
|
|
951
1212
|
accent: 0.04,
|
|
952
1213
|
},
|
|
953
1214
|
{
|
|
954
1215
|
position: vec3(-10.4, 0.28, 0.8),
|
|
955
1216
|
rotationY: 0.22,
|
|
956
1217
|
scale: { x: 1.2, y: 0.9, z: 1.2 },
|
|
957
|
-
color:
|
|
1218
|
+
color: visuals.harborTower,
|
|
958
1219
|
accent: 0.02,
|
|
959
1220
|
},
|
|
960
1221
|
];
|
|
@@ -1045,7 +1306,7 @@ function renderClothAccent(ctx, cloth, camera, viewport) {
|
|
|
1045
1306
|
cloth.grid.rows * cloth.grid.cols - 1,
|
|
1046
1307
|
(cloth.grid.rows - 1) * cloth.grid.cols,
|
|
1047
1308
|
];
|
|
1048
|
-
ctx.fillStyle =
|
|
1309
|
+
ctx.fillStyle = colorToRgba(cloth.color, 0.95);
|
|
1049
1310
|
for (const index of borderIndices) {
|
|
1050
1311
|
const point = projected[index];
|
|
1051
1312
|
if (!point) {
|
|
@@ -1109,12 +1370,15 @@ function updateShips(state, dt, shipModel) {
|
|
|
1109
1370
|
const physics = shipModel.physics;
|
|
1110
1371
|
const halfExtents = physics.halfExtents ?? [1.35, 0.95, 3.9];
|
|
1111
1372
|
let collided = false;
|
|
1373
|
+
state.contactCount = 0;
|
|
1112
1374
|
for (const ship of state.ships) {
|
|
1113
1375
|
ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
|
|
1114
1376
|
ship.rotationY += ship.angularVelocity * dt;
|
|
1115
1377
|
ship.velocity = scaleVec3(ship.velocity, 1 - (physics.linearDamping ?? 0.04) * dt);
|
|
1116
1378
|
ship.angularVelocity *= 1 - (physics.angularDamping ?? 0.08) * dt;
|
|
1117
|
-
ship.position.y =
|
|
1379
|
+
ship.position.y =
|
|
1380
|
+
sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.22 +
|
|
1381
|
+
(physics.waterline ?? 0.42);
|
|
1118
1382
|
if (Math.abs(ship.position.x) > 10) {
|
|
1119
1383
|
ship.velocity.x *= -1;
|
|
1120
1384
|
ship.angularVelocity *= -1;
|
|
@@ -1142,12 +1406,29 @@ function updateShips(state, dt, shipModel) {
|
|
|
1142
1406
|
b.angularVelocity -= 0.55;
|
|
1143
1407
|
const contactPoint = vec3((a.position.x + b.position.x) * 0.5, (a.position.y + b.position.y) * 0.5 + 0.1, (a.position.z + b.position.z) * 0.5);
|
|
1144
1408
|
spawnSpray(state, contactPoint, Math.abs(dx) + Math.abs(dz));
|
|
1409
|
+
state.waveImpulses.push({
|
|
1410
|
+
x: contactPoint.x,
|
|
1411
|
+
z: contactPoint.z,
|
|
1412
|
+
strength: Math.min(1.4, 0.2 + (Math.abs(dx) + Math.abs(dz)) * 0.18),
|
|
1413
|
+
radius: 0.8,
|
|
1414
|
+
life: 1,
|
|
1415
|
+
});
|
|
1145
1416
|
state.collisionCount += 1;
|
|
1417
|
+
state.contactCount = 1;
|
|
1146
1418
|
collided = true;
|
|
1147
1419
|
}
|
|
1148
1420
|
state.collisionFlash = collided ? 1 : Math.max(0, state.collisionFlash - dt * 1.8);
|
|
1149
1421
|
}
|
|
1150
1422
|
|
|
1423
|
+
function updateWaveImpulses(state, dt) {
|
|
1424
|
+
state.waveImpulses = state.waveImpulses
|
|
1425
|
+
.map((impulse) => ({
|
|
1426
|
+
...impulse,
|
|
1427
|
+
life: impulse.life - dt * 0.55,
|
|
1428
|
+
}))
|
|
1429
|
+
.filter((impulse) => impulse.life > 0);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1151
1432
|
function updateSpray(state, dt) {
|
|
1152
1433
|
state.sprays = state.sprays
|
|
1153
1434
|
.map((particle) => {
|
|
@@ -1268,7 +1549,7 @@ function renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDi
|
|
|
1268
1549
|
].map((point) => transformPoint(point, transform));
|
|
1269
1550
|
|
|
1270
1551
|
renderProjectedShadow(ctx, hullCorners, camera, viewport, lightDir, {
|
|
1271
|
-
planeY: sampleWave(ship.position.x, ship.position.z, state.time) * 0.24 - 0.03,
|
|
1552
|
+
planeY: sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 - 0.03,
|
|
1272
1553
|
alpha: 0.08 + shadowStrength * 0.2,
|
|
1273
1554
|
blur: 14 + shadowStrength * 24,
|
|
1274
1555
|
});
|
|
@@ -1300,13 +1581,31 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
1300
1581
|
const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
|
|
1301
1582
|
const lightDir = normalizeVec3(vec3(-0.45, 0.85, -0.24));
|
|
1302
1583
|
const lightingSnapshot = state.lightingDetail.getSnapshot();
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
|
|
1584
|
+
const visuals = resolveVisualConfig(
|
|
1585
|
+
nearLighting,
|
|
1586
|
+
lightingSnapshot,
|
|
1587
|
+
state.demoDescription?.visuals
|
|
1588
|
+
);
|
|
1589
|
+
state.demoVisuals = visuals;
|
|
1590
|
+
const reflectionStrength = visuals.reflectionStrength;
|
|
1591
|
+
const shadowStrength = visuals.shadowAccent;
|
|
1592
|
+
drawSkyAndShore(
|
|
1593
|
+
ctx,
|
|
1594
|
+
canvas,
|
|
1595
|
+
state,
|
|
1596
|
+
nearLighting,
|
|
1597
|
+
reflectionStrength,
|
|
1598
|
+
shadowStrength,
|
|
1599
|
+
visuals
|
|
1600
|
+
);
|
|
1306
1601
|
|
|
1307
1602
|
const triangles = [];
|
|
1308
|
-
pushHarborGeometry(camera, viewport, triangles);
|
|
1309
|
-
const water = buildWaterBands(
|
|
1603
|
+
pushHarborGeometry(camera, viewport, triangles, visuals);
|
|
1604
|
+
const water = buildWaterBands(
|
|
1605
|
+
state,
|
|
1606
|
+
state.fluidDetail.getSnapshot().currentLevel.config,
|
|
1607
|
+
visuals
|
|
1608
|
+
);
|
|
1310
1609
|
for (const bandMesh of water.bandMeshes) {
|
|
1311
1610
|
const bandAccent = bandMesh.band === "near" ? 0.06 : bandMesh.band === "mid" ? 0.04 : 0;
|
|
1312
1611
|
for (let index = 0; index < bandMesh.indices.length; index += 3) {
|
|
@@ -1329,7 +1628,12 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
1329
1628
|
}
|
|
1330
1629
|
}
|
|
1331
1630
|
|
|
1332
|
-
const cloth = buildClothSurface(
|
|
1631
|
+
const cloth = buildClothSurface(
|
|
1632
|
+
state,
|
|
1633
|
+
state,
|
|
1634
|
+
state.clothDetail.getSnapshot().currentLevel.config,
|
|
1635
|
+
visuals
|
|
1636
|
+
);
|
|
1333
1637
|
for (let index = 0; index < cloth.indices.length; index += 3) {
|
|
1334
1638
|
const a = cloth.positions[cloth.indices[index]];
|
|
1335
1639
|
const b = cloth.positions[cloth.indices[index + 1]];
|
|
@@ -1344,7 +1648,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
1344
1648
|
depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
|
|
1345
1649
|
worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
|
|
1346
1650
|
normal,
|
|
1347
|
-
baseColor:
|
|
1651
|
+
baseColor: cloth.color,
|
|
1348
1652
|
accent: cloth.band === "near" ? 0.1 : 0.04,
|
|
1349
1653
|
});
|
|
1350
1654
|
}
|
|
@@ -1415,21 +1719,37 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
1415
1719
|
"Near-field lighting keeps the ray-traced-primary shadow impression so the collision read stays crisp.",
|
|
1416
1720
|
]
|
|
1417
1721
|
: SCENE_NOTES;
|
|
1722
|
+
const custom = state.demoDescription ?? null;
|
|
1418
1723
|
|
|
1419
|
-
setListContent(
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1724
|
+
setListContent(
|
|
1725
|
+
dom.sceneMetrics,
|
|
1726
|
+
Array.isArray(custom?.sceneMetrics) ? custom.sceneMetrics : sceneMetrics
|
|
1727
|
+
);
|
|
1728
|
+
setListContent(
|
|
1729
|
+
dom.qualityMetrics,
|
|
1730
|
+
Array.isArray(custom?.qualityMetrics) ? custom.qualityMetrics : qualityMetrics
|
|
1731
|
+
);
|
|
1732
|
+
setListContent(
|
|
1733
|
+
dom.debugMetrics,
|
|
1734
|
+
Array.isArray(custom?.debugMetrics) ? custom.debugMetrics : debugMetrics
|
|
1735
|
+
);
|
|
1736
|
+
setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
|
|
1423
1737
|
|
|
1424
|
-
dom.status.textContent =
|
|
1738
|
+
dom.status.textContent =
|
|
1739
|
+
typeof custom?.status === "string"
|
|
1740
|
+
? custom.status
|
|
1741
|
+
: `3D scene live · ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
|
|
1425
1742
|
dom.details.textContent =
|
|
1426
|
-
|
|
1427
|
-
?
|
|
1428
|
-
:
|
|
1743
|
+
typeof custom?.details === "string"
|
|
1744
|
+
? custom.details
|
|
1745
|
+
: state.focus === "physics"
|
|
1746
|
+
? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver; GLTF ships collide on ${shipModel.physics.shape ?? "box"} volumes while visual follow-up remains downstream.`
|
|
1747
|
+
: `GLTF ships are colliding with ${shipModel.physics.shape ?? "box"} physics volumes; cloth and fluid remain continuous while the governor pressure is ${state.lastDecision.pressureLevel}.`;
|
|
1429
1748
|
}
|
|
1430
1749
|
|
|
1431
1750
|
function updateSceneState(state, dt, shipModel) {
|
|
1432
1751
|
updateShips(state, dt, shipModel);
|
|
1752
|
+
updateWaveImpulses(state, dt);
|
|
1433
1753
|
updateSpray(state, dt);
|
|
1434
1754
|
updatePhysicsSnapshot(state, shipModel);
|
|
1435
1755
|
}
|
|
@@ -1449,6 +1769,7 @@ function syncTextState(state, shipModel) {
|
|
|
1449
1769
|
})),
|
|
1450
1770
|
shipPhysics: shipModel.physics,
|
|
1451
1771
|
sprays: state.sprays.length,
|
|
1772
|
+
waveImpulses: state.waveImpulses.length,
|
|
1452
1773
|
pressure: state.lastDecision?.pressureLevel ?? "stable",
|
|
1453
1774
|
physics: {
|
|
1454
1775
|
profile: state.physics.profile,
|
|
@@ -1456,6 +1777,7 @@ function syncTextState(state, shipModel) {
|
|
|
1456
1777
|
workerJobCount: state.physics.manifest.jobs.length,
|
|
1457
1778
|
snapshot: state.physics.snapshot,
|
|
1458
1779
|
},
|
|
1780
|
+
package: state.demoDescription?.textState ?? null,
|
|
1459
1781
|
};
|
|
1460
1782
|
window.render_game_to_text = () => JSON.stringify(snapshot);
|
|
1461
1783
|
window.advanceTime = (ms) => {
|
|
@@ -1472,6 +1794,9 @@ function syncTextState(state, shipModel) {
|
|
|
1472
1794
|
export async function mountGpuShowcase(options = {}) {
|
|
1473
1795
|
injectStyles();
|
|
1474
1796
|
const root = options.root ?? document.body;
|
|
1797
|
+
const previousMarkup = root.innerHTML;
|
|
1798
|
+
const previousRenderGameToText = window.render_game_to_text;
|
|
1799
|
+
const previousAdvanceTime = window.advanceTime;
|
|
1475
1800
|
const focus = options.focus ?? new URLSearchParams(window.location.search).get("focus") ?? "integrated";
|
|
1476
1801
|
const dom = buildDemoDom(root, {
|
|
1477
1802
|
packageName: options.packageName ?? "@plasius/gpu-demo-viewer",
|
|
@@ -1483,12 +1808,22 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
1483
1808
|
const state = createSceneState({ focus });
|
|
1484
1809
|
const shipModel = await loadGltfModel(resolveShowcaseAssetUrl());
|
|
1485
1810
|
state.shipModel = shipModel;
|
|
1811
|
+
state.packageState =
|
|
1812
|
+
typeof options.createState === "function" ? options.createState() : undefined;
|
|
1486
1813
|
updatePhysicsSnapshot(state, shipModel);
|
|
1487
1814
|
state.lastDecision = recordTelemetry(state, 16.4);
|
|
1815
|
+
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
1488
1816
|
syncTextState(state, shipModel);
|
|
1489
1817
|
|
|
1490
1818
|
const ctx = dom.canvas.getContext("2d");
|
|
1819
|
+
let destroyed = false;
|
|
1820
|
+
let frameHandle = null;
|
|
1821
|
+
|
|
1491
1822
|
const renderFrame = (nowMs) => {
|
|
1823
|
+
if (destroyed) {
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1492
1827
|
if (!state.paused) {
|
|
1493
1828
|
if (state.lastTimeMs == null) {
|
|
1494
1829
|
state.lastTimeMs = nowMs;
|
|
@@ -1498,35 +1833,67 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
1498
1833
|
state.time += dt;
|
|
1499
1834
|
state.frame += 1;
|
|
1500
1835
|
updateSceneState(state, dt, shipModel);
|
|
1836
|
+
updatePackageState(state, options, shipModel, dt);
|
|
1501
1837
|
const syntheticFrame = 14.2 + state.sprays.length * 0.1 + (state.stress ? 6.4 : 0);
|
|
1502
1838
|
state.lastDecision = recordTelemetry(state, syntheticFrame);
|
|
1503
1839
|
}
|
|
1504
1840
|
|
|
1841
|
+
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
1505
1842
|
renderScene(ctx, dom.canvas, state, shipModel, dom);
|
|
1506
1843
|
syncTextState(state, shipModel);
|
|
1507
|
-
requestAnimationFrame(renderFrame);
|
|
1844
|
+
frameHandle = requestAnimationFrame(renderFrame);
|
|
1508
1845
|
};
|
|
1509
1846
|
|
|
1510
|
-
|
|
1847
|
+
const handlePauseClick = () => {
|
|
1511
1848
|
state.paused = !state.paused;
|
|
1512
1849
|
dom.pauseButton.textContent = state.paused ? "Resume" : "Pause";
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1850
|
+
};
|
|
1851
|
+
const handleStressChange = () => {
|
|
1515
1852
|
state.stress = dom.stressToggle.checked;
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1853
|
+
};
|
|
1854
|
+
const handleFocusChange = () => {
|
|
1518
1855
|
state.focus = dom.focusMode.value;
|
|
1519
1856
|
Object.assign(state.camera, {
|
|
1520
1857
|
...CAMERA_PRESETS[state.focus],
|
|
1521
1858
|
target: vec3(...CAMERA_PRESETS[state.focus].target),
|
|
1522
1859
|
});
|
|
1523
|
-
}
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1862
|
+
dom.pauseButton.addEventListener("click", handlePauseClick);
|
|
1863
|
+
dom.stressToggle.addEventListener("change", handleStressChange);
|
|
1864
|
+
dom.focusMode.addEventListener("change", handleFocusChange);
|
|
1524
1865
|
|
|
1525
|
-
requestAnimationFrame(renderFrame);
|
|
1866
|
+
frameHandle = requestAnimationFrame(renderFrame);
|
|
1526
1867
|
return {
|
|
1527
1868
|
state,
|
|
1528
1869
|
shipModel,
|
|
1529
1870
|
canvas: dom.canvas,
|
|
1871
|
+
destroy() {
|
|
1872
|
+
if (destroyed) {
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
destroyed = true;
|
|
1877
|
+
if (frameHandle != null) {
|
|
1878
|
+
cancelAnimationFrame(frameHandle);
|
|
1879
|
+
}
|
|
1880
|
+
dom.pauseButton.removeEventListener("click", handlePauseClick);
|
|
1881
|
+
dom.stressToggle.removeEventListener("change", handleStressChange);
|
|
1882
|
+
dom.focusMode.removeEventListener("change", handleFocusChange);
|
|
1883
|
+
root.innerHTML = previousMarkup;
|
|
1884
|
+
|
|
1885
|
+
if (typeof previousRenderGameToText === "function") {
|
|
1886
|
+
window.render_game_to_text = previousRenderGameToText;
|
|
1887
|
+
} else {
|
|
1888
|
+
delete window.render_game_to_text;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
if (typeof previousAdvanceTime === "function") {
|
|
1892
|
+
window.advanceTime = previousAdvanceTime;
|
|
1893
|
+
} else {
|
|
1894
|
+
delete window.advanceTime;
|
|
1895
|
+
}
|
|
1896
|
+
},
|
|
1530
1897
|
};
|
|
1531
1898
|
}
|
|
1532
1899
|
|
|
@@ -1544,6 +1911,7 @@ function updatePhysicsSnapshot(state, shipModel) {
|
|
|
1544
1911
|
contactCount: state.collisionFlash > 0.02 ? 1 : 0,
|
|
1545
1912
|
metadata: {
|
|
1546
1913
|
collisionCount: state.collisionCount,
|
|
1914
|
+
contactCount: state.contactCount,
|
|
1547
1915
|
snapshotStageId: state.physics.plan.snapshotStageId,
|
|
1548
1916
|
rigidBodyShape: shipModel.physics.shape ?? "box",
|
|
1549
1917
|
},
|