mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-28 13:09:51 +01:00
feat: isolated tank model
This commit is contained in:
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 { Lighting } from '../Lighting';
|
||||||
import { RotationInputs } from '../RotationInputs';
|
import { RotationInputs } from '../RotationInputs';
|
||||||
import { SceneProps } from '../SceneProps';
|
import { SceneProps } from '../SceneProps';
|
||||||
|
import { TankArmor } from './components/TankArmor';
|
||||||
import { TankModel } from './components/TankModel';
|
import { TankModel } from './components/TankModel';
|
||||||
|
|
||||||
export function TankDisplay() {
|
export function TankDisplay() {
|
||||||
@@ -134,6 +135,7 @@ export function TankDisplay() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TankModel ref={hullContainer} />
|
<TankModel ref={hullContainer} />
|
||||||
|
<TankArmor />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,82 +1,65 @@
|
|||||||
import { useLoader } from '@react-three/fiber';
|
import { forwardRef } from 'react';
|
||||||
import {
|
import { Group } from 'three';
|
||||||
forwardRef,
|
import { HeadsUpDisplay } from '../../../../../../../components/HeadsUpDisplay';
|
||||||
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';
|
|
||||||
|
|
||||||
export const TankArmor = forwardRef<Group>((_props, ref) => {
|
interface TankArmorProps {}
|
||||||
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`),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
export const TankArmor = forwardRef<Group, TankArmorProps>((_props, ref) => {
|
||||||
return (
|
return (
|
||||||
<group ref={hullContainer} rotation={[-Math.PI / 2, 0, 0]}>
|
<HeadsUpDisplay>
|
||||||
{Object.values(gltf.nodes).map((node) => {
|
<mesh>
|
||||||
const armor = Number(node.name.match(/.+_armor_(\d+)/)?.[1]);
|
<boxGeometry />
|
||||||
const position = new Vector3();
|
<meshBasicMaterial color="red" />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
if (node.name.startsWith('turret_')) position.add(turretOrigin);
|
{/* {armorObjects.map((object) => {
|
||||||
if (node.name.startsWith('gun_'))
|
const isHull = object.name.startsWith('hull_');
|
||||||
position.add(turretOrigin).add(gunOrigin);
|
const isVisible = isHull;
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<ArmorMesh key={object.uuid} geometry={(object as Mesh).geometry} />
|
||||||
scale={1 + 1e-3}
|
|
||||||
key={node.uuid}
|
|
||||||
geometry={(node as Mesh).geometry}
|
|
||||||
position={position}
|
|
||||||
>
|
|
||||||
<meshBasicMaterial color="red" transparent opacity={0.5} />
|
|
||||||
</mesh>
|
|
||||||
);
|
);
|
||||||
})}
|
})} */}
|
||||||
</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>
|
||||||
|
);
|
||||||
|
});
|
||||||
-171
@@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
-135
@@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
-191
@@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user