diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx
index dc876c9d6..86972f3de 100644
--- a/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx
+++ b/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx
@@ -115,7 +115,6 @@ export function TankDisplay({ duel }: TankDisplayProps) {
ref={canvas}
camera={{ fov: 20 }}
onPointerDown={handlePointerDown}
- gl={{ logarithmicDepthBuffer: true }}
>
diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx
new file mode 100644
index 000000000..7c36be823
--- /dev/null
+++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx
@@ -0,0 +1,263 @@
+import { useLoader } from '@react-three/fiber';
+import { memo, useEffect, useRef } from 'react';
+import { Euler, Group, Mesh, Vector3 } from 'three';
+import { GLTFLoader } from 'three-stdlib';
+import { degToRad } from 'three/src/math/MathUtils';
+import { ArmorMeshSpacedArmorDepth } from '../../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth';
+import {
+ X_AXIS,
+ Y_AXIS,
+ Z_AXIS,
+} from '../../../../../../../../../constants/axis';
+import { asset } from '../../../../../../../../../core/blitzkrieg/asset';
+import {
+ ModelTransformEventData,
+ modelTransformEvent,
+} from '../../../../../../../../../core/blitzkrieg/modelTransform';
+import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId';
+import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness';
+import { useModelDefinitions } from '../../../../../../../../../core/hooks/useModelDefinitions';
+import { useTankopedia } from '../../../../../../../../../stores/tankopedia';
+import { Duel } from '../../../../../page';
+
+interface SpacedArmorDepthProps {
+ duel: Duel;
+}
+
+export const SpacedArmorDepth = memo(({ duel }) => {
+ const wrapper = useRef(null);
+ const awaitedModelDefinitions = useModelDefinitions();
+ const showSpacedArmor = useTankopedia(
+ (state) => state.model.visual.showSpacedArmor,
+ );
+ const turretContainer = useRef(null);
+ const gunContainer = useRef(null);
+ const initialTankopediaState = useTankopedia.getState();
+
+ useEffect(() => {
+ if (!awaitedModelDefinitions) return;
+
+ const turretOrigin = new Vector3(
+ tankModelDefinition.turretOrigin[0],
+ tankModelDefinition.turretOrigin[1],
+ -tankModelDefinition.turretOrigin[2],
+ ).applyAxisAngle(X_AXIS, Math.PI / 2);
+ const gunOrigin = new Vector3(
+ turretModelDefinition.gunOrigin[0],
+ turretModelDefinition.gunOrigin[1],
+ -turretModelDefinition.gunOrigin[2],
+ ).applyAxisAngle(X_AXIS, 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(turretOrigin)
+ .sub(gunOrigin)
+ .applyAxisAngle(X_AXIS, pitch)
+ .add(gunOrigin)
+ .add(turretOrigin);
+ gunRotation.set(pitch, 0, 0);
+ gunContainer.current?.position.copy(gunPosition);
+ gunContainer.current?.rotation.copy(gunRotation);
+
+ if (yaw === undefined) return;
+
+ turretPosition
+ .set(0, 0, 0)
+ .sub(turretOrigin)
+ .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(X_AXIS, initialPitch)
+ .applyAxisAngle(Y_AXIS, initialRoll)
+ .applyAxisAngle(Z_AXIS, initialYaw);
+ turretRotation.x += initialPitch;
+ turretRotation.y += initialRoll;
+ turretRotation.z += initialYaw;
+ }
+
+ turretPosition.add(turretOrigin);
+ turretContainer.current?.position.copy(turretPosition);
+ turretContainer.current?.rotation.copy(turretRotation);
+ }
+
+ modelTransformEvent.on(handleModelTransform);
+
+ return () => {
+ modelTransformEvent.off(handleModelTransform);
+ };
+ }, [awaitedModelDefinitions]);
+
+ useEffect(() => {
+ const unsubscribe = useTankopedia.subscribe(
+ (state) => state.mode,
+ (mode) => {
+ if (wrapper.current) wrapper.current.visible = mode === 'armor';
+ },
+ );
+
+ return unsubscribe;
+ });
+
+ const armorGltf = useLoader(
+ GLTFLoader,
+ asset(`3d/tanks/armor/${duel.protagonist.tank.id}.glb`),
+ );
+ const modelGltf = useLoader(
+ GLTFLoader,
+ asset(`3d/tanks/models/${duel.protagonist.tank.id}.glb`),
+ );
+
+ const armorNodes = Object.values(armorGltf.nodes);
+ const modelNodes = Object.values(modelGltf.nodes);
+ const tankModelDefinition = awaitedModelDefinitions[duel.protagonist.tank.id];
+ const turretModelDefinition =
+ tankModelDefinition.turrets[duel.protagonist.turret.id];
+ const gunModelDefinition =
+ turretModelDefinition.guns[duel.protagonist.gun.id];
+ const turretOrigin = new Vector3(
+ tankModelDefinition.turretOrigin[0],
+ tankModelDefinition.turretOrigin[1],
+ -tankModelDefinition.turretOrigin[2],
+ ).applyAxisAngle(X_AXIS, Math.PI / 2);
+ const gunOrigin = new Vector3(
+ turretModelDefinition.gunOrigin[0],
+ turretModelDefinition.gunOrigin[1],
+ -turretModelDefinition.gunOrigin[2],
+ ).applyAxisAngle(X_AXIS, 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 ||
+ (spaced && !showSpacedArmor)
+ )
+ 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 || !showSpacedArmor) return null;
+
+ return (
+
+ );
+ })}
+
+
+ {armorNodes.map((node) => {
+ const isCurrentTurret = node.name.startsWith(
+ `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
+ );
+ const isVisible = isCurrentTurret;
+ const armorId = nameToArmorId(node.name);
+ const { spaced, thickness } = resolveArmor(
+ turretModelDefinition.armor,
+ armorId,
+ );
+
+ if (
+ !isVisible ||
+ thickness === undefined ||
+ (spaced && !showSpacedArmor)
+ )
+ return null;
+
+ return (
+
+ );
+ })}
+
+ {armorNodes.map((node) => {
+ const isCurrentGun = node.name.startsWith(
+ `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
+ );
+ const isVisible = isCurrentGun;
+ const armorId = nameToArmorId(node.name);
+ const { spaced, thickness } = resolveArmor(
+ gunModelDefinition.armor,
+ armorId,
+ );
+
+ if (
+ !isVisible ||
+ thickness === undefined ||
+ (spaced && !showSpacedArmor)
+ )
+ return null;
+
+ return (
+
+ );
+ })}
+
+ {modelNodes.map((node) => {
+ const isCurrentGun =
+ node.name ===
+ `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
+ const isVisible = isCurrentGun;
+
+ if (!isVisible || !showSpacedArmor) return null;
+
+ return (
+
+ );
+ })}
+
+
+
+ );
+});
diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx
index 1ed152676..27b3050ea 100644
--- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx
+++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx
@@ -1,10 +1,12 @@
import { createPortal, useFrame } from '@react-three/fiber';
import { memo } from 'react';
-import { Scene } from 'three';
+import { DepthTexture, Scene } from 'three';
import { externalModuleMaskRenderTarget } from '../../../../../../../../components/ArmorMesh';
+import { spacedArmorDepthRenderTarget } from '../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth';
import { Duel } from '../../../../page';
import { ArmorHighlighting } from './components/ArmorHighlighting';
import { ExternalModuleMask } from './components/ExternalModuleMask';
+import { SpacedArmorDepth } from './components/SpacedArmorDepth';
interface TankArmorProps {
duel: Duel;
@@ -12,11 +14,16 @@ interface TankArmorProps {
export const TankArmor = memo(({ duel }) => {
const externalModuleMaskScene = new Scene();
+ const spacedArmorDepthScene = new Scene();
const armorHighlightingScene = new Scene();
const externalModuleMaskPortal = createPortal(
,
externalModuleMaskScene,
);
+ const spacedArmorDepthPortal = createPortal(
+ ,
+ spacedArmorDepthScene,
+ );
const armorHighlightingPortal = createPortal(
,
armorHighlightingScene,
@@ -32,10 +39,25 @@ export const TankArmor = memo(({ duel }) => {
gl.setRenderTarget(externalModuleMaskRenderTarget);
gl.render(externalModuleMaskScene, camera);
+ gl.clearDepth();
+ spacedArmorDepthRenderTarget.setSize(
+ gl.domElement.width,
+ gl.domElement.height,
+ );
+ if (!spacedArmorDepthRenderTarget.depthTexture) {
+ spacedArmorDepthRenderTarget.depthTexture = new DepthTexture(
+ gl.domElement.width,
+ gl.domElement.height,
+ );
+ }
+ gl.setRenderTarget(spacedArmorDepthRenderTarget);
+ gl.render(spacedArmorDepthScene, camera);
+
gl.autoClear = false;
gl.setRenderTarget(null);
gl.render(scene, camera);
+
gl.clearDepth();
gl.render(armorHighlightingScene, camera);
}, 1);
@@ -43,6 +65,7 @@ export const TankArmor = memo(({ duel }) => {
return (
<>
{externalModuleMaskPortal}
+ {spacedArmorDepthPortal}
{armorHighlightingPortal}
>
);
diff --git a/src/components/ArmorMesh/components/Armor/index.tsx b/src/components/ArmorMesh/components/Armor/index.tsx
index a716efe86..93fd1ca57 100644
--- a/src/components/ArmorMesh/components/Armor/index.tsx
+++ b/src/components/ArmorMesh/components/Armor/index.tsx
@@ -1,4 +1,4 @@
-import { MeshProps, useFrame } from '@react-three/fiber';
+import { MeshProps, useFrame, useThree } from '@react-three/fiber';
import { useRef } from 'react';
import { MeshStandardMaterial, ShaderMaterial, Vector2 } from 'three';
import ThreeCustomShaderMaterial from 'three-custom-shader-material';
@@ -8,6 +8,7 @@ import { canSplash } from '../../../../core/blitz/canSplash';
import { isExplosive } from '../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
import { externalModuleMaskRenderTarget } from '../ExternalModuleMask';
+import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth';
import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl';
@@ -25,18 +26,23 @@ export function ArmorMesh({
duel,
...props
}: ArmorMeshProps) {
+ const camera = useThree((state) => state.camera);
const { shell } = duel.antagonist;
const material = useRef(null);
const resolution = new Vector2();
- useFrame(({ gl }) => {
+ useFrame(({ gl, camera }) => {
if (material.current) {
- material.current.uniforms.externalModuleMask.value =
- externalModuleMaskRenderTarget.texture;
material.current.uniforms.resolution.value = resolution.set(
gl.domElement.width,
gl.domElement.height,
);
+ material.current.uniforms.externalModuleMask.value =
+ externalModuleMaskRenderTarget.texture;
+ material.current.uniforms.spacedArmorDepth.value =
+ spacedArmorDepthRenderTarget.depthTexture;
+ material.current.uniforms.projectionMatrixInverse.value =
+ camera.projectionMatrixInverse;
}
});
@@ -56,8 +62,12 @@ export function ArmorMesh({
fragmentShader={fragmentShader}
vertexShader={vertexShader}
uniforms={{
- externalModuleMask: { value: null },
resolution: { value: null },
+ externalModuleMask: { value: null },
+ spacedArmorDepth: { value: null },
+ projectionMatrixInverse: { value: null },
+ zNear: { value: camera.near },
+ zFar: { value: camera.far },
isSpaced: { value: isSpaced },
isExplosive: { value: isExplosive(shell.type) },
canSplash: { value: canSplash(shell.type) },
@@ -73,23 +83,6 @@ export function ArmorMesh({
},
}}
/>
- {/* */}
>
);
diff --git a/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl b/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl
index f3946b65b..41e4b8159 100644
--- a/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl
+++ b/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl
@@ -9,10 +9,27 @@ uniform float caliber;
uniform float ricochet;
uniform float normalization;
uniform sampler2D externalModuleMask;
+uniform sampler2D spacedArmorDepth;
uniform vec2 resolution;
+varying mat4 vProjectionMatrix;
+
+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;
+}
void main() {
- vec4 externalModuleMaskColor = texture2D(externalModuleMask, gl_FragCoord.xy / resolution);
+ vec2 screenCoordinates = gl_FragCoord.xy / resolution;
+ float spacedArmorDepthValue = texture2D(spacedArmorDepth, screenCoordinates).r;
+ float currentDistance = depthToDistance(gl_FragCoord.z);
+ float spacedArmorDistance = depthToDistance(spacedArmorDepthValue);
+ float distanceFromSpacedArmor = currentDistance - spacedArmorDistance;
+ vec4 externalModuleMaskColor = texture2D(externalModuleMask, screenCoordinates);
bool isUnderExternalModule = externalModuleMaskColor.r == 1.0;
vec3 normalizedNormal = normalize(vNormal);
vec3 normalizedViewPosition = normalize(vCSMViewPosition);
@@ -27,11 +44,25 @@ void main() {
penetrationChance = 0.0;
splashChance = 0.0;
} else {
+ float piercedPenetration = penetration;
+
+ // if (isUnderExternalModule) {
+ // // TODO: ADD
+ // piercedPenetration -= 50.0;
+ // }
+
+ if (isExplosive) {
+ // 50% loss of remaining penetration per meter
+ piercedPenetration -= 0.5 * piercedPenetration * distanceFromSpacedArmor;
+ }
+
+ piercedPenetration = max(piercedPenetration, 0.0);
+
bool twoCalibersRule = caliber > 2.0 * thickness;
float finalNormalization = twoCalibersRule ? (normalization * 1.4 * caliber) / (2.0 * thickness) : normalization;
float finalThickness = thickness / cos(angle - finalNormalization);
- float delta = finalThickness - penetration;
- float randomRadius = penetration * 0.05;
+ float delta = finalThickness - piercedPenetration;
+ float randomRadius = piercedPenetration * 0.05;
if (delta > randomRadius) {
penetrationChance = 0.0;
@@ -43,7 +74,7 @@ void main() {
if (canSplash) {
float reducedFinalThickness = finalThickness * (5.0 / 11.0);
- float splashDelta = reducedFinalThickness - penetration;
+ float splashDelta = reducedFinalThickness - piercedPenetration;
if (splashDelta > randomRadius) {
splashChance = 0.0;
@@ -58,15 +89,17 @@ void main() {
}
if (isSpaced) {
- if (isExternalModule) {
- csm_FragColor = vec4(0.5, 0.0, 1.0, 0.5);
- } else {
- if (isExplosive) {
- csm_FragColor = vec4(1.0, 0.0, 1.0, 0.5);
- } else {
- csm_FragColor = vec4(1.0, 0.0, 1.0, (1.0 - penetrationChance) * 0.5);
- }
- }
+ // if (isExternalModule) {
+ // csm_FragColor = vec4(0.5, 0.0, 1.0, 0.5);
+ // } else {
+ // if (isExplosive) {
+ // csm_FragColor = vec4(1.0, 0.0, 1.0, 0.5);
+ // } else {
+ // csm_FragColor = vec4(1.0, 0.0, 1.0, (1.0 - penetrationChance) * 0.5);
+ // }
+ // }
+
+ csm_FragColor = vec4(0.0);
} else {
csm_FragColor = vec4(1.0, splashChance * 0.392, 0.0, (1.0 - penetrationChance) * 0.5);
}
diff --git a/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl b/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl
index afaeda2c0..f86d379c4 100644
--- a/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl
+++ b/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl
@@ -1,6 +1,12 @@
varying vec3 vCSMViewPosition;
+varying float vZ;
+varying mat4 vProjectionMatrix;
void main() {
vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0);
vCSMViewPosition = vViewPosition4.xyz;
+
+ vZ = -(modelViewMatrix * vec4(position, 1.0)).z;
+
+ vProjectionMatrix = projectionMatrix;
}
\ No newline at end of file
diff --git a/src/components/ArmorMesh/components/SpacedArmorDepth.tsx b/src/components/ArmorMesh/components/SpacedArmorDepth.tsx
new file mode 100644
index 000000000..5b70c6561
--- /dev/null
+++ b/src/components/ArmorMesh/components/SpacedArmorDepth.tsx
@@ -0,0 +1,19 @@
+import { MeshProps } from '@react-three/fiber';
+import { WebGLRenderTarget } from 'three';
+
+export const spacedArmorDepthRenderTarget = new WebGLRenderTarget();
+
+interface ArmorMeshSpacedArmorDepthProps extends MeshProps {
+ exclude?: boolean;
+}
+
+export function ArmorMeshSpacedArmorDepth({
+ exclude = false,
+ ...props
+}: ArmorMeshSpacedArmorDepthProps) {
+ return (
+
+
+
+ );
+}