fix: correctly re-mounting camera

This commit is contained in:
TrèsAbhi committed 2024-07-22 20:47:29 -05:00
1 parent ffdb75d028
commit a237e1541b
3 files changed
+288 -293

No files matched your search

@@ -1,4 +1,3 @@
import { PerspectiveCamera } from '@react-three/drei';
import { Canvas } from '@react-three/fiber'; import { Canvas } from '@react-three/fiber';
import { Suspense, use, useEffect, useRef } from 'react'; import { Suspense, use, useEffect, useRef } from 'react';
import { Armor } from '../../../../../../components/Armor'; import { Armor } from '../../../../../../components/Armor';
@@ -121,10 +120,7 @@ export function TankSandbox() {
return ( return (
<Canvas <Canvas
gl={{ gl={{ clippingPlanes: Object.freeze([]), localClippingEnabled: true }}
clippingPlanes: Object.freeze([]),
localClippingEnabled: true,
}}
shadows shadows
ref={canvas} ref={canvas}
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
@@ -133,11 +129,9 @@ export function TankSandbox() {
draft.shot = undefined; draft.shot = undefined;
}); });
}} }}
style={{ style={{ userSelect: 'none' }}
userSelect: 'none', camera={{ fov: 25, far: 32 }}
}}
> >
<PerspectiveCamera makeDefault fov={25} far={32} />
<Controls /> <Controls />
<SceneProps /> <SceneProps />
@@ -33,6 +33,37 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
const tankopediaPersistentStore = TankopediaPersistent.useStore(); const tankopediaPersistentStore = TankopediaPersistent.useStore();
const initialTankopediaState = tankopediaPersistentStore.getState(); const initialTankopediaState = tankopediaPersistentStore.getState();
const protagonist = Duel.use((draft) => draft.protagonist); const protagonist = Duel.use((draft) => draft.protagonist);
const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
const armorGltf = useArmor(tank.id);
const { gltf: modelGltf } = useModel(tank.id);
const armorNodes = Object.values(armorGltf.nodes);
const modelNodes = Object.values(modelGltf.nodes);
const tankModelDefinition = modelDefinitions[tank.id];
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = new Vector3(
trackModelDefinition.origin[0],
trackModelDefinition.origin[1],
-trackModelDefinition.origin[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 maskOrigin =
gunModelDefinition.mask === undefined
? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
useEffect(() => { useEffect(() => {
if (!modelDefinitions) return; if (!modelDefinitions) return;
@@ -120,38 +151,6 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
return unsubscribe; return unsubscribe;
}); });
const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
const armorGltf = useArmor(tank.id);
const { gltf: modelGltf } = useModel(tank.id);
const armorNodes = Object.values(armorGltf.nodes);
const modelNodes = Object.values(modelGltf.nodes);
const tankModelDefinition = modelDefinitions[tank.id];
const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const hullOrigin = new Vector3(
trackModelDefinition.origin[0],
trackModelDefinition.origin[1],
-trackModelDefinition.origin[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 maskOrigin =
gunModelDefinition.mask === undefined
? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
return ( return (
<group <group
ref={wrapper} ref={wrapper}
@@ -1,4 +1,5 @@
import { useThree } from '@react-three/fiber'; import { useThree } from '@react-three/fiber';
import { useCallback } from 'react';
import { import {
Intersection, Intersection,
MeshBasicMaterial, MeshBasicMaterial,
@@ -49,9 +50,6 @@ export type ArmorUserData = {
variant: ExternalModuleVariant; variant: ExternalModuleVariant;
} }
); );
type ArmorMeshIntersection = Intersection & {
object: Object3D & { userData: ArmorUserData };
};
const omitMaterial = new MeshBasicMaterial({ const omitMaterial = new MeshBasicMaterial({
colorWrite: false, colorWrite: false,
@@ -76,280 +74,284 @@ export function SpacedArmorSceneComponent({
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
const duelStore = Duel.useStore(); const duelStore = Duel.useStore();
async function shoot( const shoot = useCallback(
point: Vector3, async (
intersections: Intersection[], point: Vector3,
allowRicochet: boolean, intersections: Intersection[],
remainingPenetrationInput?: number, allowRicochet: boolean,
) { remainingPenetrationInput?: number,
const { antagonist, protagonist } = duelStore.getState(); ) => {
const shell = antagonist.shell; const { antagonist, protagonist } = duelStore.getState();
const cameraNormal = camera.position.clone().sub(point).normalize(); const shell = antagonist.shell;
const shot: TankopediaEphemeral.Shot = { const cameraNormal = camera.position.clone().sub(point).normalize();
damage: -1, const shot: TankopediaEphemeral.Shot = {
containsGaps: shell.type === ShellType.HEAT, damage: -1,
in: { containsGaps: shell.type === ShellType.HEAT,
surfaceNormal: cameraNormal, in: {
layers: [], surfaceNormal: cameraNormal,
status: 'blocked', layers: [],
}, status: 'blocked',
}; },
};
const hasCalibratedShells = await hasEquipment( const hasCalibratedShells = await hasEquipment(
103, 103,
antagonist.tank.equipment, antagonist.tank.equipment,
antagonist.equipmentMatrix, antagonist.equipmentMatrix,
); );
const hasEnhancedArmor = await hasEquipment( const hasEnhancedArmor = await hasEquipment(
110, 110,
protagonist.tank.equipment, protagonist.tank.equipment,
protagonist.equipmentMatrix, protagonist.equipmentMatrix,
); );
const penetration = const penetration =
resolveNearPenetration(shell.penetration) * resolveNearPenetration(shell.penetration) *
resolvePenetrationCoefficient(hasCalibratedShells, shell.type); resolvePenetrationCoefficient(hasCalibratedShells, shell.type);
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1; const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
const filteredIntersections = intersections.filter( const filteredIntersections = intersections.filter(
(intersection) => 'type' in intersection.object.userData, (intersection) => 'type' in intersection.object.userData,
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[]; ) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
const encounteredExternalModuleVariants: ExternalModuleVariant[] = []; const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
const noDuplicateIntersections: typeof filteredIntersections = []; const noDuplicateIntersections: typeof filteredIntersections = [];
for (const intersection of filteredIntersections) { for (const intersection of filteredIntersections) {
if (intersection.object.userData.type === ArmorType.External) { if (intersection.object.userData.type === ArmorType.External) {
if ( if (
!encounteredExternalModuleVariants.includes( !encounteredExternalModuleVariants.includes(
intersection.object.userData.variant, intersection.object.userData.variant,
) )
) { ) {
noDuplicateIntersections.push(intersection); noDuplicateIntersections.push(intersection);
encounteredExternalModuleVariants.push( encounteredExternalModuleVariants.push(
intersection.object.userData.variant, intersection.object.userData.variant,
); );
} }
} else {
noDuplicateIntersections.push(intersection);
if (intersection.object.userData.type === ArmorType.Core) {
break;
}
}
}
let remainingPenetration = remainingPenetrationInput ?? penetration;
let index = 0;
for (const intersection of noDuplicateIntersections) {
const layer = intersection.object.userData;
const surfaceNormal = intersection
.normal!.clone()
.applyQuaternion(
intersection.object.getWorldQuaternion(new Quaternion()),
);
if (shell.type === ShellType.HEAT && index > 0) {
const lastLayer = shot.in.layers.at(
-1,
) as TankopediaEphemeral.ShotLayerBase;
const distance = lastLayer.point.distanceTo(intersection.point);
remainingPenetration -= 0.5 * remainingPenetration * distance;
const blocked = remainingPenetration <= 0;
shot.in.layers.push({
type: null,
distance,
status: blocked ? 'blocked' : 'penetration',
});
if (blocked) break;
}
if (layer.type === ArmorType.External) {
const thickness = layer.thickness * thicknessCoefficient;
remainingPenetration -= thickness;
const blocked =
remainingPenetration <= 0 || shell.type === ShellType.HE;
shot.in.layers.push({
type: ArmorType.External,
index,
thickness,
point: intersection.point,
surfaceNormal,
status:
shell.type === ShellType.HE
? 'blocked'
: blocked
? 'blocked'
: 'penetration',
variant: layer.variant,
});
} else {
const thickness = layer.thickness;
const ricochet = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!,
);
const angle = surfaceNormal.angleTo(cameraNormal);
const threeCalibersRule =
shell.caliber > thickness * 3 || index > 0 || !allowRicochet;
const twoCalibersRule = shell.caliber > thickness * 2;
const normalization = degToRad(shell.normalization ?? 0);
const finalNormalization = twoCalibersRule
? (1.4 * normalization * shell.caliber) / (2.0 * thickness)
: normalization;
const finalThickness =
thickness / Math.cos(Math.max(0, angle - finalNormalization));
if (!threeCalibersRule && angle >= ricochet) {
remainingPenetration *= 0.75;
shot.in.layers.push({
type: layer.type,
index,
thickness,
thicknessAngled: finalThickness,
point: intersection.point,
surfaceNormal,
status: 'ricochet',
angle,
});
break;
} else { } else {
remainingPenetration -= finalThickness; noDuplicateIntersections.push(intersection);
if (intersection.object.userData.type === ArmorType.Core) {
break;
}
}
}
let remainingPenetration = remainingPenetrationInput ?? penetration;
let index = 0;
for (const intersection of noDuplicateIntersections) {
const layer = intersection.object.userData;
const surfaceNormal = intersection
.normal!.clone()
.applyQuaternion(
intersection.object.getWorldQuaternion(new Quaternion()),
);
if (shell.type === ShellType.HEAT && index > 0) {
const lastLayer = shot.in.layers.at(
-1,
) as TankopediaEphemeral.ShotLayerBase;
const distance = lastLayer.point.distanceTo(intersection.point);
remainingPenetration -= 0.5 * remainingPenetration * distance;
const blocked = remainingPenetration <= 0; const blocked = remainingPenetration <= 0;
shot.in.layers.push({ shot.in.layers.push({
type: layer.type, type: null,
distance,
status: blocked ? 'blocked' : 'penetration',
});
if (blocked) break;
}
if (layer.type === ArmorType.External) {
const thickness = layer.thickness * thicknessCoefficient;
remainingPenetration -= thickness;
const blocked =
remainingPenetration <= 0 || shell.type === ShellType.HE;
shot.in.layers.push({
type: ArmorType.External,
index, index,
thickness, thickness,
thicknessAngled: finalThickness,
point: intersection.point, point: intersection.point,
surfaceNormal: intersection.face!.normal, surfaceNormal,
status: status:
shell.type === ShellType.HE && layer.type !== ArmorType.Core shell.type === ShellType.HE
? 'blocked' ? 'blocked'
: blocked : blocked
? 'blocked' ? 'blocked'
: 'penetration', : 'penetration',
angle, variant: layer.variant,
}); });
}
}
if (shell.type !== ShellType.HE && remainingPenetration <= 0) {
break;
}
index++;
}
const lastLayer = shot.in.layers.at(-1) as
| TankopediaEphemeral.ShotLayerNonExternal
| undefined;
const firstLayer = shot.in.layers[0] as
| TankopediaEphemeral.ShotLayerBase
| undefined;
if (!lastLayer || !firstLayer) {
return null;
} else {
if (shell.type === ShellType.HE) {
const totalSpacedArmorThickness = shot.in.layers.reduce(
(acc, layer) => {
if (layer.type === null || layer.type === ArmorType.Core) {
return acc;
}
if (layer.type === ArmorType.External) {
return acc + layer.thickness;
}
return acc + layer.thicknessAngled;
},
0,
);
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
lastLayer.point,
);
const finalDamage = Math.max(
0,
0.5 *
shell.damage.armor *
(1 - distanceFromSpacedArmor / shell.explosionRadius!) -
1.1 *
(lastLayer.thicknessAngled +
Math.min(penetration, totalSpacedArmorThickness)),
);
/**
* Above is some peculiar math. We're multiplying 1.1 by the
* thickness of the core plate plus the total spaced armor
* thickness capped to the penetration itself. This is because
* the shader is normalized by the penetration value; hence,
* the total spaced armor thickness cannot ever be thicker
* than the penetration. This is a precision error on the
* shader-side and I do not intend on fixing it. Instead,
* for parity's sake, I am capping the value here.
*/
if (shot.in.layers.length > 1 || lastLayer.status === 'blocked') {
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
shot.damage = finalDamage;
} else { } else {
shot.in.status = 'penetration'; const thickness = layer.thickness;
shot.damage = shell.damage.armor; const ricochet = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!,
);
const angle = surfaceNormal.angleTo(cameraNormal);
const threeCalibersRule =
shell.caliber > thickness * 3 || index > 0 || !allowRicochet;
const twoCalibersRule = shell.caliber > thickness * 2;
const normalization = degToRad(shell.normalization ?? 0);
const finalNormalization = twoCalibersRule
? (1.4 * normalization * shell.caliber) / (2.0 * thickness)
: normalization;
const finalThickness =
thickness / Math.cos(Math.max(0, angle - finalNormalization));
if (!threeCalibersRule && angle >= ricochet) {
remainingPenetration *= 0.75;
shot.in.layers.push({
type: layer.type,
index,
thickness,
thicknessAngled: finalThickness,
point: intersection.point,
surfaceNormal,
status: 'ricochet',
angle,
});
break;
} else {
remainingPenetration -= finalThickness;
const blocked = remainingPenetration <= 0;
shot.in.layers.push({
type: layer.type,
index,
thickness,
thicknessAngled: finalThickness,
point: intersection.point,
surfaceNormal: intersection.face!.normal,
status:
shell.type === ShellType.HE && layer.type !== ArmorType.Core
? 'blocked'
: blocked
? 'blocked'
: 'penetration',
angle,
});
}
} }
if (shell.type !== ShellType.HE && remainingPenetration <= 0) {
break;
}
index++;
}
const lastLayer = shot.in.layers.at(-1) as
| TankopediaEphemeral.ShotLayerNonExternal
| undefined;
const firstLayer = shot.in.layers[0] as
| TankopediaEphemeral.ShotLayerBase
| undefined;
if (!lastLayer || !firstLayer) {
return null;
} else { } else {
if (lastLayer.status === 'blocked') { if (shell.type === ShellType.HE) {
shot.in.status = 'blocked'; const totalSpacedArmorThickness = shot.in.layers.reduce(
shot.damage = 0; (acc, layer) => {
} else if (lastLayer.status === 'penetration') { if (layer.type === null || layer.type === ArmorType.Core) {
shot.in.status = 'penetration'; return acc;
shot.damage = shell.damage.armor; }
if (layer.type === ArmorType.External) {
return acc + layer.thickness;
}
return acc + layer.thicknessAngled;
},
0,
);
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
lastLayer.point,
);
const finalDamage = Math.max(
0,
0.5 *
shell.damage.armor *
(1 - distanceFromSpacedArmor / shell.explosionRadius!) -
1.1 *
(lastLayer.thicknessAngled +
Math.min(penetration, totalSpacedArmorThickness)),
);
/**
* Above is some peculiar math. We're multiplying 1.1 by the
* thickness of the core plate plus the total spaced armor
* thickness capped to the penetration itself. This is because
* the shader is normalized by the penetration value; hence,
* the total spaced armor thickness cannot ever be thicker
* than the penetration. This is a precision error on the
* shader-side and I do not intend on fixing it. Instead,
* for parity's sake, I am capping the value here.
*/
if (shot.in.layers.length > 1 || lastLayer.status === 'blocked') {
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
shot.damage = finalDamage;
} else {
shot.in.status = 'penetration';
shot.damage = shell.damage.armor;
}
} else { } else {
const caster = new Raycaster(); if (lastLayer.status === 'blocked') {
// https://math.stackexchange.com/a/13263/1222875 shot.in.status = 'blocked';
const shellNormal = cameraNormal.clone().multiplyScalar(-1); shot.damage = 0;
const ricochetNormal = shellNormal } else if (lastLayer.status === 'penetration') {
.clone() shot.in.status = 'penetration';
.sub( shot.damage = shell.damage.armor;
lastLayer.surfaceNormal } else {
.clone() const caster = new Raycaster();
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)), // https://math.stackexchange.com/a/13263/1222875
const shellNormal = cameraNormal.clone().multiplyScalar(-1);
const ricochetNormal = shellNormal
.clone()
.sub(
lastLayer.surfaceNormal
.clone()
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)),
);
caster.set(lastLayer.point, ricochetNormal);
const ricochetIntersections = caster.intersectObjects(
scene.children,
true,
);
const outShot = await shoot(
lastLayer.point,
ricochetIntersections,
false,
remainingPenetration,
); );
caster.set(lastLayer.point, ricochetNormal); shot.in.status = 'ricochet';
if (outShot === null) {
const ricochetIntersections = caster.intersectObjects( shot.damage = 0;
scene.children, shot.out = {
true, surfaceNormal: ricochetNormal,
); layers: [],
const outShot = await shoot( status: 'ricochet',
lastLayer.point, };
ricochetIntersections, } else {
false, shot.damage = outShot.damage;
remainingPenetration, shot.out = outShot.in;
); shot.out.surfaceNormal = ricochetNormal;
}
shot.in.status = 'ricochet';
if (outShot === null) {
shot.damage = 0;
shot.out = {
surfaceNormal: ricochetNormal,
layers: [],
status: 'ricochet',
};
} else {
shot.damage = outShot.damage;
shot.out = outShot.in;
shot.out.surfaceNormal = ricochetNormal;
} }
} }
} }
}
return shot; return shot;
} },
[camera],
);
return ( return (
<> <>