feat: explosive shells under spaced armor

This commit is contained in:
TrèsAbhi committed 2024-01-15 21:55:09 -06:00
1 parent 80b161b9fc
commit 730ad56677
7 files changed
+373 -37

No files matched your search

@@ -115,7 +115,6 @@ export function TankDisplay({ duel }: TankDisplayProps) {
ref={canvas}
camera={{ fov: 20 }}
onPointerDown={handlePointerDown}
gl={{ logarithmicDepthBuffer: true }}
>
<Controls />
<SceneProps />
@@ -0,0 +1,263 @@
import { useLoader } from '@react-three/fiber';
import { memo, useEffect, useRef } from 'react';
import { Euler, Group, Mesh, Vector3 } from 'three';
import { GLTFLoader } from 'three-stdlib';
import { degToRad } from 'three/src/math/MathUtils';
import { ArmorMeshSpacedArmorDepth } from '../../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth';
import {
X_AXIS,
Y_AXIS,
Z_AXIS,
} from '../../../../../../../../../constants/axis';
import { asset } from '../../../../../../../../../core/blitzkrieg/asset';
import {
ModelTransformEventData,
modelTransformEvent,
} from '../../../../../../../../../core/blitzkrieg/modelTransform';
import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId';
import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness';
import { useModelDefinitions } from '../../../../../../../../../core/hooks/useModelDefinitions';
import { useTankopedia } from '../../../../../../../../../stores/tankopedia';
import { Duel } from '../../../../../page';
interface SpacedArmorDepthProps {
duel: Duel;
}
export const SpacedArmorDepth = memo<SpacedArmorDepthProps>(({ duel }) => {
const wrapper = useRef<Group>(null);
const awaitedModelDefinitions = useModelDefinitions();
const showSpacedArmor = useTankopedia(
(state) => state.model.visual.showSpacedArmor,
);
const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
const initialTankopediaState = useTankopedia.getState();
useEffect(() => {
if (!awaitedModelDefinitions) return;
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();
const turretRotation = new Euler();
const gunPosition = new Vector3();
const gunRotation = new Euler();
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
gunPosition
.set(0, 0, 0)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(X_AXIS, pitch)
.add(gunOrigin)
.add(turretOrigin);
gunRotation.set(pitch, 0, 0);
gunContainer.current?.position.copy(gunPosition);
gunContainer.current?.rotation.copy(gunRotation);
if (yaw === undefined) return;
turretPosition
.set(0, 0, 0)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
turretRotation.set(0, 0, yaw);
if (tankModelDefinition.turretRotation) {
const initialPitch = -degToRad(
tankModelDefinition.turretRotation.pitch,
);
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
turretPosition
.applyAxisAngle(X_AXIS, initialPitch)
.applyAxisAngle(Y_AXIS, initialRoll)
.applyAxisAngle(Z_AXIS, initialYaw);
turretRotation.x += initialPitch;
turretRotation.y += initialRoll;
turretRotation.z += initialYaw;
}
turretPosition.add(turretOrigin);
turretContainer.current?.position.copy(turretPosition);
turretContainer.current?.rotation.copy(turretRotation);
}
modelTransformEvent.on(handleModelTransform);
return () => {
modelTransformEvent.off(handleModelTransform);
};
}, [awaitedModelDefinitions]);
useEffect(() => {
const unsubscribe = useTankopedia.subscribe(
(state) => state.mode,
(mode) => {
if (wrapper.current) wrapper.current.visible = mode === 'armor';
},
);
return unsubscribe;
});
const armorGltf = useLoader(
GLTFLoader,
asset(`3d/tanks/armor/${duel.protagonist.tank.id}.glb`),
);
const modelGltf = useLoader(
GLTFLoader,
asset(`3d/tanks/models/${duel.protagonist.tank.id}.glb`),
);
const armorNodes = Object.values(armorGltf.nodes);
const modelNodes = Object.values(modelGltf.nodes);
const tankModelDefinition = awaitedModelDefinitions[duel.protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[duel.protagonist.turret.id];
const gunModelDefinition =
turretModelDefinition.guns[duel.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 (
<group
ref={wrapper}
rotation={[-Math.PI / 2, 0, 0]}
visible={initialTankopediaState.mode === 'armor'}
>
{armorNodes.map((node) => {
const isHull = node.name.startsWith('hull_');
const isVisible = isHull;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor,
armorId,
);
if (
!isVisible ||
thickness === undefined ||
(spaced && !showSpacedArmor)
)
return null;
return (
<ArmorMeshSpacedArmorDepth
exclude
key={node.uuid}
geometry={(node as Mesh).geometry}
/>
);
})}
{modelNodes.map((node) => {
const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_');
const isVisible = isWheel || isTrack;
if (!isVisible || !showSpacedArmor) return null;
return (
<ArmorMeshSpacedArmorDepth
key={node.uuid}
geometry={(node as Mesh).geometry}
/>
);
})}
<group ref={turretContainer}>
{armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor,
armorId,
);
if (
!isVisible ||
thickness === undefined ||
(spaced && !showSpacedArmor)
)
return null;
return (
<ArmorMeshSpacedArmorDepth
exclude
key={node.uuid}
geometry={(node as Mesh).geometry}
position={turretOrigin}
/>
);
})}
<group ref={gunContainer}>
{armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
);
const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor,
armorId,
);
if (
!isVisible ||
thickness === undefined ||
(spaced && !showSpacedArmor)
)
return null;
return (
<ArmorMeshSpacedArmorDepth
exclude
key={node.uuid}
geometry={(node as Mesh).geometry}
position={turretOrigin.clone().add(gunOrigin)}
/>
);
})}
{modelNodes.map((node) => {
const isCurrentGun =
node.name ===
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
const isVisible = isCurrentGun;
if (!isVisible || !showSpacedArmor) return null;
return (
<ArmorMeshSpacedArmorDepth
key={node.uuid}
geometry={(node as Mesh).geometry}
/>
);
})}
</group>
</group>
</group>
);
});
@@ -1,10 +1,12 @@
import { createPortal, useFrame } from '@react-three/fiber';
import { memo } from 'react';
import { Scene } from 'three';
import { DepthTexture, Scene } from 'three';
import { externalModuleMaskRenderTarget } from '../../../../../../../../components/ArmorMesh';
import { spacedArmorDepthRenderTarget } from '../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth';
import { Duel } from '../../../../page';
import { ArmorHighlighting } from './components/ArmorHighlighting';
import { ExternalModuleMask } from './components/ExternalModuleMask';
import { SpacedArmorDepth } from './components/SpacedArmorDepth';
interface TankArmorProps {
duel: Duel;
@@ -12,11 +14,16 @@ interface TankArmorProps {
export const TankArmor = memo<TankArmorProps>(({ duel }) => {
const externalModuleMaskScene = new Scene();
const spacedArmorDepthScene = new Scene();
const armorHighlightingScene = new Scene();
const externalModuleMaskPortal = createPortal(
<ExternalModuleMask duel={duel} />,
externalModuleMaskScene,
);
const spacedArmorDepthPortal = createPortal(
<SpacedArmorDepth duel={duel} />,
spacedArmorDepthScene,
);
const armorHighlightingPortal = createPortal(
<ArmorHighlighting duel={duel} />,
armorHighlightingScene,
@@ -32,10 +39,25 @@ export const TankArmor = memo<TankArmorProps>(({ duel }) => {
gl.setRenderTarget(externalModuleMaskRenderTarget);
gl.render(externalModuleMaskScene, camera);
gl.clearDepth();
spacedArmorDepthRenderTarget.setSize(
gl.domElement.width,
gl.domElement.height,
);
if (!spacedArmorDepthRenderTarget.depthTexture) {
spacedArmorDepthRenderTarget.depthTexture = new DepthTexture(
gl.domElement.width,
gl.domElement.height,
);
}
gl.setRenderTarget(spacedArmorDepthRenderTarget);
gl.render(spacedArmorDepthScene, camera);
gl.autoClear = false;
gl.setRenderTarget(null);
gl.render(scene, camera);
gl.clearDepth();
gl.render(armorHighlightingScene, camera);
}, 1);
@@ -43,6 +65,7 @@ export const TankArmor = memo<TankArmorProps>(({ duel }) => {
return (
<>
{externalModuleMaskPortal}
{spacedArmorDepthPortal}
{armorHighlightingPortal}
</>
);
@@ -1,4 +1,4 @@
import { MeshProps, useFrame } from '@react-three/fiber';
import { MeshProps, useFrame, useThree } from '@react-three/fiber';
import { useRef } from 'react';
import { MeshStandardMaterial, ShaderMaterial, Vector2 } from 'three';
import ThreeCustomShaderMaterial from 'three-custom-shader-material';
@@ -8,6 +8,7 @@ import { canSplash } from '../../../../core/blitz/canSplash';
import { isExplosive } from '../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
import { externalModuleMaskRenderTarget } from '../ExternalModuleMask';
import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth';
import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl';
@@ -25,18 +26,23 @@ export function ArmorMesh({
duel,
...props
}: ArmorMeshProps) {
const camera = useThree((state) => state.camera);
const { shell } = duel.antagonist;
const material = useRef<ShaderMaterial>(null);
const resolution = new Vector2();
useFrame(({ gl }) => {
useFrame(({ gl, camera }) => {
if (material.current) {
material.current.uniforms.externalModuleMask.value =
externalModuleMaskRenderTarget.texture;
material.current.uniforms.resolution.value = resolution.set(
gl.domElement.width,
gl.domElement.height,
);
material.current.uniforms.externalModuleMask.value =
externalModuleMaskRenderTarget.texture;
material.current.uniforms.spacedArmorDepth.value =
spacedArmorDepthRenderTarget.depthTexture;
material.current.uniforms.projectionMatrixInverse.value =
camera.projectionMatrixInverse;
}
});
@@ -56,8 +62,12 @@ export function ArmorMesh({
fragmentShader={fragmentShader}
vertexShader={vertexShader}
uniforms={{
externalModuleMask: { value: null },
resolution: { value: null },
externalModuleMask: { value: null },
spacedArmorDepth: { value: null },
projectionMatrixInverse: { value: null },
zNear: { value: camera.near },
zFar: { value: camera.far },
isSpaced: { value: isSpaced },
isExplosive: { value: isExplosive(shell.type) },
canSplash: { value: canSplash(shell.type) },
@@ -73,23 +83,6 @@ export function ArmorMesh({
},
}}
/>
{/* <shaderMaterial
ref={material}
depthFunc={EqualDepth}
depthWrite={false}
transparent
args={[
{
vertexShader,
fragmentShader,
uniforms: {
externalModuleMask: { value: null },
resolution: { value: null },
isSpaced: { value: isSpaced },
},
},
]}
/> */}
</mesh>
</>
);
@@ -9,10 +9,27 @@ uniform float caliber;
uniform float ricochet;
uniform float normalization;
uniform sampler2D externalModuleMask;
uniform sampler2D spacedArmorDepth;
uniform vec2 resolution;
varying mat4 vProjectionMatrix;
float depthToDistance(float depth) {
mat4 projectionMatrixInverse = inverse(vProjectionMatrix);
vec4 clipPosition = vec4(gl_FragCoord.xy / resolution * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
vec4 eyePosition = projectionMatrixInverse * clipPosition;
vec3 NDCPosition = eyePosition.xyz / eyePosition.w;
float worldDistance = length(NDCPosition);
return worldDistance;
}
void main() {
vec4 externalModuleMaskColor = texture2D(externalModuleMask, gl_FragCoord.xy / resolution);
vec2 screenCoordinates = gl_FragCoord.xy / resolution;
float spacedArmorDepthValue = texture2D(spacedArmorDepth, screenCoordinates).r;
float currentDistance = depthToDistance(gl_FragCoord.z);
float spacedArmorDistance = depthToDistance(spacedArmorDepthValue);
float distanceFromSpacedArmor = currentDistance - spacedArmorDistance;
vec4 externalModuleMaskColor = texture2D(externalModuleMask, screenCoordinates);
bool isUnderExternalModule = externalModuleMaskColor.r == 1.0;
vec3 normalizedNormal = normalize(vNormal);
vec3 normalizedViewPosition = normalize(vCSMViewPosition);
@@ -27,11 +44,25 @@ void main() {
penetrationChance = 0.0;
splashChance = 0.0;
} else {
float piercedPenetration = penetration;
// if (isUnderExternalModule) {
// // TODO: ADD
// piercedPenetration -= 50.0;
// }
if (isExplosive) {
// 50% loss of remaining penetration per meter
piercedPenetration -= 0.5 * piercedPenetration * distanceFromSpacedArmor;
}
piercedPenetration = max(piercedPenetration, 0.0);
bool twoCalibersRule = caliber > 2.0 * thickness;
float finalNormalization = twoCalibersRule ? (normalization * 1.4 * caliber) / (2.0 * thickness) : normalization;
float finalThickness = thickness / cos(angle - finalNormalization);
float delta = finalThickness - penetration;
float randomRadius = penetration * 0.05;
float delta = finalThickness - piercedPenetration;
float randomRadius = piercedPenetration * 0.05;
if (delta > randomRadius) {
penetrationChance = 0.0;
@@ -43,7 +74,7 @@ void main() {
if (canSplash) {
float reducedFinalThickness = finalThickness * (5.0 / 11.0);
float splashDelta = reducedFinalThickness - penetration;
float splashDelta = reducedFinalThickness - piercedPenetration;
if (splashDelta > randomRadius) {
splashChance = 0.0;
@@ -58,15 +89,17 @@ void main() {
}
if (isSpaced) {
if (isExternalModule) {
csm_FragColor = vec4(0.5, 0.0, 1.0, 0.5);
} else {
if (isExplosive) {
csm_FragColor = vec4(1.0, 0.0, 1.0, 0.5);
} else {
csm_FragColor = vec4(1.0, 0.0, 1.0, (1.0 - penetrationChance) * 0.5);
}
}
// if (isExternalModule) {
// csm_FragColor = vec4(0.5, 0.0, 1.0, 0.5);
// } else {
// if (isExplosive) {
// csm_FragColor = vec4(1.0, 0.0, 1.0, 0.5);
// } else {
// csm_FragColor = vec4(1.0, 0.0, 1.0, (1.0 - penetrationChance) * 0.5);
// }
// }
csm_FragColor = vec4(0.0);
} else {
csm_FragColor = vec4(1.0, splashChance * 0.392, 0.0, (1.0 - penetrationChance) * 0.5);
}
@@ -1,6 +1,12 @@
varying vec3 vCSMViewPosition;
varying float vZ;
varying mat4 vProjectionMatrix;
void main() {
vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0);
vCSMViewPosition = vViewPosition4.xyz;
vZ = -(modelViewMatrix * vec4(position, 1.0)).z;
vProjectionMatrix = projectionMatrix;
}
@@ -0,0 +1,19 @@
import { MeshProps } from '@react-three/fiber';
import { WebGLRenderTarget } from 'three';
export const spacedArmorDepthRenderTarget = new WebGLRenderTarget();
interface ArmorMeshSpacedArmorDepthProps extends MeshProps {
exclude?: boolean;
}
export function ArmorMeshSpacedArmorDepth({
exclude = false,
...props
}: ArmorMeshSpacedArmorDepthProps) {
return (
<mesh {...props}>
<meshBasicMaterial />
</mesh>
);
}