mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: heat shells
This commit is contained in:
1 parent
06c878dd2c
commit
522b40aff1
11 files changed
+183
-90
No files matched your search
Vendored
+3
-1
@@ -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"]
|
||||
}
|
||||
@@ -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],
|
||||
|
||||
+27
-15
@@ -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],
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user