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