From 80b161b9fc556e98276d8ef3e2115c62d60316c8 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 18:01:48 -0600 Subject: [PATCH] feat: 3CR exception mask --- .../tankopedia/[id]/components/Lighting.tsx | 4 +- .../components/ArmorHighlighting.tsx} | 168 +++++------ .../components/ExternalModuleMask.tsx | 263 ++++++++++++++++++ .../Model/components/TankArmor/index.tsx | 49 ++++ .../ArmorMesh/components/Armor/index.tsx | 96 +++++++ .../Armor}/shaders/fragment.glsl | 6 +- .../Armor}/shaders/vertex.glsl | 0 .../components/ExternalModuleMask.tsx | 29 ++ src/components/ArmorMesh/index.tsx | 65 +---- 9 files changed, 509 insertions(+), 171 deletions(-) rename src/app/tools/tankopedia/[id]/components/Model/components/{TankArmor.tsx => TankArmor/components/ArmorHighlighting.tsx} (58%) create mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx create mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx create mode 100644 src/components/ArmorMesh/components/Armor/index.tsx rename src/components/ArmorMesh/{ => components/Armor}/shaders/fragment.glsl (86%) rename src/components/ArmorMesh/{ => components/Armor}/shaders/vertex.glsl (100%) create mode 100644 src/components/ArmorMesh/components/ExternalModuleMask.tsx diff --git a/src/app/tools/tankopedia/[id]/components/Lighting.tsx b/src/app/tools/tankopedia/[id]/components/Lighting.tsx index 9b6a178f2..fb9d16200 100644 --- a/src/app/tools/tankopedia/[id]/components/Lighting.tsx +++ b/src/app/tools/tankopedia/[id]/components/Lighting.tsx @@ -3,13 +3,13 @@ export function Lighting() { <> diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx similarity index 58% rename from src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx rename to src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx index 9d22dcbef..b6d3d7003 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx @@ -1,27 +1,30 @@ import { useLoader } from '@react-three/fiber'; -import { memo, useEffect, useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { Euler, Group, Mesh, Vector3 } from 'three'; import { GLTFLoader } from 'three-stdlib'; import { degToRad } from 'three/src/math/MathUtils'; -import { ArmorMesh } from '../../../../../../../components/ArmorMesh'; -import { HeadsUpDisplay } from '../../../../../../../components/HeadsUpDisplay'; -import { X_AXIS, Y_AXIS, Z_AXIS } from '../../../../../../../constants/axis'; -import { asset } from '../../../../../../../core/blitzkrieg/asset'; +import { ArmorMesh } from '../../../../../../../../../components/ArmorMesh'; +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'; +} 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 TankArmorProps { +interface ArmorHighlightingProps { duel: Duel; } -export const TankArmor = memo(({ duel }) => { +export function ArmorHighlighting({ duel }: ArmorHighlightingProps) { const wrapper = useRef(null); const awaitedModelDefinitions = useModelDefinitions(); const showSpacedArmor = useTankopedia( @@ -136,18 +139,47 @@ export const TankArmor = memo(({ duel }) => { ).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 ( + + ); + })} + + {armorNodes.map((node) => { - const isHull = node.name.startsWith('hull_'); - const isVisible = isHull; + const isCurrentTurret = node.name.startsWith( + `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentTurret; const armorId = nameToArmorId(node.name); const { spaced, thickness } = resolveArmor( - tankModelDefinition.armor, + turretModelDefinition.armor, armorId, ); @@ -163,43 +195,21 @@ export const TankArmor = memo(({ duel }) => { duel={duel} key={node.uuid} geometry={(node as Mesh).geometry} + position={turretOrigin} isSpaced={spaced ?? false} thickness={thickness} /> ); })} - - {modelNodes.map((node) => { - const isWheel = node.name.startsWith('chassis_wheel_'); - const isTrack = node.name.startsWith('chassis_track_'); - const isVisible = isWheel || isTrack; - const thickness = 50; - - if (!isVisible || !showSpacedArmor) return null; - - return ( - - ); - })} - - + {armorNodes.map((node) => { - const isCurrentTurret = node.name.startsWith( - `turret_${turretModelDefinition.model - .toString() - .padStart(2, '0')}`, + const isCurrentGun = node.name.startsWith( + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, ); - const isVisible = isCurrentTurret; + const isVisible = isCurrentGun; const armorId = nameToArmorId(node.name); const { spaced, thickness } = resolveArmor( - turretModelDefinition.armor, + gunModelDefinition.armor, armorId, ); @@ -215,66 +225,14 @@ export const TankArmor = memo(({ duel }) => { duel={duel} key={node.uuid} geometry={(node as Mesh).geometry} - position={turretOrigin} + position={turretOrigin.clone().add(gunOrigin)} isSpaced={spaced ?? false} thickness={thickness} /> ); })} - - {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; - const thickness = 50; - - if (!isVisible || !showSpacedArmor) return null; - - return ( - - ); - })} - - + ); -}); +} diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx new file mode 100644 index 000000000..cc3a187ad --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.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 { ArmorMeshExternalModuleMask } from '../../../../../../../../../components/ArmorMesh'; +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 ExternalModuleMaskProps { + duel: Duel; +} + +export const ExternalModuleMask = 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 new file mode 100644 index 000000000..1ed152676 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx @@ -0,0 +1,49 @@ +import { createPortal, useFrame } from '@react-three/fiber'; +import { memo } from 'react'; +import { Scene } from 'three'; +import { externalModuleMaskRenderTarget } from '../../../../../../../../components/ArmorMesh'; +import { Duel } from '../../../../page'; +import { ArmorHighlighting } from './components/ArmorHighlighting'; +import { ExternalModuleMask } from './components/ExternalModuleMask'; + +interface TankArmorProps { + duel: Duel; +} + +export const TankArmor = memo(({ duel }) => { + const externalModuleMaskScene = new Scene(); + const armorHighlightingScene = new Scene(); + const externalModuleMaskPortal = createPortal( + , + externalModuleMaskScene, + ); + const armorHighlightingPortal = createPortal( + , + armorHighlightingScene, + ); + + useFrame(({ gl, scene, camera }) => { + gl.autoClear = true; + + externalModuleMaskRenderTarget.setSize( + gl.domElement.width, + gl.domElement.height, + ); + gl.setRenderTarget(externalModuleMaskRenderTarget); + gl.render(externalModuleMaskScene, camera); + + gl.autoClear = false; + + gl.setRenderTarget(null); + gl.render(scene, camera); + gl.clearDepth(); + gl.render(armorHighlightingScene, camera); + }, 1); + + return ( + <> + {externalModuleMaskPortal} + {armorHighlightingPortal} + + ); +}); diff --git a/src/components/ArmorMesh/components/Armor/index.tsx b/src/components/ArmorMesh/components/Armor/index.tsx new file mode 100644 index 000000000..a716efe86 --- /dev/null +++ b/src/components/ArmorMesh/components/Armor/index.tsx @@ -0,0 +1,96 @@ +import { MeshProps, useFrame } from '@react-three/fiber'; +import { useRef } from 'react'; +import { MeshStandardMaterial, ShaderMaterial, Vector2 } from 'three'; +import ThreeCustomShaderMaterial from 'three-custom-shader-material'; +import { degToRad } from 'three/src/math/MathUtils'; +import { Duel } from '../../../../app/tools/tankopedia/[id]/page'; +import { canSplash } from '../../../../core/blitz/canSplash'; +import { isExplosive } from '../../../../core/blitz/isExplosive'; +import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; +import { externalModuleMaskRenderTarget } from '../ExternalModuleMask'; +import fragmentShader from './shaders/fragment.glsl'; +import vertexShader from './shaders/vertex.glsl'; + +interface ArmorMeshProps extends MeshProps { + duel: Duel; + isSpaced: boolean; + isExternalModule?: boolean; + thickness: number; +} + +export function ArmorMesh({ + isSpaced, + isExternalModule = false, + thickness, + duel, + ...props +}: ArmorMeshProps) { + const { shell } = duel.antagonist; + const material = useRef(null); + const resolution = new Vector2(); + + useFrame(({ gl }) => { + if (material.current) { + material.current.uniforms.externalModuleMask.value = + externalModuleMaskRenderTarget.texture; + material.current.uniforms.resolution.value = resolution.set( + gl.domElement.width, + gl.domElement.height, + ); + } + }); + + return ( + <> + + + + + + + {/* */} + + + ); +} diff --git a/src/components/ArmorMesh/shaders/fragment.glsl b/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl similarity index 86% rename from src/components/ArmorMesh/shaders/fragment.glsl rename to src/components/ArmorMesh/components/Armor/shaders/fragment.glsl index 22d5cb206..f3946b65b 100644 --- a/src/components/ArmorMesh/shaders/fragment.glsl +++ b/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl @@ -8,8 +8,12 @@ uniform float penetration; uniform float caliber; uniform float ricochet; uniform float normalization; +uniform sampler2D externalModuleMask; +uniform vec2 resolution; void main() { + vec4 externalModuleMaskColor = texture2D(externalModuleMask, gl_FragCoord.xy / resolution); + bool isUnderExternalModule = externalModuleMaskColor.r == 1.0; vec3 normalizedNormal = normalize(vNormal); vec3 normalizedViewPosition = normalize(vCSMViewPosition); float dotProduct = dot(normalizedNormal, -normalizedViewPosition); @@ -19,7 +23,7 @@ void main() { float splashChance = -1.0; bool threeCalibersRule = caliber > 3.0 * thickness; - if (!isExternalModule && !isExplosive && !threeCalibersRule && angle >= ricochet) { + if (!isUnderExternalModule && !isExternalModule && !isExplosive && !threeCalibersRule && angle >= ricochet) { penetrationChance = 0.0; splashChance = 0.0; } else { diff --git a/src/components/ArmorMesh/shaders/vertex.glsl b/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl similarity index 100% rename from src/components/ArmorMesh/shaders/vertex.glsl rename to src/components/ArmorMesh/components/Armor/shaders/vertex.glsl diff --git a/src/components/ArmorMesh/components/ExternalModuleMask.tsx b/src/components/ArmorMesh/components/ExternalModuleMask.tsx new file mode 100644 index 000000000..6c7c0d1f2 --- /dev/null +++ b/src/components/ArmorMesh/components/ExternalModuleMask.tsx @@ -0,0 +1,29 @@ +import { MeshProps } from '@react-three/fiber'; +import { WebGLRenderTarget } from 'three'; + +export const externalModuleMaskRenderTarget = new WebGLRenderTarget(); + +interface ArmorMeshExternalModuleMaskProps extends MeshProps { + exclude?: boolean; +} + +export function ArmorMeshExternalModuleMask({ + exclude = false, + ...props +}: ArmorMeshExternalModuleMaskProps) { + return ( + <> + {exclude && ( + + + + )} + + {!exclude && ( + + + + )} + + ); +} diff --git a/src/components/ArmorMesh/index.tsx b/src/components/ArmorMesh/index.tsx index 370559173..b64eb57f2 100644 --- a/src/components/ArmorMesh/index.tsx +++ b/src/components/ArmorMesh/index.tsx @@ -1,63 +1,2 @@ -import { MeshProps } from '@react-three/fiber'; -import { MeshStandardMaterial } from 'three'; -import CustomShaderMaterial from 'three-custom-shader-material'; -import { degToRad } from 'three/src/math/MathUtils'; -import { Duel } from '../../app/tools/tankopedia/[id]/page'; -import { canSplash } from '../../core/blitz/canSplash'; -import { isExplosive } from '../../core/blitz/isExplosive'; -import { resolveNearPenetration } from '../../core/blitz/resolveNearPenetration'; -import fragmentShader from './shaders/fragment.glsl'; -import vertexShader from './shaders/vertex.glsl'; - -interface ArmorMeshProps extends MeshProps { - duel: Duel; - isSpaced: boolean; - isExternalModule?: boolean; - thickness: number; -} - -export function ArmorMesh({ - isSpaced, - isExternalModule = false, - thickness, - duel, - ...props -}: ArmorMeshProps) { - const { shell } = duel.antagonist; - - return ( - <> - {!isSpaced && ( - - - - )} - - - - - - ); -} +export * from './components/Armor'; +export * from './components/ExternalModuleMask';