mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
feat: basic click functionality
This commit is contained in:
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 { Options } from './components/Options';
|
||||
import { TankArmor } from './components/TankArmor';
|
||||
import { ExternalModuleMask } from './components/TankArmor/components/ExternalModuleMask';
|
||||
import { SpacedArmorDepth } from './components/TankArmor/components/SpacedArmorDepth';
|
||||
import { TankModel } from './components/TankModel';
|
||||
|
||||
export function TankSandbox() {
|
||||
@@ -173,6 +175,8 @@ export function TankSandbox() {
|
||||
<Lighting />
|
||||
<TankModel />
|
||||
<TankArmor />
|
||||
<ExternalModuleMask ornamental />
|
||||
<SpacedArmorDepth ornamental />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
) : (
|
||||
|
||||
+217
-204
@@ -15,6 +15,10 @@ import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDe
|
||||
import { useDuel } from '../../../../../../../../../stores/duel';
|
||||
import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia';
|
||||
|
||||
interface ExternalModuleMaskProps {
|
||||
ornamental?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* When rendered, generates a mask and thickness buffer for external modules.
|
||||
* - R: 1 means external module
|
||||
@@ -22,15 +26,120 @@ import { useTankopediaTemporary } from '../../../../../../../../../stores/tankop
|
||||
* - B: no data
|
||||
* - A: 1 means is armor; 0 is background
|
||||
*/
|
||||
export const ExternalModuleMask = memo(() => {
|
||||
const protagonist = useDuel((state) => state.protagonist!);
|
||||
const wrapper = useRef<Group>(null);
|
||||
const modelDefinitions = useModelDefinitions();
|
||||
const turretContainer = useRef<Group>(null);
|
||||
const gunContainer = useRef<Group>(null);
|
||||
const initialTankopediaState = useTankopediaTemporary.getState();
|
||||
export const ExternalModuleMask = memo<ExternalModuleMaskProps>(
|
||||
({ ornamental = false }) => {
|
||||
const protagonist = useDuel((state) => state.protagonist!);
|
||||
const wrapper = useRef<Group>(null);
|
||||
const modelDefinitions = useModelDefinitions();
|
||||
const turretContainer = useRef<Group>(null);
|
||||
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(
|
||||
tankModelDefinition.hullOrigin[0],
|
||||
tankModelDefinition.hullOrigin[1],
|
||||
@@ -46,223 +155,127 @@ export const ExternalModuleMask = memo(() => {
|
||||
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);
|
||||
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 (yaw === undefined) return;
|
||||
if (!isVisible || thickness === undefined || spaced) return null;
|
||||
|
||||
turretPosition
|
||||
.set(0, 0, 0)
|
||||
.sub(turretOrigin)
|
||||
.sub(hullOrigin)
|
||||
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
||||
turretRotation.set(0, 0, yaw);
|
||||
return (
|
||||
<ArmorMeshExternalModuleMask
|
||||
ornamental={ornamental}
|
||||
exclude
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
|
||||
if (tankModelDefinition.turretRotation) {
|
||||
const initialPitch = -degToRad(
|
||||
tankModelDefinition.turretRotation.pitch,
|
||||
);
|
||||
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
|
||||
const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
|
||||
{modelNodes.map((node) => {
|
||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||
const isTrack = node.name.startsWith('chassis_track_');
|
||||
const isVisible = isWheel || isTrack;
|
||||
|
||||
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 { thickness, spaced } = resolveArmor(
|
||||
tankModelDefinition.armor,
|
||||
armorId,
|
||||
);
|
||||
|
||||
if (!isVisible || thickness === undefined || spaced) return null;
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<ArmorMeshExternalModuleMask
|
||||
exclude
|
||||
ornamental={ornamental}
|
||||
maxThickness={maxThickness}
|
||||
thickness={tankModelDefinition.trackThickness}
|
||||
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;
|
||||
<group ref={turretContainer}>
|
||||
<group position={hullOrigin}>
|
||||
{armorNodes.map((node) => {
|
||||
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 (
|
||||
<ArmorMeshExternalModuleMask
|
||||
maxThickness={maxThickness}
|
||||
thickness={tankModelDefinition.trackThickness}
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<ArmorMeshExternalModuleMask
|
||||
ornamental={ornamental}
|
||||
exclude
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
position={turretOrigin}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
|
||||
<group ref={turretContainer}>
|
||||
<group position={hullOrigin}>
|
||||
{armorNodes.map((node) => {
|
||||
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,
|
||||
);
|
||||
<group ref={gunContainer}>
|
||||
{armorNodes.map((node) => {
|
||||
const isCurrentGun = node.name.startsWith(
|
||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
||||
);
|
||||
const isVisible = isCurrentGun;
|
||||
const armorId = nameToArmorId(node.name);
|
||||
const { thickness, spaced } = resolveArmor(
|
||||
gunModelDefinition.armor,
|
||||
armorId,
|
||||
);
|
||||
|
||||
if (!isVisible || thickness === undefined || spaced) return null;
|
||||
if (!isVisible || thickness === undefined || spaced) return null;
|
||||
|
||||
return (
|
||||
<ArmorMeshExternalModuleMask
|
||||
exclude
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
position={turretOrigin}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
return (
|
||||
<ArmorMeshExternalModuleMask
|
||||
ornamental={ornamental}
|
||||
exclude
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
position={turretOrigin.clone().add(gunOrigin)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<group ref={gunContainer}>
|
||||
{armorNodes.map((node) => {
|
||||
const isCurrentGun = node.name.startsWith(
|
||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
||||
);
|
||||
const isVisible = isCurrentGun;
|
||||
const armorId = nameToArmorId(node.name);
|
||||
const { thickness, spaced } = resolveArmor(
|
||||
gunModelDefinition.armor,
|
||||
armorId,
|
||||
);
|
||||
{modelNodes.map((node) => {
|
||||
const isCurrentGun =
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
||||
const isVisible = isCurrentGun;
|
||||
|
||||
if (!isVisible || thickness === undefined || spaced) return null;
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<ArmorMeshExternalModuleMask
|
||||
exclude
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
position={turretOrigin.clone().add(gunOrigin)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{modelNodes.map((node) => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<ArmorMeshExternalModuleMask
|
||||
ornamental={ornamental}
|
||||
maxThickness={maxThickness}
|
||||
thickness={gunModelDefinition.barrelThickness}
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
+200
-187
@@ -15,6 +15,10 @@ import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDe
|
||||
import { useDuel } from '../../../../../../../../../stores/duel';
|
||||
import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia';
|
||||
|
||||
interface SpacedArmorDepthProps {
|
||||
ornamental?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -26,17 +30,122 @@ import { useTankopediaTemporary } from '../../../../../../../../../stores/tankop
|
||||
* - B: no data
|
||||
* - A: 1 means is armor; 0 is background
|
||||
*/
|
||||
export const SpacedArmorDepth = memo(() => {
|
||||
const protagonist = useDuel((state) => state.protagonist!);
|
||||
const wrapper = useRef<Group>(null);
|
||||
const modelDefinitions = useModelDefinitions();
|
||||
const turretContainer = useRef<Group>(null);
|
||||
const gunContainer = useRef<Group>(null);
|
||||
const initialTankopediaState = useTankopediaTemporary.getState();
|
||||
export const SpacedArmorDepth = memo<SpacedArmorDepthProps>(
|
||||
({ ornamental = false }) => {
|
||||
const protagonist = useDuel((state) => state.protagonist!);
|
||||
const wrapper = useRef<Group>(null);
|
||||
const modelDefinitions = useModelDefinitions();
|
||||
const turretContainer = useRef<Group>(null);
|
||||
const gunContainer = useRef<Group>(null);
|
||||
const initialTankopediaState = useTankopediaTemporary.getState();
|
||||
|
||||
useEffect(() => {
|
||||
if (!modelDefinitions) return;
|
||||
useEffect(() => {
|
||||
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(
|
||||
tankModelDefinition.hullOrigin[0],
|
||||
tankModelDefinition.hullOrigin[1],
|
||||
@@ -52,165 +161,20 @@ export const SpacedArmorDepth = memo(() => {
|
||||
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(
|
||||
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}>
|
||||
return (
|
||||
<group
|
||||
ref={wrapper}
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
visible={initialTankopediaState.mode === 'armor'}
|
||||
>
|
||||
<group position={hullOrigin}>
|
||||
{armorNodes.map((node) => {
|
||||
const isCurrentTurret = node.name.startsWith(
|
||||
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
||||
);
|
||||
const isVisible = isCurrentTurret;
|
||||
const isHull = node.name.startsWith('hull_');
|
||||
const isVisible = isHull;
|
||||
const armorId = nameToArmorId(node.name);
|
||||
const { spaced, thickness } = resolveArmor(
|
||||
turretModelDefinition.armor,
|
||||
tankModelDefinition.armor,
|
||||
armorId,
|
||||
);
|
||||
|
||||
@@ -218,27 +182,44 @@ export const SpacedArmorDepth = memo(() => {
|
||||
|
||||
return (
|
||||
<ArmorMeshSpacedArmorDepth
|
||||
ornamental={ornamental}
|
||||
include={spaced}
|
||||
isExternalModule={false}
|
||||
thickness={thickness}
|
||||
maxThickness={maxThickness}
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
position={turretOrigin}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</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}>
|
||||
{armorNodes.map((node) => {
|
||||
const isCurrentGun = node.name.startsWith(
|
||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
||||
const isCurrentTurret = node.name.startsWith(
|
||||
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
||||
);
|
||||
const isVisible = isCurrentGun;
|
||||
const isVisible = isCurrentTurret;
|
||||
const armorId = nameToArmorId(node.name);
|
||||
const { spaced, thickness } = resolveArmor(
|
||||
gunModelDefinition.armor,
|
||||
turretModelDefinition.armor,
|
||||
armorId,
|
||||
);
|
||||
|
||||
@@ -246,36 +227,68 @@ export const SpacedArmorDepth = memo(() => {
|
||||
|
||||
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)}
|
||||
position={turretOrigin}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</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) => {
|
||||
const isCurrentGun =
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
||||
const isVisible = isCurrentGun;
|
||||
if (!isVisible || thickness === undefined) return null;
|
||||
|
||||
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 (
|
||||
<ArmorMeshSpacedArmorDepth
|
||||
isExternalModule
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{modelNodes.map((node) => {
|
||||
const isCurrentGun =
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
||||
const isVisible = isCurrentGun;
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<ArmorMeshSpacedArmorDepth
|
||||
ornamental={ornamental}
|
||||
isExternalModule
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -6,7 +6,10 @@ import { canSplash } from '../../../../core/blitz/canSplash';
|
||||
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
||||
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
|
||||
import { useDuel } from '../../../../stores/duel';
|
||||
import { useTankopediaPersistent } from '../../../../stores/tankopedia';
|
||||
import {
|
||||
useTankopediaPersistent,
|
||||
useTankopediaTemporary,
|
||||
} from '../../../../stores/tankopedia';
|
||||
import { externalModuleMaskRenderTarget } from '../ExternalModuleMask';
|
||||
import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth';
|
||||
import fragmentShader from './shaders/fragment.glsl';
|
||||
@@ -17,6 +20,11 @@ interface ArmorMeshProps extends MeshProps {
|
||||
maxThickness: number;
|
||||
}
|
||||
|
||||
export type ArmorMeshUserData = {
|
||||
type: 'coreArmor' | 'spacedArmor' | 'externalModule';
|
||||
thickness: number;
|
||||
};
|
||||
|
||||
export function ArmorMesh({
|
||||
thickness,
|
||||
maxThickness,
|
||||
@@ -109,7 +117,26 @@ export function ArmorMesh({
|
||||
<meshBasicMaterial colorWrite={false} />
|
||||
</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
|
||||
ref={material}
|
||||
fragmentShader={fragmentShader}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { MeshProps } from '@react-three/fiber';
|
||||
import { Color, WebGLRenderTarget } from 'three';
|
||||
import { ArmorMeshUserData } from './Armor';
|
||||
|
||||
export const externalModuleMaskRenderTarget = new WebGLRenderTarget();
|
||||
|
||||
type ArmorMeshExternalModuleMaskProps = MeshProps &
|
||||
(
|
||||
type ArmorMeshExternalModuleMaskProps = MeshProps & {
|
||||
ornamental?: boolean;
|
||||
} & (
|
||||
| {
|
||||
exclude?: false;
|
||||
maxThickness: number;
|
||||
@@ -15,22 +17,34 @@ type ArmorMeshExternalModuleMaskProps = MeshProps &
|
||||
}
|
||||
);
|
||||
|
||||
export function ArmorMeshExternalModuleMask(
|
||||
props: ArmorMeshExternalModuleMaskProps,
|
||||
) {
|
||||
export function ArmorMeshExternalModuleMask({
|
||||
ornamental = false,
|
||||
...props
|
||||
}: ArmorMeshExternalModuleMaskProps) {
|
||||
return (
|
||||
<>
|
||||
{props.exclude && (
|
||||
{(props.exclude || ornamental) && (
|
||||
<mesh {...props} renderOrder={0}>
|
||||
<meshBasicMaterial colorWrite={false} />
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
{!props.exclude && (
|
||||
<mesh {...props} renderOrder={1}>
|
||||
<mesh
|
||||
{...props}
|
||||
renderOrder={1}
|
||||
onClick={ornamental ? () => {} : undefined}
|
||||
userData={
|
||||
{
|
||||
type: 'externalModule',
|
||||
thickness: props.thickness,
|
||||
} satisfies ArmorMeshUserData
|
||||
}
|
||||
>
|
||||
<meshBasicMaterial
|
||||
color={new Color(1, props.thickness / props.maxThickness, 0)}
|
||||
depthWrite={false}
|
||||
colorWrite={!ornamental}
|
||||
/>
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { MeshProps } from '@react-three/fiber';
|
||||
import { WebGLRenderTarget } from 'three';
|
||||
import { ArmorMeshUserData } from '../Armor';
|
||||
import fragmentShader from './shaders/fragment.glsl';
|
||||
import vertexShader from './shaders/vertex.glsl';
|
||||
|
||||
export const spacedArmorDepthRenderTarget = new WebGLRenderTarget();
|
||||
|
||||
type ArmorMeshSpacedArmorDepthProps = MeshProps &
|
||||
(
|
||||
type ArmorMeshSpacedArmorDepthProps = MeshProps & {
|
||||
ornamental?: boolean;
|
||||
} & (
|
||||
| {
|
||||
include?: false;
|
||||
isExternalModule: boolean;
|
||||
@@ -18,27 +20,44 @@ type ArmorMeshSpacedArmorDepthProps = MeshProps &
|
||||
}
|
||||
);
|
||||
|
||||
export function ArmorMeshSpacedArmorDepth(
|
||||
props: ArmorMeshSpacedArmorDepthProps,
|
||||
) {
|
||||
export function ArmorMeshSpacedArmorDepth({
|
||||
ornamental = false,
|
||||
...props
|
||||
}: ArmorMeshSpacedArmorDepthProps) {
|
||||
return (
|
||||
<>
|
||||
{!props.include && (
|
||||
{!props.include && !ornamental && (
|
||||
<mesh {...props} renderOrder={props.isExternalModule ? 2 : 0}>
|
||||
<meshBasicMaterial colorWrite={false} />
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
{props.include && (
|
||||
<mesh {...props} renderOrder={1}>
|
||||
<shaderMaterial
|
||||
fragmentShader={fragmentShader}
|
||||
vertexShader={vertexShader}
|
||||
uniforms={{
|
||||
thickness: { value: props.thickness },
|
||||
maxThickness: { value: props.maxThickness },
|
||||
}}
|
||||
/>
|
||||
<mesh
|
||||
{...props}
|
||||
renderOrder={1}
|
||||
onClick={ornamental ? () => {} : undefined}
|
||||
userData={
|
||||
{
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in new issue
Block a user