feat: isolated tank model

This commit is contained in:
TrèsAbhi committed 2024-01-10 13:00:37 -06:00
1 parent 4a6b5e3aa9
commit 9e0c4ddb9a
10 files changed
+547 -769

No files matched your search

@@ -20,6 +20,7 @@ import { Controls } from '../Control';
import { Lighting } from '../Lighting';
import { RotationInputs } from '../RotationInputs';
import { SceneProps } from '../SceneProps';
import { TankArmor } from './components/TankArmor';
import { TankModel } from './components/TankModel';
export function TankDisplay() {
@@ -134,6 +135,7 @@ export function TankDisplay() {
}
>
<TankModel ref={hullContainer} />
<TankArmor />
</Suspense>
</Canvas>
</div>
@@ -1,82 +1,65 @@
import { useLoader } from '@react-three/fiber';
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import { Group, Mesh, Vector3 } from 'three';
import { GLTFLoader } from 'three-stdlib';
import { X_AXIS } from '../../../../../../../constants/axis';
import { asset } from '../../../../../../../core/blitzkrieg/asset';
import {
ModelDefinitions,
modelDefinitions,
} from '../../../../../../../core/blitzkrieg/modelDefinitions';
import { useTankopedia } from '../../../../../../../stores/tankopedia';
import { forwardRef } from 'react';
import { Group } from 'three';
import { HeadsUpDisplay } from '../../../../../../../components/HeadsUpDisplay';
export const TankArmor = forwardRef<Group>((_props, ref) => {
const hullContainer = useRef<Group>(null);
const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.protagonist;
});
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
ModelDefinitions | undefined
>(undefined);
useEffect(() => {
(async () => {
setAwaitedModelDefinitions(await modelDefinitions);
})();
}, []);
useImperativeHandle(ref, () => hullContainer.current!);
if (!awaitedModelDefinitions) return null;
if (!protagonist) return null;
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2);
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const gltf = useLoader(
GLTFLoader,
asset(`3d/tanks/armor/${protagonist.tank.id}.glb`),
);
interface TankArmorProps {}
export const TankArmor = forwardRef<Group, TankArmorProps>((_props, ref) => {
return (
<group ref={hullContainer} rotation={[-Math.PI / 2, 0, 0]}>
{Object.values(gltf.nodes).map((node) => {
const armor = Number(node.name.match(/.+_armor_(\d+)/)?.[1]);
const position = new Vector3();
<HeadsUpDisplay>
<mesh>
<boxGeometry />
<meshBasicMaterial color="red" />
</mesh>
if (node.name.startsWith('turret_')) position.add(turretOrigin);
if (node.name.startsWith('gun_'))
position.add(turretOrigin).add(gunOrigin);
{/* {armorObjects.map((object) => {
const isHull = object.name.startsWith('hull_');
const isVisible = isHull;
if (!isVisible) return null;
return (
<mesh
scale={1 + 1e-3}
key={node.uuid}
geometry={(node as Mesh).geometry}
position={position}
>
<meshBasicMaterial color="red" transparent opacity={0.5} />
</mesh>
<ArmorMesh key={object.uuid} geometry={(object as Mesh).geometry} />
);
})}
</group>
})} */}
{/* {armorObjects.map((object) => {
const isCurrentTurret = object.name.startsWith(
`turret_${model.toString().padStart(2, '0')}`,
);
const isVisible = isCurrentTurret;
if (!isVisible) return null;
return (
<mesh
position={turretOrigin}
key={object.uuid}
geometry={(object as Mesh).geometry}
>
<ArmorMesh />
</mesh>
);
})} */}
{/* {armorObjects.map((object) => {
const isCurrentGun = object.name.startsWith(
`gun_${model.toString().padStart(2, '0')}`,
);
const isVisible = isCurrentGun;
if (!isVisible) return null;
return (
<mesh
position={turretOrigin.clone().add(gunOrigin)}
key={object.uuid}
geometry={(object as Mesh).geometry}
>
<ArmorHighlightMaterial />
</mesh>
);
})} */}
</HeadsUpDisplay>
);
});
@@ -0,0 +1,414 @@
import { ThreeEvent, useLoader, useThree } from '@react-three/fiber';
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import {
Euler,
Group,
Mesh,
MeshStandardMaterial,
Vector2,
Vector3,
} from 'three';
import { GLTFLoader } from 'three-stdlib';
import { X_AXIS } from '../../../../../../../constants/axis';
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
import { asset } from '../../../../../../../core/blitzkrieg/asset';
import {
ModelDefinitions,
modelDefinitions,
} from '../../../../../../../core/blitzkrieg/modelDefinitions';
import { resolveJsxTree } from '../../../../../../../core/blitzkrieg/resolveJsxTree';
import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle180';
import mutateTankopedia, {
useTankopedia,
} from '../../../../../../../stores/tankopedia';
export const TankModel = forwardRef<Group>((_props, ref) => {
// "TypeError: dispatcher.use is not a function"
// const awaitedModelDefinitions = use(modelDefinitions);
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
ModelDefinitions | undefined
>(undefined);
const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.protagonist;
});
const canvas = useThree((state) => state.gl.domElement);
if (!protagonist) return null;
const modelGltf = useLoader(
GLTFLoader,
asset(`3d/tanks/models/${protagonist.tank.id}.glb`),
);
const armorGltf = useLoader(
GLTFLoader,
asset(`3d/tanks/armor/${protagonist.tank.id}.glb`),
);
const model = useTankopedia((state) => state.model);
const hullContainer = useRef<Group>(null);
const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
const nodes = Object.values(modelGltf.nodes);
useImperativeHandle(ref, () => hullContainer.current!);
useEffect(() => {
(async () => {
setAwaitedModelDefinitions(await modelDefinitions);
})();
}, []);
if (!awaitedModelDefinitions) return null;
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2);
const turretPosition = new Vector3()
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), model.turretYaw);
const turretRotation = new Euler(0, 0, model.turretYaw);
if (tankModelDefinition.turretRotation) {
const pitch = -tankModelDefinition.turretRotation.pitch * (Math.PI / 180);
const yaw = -tankModelDefinition.turretRotation.yaw * (Math.PI / 180);
const roll = -tankModelDefinition.turretRotation.roll * (Math.PI / 180);
turretPosition
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
.applyAxisAngle(new Vector3(0, 1, 0), roll)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
turretRotation.x += pitch;
turretRotation.y += roll;
turretRotation.z += yaw;
}
turretPosition.add(turretOrigin);
return (
<group ref={hullContainer} rotation={[-Math.PI / 2, 0, model.hullYaw]}>
{nodes.map((node) => {
const isHull = node.name === 'hull';
const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_');
const isVisible = isHull || isWheel || isTrack;
let draftHullYaw = 0;
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
mutateTankopedia((draft) => {
draft.model.controlsEnabled = false;
});
if (isHull) {
draftHullYaw = model.hullYaw;
window.addEventListener('pointermove', handlePointerMoveHull);
window.addEventListener('pointerup', handlePointerUpHull);
} else if (isTrack) {
window.addEventListener('pointermove', handlePointerMoveTrack);
window.addEventListener('pointerup', handlePointerUpTrack);
}
}
function handlePointerMoveHull(event: PointerEvent) {
draftHullYaw += event.movementX * ((2 * Math.PI) / canvas.width);
if (hullContainer.current) {
hullContainer.current.rotation.z = draftHullYaw;
}
}
function handlePointerUpHull() {
mutateTankopedia((draft) => {
draft.model.controlsEnabled = true;
draft.model.hullYaw = normalizeAnglePI(draftHullYaw);
});
window.removeEventListener('pointermove', handlePointerMoveHull);
window.removeEventListener('pointerup', handlePointerUpHull);
}
function handlePointerMoveTrack(event: PointerEvent) {
const mesh = node as Mesh;
const material = mesh.material as MeshStandardMaterial;
const offset = new Vector2(
0,
event.movementY * (6 / canvas.height) +
event.movementX * (6 / canvas.width),
);
material.map?.offset.add(offset);
material.roughnessMap?.offset.add(offset);
material.normalMap?.offset.add(offset);
}
function handlePointerUpTrack() {
mutateTankopedia((draft) => {
draft.model.controlsEnabled = true;
});
window.removeEventListener('pointermove', handlePointerMoveTrack);
window.removeEventListener('pointerup', handlePointerUpTrack);
}
if (!isVisible) return null;
return (
<mesh
children={resolveJsxTree(node)}
key={node.uuid}
castShadow
receiveShadow
geometry={(node as Mesh).geometry}
material={(node as Mesh).material}
position={node.position}
rotation={node.rotation}
scale={node.scale}
onPointerDown={handlePointerDown}
/>
);
})}
<group
position={turretPosition}
rotation={turretRotation}
ref={turretContainer}
>
{nodes.map((node) => {
const isTurret = node.name.startsWith('turret_');
const isCurrentTurret =
node.name ===
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`;
const isVisible = isCurrentTurret;
let draftPitch = 0;
let draftYaw = 0;
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
if (isTurret) {
draftYaw = model.turretYaw;
draftPitch = model.gunPitch;
mutateTankopedia((draft) => {
draft.model.controlsEnabled = false;
});
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
}
function handlePointerMove(event: PointerEvent) {
[draftPitch, draftYaw] = applyPitchYawLimits(
draftPitch,
draftYaw + event.movementX * (Math.PI / canvas.width),
gunModelDefinition.pitch,
turretModelDefinition.yaw,
);
if (gunContainer.current) {
gunContainer.current.position
.set(0, 0, 0)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(new Vector3(1, 0, 0), draftPitch)
.add(turretOrigin)
.add(gunOrigin);
gunContainer.current.rotation.x = draftPitch;
}
if (turretContainer.current) {
turretContainer.current.position
.set(0, 0, 0)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), draftYaw);
turretContainer.current.rotation.z = draftYaw;
if (tankModelDefinition.turretRotation) {
const pitch =
-tankModelDefinition.turretRotation.pitch * (Math.PI / 180);
const yaw =
-tankModelDefinition.turretRotation.yaw * (Math.PI / 180);
const roll =
-tankModelDefinition.turretRotation.roll * (Math.PI / 180);
turretContainer.current.position
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
.applyAxisAngle(new Vector3(0, 1, 0), roll)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
}
turretContainer.current.position.add(turretOrigin);
}
}
function handlePointerUp() {
mutateTankopedia((state) => {
state.model.controlsEnabled = true;
state.model.gunPitch = normalizeAnglePI(draftPitch);
state.model.turretYaw = normalizeAnglePI(draftYaw);
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
if (!isVisible) return null;
return (
<mesh
visible={isVisible}
onPointerDown={handlePointerDown}
children={resolveJsxTree(node)}
key={node.uuid}
castShadow
receiveShadow
geometry={(node as Mesh).geometry}
material={(node as Mesh).material}
position={node.position}
rotation={node.rotation}
scale={node.scale}
/>
);
})}
<group
position={new Vector3()
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(new Vector3(1, 0, 0), model.gunPitch)
.add(turretOrigin)
.add(gunOrigin)}
rotation={[model.gunPitch, 0, 0]}
ref={gunContainer}
>
{nodes.map((node) => {
const isCurrentMantlet =
node.name ===
`gun_${gunModelDefinition.model
.toString()
.padStart(2, '0')}_mask`;
const isCurrentGun =
node.name ===
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
const isVisible = isCurrentGun || isCurrentMantlet;
let draftPitch = 0;
let draftYaw = 0;
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
mutateTankopedia((draft) => {
draft.model.controlsEnabled = false;
});
draftPitch = model.gunPitch;
draftYaw = model.turretYaw;
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
[draftPitch, draftYaw] = applyPitchYawLimits(
draftPitch - event.movementY * (Math.PI / canvas.height),
draftYaw + event.movementX * (Math.PI / canvas.width),
gunModelDefinition.pitch,
turretModelDefinition.yaw,
);
if (gunContainer.current) {
gunContainer.current.position
.set(0, 0, 0)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(new Vector3(1, 0, 0), draftPitch)
.add(turretOrigin)
.add(gunOrigin);
gunContainer.current.rotation.x = draftPitch;
}
if (turretContainer.current) {
turretContainer.current.position
.set(0, 0, 0)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), draftYaw);
turretContainer.current.rotation.z = draftYaw;
if (tankModelDefinition.turretRotation) {
const pitch =
-tankModelDefinition.turretRotation.pitch * (Math.PI / 180);
const yaw =
-tankModelDefinition.turretRotation.yaw * (Math.PI / 180);
const roll =
-tankModelDefinition.turretRotation.roll * (Math.PI / 180);
turretContainer.current.position
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
.applyAxisAngle(new Vector3(0, 1, 0), roll)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
}
turretContainer.current.position.add(turretOrigin);
}
}
function handlePointerUp() {
mutateTankopedia((state) => {
state.model.controlsEnabled = true;
state.model.gunPitch = normalizeAnglePI(draftPitch);
state.model.turretYaw = normalizeAnglePI(draftYaw);
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
if (!isVisible) return null;
return (
<mesh
visible={isVisible}
onPointerDown={handlePointerDown}
children={resolveJsxTree(node)}
key={node.uuid}
castShadow
receiveShadow
geometry={(node as Mesh).geometry}
material={(node as Mesh).material}
position={node.position}
rotation={node.rotation}
scale={node.scale}
/>
);
})}
</group>
{/* <GunContainer
ref={gunContainer}
armorObjects={Object.values(armorGltf.nodes)}
initialTurretRotation={tankModelDefinition.turretRotation}
onPitchStart={() => {
mutateTankopedia((state) => {
state.model.controlsEnabled = false;
});
}}
pitchLimits={gunModelDefinition.pitch}
turretContainer={turretContainer}
yaw={model.turretYaw}
onPitchEnd={(pitch, yaw) => {
mutateTankopedia((state) => {
state.model.controlsEnabled = true;
state.model.gunPitch = pitch;
state.model.turretYaw = yaw;
});
}}
yawLimits={turretModelDefinition.yaw}
gunOrigin={gunOrigin}
pitch={model.gunPitch}
model={gunModelDefinition.model}
modelObjects={Object.values(modelGltf.nodes)}
turretOrigin={turretOrigin}
/> */}
</group>
</group>
);
});
@@ -1,171 +0,0 @@
import { ThreeEvent, useThree } from '@react-three/fiber';
import { RefObject, forwardRef, useImperativeHandle, useRef } from 'react';
import { Group, Mesh, Object3D, Vector3 } from 'three';
import { ArmorHighlightMaterial } from '../../../../../../../../../components/ArmorHighlightMaterial';
import { applyPitchYawLimits } from '../../../../../../../../../core/blitz/applyPitchYawLimits';
import {
InitialTurretRotation,
PitchLimits,
YawLimits,
} from '../../../../../../../../../core/blitzkrieg/modelDefinitions';
import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree';
import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180';
interface GunContainerProps {
turretOrigin: Vector3;
gunOrigin: Vector3;
pitch: number;
yaw: number;
modelObjects: Object3D[];
armorObjects: Object3D[];
model: number;
yawLimits?: YawLimits;
pitchLimits: PitchLimits;
onPitchStart: () => void;
onPitchEnd: (pitch: number, yaw: number) => void;
initialTurretRotation?: InitialTurretRotation;
turretContainer: RefObject<Group>;
}
export const GunContainer = forwardRef<Group, GunContainerProps>(
(
{
gunOrigin,
turretOrigin,
pitch,
initialTurretRotation,
yaw,
modelObjects,
yawLimits,
onPitchStart,
turretContainer,
armorObjects,
pitchLimits,
model,
onPitchEnd,
}: GunContainerProps,
ref,
) => {
const canvas = useThree((state) => state.gl.domElement);
const gunContainer = useRef<Group>(null);
useImperativeHandle(ref, () => gunContainer.current!);
return (
<group
position={new Vector3()
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
.add(turretOrigin)
.add(gunOrigin)}
rotation={[pitch, 0, 0]}
ref={gunContainer}
>
{modelObjects.map((object) => {
const isCurrentMantlet =
object.name === `gun_${model.toString().padStart(2, '0')}_mask`;
const isCurrentGun =
object.name === `gun_${model.toString().padStart(2, '0')}`;
const isVisible = isCurrentGun || isCurrentMantlet;
let draftPitch = 0;
let draftYaw = 0;
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
onPitchStart();
draftPitch = pitch;
draftYaw = yaw;
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
[draftPitch, draftYaw] = applyPitchYawLimits(
draftPitch - event.movementY * (Math.PI / canvas.height),
draftYaw + event.movementX * (Math.PI / canvas.width),
pitchLimits,
yawLimits,
);
if (gunContainer.current) {
gunContainer.current.position
.set(0, 0, 0)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(new Vector3(1, 0, 0), draftPitch)
.add(turretOrigin)
.add(gunOrigin);
gunContainer.current.rotation.x = draftPitch;
}
if (turretContainer.current) {
turretContainer.current.position
.set(0, 0, 0)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), draftYaw);
turretContainer.current.rotation.z = draftYaw;
if (initialTurretRotation) {
const pitch = -initialTurretRotation.pitch * (Math.PI / 180);
const yaw = -initialTurretRotation.yaw * (Math.PI / 180);
const roll = -initialTurretRotation.roll * (Math.PI / 180);
turretContainer.current.position
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
.applyAxisAngle(new Vector3(0, 1, 0), roll)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
}
turretContainer.current.position.add(turretOrigin);
}
}
function handlePointerUp() {
onPitchEnd(
normalizeAnglePI(draftPitch),
normalizeAnglePI(draftYaw),
);
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
if (!isVisible) return null;
return (
<mesh
visible={isVisible}
onPointerDown={handlePointerDown}
children={resolveJsxTree(object)}
key={object.uuid}
castShadow
receiveShadow
geometry={(object as Mesh).geometry}
material={(object as Mesh).material}
position={object.position}
rotation={object.rotation}
scale={object.scale}
/>
);
})}
{armorObjects.map((object) => {
const isCurrentGun = object.name.startsWith(
`gun_${model.toString().padStart(2, '0')}`,
);
const isVisible = isCurrentGun;
if (!isVisible) return null;
return (
<mesh
position={turretOrigin.clone().add(gunOrigin)}
key={object.uuid}
geometry={(object as Mesh).geometry}
>
<ArmorHighlightMaterial />
</mesh>
);
})}
</group>
);
},
);
@@ -1,135 +0,0 @@
import { ThreeEvent, useThree } from '@react-three/fiber';
import { ReactNode, forwardRef, useImperativeHandle, useRef } from 'react';
import {
Euler,
Group,
Mesh,
MeshStandardMaterial,
Object3D,
Vector2,
} from 'three';
import { ArmorHighlightMaterial } from '../../../../../../../../../components/ArmorHighlightMaterial';
import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree';
import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180';
interface HullContainerProps {
children: ReactNode;
yaw: number;
modelObjects: Object3D[];
armorObjects: Object3D[];
onYawStart: () => void;
onYawEnd: (yaw: number) => void;
onTrackStart: () => void;
onTrackEnd: () => void;
}
export const HullContainer = forwardRef<Group, HullContainerProps>(
(
{
children,
yaw,
armorObjects,
modelObjects,
onYawStart,
onYawEnd,
onTrackEnd,
onTrackStart,
},
ref,
) => {
const canvas = useThree((state) => state.gl.domElement);
const hullContainer = useRef<Group>(null);
const rotation = [-Math.PI / 2, 0, yaw] as unknown as Euler;
useImperativeHandle(ref, () => hullContainer.current!);
return (
<group ref={hullContainer} rotation={rotation}>
{modelObjects.map((object) => {
const isHull = object.name === 'hull';
const isWheel = object.name.startsWith('chassis_wheel_');
const isTrack = object.name.startsWith('chassis_track_');
const isVisible = isHull || isWheel || isTrack;
let draftHullYaw = 0;
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
if (isHull) {
draftHullYaw = yaw;
onYawStart();
window.addEventListener('pointermove', handlePointerMoveHull);
window.addEventListener('pointerup', handlePointerUpHull);
} else if (isTrack) {
onTrackStart();
window.addEventListener('pointermove', handlePointerMoveTrack);
window.addEventListener('pointerup', handlePointerUpTrack);
}
}
function handlePointerMoveHull(event: PointerEvent) {
draftHullYaw += event.movementX * ((2 * Math.PI) / canvas.width);
if (hullContainer.current) {
hullContainer.current.rotation.z = draftHullYaw;
}
}
function handlePointerUpHull() {
onYawEnd(normalizeAnglePI(draftHullYaw));
window.removeEventListener('pointermove', handlePointerMoveHull);
window.removeEventListener('pointerup', handlePointerUpHull);
}
function handlePointerMoveTrack(event: PointerEvent) {
const mesh = object as Mesh;
const material = mesh.material as MeshStandardMaterial;
const offset = new Vector2(
0,
event.movementY * (6 / canvas.height) +
event.movementX * (6 / canvas.width),
);
material.map?.offset.add(offset);
material.roughnessMap?.offset.add(offset);
material.normalMap?.offset.add(offset);
}
function handlePointerUpTrack() {
onTrackEnd();
window.removeEventListener('pointermove', handlePointerMoveTrack);
window.removeEventListener('pointerup', handlePointerUpTrack);
}
if (!isVisible) return null;
return (
<mesh
children={resolveJsxTree(object)}
key={object.uuid}
castShadow
receiveShadow
geometry={(object as Mesh).geometry}
material={(object as Mesh).material}
position={object.position}
rotation={object.rotation}
scale={object.scale}
onPointerDown={handlePointerDown}
/>
);
})}
{armorObjects.map((object) => {
const isHull = object.name.startsWith('hull_');
const isVisible = isHull;
if (!isVisible) return null;
return (
<mesh key={object.uuid} geometry={(object as Mesh).geometry}>
<ArmorHighlightMaterial />
</mesh>
);
})}
{children}
</group>
);
},
);
@@ -1,191 +0,0 @@
import { ThreeEvent, useThree } from '@react-three/fiber';
import {
ReactNode,
RefObject,
forwardRef,
useImperativeHandle,
useRef,
} from 'react';
import { Euler, Group, Mesh, Object3D, Vector3 } from 'three';
import { ArmorHighlightMaterial } from '../../../../../../../../../components/ArmorHighlightMaterial';
import { applyPitchYawLimits } from '../../../../../../../../../core/blitz/applyPitchYawLimits';
import {
InitialTurretRotation,
PitchLimits,
YawLimits,
} from '../../../../../../../../../core/blitzkrieg/modelDefinitions';
import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree';
import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180';
interface TurretContainerProps {
turretOrigin: Vector3;
gunOrigin: Vector3;
modelObjects: Object3D[];
armorObjects: Object3D[];
children: ReactNode;
yaw: number;
pitch: number;
model: number;
yawLimits?: YawLimits;
pitchLimits: PitchLimits;
onYawStart: () => void;
onYawEnd: (pitch: number, yaw: number) => void;
gunContainer: RefObject<Group>;
initialTurretRotation?: InitialTurretRotation;
}
export const TurretContainer = forwardRef(
(
{
gunContainer,
turretOrigin,
yaw,
children,
modelObjects,
yawLimits,
model,
onYawEnd,
pitch,
gunOrigin,
armorObjects,
pitchLimits,
onYawStart,
initialTurretRotation,
}: TurretContainerProps,
ref,
) => {
const canvas = useThree((state) => state.gl.domElement);
const turretContainer = useRef<Group>(null);
const position = new Vector3()
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
const rotation = new Euler(0, 0, yaw);
if (initialTurretRotation) {
const pitch = -initialTurretRotation.pitch * (Math.PI / 180);
const yaw = -initialTurretRotation.yaw * (Math.PI / 180);
const roll = -initialTurretRotation.roll * (Math.PI / 180);
position
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
.applyAxisAngle(new Vector3(0, 1, 0), roll)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
rotation.x += pitch;
rotation.y += roll;
rotation.z += yaw;
}
position.add(turretOrigin);
useImperativeHandle(ref, () => turretContainer.current!);
return (
<group position={position} rotation={rotation} ref={turretContainer}>
{modelObjects.map((object) => {
const isTurret = object.name.startsWith('turret_');
const isCurrentTurret =
object.name === `turret_${model.toString().padStart(2, '0')}`;
const isVisible = isCurrentTurret;
let draftPitch = 0;
let draftYaw = 0;
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
if (isTurret) {
draftYaw = yaw;
draftPitch = pitch;
onYawStart();
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
}
function handlePointerMove(event: PointerEvent) {
[draftPitch, draftYaw] = applyPitchYawLimits(
draftPitch,
draftYaw + event.movementX * (Math.PI / canvas.width),
pitchLimits,
yawLimits,
);
if (gunContainer.current) {
gunContainer.current.position
.set(0, 0, 0)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(new Vector3(1, 0, 0), draftPitch)
.add(turretOrigin)
.add(gunOrigin);
gunContainer.current.rotation.x = draftPitch;
}
if (turretContainer.current) {
turretContainer.current.position
.set(0, 0, 0)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), draftYaw);
turretContainer.current.rotation.z = draftYaw;
if (initialTurretRotation) {
const pitch = -initialTurretRotation.pitch * (Math.PI / 180);
const yaw = -initialTurretRotation.yaw * (Math.PI / 180);
const roll = -initialTurretRotation.roll * (Math.PI / 180);
turretContainer.current.position
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
.applyAxisAngle(new Vector3(0, 1, 0), roll)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
}
turretContainer.current.position.add(turretOrigin);
}
}
function handlePointerUp() {
onYawEnd(normalizeAnglePI(draftPitch), normalizeAnglePI(draftYaw));
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
if (!isVisible) return null;
return (
<mesh
visible={isVisible}
onPointerDown={handlePointerDown}
children={resolveJsxTree(object)}
key={object.uuid}
castShadow
receiveShadow
geometry={(object as Mesh).geometry}
material={(object as Mesh).material}
position={object.position}
rotation={object.rotation}
scale={object.scale}
/>
);
})}
{armorObjects.map((object) => {
const isCurrentTurret = object.name.startsWith(
`turret_${model.toString().padStart(2, '0')}`,
);
const isVisible = isCurrentTurret;
if (!isVisible) return null;
return (
<mesh
position={turretOrigin}
key={object.uuid}
geometry={(object as Mesh).geometry}
>
<ArmorHighlightMaterial />
</mesh>
);
})}
{children}
</group>
);
},
);
@@ -1,157 +0,0 @@
import { useLoader } from '@react-three/fiber';
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import { Group, Vector3 } from 'three';
import { GLTFLoader } from 'three-stdlib';
import { X_AXIS } from '../../../../../../../../constants/axis';
import { asset } from '../../../../../../../../core/blitzkrieg/asset';
import {
ModelDefinitions,
modelDefinitions,
} from '../../../../../../../../core/blitzkrieg/modelDefinitions';
import mutateTankopedia, {
useTankopedia,
} from '../../../../../../../../stores/tankopedia';
import { GunContainer } from './components/GunContainer';
import { HullContainer } from './components/HullContainer';
import { TurretContainer } from './components/TurretContainer';
export const TankModel = forwardRef<Group>((_props, ref) => {
// "TypeError: dispatcher.use is not a function"
// const awaitedModelDefinitions = use(modelDefinitions);
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
ModelDefinitions | undefined
>(undefined);
const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.protagonist;
});
if (!protagonist) return null;
const modelGltf = useLoader(
GLTFLoader,
asset(`3d/tanks/models/${protagonist.tank.id}.glb`),
);
const armorGltf = useLoader(
GLTFLoader,
asset(`3d/tanks/armor/${protagonist.tank.id}.glb`),
);
const model = useTankopedia((state) => state.model);
const hullContainer = useRef<Group>(null);
const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
useImperativeHandle(ref, () => hullContainer.current!);
useEffect(() => {
(async () => {
setAwaitedModelDefinitions(await modelDefinitions);
})();
}, []);
if (!awaitedModelDefinitions) return null;
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2);
return (
<HullContainer
modelObjects={Object.values(modelGltf.nodes)}
armorObjects={Object.values(armorGltf.nodes)}
yaw={model.hullYaw}
ref={hullContainer}
onYawStart={() =>
mutateTankopedia((draft) => {
draft.model.controlsEnabled = false;
})
}
onYawEnd={(yaw) => {
mutateTankopedia((draft) => {
draft.model.controlsEnabled = true;
draft.model.hullYaw = yaw;
});
}}
onTrackStart={() => {
mutateTankopedia((draft) => {
draft.model.controlsEnabled = false;
});
}}
onTrackEnd={() => {
mutateTankopedia((draft) => {
draft.model.controlsEnabled = true;
});
}}
>
<TurretContainer
armorObjects={Object.values(armorGltf.nodes)}
initialTurretRotation={tankModelDefinition.turretRotation}
gunOrigin={gunOrigin}
ref={turretContainer}
modelObjects={Object.values(modelGltf.nodes)}
model={turretModelDefinition.model}
onYawStart={() =>
mutateTankopedia((state) => {
state.model.controlsEnabled = false;
})
}
yawLimits={turretModelDefinition.yaw}
pitchLimits={gunModelDefinition.pitch}
pitch={model.gunPitch}
onYawEnd={(pitch, yaw) => {
mutateTankopedia((state) => {
state.model.controlsEnabled = true;
state.model.gunPitch = pitch;
state.model.turretYaw = yaw;
});
}}
gunContainer={gunContainer}
turretOrigin={turretOrigin}
yaw={model.turretYaw}
>
<GunContainer
ref={gunContainer}
armorObjects={Object.values(armorGltf.nodes)}
initialTurretRotation={tankModelDefinition.turretRotation}
onPitchStart={() => {
mutateTankopedia((state) => {
state.model.controlsEnabled = false;
});
}}
pitchLimits={gunModelDefinition.pitch}
turretContainer={turretContainer}
yaw={model.turretYaw}
onPitchEnd={(pitch, yaw) => {
mutateTankopedia((state) => {
state.model.controlsEnabled = true;
state.model.gunPitch = pitch;
state.model.turretYaw = yaw;
});
}}
yawLimits={turretModelDefinition.yaw}
gunOrigin={gunOrigin}
pitch={model.gunPitch}
model={gunModelDefinition.model}
modelObjects={Object.values(modelGltf.nodes)}
turretOrigin={turretOrigin}
/>
</TurretContainer>
</HullContainer>
);
});
-42
View File
@@ -1,42 +0,0 @@
import { GreaterEqualDepth } from 'three';
const INFLATE = '0.0';
export function ArmorHighlightMaterial() {
return (
<shaderMaterial
depthFunc={GreaterEqualDepth}
args={[
{
transparent: true,
vertexShader: `
varying vec3 vNormal;
varying vec3 vViewPosition; // Camera position in view space
void main() {
vNormal = normalMatrix * normal; // Transform normal to view space
vViewPosition = (modelViewMatrix * vec4(position, 1.0)).xyz; // Camera position in view space
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
// Fragment Shader
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
// Use vNormal for your fragment shader calculations
vec3 normal = normalize(vNormal);
float uhhh = dot(normal, vViewPosition) * 0.5 + 1.0;
// ... rest of your fragment shader code ...
gl_FragColor = vec4(1.0, 0.0, 0.0, uhhh);
}
`,
},
]}
/>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { MeshProps } from '@react-three/fiber';
interface ArmorMeshProps extends MeshProps {}
export function ArmorMesh(props: ArmorMeshProps) {
return (
<>
<mesh {...props}>
<meshBasicMaterial colorWrite={false} />
</mesh>
<mesh {...props} renderOrder={1}>
<meshBasicMaterial
transparent
opacity={Math.random() / 2}
color="red"
depthWrite={false}
/>
</mesh>
</>
);
}
+53
View File
@@ -0,0 +1,53 @@
import { createPortal, useFrame, useThree } from '@react-three/fiber';
import { isInteger } from 'lodash';
import { ReactNode } from 'react';
import { Scene } from 'three';
interface HeadsUpDisplayProps {
priority?: number;
children: ReactNode;
}
/**
* Renders `children` above the default scene, even if the priority is a
* negative number.
*/
export function HeadsUpDisplay({
priority = 1,
children,
}: HeadsUpDisplayProps) {
const { gl: renderer, scene: defaultScene, camera } = useThree();
const scene = new Scene();
const portal = createPortal(children, scene, {
events: { priority: priority + 1 },
});
useFrame(() => {
if (priority === 1) {
renderer.clear();
renderer.render(defaultScene, camera);
}
renderer.clearDepth();
renderer.render(scene, camera);
}, priority);
renderer.autoClear = false;
if (!isInteger(priority)) {
throw new TypeError('Priority property must be an integer');
}
return portal;
}
/**
* Use this component if the default scene disappears on usage of the
* HeadsUpDisplay; caused because there are not HeadsUpDisplay components
* with `1` priority.
*/
export function RenderDefault() {
const { gl: renderer, scene, camera } = useThree();
useFrame(() => renderer.render(scene, camera));
return null;
}