feat: core armor

This commit is contained in:
TrèsAbhi committed 2024-02-27 11:52:36 -06:00
1 parent fabf649608
commit 2689568650
14 files changed
+430 -24

No files matched your search

@@ -4,6 +4,7 @@ import { PerspectiveCamera } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
import { Suspense, use, useEffect, useRef, useState } from 'react';
import { isMobile } from 'react-device-detect';
import { Armor } from '../../../../../../components/Armor';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
@@ -18,10 +19,12 @@ import mutateTankopediaPersistent, {
} from '../../../../../../stores/tankopedia';
import { AntagonistBar } from '../AntagonistBar';
import { Controls } from '../Control';
import { Lighting } from '../Lighting';
import { RotationInputs } from '../QuickInputs';
import { SceneProps } from '../SceneProps';
import { ModelLoader } from './components/ModelLoader';
import { Options } from './components/Options';
import { SpacedArmorScene } from './components/TankArmor/components/Spaced';
import { TankModel } from './components/TankModel';
export function TankSandbox() {
const canvas = useRef<HTMLCanvasElement>(null);
@@ -189,13 +192,16 @@ export function TankSandbox() {
>
<PerspectiveCamera makeDefault fov={25} far={32} />
<Controls />
{/* <SceneProps /> */}
<SceneProps />
<Suspense fallback={<ModelLoader />}>
{/* <ShotDisplay />
*/}
{/* <SpacedArmorScene /> */}
<Lighting />
<TankModel /> */}
<SpacedArmorScene />
<TankModel />
<Armor />
</Suspense>
</Canvas>
</div>
@@ -0,0 +1,71 @@
import { useFrame } from '@react-three/fiber';
import {
MeshBasicMaterial,
Object3D,
ShaderMaterial,
Vector2,
WebGLRenderTarget,
} 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';
export const spacedArmorRenderTarget = new WebGLRenderTarget();
interface CoreArmorProps {
thickness: number;
node: Object3D;
}
const excludeMaterial = new MeshBasicMaterial({
colorWrite: false,
});
export function CoreArmor({ node, thickness }: CoreArmorProps) {
const { shell } = useDuel.getState().antagonist!;
const penetration = resolveNearPenetration(shell.penetration);
const material = new ShaderMaterial({
fragmentShader,
vertexShader,
transparent: true,
uniforms: {
thickness: { value: thickness },
penetration: { value: penetration },
caliber: { value: shell.caliber },
ricochet: { value: degToRad(shell.ricochet ?? 90) },
normalization: { value: degToRad(shell.normalization ?? 0) },
resolution: { value: new Vector2() },
spacedArmorBuffer: { value: null },
spacedArmorDepth: { value: null },
},
});
useFrame(({ gl }) => {
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
gl.getPixelRatio(),
);
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
material.uniforms.spacedArmorDepth.value =
spacedArmorRenderTarget.depthTexture;
});
return (
<>
{jsxTree(node, {
renderOrder: 0,
material: excludeMaterial,
})}
{jsxTree(node, {
renderOrder: 1,
material,
})}
</>
);
}
@@ -0,0 +1,46 @@
varying vec3 vNormal;
varying vec3 vViewPosition;
uniform float thickness;
uniform float penetration;
uniform float caliber;
uniform float ricochet;
uniform float normalization;
uniform vec2 resolution;
uniform sampler2D spacedArmorBuffer;
uniform highp sampler2D spacedArmorDepth;
void main() {
float penetrationChance = -1.0;
float splashChance = -1.0;
vec2 screenCoordinates = gl_FragCoord.xy / resolution;
float angle = acos(dot(vNormal, -vViewPosition) / length(vViewPosition));
vec4 spacedArmorBufferFragment = texture2D(spacedArmorBuffer, screenCoordinates);
bool isUnderSpacedArmor = spacedArmorBufferFragment.a != 0.0;
bool threeCalibersRule = caliber > thickness * 3.0;
if (!threeCalibersRule && !isUnderSpacedArmor && angle >= ricochet) {
penetrationChance = 0.0;
} else {
bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0;
float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization;
float finalThickness = thickness / cos(angle - finalNormalization);
float remainingPenetration = penetration;
if (isUnderSpacedArmor) {
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration;
remainingPenetration -= spacedArmorThickness + finalThickness;
} else {
remainingPenetration -= finalThickness;
}
penetrationChance = remainingPenetration < 0.0 ? 0.0 : 1.0;
}
vec3 color = vec3(1.0, splashChance * 0.392, 0.0);
gl_FragColor = vec4(color, (1.0 - penetrationChance) * 0.5);
}
@@ -0,0 +1,226 @@
import { memo, useEffect, useRef } from 'react';
import { Euler, Group, Vector3 } from 'three';
import { degToRad } from 'three/src/math/MathUtils';
import { I_HAT, J_HAT, K_HAT } from '../../../constants/axis';
import {
ModelTransformEventData,
modelTransformEvent,
} from '../../../core/blitzkrieg/modelTransform';
import { nameToArmorId } from '../../../core/blitzkrieg/nameToArmorId';
import { resolveArmor } from '../../../core/blitzkrieg/resolveThickness';
import { useArmor } from '../../../hooks/useArmor';
import { useModel } from '../../../hooks/useModel';
import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
import { useDuel } from '../../../stores/duel';
import { useTankopediaPersistent } from '../../../stores/tankopedia';
import { CoreArmor } from './CoreArmor';
interface CoreArmorSceneProps {
ornamental?: boolean;
}
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);
const gunContainer = useRef<Group>(null);
const initialTankopediaState = useTankopediaPersistent.getState();
useEffect(() => {
if (!modelDefinitions) return;
const hullOrigin = new Vector3(
trackModelDefinition.origin[0],
trackModelDefinition.origin[1],
-trackModelDefinition.origin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(I_HAT, 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(hullOrigin)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(I_HAT, pitch)
.add(gunOrigin)
.add(turretOrigin)
.add(hullOrigin);
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(hullOrigin)
.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(I_HAT, initialPitch)
.applyAxisAngle(J_HAT, initialRoll)
.applyAxisAngle(K_HAT, initialYaw);
turretRotation.x += initialPitch;
turretRotation.y += initialRoll;
turretRotation.z += initialYaw;
}
turretPosition.add(turretOrigin).add(hullOrigin);
turretContainer.current?.position.copy(turretPosition);
turretContainer.current?.rotation.copy(turretRotation);
}
handleModelTransform(protagonist);
modelTransformEvent.on(handleModelTransform);
return () => {
modelTransformEvent.off(handleModelTransform);
};
});
useEffect(() => {
const unsubscribe = useTankopediaPersistent.subscribe(
(state) => state.mode,
(mode) => {
if (wrapper.current) wrapper.current.visible = mode === 'armor';
},
);
return unsubscribe;
});
const armorGltf = useArmor(protagonist.tank.id);
const { gltf: modelGltf } = useModel(protagonist.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 hullOrigin = new Vector3(
trackModelDefinition.origin[0],
trackModelDefinition.origin[1],
-trackModelDefinition.origin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(I_HAT, Math.PI / 2);
return (
<group
ref={wrapper}
rotation={[-Math.PI / 2, 0, 0]}
visible={initialTankopediaState.mode === 'armor'}
>
<group position={hullOrigin}>
{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 || spaced || thickness === undefined) return null;
return (
<CoreArmor key={node.uuid} thickness={thickness} node={node} />
);
})}
</group>
<group ref={turretContainer}>
<group position={hullOrigin}>
{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 || spaced || thickness === undefined) return null;
return (
<group key={node.uuid} position={turretOrigin}>
<CoreArmor
key={node.uuid}
thickness={thickness}
node={node}
/>
</group>
);
})}
</group>
<group ref={gunContainer}>
<group position={hullOrigin}>
{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 || spaced || thickness === undefined)
return null;
return (
<group
key={node.uuid}
position={turretOrigin.clone().add(gunOrigin)}
>
<CoreArmor thickness={thickness} node={node} />
</group>
);
})}
</group>
</group>
</group>
</group>
);
},
);
@@ -7,8 +7,8 @@ import {
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree';
import { useDuel } from '../../../../../../stores/duel';
import fragmentShader from './fragment.glsl';
import vertexShader from './vertex.glsl';
import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl';
interface SpacedArmorSubExternalProps {
node: Object3D;
@@ -3,8 +3,8 @@ 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 './fragment.glsl';
import vertexShader from './vertex.glsl';
import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl';
interface SpacedArmorSubSpacedProps {
node: Object3D;
@@ -18,8 +18,6 @@ export function SpacedArmorSubSpaced({
const { shell } = useDuel.getState().antagonist!;
const penetration = resolveNearPenetration(shell.penetration);
console.log(shell.ricochet);
return jsxTree(node, {
renderOrder: 2,
material: new ShaderMaterial({
@@ -0,0 +1,11 @@
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0);
vViewPosition = vViewPosition4.xyz;
vNormal = normalMatrix * normal;
gl_Position = projectionMatrix * vViewPosition4;
}
@@ -1,13 +1,10 @@
import { MeshBasicMaterial, Object3D } from 'three';
import { ArmorType } from '../../../../app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/Spaced';
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
import { jsxTree } from '../../../../core/blitzkrieg/jsxTree';
import { useDuel } from '../../../../stores/duel';
import { ArmorType } from '../SpacedArmorScene';
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
// import fragmentShader from './shaders/fragment.glsl';
// import vertexShader from './shaders/vertex.glsl';
interface SpacedArmorProps {
node: Object3D;
ornamental?: boolean;
@@ -1,19 +1,19 @@
import { memo, useEffect, useRef } from 'react';
import { Euler, Group, Vector3 } from 'three';
import { degToRad } from 'three/src/math/MathUtils';
import { SpacedArmor } from '../../../../../../../../../components/Armor/components/SpacedArmor';
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis';
import { I_HAT, J_HAT, K_HAT } from '../../../constants/axis';
import {
ModelTransformEventData,
modelTransformEvent,
} from '../../../../../../../../../core/blitzkrieg/modelTransform';
import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId';
import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness';
import { useArmor } from '../../../../../../../../../hooks/useArmor';
import { useModel } from '../../../../../../../../../hooks/useModel';
import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions';
import { useDuel } from '../../../../../../../../../stores/duel';
import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia';
} from '../../../core/blitzkrieg/modelTransform';
import { nameToArmorId } from '../../../core/blitzkrieg/nameToArmorId';
import { resolveArmor } from '../../../core/blitzkrieg/resolveThickness';
import { useArmor } from '../../../hooks/useArmor';
import { useModel } from '../../../hooks/useModel';
import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
import { useDuel } from '../../../stores/duel';
import { useTankopediaPersistent } from '../../../stores/tankopedia';
import { SpacedArmor } from './SpacedArmor';
export enum ArmorType {
Core,
+51
View File
@@ -0,0 +1,51 @@
import { createPortal, useFrame } from '@react-three/fiber';
import { DepthTexture, Scene, Vector2 } from 'three';
import { spacedArmorRenderTarget } from './components/CoreArmor';
import { CoreArmorScene } from './components/CoreArmorScene';
import { SpacedArmorScene } from './components/SpacedArmorScene';
export function Armor() {
const spacedArmorScene = new Scene();
const coreArmorScene = new Scene();
const spacedArmorPortal = createPortal(
<SpacedArmorScene />,
spacedArmorScene,
);
const coreArmorPortal = createPortal(<CoreArmorScene />, coreArmorScene);
const renderSize = new Vector2();
const newRenderSize = new Vector2();
useFrame(({ gl, camera, scene }) => {
gl.autoClear = true;
gl.getSize(newRenderSize).multiplyScalar(gl.getPixelRatio());
if (!newRenderSize.equals(renderSize)) {
renderSize.copy(newRenderSize);
spacedArmorRenderTarget.depthTexture = new DepthTexture(
renderSize.x,
renderSize.y,
);
}
spacedArmorRenderTarget.setSize(renderSize.x, renderSize.y);
gl.setRenderTarget(spacedArmorRenderTarget);
gl.render(spacedArmorScene, camera);
gl.autoClear = false;
gl.setRenderTarget(null);
gl.render(scene, camera);
gl.clearDepth();
gl.render(coreArmorScene, camera);
}, 1);
return (
<>
{spacedArmorPortal}
{coreArmorPortal}
</>
);
}