From 730ad56677870c32621d3745f858024662d336d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Mon, 15 Jan 2024 21:55:09 -0600 Subject: [PATCH] feat: explosive shells under spaced armor --- .../[id]/components/Model/TankDisplay.tsx | 1 - .../TankArmor/components/SpacedArmorDepth.tsx | 263 ++++++++++++++++++ .../Model/components/TankArmor/index.tsx | 25 +- .../ArmorMesh/components/Armor/index.tsx | 37 +-- .../components/Armor/shaders/fragment.glsl | 59 +++- .../components/Armor/shaders/vertex.glsl | 6 + .../ArmorMesh/components/SpacedArmorDepth.tsx | 19 ++ 7 files changed, 373 insertions(+), 37 deletions(-) create mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx create mode 100644 src/components/ArmorMesh/components/SpacedArmorDepth.tsx diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx index dc876c9d6..86972f3de 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx @@ -115,7 +115,6 @@ export function TankDisplay({ duel }: TankDisplayProps) { ref={canvas} camera={{ fov: 20 }} onPointerDown={handlePointerDown} - gl={{ logarithmicDepthBuffer: true }} > diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx new file mode 100644 index 000000000..7c36be823 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx @@ -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(({ duel }) => { + const wrapper = useRef(null); + const awaitedModelDefinitions = useModelDefinitions(); + const showSpacedArmor = useTankopedia( + (state) => state.model.visual.showSpacedArmor, + ); + const turretContainer = useRef(null); + const gunContainer = useRef(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 ( + + {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 ( + + ); + })} + + {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 ( + + ); + })} + + + {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 ( + + ); + })} + + {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 ( + + ); + })} + + {modelNodes.map((node) => { + const isCurrentGun = + node.name === + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; + const isVisible = isCurrentGun; + + if (!isVisible || !showSpacedArmor) return null; + + return ( + + ); + })} + + + + ); +}); diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx index 1ed152676..27b3050ea 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx @@ -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(({ duel }) => { const externalModuleMaskScene = new Scene(); + const spacedArmorDepthScene = new Scene(); const armorHighlightingScene = new Scene(); const externalModuleMaskPortal = createPortal( , externalModuleMaskScene, ); + const spacedArmorDepthPortal = createPortal( + , + spacedArmorDepthScene, + ); const armorHighlightingPortal = createPortal( , armorHighlightingScene, @@ -32,10 +39,25 @@ export const TankArmor = memo(({ 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(({ duel }) => { return ( <> {externalModuleMaskPortal} + {spacedArmorDepthPortal} {armorHighlightingPortal} ); diff --git a/src/components/ArmorMesh/components/Armor/index.tsx b/src/components/ArmorMesh/components/Armor/index.tsx index a716efe86..93fd1ca57 100644 --- a/src/components/ArmorMesh/components/Armor/index.tsx +++ b/src/components/ArmorMesh/components/Armor/index.tsx @@ -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(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({ }, }} /> - {/* */} ); diff --git a/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl b/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl index f3946b65b..41e4b8159 100644 --- a/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl +++ b/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl @@ -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); } diff --git a/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl b/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl index afaeda2c0..f86d379c4 100644 --- a/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl +++ b/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl @@ -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; } \ No newline at end of file diff --git a/src/components/ArmorMesh/components/SpacedArmorDepth.tsx b/src/components/ArmorMesh/components/SpacedArmorDepth.tsx new file mode 100644 index 000000000..5b70c6561 --- /dev/null +++ b/src/components/ArmorMesh/components/SpacedArmorDepth.tsx @@ -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 ( + + + + ); +}