feat: heat shells

This commit is contained in:
TrèsAbhi committed 2024-02-27 17:15:24 -06:00
1 parent 06c878dd2c
commit 522b40aff1
11 files changed
+183 -90

No files matched your search

@@ -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,
@@ -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);
}
@@ -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;
@@ -20,7 +20,6 @@ interface CoreArmorSceneProps {
export const CoreArmorScene = memo<CoreArmorSceneProps>(
({ ornamental = false }) => {
const protagonist = useDuel((state) => state.protagonist!);
const wrapper = useRef<Group>(null);
const modelDefinitions = useModelDefinitions();
const turretContainer = useRef<Group>(null);
@@ -96,7 +95,7 @@ export const CoreArmorScene = memo<CoreArmorSceneProps>(
turretContainer.current?.rotation.copy(turretRotation);
}
handleModelTransform(protagonist);
handleModelTransform(useDuel.getState().protagonist!);
modelTransformEvent.on(handleModelTransform);
return () => {
@@ -115,15 +114,16 @@ export const CoreArmorScene = memo<CoreArmorSceneProps>(
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],
@@ -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,
})}
</>
);
@@ -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) },
},
}),
});
}
@@ -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,
})}
</>
);
}
@@ -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 (
@@ -27,7 +27,6 @@ interface SpacedArmorSceneProps {
export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
({ ornamental = false }) => {
const protagonist = useDuel((state) => state.protagonist!);
const wrapper = useRef<Group>(null);
const modelDefinitions = useModelDefinitions();
const turretContainer = useRef<Group>(null);
@@ -103,7 +102,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
turretContainer.current?.rotation.copy(turretRotation);
}
handleModelTransform(protagonist);
handleModelTransform(useDuel.getState().protagonist!);
modelTransformEvent.on(handleModelTransform);
return () => {
@@ -122,17 +121,18 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
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],
+3 -2
View File
@@ -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}
</>
);
}
});