From b3a6a47a6a76a7e0b8ec69868177153cecd6b96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Mon, 26 Feb 2024 13:29:05 -0600 Subject: [PATCH 01/32] feat: start of new engine --- .../[id]/components/Model/TankSandbox.tsx | 18 +- .../components/ArmorHighlighting.tsx | 213 ------------- .../components/ExternalModuleMask.tsx | 286 ------------------ .../{SpacedArmorDepth.tsx => Spaced.tsx} | 88 +++--- .../Model/components/TankArmor/index.tsx | 67 ---- .../Armor/components/SpacedArmor.tsx | 34 +++ src/stores/tankopedia.ts | 3 +- 7 files changed, 82 insertions(+), 627 deletions(-) delete mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx delete mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx rename src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/{SpacedArmorDepth.tsx => Spaced.tsx} (83%) create mode 100644 src/components/Armor/components/SpacedArmor.tsx diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx index a8a087792..b73d552fd 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -4,7 +4,6 @@ import { PerspectiveCamera } from '@react-three/drei'; import { Canvas } from '@react-three/fiber'; import { Suspense, use, useEffect, useRef, useState } from 'react'; import { isMobile } from 'react-device-detect'; -import { ShotDisplay } from '../../../../../../components/ShotDisplay'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; @@ -19,15 +18,10 @@ import mutateTankopediaPersistent, { } from '../../../../../../stores/tankopedia'; import { AntagonistBar } from '../AntagonistBar'; import { Controls } from '../Control'; -import { Lighting } from '../Lighting'; import { RotationInputs } from '../QuickInputs'; -import { SceneProps } from '../SceneProps'; import { ModelLoader } from './components/ModelLoader'; import { Options } from './components/Options'; -import { TankArmor } from './components/TankArmor'; -import { ExternalModuleMask } from './components/TankArmor/components/ExternalModuleMask'; -import { SpacedArmorDepth } from './components/TankArmor/components/SpacedArmorDepth'; -import { TankModel } from './components/TankModel'; +import { SpacedArmorScene } from './components/TankArmor/components/Spaced'; export function TankSandbox() { const canvas = useRef(null); @@ -195,15 +189,13 @@ export function TankSandbox() { > - + {/* */} }> - + {/* - - - - + */} + diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx deleted file mode 100644 index e547a01fe..000000000 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { Euler, Group, Mesh, Vector3 } from 'three'; -import { degToRad } from 'three/src/math/MathUtils'; -import { ArmorMesh } from '../../../../../../../../../components/ArmorMesh'; -import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis'; -import { - ModelTransformEventData, - modelTransformEvent, -} from '../../../../../../../../../core/blitzkrieg/modelTransform'; -import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId'; -import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness'; -import { useArmor } from '../../../../../../../../../hooks/useArmor'; -import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions'; -import { useDuel } from '../../../../../../../../../stores/duel'; -import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia'; - -export function ArmorHighlighting() { - const protagonist = useDuel((state) => state.protagonist!); - const wrapper = useRef(null); - const modelDefinitions = useModelDefinitions(); - const gunContainer = useRef(null); - const turretContainer = useRef(null); - const initialTankopediaState = useTankopediaPersistent.getState(); - - useEffect(() => { - 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 turretPosition = new Vector3(); - const turretRotation = new Euler(); - const gunPosition = new Vector3(); - const gunRotation = new Euler(); - - function handleModelTransform({ pitch, yaw }: ModelTransformEventData) { - if (yaw === undefined) return; - - gunPosition - .set(0, 0, 0) - .sub(hullOrigin) - .sub(turretOrigin) - .sub(gunOrigin) - .applyAxisAngle(I_HAT, pitch) - .add(gunOrigin) - .add(turretOrigin) - .add(hullOrigin); - gunRotation.set(pitch, 0, 0); - gunContainer.current?.position.copy(gunPosition); - gunContainer.current?.rotation.copy(gunRotation); - turretPosition - .set(0, 0, 0) - .sub(hullOrigin) - .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(I_HAT, initialPitch) - .applyAxisAngle(J_HAT, initialRoll) - .applyAxisAngle(K_HAT, initialYaw); - turretRotation.x += initialPitch; - turretRotation.y += initialRoll; - turretRotation.z += initialYaw; - } - - turretPosition.add(turretOrigin).add(hullOrigin); - turretContainer.current?.position.copy(turretPosition); - turretContainer.current?.rotation.copy(turretRotation); - } - const unsubscribe = useTankopediaPersistent.subscribe( - (state) => state.mode, - (mode) => { - if (wrapper.current) wrapper.current.visible = mode === 'armor'; - }, - ); - - handleModelTransform(protagonist); - modelTransformEvent.on(handleModelTransform); - - return () => { - modelTransformEvent.off(handleModelTransform); - unsubscribe(); - }; - }); - - const armorGltf = useArmor(protagonist.tank.id); - const armorNodes = Object.values(armorGltf.nodes); - const tankModelDefinition = modelDefinitions[protagonist.tank.id]; - const trackModelDefinition = tankModelDefinition.tracks[protagonist.track.id]; - const turretModelDefinition = - tankModelDefinition.turrets[protagonist.turret.id]; - const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; - const hullOrigin = new Vector3( - trackModelDefinition.origin[0], - trackModelDefinition.origin[1], - -trackModelDefinition.origin[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 maxThickness = Math.max( - trackModelDefinition.thickness, - gunModelDefinition.thickness, - ...armorNodes - .map((node) => { - const armorId = nameToArmorId(node.name); - return resolveArmor(tankModelDefinition.armor, armorId).thickness; - }) - .filter(Boolean), - ); - - 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) 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) 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) 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 deleted file mode 100644 index 66858e5bd..000000000 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import { memo, useEffect, useRef } from 'react'; -import { Euler, Group, Vector3 } from 'three'; -import { degToRad } from 'three/src/math/MathUtils'; -import { ArmorMeshExternalModuleMask } from '../../../../../../../../../components/ArmorMesh'; -import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis'; -import { - ModelTransformEventData, - modelTransformEvent, -} from '../../../../../../../../../core/blitzkrieg/modelTransform'; -import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId'; -import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness'; -import { useArmor } from '../../../../../../../../../hooks/useArmor'; -import { useModel } from '../../../../../../../../../hooks/useModel'; -import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions'; -import { useDuel } from '../../../../../../../../../stores/duel'; -import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia'; - -interface ExternalModuleMaskProps { - ornamental?: boolean; -} - -/** - * When rendered, generates a mask and thickness buffer for external modules. - * - R: 1 means external module - * - G: normalized thickness; multiply with max thickness to get nominal thickness - * - B: no data - * - A: 1 means is armor; 0 is background - */ -export const ExternalModuleMask = memo( - ({ ornamental = false }) => { - const protagonist = useDuel((state) => state.protagonist!); - const wrapper = useRef(null); - const modelDefinitions = useModelDefinitions(); - const turretContainer = useRef(null); - const gunContainer = useRef(null); - const initialTankopediaState = useTankopediaPersistent.getState(); - - useEffect(() => { - 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 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(hullOrigin) - .sub(turretOrigin) - .sub(gunOrigin) - .applyAxisAngle(I_HAT, pitch) - .add(gunOrigin) - .add(turretOrigin) - .add(hullOrigin); - 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) - .sub(hullOrigin) - .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(I_HAT, initialPitch) - .applyAxisAngle(J_HAT, initialRoll) - .applyAxisAngle(K_HAT, initialYaw); - turretRotation.x += initialPitch; - turretRotation.y += initialRoll; - turretRotation.z += initialYaw; - } - - turretPosition.add(turretOrigin).add(hullOrigin); - turretContainer.current?.position.copy(turretPosition); - turretContainer.current?.rotation.copy(turretRotation); - } - - handleModelTransform(protagonist); - modelTransformEvent.on(handleModelTransform); - - return () => { - modelTransformEvent.off(handleModelTransform); - }; - }); - - useEffect(() => { - const unsubscribe = useTankopediaPersistent.subscribe( - (state) => state.mode, - (mode) => { - if (wrapper.current) wrapper.current.visible = mode === 'armor'; - }, - ); - - return unsubscribe; - }); - - const armorGltf = useArmor(protagonist.tank.id); - const { gltf: modelGltf } = useModel(protagonist.tank.id); - - const armorNodes = Object.values(armorGltf.nodes); - const modelNodes = Object.values(modelGltf.nodes); - const tankModelDefinition = modelDefinitions[protagonist.tank.id]; - const trackModelDefinition = - tankModelDefinition.tracks[protagonist.track.id]; - const turretModelDefinition = - tankModelDefinition.turrets[protagonist.turret.id]; - const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; - const maxThickness = Math.max( - trackModelDefinition.thickness, - gunModelDefinition.thickness, - ...armorNodes - .map((node) => { - const armorId = nameToArmorId(node.name); - return resolveArmor(tankModelDefinition.armor, armorId).thickness; - }) - .filter(Boolean), - ); - 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); - - return ( - - - {armorNodes.map((node) => { - const isHull = node.name.startsWith('hull_'); - const isVisible = isHull; - const armorId = nameToArmorId(node.name); - const { thickness, spaced } = resolveArmor( - tankModelDefinition.armor, - armorId, - ); - - if (!isVisible || thickness === undefined || spaced) 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) 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 { thickness, spaced } = resolveArmor( - turretModelDefinition.armor, - armorId, - ); - - if (!isVisible || thickness === undefined || spaced) 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 { thickness, spaced } = resolveArmor( - gunModelDefinition.armor, - armorId, - ); - - if (!isVisible || thickness === undefined || spaced) return null; - - return ( - - - - ); - })} - - {modelNodes.map((node) => { - const isCurrentGun = - node.name === - `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; - const isVisible = isCurrentGun; - - if (!isVisible) return null; - - return ( - - ); - })} - - - - ); - }, -); 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/Spaced.tsx similarity index 83% rename from src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx rename to src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx index a856743ae..1f768debe 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx @@ -1,8 +1,9 @@ import { memo, useEffect, useRef } from 'react'; import { Euler, Group, Vector3 } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; -import { ArmorMeshSpacedArmorDepth } from '../../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth'; +import { SpacedArmor } from '../../../../../../../../../components/Armor/components/SpacedArmor'; import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis'; +import { resolveNearPenetration } from '../../../../../../../../../core/blitz/resolveNearPenetration'; import { ModelTransformEventData, modelTransformEvent, @@ -15,22 +16,17 @@ import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDe import { useDuel } from '../../../../../../../../../stores/duel'; import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia'; -interface SpacedArmorDepthProps { +export enum ArmorType { + Core, + Spaced, + External, +} + +interface SpacedArmorSceneProps { ornamental?: boolean; } -/** - * When rendered, this component provides the depth buffer for the armor which - * when compared to the depth of the core armor will give you the distance - * between the first point of contact and the core armor. The color channels - * hold information about the thickness of the spaced armor (external modules - * not included). - * - R: angle of the armor where 0 is 0 and 1 is 90 - * - G: normalized thickness; multiply with max thickness to get nominal thickness - * - B: no data - * - A: 1 means is armor; 0 is background - */ -export const SpacedArmorDepth = memo( +export const SpacedArmorScene = memo( ({ ornamental = false }) => { const protagonist = useDuel((state) => state.protagonist!); const wrapper = useRef(null); @@ -138,16 +134,6 @@ export const SpacedArmorDepth = memo( const turretModelDefinition = tankModelDefinition.turrets[protagonist.turret.id]; const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; - const maxThickness = Math.max( - trackModelDefinition.thickness, - gunModelDefinition.thickness, - ...armorNodes - .map((node) => { - const armorId = nameToArmorId(node.name); - return resolveArmor(tankModelDefinition.armor, armorId).thickness; - }) - .filter(Boolean), - ); const hullOrigin = new Vector3( trackModelDefinition.origin[0], trackModelDefinition.origin[1], @@ -164,6 +150,17 @@ export const SpacedArmorDepth = memo( -turretModelDefinition.gunOrigin[2], ).applyAxisAngle(I_HAT, Math.PI / 2); + console.log( + 'test', + gunModelDefinition.thickness, + resolveNearPenetration(useDuel.getState().antagonist!.shell.penetration), + (gunModelDefinition.thickness / + resolveNearPenetration( + useDuel.getState().antagonist!.shell.penetration, + )) * + 255, + ); + return ( ( if (!isVisible || thickness === undefined) return null; return ( - ); @@ -203,10 +198,11 @@ export const SpacedArmorDepth = memo( if (!isVisible) return null; return ( - ); @@ -229,12 +225,11 @@ export const SpacedArmorDepth = memo( return ( - @@ -261,12 +256,10 @@ export const SpacedArmorDepth = memo( key={node.uuid} position={turretOrigin.clone().add(gunOrigin)} > - @@ -275,18 +268,19 @@ export const SpacedArmorDepth = memo( {modelNodes.map((node) => { - const isCurrentGun = - node.name === - `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; + const isCurrentGun = node.name.startsWith( + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, + ); const isVisible = isCurrentGun; if (!isVisible) 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 df4cc78d5..e69de29bb 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,67 +0,0 @@ -import { createPortal, useFrame } from '@react-three/fiber'; -import { memo } from 'react'; -import { DepthTexture, Scene, Vector2 } from 'three'; -import { externalModuleMaskRenderTarget } from '../../../../../../../../components/ArmorMesh'; -import { spacedArmorDepthRenderTarget } from '../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth'; -import { ArmorHighlighting } from './components/ArmorHighlighting'; -import { ExternalModuleMask } from './components/ExternalModuleMask'; -import { SpacedArmorDepth } from './components/SpacedArmorDepth'; - -export const TankArmor = memo(() => { - const externalModuleMaskScene = new Scene(); - const spacedArmorDepthScene = new Scene(); - const armorHighlightingScene = new Scene(); - const externalModuleMaskPortal = createPortal( - , - externalModuleMaskScene, - ); - const spacedArmorDepthPortal = createPortal( - , - spacedArmorDepthScene, - ); - const armorHighlightingPortal = createPortal( - , - armorHighlightingScene, - ); - const renderSize = new Vector2(); - const newRenderSize = new Vector2(); - - useFrame(({ gl, scene, camera }) => { - const pixelRatio = gl.getPixelRatio(); - gl.autoClear = true; - - gl.getSize(newRenderSize).multiplyScalar(pixelRatio); - if (!newRenderSize.equals(renderSize)) { - renderSize.copy(newRenderSize); - spacedArmorDepthRenderTarget.depthTexture = new DepthTexture( - renderSize.x, - renderSize.y, - ); - } - - externalModuleMaskRenderTarget.setSize(renderSize.x, renderSize.y); - gl.setRenderTarget(externalModuleMaskRenderTarget); - gl.render(externalModuleMaskScene, camera); - - gl.clearDepth(); - spacedArmorDepthRenderTarget.setSize(renderSize.x, renderSize.y * 0.5); - gl.setRenderTarget(spacedArmorDepthRenderTarget); - gl.render(spacedArmorDepthScene, camera); - - gl.autoClear = false; - - gl.setRenderTarget(null); - gl.render(scene, camera); - - gl.clearDepth(); - gl.render(armorHighlightingScene, camera); - }, 1); - - return ( - <> - {externalModuleMaskPortal} - {spacedArmorDepthPortal} - {armorHighlightingPortal} - - ); -}); diff --git a/src/components/Armor/components/SpacedArmor.tsx b/src/components/Armor/components/SpacedArmor.tsx new file mode 100644 index 000000000..a4706ccf5 --- /dev/null +++ b/src/components/Armor/components/SpacedArmor.tsx @@ -0,0 +1,34 @@ +import { AdditiveBlending, Color, MeshBasicMaterial, Object3D } from 'three'; +import { ArmorType } from '../../../app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced'; +import { resolveNearPenetration } from '../../../core/blitz/resolveNearPenetration'; +import { jsxTree } from '../../../core/blitzkrieg/jsxTree'; +import { useDuel } from '../../../stores/duel'; + +interface SpacedArmorProps { + node: Object3D; + ornamental?: boolean; + type: ArmorType; + thickness: number; +} + +const omitColor = new Color(0, 0, 0); + +export function SpacedArmor({ node, type, thickness }: SpacedArmorProps) { + const penetrationRaw = useDuel( + (state) => state.antagonist!.shell.penetration, + ); + const penetration = resolveNearPenetration(penetrationRaw); + + return jsxTree(node, { + material: new MeshBasicMaterial({ + color: + type === ArmorType.Core + ? omitColor + : type === ArmorType.Spaced + ? new Color(0, 0, 0) + : new Color(thickness / penetration, 0, 0), + depthWrite: type === ArmorType.Core, + blending: type === ArmorType.Core ? undefined : AdditiveBlending, + }), + }); +} diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index f2f8b5d26..773014f21 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -5,6 +5,7 @@ import { create } from 'zustand'; import { persist, subscribeWithSelector } from 'zustand/middleware'; import { ENVIRONMENTS } from '../app/tools/tankopedia/[id]/components/Lighting'; import { TankType, TreeType } from '../components/Tanks'; +import isDev from '../core/blitzkrieg/isDev'; import { Tier } from '../core/blitzkrieg/tankDefinitions'; export type TankopediaSortBy = keyof typeof SORT_NAMES; @@ -123,7 +124,7 @@ export const useTankopediaPersistent = create()( test: 'include', page: 0, }, - mode: 'model', + mode: isDev() ? 'armor' : 'model', })), { name: 'tankopedia', merge }, ), From 3500389628e6660e8b6f090753d07aab52758c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:29:12 -0600 Subject: [PATCH 02/32] feat: something --- .../Armor/components/SpacedArmor.tsx | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/components/Armor/components/SpacedArmor.tsx b/src/components/Armor/components/SpacedArmor.tsx index a4706ccf5..afc2745b4 100644 --- a/src/components/Armor/components/SpacedArmor.tsx +++ b/src/components/Armor/components/SpacedArmor.tsx @@ -19,16 +19,31 @@ export function SpacedArmor({ node, type, thickness }: SpacedArmorProps) { ); const penetration = resolveNearPenetration(penetrationRaw); - return jsxTree(node, { - material: new MeshBasicMaterial({ - color: - type === ArmorType.Core - ? omitColor - : type === ArmorType.Spaced - ? new Color(0, 0, 0) - : new Color(thickness / penetration, 0, 0), - depthWrite: type === ArmorType.Core, - blending: type === ArmorType.Core ? undefined : AdditiveBlending, - }), - }); + return ( + <> + {type === ArmorType.External && + jsxTree(node, { + renderOrder: 0, + + material: new MeshBasicMaterial({ + colorWrite: false, + }), + })} + + {jsxTree(node, { + renderOrder: 1, + + material: new MeshBasicMaterial({ + color: + type === ArmorType.Core + ? omitColor + : type === ArmorType.Spaced + ? new Color(0, 0, 0) + : new Color(thickness / penetration, 0, 0), + depthWrite: type === ArmorType.Core, + blending: type === ArmorType.Core ? undefined : AdditiveBlending, + }), + })} + + ); } From 080af86e1b2eb388276fdff8e766d689eb214651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:31:49 -0600 Subject: [PATCH 03/32] feat: shaders --- .../TankArmor/components/Spaced.tsx | 6 +- .../Armor/components/SpacedArmor.tsx | 49 ------------- .../Armor/components/SpacedArmor/index.tsx | 71 +++++++++++++++++++ .../materials/externalModule/fragment.glsl | 6 ++ .../materials/externalModule/index.ts | 19 +++++ .../materials/externalModule/vertex.glsl | 4 ++ .../materials/spaced/fragment.glsl | 6 ++ .../SpacedArmor/materials/spaced/index.ts | 19 +++++ .../SpacedArmor/materials/spaced/vertex.glsl | 11 +++ 9 files changed, 139 insertions(+), 52 deletions(-) delete mode 100644 src/components/Armor/components/SpacedArmor.tsx create mode 100644 src/components/Armor/components/SpacedArmor/index.tsx create mode 100644 src/components/Armor/components/SpacedArmor/materials/externalModule/fragment.glsl create mode 100644 src/components/Armor/components/SpacedArmor/materials/externalModule/index.ts create mode 100644 src/components/Armor/components/SpacedArmor/materials/externalModule/vertex.glsl create mode 100644 src/components/Armor/components/SpacedArmor/materials/spaced/fragment.glsl create mode 100644 src/components/Armor/components/SpacedArmor/materials/spaced/index.ts create mode 100644 src/components/Armor/components/SpacedArmor/materials/spaced/vertex.glsl diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx index 1f768debe..3db354953 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx @@ -268,9 +268,9 @@ export const SpacedArmorScene = memo( {modelNodes.map((node) => { - const isCurrentGun = node.name.startsWith( - `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, - ); + const isCurrentGun = + node.name === + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; const isVisible = isCurrentGun; if (!isVisible) return null; diff --git a/src/components/Armor/components/SpacedArmor.tsx b/src/components/Armor/components/SpacedArmor.tsx deleted file mode 100644 index afc2745b4..000000000 --- a/src/components/Armor/components/SpacedArmor.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { AdditiveBlending, Color, MeshBasicMaterial, Object3D } from 'three'; -import { ArmorType } from '../../../app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced'; -import { resolveNearPenetration } from '../../../core/blitz/resolveNearPenetration'; -import { jsxTree } from '../../../core/blitzkrieg/jsxTree'; -import { useDuel } from '../../../stores/duel'; - -interface SpacedArmorProps { - node: Object3D; - ornamental?: boolean; - type: ArmorType; - thickness: number; -} - -const omitColor = new Color(0, 0, 0); - -export function SpacedArmor({ node, type, thickness }: SpacedArmorProps) { - const penetrationRaw = useDuel( - (state) => state.antagonist!.shell.penetration, - ); - const penetration = resolveNearPenetration(penetrationRaw); - - return ( - <> - {type === ArmorType.External && - jsxTree(node, { - renderOrder: 0, - - material: new MeshBasicMaterial({ - colorWrite: false, - }), - })} - - {jsxTree(node, { - renderOrder: 1, - - material: new MeshBasicMaterial({ - color: - type === ArmorType.Core - ? omitColor - : type === ArmorType.Spaced - ? new Color(0, 0, 0) - : new Color(thickness / penetration, 0, 0), - depthWrite: type === ArmorType.Core, - blending: type === ArmorType.Core ? undefined : AdditiveBlending, - }), - })} - - ); -} diff --git a/src/components/Armor/components/SpacedArmor/index.tsx b/src/components/Armor/components/SpacedArmor/index.tsx new file mode 100644 index 000000000..228df8f10 --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/index.tsx @@ -0,0 +1,71 @@ +import { MeshBasicMaterial, Object3D } from 'three'; +import { ArmorType } from '../../../../app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced'; +import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; +import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; +import { useDuel } from '../../../../stores/duel'; +import { externalModuleMaterial } from './materials/externalModule'; +import { spacedMaterial } from './materials/spaced'; +// import fragmentShader from './shaders/fragment.glsl'; +// import vertexShader from './shaders/vertex.glsl'; + +interface SpacedArmorProps { + node: Object3D; + ornamental?: boolean; + type: ArmorType; + thickness: number; +} + +const omitMaterial = new MeshBasicMaterial({ + colorWrite: false, + depthWrite: true, +}); + +export function SpacedArmor({ node, type, thickness }: SpacedArmorProps) { + const penetrationRaw = useDuel( + (state) => state.antagonist!.shell.penetration, + ); + const penetration = resolveNearPenetration(penetrationRaw); + + /** + * Render orders allocations: + * - 0 : core omission + * - 1-2: spaced without depth buffer write but do test to allow additive blending + * - 3-4: external with depth buffer write in first pass and read in second to simplify geometry + */ + + return ( + <> + {type === ArmorType.Core && + jsxTree(node, { + renderOrder: 0, + material: omitMaterial, + })} + + {type === ArmorType.Spaced && ( + <> + {jsxTree(node, { + renderOrder: 2, + material: spacedMaterial(thickness, penetration), + })} + + )} + + {type === ArmorType.External && ( + <> + {jsxTree(node, { + renderOrder: 4, + material: externalModuleMaterial(thickness, penetration), + })} + {jsxTree(node, { + renderOrder: 3, + material: new MeshBasicMaterial({ + colorWrite: false, + depthTest: true, + depthWrite: true, + }), + })} + + )} + + ); +} diff --git a/src/components/Armor/components/SpacedArmor/materials/externalModule/fragment.glsl b/src/components/Armor/components/SpacedArmor/materials/externalModule/fragment.glsl new file mode 100644 index 000000000..0ab70c10d --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/materials/externalModule/fragment.glsl @@ -0,0 +1,6 @@ +uniform float thickness; +uniform float penetration; + +void main() { + gl_FragColor = vec4(thickness / penetration, 0.0, 0.0, 1.0); +} \ No newline at end of file diff --git a/src/components/Armor/components/SpacedArmor/materials/externalModule/index.ts b/src/components/Armor/components/SpacedArmor/materials/externalModule/index.ts new file mode 100644 index 000000000..df5683ddc --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/materials/externalModule/index.ts @@ -0,0 +1,19 @@ +import { AdditiveBlending, ShaderMaterial } from 'three'; +import fragmentShader from './fragment.glsl'; +import vertexShader from './vertex.glsl'; + +export function externalModuleMaterial(thickness: number, penetration: number) { + return new ShaderMaterial({ + fragmentShader, + vertexShader, + + depthTest: true, + depthWrite: false, + blending: AdditiveBlending, + + uniforms: { + thickness: { value: thickness }, + penetration: { value: penetration }, + }, + }); +} diff --git a/src/components/Armor/components/SpacedArmor/materials/externalModule/vertex.glsl b/src/components/Armor/components/SpacedArmor/materials/externalModule/vertex.glsl new file mode 100644 index 000000000..5f9df37ef --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/materials/externalModule/vertex.glsl @@ -0,0 +1,4 @@ +void main() { + vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0); + gl_Position = projectionMatrix * vViewPosition4; +} \ No newline at end of file diff --git a/src/components/Armor/components/SpacedArmor/materials/spaced/fragment.glsl b/src/components/Armor/components/SpacedArmor/materials/spaced/fragment.glsl new file mode 100644 index 000000000..0ab70c10d --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/materials/spaced/fragment.glsl @@ -0,0 +1,6 @@ +uniform float thickness; +uniform float penetration; + +void main() { + gl_FragColor = vec4(thickness / penetration, 0.0, 0.0, 1.0); +} \ No newline at end of file diff --git a/src/components/Armor/components/SpacedArmor/materials/spaced/index.ts b/src/components/Armor/components/SpacedArmor/materials/spaced/index.ts new file mode 100644 index 000000000..1e0fb7f4d --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/materials/spaced/index.ts @@ -0,0 +1,19 @@ +import { AdditiveBlending, ShaderMaterial } from 'three'; +import fragmentShader from './fragment.glsl'; +import vertexShader from './vertex.glsl'; + +export function spacedMaterial(thickness: number, penetration: number) { + return new ShaderMaterial({ + fragmentShader, + vertexShader, + + depthTest: true, + depthWrite: false, + blending: AdditiveBlending, + + uniforms: { + thickness: { value: thickness }, + penetration: { value: penetration }, + }, + }); +} diff --git a/src/components/Armor/components/SpacedArmor/materials/spaced/vertex.glsl b/src/components/Armor/components/SpacedArmor/materials/spaced/vertex.glsl new file mode 100644 index 000000000..82f94e255 --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/materials/spaced/vertex.glsl @@ -0,0 +1,11 @@ +varying vec3 vNormal; +varying vec3 vViewPosition; + +void main() { + vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0); + + vViewPosition = vViewPosition4.xyz; + vNormal = normalMatrix * normal; + + gl_Position = projectionMatrix * vViewPosition4; +} \ No newline at end of file From fabf649608b14e045675b08b17727281c9596813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:02:14 -0600 Subject: [PATCH 04/32] feat: spaced armor finish --- .../TankArmor/components/Spaced.tsx | 12 ----- .../SpacedArmorSubExternal}/fragment.glsl | 0 .../SpacedArmorSubExternal/index.tsx | 54 +++++++++++++++++++ .../SpacedArmorSubExternal}/vertex.glsl | 0 .../SpacedArmorSubSpaced/fragment.glsl | 24 +++++++++ .../components/SpacedArmorSubSpaced/index.ts | 42 +++++++++++++++ .../SpacedArmorSubSpaced}/vertex.glsl | 0 .../Armor/components/SpacedArmor/index.tsx | 26 ++------- .../materials/externalModule/index.ts | 19 ------- .../materials/spaced/fragment.glsl | 6 --- .../SpacedArmor/materials/spaced/index.ts | 19 ------- 11 files changed, 124 insertions(+), 78 deletions(-) rename src/components/Armor/components/SpacedArmor/{materials/externalModule => components/SpacedArmorSubExternal}/fragment.glsl (100%) create mode 100644 src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx rename src/components/Armor/components/SpacedArmor/{materials/externalModule => components/SpacedArmorSubExternal}/vertex.glsl (100%) create mode 100644 src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/fragment.glsl create mode 100644 src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts rename src/components/Armor/components/SpacedArmor/{materials/spaced => components/SpacedArmorSubSpaced}/vertex.glsl (100%) delete mode 100644 src/components/Armor/components/SpacedArmor/materials/externalModule/index.ts delete mode 100644 src/components/Armor/components/SpacedArmor/materials/spaced/fragment.glsl delete mode 100644 src/components/Armor/components/SpacedArmor/materials/spaced/index.ts diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx index 3db354953..96fc79bf1 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx @@ -3,7 +3,6 @@ import { Euler, Group, Vector3 } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; import { SpacedArmor } from '../../../../../../../../../components/Armor/components/SpacedArmor'; import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis'; -import { resolveNearPenetration } from '../../../../../../../../../core/blitz/resolveNearPenetration'; import { ModelTransformEventData, modelTransformEvent, @@ -150,17 +149,6 @@ export const SpacedArmorScene = memo( -turretModelDefinition.gunOrigin[2], ).applyAxisAngle(I_HAT, Math.PI / 2); - console.log( - 'test', - gunModelDefinition.thickness, - resolveNearPenetration(useDuel.getState().antagonist!.shell.penetration), - (gunModelDefinition.thickness / - resolveNearPenetration( - useDuel.getState().antagonist!.shell.penetration, - )) * - 255, - ); - return ( + {jsxTree(node, { + renderOrder: 4, + material: new ShaderMaterial({ + fragmentShader, + vertexShader, + + depthTest: true, + depthWrite: false, + blending: AdditiveBlending, + + uniforms: { + thickness: { value: thickness }, + penetration: { value: penetration }, + }, + }), + })} + {jsxTree(node, { + renderOrder: 3, + material: new MeshBasicMaterial({ + colorWrite: false, + depthTest: true, + depthWrite: true, + }), + })} + + ); +} diff --git a/src/components/Armor/components/SpacedArmor/materials/externalModule/vertex.glsl b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/vertex.glsl similarity index 100% rename from src/components/Armor/components/SpacedArmor/materials/externalModule/vertex.glsl rename to src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/vertex.glsl diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/fragment.glsl b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/fragment.glsl new file mode 100644 index 000000000..04057b4d7 --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/fragment.glsl @@ -0,0 +1,24 @@ +varying vec3 vNormal; +varying vec3 vViewPosition; + +uniform float thickness; +uniform float penetration; +uniform float caliber; +uniform float ricochet; +uniform float normalization; + +void main() { + float angle = acos(dot(vNormal, -vViewPosition) / length(vViewPosition)); + + bool threeCalibersRule = caliber > thickness * 3.0; + if (!threeCalibersRule && angle >= ricochet) { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + return; + } + + bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0; + float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization; + float finalThickness = thickness / cos(angle - finalNormalization); + + gl_FragColor = vec4(finalThickness / penetration, 0.0, 0.0, 1.0); +} \ No newline at end of file diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts new file mode 100644 index 000000000..ec3ebeb42 --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts @@ -0,0 +1,42 @@ +import { AdditiveBlending, Object3D, ShaderMaterial } from 'three'; +import { degToRad } from 'three/src/math/MathUtils'; +import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; +import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree'; +import { useDuel } from '../../../../../../stores/duel'; +import fragmentShader from './fragment.glsl'; +import vertexShader from './vertex.glsl'; + +interface SpacedArmorSubSpacedProps { + node: Object3D; + thickness: number; +} + +export function SpacedArmorSubSpaced({ + node, + thickness, +}: SpacedArmorSubSpacedProps) { + const { shell } = useDuel.getState().antagonist!; + const penetration = resolveNearPenetration(shell.penetration); + + console.log(shell.ricochet); + + return jsxTree(node, { + renderOrder: 2, + material: new ShaderMaterial({ + fragmentShader, + vertexShader, + + depthTest: true, + depthWrite: false, + blending: AdditiveBlending, + + uniforms: { + thickness: { value: thickness }, + penetration: { value: penetration }, + caliber: { value: shell.caliber }, + ricochet: { value: degToRad(shell.ricochet ?? 90) }, + normalization: { value: degToRad(shell.normalization ?? 0) }, + }, + }), + }); +} diff --git a/src/components/Armor/components/SpacedArmor/materials/spaced/vertex.glsl b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/vertex.glsl similarity index 100% rename from src/components/Armor/components/SpacedArmor/materials/spaced/vertex.glsl rename to src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/vertex.glsl diff --git a/src/components/Armor/components/SpacedArmor/index.tsx b/src/components/Armor/components/SpacedArmor/index.tsx index 228df8f10..eab1537af 100644 --- a/src/components/Armor/components/SpacedArmor/index.tsx +++ b/src/components/Armor/components/SpacedArmor/index.tsx @@ -3,8 +3,8 @@ import { ArmorType } from '../../../../app/tools/tankopedia/[id]/components/Mode import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; import { useDuel } from '../../../../stores/duel'; -import { externalModuleMaterial } from './materials/externalModule'; -import { spacedMaterial } from './materials/spaced'; +import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal'; +import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced'; // import fragmentShader from './shaders/fragment.glsl'; // import vertexShader from './shaders/vertex.glsl'; @@ -42,29 +42,11 @@ export function SpacedArmor({ node, type, thickness }: SpacedArmorProps) { })} {type === ArmorType.Spaced && ( - <> - {jsxTree(node, { - renderOrder: 2, - material: spacedMaterial(thickness, penetration), - })} - + )} {type === ArmorType.External && ( - <> - {jsxTree(node, { - renderOrder: 4, - material: externalModuleMaterial(thickness, penetration), - })} - {jsxTree(node, { - renderOrder: 3, - material: new MeshBasicMaterial({ - colorWrite: false, - depthTest: true, - depthWrite: true, - }), - })} - + )} ); diff --git a/src/components/Armor/components/SpacedArmor/materials/externalModule/index.ts b/src/components/Armor/components/SpacedArmor/materials/externalModule/index.ts deleted file mode 100644 index df5683ddc..000000000 --- a/src/components/Armor/components/SpacedArmor/materials/externalModule/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { AdditiveBlending, ShaderMaterial } from 'three'; -import fragmentShader from './fragment.glsl'; -import vertexShader from './vertex.glsl'; - -export function externalModuleMaterial(thickness: number, penetration: number) { - return new ShaderMaterial({ - fragmentShader, - vertexShader, - - depthTest: true, - depthWrite: false, - blending: AdditiveBlending, - - uniforms: { - thickness: { value: thickness }, - penetration: { value: penetration }, - }, - }); -} diff --git a/src/components/Armor/components/SpacedArmor/materials/spaced/fragment.glsl b/src/components/Armor/components/SpacedArmor/materials/spaced/fragment.glsl deleted file mode 100644 index 0ab70c10d..000000000 --- a/src/components/Armor/components/SpacedArmor/materials/spaced/fragment.glsl +++ /dev/null @@ -1,6 +0,0 @@ -uniform float thickness; -uniform float penetration; - -void main() { - gl_FragColor = vec4(thickness / penetration, 0.0, 0.0, 1.0); -} \ No newline at end of file diff --git a/src/components/Armor/components/SpacedArmor/materials/spaced/index.ts b/src/components/Armor/components/SpacedArmor/materials/spaced/index.ts deleted file mode 100644 index 1e0fb7f4d..000000000 --- a/src/components/Armor/components/SpacedArmor/materials/spaced/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { AdditiveBlending, ShaderMaterial } from 'three'; -import fragmentShader from './fragment.glsl'; -import vertexShader from './vertex.glsl'; - -export function spacedMaterial(thickness: number, penetration: number) { - return new ShaderMaterial({ - fragmentShader, - vertexShader, - - depthTest: true, - depthWrite: false, - blending: AdditiveBlending, - - uniforms: { - thickness: { value: thickness }, - penetration: { value: penetration }, - }, - }); -} From 26895686508c706c8c470b2fe669c03e049366cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:52:36 -0600 Subject: [PATCH 05/32] feat: core armor --- .../[id]/components/Model/TankSandbox.tsx | 14 +- .../Armor/components/CoreArmor/index.tsx | 71 ++++++ .../CoreArmor/shaders/fragment.glsl | 46 ++++ .../shaders}/vertex.glsl | 0 .../Armor/components/CoreArmorScene.tsx | 226 ++++++++++++++++++ .../SpacedArmorSubExternal/index.tsx | 4 +- .../{ => shaders}/fragment.glsl | 0 .../{ => shaders}/vertex.glsl | 0 .../components/SpacedArmorSubSpaced/index.ts | 6 +- .../{ => shaders}/fragment.glsl | 0 .../SpacedArmorSubSpaced/shaders/vertex.glsl | 11 + .../Armor/components/SpacedArmor/index.tsx | 5 +- .../Armor/components/SpacedArmorScene.tsx} | 20 +- src/components/Armor/index.tsx | 51 ++++ 14 files changed, 430 insertions(+), 24 deletions(-) create mode 100644 src/components/Armor/components/CoreArmor/index.tsx create mode 100644 src/components/Armor/components/CoreArmor/shaders/fragment.glsl rename src/components/Armor/components/{SpacedArmor/components/SpacedArmorSubSpaced => CoreArmor/shaders}/vertex.glsl (100%) create mode 100644 src/components/Armor/components/CoreArmorScene.tsx rename src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/{ => shaders}/fragment.glsl (100%) rename src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/{ => shaders}/vertex.glsl (100%) rename src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/{ => shaders}/fragment.glsl (100%) create mode 100644 src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/shaders/vertex.glsl rename src/{app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx => components/Armor/components/SpacedArmorScene.tsx} (91%) create mode 100644 src/components/Armor/index.tsx diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx index b73d552fd..563d57d4c 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -4,6 +4,7 @@ import { PerspectiveCamera } from '@react-three/drei'; import { Canvas } from '@react-three/fiber'; import { Suspense, use, useEffect, useRef, useState } from 'react'; import { isMobile } from 'react-device-detect'; +import { Armor } from '../../../../../../components/Armor'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; @@ -18,10 +19,12 @@ import mutateTankopediaPersistent, { } from '../../../../../../stores/tankopedia'; import { AntagonistBar } from '../AntagonistBar'; import { Controls } from '../Control'; +import { Lighting } from '../Lighting'; import { RotationInputs } from '../QuickInputs'; +import { SceneProps } from '../SceneProps'; import { ModelLoader } from './components/ModelLoader'; import { Options } from './components/Options'; -import { SpacedArmorScene } from './components/TankArmor/components/Spaced'; +import { TankModel } from './components/TankModel'; export function TankSandbox() { const canvas = useRef(null); @@ -189,13 +192,16 @@ export function TankSandbox() { > - {/* */} + }> {/* + + */} + {/* */} - */} - + + diff --git a/src/components/Armor/components/CoreArmor/index.tsx b/src/components/Armor/components/CoreArmor/index.tsx new file mode 100644 index 000000000..b718fea31 --- /dev/null +++ b/src/components/Armor/components/CoreArmor/index.tsx @@ -0,0 +1,71 @@ +import { useFrame } from '@react-three/fiber'; +import { + MeshBasicMaterial, + Object3D, + ShaderMaterial, + Vector2, + WebGLRenderTarget, +} from 'three'; +import { degToRad } from 'three/src/math/MathUtils'; +import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; +import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; +import { useDuel } from '../../../../stores/duel'; +import fragmentShader from './shaders/fragment.glsl'; +import vertexShader from './shaders/vertex.glsl'; + +export const spacedArmorRenderTarget = new WebGLRenderTarget(); + +interface CoreArmorProps { + thickness: number; + node: Object3D; +} + +const excludeMaterial = new MeshBasicMaterial({ + colorWrite: false, +}); + +export function CoreArmor({ node, thickness }: CoreArmorProps) { + const { shell } = useDuel.getState().antagonist!; + const penetration = resolveNearPenetration(shell.penetration); + const material = new ShaderMaterial({ + fragmentShader, + vertexShader, + + transparent: true, + + uniforms: { + thickness: { value: thickness }, + penetration: { value: penetration }, + caliber: { value: shell.caliber }, + ricochet: { value: degToRad(shell.ricochet ?? 90) }, + normalization: { value: degToRad(shell.normalization ?? 0) }, + + resolution: { value: new Vector2() }, + spacedArmorBuffer: { value: null }, + spacedArmorDepth: { value: null }, + }, + }); + + useFrame(({ gl }) => { + gl.getSize(material.uniforms.resolution.value).multiplyScalar( + gl.getPixelRatio(), + ); + material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture; + material.uniforms.spacedArmorDepth.value = + spacedArmorRenderTarget.depthTexture; + }); + + return ( + <> + {jsxTree(node, { + renderOrder: 0, + material: excludeMaterial, + })} + + {jsxTree(node, { + renderOrder: 1, + material, + })} + + ); +} diff --git a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl new file mode 100644 index 000000000..f44b9e4c1 --- /dev/null +++ b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl @@ -0,0 +1,46 @@ +varying vec3 vNormal; +varying vec3 vViewPosition; + +uniform float thickness; +uniform float penetration; +uniform float caliber; +uniform float ricochet; +uniform float normalization; +uniform vec2 resolution; + +uniform sampler2D spacedArmorBuffer; +uniform highp sampler2D spacedArmorDepth; + +void main() { + float penetrationChance = -1.0; + float splashChance = -1.0; + + vec2 screenCoordinates = gl_FragCoord.xy / resolution; + float angle = acos(dot(vNormal, -vViewPosition) / length(vViewPosition)); + + vec4 spacedArmorBufferFragment = texture2D(spacedArmorBuffer, screenCoordinates); + bool isUnderSpacedArmor = spacedArmorBufferFragment.a != 0.0; + + bool threeCalibersRule = caliber > thickness * 3.0; + if (!threeCalibersRule && !isUnderSpacedArmor && angle >= ricochet) { + penetrationChance = 0.0; + } else { + + bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0; + float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization; + float finalThickness = thickness / cos(angle - finalNormalization); + + float remainingPenetration = penetration; + if (isUnderSpacedArmor) { + float spacedArmorThickness = spacedArmorBufferFragment.r * penetration; + remainingPenetration -= spacedArmorThickness + finalThickness; + } else { + remainingPenetration -= finalThickness; + } + + penetrationChance = remainingPenetration < 0.0 ? 0.0 : 1.0; + } + + vec3 color = vec3(1.0, splashChance * 0.392, 0.0); + gl_FragColor = vec4(color, (1.0 - penetrationChance) * 0.5); +} \ No newline at end of file diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/vertex.glsl b/src/components/Armor/components/CoreArmor/shaders/vertex.glsl similarity index 100% rename from src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/vertex.glsl rename to src/components/Armor/components/CoreArmor/shaders/vertex.glsl diff --git a/src/components/Armor/components/CoreArmorScene.tsx b/src/components/Armor/components/CoreArmorScene.tsx new file mode 100644 index 000000000..dfe78232b --- /dev/null +++ b/src/components/Armor/components/CoreArmorScene.tsx @@ -0,0 +1,226 @@ +import { memo, useEffect, useRef } from 'react'; +import { Euler, Group, Vector3 } from 'three'; +import { degToRad } from 'three/src/math/MathUtils'; +import { I_HAT, J_HAT, K_HAT } from '../../../constants/axis'; +import { + ModelTransformEventData, + modelTransformEvent, +} from '../../../core/blitzkrieg/modelTransform'; +import { nameToArmorId } from '../../../core/blitzkrieg/nameToArmorId'; +import { resolveArmor } from '../../../core/blitzkrieg/resolveThickness'; +import { useArmor } from '../../../hooks/useArmor'; +import { useModel } from '../../../hooks/useModel'; +import { useModelDefinitions } from '../../../hooks/useModelDefinitions'; +import { useDuel } from '../../../stores/duel'; +import { useTankopediaPersistent } from '../../../stores/tankopedia'; +import { CoreArmor } from './CoreArmor'; + +interface CoreArmorSceneProps { + ornamental?: boolean; +} + +export const CoreArmorScene = memo( + ({ ornamental = false }) => { + const protagonist = useDuel((state) => state.protagonist!); + const wrapper = useRef(null); + const modelDefinitions = useModelDefinitions(); + const turretContainer = useRef(null); + const gunContainer = useRef(null); + const initialTankopediaState = useTankopediaPersistent.getState(); + + useEffect(() => { + if (!modelDefinitions) return; + + 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 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(hullOrigin) + .sub(turretOrigin) + .sub(gunOrigin) + .applyAxisAngle(I_HAT, pitch) + .add(gunOrigin) + .add(turretOrigin) + .add(hullOrigin); + 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(hullOrigin) + .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(I_HAT, initialPitch) + .applyAxisAngle(J_HAT, initialRoll) + .applyAxisAngle(K_HAT, initialYaw); + turretRotation.x += initialPitch; + turretRotation.y += initialRoll; + turretRotation.z += initialYaw; + } + + turretPosition.add(turretOrigin).add(hullOrigin); + turretContainer.current?.position.copy(turretPosition); + turretContainer.current?.rotation.copy(turretRotation); + } + + handleModelTransform(protagonist); + modelTransformEvent.on(handleModelTransform); + + return () => { + modelTransformEvent.off(handleModelTransform); + }; + }); + + useEffect(() => { + const unsubscribe = useTankopediaPersistent.subscribe( + (state) => state.mode, + (mode) => { + if (wrapper.current) wrapper.current.visible = mode === 'armor'; + }, + ); + + return unsubscribe; + }); + + const armorGltf = useArmor(protagonist.tank.id); + const { gltf: modelGltf } = useModel(protagonist.tank.id); + + const armorNodes = Object.values(armorGltf.nodes); + const tankModelDefinition = modelDefinitions[protagonist.tank.id]; + const trackModelDefinition = + tankModelDefinition.tracks[protagonist.track.id]; + const turretModelDefinition = + tankModelDefinition.turrets[protagonist.turret.id]; + const gunModelDefinition = turretModelDefinition.guns[protagonist.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); + + 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 || spaced || thickness === undefined) 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 || spaced || thickness === undefined) 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 || spaced || thickness === undefined) + return null; + + return ( + + + + ); + })} + + + + + ); + }, +); diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx index 2c36c0ac6..5c0a7c80b 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx @@ -7,8 +7,8 @@ import { import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree'; import { useDuel } from '../../../../../../stores/duel'; -import fragmentShader from './fragment.glsl'; -import vertexShader from './vertex.glsl'; +import fragmentShader from './shaders/fragment.glsl'; +import vertexShader from './shaders/vertex.glsl'; interface SpacedArmorSubExternalProps { node: Object3D; diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/fragment.glsl b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/shaders/fragment.glsl similarity index 100% rename from src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/fragment.glsl rename to src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/shaders/fragment.glsl diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/vertex.glsl b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/shaders/vertex.glsl similarity index 100% rename from src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/vertex.glsl rename to src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/shaders/vertex.glsl diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts index ec3ebeb42..a086aaaff 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts @@ -3,8 +3,8 @@ import { degToRad } from 'three/src/math/MathUtils'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree'; import { useDuel } from '../../../../../../stores/duel'; -import fragmentShader from './fragment.glsl'; -import vertexShader from './vertex.glsl'; +import fragmentShader from './shaders/fragment.glsl'; +import vertexShader from './shaders/vertex.glsl'; interface SpacedArmorSubSpacedProps { node: Object3D; @@ -18,8 +18,6 @@ export function SpacedArmorSubSpaced({ const { shell } = useDuel.getState().antagonist!; const penetration = resolveNearPenetration(shell.penetration); - console.log(shell.ricochet); - return jsxTree(node, { renderOrder: 2, material: new ShaderMaterial({ diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/fragment.glsl b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/shaders/fragment.glsl similarity index 100% rename from src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/fragment.glsl rename to src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/shaders/fragment.glsl diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/shaders/vertex.glsl b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/shaders/vertex.glsl new file mode 100644 index 000000000..82f94e255 --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/shaders/vertex.glsl @@ -0,0 +1,11 @@ +varying vec3 vNormal; +varying vec3 vViewPosition; + +void main() { + vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0); + + vViewPosition = vViewPosition4.xyz; + vNormal = normalMatrix * normal; + + gl_Position = projectionMatrix * vViewPosition4; +} \ No newline at end of file diff --git a/src/components/Armor/components/SpacedArmor/index.tsx b/src/components/Armor/components/SpacedArmor/index.tsx index eab1537af..365478cec 100644 --- a/src/components/Armor/components/SpacedArmor/index.tsx +++ b/src/components/Armor/components/SpacedArmor/index.tsx @@ -1,13 +1,10 @@ import { MeshBasicMaterial, Object3D } from 'three'; -import { ArmorType } from '../../../../app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced'; import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; import { useDuel } from '../../../../stores/duel'; +import { ArmorType } from '../SpacedArmorScene'; import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal'; import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced'; -// import fragmentShader from './shaders/fragment.glsl'; -// import vertexShader from './shaders/vertex.glsl'; - interface SpacedArmorProps { node: Object3D; ornamental?: boolean; diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx b/src/components/Armor/components/SpacedArmorScene.tsx similarity index 91% rename from src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx rename to src/components/Armor/components/SpacedArmorScene.tsx index 96fc79bf1..0fa76c37a 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced.tsx +++ b/src/components/Armor/components/SpacedArmorScene.tsx @@ -1,19 +1,19 @@ import { memo, useEffect, useRef } from 'react'; import { Euler, Group, Vector3 } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; -import { SpacedArmor } from '../../../../../../../../../components/Armor/components/SpacedArmor'; -import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis'; +import { I_HAT, J_HAT, K_HAT } from '../../../constants/axis'; import { ModelTransformEventData, modelTransformEvent, -} from '../../../../../../../../../core/blitzkrieg/modelTransform'; -import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId'; -import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness'; -import { useArmor } from '../../../../../../../../../hooks/useArmor'; -import { useModel } from '../../../../../../../../../hooks/useModel'; -import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions'; -import { useDuel } from '../../../../../../../../../stores/duel'; -import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia'; +} from '../../../core/blitzkrieg/modelTransform'; +import { nameToArmorId } from '../../../core/blitzkrieg/nameToArmorId'; +import { resolveArmor } from '../../../core/blitzkrieg/resolveThickness'; +import { useArmor } from '../../../hooks/useArmor'; +import { useModel } from '../../../hooks/useModel'; +import { useModelDefinitions } from '../../../hooks/useModelDefinitions'; +import { useDuel } from '../../../stores/duel'; +import { useTankopediaPersistent } from '../../../stores/tankopedia'; +import { SpacedArmor } from './SpacedArmor'; export enum ArmorType { Core, diff --git a/src/components/Armor/index.tsx b/src/components/Armor/index.tsx new file mode 100644 index 000000000..22300dea7 --- /dev/null +++ b/src/components/Armor/index.tsx @@ -0,0 +1,51 @@ +import { createPortal, useFrame } from '@react-three/fiber'; +import { DepthTexture, Scene, Vector2 } from 'three'; +import { spacedArmorRenderTarget } from './components/CoreArmor'; +import { CoreArmorScene } from './components/CoreArmorScene'; +import { SpacedArmorScene } from './components/SpacedArmorScene'; + +export function Armor() { + const spacedArmorScene = new Scene(); + const coreArmorScene = new Scene(); + const spacedArmorPortal = createPortal( + , + spacedArmorScene, + ); + const coreArmorPortal = createPortal(, coreArmorScene); + + const renderSize = new Vector2(); + const newRenderSize = new Vector2(); + + useFrame(({ gl, camera, scene }) => { + gl.autoClear = true; + + gl.getSize(newRenderSize).multiplyScalar(gl.getPixelRatio()); + + if (!newRenderSize.equals(renderSize)) { + renderSize.copy(newRenderSize); + spacedArmorRenderTarget.depthTexture = new DepthTexture( + renderSize.x, + renderSize.y, + ); + } + + spacedArmorRenderTarget.setSize(renderSize.x, renderSize.y); + gl.setRenderTarget(spacedArmorRenderTarget); + gl.render(spacedArmorScene, camera); + + gl.autoClear = false; + + gl.setRenderTarget(null); + gl.render(scene, camera); + + gl.clearDepth(); + gl.render(coreArmorScene, camera); + }, 1); + + return ( + <> + {spacedArmorPortal} + {coreArmorPortal} + + ); +} From f5c589373dabba6b17e98362fb42d6ceb10f665f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 13:50:15 -0600 Subject: [PATCH 06/32] feat: finalize ap --- .../components/CoreArmor/shaders/fragment.glsl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl index f44b9e4c1..7a5aafc5c 100644 --- a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl +++ b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl @@ -25,22 +25,22 @@ void main() { if (!threeCalibersRule && !isUnderSpacedArmor && angle >= ricochet) { penetrationChance = 0.0; } else { - bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0; float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization; float finalThickness = thickness / cos(angle - finalNormalization); - float remainingPenetration = penetration; + if (isUnderSpacedArmor) { - float spacedArmorThickness = spacedArmorBufferFragment.r * penetration; - remainingPenetration -= spacedArmorThickness + finalThickness; - } else { - remainingPenetration -= finalThickness; + remainingPenetration -= spacedArmorBufferFragment.r * penetration; } - penetrationChance = remainingPenetration < 0.0 ? 0.0 : 1.0; + float delta = finalThickness - remainingPenetration; + float randomization = remainingPenetration * 0.05; + + penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0); } vec3 color = vec3(1.0, splashChance * 0.392, 0.0); + gl_FragColor = vec4(0.0, penetrationChance, 0.0, 1.0); gl_FragColor = vec4(color, (1.0 - penetrationChance) * 0.5); } \ No newline at end of file From 06c878dd2c3065009931356385cf4cf7b290c47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:34:12 -0600 Subject: [PATCH 07/32] feat: some cleaning up --- .../[id]/components/Model/TankSandbox.tsx | 4 +--- .../Armor/components/CoreArmorScene.tsx | 2 -- .../SpacedArmorSubExternal/index.tsx | 21 +++++++++---------- .../components/SpacedArmorSubSpaced/index.ts | 2 +- .../Armor/components/SpacedArmor/index.tsx | 7 ------- 5 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx index 563d57d4c..5f24b01eb 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -195,9 +195,7 @@ export function TankSandbox() { }> - {/* - - */} + {/* */} {/* */} diff --git a/src/components/Armor/components/CoreArmorScene.tsx b/src/components/Armor/components/CoreArmorScene.tsx index dfe78232b..99778c6f9 100644 --- a/src/components/Armor/components/CoreArmorScene.tsx +++ b/src/components/Armor/components/CoreArmorScene.tsx @@ -9,7 +9,6 @@ import { import { nameToArmorId } from '../../../core/blitzkrieg/nameToArmorId'; import { resolveArmor } from '../../../core/blitzkrieg/resolveThickness'; import { useArmor } from '../../../hooks/useArmor'; -import { useModel } from '../../../hooks/useModel'; import { useModelDefinitions } from '../../../hooks/useModelDefinitions'; import { useDuel } from '../../../stores/duel'; import { useTankopediaPersistent } from '../../../stores/tankopedia'; @@ -117,7 +116,6 @@ export const CoreArmorScene = memo( }); const armorGltf = useArmor(protagonist.tank.id); - const { gltf: modelGltf } = useModel(protagonist.tank.id); const armorNodes = Object.values(armorGltf.nodes); const tankModelDefinition = modelDefinitions[protagonist.tank.id]; diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx index 5c0a7c80b..0f6ca3e72 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx @@ -19,12 +19,19 @@ export function SpacedArmorSubExternal({ node, thickness, }: SpacedArmorSubExternalProps) { - const penetration = resolveNearPenetration( - useDuel.getState().antagonist!.shell.penetration, - ); + const shell = useDuel((state) => state.antagonist!.shell); + const penetration = resolveNearPenetration(shell.penetration); return ( <> + {jsxTree(node, { + renderOrder: 3, + material: new MeshBasicMaterial({ + colorWrite: false, + depthTest: true, + depthWrite: true, + }), + })} {jsxTree(node, { renderOrder: 4, material: new ShaderMaterial({ @@ -41,14 +48,6 @@ export function SpacedArmorSubExternal({ }, }), })} - {jsxTree(node, { - renderOrder: 3, - material: new MeshBasicMaterial({ - colorWrite: false, - depthTest: true, - depthWrite: true, - }), - })} ); } diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts index a086aaaff..e3b941c26 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts @@ -15,7 +15,7 @@ export function SpacedArmorSubSpaced({ node, thickness, }: SpacedArmorSubSpacedProps) { - const { shell } = useDuel.getState().antagonist!; + const shell = useDuel((state) => state.antagonist!.shell); const penetration = resolveNearPenetration(shell.penetration); return jsxTree(node, { diff --git a/src/components/Armor/components/SpacedArmor/index.tsx b/src/components/Armor/components/SpacedArmor/index.tsx index 365478cec..219b69655 100644 --- a/src/components/Armor/components/SpacedArmor/index.tsx +++ b/src/components/Armor/components/SpacedArmor/index.tsx @@ -1,7 +1,5 @@ import { MeshBasicMaterial, Object3D } from 'three'; -import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; -import { useDuel } from '../../../../stores/duel'; import { ArmorType } from '../SpacedArmorScene'; import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal'; import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced'; @@ -18,11 +16,6 @@ const omitMaterial = new MeshBasicMaterial({ }); export function SpacedArmor({ node, type, thickness }: SpacedArmorProps) { - const penetrationRaw = useDuel( - (state) => state.antagonist!.shell.penetration, - ); - const penetration = resolveNearPenetration(penetrationRaw); - /** * Render orders allocations: * - 0 : core omission From 522b40aff10a0fd21df273e4cd508a5bd5717051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:15:24 -0600 Subject: [PATCH 08/32] feat: heat shells --- .vscode/settings.json | 4 +- .../Armor/components/CoreArmor/index.tsx | 32 ++++++-- .../CoreArmor/shaders/fragment.glsl | 30 +++++++- .../components/CoreArmor/shaders/vertex.glsl | 2 + .../Armor/components/CoreArmorScene.tsx | 20 ++--- .../SpacedArmorSubExternal/index.tsx | 42 +++++++---- .../components/SpacedArmorSubSpaced/index.ts | 40 ---------- .../components/SpacedArmorSubSpaced/index.tsx | 75 +++++++++++++++++++ .../Armor/components/SpacedArmor/index.tsx | 1 + .../Armor/components/SpacedArmorScene.tsx | 22 +++--- src/components/Armor/index.tsx | 5 +- 11 files changed, 183 insertions(+), 90 deletions(-) delete mode 100644 src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts create mode 100644 src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 7ef122024..b395babe4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -54,6 +54,7 @@ "glsl", "GLTF", "haha", + "highp", "iife", "immer", "Inclusivity", @@ -145,5 +146,6 @@ "zustand" ], "deno.enable": false, - "deno.unstable": false + "deno.unstable": false, + "cSpell.enableFiletypes": ["glsl"] } diff --git a/src/components/Armor/components/CoreArmor/index.tsx b/src/components/Armor/components/CoreArmor/index.tsx index b718fea31..d2e7170a9 100644 --- a/src/components/Armor/components/CoreArmor/index.tsx +++ b/src/components/Armor/components/CoreArmor/index.tsx @@ -1,4 +1,5 @@ import { useFrame } from '@react-three/fiber'; +import { useEffect } from 'react'; import { MeshBasicMaterial, Object3D, @@ -7,8 +8,10 @@ import { WebGLRenderTarget, } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; +import { isExplosive } from '../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; +import { ShellDefinition } from '../../../../core/blitzkrieg/tankDefinitions'; import { useDuel } from '../../../../stores/duel'; import fragmentShader from './shaders/fragment.glsl'; import vertexShader from './shaders/vertex.glsl'; @@ -25,8 +28,6 @@ const excludeMaterial = new MeshBasicMaterial({ }); export function CoreArmor({ node, thickness }: CoreArmorProps) { - const { shell } = useDuel.getState().antagonist!; - const penetration = resolveNearPenetration(shell.penetration); const material = new ShaderMaterial({ fragmentShader, vertexShader, @@ -35,10 +36,11 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { uniforms: { thickness: { value: thickness }, - penetration: { value: penetration }, - caliber: { value: shell.caliber }, - ricochet: { value: degToRad(shell.ricochet ?? 90) }, - normalization: { value: degToRad(shell.normalization ?? 0) }, + penetration: { value: null }, + caliber: { value: null }, + ricochet: { value: null }, + normalization: { value: null }, + isExplosive: { value: null }, resolution: { value: new Vector2() }, spacedArmorBuffer: { value: null }, @@ -46,6 +48,23 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { }, }); + useEffect(() => { + function handleChange(shell: ShellDefinition) { + material.uniforms.penetration.value = resolveNearPenetration( + shell.penetration, + ); + material.uniforms.caliber.value = shell.caliber; + material.uniforms.ricochet.value = degToRad(shell.ricochet ?? 90); + material.uniforms.normalization.value = degToRad( + shell.normalization ?? 0, + ); + material.uniforms.isExplosive.value = isExplosive(shell.type); + } + + handleChange(useDuel.getState().antagonist!.shell); + return useDuel.subscribe((state) => state.antagonist!.shell, handleChange); + }, []); + useFrame(({ gl }) => { gl.getSize(material.uniforms.resolution.value).multiplyScalar( gl.getPixelRatio(), @@ -61,7 +80,6 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { renderOrder: 0, material: excludeMaterial, })} - {jsxTree(node, { renderOrder: 1, material, diff --git a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl index 7a5aafc5c..dd6faaed4 100644 --- a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl +++ b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl @@ -1,5 +1,6 @@ varying vec3 vNormal; varying vec3 vViewPosition; +varying mat4 vProjectionMatrix; uniform float thickness; uniform float penetration; @@ -7,10 +8,17 @@ uniform float caliber; uniform float ricochet; uniform float normalization; uniform vec2 resolution; +uniform bool isExplosive; -uniform sampler2D spacedArmorBuffer; +uniform highp sampler2D spacedArmorBuffer; uniform highp sampler2D spacedArmorDepth; +float depthToDistance(float depth) { + vec4 clipPosition = vec4(gl_FragCoord.xy / resolution * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); + vec4 eyePosition = inverse(vProjectionMatrix) * clipPosition; + return length(eyePosition.xyz / eyePosition.w); +} + void main() { float penetrationChance = -1.0; float splashChance = -1.0; @@ -31,16 +39,30 @@ void main() { float remainingPenetration = penetration; if (isUnderSpacedArmor) { - remainingPenetration -= spacedArmorBufferFragment.r * penetration; + float spacedArmorThickness = spacedArmorBufferFragment.r * penetration; + remainingPenetration -= spacedArmorThickness; + + if (isExplosive && remainingPenetration > 0.0) { + // world space is in meters + vec4 spacedArmorDepthFragment = texture2D(spacedArmorDepth, screenCoordinates); + float spacedArmorDistance = depthToDistance(spacedArmorDepthFragment.r); + float coreArmorDistance = depthToDistance(gl_FragCoord.z); + float distanceFromSpacedArmor = coreArmorDistance - spacedArmorDistance; + + // there is a 50% penetration loss per meter for HE based shells + remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor; + } } + remainingPenetration = max(0.0, remainingPenetration); float delta = finalThickness - remainingPenetration; float randomization = remainingPenetration * 0.05; - penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0); } vec3 color = vec3(1.0, splashChance * 0.392, 0.0); - gl_FragColor = vec4(0.0, penetrationChance, 0.0, 1.0); gl_FragColor = vec4(color, (1.0 - penetrationChance) * 0.5); + + // non-trigonometric red-green mixing: https://www.desmos.com/calculator/ceo1oq7l67 + // gl_FragColor = vec4(-pow(penetrationChance, 2.0) + 1.0, -pow(penetrationChance - 1.0, 2.0) + 1.0, 0.0, 0.5); } \ No newline at end of file diff --git a/src/components/Armor/components/CoreArmor/shaders/vertex.glsl b/src/components/Armor/components/CoreArmor/shaders/vertex.glsl index 82f94e255..0e4d1407f 100644 --- a/src/components/Armor/components/CoreArmor/shaders/vertex.glsl +++ b/src/components/Armor/components/CoreArmor/shaders/vertex.glsl @@ -1,10 +1,12 @@ varying vec3 vNormal; varying vec3 vViewPosition; +varying mat4 vProjectionMatrix; void main() { vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0); vViewPosition = vViewPosition4.xyz; + vProjectionMatrix = projectionMatrix; vNormal = normalMatrix * normal; gl_Position = projectionMatrix * vViewPosition4; diff --git a/src/components/Armor/components/CoreArmorScene.tsx b/src/components/Armor/components/CoreArmorScene.tsx index 99778c6f9..d63d7d7f2 100644 --- a/src/components/Armor/components/CoreArmorScene.tsx +++ b/src/components/Armor/components/CoreArmorScene.tsx @@ -20,7 +20,6 @@ interface CoreArmorSceneProps { export const CoreArmorScene = memo( ({ ornamental = false }) => { - const protagonist = useDuel((state) => state.protagonist!); const wrapper = useRef(null); const modelDefinitions = useModelDefinitions(); const turretContainer = useRef(null); @@ -96,7 +95,7 @@ export const CoreArmorScene = memo( turretContainer.current?.rotation.copy(turretRotation); } - handleModelTransform(protagonist); + handleModelTransform(useDuel.getState().protagonist!); modelTransformEvent.on(handleModelTransform); return () => { @@ -115,15 +114,16 @@ export const CoreArmorScene = memo( return unsubscribe; }); - const armorGltf = useArmor(protagonist.tank.id); - + const tank = useDuel((state) => state.protagonist!.tank); + const track = useDuel((state) => state.protagonist!.track); + const turret = useDuel((state) => state.protagonist!.turret); + const gun = useDuel((state) => state.protagonist!.gun); + const armorGltf = useArmor(tank.id); const armorNodes = Object.values(armorGltf.nodes); - const tankModelDefinition = modelDefinitions[protagonist.tank.id]; - const trackModelDefinition = - tankModelDefinition.tracks[protagonist.track.id]; - const turretModelDefinition = - tankModelDefinition.turrets[protagonist.turret.id]; - const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; + 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], diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx index 0f6ca3e72..ddff80d45 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { AdditiveBlending, MeshBasicMaterial, @@ -6,6 +7,7 @@ import { } from 'three'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree'; +import { ShellDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions'; import { useDuel } from '../../../../../../stores/duel'; import fragmentShader from './shaders/fragment.glsl'; import vertexShader from './shaders/vertex.glsl'; @@ -19,8 +21,30 @@ export function SpacedArmorSubExternal({ node, thickness, }: SpacedArmorSubExternalProps) { - const shell = useDuel((state) => state.antagonist!.shell); - const penetration = resolveNearPenetration(shell.penetration); + const material = new ShaderMaterial({ + fragmentShader, + vertexShader, + + depthTest: true, + depthWrite: false, + blending: AdditiveBlending, + + uniforms: { + thickness: { value: thickness }, + penetration: { value: null }, + }, + }); + + useEffect(() => { + function handleChange(shell: ShellDefinition) { + material.uniforms.penetration.value = resolveNearPenetration( + shell.penetration, + ); + } + + handleChange(useDuel.getState().antagonist!.shell); + return useDuel.subscribe((state) => state.antagonist!.shell, handleChange); + }, []); return ( <> @@ -34,19 +58,7 @@ export function SpacedArmorSubExternal({ })} {jsxTree(node, { renderOrder: 4, - material: new ShaderMaterial({ - fragmentShader, - vertexShader, - - depthTest: true, - depthWrite: false, - blending: AdditiveBlending, - - uniforms: { - thickness: { value: thickness }, - penetration: { value: penetration }, - }, - }), + material, })} ); diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts deleted file mode 100644 index e3b941c26..000000000 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AdditiveBlending, Object3D, ShaderMaterial } from 'three'; -import { degToRad } from 'three/src/math/MathUtils'; -import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; -import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree'; -import { useDuel } from '../../../../../../stores/duel'; -import fragmentShader from './shaders/fragment.glsl'; -import vertexShader from './shaders/vertex.glsl'; - -interface SpacedArmorSubSpacedProps { - node: Object3D; - thickness: number; -} - -export function SpacedArmorSubSpaced({ - node, - thickness, -}: SpacedArmorSubSpacedProps) { - const shell = useDuel((state) => state.antagonist!.shell); - const penetration = resolveNearPenetration(shell.penetration); - - return jsxTree(node, { - renderOrder: 2, - material: new ShaderMaterial({ - fragmentShader, - vertexShader, - - depthTest: true, - depthWrite: false, - blending: AdditiveBlending, - - uniforms: { - thickness: { value: thickness }, - penetration: { value: penetration }, - caliber: { value: shell.caliber }, - ricochet: { value: degToRad(shell.ricochet ?? 90) }, - normalization: { value: degToRad(shell.normalization ?? 0) }, - }, - }), - }); -} diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx new file mode 100644 index 000000000..dd0906145 --- /dev/null +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx @@ -0,0 +1,75 @@ +import { useEffect } from 'react'; +import { + AdditiveBlending, + MeshBasicMaterial, + Object3D, + ShaderMaterial, +} from 'three'; +import { degToRad } from 'three/src/math/MathUtils'; +import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; +import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree'; +import { ShellDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions'; +import { useDuel } from '../../../../../../stores/duel'; +import fragmentShader from './shaders/fragment.glsl'; +import vertexShader from './shaders/vertex.glsl'; + +interface SpacedArmorSubSpacedProps { + node: Object3D; + thickness: number; +} + +const depthWriteMaterial = new MeshBasicMaterial({ + depthWrite: true, + colorWrite: false, +}); + +export function SpacedArmorSubSpaced({ + node, + thickness, +}: SpacedArmorSubSpacedProps) { + const material = new ShaderMaterial({ + fragmentShader, + vertexShader, + + depthTest: true, + depthWrite: false, + blending: AdditiveBlending, + + uniforms: { + thickness: { value: thickness }, + penetration: { value: null }, + caliber: { value: null }, + ricochet: { value: null }, + normalization: { value: null }, + }, + }); + + useEffect(() => { + function handleChange(shell: ShellDefinition) { + material.uniforms.penetration.value = resolveNearPenetration( + shell.penetration, + ); + material.uniforms.caliber.value = shell.caliber; + material.uniforms.ricochet.value = degToRad(shell.ricochet ?? 90); + material.uniforms.normalization.value = degToRad( + shell.normalization ?? 0, + ); + } + + handleChange(useDuel.getState().antagonist!.shell); + return useDuel.subscribe((state) => state.antagonist!.shell, handleChange); + }, []); + + return ( + <> + {jsxTree(node, { + renderOrder: 2, + material, + })} + {jsxTree(node, { + renderOrder: 5, + material: depthWriteMaterial, + })} + + ); +} diff --git a/src/components/Armor/components/SpacedArmor/index.tsx b/src/components/Armor/components/SpacedArmor/index.tsx index 219b69655..61be3770d 100644 --- a/src/components/Armor/components/SpacedArmor/index.tsx +++ b/src/components/Armor/components/SpacedArmor/index.tsx @@ -21,6 +21,7 @@ export function SpacedArmor({ node, type, thickness }: SpacedArmorProps) { * - 0 : core omission * - 1-2: spaced without depth buffer write but do test to allow additive blending * - 3-4: external with depth buffer write in first pass and read in second to simplify geometry + * - 5 : spaced depth write */ return ( diff --git a/src/components/Armor/components/SpacedArmorScene.tsx b/src/components/Armor/components/SpacedArmorScene.tsx index 0fa76c37a..781dd802f 100644 --- a/src/components/Armor/components/SpacedArmorScene.tsx +++ b/src/components/Armor/components/SpacedArmorScene.tsx @@ -27,7 +27,6 @@ interface SpacedArmorSceneProps { export const SpacedArmorScene = memo( ({ ornamental = false }) => { - const protagonist = useDuel((state) => state.protagonist!); const wrapper = useRef(null); const modelDefinitions = useModelDefinitions(); const turretContainer = useRef(null); @@ -103,7 +102,7 @@ export const SpacedArmorScene = memo( turretContainer.current?.rotation.copy(turretRotation); } - handleModelTransform(protagonist); + handleModelTransform(useDuel.getState().protagonist!); modelTransformEvent.on(handleModelTransform); return () => { @@ -122,17 +121,18 @@ export const SpacedArmorScene = memo( return unsubscribe; }); - const armorGltf = useArmor(protagonist.tank.id); - const { gltf: modelGltf } = useModel(protagonist.tank.id); - + const tank = useDuel((state) => state.protagonist!.tank); + const track = useDuel((state) => state.protagonist!.track); + const turret = useDuel((state) => state.protagonist!.turret); + const gun = useDuel((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[protagonist.tank.id]; - const trackModelDefinition = - tankModelDefinition.tracks[protagonist.track.id]; - const turretModelDefinition = - tankModelDefinition.turrets[protagonist.turret.id]; - const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; + 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], diff --git a/src/components/Armor/index.tsx b/src/components/Armor/index.tsx index 22300dea7..757a9ce3f 100644 --- a/src/components/Armor/index.tsx +++ b/src/components/Armor/index.tsx @@ -1,10 +1,11 @@ import { createPortal, useFrame } from '@react-three/fiber'; +import { memo } from 'react'; import { DepthTexture, Scene, Vector2 } from 'three'; import { spacedArmorRenderTarget } from './components/CoreArmor'; import { CoreArmorScene } from './components/CoreArmorScene'; import { SpacedArmorScene } from './components/SpacedArmorScene'; -export function Armor() { +export const Armor = memo(() => { const spacedArmorScene = new Scene(); const coreArmorScene = new Scene(); const spacedArmorPortal = createPortal( @@ -48,4 +49,4 @@ export function Armor() { {coreArmorPortal} ); -} +}); From 79d03233ae9c903bc5bb263c4dc8d591062d1c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:21:41 -0600 Subject: [PATCH 09/32] feat: he shells --- .../Armor/components/CoreArmor/index.tsx | 7 ++++++ .../CoreArmor/shaders/fragment.glsl | 24 ++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/components/Armor/components/CoreArmor/index.tsx b/src/components/Armor/components/CoreArmor/index.tsx index d2e7170a9..ee967dbd1 100644 --- a/src/components/Armor/components/CoreArmor/index.tsx +++ b/src/components/Armor/components/CoreArmor/index.tsx @@ -8,6 +8,7 @@ import { WebGLRenderTarget, } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; +import { canSplash } from '../../../../core/blitz/canSplash'; import { isExplosive } from '../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; @@ -41,6 +42,9 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { ricochet: { value: null }, normalization: { value: null }, isExplosive: { value: null }, + canSplash: { value: null }, + damage: { value: null }, + explosionRadius: { value: null }, resolution: { value: new Vector2() }, spacedArmorBuffer: { value: null }, @@ -59,6 +63,9 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { shell.normalization ?? 0, ); material.uniforms.isExplosive.value = isExplosive(shell.type); + material.uniforms.canSplash.value = canSplash(shell.type); + material.uniforms.damage.value = shell.damage.armor; + material.uniforms.explosionRadius.value = shell.explosionRadius; } handleChange(useDuel.getState().antagonist!.shell); diff --git a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl index dd6faaed4..eb8b31213 100644 --- a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl +++ b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl @@ -9,6 +9,9 @@ uniform float ricochet; uniform float normalization; uniform vec2 resolution; uniform bool isExplosive; +uniform bool canSplash; +uniform float damage; +uniform float explosionRadius; uniform highp sampler2D spacedArmorBuffer; uniform highp sampler2D spacedArmorDepth; @@ -38,17 +41,17 @@ void main() { float finalThickness = thickness / cos(angle - finalNormalization); float remainingPenetration = penetration; + // world space is in meters + vec4 spacedArmorDepthFragment = texture2D(spacedArmorDepth, screenCoordinates); + float spacedArmorDistance = depthToDistance(spacedArmorDepthFragment.r); + float coreArmorDistance = depthToDistance(gl_FragCoord.z); + float distanceFromSpacedArmor = coreArmorDistance - spacedArmorDistance; + if (isUnderSpacedArmor) { float spacedArmorThickness = spacedArmorBufferFragment.r * penetration; remainingPenetration -= spacedArmorThickness; if (isExplosive && remainingPenetration > 0.0) { - // world space is in meters - vec4 spacedArmorDepthFragment = texture2D(spacedArmorDepth, screenCoordinates); - float spacedArmorDistance = depthToDistance(spacedArmorDepthFragment.r); - float coreArmorDistance = depthToDistance(gl_FragCoord.z); - float distanceFromSpacedArmor = coreArmorDistance - spacedArmorDistance; - // there is a 50% penetration loss per meter for HE based shells remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor; } @@ -58,6 +61,15 @@ void main() { float delta = finalThickness - remainingPenetration; float randomization = remainingPenetration * 0.05; penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0); + + if (canSplash) { + // only allow splashing if the damage equation deals more than 0 damage + float nominalArmorThickness = finalThickness - remainingPenetration; + float finalDamage = 0.5 * damage * (1.0 - distanceFromSpacedArmor / explosionRadius) - 1.1 * nominalArmorThickness; + splashChance = finalDamage > 0.0 ? 1.0 : 0.0; + } else { + splashChance = 0.0; + } } vec3 color = vec3(1.0, splashChance * 0.392, 0.0); From 4b1fda9680d44fed49dee43237bf292201c41d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:36:05 -0600 Subject: [PATCH 10/32] feat: green penetration --- .../Armor/components/CoreArmor/index.tsx | 25 ++++++++++++++++--- .../CoreArmor/shaders/fragment.glsl | 12 ++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/components/Armor/components/CoreArmor/index.tsx b/src/components/Armor/components/CoreArmor/index.tsx index ee967dbd1..e13543fa4 100644 --- a/src/components/Armor/components/CoreArmor/index.tsx +++ b/src/components/Armor/components/CoreArmor/index.tsx @@ -14,6 +14,7 @@ import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetr import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; import { ShellDefinition } from '../../../../core/blitzkrieg/tankDefinitions'; import { useDuel } from '../../../../stores/duel'; +import { useTankopediaPersistent } from '../../../../stores/tankopedia'; import fragmentShader from './shaders/fragment.glsl'; import vertexShader from './shaders/vertex.glsl'; @@ -45,6 +46,7 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { canSplash: { value: null }, damage: { value: null }, explosionRadius: { value: null }, + greenPenetration: { value: null }, resolution: { value: new Vector2() }, spacedArmorBuffer: { value: null }, @@ -53,7 +55,7 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { }); useEffect(() => { - function handleChange(shell: ShellDefinition) { + function handleShellChange(shell: ShellDefinition) { material.uniforms.penetration.value = resolveNearPenetration( shell.penetration, ); @@ -67,9 +69,26 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { material.uniforms.damage.value = shell.damage.armor; material.uniforms.explosionRadius.value = shell.explosionRadius; } + function handleGreenPenetrationChange(greenPenetration: boolean) { + material.uniforms.greenPenetration.value = greenPenetration; + } - handleChange(useDuel.getState().antagonist!.shell); - return useDuel.subscribe((state) => state.antagonist!.shell, handleChange); + handleShellChange(useDuel.getState().antagonist!.shell); + handleGreenPenetrationChange( + useTankopediaPersistent.getState().model.visual.greenPenetration, + ); + + const unsubscribes = [ + useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange), + useTankopediaPersistent.subscribe( + (state) => state.model.visual.greenPenetration, + handleGreenPenetrationChange, + ), + ]; + + return () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()); + }; }, []); useFrame(({ gl }) => { diff --git a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl index eb8b31213..4e45e1aa5 100644 --- a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl +++ b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl @@ -12,6 +12,7 @@ uniform bool isExplosive; uniform bool canSplash; uniform float damage; uniform float explosionRadius; +uniform bool greenPenetration; uniform highp sampler2D spacedArmorBuffer; uniform highp sampler2D spacedArmorDepth; @@ -73,8 +74,13 @@ void main() { } vec3 color = vec3(1.0, splashChance * 0.392, 0.0); - gl_FragColor = vec4(color, (1.0 - penetrationChance) * 0.5); + if (greenPenetration) { + // non-trigonometric red-green mixing: https://www.desmos.com/calculator/ceo1oq7l67 + float falling = -pow(penetrationChance, 2.0) + 1.0; + float gaining = -pow(penetrationChance - 1.0, 2.0) + 1.0; + gl_FragColor = vec4(falling * color + gaining * vec3(0.0, 1.0, 0.0), 0.5); + } else { + gl_FragColor = vec4(color, (1.0 - penetrationChance) * 0.5); + } - // non-trigonometric red-green mixing: https://www.desmos.com/calculator/ceo1oq7l67 - // gl_FragColor = vec4(-pow(penetrationChance, 2.0) + 1.0, -pow(penetrationChance - 1.0, 2.0) + 1.0, 0.0, 0.5); } \ No newline at end of file From 01f919ddd3a8d0b34a66aa10cb3af65731c59464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:39:01 -0600 Subject: [PATCH 11/32] feat: wireframe mode --- .../Armor/components/CoreArmor/index.tsx | 22 ++++++++++++------- .../CoreArmor/shaders/fragment.glsl | 6 +++-- src/stores/tankopedia.ts | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/Armor/components/CoreArmor/index.tsx b/src/components/Armor/components/CoreArmor/index.tsx index e13543fa4..b34b50326 100644 --- a/src/components/Armor/components/CoreArmor/index.tsx +++ b/src/components/Armor/components/CoreArmor/index.tsx @@ -14,7 +14,10 @@ import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetr import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; import { ShellDefinition } from '../../../../core/blitzkrieg/tankDefinitions'; import { useDuel } from '../../../../stores/duel'; -import { useTankopediaPersistent } from '../../../../stores/tankopedia'; +import { + TankopediaPersistent, + useTankopediaPersistent, +} from '../../../../stores/tankopedia'; import fragmentShader from './shaders/fragment.glsl'; import vertexShader from './shaders/vertex.glsl'; @@ -47,6 +50,7 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { damage: { value: null }, explosionRadius: { value: null }, greenPenetration: { value: null }, + wireframe: { value: null }, resolution: { value: new Vector2() }, spacedArmorBuffer: { value: null }, @@ -69,20 +73,22 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { material.uniforms.damage.value = shell.damage.armor; material.uniforms.explosionRadius.value = shell.explosionRadius; } - function handleGreenPenetrationChange(greenPenetration: boolean) { - material.uniforms.greenPenetration.value = greenPenetration; + function handleVisualChange( + visual: TankopediaPersistent['model']['visual'], + ) { + material.uniforms.greenPenetration.value = visual.greenPenetration; + material.uniforms.wireframe.value = visual.wireframe; + material.wireframe = visual.wireframe; } handleShellChange(useDuel.getState().antagonist!.shell); - handleGreenPenetrationChange( - useTankopediaPersistent.getState().model.visual.greenPenetration, - ); + handleVisualChange(useTankopediaPersistent.getState().model.visual); const unsubscribes = [ useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange), useTankopediaPersistent.subscribe( - (state) => state.model.visual.greenPenetration, - handleGreenPenetrationChange, + (state) => state.model.visual, + handleVisualChange, ), ]; diff --git a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl index 4e45e1aa5..eb579b1cf 100644 --- a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl +++ b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl @@ -13,6 +13,7 @@ uniform bool canSplash; uniform float damage; uniform float explosionRadius; uniform bool greenPenetration; +uniform bool wireframe; uniform highp sampler2D spacedArmorBuffer; uniform highp sampler2D spacedArmorDepth; @@ -73,14 +74,15 @@ void main() { } } + float alpha = wireframe ? 1.0 : 0.5; vec3 color = vec3(1.0, splashChance * 0.392, 0.0); if (greenPenetration) { // non-trigonometric red-green mixing: https://www.desmos.com/calculator/ceo1oq7l67 float falling = -pow(penetrationChance, 2.0) + 1.0; float gaining = -pow(penetrationChance - 1.0, 2.0) + 1.0; - gl_FragColor = vec4(falling * color + gaining * vec3(0.0, 1.0, 0.0), 0.5); + gl_FragColor = vec4(falling * color + gaining * vec3(0.0, 1.0, 0.0), alpha); } else { - gl_FragColor = vec4(color, (1.0 - penetrationChance) * 0.5); + gl_FragColor = vec4(color, (1.0 - penetrationChance) * alpha); } } \ No newline at end of file diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 773014f21..357d87d83 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -13,7 +13,7 @@ export type TankopediaSortDirection = 'ascending' | 'descending'; export type TankopediaTestTankDisplay = 'include' | 'exclude' | 'only'; export type TankopediaMode = 'model' | 'armor'; -interface TankopediaPersistent { +export interface TankopediaPersistent { model: { visual: { wireframe: boolean; From 07cdb0788c4fb7ca6d428c7c5c348ae681f9d3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:44:18 -0600 Subject: [PATCH 12/32] feat: opaque mode --- .../components/Model/components/Options.tsx | 38 +++++++++++++------ .../Armor/components/CoreArmor/index.tsx | 2 + .../CoreArmor/shaders/fragment.glsl | 4 +- src/stores/tankopedia.ts | 2 + 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx index 9896894d8..e3ac0d6c9 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx @@ -30,6 +30,7 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) { const wireframe = useTankopediaPersistent( (state) => state.model.visual.wireframe, ); + const opaque = useTankopediaPersistent((state) => state.model.visual.opaque); const fullScreenAvailable = typeof document !== 'undefined' && document.fullscreenEnabled; const environment = useTankopediaPersistent( @@ -78,6 +79,8 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) { + Armor + { @@ -89,6 +92,30 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) { Green penetration + { + mutateTankopediaPersistent((draft) => { + draft.model.visual.wireframe = checked; + }); + }} + > + Wireframe + + + { + mutateTankopediaPersistent((draft) => { + draft.model.visual.opaque = checked; + }); + }} + > + Opaque + + + Environment + { @@ -132,17 +159,6 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) { - - { - mutateTankopediaPersistent((draft) => { - draft.model.visual.wireframe = checked; - }); - }} - > - Wireframe - )} diff --git a/src/components/Armor/components/CoreArmor/index.tsx b/src/components/Armor/components/CoreArmor/index.tsx index b34b50326..8ddb62118 100644 --- a/src/components/Armor/components/CoreArmor/index.tsx +++ b/src/components/Armor/components/CoreArmor/index.tsx @@ -51,6 +51,7 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { explosionRadius: { value: null }, greenPenetration: { value: null }, wireframe: { value: null }, + opaque: { value: null }, resolution: { value: new Vector2() }, spacedArmorBuffer: { value: null }, @@ -78,6 +79,7 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { ) { material.uniforms.greenPenetration.value = visual.greenPenetration; material.uniforms.wireframe.value = visual.wireframe; + material.uniforms.opaque.value = visual.opaque || visual.wireframe; material.wireframe = visual.wireframe; } diff --git a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl index eb579b1cf..b5ad53518 100644 --- a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl +++ b/src/components/Armor/components/CoreArmor/shaders/fragment.glsl @@ -14,6 +14,7 @@ uniform float damage; uniform float explosionRadius; uniform bool greenPenetration; uniform bool wireframe; +uniform bool opaque; uniform highp sampler2D spacedArmorBuffer; uniform highp sampler2D spacedArmorDepth; @@ -74,7 +75,7 @@ void main() { } } - float alpha = wireframe ? 1.0 : 0.5; + float alpha = opaque ? 1.0 : 0.5; vec3 color = vec3(1.0, splashChance * 0.392, 0.0); if (greenPenetration) { // non-trigonometric red-green mixing: https://www.desmos.com/calculator/ceo1oq7l67 @@ -84,5 +85,4 @@ void main() { } else { gl_FragColor = vec4(color, (1.0 - penetrationChance) * alpha); } - } \ No newline at end of file diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 357d87d83..817492ab1 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -17,6 +17,7 @@ export interface TankopediaPersistent { model: { visual: { wireframe: boolean; + opaque: boolean; environment: (typeof ENVIRONMENTS)[number]; controlsEnabled: boolean; showGrid: boolean; @@ -105,6 +106,7 @@ export const useTankopediaPersistent = create()( model: { visual: { wireframe: false, + opaque: false, environment: ENVIRONMENTS[0], controlsEnabled: true, showGrid: true, From b2f026c212466b39dbceb4818576d63b5a81aca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:56:01 -0600 Subject: [PATCH 13/32] fix: persist settings --- src/stores/app.ts | 3 ++- src/stores/session.ts | 3 ++- src/stores/tankopedia.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/stores/app.ts b/src/stores/app.ts index 1c1366937..e615eb1f9 100644 --- a/src/stores/app.ts +++ b/src/stores/app.ts @@ -1,3 +1,4 @@ +import { merge } from 'lodash'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; @@ -10,6 +11,6 @@ export const useApp = create()( (set) => ({ devBuildAgreementTime: 0, }), - { name: 'app' }, + { name: 'app', merge: (a, b) => merge(b, a) }, ), ); diff --git a/src/stores/session.ts b/src/stores/session.ts index ffc8b46c9..3a2011e9f 100644 --- a/src/stores/session.ts +++ b/src/stores/session.ts @@ -1,4 +1,5 @@ import { produce } from 'immer'; +import { merge } from 'lodash'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { CustomColumnDisplay } from '../app/tools/session/components/CustomColumn'; @@ -83,7 +84,7 @@ export const useSession = create()( ], color: 'slate', }), - { name: 'session' }, + { name: 'session', merge: (a, b) => merge(b, a) }, ), ); diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 817492ab1..5b08f2437 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -128,7 +128,7 @@ export const useTankopediaPersistent = create()( }, mode: isDev() ? 'armor' : 'model', })), - { name: 'tankopedia', merge }, + { name: 'tankopedia', merge: (a, b) => merge(b, a) }, ), ); From d37706744121c300e5f944db62a2860f01d2e083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:57:59 -0600 Subject: [PATCH 14/32] feat: remove unused code --- .../ArmorMesh/components/Armor/index.tsx | 459 ------------------ .../components/Armor/shaders/fragment.glsl | 120 ----- .../components/Armor/shaders/vertex.glsl | 13 - .../components/ExternalModuleMask.tsx | 52 -- .../components/SpacedArmorDepth/index.tsx | 72 --- .../SpacedArmorDepth/shaders/fragment.glsl | 11 - .../SpacedArmorDepth/shaders/vertex.glsl | 10 - src/components/ArmorMesh/index.tsx | 2 - 8 files changed, 739 deletions(-) delete mode 100644 src/components/ArmorMesh/components/Armor/index.tsx delete mode 100644 src/components/ArmorMesh/components/Armor/shaders/fragment.glsl delete mode 100644 src/components/ArmorMesh/components/Armor/shaders/vertex.glsl delete mode 100644 src/components/ArmorMesh/components/ExternalModuleMask.tsx delete mode 100644 src/components/ArmorMesh/components/SpacedArmorDepth/index.tsx delete mode 100644 src/components/ArmorMesh/components/SpacedArmorDepth/shaders/fragment.glsl delete mode 100644 src/components/ArmorMesh/components/SpacedArmorDepth/shaders/vertex.glsl delete mode 100644 src/components/ArmorMesh/index.tsx diff --git a/src/components/ArmorMesh/components/Armor/index.tsx b/src/components/ArmorMesh/components/Armor/index.tsx deleted file mode 100644 index 816799dfc..000000000 --- a/src/components/ArmorMesh/components/Armor/index.tsx +++ /dev/null @@ -1,459 +0,0 @@ -import { MeshProps, useFrame, useThree } from '@react-three/fiber'; -import { memo, useEffect, useRef } from 'react'; -import { - IUniform, - Mesh, - Quaternion, - Raycaster, - ShaderMaterial, - Vector2, - Vector3, -} from 'three'; -import { degToRad } from 'three/src/math/MathUtils'; -import { canRicochet } from '../../../../core/blitz/canRicochet'; -import { canSplash } from '../../../../core/blitz/canSplash'; -import { isExplosive } from '../../../../core/blitz/isExplosive'; -import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; -import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment'; -import { useAwait } from '../../../../hooks/useAwait'; -import { useDuel } from '../../../../stores/duel'; -import { - ArmorPiercingLayer, - Shot, - mutateTankopediaTemporary, - useTankopediaPersistent, -} from '../../../../stores/tankopedia'; -import { externalModuleMaskRenderTarget } from '../ExternalModuleMask'; -import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth'; -import fragmentShader from './shaders/fragment.glsl'; -import vertexShader from './shaders/vertex.glsl'; - -interface ArmorMeshProps extends MeshProps { - thickness: number; - maxThickness: number; -} - -export type ArmorMeshUserData = { - type: 'coreArmor' | 'spacedArmor' | 'externalModule'; - thickness: number; -}; - -export const ArmorMesh = memo( - ({ thickness, maxThickness, ...props }) => { - const camera = useThree((state) => state.camera); - const initialShell = useDuel.getState().antagonist!.shell; - const initialTankopedia = useTankopediaPersistent.getState(); - const material = useRef(null); - const explosionCapable = isExplosive(initialShell.type); - const ricochetCapable = canRicochet(initialShell.type); - const cameraNormal = new Vector3(); - const shellNormal = new Vector3(); - const surfaceNormal = new Vector3(); - const mesh = useRef(null); - const scene = useThree((state) => state.scene); - const hasCalibratedShells = useAwait(hasEquipment(103, true)); - const hasEnhancedArmor = useAwait(hasEquipment(110)); - const wireframe = initialTankopedia.model.visual.wireframe; - - useEffect(() => { - async function updateQuickEquipments() { - const shell = useDuel.getState().antagonist!.shell; - const nearPenetration = resolveNearPenetration(shell.penetration); - const hasCalibratedShells = await hasEquipment(103, true); - const hasEnhancedArmor = await hasEquipment(110); - - if (material.current) { - material.current.uniforms.thickness.value = - thickness * (hasEnhancedArmor ? 1.04 : 1); - material.current.uniforms.penetration.value = - nearPenetration * - (hasCalibratedShells ? (isExplosive(shell.type) ? 1.1 : 1.05) : 1); - } - } - - function updateShellProperties() { - const shell = useDuel.getState().antagonist!.shell; - - if (material.current) { - material.current.uniforms.canRicochet.value = canRicochet(shell.type); - material.current.uniforms.isExplosive.value = isExplosive(shell.type); - material.current.uniforms.canSplash.value = canSplash(shell.type); - material.current.uniforms.caliber.value = shell.caliber; - material.current.uniforms.ricochetAngle.value = degToRad( - shell.ricochet ?? 90, - ); - material.current.uniforms.damage.value = shell.damage.armor; - material.current.uniforms.explosionRadius.value = - shell.explosionRadius ?? 0; - } - - updateQuickEquipments(); - } - - const unsubscribes = [ - useDuel.subscribe( - (state) => state.protagonist!.equipment, - updateQuickEquipments, - ), - useDuel.subscribe( - (state) => state.antagonist!.equipment, - updateQuickEquipments, - ), - useDuel.subscribe( - (state) => state.antagonist!.shell, - updateShellProperties, - ), - useTankopediaPersistent.subscribe( - (state) => state.model.visual.greenPenetration, - (greenPenetration) => { - if (material.current) { - material.current.uniforms.greenPenetration.value = - greenPenetration; - } - }, - ), - useTankopediaPersistent.subscribe( - (state) => state.model.visual.wireframe, - (wireframe) => { - if (material.current) { - material.current.wireframe = wireframe; - material.current.uniforms.wireframe.value = wireframe; - } - }, - ), - ]; - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }, []); - - useFrame(({ gl, camera }) => { - if (material.current) { - gl.getSize(material.current.uniforms.resolution.value); - - ( - material.current.uniforms.resolution as IUniform - ).value.multiplyScalar(gl.getPixelRatio()); - - material.current.uniforms.externalModuleMask.value = - externalModuleMaskRenderTarget.texture; - material.current.uniforms.spacedArmorDepth.value = - spacedArmorDepthRenderTarget.depthTexture; - material.current.uniforms.spacedArmorMask.value = - spacedArmorDepthRenderTarget.texture; - material.current.uniforms.projectionMatrixInverse.value = - camera.projectionMatrixInverse; - } - }); - - return ( - <> - - - - - { - // if (useTankopediaTemporary.getState().mode !== 'armor') return; - - shellNormal.copy( - cameraNormal.copy(camera.position).sub(event.point).normalize(), - ); - surfaceNormal - .copy(event.normal!) - .applyQuaternion( - event.object.getWorldQuaternion(new Quaternion()), - ); - const angle = Math.acos(surfaceNormal.dot(shellNormal)); - const shell = useDuel.getState().antagonist!.shell; - const hasCalibratedShells = await hasEquipment(103, true); - const resolvedPenetration = - resolveNearPenetration(shell.penetration) * - (hasCalibratedShells - ? isExplosive(shell.type) - ? 1.1 - : 1.05 - : 1); - const indexedObjects = new Set(); - const intersections = event.intersections - .filter(({ object }) => Boolean(object.userData.type)) - .map((event) => { - const typedEvent = event as typeof event & { - object: (typeof event)['object'] & { - userData: ArmorMeshUserData; - }; - }; - - return typedEvent; - }) - .filter(({ object }) => { - if (indexedObjects.has(object.userData.type)) return false; - indexedObjects.add(object.userData.type); - return true; - }); - const intersectionsTillFirstCoreArmor = intersections.slice( - 0, - intersections.findIndex( - ({ object }) => object.userData.type === 'coreArmor', - ) + 1, - ); - const coreArmorMesh = - intersectionsTillFirstCoreArmor.at(-1)!.object; - const hasEnhancedArmor = await hasEquipment(110); - const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1; - - if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return; - - let hasRicochet = false; - const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor - .map((event, index) => { - if (hasRicochet) return null; - - switch (event.object.userData.type) { - case 'externalModule': { - return { - nominal: - event.object.userData.thickness * thicknessCoefficient, - angled: - event.object.userData.thickness * thicknessCoefficient, - type: 'external', - distance: event.distance, - ricochet: false, - } satisfies ArmorPiercingLayer; - } - - case 'spacedArmor': - case 'coreArmor': { - const threeCalibersRule = - shell.caliber > - 3 * - (event.object.userData.thickness * - thicknessCoefficient); - - if ( - index === 0 && - canRicochet(shell.type) && - !threeCalibersRule && - angle >= degToRad(shell.ricochet!) - ) { - hasRicochet = true; - } - - const twoCalibersRule = - shell.caliber > - 2 * - (event.object.userData.thickness * - thicknessCoefficient); - const normalization = - twoCalibersRule && event.object.userData.thickness > 0 - ? ((shell.normalization ?? 0) * 1.4 * shell.caliber) / - (2 * - event.object.userData.thickness * - thicknessCoefficient) - : shell.normalization ?? 0; - const angled = - (event.object.userData.thickness * thicknessCoefficient) / - Math.cos(angle - degToRad(normalization)); - - return { - ricochet: hasRicochet, - nominal: - event.object.userData.thickness * thicknessCoefficient, - angled, - type: - event.object.userData.type === 'coreArmor' - ? 'core' - : 'spaced', - distance: event.distance, - } satisfies ArmorPiercingLayer; - } - } - }) - .filter(Boolean); - const gaps = armorPiercingLayersRaw.map((layerA, aIndex) => { - if (aIndex === armorPiercingLayersRaw.length - 1) return null; - const layerB = armorPiercingLayersRaw[aIndex + 1]; - - return { - type: 'gap', - gap: Math.abs(layerB.distance - layerA.distance), - } satisfies ArmorPiercingLayer; - }); - const armorPiercingLayers: ArmorPiercingLayer[] = []; - - armorPiercingLayersRaw.forEach((layer, index) => { - const gap = gaps[index]; - - armorPiercingLayers.push(layer); - if (gap) armorPiercingLayers.push(gap); - }); - - let remainingPenetration = resolvedPenetration; - const explosiveCapable = isExplosive(shell.type); - - armorPiercingLayers.forEach((layer) => { - if (layer.type !== 'gap') { - if (layer.ricochet) { - remainingPenetration *= 0.75; - } else { - remainingPenetration -= layer.angled; - } - } else if (explosiveCapable) { - // there is a 50% penetration loss per meter for HE based shells - remainingPenetration -= 0.5 * remainingPenetration * layer.gap; - } - - remainingPenetration = Math.max(0, remainingPenetration); - }); - - const hasPenetrated = remainingPenetration > 0; - let ricochet: Shot['ricochet'] = undefined; - - if (hasRicochet) { - const raycaster = new Raycaster(); - const bounceNormal = shellNormal - .clone() - .multiplyScalar(-1) - .sub( - surfaceNormal - .clone() - .multiplyScalar( - 2 * - surfaceNormal.dot( - shellNormal.clone().multiplyScalar(-1), - ), - ), - ); - raycaster.set(event.point, bounceNormal); - const intersections = raycaster - .intersectObjects(scene.children, true) - .filter( - (intersection) => - typeof intersection.object.userData?.type === 'string', - ); - const indexOfCoreArmor = intersections.findIndex( - ({ object }) => object.userData.type === 'coreArmor', - ); - const intersectionsTillFirstCoreArmor = intersections.slice( - 0, - indexOfCoreArmor + 1, - ); - - if (intersectionsTillFirstCoreArmor.length > 0) { - intersectionsTillFirstCoreArmor.forEach((intersection) => { - switch (intersection.object.userData.type) { - case 'externalModule': { - remainingPenetration -= - intersection.object.userData.thickness; - break; - } - - case 'spacedArmor': - case 'coreArmor': { - const angle = bounceNormal.angleTo( - intersection.object.position.clone().sub(event.point), - ); - const twoCalibersRule = - shell.caliber > - 2 * - (intersection.object.userData.thickness * - thicknessCoefficient); - const normalization = twoCalibersRule - ? ((shell.normalization ?? 0) * 1.4 * shell.caliber) / - (2 * - intersection.object.userData.thickness * - thicknessCoefficient) - : shell.normalization ?? 0; - const angled = - (intersection.object.userData.thickness * - thicknessCoefficient) / - Math.cos(angle - degToRad(normalization)); - - remainingPenetration -= angled; - } - } - }); - - ricochet = { - point: intersectionsTillFirstCoreArmor - .at(-1)! - .point.toArray(), - distance: intersectionsTillFirstCoreArmor.at(-1)!.distance, - penetration: remainingPenetration > 0, - }; - } - } - - mutateTankopediaTemporary((draft) => { - draft.shot = { - type: hasRicochet - ? 'ricochet' - : hasPenetrated - ? 'penetration' - : 'block', - point: event.point.toArray(), - thicknesses: armorPiercingLayers, - shellNormal: shellNormal.toArray(), - surfaceNormal: surfaceNormal.toArray(), - angle, - ricochet, - }; - }); - }} - > - - - - ); - }, - () => true, -); diff --git a/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl b/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl deleted file mode 100644 index 1bb58ff26..000000000 --- a/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl +++ /dev/null @@ -1,120 +0,0 @@ -#define PI 3.1415926535897932384626433832795 - -varying vec3 vViewPosition; -varying vec3 vNormal; -uniform bool isExplosive; -uniform bool canRicochet; -uniform bool canSplash; -uniform float thickness; -uniform float penetration; -uniform float caliber; -uniform float ricochetAngle; -uniform float normalization; -uniform sampler2D externalModuleMask; -uniform highp sampler2D spacedArmorDepth; -uniform sampler2D spacedArmorMask; -uniform vec2 resolution; -varying mat4 vProjectionMatrix; -uniform float maxThickness; -uniform bool greenPenetration; -uniform float damage; -uniform float explosionRadius; -uniform bool wireframe; - -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; -} - -float angleTo(vec3 a, vec3 b) { - return acos(dot(a, b) / (length(a) * length(b))); -} - -void main() { - float penetrationChance = -1.0; - float splashChance = -1.0; - - vec2 screenCoordinates = gl_FragCoord.xy / resolution; - vec4 externalModuleMaskColor = texture2D(externalModuleMask, screenCoordinates); - vec4 spacedArmorMaskColor = texture2D(spacedArmorMask, screenCoordinates); - bool isUnderExternalModule = externalModuleMaskColor.r == 1.0; - bool isUnderSpacedArmor = spacedArmorMaskColor.a == 1.0; - float spacedArmorAngle = spacedArmorMaskColor.r * (PI / 2.0); - - float coreArmorAngle = angleTo(vNormal, -vViewPosition); - bool coreArmorThreeCalibersRule = caliber > 3.0 * thickness; - float spacedArmorNominalThickness = spacedArmorMaskColor.g * maxThickness; - bool spacedArmorThreeCalibersRule = isUnderSpacedArmor && caliber > 3.0 * spacedArmorNominalThickness; - bool hasRicochetedSpacedArmor = isUnderSpacedArmor && !spacedArmorThreeCalibersRule && spacedArmorAngle >= ricochetAngle; - bool hasRicochetedCoreArmor = !hasRicochetedSpacedArmor && !isUnderExternalModule && canRicochet && !coreArmorThreeCalibersRule && coreArmorAngle >= ricochetAngle; - - if (hasRicochetedCoreArmor || hasRicochetedSpacedArmor) { - penetrationChance = 0.0; - splashChance = 0.0; - } else { - // time to chip away at the penetration, one case at a time - float remainingPenetration = penetration; - - if (isUnderExternalModule) { - // external modules don't care about angle, they'll reduce penetration by their thickness - float externalModuleThickness = externalModuleMaskColor.g * maxThickness; - remainingPenetration -= externalModuleThickness; - } - - if (isUnderSpacedArmor) { - // spaced armor on the other hand, does care about angle - bool spacedArmorTwoCalibersRule = caliber > 2.0 * spacedArmorNominalThickness && spacedArmorNominalThickness > 0.0; - float finalSpacedArmorNormalization = spacedArmorTwoCalibersRule ? (normalization * 1.4 * caliber) / (2.0 * spacedArmorNominalThickness) : normalization; - float finalSpacedArmorThickness = spacedArmorNominalThickness / cos(spacedArmorAngle - finalSpacedArmorNormalization); - remainingPenetration -= finalSpacedArmorThickness; - } - remainingPenetration = max(remainingPenetration, 0.0); - - float spacedArmorDepth = texture2D(spacedArmorDepth, screenCoordinates).r; - float spacedArmorDistance = depthToDistance(spacedArmorDepth); - float coreArmorDistance = depthToDistance(gl_FragCoord.z); - float distanceFromSpacedArmor = abs(coreArmorDistance - spacedArmorDistance); - if (isExplosive) { - // there is a 50% penetration loss per meter for HE based shells - remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor; - } - - bool coreArmorTwoCalibersRule = caliber > 2.0 * thickness && thickness > 0.0; - float finalNormalization = coreArmorTwoCalibersRule ? (normalization * 1.4 * caliber) / (2.0 * thickness) : normalization; - float finalCoreArmorThickness = thickness / cos(coreArmorAngle - finalNormalization); - float deltaPenetration = finalCoreArmorThickness - remainingPenetration; - float randomRadius = remainingPenetration * 0.05; - - if (deltaPenetration > randomRadius) { - // even with a 5% buff to the penetration, can't penetrate - penetrationChance = 0.0; - } else if (deltaPenetration < -randomRadius) { - // even with a 5% nerf, can penetrate - penetrationChance = 1.0; - } else { - // transition area - penetrationChance = max(1.0 - (deltaPenetration + randomRadius) / (2.0 * randomRadius), 0.0); - } - - if (canSplash) { - // only allow splasing if the damage equation deals more than 0 damage - float nominalArmorThickness = finalCoreArmorThickness - remainingPenetration; - float finalDamage = 0.5 * damage * (1.0 - distanceFromSpacedArmor / explosionRadius) - 1.1 * nominalArmorThickness; - splashChance = finalDamage > 0.0 ? 1.0 : 0.0; - } else { - splashChance = 0.0; - } - } - - vec3 color = vec3(1.0, splashChance * 0.392, 0.0); - if (greenPenetration || wireframe) { - gl_FragColor = vec4(mix(color, vec3(0.0, 1.0, 0.0), penetrationChance), wireframe ? 1.0 : 0.5); - } else { - gl_FragColor = vec4(color, (1.0 - penetrationChance) * 0.5); - } -} \ No newline at end of file diff --git a/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl b/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl deleted file mode 100644 index 0e4d1407f..000000000 --- a/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl +++ /dev/null @@ -1,13 +0,0 @@ -varying vec3 vNormal; -varying vec3 vViewPosition; -varying mat4 vProjectionMatrix; - -void main() { - vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0); - - vViewPosition = vViewPosition4.xyz; - vProjectionMatrix = projectionMatrix; - vNormal = normalMatrix * normal; - - gl_Position = projectionMatrix * vViewPosition4; -} \ No newline at end of file diff --git a/src/components/ArmorMesh/components/ExternalModuleMask.tsx b/src/components/ArmorMesh/components/ExternalModuleMask.tsx deleted file mode 100644 index 1d5aed6bb..000000000 --- a/src/components/ArmorMesh/components/ExternalModuleMask.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Color, MeshBasicMaterial, Object3D, WebGLRenderTarget } from 'three'; -import { jsxTree } from '../../../core/blitzkrieg/jsxTree'; -import { ArmorMeshUserData } from './Armor'; - -export const externalModuleMaskRenderTarget = new WebGLRenderTarget(); - -type ArmorMeshExternalModuleMaskProps = { - ornamental?: boolean; - node: Object3D; -} & ( - | { - exclude?: false; - maxThickness: number; - thickness: number; - } - | { - exclude: true; - } -); - -const excludeMaterial = new MeshBasicMaterial({ - colorWrite: false, -}); - -export function ArmorMeshExternalModuleMask({ - ornamental = false, - node, - ...props -}: ArmorMeshExternalModuleMaskProps) { - return ( - <> - {props.exclude && - !ornamental && - jsxTree(node, { renderOrder: 0, material: excludeMaterial })} - - {!props.exclude && - jsxTree(node, { - renderOrder: 1, - onClick: ornamental ? () => {} : undefined, - userData: { - type: 'externalModule', - thickness: props.thickness, - } satisfies ArmorMeshUserData, - material: new MeshBasicMaterial({ - color: new Color(1, props.thickness / props.maxThickness, 0), - depthWrite: false, - colorWrite: !ornamental, - }), - })} - - ); -} diff --git a/src/components/ArmorMesh/components/SpacedArmorDepth/index.tsx b/src/components/ArmorMesh/components/SpacedArmorDepth/index.tsx deleted file mode 100644 index 9d61e0796..000000000 --- a/src/components/ArmorMesh/components/SpacedArmorDepth/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { - MeshBasicMaterial, - Object3D, - ShaderMaterial, - WebGLRenderTarget, -} from 'three'; -import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; -import { ArmorMeshUserData } from '../Armor'; -import fragmentShader from './shaders/fragment.glsl'; -import vertexShader from './shaders/vertex.glsl'; - -export const spacedArmorDepthRenderTarget = new WebGLRenderTarget(); - -type ArmorMeshSpacedArmorDepthProps = { - ornamental?: boolean; - node: Object3D; -} & ( - | { - include?: false; - isExternalModule: boolean; - } - | { - include: true; - thickness: number; - maxThickness: number; - } -); - -const excludeMaterial = new MeshBasicMaterial({ - colorWrite: false, -}); -const includeMaterial = new MeshBasicMaterial({ - colorWrite: false, - depthWrite: false, -}); - -export function ArmorMeshSpacedArmorDepth({ - ornamental = false, - node, - ...props -}: ArmorMeshSpacedArmorDepthProps) { - return ( - <> - {!props.include && - !ornamental && - jsxTree(node, { - renderOrder: props.isExternalModule ? 2 : 0, - material: excludeMaterial, - })} - - {props.include && - jsxTree(node, { - renderOrder: 1, - onClick: ornamental ? () => {} : undefined, - userData: { - type: 'spacedArmor', - thickness: props.thickness, - } satisfies ArmorMeshUserData, - material: ornamental - ? includeMaterial - : new ShaderMaterial({ - fragmentShader, - vertexShader, - uniforms: { - thickness: { value: props.thickness }, - maxThickness: { value: props.maxThickness }, - }, - }), - })} - - ); -} diff --git a/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/fragment.glsl b/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/fragment.glsl deleted file mode 100644 index 6fe90ce36..000000000 --- a/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/fragment.glsl +++ /dev/null @@ -1,11 +0,0 @@ -#define PI 3.1415926535897932384626433832795 - -varying vec3 vViewPosition; -varying vec3 vNormal; -uniform float thickness; -uniform float maxThickness; - -void main() { - float angle = acos(dot(vNormal, -normalize(vViewPosition))); - gl_FragColor = vec4(angle / (PI / 2.0), thickness / maxThickness, 0.0, 1.0); -} \ No newline at end of file diff --git a/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/vertex.glsl b/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/vertex.glsl deleted file mode 100644 index d57a82f32..000000000 --- a/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/vertex.glsl +++ /dev/null @@ -1,10 +0,0 @@ -varying vec3 vViewPosition; -varying vec3 vNormal; - -void main() { - vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0); - vViewPosition = vViewPosition4.xyz; - vNormal = normalMatrix * normal; - - gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); -} \ No newline at end of file diff --git a/src/components/ArmorMesh/index.tsx b/src/components/ArmorMesh/index.tsx deleted file mode 100644 index b64eb57f2..000000000 --- a/src/components/ArmorMesh/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from './components/Armor'; -export * from './components/ExternalModuleMask'; From cc4177bf1466f0303cda337717840686f2b1e17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 18:08:50 -0600 Subject: [PATCH 15/32] feat: enhanced armor --- .../Armor/components/CoreArmor/index.tsx | 16 ++++++++-- .../SpacedArmorSubExternal/index.tsx | 29 +++++++++++++++---- .../components/SpacedArmorSubSpaced/index.tsx | 29 +++++++++++++++---- src/core/blitzkrieg/hasEquipment.ts | 16 +++++----- 4 files changed, 71 insertions(+), 19 deletions(-) diff --git a/src/components/Armor/components/CoreArmor/index.tsx b/src/components/Armor/components/CoreArmor/index.tsx index 8ddb62118..5c81454eb 100644 --- a/src/components/Armor/components/CoreArmor/index.tsx +++ b/src/components/Armor/components/CoreArmor/index.tsx @@ -11,9 +11,10 @@ import { degToRad } from 'three/src/math/MathUtils'; import { canSplash } from '../../../../core/blitz/canSplash'; import { isExplosive } from '../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; +import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment'; import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; import { ShellDefinition } from '../../../../core/blitzkrieg/tankDefinitions'; -import { useDuel } from '../../../../stores/duel'; +import { EquipmentMatrix, useDuel } from '../../../../stores/duel'; import { TankopediaPersistent, useTankopediaPersistent, @@ -40,7 +41,7 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { transparent: true, uniforms: { - thickness: { value: thickness }, + thickness: { value: null }, penetration: { value: null }, caliber: { value: null }, ricochet: { value: null }, @@ -82,9 +83,16 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { material.uniforms.opaque.value = visual.opaque || visual.wireframe; material.wireframe = visual.wireframe; } + async function handleEquipmentChange(equipment: EquipmentMatrix) { + const hasEnhancedArmor = await hasEquipment(110, false, equipment); + material.uniforms.thickness.value = hasEnhancedArmor + ? thickness * 1.04 + : thickness; + } handleShellChange(useDuel.getState().antagonist!.shell); handleVisualChange(useTankopediaPersistent.getState().model.visual); + handleEquipmentChange(useDuel.getState().protagonist!.equipment); const unsubscribes = [ useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange), @@ -92,6 +100,10 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { (state) => state.model.visual, handleVisualChange, ), + useDuel.subscribe( + (state) => state.protagonist!.equipment, + handleEquipmentChange, + ), ]; return () => { diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx index ddff80d45..8641cc63f 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx @@ -6,9 +6,10 @@ import { ShaderMaterial, } from 'three'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; +import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment'; import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree'; import { ShellDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions'; -import { useDuel } from '../../../../../../stores/duel'; +import { EquipmentMatrix, useDuel } from '../../../../../../stores/duel'; import fragmentShader from './shaders/fragment.glsl'; import vertexShader from './shaders/vertex.glsl'; @@ -30,20 +31,38 @@ export function SpacedArmorSubExternal({ blending: AdditiveBlending, uniforms: { - thickness: { value: thickness }, + thickness: { value: null }, penetration: { value: null }, }, }); useEffect(() => { - function handleChange(shell: ShellDefinition) { + function handleShellChange(shell: ShellDefinition) { material.uniforms.penetration.value = resolveNearPenetration( shell.penetration, ); } + async function handleEquipmentChange(equipment: EquipmentMatrix) { + const hasEnhancedArmor = await hasEquipment(110, false, equipment); + material.uniforms.thickness.value = hasEnhancedArmor + ? thickness * 1.04 + : thickness; + } - handleChange(useDuel.getState().antagonist!.shell); - return useDuel.subscribe((state) => state.antagonist!.shell, handleChange); + handleShellChange(useDuel.getState().antagonist!.shell); + handleEquipmentChange(useDuel.getState().protagonist!.equipment); + + const unsubscribes = [ + useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange), + useDuel.subscribe( + (state) => state.protagonist!.equipment, + handleEquipmentChange, + ), + ]; + + return () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()); + }; }, []); return ( diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx index dd0906145..ac264e31d 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx @@ -7,9 +7,10 @@ import { } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; +import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment'; import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree'; import { ShellDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions'; -import { useDuel } from '../../../../../../stores/duel'; +import { EquipmentMatrix, useDuel } from '../../../../../../stores/duel'; import fragmentShader from './shaders/fragment.glsl'; import vertexShader from './shaders/vertex.glsl'; @@ -36,7 +37,7 @@ export function SpacedArmorSubSpaced({ blending: AdditiveBlending, uniforms: { - thickness: { value: thickness }, + thickness: { value: null }, penetration: { value: null }, caliber: { value: null }, ricochet: { value: null }, @@ -45,7 +46,7 @@ export function SpacedArmorSubSpaced({ }); useEffect(() => { - function handleChange(shell: ShellDefinition) { + function handleShellChange(shell: ShellDefinition) { material.uniforms.penetration.value = resolveNearPenetration( shell.penetration, ); @@ -55,9 +56,27 @@ export function SpacedArmorSubSpaced({ shell.normalization ?? 0, ); } + async function handleEquipmentChange(equipment: EquipmentMatrix) { + const hasEnhancedArmor = await hasEquipment(110, false, equipment); + material.uniforms.thickness.value = hasEnhancedArmor + ? thickness * 1.04 + : thickness; + } - handleChange(useDuel.getState().antagonist!.shell); - return useDuel.subscribe((state) => state.antagonist!.shell, handleChange); + handleShellChange(useDuel.getState().antagonist!.shell); + handleEquipmentChange(useDuel.getState().protagonist!.equipment); + + const unsubscribes = [ + useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange), + useDuel.subscribe( + (state) => state.protagonist!.equipment, + handleEquipmentChange, + ), + ]; + + return () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()); + }; }, []); return ( diff --git a/src/core/blitzkrieg/hasEquipment.ts b/src/core/blitzkrieg/hasEquipment.ts index 396153b31..d0ddf713f 100644 --- a/src/core/blitzkrieg/hasEquipment.ts +++ b/src/core/blitzkrieg/hasEquipment.ts @@ -1,15 +1,17 @@ import { useDuel } from '../../stores/duel'; import { equipmentDefinitions } from './equipmentDefinitions'; -export async function hasEquipment(id: number, antagonist = false) { +export async function hasEquipment( + id: number, + antagonist = false, + equipmentMatrix = useDuel.getState()[ + antagonist ? 'antagonist' : 'protagonist' + ]!.equipment, +) { const awaitedEquipmentDefinitions = await equipmentDefinitions; const member = useDuel.getState()[antagonist ? 'antagonist' : 'protagonist']!; - const equipmentRows = - awaitedEquipmentDefinitions.presets[member.tank.equipment]; - const equipmentMatrix = - useDuel.getState()[antagonist ? 'antagonist' : 'protagonist']!.equipment; - - return equipmentRows.some((equipmentRow, rowIndex) => { + const presetRows = awaitedEquipmentDefinitions.presets[member.tank.equipment]; + return presetRows.some((equipmentRow, rowIndex) => { return equipmentRow.some((equipment, columnIndex) => { const choice = equipmentMatrix[rowIndex][columnIndex]; if (choice === 0) return false; From d5316988670a4cab503897f3506e5c0a6d718f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 27 Feb 2024 18:13:26 -0600 Subject: [PATCH 16/32] feat: calibrated shells --- .../Armor/components/CoreArmor/index.tsx | 21 +++++++++++++++--- .../SpacedArmorSubExternal/index.tsx | 22 ++++++++++++++++--- .../components/SpacedArmorSubSpaced/index.tsx | 22 ++++++++++++++++--- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/components/Armor/components/CoreArmor/index.tsx b/src/components/Armor/components/CoreArmor/index.tsx index 5c81454eb..28fda6c92 100644 --- a/src/components/Armor/components/CoreArmor/index.tsx +++ b/src/components/Armor/components/CoreArmor/index.tsx @@ -83,16 +83,27 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { material.uniforms.opaque.value = visual.opaque || visual.wireframe; material.wireframe = visual.wireframe; } - async function handleEquipmentChange(equipment: EquipmentMatrix) { + async function handleProtagonistEquipmentChange( + equipment: EquipmentMatrix, + ) { const hasEnhancedArmor = await hasEquipment(110, false, equipment); material.uniforms.thickness.value = hasEnhancedArmor ? thickness * 1.04 : thickness; } + async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { + const shell = useDuel.getState().antagonist!.shell; + const penetration = resolveNearPenetration(shell.penetration); + const hasCalibratedShells = await hasEquipment(103, true, equipment); + material.uniforms.penetration.value = hasCalibratedShells + ? penetration * (isExplosive(shell.type) ? 1.1 : 1.05) + : penetration; + } handleShellChange(useDuel.getState().antagonist!.shell); handleVisualChange(useTankopediaPersistent.getState().model.visual); - handleEquipmentChange(useDuel.getState().protagonist!.equipment); + handleProtagonistEquipmentChange(useDuel.getState().protagonist!.equipment); + handleAntagonistEquipmentChange(useDuel.getState().antagonist!.equipment); const unsubscribes = [ useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange), @@ -102,7 +113,11 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { ), useDuel.subscribe( (state) => state.protagonist!.equipment, - handleEquipmentChange, + handleProtagonistEquipmentChange, + ), + useDuel.subscribe( + (state) => state.antagonist!.equipment, + handleAntagonistEquipmentChange, ), ]; diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx index 8641cc63f..8512f1ae5 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx @@ -5,6 +5,7 @@ import { Object3D, ShaderMaterial, } from 'three'; +import { isExplosive } from '../../../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment'; import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree'; @@ -42,21 +43,36 @@ export function SpacedArmorSubExternal({ shell.penetration, ); } - async function handleEquipmentChange(equipment: EquipmentMatrix) { + async function handleProtagonistEquipmentChange( + equipment: EquipmentMatrix, + ) { const hasEnhancedArmor = await hasEquipment(110, false, equipment); material.uniforms.thickness.value = hasEnhancedArmor ? thickness * 1.04 : thickness; } + async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { + const shell = useDuel.getState().antagonist!.shell; + const penetration = resolveNearPenetration(shell.penetration); + const hasCalibratedShells = await hasEquipment(103, true, equipment); + material.uniforms.penetration.value = hasCalibratedShells + ? penetration * (isExplosive(shell.type) ? 1.1 : 1.05) + : penetration; + } handleShellChange(useDuel.getState().antagonist!.shell); - handleEquipmentChange(useDuel.getState().protagonist!.equipment); + handleProtagonistEquipmentChange(useDuel.getState().protagonist!.equipment); + handleAntagonistEquipmentChange(useDuel.getState().antagonist!.equipment); const unsubscribes = [ useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange), useDuel.subscribe( (state) => state.protagonist!.equipment, - handleEquipmentChange, + handleProtagonistEquipmentChange, + ), + useDuel.subscribe( + (state) => state.antagonist!.equipment, + handleAntagonistEquipmentChange, ), ]; diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx index ac264e31d..2f651ce98 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx +++ b/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx @@ -6,6 +6,7 @@ import { ShaderMaterial, } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; +import { isExplosive } from '../../../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment'; import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree'; @@ -56,21 +57,36 @@ export function SpacedArmorSubSpaced({ shell.normalization ?? 0, ); } - async function handleEquipmentChange(equipment: EquipmentMatrix) { + async function handleProtagonistEquipmentChange( + equipment: EquipmentMatrix, + ) { const hasEnhancedArmor = await hasEquipment(110, false, equipment); material.uniforms.thickness.value = hasEnhancedArmor ? thickness * 1.04 : thickness; } + async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { + const shell = useDuel.getState().antagonist!.shell; + const penetration = resolveNearPenetration(shell.penetration); + const hasCalibratedShells = await hasEquipment(103, true, equipment); + material.uniforms.penetration.value = hasCalibratedShells + ? penetration * (isExplosive(shell.type) ? 1.1 : 1.05) + : penetration; + } handleShellChange(useDuel.getState().antagonist!.shell); - handleEquipmentChange(useDuel.getState().protagonist!.equipment); + handleProtagonistEquipmentChange(useDuel.getState().protagonist!.equipment); + handleAntagonistEquipmentChange(useDuel.getState().antagonist!.equipment); const unsubscribes = [ useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange), useDuel.subscribe( (state) => state.protagonist!.equipment, - handleEquipmentChange, + handleProtagonistEquipmentChange, + ), + useDuel.subscribe( + (state) => state.antagonist!.equipment, + handleAntagonistEquipmentChange, ), ]; From 35788657a8413db5a3ad529b7538250320d89553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 28 Feb 2024 14:59:19 -0600 Subject: [PATCH 17/32] fix: remove default tab --- docs/changelogs/1.16.1.md | 3 +++ src/stores/tankopedia.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 docs/changelogs/1.16.1.md diff --git a/docs/changelogs/1.16.1.md b/docs/changelogs/1.16.1.md new file mode 100644 index 000000000..8e4bf787f --- /dev/null +++ b/docs/changelogs/1.16.1.md @@ -0,0 +1,3 @@ +# Blitzkrieg 1.16.1 + +Rewritten the armor sandbox for a 25%+ performance boost and more accuracy. diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 5b08f2437..206e7451d 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -5,7 +5,6 @@ import { create } from 'zustand'; import { persist, subscribeWithSelector } from 'zustand/middleware'; import { ENVIRONMENTS } from '../app/tools/tankopedia/[id]/components/Lighting'; import { TankType, TreeType } from '../components/Tanks'; -import isDev from '../core/blitzkrieg/isDev'; import { Tier } from '../core/blitzkrieg/tankDefinitions'; export type TankopediaSortBy = keyof typeof SORT_NAMES; @@ -126,7 +125,7 @@ export const useTankopediaPersistent = create()( test: 'include', page: 0, }, - mode: isDev() ? 'armor' : 'model', + mode: 'model', })), { name: 'tankopedia', merge: (a, b) => merge(b, a) }, ), From 69c388f243da69ce6efd5ca5d4287a5307f71979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:41:59 -0600 Subject: [PATCH 18/32] feat: starting work on on-click armor --- docs/changelogs/1.16.1.md | 2 +- .../Armor/components/CoreArmorScene.tsx | 317 +++++++------- .../index.tsx | 9 +- .../shaders/fragment.glsl | 5 +- .../shaders/vertex.glsl | 0 .../Armor/components/SpacedArmor/index.tsx | 44 -- .../Armor/components/SpacedArmorScene.tsx | 397 +++++++++--------- .../SpacedArmorSubExternal/index.tsx | 9 + .../shaders/fragment.glsl | 0 .../shaders/vertex.glsl | 0 .../components/SpacedArmorSubSpaced/index.tsx | 6 + .../shaders/fragment.glsl | 0 .../SpacedArmorSubSpaced/shaders/vertex.glsl | 0 .../SpacedArmorSceneComponent/index.tsx | 212 ++++++++++ src/components/Armor/index.tsx | 2 +- src/components/HeadsUpDisplay.tsx | 53 --- 16 files changed, 585 insertions(+), 471 deletions(-) rename src/components/Armor/components/{CoreArmor => CoreArmorSceneComponent}/index.tsx (96%) rename src/components/Armor/components/{CoreArmor => CoreArmorSceneComponent}/shaders/fragment.glsl (95%) rename src/components/Armor/components/{CoreArmor => CoreArmorSceneComponent}/shaders/vertex.glsl (100%) delete mode 100644 src/components/Armor/components/SpacedArmor/index.tsx rename src/components/Armor/components/{SpacedArmor => SpacedArmorSceneComponent}/components/SpacedArmorSubExternal/index.tsx (92%) rename src/components/Armor/components/{SpacedArmor => SpacedArmorSceneComponent}/components/SpacedArmorSubExternal/shaders/fragment.glsl (100%) rename src/components/Armor/components/{SpacedArmor => SpacedArmorSceneComponent}/components/SpacedArmorSubExternal/shaders/vertex.glsl (100%) rename src/components/Armor/components/{SpacedArmor => SpacedArmorSceneComponent}/components/SpacedArmorSubSpaced/index.tsx (95%) rename src/components/Armor/components/{SpacedArmor => SpacedArmorSceneComponent}/components/SpacedArmorSubSpaced/shaders/fragment.glsl (100%) rename src/components/Armor/components/{SpacedArmor => SpacedArmorSceneComponent}/components/SpacedArmorSubSpaced/shaders/vertex.glsl (100%) create mode 100644 src/components/Armor/components/SpacedArmorSceneComponent/index.tsx delete mode 100644 src/components/HeadsUpDisplay.tsx diff --git a/docs/changelogs/1.16.1.md b/docs/changelogs/1.16.1.md index 8e4bf787f..bb1f0ed7a 100644 --- a/docs/changelogs/1.16.1.md +++ b/docs/changelogs/1.16.1.md @@ -1,3 +1,3 @@ # Blitzkrieg 1.16.1 -Rewritten the armor sandbox for a 25%+ performance boost and more accuracy. +Armor sandbox gains 25% rendering speed and 40% memory efficiency. diff --git a/src/components/Armor/components/CoreArmorScene.tsx b/src/components/Armor/components/CoreArmorScene.tsx index d63d7d7f2..b5b133c3d 100644 --- a/src/components/Armor/components/CoreArmorScene.tsx +++ b/src/components/Armor/components/CoreArmorScene.tsx @@ -12,118 +12,18 @@ import { useArmor } from '../../../hooks/useArmor'; import { useModelDefinitions } from '../../../hooks/useModelDefinitions'; import { useDuel } from '../../../stores/duel'; import { useTankopediaPersistent } from '../../../stores/tankopedia'; -import { CoreArmor } from './CoreArmor'; +import { CoreArmorSceneComponent } from './CoreArmorSceneComponent'; -interface CoreArmorSceneProps { - ornamental?: boolean; -} +export const CoreArmorScene = memo(() => { + const wrapper = useRef(null); + const modelDefinitions = useModelDefinitions(); + const turretContainer = useRef(null); + const gunContainer = useRef(null); + const initialTankopediaState = useTankopediaPersistent.getState(); -export const CoreArmorScene = memo( - ({ ornamental = false }) => { - const wrapper = useRef(null); - const modelDefinitions = useModelDefinitions(); - const turretContainer = useRef(null); - const gunContainer = useRef(null); - const initialTankopediaState = useTankopediaPersistent.getState(); + useEffect(() => { + if (!modelDefinitions) return; - useEffect(() => { - if (!modelDefinitions) return; - - 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 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(hullOrigin) - .sub(turretOrigin) - .sub(gunOrigin) - .applyAxisAngle(I_HAT, pitch) - .add(gunOrigin) - .add(turretOrigin) - .add(hullOrigin); - 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(hullOrigin) - .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(I_HAT, initialPitch) - .applyAxisAngle(J_HAT, initialRoll) - .applyAxisAngle(K_HAT, initialYaw); - turretRotation.x += initialPitch; - turretRotation.y += initialRoll; - turretRotation.z += initialYaw; - } - - turretPosition.add(turretOrigin).add(hullOrigin); - turretContainer.current?.position.copy(turretPosition); - turretContainer.current?.rotation.copy(turretRotation); - } - - handleModelTransform(useDuel.getState().protagonist!); - modelTransformEvent.on(handleModelTransform); - - return () => { - modelTransformEvent.off(handleModelTransform); - }; - }); - - useEffect(() => { - const unsubscribe = useTankopediaPersistent.subscribe( - (state) => state.mode, - (mode) => { - if (wrapper.current) wrapper.current.visible = mode === 'armor'; - }, - ); - - return unsubscribe; - }); - - const tank = useDuel((state) => state.protagonist!.tank); - const track = useDuel((state) => state.protagonist!.track); - const turret = useDuel((state) => state.protagonist!.turret); - const gun = useDuel((state) => state.protagonist!.gun); - const armorGltf = useArmor(tank.id); - const armorNodes = Object.values(armorGltf.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], @@ -139,86 +39,181 @@ export const CoreArmorScene = memo( turretModelDefinition.gunOrigin[1], -turretModelDefinition.gunOrigin[2], ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretPosition = new Vector3(); + const turretRotation = new Euler(); + const gunPosition = new Vector3(); + const gunRotation = new Euler(); - return ( - + function handleModelTransform({ yaw, pitch }: ModelTransformEventData) { + gunPosition + .set(0, 0, 0) + .sub(hullOrigin) + .sub(turretOrigin) + .sub(gunOrigin) + .applyAxisAngle(I_HAT, pitch) + .add(gunOrigin) + .add(turretOrigin) + .add(hullOrigin); + 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(hullOrigin) + .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(I_HAT, initialPitch) + .applyAxisAngle(J_HAT, initialRoll) + .applyAxisAngle(K_HAT, initialYaw); + turretRotation.x += initialPitch; + turretRotation.y += initialRoll; + turretRotation.z += initialYaw; + } + + turretPosition.add(turretOrigin).add(hullOrigin); + turretContainer.current?.position.copy(turretPosition); + turretContainer.current?.rotation.copy(turretRotation); + } + + handleModelTransform(useDuel.getState().protagonist!); + modelTransformEvent.on(handleModelTransform); + + return () => { + modelTransformEvent.off(handleModelTransform); + }; + }); + + useEffect(() => { + const unsubscribe = useTankopediaPersistent.subscribe( + (state) => state.mode, + (mode) => { + if (wrapper.current) wrapper.current.visible = mode === 'armor'; + }, + ); + + return unsubscribe; + }); + + const tank = useDuel((state) => state.protagonist!.tank); + const track = useDuel((state) => state.protagonist!.track); + const turret = useDuel((state) => state.protagonist!.turret); + const gun = useDuel((state) => state.protagonist!.gun); + const armorGltf = useArmor(tank.id); + const armorNodes = Object.values(armorGltf.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); + + 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 || spaced || thickness === undefined) 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, ); if (!isVisible || spaced || thickness === undefined) 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, ); if (!isVisible || spaced || thickness === undefined) 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 || spaced || thickness === undefined) - return null; - - return ( - - - - ); - })} - - - ); - }, -); + + ); +}); diff --git a/src/components/Armor/components/CoreArmor/index.tsx b/src/components/Armor/components/CoreArmorSceneComponent/index.tsx similarity index 96% rename from src/components/Armor/components/CoreArmor/index.tsx rename to src/components/Armor/components/CoreArmorSceneComponent/index.tsx index 28fda6c92..c646ee157 100644 --- a/src/components/Armor/components/CoreArmor/index.tsx +++ b/src/components/Armor/components/CoreArmorSceneComponent/index.tsx @@ -24,7 +24,7 @@ import vertexShader from './shaders/vertex.glsl'; export const spacedArmorRenderTarget = new WebGLRenderTarget(); -interface CoreArmorProps { +interface CoreArmorSceneComponentProps { thickness: number; node: Object3D; } @@ -33,7 +33,10 @@ const excludeMaterial = new MeshBasicMaterial({ colorWrite: false, }); -export function CoreArmor({ node, thickness }: CoreArmorProps) { +export function CoreArmorSceneComponent({ + node, + thickness, +}: CoreArmorSceneComponentProps) { const material = new ShaderMaterial({ fragmentShader, vertexShader, @@ -51,7 +54,6 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { damage: { value: null }, explosionRadius: { value: null }, greenPenetration: { value: null }, - wireframe: { value: null }, opaque: { value: null }, resolution: { value: new Vector2() }, @@ -79,7 +81,6 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) { visual: TankopediaPersistent['model']['visual'], ) { material.uniforms.greenPenetration.value = visual.greenPenetration; - material.uniforms.wireframe.value = visual.wireframe; material.uniforms.opaque.value = visual.opaque || visual.wireframe; material.wireframe = visual.wireframe; } diff --git a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl b/src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl similarity index 95% rename from src/components/Armor/components/CoreArmor/shaders/fragment.glsl rename to src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl index b5ad53518..7bd9312f1 100644 --- a/src/components/Armor/components/CoreArmor/shaders/fragment.glsl +++ b/src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl @@ -13,7 +13,6 @@ uniform bool canSplash; uniform float damage; uniform float explosionRadius; uniform bool greenPenetration; -uniform bool wireframe; uniform bool opaque; uniform highp sampler2D spacedArmorBuffer; @@ -35,8 +34,8 @@ void main() { vec4 spacedArmorBufferFragment = texture2D(spacedArmorBuffer, screenCoordinates); bool isUnderSpacedArmor = spacedArmorBufferFragment.a != 0.0; - bool threeCalibersRule = caliber > thickness * 3.0; - if (!threeCalibersRule && !isUnderSpacedArmor && angle >= ricochet) { + bool threeCalibersRule = caliber > thickness * 3.0 || isUnderSpacedArmor; + if (!threeCalibersRule && angle >= ricochet) { penetrationChance = 0.0; } else { bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0; diff --git a/src/components/Armor/components/CoreArmor/shaders/vertex.glsl b/src/components/Armor/components/CoreArmorSceneComponent/shaders/vertex.glsl similarity index 100% rename from src/components/Armor/components/CoreArmor/shaders/vertex.glsl rename to src/components/Armor/components/CoreArmorSceneComponent/shaders/vertex.glsl diff --git a/src/components/Armor/components/SpacedArmor/index.tsx b/src/components/Armor/components/SpacedArmor/index.tsx deleted file mode 100644 index 61be3770d..000000000 --- a/src/components/Armor/components/SpacedArmor/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { MeshBasicMaterial, Object3D } from 'three'; -import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; -import { ArmorType } from '../SpacedArmorScene'; -import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal'; -import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced'; -interface SpacedArmorProps { - node: Object3D; - ornamental?: boolean; - type: ArmorType; - thickness: number; -} - -const omitMaterial = new MeshBasicMaterial({ - colorWrite: false, - depthWrite: true, -}); - -export function SpacedArmor({ node, type, thickness }: SpacedArmorProps) { - /** - * Render orders allocations: - * - 0 : core omission - * - 1-2: spaced without depth buffer write but do test to allow additive blending - * - 3-4: external with depth buffer write in first pass and read in second to simplify geometry - * - 5 : spaced depth write - */ - - return ( - <> - {type === ArmorType.Core && - jsxTree(node, { - renderOrder: 0, - material: omitMaterial, - })} - - {type === ArmorType.Spaced && ( - - )} - - {type === ArmorType.External && ( - - )} - - ); -} diff --git a/src/components/Armor/components/SpacedArmorScene.tsx b/src/components/Armor/components/SpacedArmorScene.tsx index 781dd802f..99af51133 100644 --- a/src/components/Armor/components/SpacedArmorScene.tsx +++ b/src/components/Armor/components/SpacedArmorScene.tsx @@ -13,7 +13,7 @@ import { useModel } from '../../../hooks/useModel'; import { useModelDefinitions } from '../../../hooks/useModelDefinitions'; import { useDuel } from '../../../stores/duel'; import { useTankopediaPersistent } from '../../../stores/tankopedia'; -import { SpacedArmor } from './SpacedArmor'; +import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent'; export enum ArmorType { Core, @@ -21,118 +21,16 @@ export enum ArmorType { External, } -interface SpacedArmorSceneProps { - ornamental?: boolean; -} +export const SpacedArmorScene = memo(() => { + const wrapper = useRef(null); + const modelDefinitions = useModelDefinitions(); + const turretContainer = useRef(null); + const gunContainer = useRef(null); + const initialTankopediaState = useTankopediaPersistent.getState(); -export const SpacedArmorScene = memo( - ({ ornamental = false }) => { - const wrapper = useRef(null); - const modelDefinitions = useModelDefinitions(); - const turretContainer = useRef(null); - const gunContainer = useRef(null); - const initialTankopediaState = useTankopediaPersistent.getState(); + useEffect(() => { + if (!modelDefinitions) return; - useEffect(() => { - if (!modelDefinitions) return; - - 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 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(hullOrigin) - .sub(turretOrigin) - .sub(gunOrigin) - .applyAxisAngle(I_HAT, pitch) - .add(gunOrigin) - .add(turretOrigin) - .add(hullOrigin); - 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(hullOrigin) - .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(I_HAT, initialPitch) - .applyAxisAngle(J_HAT, initialRoll) - .applyAxisAngle(K_HAT, initialYaw); - turretRotation.x += initialPitch; - turretRotation.y += initialRoll; - turretRotation.z += initialYaw; - } - - turretPosition.add(turretOrigin).add(hullOrigin); - turretContainer.current?.position.copy(turretPosition); - turretContainer.current?.rotation.copy(turretRotation); - } - - handleModelTransform(useDuel.getState().protagonist!); - modelTransformEvent.on(handleModelTransform); - - return () => { - modelTransformEvent.off(handleModelTransform); - }; - }); - - useEffect(() => { - const unsubscribe = useTankopediaPersistent.subscribe( - (state) => state.mode, - (mode) => { - if (wrapper.current) wrapper.current.visible = mode === 'armor'; - }, - ); - - return unsubscribe; - }); - - const tank = useDuel((state) => state.protagonist!.tank); - const track = useDuel((state) => state.protagonist!.track); - const turret = useDuel((state) => state.protagonist!.turret); - const gun = useDuel((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], @@ -148,74 +46,196 @@ export const SpacedArmorScene = memo( turretModelDefinition.gunOrigin[1], -turretModelDefinition.gunOrigin[2], ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretPosition = new Vector3(); + const turretRotation = new Euler(); + const gunPosition = new Vector3(); + const gunRotation = new Euler(); - return ( - + function handleModelTransform({ yaw, pitch }: ModelTransformEventData) { + gunPosition + .set(0, 0, 0) + .sub(hullOrigin) + .sub(turretOrigin) + .sub(gunOrigin) + .applyAxisAngle(I_HAT, pitch) + .add(gunOrigin) + .add(turretOrigin) + .add(hullOrigin); + 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(hullOrigin) + .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(I_HAT, initialPitch) + .applyAxisAngle(J_HAT, initialRoll) + .applyAxisAngle(K_HAT, initialYaw); + turretRotation.x += initialPitch; + turretRotation.y += initialRoll; + turretRotation.z += initialYaw; + } + + turretPosition.add(turretOrigin).add(hullOrigin); + turretContainer.current?.position.copy(turretPosition); + turretContainer.current?.rotation.copy(turretRotation); + } + + handleModelTransform(useDuel.getState().protagonist!); + modelTransformEvent.on(handleModelTransform); + + return () => { + modelTransformEvent.off(handleModelTransform); + }; + }); + + useEffect(() => { + const unsubscribe = useTankopediaPersistent.subscribe( + (state) => state.mode, + (mode) => { + if (wrapper.current) wrapper.current.visible = mode === 'armor'; + }, + ); + + return unsubscribe; + }); + + const tank = useDuel((state) => state.protagonist!.tank); + const track = useDuel((state) => state.protagonist!.track); + const turret = useDuel((state) => state.protagonist!.turret); + const gun = useDuel((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); + + 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) 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) 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, ); if (!isVisible || thickness === undefined) 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) 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, ); if (!isVisible || thickness === undefined) 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) return null; + {modelNodes.map((node) => { + const isCurrentGun = + node.name === + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; + const isVisible = isCurrentGun; - return ( - - - - ); - })} - + if (!isVisible) return null; - {modelNodes.map((node) => { - const isCurrentGun = - node.name === - `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; - const isVisible = isCurrentGun; - - if (!isVisible) return null; - - return ( - - ); - })} - + return ( + + ); + })} - ); - }, -); + + ); +}); diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx similarity index 92% rename from src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx rename to src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx index 8512f1ae5..addd42ef7 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx @@ -5,6 +5,7 @@ import { Object3D, ShaderMaterial, } from 'three'; +import { ArmorUserData, ExternalModuleVariant } from '../..'; import { isExplosive } from '../../../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment'; @@ -17,11 +18,13 @@ import vertexShader from './shaders/vertex.glsl'; interface SpacedArmorSubExternalProps { node: Object3D; thickness: number; + variant: ExternalModuleVariant; } export function SpacedArmorSubExternal({ node, thickness, + variant, }: SpacedArmorSubExternalProps) { const material = new ShaderMaterial({ fragmentShader, @@ -90,6 +93,12 @@ export function SpacedArmorSubExternal({ depthTest: true, depthWrite: true, }), + onClick() {}, + userData: { + type: 'external', + thickness, + variant, + } satisfies ArmorUserData, })} {jsxTree(node, { renderOrder: 4, diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/shaders/fragment.glsl b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/shaders/fragment.glsl similarity index 100% rename from src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/shaders/fragment.glsl rename to src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/shaders/fragment.glsl diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/shaders/vertex.glsl b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/shaders/vertex.glsl similarity index 100% rename from src/components/Armor/components/SpacedArmor/components/SpacedArmorSubExternal/shaders/vertex.glsl rename to src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/shaders/vertex.glsl diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx similarity index 95% rename from src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx rename to src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx index 2f651ce98..175f9f208 100644 --- a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx @@ -6,6 +6,7 @@ import { ShaderMaterial, } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; +import { ArmorUserData } from '../..'; import { isExplosive } from '../../../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment'; @@ -100,6 +101,11 @@ export function SpacedArmorSubSpaced({ {jsxTree(node, { renderOrder: 2, material, + onClick() {}, + userData: { + type: 'spaced', + thickness, + } satisfies ArmorUserData, })} {jsxTree(node, { renderOrder: 5, diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/shaders/fragment.glsl b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/shaders/fragment.glsl similarity index 100% rename from src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/shaders/fragment.glsl rename to src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/shaders/fragment.glsl diff --git a/src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/shaders/vertex.glsl b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/shaders/vertex.glsl similarity index 100% rename from src/components/Armor/components/SpacedArmor/components/SpacedArmorSubSpaced/shaders/vertex.glsl rename to src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/shaders/vertex.glsl diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx new file mode 100644 index 000000000..df19938db --- /dev/null +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -0,0 +1,212 @@ +import { useThree } from '@react-three/fiber'; +import { Intersection, MeshBasicMaterial, Object3D, Vector3 } from 'three'; +import { degToRad } from 'three/src/math/MathUtils'; +import { I_HAT } from '../../../../constants/axis'; +import { isExplosive } from '../../../../core/blitz/isExplosive'; +import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; +import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment'; +import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; +import { useDuel } from '../../../../stores/duel'; +import { ArmorType } from '../SpacedArmorScene'; +import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal'; +import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced'; + +type SpacedArmorSceneComponentProps = { + node: Object3D; + thickness: number; +} & ( + | { + type: Exclude; + } + | { + type: ArmorType.External; + variant: ExternalModuleVariant; + } +); + +export type ExternalModuleVariant = 'gun' | 'track'; +export type ArmorUserData = { + thickness: number; +} & ( + | { + type: 'core' | 'spaced'; + } + | { + type: 'external'; + variant: ExternalModuleVariant; + } +); +type ArmorMeshIntersection = Intersection & { + eventObject: Object3D; + object: Object3D & { userData: ArmorUserData }; +}; + +const omitMaterial = new MeshBasicMaterial({ + colorWrite: false, + depthWrite: true, +}); + +/** + * Render orders allocations: + * - 0 : core omission + * - 1-2: spaced without depth buffer write but do test to allow additive blending + * - 3-4: external with depth buffer write in first pass and read in second to simplify geometry + * - 5 : spaced depth write + */ +export function SpacedArmorSceneComponent({ + node, + thickness, + ...props +}: SpacedArmorSceneComponentProps) { + const camera = useThree((state) => state.camera); + const cameraNormal = new Vector3(); + + return ( + <> + {props.type === ArmorType.Core && + jsxTree(node, { + renderOrder: 0, + material: omitMaterial, + userData: { + type: 'core', + thickness, + } satisfies ArmorUserData, + + async onClick(event) { + event.stopPropagation(); + + const intersectionsAll = event.intersections.filter( + ({ object }) => object.userData.type !== undefined, + ) as ArmorMeshIntersection[]; + const intersectionsWithDuplicates = intersectionsAll.slice( + 0, + intersectionsAll.findIndex( + ({ object }) => object.userData.type === 'core', + ), + ); + const firstGunIntersectionIndex = + intersectionsWithDuplicates.findIndex( + ({ object }) => + object.userData.type === 'external' && + object.userData.variant === 'gun', + ); + const firstTrackIntersectionIndex = + intersectionsWithDuplicates.findIndex( + ({ object }) => + object.userData.type === 'external' && + object.userData.variant === 'track', + ); + let intersections = intersectionsWithDuplicates; + + if (firstGunIntersectionIndex !== -1) { + intersections = intersections.filter(({ object }, index) => { + return object.userData.type === 'external' && + object.userData.variant === 'gun' + ? index === firstGunIntersectionIndex + : true; + }); + } + if (firstTrackIntersectionIndex !== -1) { + intersections = intersections.filter(({ object }, index) => { + return object.userData.type === 'external' && + object.userData.variant === 'track' + ? index === firstTrackIntersectionIndex + : true; + }); + } + + const hasCalibratedShells = await hasEquipment(103, true); + const hasEnhancedArmor = await hasEquipment(110); + const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1; + const shell = useDuel.getState().antagonist!.shell; + const penetration = + resolveNearPenetration(shell.penetration) * + (hasCalibratedShells + ? isExplosive(shell.type) + ? 1.1 + : 1.05 + : 1); + const shotLog: ({ + thickness: number; + status: 'blocked' | 'penetration' | 'ricochet'; + } & ( + | { + type: ArmorType.External; + } + | { + type: Exclude; + thicknessAngled: number; + } + ))[] = []; // TODO: type this + let remainingPenetration = penetration; + let penetrationChance = -1; + let splashChance = -1; + + camera.getWorldDirection(cameraNormal).multiplyScalar(-1); + + let index = 0; + for (const intersection of intersections) { + const thickness = + thicknessCoefficient * intersection.object.userData.thickness; + + if (intersection.object.userData.type === 'external') { + remainingPenetration -= thickness; + const blocked = remainingPenetration < 0; + + shotLog.push({ + type: ArmorType.External, + thickness: intersection.object.userData.thickness, + status: blocked ? 'blocked' : 'penetration', + }); + + if (blocked) break; + } else if (intersection.object.userData.type === 'spaced') { + const normal = intersection.normal!.applyAxisAngle( + I_HAT, + -Math.PI / 2, + ); + const angle = normal.angleTo(cameraNormal); + const ricochet = degToRad(shell.ricochet ?? 90); + const normalization = degToRad(shell.normalization ?? 0); + const threeCalibersRule = + shell.caliber > thickness || index > 0; + const twoCalibersRule = shell.caliber > thickness * 2; + const finalNormalization = twoCalibersRule + ? (1.4 * normalization * shell.caliber) / (2.0 * thickness) + : normalization; + const finalThickness = + thickness / Math.cos(angle - finalNormalization); + remainingPenetration -= finalThickness; + + if (!threeCalibersRule && angle >= ricochet) { + shotLog.push({ + type: ArmorType.Spaced, + thickness, + thicknessAngled: finalThickness, + status: 'ricochet', + }); + continue; + } + } else throw new Error('Core should be omitted'); + + index++; + } + + console.log(JSON.stringify(shotLog, null, 2)); + }, + })} + + {props.type === ArmorType.Spaced && ( + + )} + + {props.type === ArmorType.External && ( + + )} + + ); +} diff --git a/src/components/Armor/index.tsx b/src/components/Armor/index.tsx index 757a9ce3f..909c3e0fd 100644 --- a/src/components/Armor/index.tsx +++ b/src/components/Armor/index.tsx @@ -1,8 +1,8 @@ import { createPortal, useFrame } from '@react-three/fiber'; import { memo } from 'react'; import { DepthTexture, Scene, Vector2 } from 'three'; -import { spacedArmorRenderTarget } from './components/CoreArmor'; import { CoreArmorScene } from './components/CoreArmorScene'; +import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent'; import { SpacedArmorScene } from './components/SpacedArmorScene'; export const Armor = memo(() => { diff --git a/src/components/HeadsUpDisplay.tsx b/src/components/HeadsUpDisplay.tsx deleted file mode 100644 index b64759a1f..000000000 --- a/src/components/HeadsUpDisplay.tsx +++ /dev/null @@ -1,53 +0,0 @@ -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; -} From f55313e0cd3c436eca184022d9688cf988f49404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:29:21 -0600 Subject: [PATCH 19/32] feat: shot display --- .../[id]/components/Model/TankSandbox.tsx | 3 +- src/components/Armor/components/ShotScene.tsx | 88 ++++++ .../Armor/components/SpacedArmorScene.tsx | 13 +- .../SpacedArmorSubExternal/index.tsx | 3 +- .../components/SpacedArmorSubSpaced/index.tsx | 3 +- .../SpacedArmorSceneComponent/index.tsx | 291 +++++++++++------- src/components/Armor/index.tsx | 15 +- src/components/ShotDisplay.tsx | 226 -------------- src/constants/axis.ts | 1 + src/stores/tankopedia.ts | 41 ++- 10 files changed, 333 insertions(+), 351 deletions(-) create mode 100644 src/components/Armor/components/ShotScene.tsx delete mode 100644 src/components/ShotDisplay.tsx diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx index 5f24b01eb..ed1d98ae0 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -5,6 +5,7 @@ import { Canvas } from '@react-three/fiber'; import { Suspense, use, useEffect, useRef, useState } from 'react'; import { isMobile } from 'react-device-detect'; import { Armor } from '../../../../../../components/Armor'; +import { ShotScene } from '../../../../../../components/Armor/components/ShotScene'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; @@ -195,7 +196,7 @@ export function TankSandbox() { }> - {/* */} + {/* TODO: add rotation cube to the top right */} {/* */} diff --git a/src/components/Armor/components/ShotScene.tsx b/src/components/Armor/components/ShotScene.tsx new file mode 100644 index 000000000..9fa423a3c --- /dev/null +++ b/src/components/Armor/components/ShotScene.tsx @@ -0,0 +1,88 @@ +import { Text } from '@radix-ui/themes'; +import { Html } from '@react-three/drei'; +import { DoubleSide, Euler, Quaternion, Vector3 } from 'three'; +import { J_HAT } from '../../../constants/axis'; +import { + ShotLayerGap, + useTankopediaTemporary, +} from '../../../stores/tankopedia'; + +// export const SHOT_NAMES: Record = { +// ricochet: 'Ricochet', +// penetration: 'Penetration', +// block: 'Blocked', +// }; + +// export const SHOT_ICONS: Record = { +// ricochet: , +// block: , +// penetration: , +// }; + +// const THICKNESS = 0.05; +// const LENGTH = 4; + +export function ShotScene() { + const shot = useTankopediaTemporary((state) => state.shot); + + return ( + <> + {shot?.map((layer, index) => { + if (layer.type === null) return; + + const shellRotation = new Euler().setFromQuaternion( + new Quaternion().setFromAxisAngle( + new Vector3().crossVectors(J_HAT, layer.shellNormal).normalize(), + J_HAT.angleTo(layer.shellNormal), + ), + ); + + return ( + <> + + + + + + + {shot.length > 1 && ( + + {layer.index + 1} + + )} + + {(() => { + let length = 4; + + if (layer.index !== 0) { + const gap = shot[index - 1] as ShotLayerGap; + length = gap.distance; + } + + return ( + + + + + ); + })()} + + + ); + })} + + ); +} diff --git a/src/components/Armor/components/SpacedArmorScene.tsx b/src/components/Armor/components/SpacedArmorScene.tsx index 99af51133..56337dfe9 100644 --- a/src/components/Armor/components/SpacedArmorScene.tsx +++ b/src/components/Armor/components/SpacedArmorScene.tsx @@ -1,5 +1,5 @@ import { memo, useEffect, useRef } from 'react'; -import { Euler, Group, Vector3 } from 'three'; +import { Euler, Group, Scene, Vector3 } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; import { I_HAT, J_HAT, K_HAT } from '../../../constants/axis'; import { @@ -21,7 +21,11 @@ export enum ArmorType { External, } -export const SpacedArmorScene = memo(() => { +interface SpacedArmorSceneProps { + scene: Scene; +} + +export const SpacedArmorScene = memo(({ scene }) => { const wrapper = useRef(null); const modelDefinitions = useModelDefinitions(); const turretContainer = useRef(null); @@ -162,6 +166,7 @@ export const SpacedArmorScene = memo(() => { return ( { return ( { return ( { position={turretOrigin.clone().add(gunOrigin)} > { return ( ; @@ -29,15 +38,14 @@ export type ArmorUserData = { thickness: number; } & ( | { - type: 'core' | 'spaced'; + type: Exclude; } | { - type: 'external'; + type: ArmorType.External; variant: ExternalModuleVariant; } ); type ArmorMeshIntersection = Intersection & { - eventObject: Object3D; object: Object3D & { userData: ArmorUserData }; }; @@ -56,6 +64,7 @@ const omitMaterial = new MeshBasicMaterial({ export function SpacedArmorSceneComponent({ node, thickness, + scene, ...props }: SpacedArmorSceneComponentProps) { const camera = useThree((state) => state.camera); @@ -68,53 +77,13 @@ export function SpacedArmorSceneComponent({ renderOrder: 0, material: omitMaterial, userData: { - type: 'core', + type: ArmorType.Core, thickness, } satisfies ArmorUserData, async onClick(event) { event.stopPropagation(); - const intersectionsAll = event.intersections.filter( - ({ object }) => object.userData.type !== undefined, - ) as ArmorMeshIntersection[]; - const intersectionsWithDuplicates = intersectionsAll.slice( - 0, - intersectionsAll.findIndex( - ({ object }) => object.userData.type === 'core', - ), - ); - const firstGunIntersectionIndex = - intersectionsWithDuplicates.findIndex( - ({ object }) => - object.userData.type === 'external' && - object.userData.variant === 'gun', - ); - const firstTrackIntersectionIndex = - intersectionsWithDuplicates.findIndex( - ({ object }) => - object.userData.type === 'external' && - object.userData.variant === 'track', - ); - let intersections = intersectionsWithDuplicates; - - if (firstGunIntersectionIndex !== -1) { - intersections = intersections.filter(({ object }, index) => { - return object.userData.type === 'external' && - object.userData.variant === 'gun' - ? index === firstGunIntersectionIndex - : true; - }); - } - if (firstTrackIntersectionIndex !== -1) { - intersections = intersections.filter(({ object }, index) => { - return object.userData.type === 'external' && - object.userData.variant === 'track' - ? index === firstTrackIntersectionIndex - : true; - }); - } - const hasCalibratedShells = await hasEquipment(103, true); const hasEnhancedArmor = await hasEquipment(110); const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1; @@ -126,73 +95,189 @@ export function SpacedArmorSceneComponent({ ? 1.1 : 1.05 : 1); - const shotLog: ({ - thickness: number; - status: 'blocked' | 'penetration' | 'ricochet'; - } & ( - | { - type: ArmorType.External; - } - | { - type: Exclude; - thicknessAngled: number; - } - ))[] = []; // TODO: type this + const shot: Shot = []; // TODO: type this let remainingPenetration = penetration; let penetrationChance = -1; let splashChance = -1; - camera.getWorldDirection(cameraNormal).multiplyScalar(-1); + const shellNormal = camera.position + .clone() + .sub(event.point) + .normalize(); + shellNormal + .copy(camera.getWorldDirection(cameraNormal)) + .multiplyScalar(-1); + pushLogs( + shellNormal, + filterIntersections(event.intersections), + true, + ); - let index = 0; - for (const intersection of intersections) { - const thickness = - thicknessCoefficient * intersection.object.userData.thickness; + console.clear(); + console.log( + JSON.stringify( + shot.map((i) => ({ + ...i, + ...(i.angle ? { angle: radToDeg(i.angle) } : {}), + })), + null, + 2, + ), + ); - if (intersection.object.userData.type === 'external') { - remainingPenetration -= thickness; - const blocked = remainingPenetration < 0; - - shotLog.push({ - type: ArmorType.External, - thickness: intersection.object.userData.thickness, - status: blocked ? 'blocked' : 'penetration', - }); - - if (blocked) break; - } else if (intersection.object.userData.type === 'spaced') { - const normal = intersection.normal!.applyAxisAngle( - I_HAT, - -Math.PI / 2, + function filterIntersections(intersectionsRaw: Intersection[]) { + const intersectionsAll = intersectionsRaw.filter( + ({ object }) => object.userData.type !== undefined, + ) as ArmorMeshIntersection[]; + const intersectionsWithDuplicates = intersectionsAll.slice( + 0, + intersectionsAll.findIndex( + ({ object }) => object.userData.type === ArmorType.Core, + ) + 1, + ); + const firstGunIntersectionIndex = + intersectionsWithDuplicates.findIndex( + ({ object }) => + object.userData.type === ArmorType.External && + object.userData.variant === 'gun', ); - const angle = normal.angleTo(cameraNormal); - const ricochet = degToRad(shell.ricochet ?? 90); - const normalization = degToRad(shell.normalization ?? 0); - const threeCalibersRule = - shell.caliber > thickness || index > 0; - const twoCalibersRule = shell.caliber > thickness * 2; - const finalNormalization = twoCalibersRule - ? (1.4 * normalization * shell.caliber) / (2.0 * thickness) - : normalization; - const finalThickness = - thickness / Math.cos(angle - finalNormalization); - remainingPenetration -= finalThickness; + const firstTrackIntersectionIndex = + intersectionsWithDuplicates.findIndex( + ({ object }) => + object.userData.type === ArmorType.External && + object.userData.variant === 'track', + ); + let intersections = intersectionsWithDuplicates; - if (!threeCalibersRule && angle >= ricochet) { - shotLog.push({ - type: ArmorType.Spaced, - thickness, - thicknessAngled: finalThickness, - status: 'ricochet', - }); - continue; - } - } else throw new Error('Core should be omitted'); + if (firstGunIntersectionIndex !== -1) { + intersections = intersections.filter(({ object }, index) => { + return object.userData.type === ArmorType.External && + object.userData.variant === 'gun' + ? index === firstGunIntersectionIndex + : true; + }); + } + if (firstTrackIntersectionIndex !== -1) { + intersections = intersections.filter(({ object }, index) => { + return object.userData.type === ArmorType.External && + object.userData.variant === 'track' + ? index === firstTrackIntersectionIndex + : true; + }); + } - index++; + return intersections; } - console.log(JSON.stringify(shotLog, null, 2)); + function pushLogs( + shellNormal: Vector3, + intersections: ArmorMeshIntersection[], + allowRicochets: boolean, + ) { + let index = 0; + for (const intersection of intersections) { + const surfaceNormal = intersection + .normal!.clone() + .applyQuaternion( + event.object.getWorldQuaternion(new Quaternion()), + ); + const thickness = + thicknessCoefficient * intersection.object.userData.thickness; + + if (index !== 0) { + const previousIntersection = intersections[index - 1]; + shot.push({ + type: null, + distance: + intersection.distance - previousIntersection.distance, + }); + } + + if (intersection.object.userData.type === ArmorType.External) { + remainingPenetration -= thickness; + const blocked = remainingPenetration < 0; + + shot.push({ + type: ArmorType.External, + index, + shellNormal, + surfaceNormal, + point: intersection.point, + thickness: intersection.object.userData.thickness, + status: blocked ? 'blocked' : 'penetration', + }); + + if (blocked) break; + } else { + const type = intersection.object.userData.type; + shellNormal.copy( + cameraNormal + .copy(camera.position) + .sub(event.point) + .normalize(), + ); + const angle = surfaceNormal.angleTo(shellNormal); + const ricochet = degToRad(shell.ricochet ?? 90); + const normalization = degToRad(shell.normalization ?? 0); + const threeCalibersRule = + shell.caliber > thickness * 3 || + index > 0 || + !allowRicochets; + const twoCalibersRule = shell.caliber > thickness * 2; + const finalNormalization = twoCalibersRule + ? (1.4 * normalization * shell.caliber) / (2.0 * thickness) + : normalization; + const finalThickness = + thickness / Math.cos(angle - finalNormalization); + remainingPenetration -= finalThickness; + + if (!threeCalibersRule && angle >= ricochet) { + shot.push({ + type, + index, + shellNormal, + surfaceNormal, + point: intersection.point, + thickness, + thicknessAngled: finalThickness, + angle, + status: 'ricochet', + }); + + const raycaster = new Raycaster(); + const newShellNormal = cameraNormal + .clone() + .reflect(surfaceNormal); + + raycaster.set(intersection.point, newShellNormal); + pushLogs( + newShellNormal, + raycaster.intersectObjects(scene.children, true), + false, + ); + + break; + } else { + const blocked = remainingPenetration < 0; + shot.push({ + type, + index, + shellNormal, + surfaceNormal, + point: intersection.point, + thickness, + thicknessAngled: finalThickness, + angle, + status: blocked ? 'blocked' : 'penetration', + }); + } + } + + index++; + } + } + + useTankopediaTemporary.setState({ shot }); }, })} diff --git a/src/components/Armor/index.tsx b/src/components/Armor/index.tsx index 909c3e0fd..c3a889805 100644 --- a/src/components/Armor/index.tsx +++ b/src/components/Armor/index.tsx @@ -1,18 +1,21 @@ import { createPortal, useFrame } from '@react-three/fiber'; -import { memo } from 'react'; +import { memo, useState } from 'react'; import { DepthTexture, Scene, Vector2 } from 'three'; import { CoreArmorScene } from './components/CoreArmorScene'; import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent'; +import { ShotScene } from './components/ShotScene'; import { SpacedArmorScene } from './components/SpacedArmorScene'; export const Armor = memo(() => { - const spacedArmorScene = new Scene(); - const coreArmorScene = new Scene(); + const [spacedArmorScene] = useState(() => new Scene()); + const [coreArmorScene] = useState(() => new Scene()); + const [shotScene] = useState(() => new Scene()); const spacedArmorPortal = createPortal( - , + , spacedArmorScene, ); const coreArmorPortal = createPortal(, coreArmorScene); + const shotPortal = createPortal(, shotScene); const renderSize = new Vector2(); const newRenderSize = new Vector2(); @@ -41,12 +44,16 @@ export const Armor = memo(() => { gl.clearDepth(); gl.render(coreArmorScene, camera); + + gl.clearDepth(); + gl.render(shotScene, camera); }, 1); return ( <> {spacedArmorPortal} {coreArmorPortal} + {shotPortal} ); }); diff --git a/src/components/ShotDisplay.tsx b/src/components/ShotDisplay.tsx deleted file mode 100644 index 2e2c29a05..000000000 --- a/src/components/ShotDisplay.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { Card, Flex, Text } from '@radix-ui/themes'; -import { Html } from '@react-three/drei'; -import { useFrame } from '@react-three/fiber'; -import { ReactNode, useEffect, useRef } from 'react'; -import { Group, Mesh, Vector3 } from 'three'; -import { radToDeg } from 'three/src/math/MathUtils'; -import { isExplosive } from '../core/blitz/isExplosive'; -import { AngledPenetration } from '../icons/AngledPenetration'; -import { Block } from '../icons/Block'; -import { NominalPenetration } from '../icons/NominalPenetration'; -import { Ricochet } from '../icons/Ricochet'; -import { useDuel } from '../stores/duel'; -import { Shot, useTankopediaTemporary } from '../stores/tankopedia'; - -export const SHOT_NAMES: Record = { - ricochet: 'Ricochet', - penetration: 'Penetration', - block: 'Blocked', -}; - -export const SHOT_ICONS: Record = { - ricochet: , - block: , - penetration: , -}; - -const THICKNESS = 0.05; -const LENGTH = 4; - -export function ShotDisplay() { - const shot = useTankopediaTemporary((state) => state.shot); - const entryWrapper = useRef(null); - const entryLine = useRef(null); - const circle = useRef(null); - const ricochetCircle = useRef(null); - const exitWrapper = useRef(null); - const exitLine = useRef(null); - const secondLineLength = shot?.ricochet ? shot?.ricochet.distance : LENGTH; - const number = shot && (shot.thicknesses.length > 1 || shot.ricochet); - const shell = useDuel((state) => state.antagonist!.shell); - - useFrame(({ clock }) => { - const t1 = clock.elapsedTime % 1; - const t2 = (clock.elapsedTime + 0.5) % 1; - - entryLine.current?.position.set(0, 0, LENGTH - t1 * LENGTH); - entryLine.current?.scale.set(1, -2 * Math.abs(t1 - 0.5) + 1, 1); - exitLine.current?.position.set(0, 0, t2 * secondLineLength); - exitLine.current?.scale.set(1, -2 * Math.abs(t2 - 0.5) + 1, 1); - }); - - useEffect(() => { - if (!shot) return; - - const point = new Vector3(...shot.point); - const shellNormal = new Vector3(...shot.shellNormal); - const surfaceNormal = new Vector3(...shot.surfaceNormal); - const bounceNormal = shellNormal - .clone() - .multiplyScalar(-1) - .sub( - surfaceNormal - .clone() - .multiplyScalar( - 2 * surfaceNormal.dot(shellNormal.clone().multiplyScalar(-1)), - ), - ); - - entryWrapper.current?.position.set(...shot.point); - entryWrapper.current?.lookAt(point.clone().add(shellNormal)); - exitWrapper.current?.position.set(...shot.point); - exitWrapper.current?.lookAt(point.clone().add(bounceNormal)); - circle.current?.position.set(...shot.point); - circle.current?.lookAt(point.clone().add(surfaceNormal)); - }, [shot]); - - if (!shot) return null; - - return ( - <> - - - - - - - - - - - {SHOT_ICONS[shot.type]} - - {SHOT_NAMES[shot.type]} at {Math.round(radToDeg(shot.angle))}° - - - - - {shot.thicknesses.map((layer, index) => ( - - {number && ( - - {index + 1}. {layer.type[0].toUpperCase()} - {layer.type.slice(1)} - - )} - - - {layer.type !== 'gap' && ( - - )} - - {Math.round( - layer.type === 'gap' - ? layer.gap * 1000 - : layer.nominal, - )} - mm - {layer.type === 'gap' && isExplosive(shell.type) - ? ` (-${Math.round(Math.min(100, 0.5 * layer.gap * 100))}% penetration)` - : ''} - - - {layer.type !== 'external' && layer.type !== 'gap' && ( - - - {Math.round(layer.angled)}mm - - )} - - ))} - - {shot.ricochet && ( - <> - - {shot.thicknesses.length + 1}. Ricochet - - (-25% penetration) - - {shot.ricochet.penetration ? ( - - {shot.thicknesses.length + 2}. Penetration - - - ) : ( - - {shot.thicknesses.length + 2}. Blocked - - - )} - - )} - - - - - - - - - - - - - - - - {shot.type === 'ricochet' && ( - - - - - - - - - - - {shot.ricochet && ( - - - - - )} - - )} - - ); -} diff --git a/src/constants/axis.ts b/src/constants/axis.ts index 9d600d2be..784321114 100644 --- a/src/constants/axis.ts +++ b/src/constants/axis.ts @@ -1,5 +1,6 @@ import { Vector3 } from 'three'; +export const ZERO_VECTOR = new Vector3(); export const I_HAT = new Vector3(1, 0, 0); export const J_HAT = new Vector3(0, 1, 0); export const K_HAT = new Vector3(0, 0, 1); diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 206e7451d..c83f6d8fe 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -1,9 +1,10 @@ import { produce } from 'immer'; import { merge } from 'lodash'; -import { Vector3Tuple } from 'three'; +import { Vector3 } from 'three'; import { create } from 'zustand'; import { persist, subscribeWithSelector } from 'zustand/middleware'; import { ENVIRONMENTS } from '../app/tools/tankopedia/[id]/components/Lighting'; +import { ArmorType } from '../components/Armor/components/SpacedArmorScene'; import { TankType, TreeType } from '../components/Tanks'; import { Tier } from '../core/blitzkrieg/tankDefinitions'; @@ -43,20 +44,34 @@ interface TankopediaTemporary { shot?: Shot; } -export interface Shot { - point: Vector3Tuple; - surfaceNormal: Vector3Tuple; - shellNormal: Vector3Tuple; - thicknesses: ArmorPiercingLayer[]; - type: 'ricochet' | 'penetration' | 'block'; - angle: number; - ricochet?: { - distance: number; - point: Vector3Tuple; - penetration: boolean; - }; +export interface ShotLayerBase { + index: number; + thickness: number; + point: Vector3; + shellNormal: Vector3; + surfaceNormal: Vector3; + status: 'blocked' | 'penetration' | 'ricochet'; } +interface ShotLayerExternal extends ShotLayerBase { + type: ArmorType.External; +} + +interface ShotLayerNonExternal extends ShotLayerBase { + type: Exclude; + angle: number; + thicknessAngled: number; +} + +export interface ShotLayerGap { + type: null; + distance: number; +} + +type ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap; + +export type Shot = ShotLayer[]; + export type ArmorPiercingLayer = | { distance: number; From 7f49245f15ff9e923a3d174a8f78dc3ce756db3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 28 Feb 2024 23:06:37 -0600 Subject: [PATCH 20/32] feat: more progress --- .../[id]/components/Model/TankSandbox.tsx | 3 +- .../{ShotScene.tsx => ShotDisplay.tsx} | 51 +++++---- .../SpacedArmorSceneComponent/index.tsx | 101 +++++++++++------- src/components/Armor/index.tsx | 7 -- 4 files changed, 95 insertions(+), 67 deletions(-) rename src/components/Armor/components/{ShotScene.tsx => ShotDisplay.tsx} (60%) diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx index ed1d98ae0..e7e9744a6 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -5,7 +5,7 @@ import { Canvas } from '@react-three/fiber'; import { Suspense, use, useEffect, useRef, useState } from 'react'; import { isMobile } from 'react-device-detect'; import { Armor } from '../../../../../../components/Armor'; -import { ShotScene } from '../../../../../../components/Armor/components/ShotScene'; +import { ShotDisplay } from '../../../../../../components/Armor/components/ShotDisplay'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; @@ -201,6 +201,7 @@ export function TankSandbox() { + diff --git a/src/components/Armor/components/ShotScene.tsx b/src/components/Armor/components/ShotDisplay.tsx similarity index 60% rename from src/components/Armor/components/ShotScene.tsx rename to src/components/Armor/components/ShotDisplay.tsx index 9fa423a3c..2009a7288 100644 --- a/src/components/Armor/components/ShotScene.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -1,6 +1,6 @@ import { Text } from '@radix-ui/themes'; import { Html } from '@react-three/drei'; -import { DoubleSide, Euler, Quaternion, Vector3 } from 'three'; +import { Euler, Quaternion, Vector3 } from 'three'; import { J_HAT } from '../../../constants/axis'; import { ShotLayerGap, @@ -22,7 +22,7 @@ import { // const THICKNESS = 0.05; // const LENGTH = 4; -export function ShotScene() { +export function ShotDisplay() { const shot = useTankopediaTemporary((state) => state.shot); return ( @@ -39,28 +39,39 @@ export function ShotScene() { return ( <> - - - - - {shot.length > 1 && ( - - {layer.index + 1} + +
+ + {layer.index + 1} + +
)} @@ -73,9 +84,9 @@ export function ShotScene() { } return ( - + - + ); })()} diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index 81950104e..5ac920e80 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -8,13 +8,17 @@ import { Scene, Vector3, } from 'three'; -import { degToRad, radToDeg } from 'three/src/math/MathUtils'; +import { degToRad } from 'three/src/math/MathUtils'; import { isExplosive } from '../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment'; import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; import { useDuel } from '../../../../stores/duel'; -import { Shot, useTankopediaTemporary } from '../../../../stores/tankopedia'; +import { + Shot, + ShotLayerBase, + useTankopediaTemporary, +} from '../../../../stores/tankopedia'; import { ArmorType } from '../SpacedArmorScene'; import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal'; import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced'; @@ -68,7 +72,7 @@ export function SpacedArmorSceneComponent({ ...props }: SpacedArmorSceneComponentProps) { const camera = useThree((state) => state.camera); - const cameraNormal = new Vector3(); + // const cameraNormal = new Vector3(); return ( <> @@ -104,26 +108,26 @@ export function SpacedArmorSceneComponent({ .clone() .sub(event.point) .normalize(); - shellNormal - .copy(camera.getWorldDirection(cameraNormal)) - .multiplyScalar(-1); + const cameraNormal = shellNormal.clone().multiplyScalar(-1); + pushLogs( shellNormal, + cameraNormal, filterIntersections(event.intersections), - true, + 0, ); - console.clear(); - console.log( - JSON.stringify( - shot.map((i) => ({ - ...i, - ...(i.angle ? { angle: radToDeg(i.angle) } : {}), - })), - null, - 2, - ), - ); + // console.clear(); + // console.log( + // JSON.stringify( + // shot.map((i) => ({ + // ...i, + // ...(i.angle ? { angle: radToDeg(i.angle) } : {}), + // })), + // null, + // 2, + // ), + // ); function filterIntersections(intersectionsRaw: Intersection[]) { const intersectionsAll = intersectionsRaw.filter( @@ -166,15 +170,35 @@ export function SpacedArmorSceneComponent({ }); } + console.log( + JSON.stringify( + intersections.map((i) => i.object.userData), + null, + 2, + ), + ); + return intersections; } function pushLogs( shellNormal: Vector3, + cameraNormal: Vector3, intersections: ArmorMeshIntersection[], - allowRicochets: boolean, + startingIndex: number, ) { - let index = 0; + let layerIndex = startingIndex; + let loopIndex = 0; + + if (layerIndex !== 0) { + shot.push({ + type: null, + distance: (shot.at(-1) as ShotLayerBase).point.distanceTo( + intersections[0].point, + ), + }); + } + for (const intersection of intersections) { const surfaceNormal = intersection .normal!.clone() @@ -184,8 +208,8 @@ export function SpacedArmorSceneComponent({ const thickness = thicknessCoefficient * intersection.object.userData.thickness; - if (index !== 0) { - const previousIntersection = intersections[index - 1]; + if (layerIndex !== 0 && startingIndex === 0) { + const previousIntersection = intersections[layerIndex - 1]; shot.push({ type: null, distance: @@ -199,7 +223,7 @@ export function SpacedArmorSceneComponent({ shot.push({ type: ArmorType.External, - index, + index: layerIndex, shellNormal, surfaceNormal, point: intersection.point, @@ -210,19 +234,11 @@ export function SpacedArmorSceneComponent({ if (blocked) break; } else { const type = intersection.object.userData.type; - shellNormal.copy( - cameraNormal - .copy(camera.position) - .sub(event.point) - .normalize(), - ); const angle = surfaceNormal.angleTo(shellNormal); const ricochet = degToRad(shell.ricochet ?? 90); const normalization = degToRad(shell.normalization ?? 0); const threeCalibersRule = - shell.caliber > thickness * 3 || - index > 0 || - !allowRicochets; + shell.caliber > thickness * 3 || layerIndex > 0; const twoCalibersRule = shell.caliber > thickness * 2; const finalNormalization = twoCalibersRule ? (1.4 * normalization * shell.caliber) / (2.0 * thickness) @@ -234,7 +250,7 @@ export function SpacedArmorSceneComponent({ if (!threeCalibersRule && angle >= ricochet) { shot.push({ type, - index, + index: layerIndex, shellNormal, surfaceNormal, point: intersection.point, @@ -245,15 +261,21 @@ export function SpacedArmorSceneComponent({ }); const raycaster = new Raycaster(); - const newShellNormal = cameraNormal + const newShellNormal = shellNormal .clone() .reflect(surfaceNormal); + const newCameraNormal = newShellNormal + .clone() + .multiplyScalar(-1); - raycaster.set(intersection.point, newShellNormal); + raycaster.set(intersection.point, newCameraNormal); pushLogs( newShellNormal, - raycaster.intersectObjects(scene.children, true), - false, + newCameraNormal, + filterIntersections( + raycaster.intersectObjects(scene.children, true), + ), + layerIndex + 1, ); break; @@ -261,7 +283,7 @@ export function SpacedArmorSceneComponent({ const blocked = remainingPenetration < 0; shot.push({ type, - index, + index: layerIndex, shellNormal, surfaceNormal, point: intersection.point, @@ -273,7 +295,8 @@ export function SpacedArmorSceneComponent({ } } - index++; + layerIndex++; + loopIndex++; } } diff --git a/src/components/Armor/index.tsx b/src/components/Armor/index.tsx index c3a889805..67ac23c84 100644 --- a/src/components/Armor/index.tsx +++ b/src/components/Armor/index.tsx @@ -3,19 +3,16 @@ import { memo, useState } from 'react'; import { DepthTexture, Scene, Vector2 } from 'three'; import { CoreArmorScene } from './components/CoreArmorScene'; import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent'; -import { ShotScene } from './components/ShotScene'; import { SpacedArmorScene } from './components/SpacedArmorScene'; export const Armor = memo(() => { const [spacedArmorScene] = useState(() => new Scene()); const [coreArmorScene] = useState(() => new Scene()); - const [shotScene] = useState(() => new Scene()); const spacedArmorPortal = createPortal( , spacedArmorScene, ); const coreArmorPortal = createPortal(, coreArmorScene); - const shotPortal = createPortal(, shotScene); const renderSize = new Vector2(); const newRenderSize = new Vector2(); @@ -44,16 +41,12 @@ export const Armor = memo(() => { gl.clearDepth(); gl.render(coreArmorScene, camera); - - gl.clearDepth(); - gl.render(shotScene, camera); }, 1); return ( <> {spacedArmorPortal} {coreArmorPortal} - {shotPortal} ); }); From bf6991ce0605dcb322bc8a28fea60080f1891048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:30:48 -0600 Subject: [PATCH 21/32] feat: proper ricochet visuals --- .../Armor/components/ShotDisplay.tsx | 93 ++++++++++++------- .../SpacedArmorSceneComponent/index.tsx | 14 +-- 2 files changed, 66 insertions(+), 41 deletions(-) diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index 2009a7288..172279a7e 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -1,4 +1,4 @@ -import { Text } from '@radix-ui/themes'; +import { Card, Inset, Text } from '@radix-ui/themes'; import { Html } from '@react-three/drei'; import { Euler, Quaternion, Vector3 } from 'three'; import { J_HAT } from '../../../constants/axis'; @@ -22,9 +22,13 @@ import { // const THICKNESS = 0.05; // const LENGTH = 4; +const LENGTH_INFINITY = 4; + export function ShotDisplay() { const shot = useTankopediaTemporary((state) => state.shot); + console.log(shot); + return ( <> {shot?.map((layer, index) => { @@ -44,39 +48,36 @@ export function ShotDisplay() { position={layer.point} rotation={shellRotation} > - {shot.length > 1 && ( - -
- - {layer.index + 1} - -
- - )} + + + + {shot.length > 1 && ( + + {layer.index + 1} + + )} + + + {(() => { - let length = 4; + let length = LENGTH_INFINITY; if (layer.index !== 0) { const gap = shot[index - 1] as ShotLayerGap; @@ -91,6 +92,34 @@ export function ShotDisplay() { ); })()}
+ + {(() => { + if (layer.status !== 'ricochet' || index !== shot.length - 1) + return null; + const ricochetShellNormal = layer.shellNormal + .clone() + .reflect(layer.surfaceNormal) + .multiplyScalar(-1); + const ricochetShellRotation = new Euler().setFromQuaternion( + new Quaternion().setFromAxisAngle( + new Vector3() + .crossVectors(J_HAT, ricochetShellNormal) + .normalize(), + J_HAT.angleTo(ricochetShellNormal), + ), + ); + + return ( + + + + + + + ); + })()} ); })} diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index 5ac920e80..a5d036cc3 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -170,14 +170,6 @@ export function SpacedArmorSceneComponent({ }); } - console.log( - JSON.stringify( - intersections.map((i) => i.object.userData), - null, - 2, - ), - ); - return intersections; } @@ -190,7 +182,7 @@ export function SpacedArmorSceneComponent({ let layerIndex = startingIndex; let loopIndex = 0; - if (layerIndex !== 0) { + if (layerIndex !== 0 && intersections.length > 0) { shot.push({ type: null, distance: (shot.at(-1) as ShotLayerBase).point.distanceTo( @@ -260,6 +252,8 @@ export function SpacedArmorSceneComponent({ status: 'ricochet', }); + remainingPenetration *= 0.75; + const raycaster = new Raycaster(); const newShellNormal = shellNormal .clone() @@ -300,6 +294,8 @@ export function SpacedArmorSceneComponent({ } } + console.log('setting', shot); + useTankopediaTemporary.setState({ shot }); }, })} From fa8922e0a69e5d42084a5fb502dfe0acfb27e372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:40:36 -0600 Subject: [PATCH 22/32] feat: he mechanics --- .../Armor/components/ShotDisplay.tsx | 12 ++++-- .../SpacedArmorSceneComponent/index.tsx | 40 +++++++++++-------- src/stores/tankopedia.ts | 1 + 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index 172279a7e..147cb71cf 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -26,6 +26,9 @@ const LENGTH_INFINITY = 4; export function ShotDisplay() { const shot = useTankopediaTemporary((state) => state.shot); + const hasMultipleLayers = shot?.some( + (layer, index) => index !== 0 && layer.type !== null, + ); console.log(shot); @@ -40,6 +43,9 @@ export function ShotDisplay() { J_HAT.angleTo(layer.shellNormal), ), ); + const nextLayer = shot[index + 1] as ShotLayerGap | undefined; + const trueStatus = + nextLayer?.status === 'wasted' ? 'blocked' : layer.status; return ( <> @@ -52,16 +58,16 @@ export function ShotDisplay() { - {shot.length > 1 && ( + {hasMultipleLayers && ( ({ - // ...i, - // ...(i.angle ? { angle: radToDeg(i.angle) } : {}), - // })), - // null, - // 2, - // ), - // ); - function filterIntersections(intersectionsRaw: Intersection[]) { const intersectionsAll = intersectionsRaw.filter( ({ object }) => object.userData.type !== undefined, @@ -183,12 +172,20 @@ export function SpacedArmorSceneComponent({ let loopIndex = 0; if (layerIndex !== 0 && intersections.length > 0) { + const distance = ( + shot.at(-1) as ShotLayerBase + ).point.distanceTo(intersections[0].point); + if (explosive) + remainingPenetration -= 0.5 * remainingPenetration * distance; + const wasted = remainingPenetration < 0; + shot.push({ type: null, - distance: (shot.at(-1) as ShotLayerBase).point.distanceTo( - intersections[0].point, - ), + status: wasted ? 'wasted' : 'nominal', + distance, }); + + if (wasted) return; } for (const intersection of intersections) { @@ -202,11 +199,20 @@ export function SpacedArmorSceneComponent({ if (layerIndex !== 0 && startingIndex === 0) { const previousIntersection = intersections[layerIndex - 1]; + const distance = + intersection.distance - previousIntersection.distance; + if (explosive) + remainingPenetration -= + 0.5 * remainingPenetration * distance; + const wasted = remainingPenetration < 0; + shot.push({ type: null, - distance: - intersection.distance - previousIntersection.distance, + status: wasted ? 'wasted' : 'nominal', + distance, }); + + if (wasted) return; } if (intersection.object.userData.type === ArmorType.External) { diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index c83f6d8fe..0d9137415 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -65,6 +65,7 @@ interface ShotLayerNonExternal extends ShotLayerBase { export interface ShotLayerGap { type: null; + status: 'nominal' | 'wasted'; distance: number; } From c0a3628b3b7b258b714254d1afcfd8075856224a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 11:16:00 -0600 Subject: [PATCH 23/32] feat: list --- .../Armor/components/ShotDisplay.tsx | 127 ++++++++++++++++-- .../SpacedArmorSceneComponent/index.tsx | 37 +++-- src/stores/tankopedia.ts | 9 +- 3 files changed, 138 insertions(+), 35 deletions(-) diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index 147cb71cf..d7edac3fb 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -1,11 +1,19 @@ -import { Card, Inset, Text } from '@radix-ui/themes'; +import { Card, Flex, Inset, Text } from '@radix-ui/themes'; import { Html } from '@react-three/drei'; import { Euler, Quaternion, Vector3 } from 'three'; +import { radToDeg } from 'three/src/math/MathUtils'; import { J_HAT } from '../../../constants/axis'; +import { isExplosive } from '../../../core/blitz/isExplosive'; +import { AngledPenetration } from '../../../icons/AngledPenetration'; +import { Block } from '../../../icons/Block'; +import { NominalPenetration } from '../../../icons/NominalPenetration'; +import { Ricochet } from '../../../icons/Ricochet'; +import { useDuel } from '../../../stores/duel'; import { ShotLayerGap, useTankopediaTemporary, } from '../../../stores/tankopedia'; +import { ArmorType } from './SpacedArmorScene'; // export const SHOT_NAMES: Record = { // ricochet: 'Ricochet', @@ -13,12 +21,19 @@ import { // block: 'Blocked', // }; -// export const SHOT_ICONS: Record = { +// export const SHOT_ICONS: Record = { // ricochet: , // block: , // penetration: , // }; +const layerTypeNames: Record = { + [ArmorType.Core]: 'Core', + [ArmorType.Spaced]: 'Spaced', + [ArmorType.External]: 'External', + null: 'Gap', +}; + // const THICKNESS = 0.05; // const LENGTH = 4; @@ -26,15 +41,15 @@ const LENGTH_INFINITY = 4; export function ShotDisplay() { const shot = useTankopediaTemporary((state) => state.shot); - const hasMultipleLayers = shot?.some( + const hasMultipleLayers = shot?.layers.some( (layer, index) => index !== 0 && layer.type !== null, ); - - console.log(shot); + const { shell } = useDuel.getState().antagonist!; + const explosive = isExplosive(shell.type); return ( <> - {shot?.map((layer, index) => { + {shot?.layers.map((layer, index) => { if (layer.type === null) return; const shellRotation = new Euler().setFromQuaternion( @@ -43,9 +58,9 @@ export function ShotDisplay() { J_HAT.angleTo(layer.shellNormal), ), ); - const nextLayer = shot[index + 1] as ShotLayerGap | undefined; + const nextLayer = shot.layers[index + 1] as ShotLayerGap | undefined; const trueStatus = - nextLayer?.status === 'wasted' ? 'blocked' : layer.status; + nextLayer?.status === 'blocked' ? 'blocked' : layer.status; return ( <> @@ -54,7 +69,12 @@ export function ShotDisplay() { position={layer.point} rotation={shellRotation} > - + {(() => { - if (layer.status !== 'ricochet' || index !== shot.length - 1) + if ( + layer.status !== 'ricochet' || + index !== shot.layers.length - 1 + ) return null; const ricochetShellNormal = layer.shellNormal .clone() @@ -129,6 +152,88 @@ export function ShotDisplay() { ); })} + + {shot && + (() => { + const lastLayer = shot.layers.at(-1)!; + const lastStatus = lastLayer.status; + + return ( + + + + + {lastStatus[0].toUpperCase()} + {lastStatus.slice(1)} + + + {shot.layers.map((layer) => { + if (layer.type === null && !explosive) return null; + + return ( + + + {hasMultipleLayers && ( + + {layer.type === null + ? null + : `${layer.index + 1}.`} + + )} + + {layerTypeNames[`${layer.type}`]} + + + + {layer.type !== null && ( + <> + + + + {Math.round(layer.thickness)}mm + + + + {layer.type !== ArmorType.External && ( + + {layer.status === 'ricochet' ? ( + + ) : layer.status === 'blocked' ? ( + + ) : ( + + )} + + {Math.round(layer.thicknessAngled)}mm ( + {Math.round(radToDeg(layer.angle))}°) + + + )} + + )} + + {layer.type === null && ( + + {Math.round(layer.distance * 1000)}mm (- + {Math.round(Math.min(100, layer.distance * 50))}% + penetration) + + )} + + ); + })} + + + + ); + })()} ); } diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index 24eefb27b..7bc70448c 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -100,7 +100,10 @@ export function SpacedArmorSceneComponent({ ? 1.1 : 1.05 : 1); - const shot: Shot = []; // TODO: type this + const shot: Shot = { + layers: [], + point: event.point, + }; // TODO: type this let remainingPenetration = penetration; let penetrationChance = -1; let splashChance = -1; @@ -109,14 +112,8 @@ export function SpacedArmorSceneComponent({ .clone() .sub(event.point) .normalize(); - const cameraNormal = shellNormal.clone().multiplyScalar(-1); - pushLogs( - shellNormal, - cameraNormal, - filterIntersections(event.intersections), - 0, - ); + pushLogs(shellNormal, filterIntersections(event.intersections), 0); function filterIntersections(intersectionsRaw: Intersection[]) { const intersectionsAll = intersectionsRaw.filter( @@ -164,7 +161,6 @@ export function SpacedArmorSceneComponent({ function pushLogs( shellNormal: Vector3, - cameraNormal: Vector3, intersections: ArmorMeshIntersection[], startingIndex: number, ) { @@ -173,15 +169,15 @@ export function SpacedArmorSceneComponent({ if (layerIndex !== 0 && intersections.length > 0) { const distance = ( - shot.at(-1) as ShotLayerBase + shot.layers.at(-1) as ShotLayerBase ).point.distanceTo(intersections[0].point); if (explosive) remainingPenetration -= 0.5 * remainingPenetration * distance; const wasted = remainingPenetration < 0; - shot.push({ + shot.layers.push({ type: null, - status: wasted ? 'wasted' : 'nominal', + status: wasted ? 'blocked' : 'penetration', distance, }); @@ -206,9 +202,9 @@ export function SpacedArmorSceneComponent({ 0.5 * remainingPenetration * distance; const wasted = remainingPenetration < 0; - shot.push({ + shot.layers.push({ type: null, - status: wasted ? 'wasted' : 'nominal', + status: wasted ? 'blocked' : 'penetration', distance, }); @@ -219,7 +215,7 @@ export function SpacedArmorSceneComponent({ remainingPenetration -= thickness; const blocked = remainingPenetration < 0; - shot.push({ + shot.layers.push({ type: ArmorType.External, index: layerIndex, shellNormal, @@ -242,11 +238,13 @@ export function SpacedArmorSceneComponent({ ? (1.4 * normalization * shell.caliber) / (2.0 * thickness) : normalization; const finalThickness = - thickness / Math.cos(angle - finalNormalization); + thickness === 0 + ? 0 + : thickness / Math.cos(angle - finalNormalization); remainingPenetration -= finalThickness; if (!threeCalibersRule && angle >= ricochet) { - shot.push({ + shot.layers.push({ type, index: layerIndex, shellNormal, @@ -271,7 +269,6 @@ export function SpacedArmorSceneComponent({ raycaster.set(intersection.point, newCameraNormal); pushLogs( newShellNormal, - newCameraNormal, filterIntersections( raycaster.intersectObjects(scene.children, true), ), @@ -281,7 +278,7 @@ export function SpacedArmorSceneComponent({ break; } else { const blocked = remainingPenetration < 0; - shot.push({ + shot.layers.push({ type, index: layerIndex, shellNormal, @@ -300,8 +297,6 @@ export function SpacedArmorSceneComponent({ } } - console.log('setting', shot); - useTankopediaTemporary.setState({ shot }); }, })} diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 0d9137415..43879b663 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -65,13 +65,16 @@ interface ShotLayerNonExternal extends ShotLayerBase { export interface ShotLayerGap { type: null; - status: 'nominal' | 'wasted'; + status: 'penetration' | 'blocked'; distance: number; } -type ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap; +export type ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap; -export type Shot = ShotLayer[]; +export type Shot = { + point: Vector3; + layers: ShotLayer[]; +}; export type ArmorPiercingLayer = | { From f5578ee788d74af44b049185793a22c9f5ac9dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 12:02:14 -0600 Subject: [PATCH 24/32] feat: damage --- src/components/Armor/components/ShotDisplay.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index d7edac3fb..eda5f8652 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -167,11 +167,13 @@ export function ShotDisplay() { pointerEvents: 'none', }} > - + {lastStatus[0].toUpperCase()} {lastStatus.slice(1)} + {lastStatus === 'penetration' && + `: ${shell.damage.module}hp`} {shot.layers.map((layer) => { From 199e156e53e060d6ae41afe3ae74b81db546160d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 12:42:15 -0600 Subject: [PATCH 25/32] feat: more accurate HE --- .../shaders/fragment.glsl | 6 +- .../Armor/components/ShotDisplay.tsx | 58 +++++++++++++++---- .../SpacedArmorSceneComponent/index.tsx | 12 ++-- src/core/math/NaNFallback.ts | 3 + src/stores/tankopedia.ts | 2 +- 5 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 src/core/math/NaNFallback.ts diff --git a/src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl b/src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl index 7bd9312f1..b5b6f4a50 100644 --- a/src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl +++ b/src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl @@ -53,8 +53,8 @@ void main() { float spacedArmorThickness = spacedArmorBufferFragment.r * penetration; remainingPenetration -= spacedArmorThickness; - if (isExplosive && remainingPenetration > 0.0) { - // there is a 50% penetration loss per meter for HE based shells + if (isExplosive && !canSplash && remainingPenetration > 0.0) { + // there is a 50% penetration loss per meter for HEAT shells remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor; } } @@ -68,7 +68,7 @@ void main() { // only allow splashing if the damage equation deals more than 0 damage float nominalArmorThickness = finalThickness - remainingPenetration; float finalDamage = 0.5 * damage * (1.0 - distanceFromSpacedArmor / explosionRadius) - 1.1 * nominalArmorThickness; - splashChance = finalDamage > 0.0 ? 1.0 : 0.0; + splashChance = finalDamage > 0.0 ? 1.0 : 0.0; // TODO: add randomization here? } else { splashChance = 0.0; } diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index eda5f8652..cf2ae9ef5 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -4,13 +4,17 @@ import { Euler, Quaternion, Vector3 } from 'three'; import { radToDeg } from 'three/src/math/MathUtils'; import { J_HAT } from '../../../constants/axis'; import { isExplosive } from '../../../core/blitz/isExplosive'; +import { NaNFallback } from '../../../core/math/NaNFallback'; import { AngledPenetration } from '../../../icons/AngledPenetration'; import { Block } from '../../../icons/Block'; import { NominalPenetration } from '../../../icons/NominalPenetration'; import { Ricochet } from '../../../icons/Ricochet'; import { useDuel } from '../../../stores/duel'; import { + ShotLayer, + ShotLayerBase, ShotLayerGap, + ShotLayerNonExternal, useTankopediaTemporary, } from '../../../stores/tankopedia'; import { ArmorType } from './SpacedArmorScene'; @@ -79,10 +83,10 @@ export function ShotDisplay() { style={{ backgroundColor: trueStatus === 'penetration' - ? '#00ff0080' + ? '#00ff00c0' : trueStatus === 'blocked' - ? '#ff000080' - : '#ffff0080', + ? '#ff0000c0' + : '#ffff00c0', border: 'none', }} > @@ -93,9 +97,11 @@ export function ShotDisplay() { width: '100%', display: 'block', textAlign: 'center', + color: + trueStatus === 'ricochet' ? 'black' : undefined, }} > - {layer.index + 1} + {layer.index + 1} )} @@ -155,25 +161,54 @@ export function ShotDisplay() { {shot && (() => { + const firstLayer = shot.layers[0] as Exclude; const lastLayer = shot.layers.at(-1)!; - const lastStatus = lastLayer.status; + const midPointLayer = (shot.layers.findLast( + (layer) => layer.type !== null && layer.status === 'ricochet', + ) ?? + shot.layers.findLast( + (layer) => layer.type !== null, + )) as ShotLayerBase; + const distanceFromSpacedArmor = firstLayer.point.distanceTo( + shot.point, + ); + + console.log(distanceFromSpacedArmor); return ( - {lastStatus[0].toUpperCase()} - {lastStatus.slice(1)} - {lastStatus === 'penetration' && - `: ${shell.damage.module}hp`} + {lastLayer.status[0].toUpperCase()} + {lastLayer.status.slice(1)} + {lastLayer.status === 'penetration' && + `: ${Math.round(shell.damage.armor)}hp`} + {lastLayer.status === 'blocked' && + shell.type === 'he' && + `: ${NaNFallback( + Math.max( + 0, + Math.round( + 0.5 * + shell.damage.armor * + (1 - + distanceFromSpacedArmor / + shell.explosionRadius!) - + 1.1 * + (midPointLayer as ShotLayerNonExternal) + .thicknessAngled, + ), + ), + 0, + )}hp`} {shot.layers.map((layer) => { @@ -223,6 +258,7 @@ export function ShotDisplay() { {layer.type === null && ( + {/* why is it x50? i can't pretend to understand this shit */} {Math.round(layer.distance * 1000)}mm (- {Math.round(Math.min(100, layer.distance * 50))}% penetration) diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index 7bc70448c..426d17479 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -9,6 +9,7 @@ import { Vector3, } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; +import { canSplash } from '../../../../core/blitz/canSplash'; import { isExplosive } from '../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment'; @@ -93,6 +94,7 @@ export function SpacedArmorSceneComponent({ const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1; const shell = useDuel.getState().antagonist!.shell; const explosive = isExplosive(shell.type); + const splashing = canSplash(shell.type); const penetration = resolveNearPenetration(shell.penetration) * (hasCalibratedShells @@ -171,17 +173,17 @@ export function SpacedArmorSceneComponent({ const distance = ( shot.layers.at(-1) as ShotLayerBase ).point.distanceTo(intersections[0].point); - if (explosive) + if (explosive && !splashing) remainingPenetration -= 0.5 * remainingPenetration * distance; - const wasted = remainingPenetration < 0; + const blocked = remainingPenetration < 0; shot.layers.push({ type: null, - status: wasted ? 'blocked' : 'penetration', + status: blocked ? 'blocked' : 'penetration', distance, }); - if (wasted) return; + if (blocked) return; } for (const intersection of intersections) { @@ -197,7 +199,7 @@ export function SpacedArmorSceneComponent({ const previousIntersection = intersections[layerIndex - 1]; const distance = intersection.distance - previousIntersection.distance; - if (explosive) + if (explosive && !splashing) remainingPenetration -= 0.5 * remainingPenetration * distance; const wasted = remainingPenetration < 0; diff --git a/src/core/math/NaNFallback.ts b/src/core/math/NaNFallback.ts new file mode 100644 index 000000000..d1b93b662 --- /dev/null +++ b/src/core/math/NaNFallback.ts @@ -0,0 +1,3 @@ +export function NaNFallback(value: number, fallback: number) { + return isNaN(value) ? fallback : value; +} diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 43879b663..3cbaeb781 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -57,7 +57,7 @@ interface ShotLayerExternal extends ShotLayerBase { type: ArmorType.External; } -interface ShotLayerNonExternal extends ShotLayerBase { +export interface ShotLayerNonExternal extends ShotLayerBase { type: Exclude; angle: number; thicknessAngled: number; From 7af1fdcf6b4d5bc58e8999ca5c5cc108a50487dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 13:17:41 -0600 Subject: [PATCH 26/32] feat: animation --- .../Armor/components/ShotDisplay.tsx | 345 +++++++++--------- src/core/math/normalToEuler.ts | 14 + 2 files changed, 185 insertions(+), 174 deletions(-) create mode 100644 src/core/math/normalToEuler.ts diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index cf2ae9ef5..3d6bc4611 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -1,10 +1,12 @@ import { Card, Flex, Inset, Text } from '@radix-ui/themes'; import { Html } from '@react-three/drei'; -import { Euler, Quaternion, Vector3 } from 'three'; +import { useFrame } from '@react-three/fiber'; +import { useRef } from 'react'; +import { Mesh } from 'three'; import { radToDeg } from 'three/src/math/MathUtils'; -import { J_HAT } from '../../../constants/axis'; import { isExplosive } from '../../../core/blitz/isExplosive'; import { NaNFallback } from '../../../core/math/NaNFallback'; +import { normalToEuler } from '../../../core/math/normalToEuler'; import { AngledPenetration } from '../../../icons/AngledPenetration'; import { Block } from '../../../icons/Block'; import { NominalPenetration } from '../../../icons/NominalPenetration'; @@ -19,18 +21,6 @@ import { } from '../../../stores/tankopedia'; import { ArmorType } from './SpacedArmorScene'; -// export const SHOT_NAMES: Record = { -// ricochet: 'Ricochet', -// penetration: 'Penetration', -// block: 'Blocked', -// }; - -// export const SHOT_ICONS: Record = { -// ricochet: , -// block: , -// penetration: , -// }; - const layerTypeNames: Record = { [ArmorType.Core]: 'Core', [ArmorType.Spaced]: 'Spaced', @@ -38,30 +28,111 @@ const layerTypeNames: Record = { null: 'Gap', }; -// const THICKNESS = 0.05; -// const LENGTH = 4; - const LENGTH_INFINITY = 4; +const TRACER_THIN = 1 / 128; +const TRACER_THICK = 1 / 64; export function ShotDisplay() { const shot = useTankopediaTemporary((state) => state.shot); + const preMidPointTracer = useRef(null); + const postMidPointTracer = useRef(null); const hasMultipleLayers = shot?.layers.some( (layer, index) => index !== 0 && layer.type !== null, ); const { shell } = useDuel.getState().antagonist!; const explosive = isExplosive(shell.type); + const midPointLayer = (shot?.layers.findLast( + (layer) => layer.type !== null && layer.status === 'ricochet', + ) ?? shot?.layers.findLast((layer) => layer.type !== null)) as ShotLayerBase; + const firstLayer = shot?.layers[0] as Exclude; + const lastLayer = shot?.layers.at(-1)!; + const distanceFromSpacedArmor = shot + ? firstLayer.point.distanceTo(shot.point) + : undefined; + const preMidPointLayers = shot?.layers.slice( + 0, + shot.layers.indexOf(midPointLayer as ShotLayer), + ); + const preMidPointGap = shot + ? preMidPointLayers!.reduce((accumulator, layer) => { + if (layer.type === null) return accumulator + layer.distance; + return accumulator; + }, 0) + LENGTH_INFINITY + : undefined; + + useFrame(({ clock }) => { + if (!shot || !preMidPointTracer.current) return; + + const t1 = clock.elapsedTime % 1; + const t2 = (clock.elapsedTime + 0.5) % 1; + + preMidPointTracer.current.position.set(0, (1 - t1) * preMidPointGap!, 0); + preMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t1 - 0.5), 1); + + if (!postMidPointTracer.current) return; + + postMidPointTracer.current.position.set(0, t2 * preMidPointGap!, 0); + postMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t2 - 0.5), 1); + }); + + if (!shot) return null; return ( <> - {shot?.layers.map((layer, index) => { + + + + + + + + + + + + + {midPointLayer.status === 'ricochet' && ( + + + + + + + + + + + )} + + {shot.layers.map((layer, index) => { if (layer.type === null) return; - const shellRotation = new Euler().setFromQuaternion( - new Quaternion().setFromAxisAngle( - new Vector3().crossVectors(J_HAT, layer.shellNormal).normalize(), - J_HAT.angleTo(layer.shellNormal), - ), - ); + const shellRotation = normalToEuler(layer.shellNormal); const nextLayer = shot.layers[index + 1] as ShotLayerGap | undefined; const trueStatus = nextLayer?.status === 'blocked' ? 'blocked' : layer.status; @@ -107,171 +178,97 @@ export function ShotDisplay() { - - {(() => { - let length = LENGTH_INFINITY; - - if (layer.index !== 0) { - const gap = shot.layers[index - 1] as ShotLayerGap; - length = gap.distance; - } - - return ( - - - - - ); - })()}
- - {(() => { - if ( - layer.status !== 'ricochet' || - index !== shot.layers.length - 1 - ) - return null; - const ricochetShellNormal = layer.shellNormal - .clone() - .reflect(layer.surfaceNormal) - .multiplyScalar(-1); - const ricochetShellRotation = new Euler().setFromQuaternion( - new Quaternion().setFromAxisAngle( - new Vector3() - .crossVectors(J_HAT, ricochetShellNormal) - .normalize(), - J_HAT.angleTo(ricochetShellNormal), - ), - ); - - return ( - - - - - - - ); - })()} ); })} - {shot && - (() => { - const firstLayer = shot.layers[0] as Exclude; - const lastLayer = shot.layers.at(-1)!; - const midPointLayer = (shot.layers.findLast( - (layer) => layer.type !== null && layer.status === 'ricochet', - ) ?? - shot.layers.findLast( - (layer) => layer.type !== null, - )) as ShotLayerBase; - const distanceFromSpacedArmor = firstLayer.point.distanceTo( - shot.point, - ); + + + + + {lastLayer.status[0].toUpperCase()} + {lastLayer.status.slice(1)} + {lastLayer.status === 'penetration' && + `: ${Math.round(shell.damage.armor)}hp`} + {lastLayer.status === 'blocked' && + shell.type === 'he' && + `: ${NaNFallback( + Math.max( + 0, + Math.round( + 0.5 * + shell.damage.armor * + (1 - + distanceFromSpacedArmor! / shell.explosionRadius!) - + 1.1 * + (midPointLayer as ShotLayerNonExternal) + .thicknessAngled, + ), + ), + 0, + )}hp`} + - console.log(distanceFromSpacedArmor); + {shot.layers.map((layer) => { + if (layer.type === null && !explosive) return null; - return ( - - - - - {lastLayer.status[0].toUpperCase()} - {lastLayer.status.slice(1)} - {lastLayer.status === 'penetration' && - `: ${Math.round(shell.damage.armor)}hp`} - {lastLayer.status === 'blocked' && - shell.type === 'he' && - `: ${NaNFallback( - Math.max( - 0, - Math.round( - 0.5 * - shell.damage.armor * - (1 - - distanceFromSpacedArmor / - shell.explosionRadius!) - - 1.1 * - (midPointLayer as ShotLayerNonExternal) - .thicknessAngled, - ), - ), - 0, - )}hp`} - + return ( + + + {hasMultipleLayers && ( + + {layer.type === null ? null : `${layer.index + 1}.`} + + )} + {layerTypeNames[`${layer.type}`]} + - {shot.layers.map((layer) => { - if (layer.type === null && !explosive) return null; + {layer.type !== null && ( + <> + + + {Math.round(layer.thickness)}mm + - return ( - - - {hasMultipleLayers && ( - - {layer.type === null - ? null - : `${layer.index + 1}.`} - + {layer.type !== ArmorType.External && ( + + {layer.status === 'ricochet' ? ( + + ) : layer.status === 'blocked' ? ( + + ) : ( + )} - {layerTypeNames[`${layer.type}`]} + {Math.round(layer.thicknessAngled)}mm ( + {Math.round(radToDeg(layer.angle))}°) + )} + + )} - {layer.type !== null && ( - <> - - - - {Math.round(layer.thickness)}mm - - - - {layer.type !== ArmorType.External && ( - - {layer.status === 'ricochet' ? ( - - ) : layer.status === 'blocked' ? ( - - ) : ( - - )} - - {Math.round(layer.thicknessAngled)}mm ( - {Math.round(radToDeg(layer.angle))}°) - - - )} - - )} - - {layer.type === null && ( - - {/* why is it x50? i can't pretend to understand this shit */} - {Math.round(layer.distance * 1000)}mm (- - {Math.round(Math.min(100, layer.distance * 50))}% - penetration) - - )} - - ); - })} + {layer.type === null && ( + + {Math.round(layer.distance * 1000)}mm (- + {Math.round(Math.min(100, layer.distance * 50))}% + penetration) + + )} - - - ); - })()} + ); + })} + + + ); } diff --git a/src/core/math/normalToEuler.ts b/src/core/math/normalToEuler.ts new file mode 100644 index 000000000..b3b005e96 --- /dev/null +++ b/src/core/math/normalToEuler.ts @@ -0,0 +1,14 @@ +import { Euler, Quaternion, Vector3 } from 'three'; +import { J_HAT } from '../../constants/axis'; + +const quaternion = new Quaternion(); +const axis = new Vector3(); + +export function normalToEuler(normal: Vector3, up = J_HAT) { + return new Euler().setFromQuaternion( + quaternion.setFromAxisAngle( + axis.crossVectors(up, normal).normalize(), + up.angleTo(normal), + ), + ); +} From d496b46b6fd706b200280c9160858dd910dad66c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:30:53 -0600 Subject: [PATCH 27/32] fix: post mid point tracer --- src/app/tools/tankopedia/[id]/page.tsx | 2 ++ .../Armor/components/ShotDisplay.tsx | 25 ++++++++++++++----- .../SpacedArmorSceneComponent/index.tsx | 5 +++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/app/tools/tankopedia/[id]/page.tsx b/src/app/tools/tankopedia/[id]/page.tsx index 0251a97c6..2a632f1ae 100644 --- a/src/app/tools/tankopedia/[id]/page.tsx +++ b/src/app/tools/tankopedia/[id]/page.tsx @@ -63,6 +63,8 @@ export default function Page({ params }: { params: { id: string } }) { }; }, [initialId, assigned]); + + return ( {assigned && ( diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index 3d6bc4611..c7a404a19 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -29,8 +29,8 @@ const layerTypeNames: Record = { }; const LENGTH_INFINITY = 4; -const TRACER_THIN = 1 / 128; const TRACER_THICK = 1 / 64; +const TRACER_THIN = TRACER_THICK / 2; export function ShotDisplay() { const shot = useTankopediaTemporary((state) => state.shot); @@ -53,12 +53,22 @@ export function ShotDisplay() { 0, shot.layers.indexOf(midPointLayer as ShotLayer), ); + const postMidPointLayers = shot?.layers.slice( + shot.layers.indexOf(midPointLayer as ShotLayer), + shot.layers.length, + ); const preMidPointGap = shot ? preMidPointLayers!.reduce((accumulator, layer) => { if (layer.type === null) return accumulator + layer.distance; return accumulator; }, 0) + LENGTH_INFINITY : undefined; + const postMidPointGap = shot + ? postMidPointLayers!.reduce((accumulator, layer) => { + if (layer.type === null) return accumulator + layer.distance; + return accumulator; + }, 0) || LENGTH_INFINITY + : undefined; useFrame(({ clock }) => { if (!shot || !preMidPointTracer.current) return; @@ -71,7 +81,7 @@ export function ShotDisplay() { if (!postMidPointTracer.current) return; - postMidPointTracer.current.position.set(0, t2 * preMidPointGap!, 0); + postMidPointTracer.current.position.set(0, t2 * postMidPointGap!, 0); postMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t2 - 0.5), 1); }); @@ -114,15 +124,18 @@ export function ShotDisplay() { .multiplyScalar(-1), )} > - + - + diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index 426d17479..85ad7dcde 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -243,7 +243,6 @@ export function SpacedArmorSceneComponent({ thickness === 0 ? 0 : thickness / Math.cos(angle - finalNormalization); - remainingPenetration -= finalThickness; if (!threeCalibersRule && angle >= ricochet) { shot.layers.push({ @@ -279,7 +278,9 @@ export function SpacedArmorSceneComponent({ break; } else { + remainingPenetration -= finalThickness; const blocked = remainingPenetration < 0; + shot.layers.push({ type, index: layerIndex, @@ -299,6 +300,8 @@ export function SpacedArmorSceneComponent({ } } + console.log(shot); + useTankopediaTemporary.setState({ shot }); }, })} From 90ec5fa80b2ae48c05e48983eb342492a8c3a349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:37:04 -0600 Subject: [PATCH 28/32] fix: a few issues --- src/components/Armor/components/ShotDisplay.tsx | 4 ++-- .../Armor/components/SpacedArmorSceneComponent/index.tsx | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index c7a404a19..88c30e7dd 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -230,11 +230,11 @@ export function ShotDisplay() { )}hp`} - {shot.layers.map((layer) => { + {shot.layers.map((layer, index) => { if (layer.type === null && !explosive) return null; return ( - + {hasMultipleLayers && ( diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index 85ad7dcde..1d7cc9685 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -300,8 +300,6 @@ export function SpacedArmorSceneComponent({ } } - console.log(shot); - useTankopediaTemporary.setState({ shot }); }, })} From c626e84f7dc67e1dd89ec78658c65b7fc390f0fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:38:57 -0600 Subject: [PATCH 29/32] feat: no depth read core tracer --- src/components/Armor/components/ShotDisplay.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index 88c30e7dd..eb2d692cf 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -95,7 +95,7 @@ export function ShotDisplay() { > - + @@ -128,6 +128,7 @@ export function ShotDisplay() { + Date: Thu, 29 Feb 2024 15:54:15 -0600 Subject: [PATCH 30/32] fix: cos domain --- .../tools/tankopedia/[id]/components/Model/TankSandbox.tsx | 1 - .../CoreArmorSceneComponent/shaders/fragment.glsl | 2 +- .../components/SpacedArmorSubSpaced/shaders/fragment.glsl | 2 +- .../Armor/components/SpacedArmorSceneComponent/index.tsx | 6 ++---- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx index e7e9744a6..3e077f02b 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -197,7 +197,6 @@ export function TankSandbox() { }> {/* TODO: add rotation cube to the top right */} - {/* */} diff --git a/src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl b/src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl index b5b6f4a50..e03696067 100644 --- a/src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl +++ b/src/components/Armor/components/CoreArmorSceneComponent/shaders/fragment.glsl @@ -40,7 +40,7 @@ void main() { } else { bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0; float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization; - float finalThickness = thickness / cos(angle - finalNormalization); + float finalThickness = thickness / cos(max(0.0, angle - finalNormalization)); float remainingPenetration = penetration; // world space is in meters diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/shaders/fragment.glsl b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/shaders/fragment.glsl index 04057b4d7..7d486d5e8 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/shaders/fragment.glsl +++ b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/shaders/fragment.glsl @@ -18,7 +18,7 @@ void main() { bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0; float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization; - float finalThickness = thickness / cos(angle - finalNormalization); + float finalThickness = thickness / cos(max(0.0, angle - finalNormalization)); gl_FragColor = vec4(finalThickness / penetration, 0.0, 0.0, 1.0); } \ No newline at end of file diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index 1d7cc9685..5bed3de2c 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -73,7 +73,6 @@ export function SpacedArmorSceneComponent({ ...props }: SpacedArmorSceneComponentProps) { const camera = useThree((state) => state.camera); - // const cameraNormal = new Vector3(); return ( <> @@ -107,8 +106,6 @@ export function SpacedArmorSceneComponent({ point: event.point, }; // TODO: type this let remainingPenetration = penetration; - let penetrationChance = -1; - let splashChance = -1; const shellNormal = camera.position .clone() @@ -242,7 +239,8 @@ export function SpacedArmorSceneComponent({ const finalThickness = thickness === 0 ? 0 - : thickness / Math.cos(angle - finalNormalization); + : thickness / + Math.cos(Math.max(0, angle - finalNormalization)); if (!threeCalibersRule && angle >= ricochet) { shot.layers.push({ From 4c0f7b61e4c1403e78b68d8876aeb879d5f17574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:58:17 -0600 Subject: [PATCH 31/32] fix: reset shot after tank change --- .../Armor/components/ShotDisplay.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index eb2d692cf..6b470feb4 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -1,7 +1,7 @@ import { Card, Flex, Inset, Text } from '@radix-ui/themes'; import { Html } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; -import { useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { Mesh } from 'three'; import { radToDeg } from 'three/src/math/MathUtils'; import { isExplosive } from '../../../core/blitz/isExplosive'; @@ -17,6 +17,7 @@ import { ShotLayerBase, ShotLayerGap, ShotLayerNonExternal, + mutateTankopediaTemporary, useTankopediaTemporary, } from '../../../stores/tankopedia'; import { ArmorType } from './SpacedArmorScene'; @@ -85,6 +86,23 @@ export function ShotDisplay() { postMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t2 - 0.5), 1); }); + useEffect(() => { + function resetShot() { + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); + } + + const unsubscribes = [ + useDuel.subscribe((state) => state.protagonist?.tank, resetShot), + useDuel.subscribe((state) => state.antagonist?.tank, resetShot), + ]; + + return () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()); + }; + }); + if (!shot) return null; return ( From 54798c30b19ec8f30c6becea64957cd44cfb6167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:05:43 -0600 Subject: [PATCH 32/32] feat: changelogs --- docs/changelogs/1.16.1.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/changelogs/1.16.1.md b/docs/changelogs/1.16.1.md index bb1f0ed7a..ed1de5eca 100644 --- a/docs/changelogs/1.16.1.md +++ b/docs/changelogs/1.16.1.md @@ -1,3 +1,28 @@ # Blitzkrieg 1.16.1 -Armor sandbox gains 25% rendering speed and 40% memory efficiency. +Armor sandbox gains 25% rendering speed and 40% memory efficiency with added accuracy and features. + +## Features + +- Multiple layers of spaced armors are now calculated properly by the visual armor highlighting +- Added opaque mode to view armor highlighting without any transparency +- Added breakdown on layers of armor penetrated + - Penetration of each layer is shown represented by a color and an index + - Green: penetration + - Red: blocked + - Yellow: ricochet + - This also shows layers encountered after a ricochet + +## Fixes + +- Accuracy now doesn't drop the further the shot is placed from the origin +- HE shells no longer loose penetration like how HEAT shells do when traveling between layers of armor +- Persistent settings are now merged in the correct order making them truly persistent now +- Enhanced armor and calibrated shells are now properly synced from the get go + +## Technical Changes + +- Spaced armor and external module scenes have been merged + - They now use additive blending + - Colors are now normalized by the penetration value instead of maximum armor plate thickness +- Shell normalization now doesn't violate arccosine's domain