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