feat: starting work on on-click armor

This commit is contained in:
TrèsAbhi committed 2024-02-28 17:41:59 -06:00
1 parent 35788657a8
commit 69c388f243
16 files changed
+259 -145

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
# Blitzkrieg 1.16.1 # Blitzkrieg 1.16.1
Rewritten the armor sandbox for a 25%+ performance boost and more accuracy. Armor sandbox gains 25% rendering speed and 40% memory efficiency.
@@ -12,14 +12,9 @@ import { useArmor } from '../../../hooks/useArmor';
import { useModelDefinitions } from '../../../hooks/useModelDefinitions'; import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
import { useDuel } from '../../../stores/duel'; import { useDuel } from '../../../stores/duel';
import { useTankopediaPersistent } from '../../../stores/tankopedia'; import { useTankopediaPersistent } from '../../../stores/tankopedia';
import { CoreArmor } from './CoreArmor'; import { CoreArmorSceneComponent } from './CoreArmorSceneComponent';
interface CoreArmorSceneProps { export const CoreArmorScene = memo(() => {
ornamental?: boolean;
}
export const CoreArmorScene = memo<CoreArmorSceneProps>(
({ ornamental = false }) => {
const wrapper = useRef<Group>(null); const wrapper = useRef<Group>(null);
const modelDefinitions = useModelDefinitions(); const modelDefinitions = useModelDefinitions();
const turretContainer = useRef<Group>(null); const turretContainer = useRef<Group>(null);
@@ -77,9 +72,7 @@ export const CoreArmorScene = memo<CoreArmorSceneProps>(
tankModelDefinition.turretRotation.pitch, tankModelDefinition.turretRotation.pitch,
); );
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw); const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
const initialRoll = -degToRad( const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
tankModelDefinition.turretRotation.roll,
);
turretPosition turretPosition
.applyAxisAngle(I_HAT, initialPitch) .applyAxisAngle(I_HAT, initialPitch)
@@ -159,7 +152,11 @@ export const CoreArmorScene = memo<CoreArmorSceneProps>(
if (!isVisible || spaced || thickness === undefined) return null; if (!isVisible || spaced || thickness === undefined) return null;
return ( return (
<CoreArmor key={node.uuid} thickness={thickness} node={node} /> <CoreArmorSceneComponent
key={node.uuid}
thickness={thickness}
node={node}
/>
); );
})} })}
</group> </group>
@@ -181,7 +178,7 @@ export const CoreArmorScene = memo<CoreArmorSceneProps>(
return ( return (
<group key={node.uuid} position={turretOrigin}> <group key={node.uuid} position={turretOrigin}>
<CoreArmor <CoreArmorSceneComponent
key={node.uuid} key={node.uuid}
thickness={thickness} thickness={thickness}
node={node} node={node}
@@ -203,15 +200,14 @@ export const CoreArmorScene = memo<CoreArmorSceneProps>(
armorId, armorId,
); );
if (!isVisible || spaced || thickness === undefined) if (!isVisible || spaced || thickness === undefined) return null;
return null;
return ( return (
<group <group
key={node.uuid} key={node.uuid}
position={turretOrigin.clone().add(gunOrigin)} position={turretOrigin.clone().add(gunOrigin)}
> >
<CoreArmor thickness={thickness} node={node} /> <CoreArmorSceneComponent thickness={thickness} node={node} />
</group> </group>
); );
})} })}
@@ -220,5 +216,4 @@ export const CoreArmorScene = memo<CoreArmorSceneProps>(
</group> </group>
</group> </group>
); );
}, });
);
@@ -24,7 +24,7 @@ import vertexShader from './shaders/vertex.glsl';
export const spacedArmorRenderTarget = new WebGLRenderTarget(); export const spacedArmorRenderTarget = new WebGLRenderTarget();
interface CoreArmorProps { interface CoreArmorSceneComponentProps {
thickness: number; thickness: number;
node: Object3D; node: Object3D;
} }
@@ -33,7 +33,10 @@ const excludeMaterial = new MeshBasicMaterial({
colorWrite: false, colorWrite: false,
}); });
export function CoreArmor({ node, thickness }: CoreArmorProps) { export function CoreArmorSceneComponent({
node,
thickness,
}: CoreArmorSceneComponentProps) {
const material = new ShaderMaterial({ const material = new ShaderMaterial({
fragmentShader, fragmentShader,
vertexShader, vertexShader,
@@ -51,7 +54,6 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) {
damage: { value: null }, damage: { value: null },
explosionRadius: { value: null }, explosionRadius: { value: null },
greenPenetration: { value: null }, greenPenetration: { value: null },
wireframe: { value: null },
opaque: { value: null }, opaque: { value: null },
resolution: { value: new Vector2() }, resolution: { value: new Vector2() },
@@ -79,7 +81,6 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) {
visual: TankopediaPersistent['model']['visual'], visual: TankopediaPersistent['model']['visual'],
) { ) {
material.uniforms.greenPenetration.value = visual.greenPenetration; material.uniforms.greenPenetration.value = visual.greenPenetration;
material.uniforms.wireframe.value = visual.wireframe;
material.uniforms.opaque.value = visual.opaque || visual.wireframe; material.uniforms.opaque.value = visual.opaque || visual.wireframe;
material.wireframe = visual.wireframe; material.wireframe = visual.wireframe;
} }
@@ -13,7 +13,6 @@ uniform bool canSplash;
uniform float damage; uniform float damage;
uniform float explosionRadius; uniform float explosionRadius;
uniform bool greenPenetration; uniform bool greenPenetration;
uniform bool wireframe;
uniform bool opaque; uniform bool opaque;
uniform highp sampler2D spacedArmorBuffer; uniform highp sampler2D spacedArmorBuffer;
@@ -35,8 +34,8 @@ void main() {
vec4 spacedArmorBufferFragment = texture2D(spacedArmorBuffer, screenCoordinates); vec4 spacedArmorBufferFragment = texture2D(spacedArmorBuffer, screenCoordinates);
bool isUnderSpacedArmor = spacedArmorBufferFragment.a != 0.0; bool isUnderSpacedArmor = spacedArmorBufferFragment.a != 0.0;
bool threeCalibersRule = caliber > thickness * 3.0; bool threeCalibersRule = caliber > thickness * 3.0 || isUnderSpacedArmor;
if (!threeCalibersRule && !isUnderSpacedArmor && angle >= ricochet) { if (!threeCalibersRule && angle >= ricochet) {
penetrationChance = 0.0; penetrationChance = 0.0;
} else { } else {
bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0; bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0;
@@ -1,44 +0,0 @@
import { MeshBasicMaterial, Object3D } from 'three';
import { jsxTree } from '../../../../core/blitzkrieg/jsxTree';
import { ArmorType } from '../SpacedArmorScene';
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
interface SpacedArmorProps {
node: Object3D;
ornamental?: boolean;
type: ArmorType;
thickness: number;
}
const omitMaterial = new MeshBasicMaterial({
colorWrite: false,
depthWrite: true,
});
export function SpacedArmor({ node, type, thickness }: SpacedArmorProps) {
/**
* Render orders allocations:
* - 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 (
<>
{type === ArmorType.Core &&
jsxTree(node, {
renderOrder: 0,
material: omitMaterial,
})}
{type === ArmorType.Spaced && (
<SpacedArmorSubSpaced node={node} thickness={thickness} />
)}
{type === ArmorType.External && (
<SpacedArmorSubExternal node={node} thickness={thickness} />
)}
</>
);
}
@@ -13,7 +13,7 @@ import { useModel } from '../../../hooks/useModel';
import { useModelDefinitions } from '../../../hooks/useModelDefinitions'; import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
import { useDuel } from '../../../stores/duel'; import { useDuel } from '../../../stores/duel';
import { useTankopediaPersistent } from '../../../stores/tankopedia'; import { useTankopediaPersistent } from '../../../stores/tankopedia';
import { SpacedArmor } from './SpacedArmor'; import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent';
export enum ArmorType { export enum ArmorType {
Core, Core,
@@ -21,12 +21,7 @@ export enum ArmorType {
External, External,
} }
interface SpacedArmorSceneProps { export const SpacedArmorScene = memo(() => {
ornamental?: boolean;
}
export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
({ ornamental = false }) => {
const wrapper = useRef<Group>(null); const wrapper = useRef<Group>(null);
const modelDefinitions = useModelDefinitions(); const modelDefinitions = useModelDefinitions();
const turretContainer = useRef<Group>(null); const turretContainer = useRef<Group>(null);
@@ -84,9 +79,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
tankModelDefinition.turretRotation.pitch, tankModelDefinition.turretRotation.pitch,
); );
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw); const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
const initialRoll = -degToRad( const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
tankModelDefinition.turretRotation.roll,
);
turretPosition turretPosition
.applyAxisAngle(I_HAT, initialPitch) .applyAxisAngle(I_HAT, initialPitch)
@@ -168,9 +161,8 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
if (!isVisible || thickness === undefined) return null; if (!isVisible || thickness === undefined) return null;
return ( return (
<SpacedArmor <SpacedArmorSceneComponent
key={node.uuid} key={node.uuid}
ornamental={ornamental}
type={spaced ? ArmorType.Spaced : ArmorType.Core} type={spaced ? ArmorType.Spaced : ArmorType.Core}
thickness={thickness} thickness={thickness}
node={node} node={node}
@@ -186,11 +178,11 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
if (!isVisible) return null; if (!isVisible) return null;
return ( return (
<SpacedArmor <SpacedArmorSceneComponent
key={node.uuid} key={node.uuid}
ornamental={ornamental}
type={ArmorType.External} type={ArmorType.External}
thickness={trackModelDefinition.thickness} thickness={trackModelDefinition.thickness}
variant="track"
node={node} node={node}
/> />
); );
@@ -213,9 +205,8 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
return ( return (
<group key={node.uuid} position={turretOrigin}> <group key={node.uuid} position={turretOrigin}>
<SpacedArmor <SpacedArmorSceneComponent
key={node.uuid} key={node.uuid}
ornamental={ornamental}
type={spaced ? ArmorType.Spaced : ArmorType.Core} type={spaced ? ArmorType.Spaced : ArmorType.Core}
thickness={thickness} thickness={thickness}
node={node} node={node}
@@ -244,8 +235,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
key={node.uuid} key={node.uuid}
position={turretOrigin.clone().add(gunOrigin)} position={turretOrigin.clone().add(gunOrigin)}
> >
<SpacedArmor <SpacedArmorSceneComponent
ornamental={ornamental}
type={spaced ? ArmorType.Spaced : ArmorType.Core} type={spaced ? ArmorType.Spaced : ArmorType.Core}
thickness={thickness} thickness={thickness}
node={node} node={node}
@@ -264,11 +254,11 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
if (!isVisible) return null; if (!isVisible) return null;
return ( return (
<SpacedArmor <SpacedArmorSceneComponent
key={node.uuid} key={node.uuid}
ornamental={ornamental}
type={ArmorType.External} type={ArmorType.External}
thickness={gunModelDefinition.thickness} thickness={gunModelDefinition.thickness}
variant="gun"
node={node} node={node}
/> />
); );
@@ -277,5 +267,4 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
</group> </group>
</group> </group>
); );
}, });
);
@@ -5,6 +5,7 @@ import {
Object3D, Object3D,
ShaderMaterial, ShaderMaterial,
} from 'three'; } from 'three';
import { ArmorUserData, ExternalModuleVariant } from '../..';
import { isExplosive } from '../../../../../../core/blitz/isExplosive'; import { isExplosive } from '../../../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment'; import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment';
@@ -17,11 +18,13 @@ import vertexShader from './shaders/vertex.glsl';
interface SpacedArmorSubExternalProps { interface SpacedArmorSubExternalProps {
node: Object3D; node: Object3D;
thickness: number; thickness: number;
variant: ExternalModuleVariant;
} }
export function SpacedArmorSubExternal({ export function SpacedArmorSubExternal({
node, node,
thickness, thickness,
variant,
}: SpacedArmorSubExternalProps) { }: SpacedArmorSubExternalProps) {
const material = new ShaderMaterial({ const material = new ShaderMaterial({
fragmentShader, fragmentShader,
@@ -90,6 +93,12 @@ export function SpacedArmorSubExternal({
depthTest: true, depthTest: true,
depthWrite: true, depthWrite: true,
}), }),
onClick() {},
userData: {
type: 'external',
thickness,
variant,
} satisfies ArmorUserData,
})} })}
{jsxTree(node, { {jsxTree(node, {
renderOrder: 4, renderOrder: 4,
@@ -6,6 +6,7 @@ import {
ShaderMaterial, ShaderMaterial,
} from 'three'; } from 'three';
import { degToRad } from 'three/src/math/MathUtils'; import { degToRad } from 'three/src/math/MathUtils';
import { ArmorUserData } from '../..';
import { isExplosive } from '../../../../../../core/blitz/isExplosive'; import { isExplosive } from '../../../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment'; import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment';
@@ -100,6 +101,11 @@ export function SpacedArmorSubSpaced({
{jsxTree(node, { {jsxTree(node, {
renderOrder: 2, renderOrder: 2,
material, material,
onClick() {},
userData: {
type: 'spaced',
thickness,
} satisfies ArmorUserData,
})} })}
{jsxTree(node, { {jsxTree(node, {
renderOrder: 5, renderOrder: 5,
@@ -0,0 +1,212 @@
import { useThree } from '@react-three/fiber';
import { Intersection, MeshBasicMaterial, Object3D, Vector3 } from 'three';
import { degToRad } from 'three/src/math/MathUtils';
import { I_HAT } from '../../../../constants/axis';
import { isExplosive } from '../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment';
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';
type SpacedArmorSceneComponentProps = {
node: Object3D;
thickness: number;
} & (
| {
type: Exclude<ArmorType, ArmorType.External>;
}
| {
type: ArmorType.External;
variant: ExternalModuleVariant;
}
);
export type ExternalModuleVariant = 'gun' | 'track';
export type ArmorUserData = {
thickness: number;
} & (
| {
type: 'core' | 'spaced';
}
| {
type: 'external';
variant: ExternalModuleVariant;
}
);
type ArmorMeshIntersection = Intersection & {
eventObject: Object3D;
object: Object3D & { userData: ArmorUserData };
};
const omitMaterial = new MeshBasicMaterial({
colorWrite: false,
depthWrite: true,
});
/**
* Render orders allocations:
* - 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
*/
export function SpacedArmorSceneComponent({
node,
thickness,
...props
}: SpacedArmorSceneComponentProps) {
const camera = useThree((state) => state.camera);
const cameraNormal = new Vector3();
return (
<>
{props.type === ArmorType.Core &&
jsxTree(node, {
renderOrder: 0,
material: omitMaterial,
userData: {
type: 'core',
thickness,
} satisfies ArmorUserData,
async onClick(event) {
event.stopPropagation();
const intersectionsAll = event.intersections.filter(
({ object }) => object.userData.type !== undefined,
) as ArmorMeshIntersection[];
const intersectionsWithDuplicates = intersectionsAll.slice(
0,
intersectionsAll.findIndex(
({ object }) => object.userData.type === 'core',
),
);
const firstGunIntersectionIndex =
intersectionsWithDuplicates.findIndex(
({ object }) =>
object.userData.type === 'external' &&
object.userData.variant === 'gun',
);
const firstTrackIntersectionIndex =
intersectionsWithDuplicates.findIndex(
({ object }) =>
object.userData.type === 'external' &&
object.userData.variant === 'track',
);
let intersections = intersectionsWithDuplicates;
if (firstGunIntersectionIndex !== -1) {
intersections = intersections.filter(({ object }, index) => {
return object.userData.type === 'external' &&
object.userData.variant === 'gun'
? index === firstGunIntersectionIndex
: true;
});
}
if (firstTrackIntersectionIndex !== -1) {
intersections = intersections.filter(({ object }, index) => {
return object.userData.type === 'external' &&
object.userData.variant === 'track'
? index === firstTrackIntersectionIndex
: true;
});
}
const hasCalibratedShells = await hasEquipment(103, true);
const hasEnhancedArmor = await hasEquipment(110);
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
const shell = useDuel.getState().antagonist!.shell;
const penetration =
resolveNearPenetration(shell.penetration) *
(hasCalibratedShells
? isExplosive(shell.type)
? 1.1
: 1.05
: 1);
const shotLog: ({
thickness: number;
status: 'blocked' | 'penetration' | 'ricochet';
} & (
| {
type: ArmorType.External;
}
| {
type: Exclude<ArmorType, ArmorType.External>;
thicknessAngled: number;
}
))[] = []; // TODO: type this
let remainingPenetration = penetration;
let penetrationChance = -1;
let splashChance = -1;
camera.getWorldDirection(cameraNormal).multiplyScalar(-1);
let index = 0;
for (const intersection of intersections) {
const thickness =
thicknessCoefficient * intersection.object.userData.thickness;
if (intersection.object.userData.type === 'external') {
remainingPenetration -= thickness;
const blocked = remainingPenetration < 0;
shotLog.push({
type: ArmorType.External,
thickness: intersection.object.userData.thickness,
status: blocked ? 'blocked' : 'penetration',
});
if (blocked) break;
} else if (intersection.object.userData.type === 'spaced') {
const normal = intersection.normal!.applyAxisAngle(
I_HAT,
-Math.PI / 2,
);
const angle = normal.angleTo(cameraNormal);
const ricochet = degToRad(shell.ricochet ?? 90);
const normalization = degToRad(shell.normalization ?? 0);
const threeCalibersRule =
shell.caliber > thickness || index > 0;
const twoCalibersRule = shell.caliber > thickness * 2;
const finalNormalization = twoCalibersRule
? (1.4 * normalization * shell.caliber) / (2.0 * thickness)
: normalization;
const finalThickness =
thickness / Math.cos(angle - finalNormalization);
remainingPenetration -= finalThickness;
if (!threeCalibersRule && angle >= ricochet) {
shotLog.push({
type: ArmorType.Spaced,
thickness,
thicknessAngled: finalThickness,
status: 'ricochet',
});
continue;
}
} else throw new Error('Core should be omitted');
index++;
}
console.log(JSON.stringify(shotLog, null, 2));
},
})}
{props.type === ArmorType.Spaced && (
<SpacedArmorSubSpaced node={node} thickness={thickness} />
)}
{props.type === ArmorType.External && (
<SpacedArmorSubExternal
node={node}
thickness={thickness}
variant={props.variant}
/>
)}
</>
);
}
+1 -1
View File
@@ -1,8 +1,8 @@
import { createPortal, useFrame } from '@react-three/fiber'; import { createPortal, useFrame } from '@react-three/fiber';
import { memo } from 'react'; import { memo } from 'react';
import { DepthTexture, Scene, Vector2 } from 'three'; import { DepthTexture, Scene, Vector2 } from 'three';
import { spacedArmorRenderTarget } from './components/CoreArmor';
import { CoreArmorScene } from './components/CoreArmorScene'; import { CoreArmorScene } from './components/CoreArmorScene';
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent';
import { SpacedArmorScene } from './components/SpacedArmorScene'; import { SpacedArmorScene } from './components/SpacedArmorScene';
export const Armor = memo(() => { export const Armor = memo(() => {
-53
View File
@@ -1,53 +0,0 @@
import { createPortal, useFrame, useThree } from '@react-three/fiber';
import { isInteger } from 'lodash';
import { ReactNode } from 'react';
import { Scene } from 'three';
interface HeadsUpDisplayProps {
priority?: number;
children: ReactNode;
}
/**
* Renders `children` above the default scene, even if the priority is a
* negative number.
*/
export function HeadsUpDisplay({
priority = 1,
children,
}: HeadsUpDisplayProps) {
const { gl: renderer, scene: defaultScene, camera } = useThree();
const scene = new Scene();
const portal = createPortal(children, scene, {
events: { priority: priority + 1 },
});
useFrame(() => {
if (priority === 1) {
renderer.clear();
renderer.render(defaultScene, camera);
}
renderer.clearDepth();
renderer.render(scene, camera);
}, priority);
renderer.autoClear = false;
if (!isInteger(priority)) {
throw new TypeError('Priority property must be an integer');
}
return portal;
}
/**
* Use this component if the default scene disappears on usage of the
* HeadsUpDisplay; caused because there are not HeadsUpDisplay components
* with `1` priority.
*/
export function RenderDefault() {
const { gl: renderer, scene, camera } = useThree();
useFrame(() => renderer.render(scene, camera));
return null;
}