feat: basic click functionality

This commit is contained in:
TrèsAbhi committed 2024-02-02 10:31:40 -06:00
1 parent 0251bfc842
commit b01a03c6ce
6 files changed
+505 -415

No files matched your search

@@ -21,6 +21,8 @@ import { SceneProps } from '../SceneProps';
import { ModelLoader } from './components/ModelLoader'; import { ModelLoader } from './components/ModelLoader';
import { Options } from './components/Options'; import { Options } from './components/Options';
import { TankArmor } from './components/TankArmor'; import { TankArmor } from './components/TankArmor';
import { ExternalModuleMask } from './components/TankArmor/components/ExternalModuleMask';
import { SpacedArmorDepth } from './components/TankArmor/components/SpacedArmorDepth';
import { TankModel } from './components/TankModel'; import { TankModel } from './components/TankModel';
export function TankSandbox() { export function TankSandbox() {
@@ -173,6 +175,8 @@ export function TankSandbox() {
<Lighting /> <Lighting />
<TankModel /> <TankModel />
<TankArmor /> <TankArmor />
<ExternalModuleMask ornamental />
<SpacedArmorDepth ornamental />
</Suspense> </Suspense>
</Canvas> </Canvas>
) : ( ) : (
@@ -15,6 +15,10 @@ import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDe
import { useDuel } from '../../../../../../../../../stores/duel'; import { useDuel } from '../../../../../../../../../stores/duel';
import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia'; import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia';
interface ExternalModuleMaskProps {
ornamental?: boolean;
}
/** /**
* When rendered, generates a mask and thickness buffer for external modules. * When rendered, generates a mask and thickness buffer for external modules.
* - R: 1 means external module * - R: 1 means external module
@@ -22,15 +26,120 @@ import { useTankopediaTemporary } from '../../../../../../../../../stores/tankop
* - B: no data * - B: no data
* - A: 1 means is armor; 0 is background * - A: 1 means is armor; 0 is background
*/ */
export const ExternalModuleMask = memo(() => { export const ExternalModuleMask = memo<ExternalModuleMaskProps>(
const protagonist = useDuel((state) => state.protagonist!); ({ ornamental = false }) => {
const wrapper = useRef<Group>(null); const protagonist = useDuel((state) => state.protagonist!);
const modelDefinitions = useModelDefinitions(); const wrapper = useRef<Group>(null);
const turretContainer = useRef<Group>(null); const modelDefinitions = useModelDefinitions();
const gunContainer = useRef<Group>(null); const turretContainer = useRef<Group>(null);
const initialTankopediaState = useTankopediaTemporary.getState(); const gunContainer = useRef<Group>(null);
const initialTankopediaState = useTankopediaTemporary.getState();
useEffect(() => { useEffect(() => {
const hullOrigin = new Vector3(
tankModelDefinition.hullOrigin[0],
tankModelDefinition.hullOrigin[1],
-tankModelDefinition.hullOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const turretPosition = new Vector3();
const turretRotation = new Euler();
const gunPosition = new Vector3();
const gunRotation = new Euler();
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
gunPosition
.set(0, 0, 0)
.sub(hullOrigin)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(I_HAT, pitch)
.add(gunOrigin)
.add(turretOrigin)
.add(hullOrigin);
gunRotation.set(pitch, 0, 0);
gunContainer.current?.position.copy(gunPosition);
gunContainer.current?.rotation.copy(gunRotation);
if (yaw === undefined) return;
turretPosition
.set(0, 0, 0)
.sub(turretOrigin)
.sub(hullOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
turretRotation.set(0, 0, yaw);
if (tankModelDefinition.turretRotation) {
const initialPitch = -degToRad(
tankModelDefinition.turretRotation.pitch,
);
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
const initialRoll = -degToRad(
tankModelDefinition.turretRotation.roll,
);
turretPosition
.applyAxisAngle(I_HAT, initialPitch)
.applyAxisAngle(J_HAT, initialRoll)
.applyAxisAngle(K_HAT, initialYaw);
turretRotation.x += initialPitch;
turretRotation.y += initialRoll;
turretRotation.z += initialYaw;
}
turretPosition.add(turretOrigin).add(hullOrigin);
turretContainer.current?.position.copy(turretPosition);
turretContainer.current?.rotation.copy(turretRotation);
}
handleModelTransform(useTankopediaTemporary.getState().model.pose);
modelTransformEvent.on(handleModelTransform);
return () => {
modelTransformEvent.off(handleModelTransform);
};
});
useEffect(() => {
const unsubscribe = useTankopediaTemporary.subscribe(
(state) => state.mode,
(mode) => {
if (wrapper.current) wrapper.current.visible = mode === 'armor';
},
);
return unsubscribe;
});
const armorGltf = useArmor(protagonist.tank.id);
const { gltf: modelGltf } = useModel(protagonist.tank.id);
const armorNodes = Object.values(armorGltf.nodes);
const modelNodes = Object.values(modelGltf.nodes);
const tankModelDefinition = modelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const maxThickness = Math.max(
tankModelDefinition.trackThickness,
gunModelDefinition.barrelThickness,
...armorNodes
.map((node) => {
const armorId = nameToArmorId(node.name);
return resolveArmor(tankModelDefinition.armor, armorId).thickness;
})
.filter(Boolean),
);
const hullOrigin = new Vector3( const hullOrigin = new Vector3(
tankModelDefinition.hullOrigin[0], tankModelDefinition.hullOrigin[0],
tankModelDefinition.hullOrigin[1], tankModelDefinition.hullOrigin[1],
@@ -46,223 +155,127 @@ export const ExternalModuleMask = memo(() => {
turretModelDefinition.gunOrigin[1], turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2], -turretModelDefinition.gunOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2); ).applyAxisAngle(I_HAT, Math.PI / 2);
const turretPosition = new Vector3();
const turretRotation = new Euler();
const gunPosition = new Vector3();
const gunRotation = new Euler();
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) { return (
gunPosition <group
.set(0, 0, 0) ref={wrapper}
.sub(hullOrigin) rotation={[-Math.PI / 2, 0, 0]}
.sub(turretOrigin) visible={initialTankopediaState.mode === 'armor'}
.sub(gunOrigin) >
.applyAxisAngle(I_HAT, pitch) <group position={hullOrigin}>
.add(gunOrigin) {armorNodes.map((node) => {
.add(turretOrigin) const isHull = node.name.startsWith('hull_');
.add(hullOrigin); const isVisible = isHull;
gunRotation.set(pitch, 0, 0); const armorId = nameToArmorId(node.name);
gunContainer.current?.position.copy(gunPosition); const { thickness, spaced } = resolveArmor(
gunContainer.current?.rotation.copy(gunRotation); tankModelDefinition.armor,
armorId,
);
if (yaw === undefined) return; if (!isVisible || thickness === undefined || spaced) return null;
turretPosition return (
.set(0, 0, 0) <ArmorMeshExternalModuleMask
.sub(turretOrigin) ornamental={ornamental}
.sub(hullOrigin) exclude
.applyAxisAngle(new Vector3(0, 0, 1), yaw); key={node.uuid}
turretRotation.set(0, 0, yaw); geometry={(node as Mesh).geometry}
/>
);
})}
</group>
if (tankModelDefinition.turretRotation) { {modelNodes.map((node) => {
const initialPitch = -degToRad( const isWheel = node.name.startsWith('chassis_wheel_');
tankModelDefinition.turretRotation.pitch, const isTrack = node.name.startsWith('chassis_track_');
); const isVisible = isWheel || isTrack;
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
turretPosition if (!isVisible) return null;
.applyAxisAngle(I_HAT, initialPitch)
.applyAxisAngle(J_HAT, initialRoll)
.applyAxisAngle(K_HAT, initialYaw);
turretRotation.x += initialPitch;
turretRotation.y += initialRoll;
turretRotation.z += initialYaw;
}
turretPosition.add(turretOrigin).add(hullOrigin);
turretContainer.current?.position.copy(turretPosition);
turretContainer.current?.rotation.copy(turretRotation);
}
handleModelTransform(useTankopediaTemporary.getState().model.pose);
modelTransformEvent.on(handleModelTransform);
return () => {
modelTransformEvent.off(handleModelTransform);
};
});
useEffect(() => {
const unsubscribe = useTankopediaTemporary.subscribe(
(state) => state.mode,
(mode) => {
if (wrapper.current) wrapper.current.visible = mode === 'armor';
},
);
return unsubscribe;
});
const armorGltf = useArmor(protagonist.tank.id);
const { gltf: modelGltf } = useModel(protagonist.tank.id);
const armorNodes = Object.values(armorGltf.nodes);
const modelNodes = Object.values(modelGltf.nodes);
const tankModelDefinition = modelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const maxThickness = Math.max(
tankModelDefinition.trackThickness,
gunModelDefinition.barrelThickness,
...armorNodes
.map((node) => {
const armorId = nameToArmorId(node.name);
return resolveArmor(tankModelDefinition.armor, armorId).thickness;
})
.filter(Boolean),
);
const hullOrigin = new Vector3(
tankModelDefinition.hullOrigin[0],
tankModelDefinition.hullOrigin[1],
-tankModelDefinition.hullOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
return (
<group
ref={wrapper}
rotation={[-Math.PI / 2, 0, 0]}
visible={initialTankopediaState.mode === 'armor'}
>
<group position={hullOrigin}>
{armorNodes.map((node) => {
const isHull = node.name.startsWith('hull_');
const isVisible = isHull;
const armorId = nameToArmorId(node.name);
const { thickness, spaced } = resolveArmor(
tankModelDefinition.armor,
armorId,
);
if (!isVisible || thickness === undefined || spaced) return null;
return ( return (
<ArmorMeshExternalModuleMask <ArmorMeshExternalModuleMask
exclude ornamental={ornamental}
maxThickness={maxThickness}
thickness={tankModelDefinition.trackThickness}
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
/> />
); );
})} })}
</group>
{modelNodes.map((node) => { <group ref={turretContainer}>
const isWheel = node.name.startsWith('chassis_wheel_'); <group position={hullOrigin}>
const isTrack = node.name.startsWith('chassis_track_'); {armorNodes.map((node) => {
const isVisible = isWheel || isTrack; const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name);
const { thickness, spaced } = resolveArmor(
turretModelDefinition.armor,
armorId,
);
if (!isVisible) return null; if (!isVisible || thickness === undefined || spaced) return null;
return ( return (
<ArmorMeshExternalModuleMask <ArmorMeshExternalModuleMask
maxThickness={maxThickness} ornamental={ornamental}
thickness={tankModelDefinition.trackThickness} exclude
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
/> position={turretOrigin}
); />
})} );
})}
</group>
<group ref={turretContainer}> <group ref={gunContainer}>
<group position={hullOrigin}> {armorNodes.map((node) => {
{armorNodes.map((node) => { const isCurrentGun = node.name.startsWith(
const isCurrentTurret = node.name.startsWith( `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`, );
); const isVisible = isCurrentGun;
const isVisible = isCurrentTurret; const armorId = nameToArmorId(node.name);
const armorId = nameToArmorId(node.name); const { thickness, spaced } = resolveArmor(
const { thickness, spaced } = resolveArmor( gunModelDefinition.armor,
turretModelDefinition.armor, armorId,
armorId, );
);
if (!isVisible || thickness === undefined || spaced) return null; if (!isVisible || thickness === undefined || spaced) return null;
return ( return (
<ArmorMeshExternalModuleMask <ArmorMeshExternalModuleMask
exclude ornamental={ornamental}
key={node.uuid} exclude
geometry={(node as Mesh).geometry} key={node.uuid}
position={turretOrigin} geometry={(node as Mesh).geometry}
/> position={turretOrigin.clone().add(gunOrigin)}
); />
})} );
</group> })}
<group ref={gunContainer}> {modelNodes.map((node) => {
{armorNodes.map((node) => { const isCurrentGun =
const isCurrentGun = node.name.startsWith( node.name ===
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
); const isVisible = isCurrentGun;
const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name);
const { thickness, spaced } = resolveArmor(
gunModelDefinition.armor,
armorId,
);
if (!isVisible || thickness === undefined || spaced) return null; if (!isVisible) return null;
return ( return (
<ArmorMeshExternalModuleMask <ArmorMeshExternalModuleMask
exclude ornamental={ornamental}
key={node.uuid} maxThickness={maxThickness}
geometry={(node as Mesh).geometry} thickness={gunModelDefinition.barrelThickness}
position={turretOrigin.clone().add(gunOrigin)} key={node.uuid}
/> geometry={(node as Mesh).geometry}
); />
})} );
})}
{modelNodes.map((node) => { </group>
const isCurrentGun =
node.name ===
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
const isVisible = isCurrentGun;
if (!isVisible) return null;
return (
<ArmorMeshExternalModuleMask
maxThickness={maxThickness}
thickness={gunModelDefinition.barrelThickness}
key={node.uuid}
geometry={(node as Mesh).geometry}
/>
);
})}
</group> </group>
</group> </group>
</group> );
); },
}); );
@@ -15,6 +15,10 @@ import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDe
import { useDuel } from '../../../../../../../../../stores/duel'; import { useDuel } from '../../../../../../../../../stores/duel';
import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia'; import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia';
interface SpacedArmorDepthProps {
ornamental?: boolean;
}
/** /**
* When rendered, this component provides the depth buffer for the armor which * When rendered, this component provides the depth buffer for the armor which
* when compared to the depth of the core armor will give you the distance * when compared to the depth of the core armor will give you the distance
@@ -26,17 +30,122 @@ import { useTankopediaTemporary } from '../../../../../../../../../stores/tankop
* - B: no data * - B: no data
* - A: 1 means is armor; 0 is background * - A: 1 means is armor; 0 is background
*/ */
export const SpacedArmorDepth = memo(() => { export const SpacedArmorDepth = memo<SpacedArmorDepthProps>(
const protagonist = useDuel((state) => state.protagonist!); ({ ornamental = false }) => {
const wrapper = useRef<Group>(null); const protagonist = useDuel((state) => state.protagonist!);
const modelDefinitions = useModelDefinitions(); const wrapper = useRef<Group>(null);
const turretContainer = useRef<Group>(null); const modelDefinitions = useModelDefinitions();
const gunContainer = useRef<Group>(null); const turretContainer = useRef<Group>(null);
const initialTankopediaState = useTankopediaTemporary.getState(); const gunContainer = useRef<Group>(null);
const initialTankopediaState = useTankopediaTemporary.getState();
useEffect(() => { useEffect(() => {
if (!modelDefinitions) return; if (!modelDefinitions) return;
const hullOrigin = new Vector3(
tankModelDefinition.hullOrigin[0],
tankModelDefinition.hullOrigin[1],
-tankModelDefinition.hullOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const turretPosition = new Vector3();
const turretRotation = new Euler();
const gunPosition = new Vector3();
const gunRotation = new Euler();
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
gunPosition
.set(0, 0, 0)
.sub(hullOrigin)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(I_HAT, pitch)
.add(gunOrigin)
.add(turretOrigin)
.add(hullOrigin);
gunRotation.set(pitch, 0, 0);
gunContainer.current?.position.copy(gunPosition);
gunContainer.current?.rotation.copy(gunRotation);
if (yaw === undefined) return;
turretPosition
.set(0, 0, 0)
.sub(hullOrigin)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
turretRotation.set(0, 0, yaw);
if (tankModelDefinition.turretRotation) {
const initialPitch = -degToRad(
tankModelDefinition.turretRotation.pitch,
);
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
const initialRoll = -degToRad(
tankModelDefinition.turretRotation.roll,
);
turretPosition
.applyAxisAngle(I_HAT, initialPitch)
.applyAxisAngle(J_HAT, initialRoll)
.applyAxisAngle(K_HAT, initialYaw);
turretRotation.x += initialPitch;
turretRotation.y += initialRoll;
turretRotation.z += initialYaw;
}
turretPosition.add(turretOrigin).add(hullOrigin);
turretContainer.current?.position.copy(turretPosition);
turretContainer.current?.rotation.copy(turretRotation);
}
handleModelTransform(useTankopediaTemporary.getState().model.pose);
modelTransformEvent.on(handleModelTransform);
return () => {
modelTransformEvent.off(handleModelTransform);
};
});
useEffect(() => {
const unsubscribe = useTankopediaTemporary.subscribe(
(state) => state.mode,
(mode) => {
if (wrapper.current) wrapper.current.visible = mode === 'armor';
},
);
return unsubscribe;
});
const armorGltf = useArmor(protagonist.tank.id);
const { gltf: modelGltf } = useModel(protagonist.tank.id);
const armorNodes = Object.values(armorGltf.nodes);
const modelNodes = Object.values(modelGltf.nodes);
const tankModelDefinition = modelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const maxThickness = Math.max(
tankModelDefinition.trackThickness,
gunModelDefinition.barrelThickness,
...armorNodes
.map((node) => {
const armorId = nameToArmorId(node.name);
return resolveArmor(tankModelDefinition.armor, armorId).thickness;
})
.filter(Boolean),
);
const hullOrigin = new Vector3( const hullOrigin = new Vector3(
tankModelDefinition.hullOrigin[0], tankModelDefinition.hullOrigin[0],
tankModelDefinition.hullOrigin[1], tankModelDefinition.hullOrigin[1],
@@ -52,165 +161,20 @@ export const SpacedArmorDepth = memo(() => {
turretModelDefinition.gunOrigin[1], turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2], -turretModelDefinition.gunOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2); ).applyAxisAngle(I_HAT, Math.PI / 2);
const turretPosition = new Vector3();
const turretRotation = new Euler();
const gunPosition = new Vector3();
const gunRotation = new Euler();
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) { return (
gunPosition <group
.set(0, 0, 0) ref={wrapper}
.sub(hullOrigin) rotation={[-Math.PI / 2, 0, 0]}
.sub(turretOrigin) visible={initialTankopediaState.mode === 'armor'}
.sub(gunOrigin) >
.applyAxisAngle(I_HAT, pitch)
.add(gunOrigin)
.add(turretOrigin)
.add(hullOrigin);
gunRotation.set(pitch, 0, 0);
gunContainer.current?.position.copy(gunPosition);
gunContainer.current?.rotation.copy(gunRotation);
if (yaw === undefined) return;
turretPosition
.set(0, 0, 0)
.sub(hullOrigin)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
turretRotation.set(0, 0, yaw);
if (tankModelDefinition.turretRotation) {
const initialPitch = -degToRad(
tankModelDefinition.turretRotation.pitch,
);
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
turretPosition
.applyAxisAngle(I_HAT, initialPitch)
.applyAxisAngle(J_HAT, initialRoll)
.applyAxisAngle(K_HAT, initialYaw);
turretRotation.x += initialPitch;
turretRotation.y += initialRoll;
turretRotation.z += initialYaw;
}
turretPosition.add(turretOrigin).add(hullOrigin);
turretContainer.current?.position.copy(turretPosition);
turretContainer.current?.rotation.copy(turretRotation);
}
handleModelTransform(useTankopediaTemporary.getState().model.pose);
modelTransformEvent.on(handleModelTransform);
return () => {
modelTransformEvent.off(handleModelTransform);
};
});
useEffect(() => {
const unsubscribe = useTankopediaTemporary.subscribe(
(state) => state.mode,
(mode) => {
if (wrapper.current) wrapper.current.visible = mode === 'armor';
},
);
return unsubscribe;
});
const armorGltf = useArmor(protagonist.tank.id);
const { gltf: modelGltf } = useModel(protagonist.tank.id);
const armorNodes = Object.values(armorGltf.nodes);
const modelNodes = Object.values(modelGltf.nodes);
const tankModelDefinition = modelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const maxThickness = Math.max(
tankModelDefinition.trackThickness,
gunModelDefinition.barrelThickness,
...armorNodes
.map((node) => {
const armorId = nameToArmorId(node.name);
return resolveArmor(tankModelDefinition.armor, armorId).thickness;
})
.filter(Boolean),
);
const hullOrigin = new Vector3(
tankModelDefinition.hullOrigin[0],
tankModelDefinition.hullOrigin[1],
-tankModelDefinition.hullOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
return (
<group
ref={wrapper}
rotation={[-Math.PI / 2, 0, 0]}
visible={initialTankopediaState.mode === 'armor'}
>
<group position={hullOrigin}>
{armorNodes.map((node) => {
const isHull = node.name.startsWith('hull_');
const isVisible = isHull;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor,
armorId,
);
if (!isVisible || thickness === undefined) return null;
return (
<ArmorMeshSpacedArmorDepth
include={spaced}
isExternalModule={false}
thickness={thickness}
maxThickness={maxThickness}
key={node.uuid}
geometry={(node as Mesh).geometry}
/>
);
})}
</group>
{modelNodes.map((node) => {
const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_');
const isVisible = isWheel || isTrack;
if (!isVisible) return null;
return (
<ArmorMeshSpacedArmorDepth
isExternalModule
key={node.uuid}
geometry={(node as Mesh).geometry}
/>
);
})}
<group ref={turretContainer}>
<group position={hullOrigin}> <group position={hullOrigin}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith( const isHull = node.name.startsWith('hull_');
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`, const isVisible = isHull;
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor, tankModelDefinition.armor,
armorId, armorId,
); );
@@ -218,27 +182,44 @@ export const SpacedArmorDepth = memo(() => {
return ( return (
<ArmorMeshSpacedArmorDepth <ArmorMeshSpacedArmorDepth
ornamental={ornamental}
include={spaced} include={spaced}
isExternalModule={false} isExternalModule={false}
thickness={thickness} thickness={thickness}
maxThickness={maxThickness} maxThickness={maxThickness}
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
position={turretOrigin}
/> />
); );
})} })}
</group> </group>
<group ref={gunContainer}> {modelNodes.map((node) => {
const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_');
const isVisible = isWheel || isTrack;
if (!isVisible) return null;
return (
<ArmorMeshSpacedArmorDepth
ornamental={ornamental}
isExternalModule
key={node.uuid}
geometry={(node as Mesh).geometry}
/>
);
})}
<group ref={turretContainer}>
<group position={hullOrigin}> <group position={hullOrigin}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith( const isCurrentTurret = node.name.startsWith(
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
); );
const isVisible = isCurrentGun; const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor, turretModelDefinition.armor,
armorId, armorId,
); );
@@ -246,36 +227,68 @@ export const SpacedArmorDepth = memo(() => {
return ( return (
<ArmorMeshSpacedArmorDepth <ArmorMeshSpacedArmorDepth
ornamental={ornamental}
include={spaced} include={spaced}
isExternalModule={false} isExternalModule={false}
thickness={thickness} thickness={thickness}
maxThickness={maxThickness} maxThickness={maxThickness}
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
position={turretOrigin.clone().add(gunOrigin)} position={turretOrigin}
/> />
); );
})} })}
</group> </group>
<group ref={gunContainer}>
<group position={hullOrigin}>
{armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
);
const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor,
armorId,
);
{modelNodes.map((node) => { if (!isVisible || thickness === undefined) return null;
const isCurrentGun =
node.name ===
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
const isVisible = isCurrentGun;
if (!isVisible) return null; return (
<ArmorMeshSpacedArmorDepth
ornamental={ornamental}
include={spaced}
isExternalModule={false}
thickness={thickness}
maxThickness={maxThickness}
key={node.uuid}
geometry={(node as Mesh).geometry}
position={turretOrigin.clone().add(gunOrigin)}
/>
);
})}
</group>
return ( {modelNodes.map((node) => {
<ArmorMeshSpacedArmorDepth const isCurrentGun =
isExternalModule node.name ===
key={node.uuid} `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
geometry={(node as Mesh).geometry} const isVisible = isCurrentGun;
/>
); if (!isVisible) return null;
})}
return (
<ArmorMeshSpacedArmorDepth
ornamental={ornamental}
isExternalModule
key={node.uuid}
geometry={(node as Mesh).geometry}
/>
);
})}
</group>
</group> </group>
</group> </group>
</group> );
); },
}); );
@@ -6,7 +6,10 @@ import { canSplash } from '../../../../core/blitz/canSplash';
import { isExplosive } from '../../../../core/blitz/isExplosive'; import { isExplosive } from '../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
import { useDuel } from '../../../../stores/duel'; import { useDuel } from '../../../../stores/duel';
import { useTankopediaPersistent } from '../../../../stores/tankopedia'; import {
useTankopediaPersistent,
useTankopediaTemporary,
} from '../../../../stores/tankopedia';
import { externalModuleMaskRenderTarget } from '../ExternalModuleMask'; import { externalModuleMaskRenderTarget } from '../ExternalModuleMask';
import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth'; import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth';
import fragmentShader from './shaders/fragment.glsl'; import fragmentShader from './shaders/fragment.glsl';
@@ -17,6 +20,11 @@ interface ArmorMeshProps extends MeshProps {
maxThickness: number; maxThickness: number;
} }
export type ArmorMeshUserData = {
type: 'coreArmor' | 'spacedArmor' | 'externalModule';
thickness: number;
};
export function ArmorMesh({ export function ArmorMesh({
thickness, thickness,
maxThickness, maxThickness,
@@ -109,7 +117,26 @@ export function ArmorMesh({
<meshBasicMaterial colorWrite={false} /> <meshBasicMaterial colorWrite={false} />
</mesh> </mesh>
<mesh {...props} renderOrder={1}> <mesh
{...props}
renderOrder={1}
userData={
{
type: 'coreArmor',
thickness,
} satisfies ArmorMeshUserData
}
onClick={(event) => {
if (useTankopediaTemporary.getState().mode !== 'armor') return;
event.intersections
.filter(({ object }) => Boolean(object.userData.type))
.map(({ object }) => {
const data = object.userData as ArmorMeshUserData;
console.log(data.type, data.thickness);
});
}}
>
<shaderMaterial <shaderMaterial
ref={material} ref={material}
fragmentShader={fragmentShader} fragmentShader={fragmentShader}
@@ -1,10 +1,12 @@
import { MeshProps } from '@react-three/fiber'; import { MeshProps } from '@react-three/fiber';
import { Color, WebGLRenderTarget } from 'three'; import { Color, WebGLRenderTarget } from 'three';
import { ArmorMeshUserData } from './Armor';
export const externalModuleMaskRenderTarget = new WebGLRenderTarget(); export const externalModuleMaskRenderTarget = new WebGLRenderTarget();
type ArmorMeshExternalModuleMaskProps = MeshProps & type ArmorMeshExternalModuleMaskProps = MeshProps & {
( ornamental?: boolean;
} & (
| { | {
exclude?: false; exclude?: false;
maxThickness: number; maxThickness: number;
@@ -15,22 +17,34 @@ type ArmorMeshExternalModuleMaskProps = MeshProps &
} }
); );
export function ArmorMeshExternalModuleMask( export function ArmorMeshExternalModuleMask({
props: ArmorMeshExternalModuleMaskProps, ornamental = false,
) { ...props
}: ArmorMeshExternalModuleMaskProps) {
return ( return (
<> <>
{props.exclude && ( {(props.exclude || ornamental) && (
<mesh {...props} renderOrder={0}> <mesh {...props} renderOrder={0}>
<meshBasicMaterial colorWrite={false} /> <meshBasicMaterial colorWrite={false} />
</mesh> </mesh>
)} )}
{!props.exclude && ( {!props.exclude && (
<mesh {...props} renderOrder={1}> <mesh
{...props}
renderOrder={1}
onClick={ornamental ? () => {} : undefined}
userData={
{
type: 'externalModule',
thickness: props.thickness,
} satisfies ArmorMeshUserData
}
>
<meshBasicMaterial <meshBasicMaterial
color={new Color(1, props.thickness / props.maxThickness, 0)} color={new Color(1, props.thickness / props.maxThickness, 0)}
depthWrite={false} depthWrite={false}
colorWrite={!ornamental}
/> />
</mesh> </mesh>
)} )}
@@ -1,12 +1,14 @@
import { MeshProps } from '@react-three/fiber'; import { MeshProps } from '@react-three/fiber';
import { WebGLRenderTarget } from 'three'; import { WebGLRenderTarget } from 'three';
import { ArmorMeshUserData } from '../Armor';
import fragmentShader from './shaders/fragment.glsl'; import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl'; import vertexShader from './shaders/vertex.glsl';
export const spacedArmorDepthRenderTarget = new WebGLRenderTarget(); export const spacedArmorDepthRenderTarget = new WebGLRenderTarget();
type ArmorMeshSpacedArmorDepthProps = MeshProps & type ArmorMeshSpacedArmorDepthProps = MeshProps & {
( ornamental?: boolean;
} & (
| { | {
include?: false; include?: false;
isExternalModule: boolean; isExternalModule: boolean;
@@ -18,27 +20,44 @@ type ArmorMeshSpacedArmorDepthProps = MeshProps &
} }
); );
export function ArmorMeshSpacedArmorDepth( export function ArmorMeshSpacedArmorDepth({
props: ArmorMeshSpacedArmorDepthProps, ornamental = false,
) { ...props
}: ArmorMeshSpacedArmorDepthProps) {
return ( return (
<> <>
{!props.include && ( {!props.include && !ornamental && (
<mesh {...props} renderOrder={props.isExternalModule ? 2 : 0}> <mesh {...props} renderOrder={props.isExternalModule ? 2 : 0}>
<meshBasicMaterial colorWrite={false} /> <meshBasicMaterial colorWrite={false} />
</mesh> </mesh>
)} )}
{props.include && ( {props.include && (
<mesh {...props} renderOrder={1}> <mesh
<shaderMaterial {...props}
fragmentShader={fragmentShader} renderOrder={1}
vertexShader={vertexShader} onClick={ornamental ? () => {} : undefined}
uniforms={{ userData={
thickness: { value: props.thickness }, {
maxThickness: { value: props.maxThickness }, type: 'spacedArmor',
}} thickness: props.thickness,
/> } satisfies ArmorMeshUserData
}
>
{!ornamental && (
<shaderMaterial
fragmentShader={fragmentShader}
vertexShader={vertexShader}
uniforms={{
thickness: { value: props.thickness },
maxThickness: { value: props.maxThickness },
}}
/>
)}
{ornamental && (
<meshBasicMaterial colorWrite={false} depthWrite={false} />
)}
</mesh> </mesh>
)} )}
</> </>