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; -}