flatten armor dir

This commit is contained in:
tresabhi committed 2026-09-27 16:46:22 -05:00
1 parent e18d6f58f9
commit e5b44d4eb8
28 files changed
+90 -101

No files matched your search

@@ -0,0 +1,251 @@
import {
canSplash,
isExplosive,
resolvePenetrationCoefficient,
} from "@blitzkit/core";
import { invalidate, useFrame } from "@react-three/fiber";
import type { QuicklimeEvent } from "quicklime";
import { useEffect } from "react";
import {
MeshBasicMaterial,
NormalBlending,
Object3D,
ShaderMaterial,
SubtractiveBlending,
UniformsLib,
UniformsUtils,
Vector2,
} from "three";
import { degToRad } from "three/src/math/MathUtils.js";
import { jsxTree } from "../../core/blitzkit/jsxTree";
import { DuelModule, getDuel } from "../../hooks/useDuel";
import { hasEquipment } from "../../hooks/useEquipment";
import { Tankopedia } from "../../stores/tankopedia";
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
import { transitionEvent } from "../Lighting";
import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from "./shaders/vertex.glsl?raw";
import { spacedArmorRenderTarget } from "./PrimaryArmorRenderTarget";
interface PrimaryArmorSceneComponentProps {
thickness: number;
node: Object3D;
}
const excludeMaterial = new MeshBasicMaterial({
colorWrite: false,
});
const date = new Date();
const isHalloween = date.getMonth() === 9 && date.getDate() === 31;
export function PrimaryArmorSceneComponent({
node,
thickness,
}: PrimaryArmorSceneComponentProps) {
const material = new ShaderMaterial({
fragmentShader,
vertexShader,
fog: true,
transparent: true,
blending: isHalloween ? SubtractiveBlending : NormalBlending,
uniforms: UniformsUtils.merge([
UniformsLib.common,
UniformsLib.fog,
{
thickness: { value: null },
penetration: { value: null },
caliber: { value: null },
ricochet: { value: null },
normalization: { value: null },
isExplosive: { value: null },
canSplash: { value: null },
damage: { value: null },
explosionRadius: { value: null },
greenPenetration: { value: null },
advancedHighlighting: { value: null },
opaque: { value: null },
inverseProjectionMatrix: { value: null },
resolution: { value: new Vector2() },
spacedArmorBuffer: { value: null },
spacedArmorDepth: { value: null },
opacity: { value: 0 },
},
]),
});
useEffect(() => {
function handleShellChange() {
const shell =
Tankopedia.state.customShell ?? getDuel("antagonist", DuelModule.Shell);
material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!,
);
material.uniforms.normalization.value = degToRad(
shell.normalization ?? 0,
);
material.uniforms.isExplosive.value = isExplosive(shell.type);
material.uniforms.canSplash.value = canSplash(shell.type);
material.uniforms.damage.value = shell.armor_damage;
material.uniforms.explosionRadius.value = shell.explosion_radius;
handleProtagonistEquipmentChange(true);
handleAntagonistEquipmentChange(true);
invalidate();
}
function handleGreenPenetrationChange(greenPenetration: boolean) {
material.uniforms.greenPenetration.value = greenPenetration;
}
function handleAdvancedHighlightingChange(advancedHighlighting: boolean) {
material.uniforms.advancedHighlighting.value = advancedHighlighting;
invalidate();
}
function handleOpaqueChange(opaque: boolean) {
material.uniforms.opaque.value = opaque;
}
function handleWireframeChange(wireframe: boolean) {
material.wireframe = wireframe;
}
function handleProtagonistEquipmentChange(noInvalidate = false) {
const equipment = Tankopedia.state.protagonist.equipment;
const protagonist = getDuel("protagonist", DuelModule.Tank);
const hasEnhancedArmor = hasEquipment(
110,
protagonist.equipment_preset,
equipment,
);
const equalizer =
(Tankopedia.state.equalize
? protagonist.equalizer?.armor
: undefined) ?? 1;
material.uniforms.thickness.value =
thickness * (hasEnhancedArmor ? 1.04 : 1) * equalizer;
if (!noInvalidate) invalidate();
}
function handleAntagonistEquipmentChange(noInvalidate = false) {
const equipment = Tankopedia.state.antagonist.equipment;
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ??
getDuel("antagonist", DuelModule.Shell);
const penetration = shell.penetration!.near;
const hasCalibratedShells = hasEquipment(
103,
Duel.state.antagonist.tank.equipment_preset,
equipment,
);
material.uniforms.penetration.value =
penetration *
resolvePenetrationCoefficient(
hasCalibratedShells,
Duel.state.equalize,
shell.type,
Duel.state.antagonist.tank.equalizer,
);
if (!noInvalidate) invalidate();
}
function handleTransitionEvent(event: QuicklimeEvent<number>) {
material.uniforms.opacity.value = event.data;
}
handleShellChange();
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
handleAdvancedHighlightingChange(
TankopediaPersistent.state.advancedHighlighting,
);
handleOpaqueChange(TankopediaPersistent.state.opaque);
handleWireframeChange(TankopediaPersistent.state.wireframe);
handleProtagonistEquipmentChange();
handleAntagonistEquipmentChange();
transitionEvent.on(handleTransitionEvent);
const unsubscribes = [
Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange),
TankopediaPersistent.on(
(state) => state.greenPenetration,
handleGreenPenetrationChange,
),
TankopediaPersistent.on(
(state) => state.advancedHighlighting,
handleAdvancedHighlightingChange,
),
TankopediaPersistent.on((state) => state.opaque, handleOpaqueChange),
Duel.on(
(state) => state.protagonist.equipmentMatrix,
() => handleProtagonistEquipmentChange(),
),
Duel.on(
(state) => state.antagonist.equipmentMatrix,
() => handleAntagonistEquipmentChange(),
),
Duel.on(
(state) => state.equalize,
() => {
handleProtagonistEquipmentChange();
handleAntagonistEquipmentChange();
},
),
() => transitionEvent.off(handleTransitionEvent),
];
return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe());
};
}, []);
useFrame(({ gl, camera }) => {
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
gl.getPixelRatio(),
);
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
material.uniforms.spacedArmorDepth.value =
spacedArmorRenderTarget.depthTexture;
material.uniforms.inverseProjectionMatrix.value =
camera.projectionMatrixInverse;
});
return (
<>
{jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={`${key}-exclude`}
renderOrder={0}
material={excludeMaterial}
/>
);
},
})}
{jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={`${key}-include`}
renderOrder={1}
material={material}
/>
);
},
})}
</>
);
}
@@ -0,0 +1,120 @@
precision mediump float;
varying vec3 vNormal;
varying vec3 vViewPosition;
uniform float opacity;
uniform float thickness;
uniform float penetration;
uniform float caliber;
uniform float ricochet;
uniform float normalization;
uniform vec2 resolution;
uniform bool isExplosive;
uniform bool canSplash;
uniform float damage;
uniform float explosionRadius;
uniform bool greenPenetration;
uniform bool advancedHighlighting;
uniform bool opaque;
uniform mat4 inverseProjectionMatrix;
uniform highp sampler2D spacedArmorBuffer;
uniform highp sampler2D spacedArmorDepth;
const float EXPLOSION_DAMAGE_FACTOR = 1.1;
const float RANDOMIZATION_FACTOR = 0.05;
const float GREEN_VALUE = 0.392;
float getDistance(vec2 coord, float depth) {
vec4 clipPos = vec4(coord * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
vec4 eyePos = inverseProjectionMatrix * clipPos;
return length(eyePos.xyz / eyePos.w);
}
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
if (advancedHighlighting && couldHaveRicochet) {
return vec3(0.0, 1.0, isThreeCalibersRule ? 1.0 : 0.0);
}
return vec3(0.0, 1.0, 0.0);
}
void main() {
#include <clipping_planes_fragment>
vec2 screenCoord = gl_FragCoord.xy / resolution;
vec4 spacedArmorData = texture2D(spacedArmorBuffer, screenCoord);
bool isUnderSpacedArmor = spacedArmorData.a != 0.0;
float viewDistance = length(vViewPosition);
float angle = acos(dot(vNormal, -vViewPosition) / viewDistance);
bool threeCalibersRule = caliber > thickness * 3.0 || isUnderSpacedArmor;
bool couldHaveRicochet = angle >= ricochet;
float penetrationChance = -1.0;
float splashChance = 0.0;
bool didRicochet = false;
if (!threeCalibersRule && couldHaveRicochet) {
penetrationChance = 0.0;
didRicochet = true;
} else {
float thicknessRatio = thickness > 0.0 ? caliber / thickness : 0.0;
bool twoCalibersRule = thicknessRatio > 2.0;
float finalNorm = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization;
float finalThickness = thickness / cos(max(0.0, angle - finalNorm));
float remainingPen = penetration;
if (isUnderSpacedArmor) {
float spacedArmorThickness = spacedArmorData.r * penetration;
remainingPen -= spacedArmorThickness;
if (isExplosive && remainingPen > 0.0) {
float spacedDist = getDistance(screenCoord, texture2D(spacedArmorDepth, screenCoord).r);
float primaryDist = getDistance(screenCoord, gl_FragCoord.z);
float distFromArmor = primaryDist - spacedDist;
if (canSplash) {
float finalDamage = 0.5 * damage * (1.0 - distFromArmor / explosionRadius) - EXPLOSION_DAMAGE_FACTOR * (finalThickness + spacedArmorThickness);
splashChance = step(0.0, finalDamage);
penetrationChance = 0.0;
} else {
remainingPen -= 0.5 * remainingPen * distFromArmor;
}
}
}
if (penetrationChance < 0.0) {
remainingPen = max(0.0, remainingPen);
float delta = finalThickness - remainingPen;
float randomization = remainingPen * RANDOMIZATION_FACTOR;
penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0);
if (canSplash) {
float splashDamage = 0.5 * damage - EXPLOSION_DAMAGE_FACTOR * finalThickness;
splashChance = step(0.0, splashDamage);
}
}
}
float alpha = opaque ? 1.0 : 0.5;
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
if (advancedHighlighting && didRicochet) {
baseColor = vec3(1.0, baseColor.g, 1.0);
}
if (greenPenetration || advancedHighlighting) {
float falling = -penetrationChance * penetrationChance + 1.0;
float gaining = -(penetrationChance - 1.0) * (penetrationChance - 1.0) + 1.0;
vec3 penColor = getPenetrationColor(threeCalibersRule, couldHaveRicochet);
gl_FragColor = vec4(falling * baseColor + gaining * penColor, alpha);
} else {
gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha);
}
gl_FragColor.a *= opacity;
}
@@ -0,0 +1,13 @@
precision mediump float;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vViewPosition = mvPosition.xyz;
vNormal = normalMatrix * normal;
gl_Position = projectionMatrix * mvPosition;
}