mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
flatten armor dir
This commit is contained in:
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;
|
||||
}
|
||||
Reference in new issue
Block a user