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