@plasius/gpu-shared 0.1.1 → 0.1.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 +15 -0
- package/dist/index.cjs +426 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{showcase-runtime-5H44EZXD.js → showcase-runtime-67CHJBNO.js} +426 -40
- package/dist/showcase-runtime-67CHJBNO.js.map +1 -0
- package/package.json +7 -7
- package/src/showcase-runtime.js +372 -44
- package/dist/showcase-runtime-5H44EZXD.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plasius/gpu-shared",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Shared browser-safe demo runtime and asset helpers for the Plasius gpu-* package family.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"author": "Plasius LTD <development@plasius.co.uk>",
|
|
54
54
|
"license": "Apache-2.0",
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@plasius/gpu-cloth": "^0.1.
|
|
57
|
-
"@plasius/gpu-debug": "^0.1.
|
|
58
|
-
"@plasius/gpu-fluid": "
|
|
59
|
-
"@plasius/gpu-lighting": "^0.1.
|
|
60
|
-
"@plasius/gpu-performance": "^0.1.
|
|
61
|
-
"@plasius/gpu-physics": "^0.1.
|
|
56
|
+
"@plasius/gpu-cloth": "^0.1.1",
|
|
57
|
+
"@plasius/gpu-debug": "^0.1.2",
|
|
58
|
+
"@plasius/gpu-fluid": "^0.1.1",
|
|
59
|
+
"@plasius/gpu-lighting": "^0.1.10",
|
|
60
|
+
"@plasius/gpu-performance": "^0.1.3",
|
|
61
|
+
"@plasius/gpu-physics": "^0.1.10"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@eslint/js": "^10.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) => {
|
|
@@ -1483,8 +1805,11 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
1483
1805
|
const state = createSceneState({ focus });
|
|
1484
1806
|
const shipModel = await loadGltfModel(resolveShowcaseAssetUrl());
|
|
1485
1807
|
state.shipModel = shipModel;
|
|
1808
|
+
state.packageState =
|
|
1809
|
+
typeof options.createState === "function" ? options.createState() : undefined;
|
|
1486
1810
|
updatePhysicsSnapshot(state, shipModel);
|
|
1487
1811
|
state.lastDecision = recordTelemetry(state, 16.4);
|
|
1812
|
+
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
1488
1813
|
syncTextState(state, shipModel);
|
|
1489
1814
|
|
|
1490
1815
|
const ctx = dom.canvas.getContext("2d");
|
|
@@ -1498,10 +1823,12 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
1498
1823
|
state.time += dt;
|
|
1499
1824
|
state.frame += 1;
|
|
1500
1825
|
updateSceneState(state, dt, shipModel);
|
|
1826
|
+
updatePackageState(state, options, shipModel, dt);
|
|
1501
1827
|
const syntheticFrame = 14.2 + state.sprays.length * 0.1 + (state.stress ? 6.4 : 0);
|
|
1502
1828
|
state.lastDecision = recordTelemetry(state, syntheticFrame);
|
|
1503
1829
|
}
|
|
1504
1830
|
|
|
1831
|
+
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
1505
1832
|
renderScene(ctx, dom.canvas, state, shipModel, dom);
|
|
1506
1833
|
syncTextState(state, shipModel);
|
|
1507
1834
|
requestAnimationFrame(renderFrame);
|
|
@@ -1544,6 +1871,7 @@ function updatePhysicsSnapshot(state, shipModel) {
|
|
|
1544
1871
|
contactCount: state.collisionFlash > 0.02 ? 1 : 0,
|
|
1545
1872
|
metadata: {
|
|
1546
1873
|
collisionCount: state.collisionCount,
|
|
1874
|
+
contactCount: state.contactCount,
|
|
1547
1875
|
snapshotStageId: state.physics.plan.snapshotStageId,
|
|
1548
1876
|
rigidBodyShape: shipModel.physics.shape ?? "box",
|
|
1549
1877
|
},
|