Compare commits

..
6 Commits
Author SHA1 Message Date
tresabhi c85cd36818 logs 2026-06-28 11:11:37 -05:00
tresabhi 5d2c63fed9 clean up some renderer stuff 2026-06-26 14:05:26 -05:00
tresabhi cead140855 optimize intro naimaiton 2026-06-26 14:01:39 -05:00
tresabhi 63a0143d4f optimize scene lighting and rnederer 2026-06-26 13:58:30 -05:00
tresabhi 19f22993f1 optimize shot display animator 2026-06-26 13:31:40 -05:00
tresabhi b2eb74914f optimize lighting loop 2026-06-26 13:22:24 -05:00
12 changed files with 228 additions and 168 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
# BlitzKit 1.30.3
# BlitzKit 1.30.4
## Fixes
+8
View File
@@ -0,0 +1,8 @@
# BlitzKit 1.30.5
## Features
- Reduced the amount of code that executes for every rendered frame, hopefully improving performance
- Reduced the number of lights by 20% to decrease the amount of shadow calculations
- Decreased the ground size by 80% for an equal reduction in shadow texture sizes
- Added the ability for BlitzKit to dismount the components that handled the intro lighting animation when not animating, reducing checks running every frame
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "blitzkit",
"version": "1.30.4",
"version": "1.30.5",
"license": "BlitzKit License",
"private": true,
"scripts": {
@@ -1,9 +1,9 @@
import { J_HAT, K_HAT } from "@blitzkit/core";
import { Box, Flex, Text } from "@radix-ui/themes";
import { Html } from "@react-three/drei";
import { invalidate, useFrame } from "@react-three/fiber";
import { useFrame } from "@react-three/fiber";
import { clamp } from "lodash-es";
import { useEffect, useRef, type ComponentProps } from "react";
import { useEffect, useRef, type ComponentProps, type RefObject } from "react";
import {
BufferGeometry,
DoubleSide,
@@ -42,11 +42,10 @@ const TRACER_THIN = TRACER_THICK / 2;
export function ShotDisplay() {
const shot = Tankopedia.use((state) => state.shot);
const inTracer = useRef<Mesh>(null);
const outTracer = useRef<Mesh>(null);
const splashRadiusWrapper = useRef<Group>(null);
const splashRadiusMaterial = useRef<MeshBasicMaterial>(null);
let animationStartTime: number | null = null;
const inTracer = useRef<Mesh>(null!);
const outTracer = useRef<Mesh>(null!);
const splashRadiusWrapper = useRef<Group>(null!);
const splashRadiusMaterial = useRef<MeshBasicMaterial>(null!);
const { locale } = useLocale();
useEffect(() => {
@@ -64,10 +63,10 @@ export function ShotDisplay() {
outlineMaterial,
).computeLineDistances();
splashRadiusWrapper.current?.add(outline);
splashRadiusWrapper.current.add(outline);
return () => {
splashRadiusWrapper.current?.remove(outline);
splashRadiusWrapper.current.remove(outline);
};
}
});
@@ -86,44 +85,6 @@ export function ShotDisplay() {
}
}, [shot]);
useFrame(({ clock }) => {
if (!shot) return;
const t = clock.elapsedTime;
if (inTracer.current) {
const tracerT1 = t % 1;
const tracerT2 = (t + 0.5) % 1;
inTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT1 - 0.5), 1);
inTracer.current.position.set(0, (1 - tracerT1) * inLength, 0);
if (outTracer.current) {
outTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT2 - 0.5), 1);
outTracer.current.position.set(0, tracerT2 * outLength, 0);
}
invalidate();
}
if (
shot.splashRadius !== undefined &&
splashRadiusWrapper.current &&
splashRadiusMaterial.current
) {
if (animationStartTime === null) animationStartTime = t;
const scale =
clamp(4 * (t - animationStartTime), 0, 1) * shot.splashRadius;
const opacity = 1 - clamp(2 * (t - animationStartTime), 0, 1);
splashRadiusWrapper.current.scale.set(scale, scale, scale);
splashRadiusMaterial.current.opacity = opacity;
invalidate();
}
});
if (!shot) return null;
const inLength =
@@ -149,6 +110,15 @@ export function ShotDisplay() {
return (
<group>
<Animator
inTracer={inTracer}
outTracer={outTracer}
inLength={inLength}
outLength={outLength}
splashRadiusWrapper={splashRadiusWrapper}
splashRadiusMaterial={splashRadiusMaterial}
/>
{shot.splashRadius && (
<group
position={inLast.point}
@@ -283,3 +253,59 @@ export function ShotDisplay() {
</group>
);
}
interface AnimatorProps {
inTracer: RefObject<Mesh>;
outTracer: RefObject<Mesh>;
splashRadiusWrapper: RefObject<Group>;
splashRadiusMaterial: RefObject<MeshBasicMaterial>;
inLength: number;
outLength: number;
}
function Animator({
inTracer,
outTracer,
splashRadiusWrapper,
splashRadiusMaterial,
inLength,
outLength,
}: AnimatorProps) {
const shot = Tankopedia.use((state) => state.shot!);
let animationStartTime: number | null = null;
useFrame(({ clock, invalidate }) => {
const t = clock.elapsedTime;
const tracerT1 = t % 1;
const tracerT2 = (t + 0.5) % 1;
inTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT1 - 0.5), 1);
inTracer.current.position.set(0, (1 - tracerT1) * inLength, 0);
if (outTracer.current) {
outTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT2 - 0.5), 1);
outTracer.current.position.set(0, tracerT2 * outLength, 0);
}
invalidate();
if (shot.splashRadius !== undefined) {
if (animationStartTime === null) animationStartTime = t;
const scale =
clamp(4 * (t - animationStartTime), 0, 1) * shot.splashRadius;
const opacity = 1 - clamp(2 * (t - animationStartTime), 0, 1);
splashRadiusWrapper.current.scale.set(scale, scale, scale);
splashRadiusMaterial.current.opacity = opacity;
invalidate();
}
});
return null;
}
@@ -1,9 +1,9 @@
import { createPortal, useFrame, useThree } from '@react-three/fiber';
import { memo, useState } from 'react';
import { DepthTexture, Scene, Vector2 } from 'three';
import { PrimaryArmorScene } from './components/PrimaryArmorScene';
import { spacedArmorRenderTarget } from './components/PrimaryArmorSceneComponent/target';
import { SpacedArmorScene } from './components/SpacedArmorScene';
import { createPortal, useFrame, useThree } from "@react-three/fiber";
import { memo, useState } from "react";
import { DepthTexture, Scene, Vector2 } from "three";
import { PrimaryArmorScene } from "./components/PrimaryArmorScene";
import { spacedArmorRenderTarget } from "./components/PrimaryArmorSceneComponent/target";
import { SpacedArmorScene } from "./components/SpacedArmorScene";
export const Armor = memo(() => {
const rootScene = useThree((state) => state.scene);
@@ -1,4 +1,4 @@
import { useFrame } from '@react-three/fiber';
import { useFrame } from "@react-three/fiber";
export const zoom = 60;
@@ -1,4 +1,4 @@
import { useFrame } from '@react-three/fiber';
import { useFrame } from "@react-three/fiber";
export function AutoClear() {
useFrame(({ gl }) => {
@@ -19,6 +19,8 @@ const poseDistances: Record<Pose, number> = {
[Pose.Default]: -1,
};
const inspectModeInitialPosition = new Vector3(-8, 2, -13);
const modelDefinitions = await awaitableModelDefinitions;
interface ControlsProps {
@@ -77,22 +79,6 @@ export function Controls({
protagonistHullOrigin.y +
protagonistTurretOrigin.y +
protagonistGunOrigin.y;
const inspectModeInitialPosition = new Vector3(-8, 2, -13);
const t0 = useRef(Date.now() / 1000);
useFrame(() => {
if (!doAutoRotate) return;
const now = Date.now() / 1000;
const t = now - t0.current;
camera.position
.copy(inspectModeInitialPosition)
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
invalidate();
});
useEffect(() => {
function handleControlsEnabled(event: QuicklimeEvent<boolean>) {
@@ -249,15 +235,37 @@ export function Controls({
}, []);
return (
<OrbitControls
enableRotate={enableRotate}
maxDistance={40}
minDistance={5}
enableZoom={zoomable}
zoomSpeed={zoomable ? undefined : 0}
ref={orbitControls}
enabled={controlsEnabledEvent.last!}
enableDamping={false}
/>
<>
{doAutoRotate && <Animator />}
<OrbitControls
enableRotate={enableRotate}
maxDistance={40}
minDistance={5}
enableZoom={zoomable}
zoomSpeed={zoomable ? undefined : 0}
ref={orbitControls}
enabled={controlsEnabledEvent.last!}
enableDamping={false}
/>
</>
);
}
function Animator() {
const clock = useThree((state) => state.clock);
const t0 = useRef(clock.elapsedTime);
useFrame(({ camera }) => {
const t = clock.elapsedTime - t0.current;
camera.position
.copy(inspectModeInitialPosition)
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
invalidate();
});
return null;
}
@@ -1,63 +1,127 @@
import { useFrame, useThree } from "@react-three/fiber";
import { times } from "lodash-es";
import { useFrame } from "@react-three/fiber";
import { clamp, times } from "lodash-es";
import { Quicklime } from "quicklime";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState, type RefObject } from "react";
import { HemisphereLight, SpotLight, type Group } from "three";
import { clamp, degToRad, lerp } from "three/src/math/MathUtils.js";
import { degToRad, lerp } from "three/src/math/MathUtils.js";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
const ANGLE = Math.PI * 2 ** -2;
const ANGLE = degToRad(10);
const REVEAL_ANIMATION_TIME = 3;
const TRANSITION_ANIMATION_TIME = 0.5;
const LIGHTS_COUNT = 5;
const THETA_OFFSET = degToRad(-152);
const LIGHT_DISTANCE = 13;
const LIGHTS_COUNT = 4;
const THETA_OFFSET = degToRad(180 - 45);
const LIGHT_DISTANCE = 20;
const LIGHT_HEIGHT_0 = 4;
const LIGHT_HEIGHT_1 = 2;
const INTENSITY_0 = 3.5;
const INTENSITY_1 = 1.5;
const LIGHT_HEIGHT_1 = 6;
const INTENSITY_0 = 6;
const INTENSITY_1 = 3;
const HEMISPHERE_INTENSITY = 2;
export const transitionEvent = new Quicklime<number>(0);
export function Lighting() {
const wrapper = useRef<Group>(null!);
const t0 = useRef(0);
const transition = useRef(false);
const clock = useThree((state) => state.clock);
const display = Tankopedia.use((state) => state.display);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const animationTime = useRef(REVEAL_ANIMATION_TIME);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const animationTime = useRef(REVEAL_ANIMATION_TIME);
const t0 = useRef(performance.now() / 1e3 - animationTime.current);
const isRevealing = useRef(true);
const [animate, setAnimate] = useState(true);
console.log(animate);
useEffect(() => {
Tankopedia.mutate((draft) => {
draft.revealed = true;
});
t0.current = performance.now() / 1000 - REVEAL_ANIMATION_TIME;
}, []);
useEffect(() => {
if (isRevealing.current) {
isRevealing.current = false;
} else {
animationTime.current = TRANSITION_ANIMATION_TIME;
t0.current = performance.now() / 1e3;
}
setAnimate(true);
}, [requestedDisplay]);
return (
<>
{animate && (
<Animator
t0={t0}
animationTime={animationTime}
wrapper={wrapper}
stop={() => {
setAnimate(false);
}}
/>
)}
<group ref={wrapper}>
<hemisphereLight intensity={0} color="#ffffff" groundColor="#afafaf" />
{times(LIGHTS_COUNT, (index) => {
const x = index / (LIGHTS_COUNT - 1);
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) + THETA_OFFSET;
const position = [
LIGHT_DISTANCE * Math.sin(theta),
lerp(LIGHT_HEIGHT_0, LIGHT_HEIGHT_1, x),
LIGHT_DISTANCE * Math.cos(theta),
] as const;
const intensity = lerp(INTENSITY_0, INTENSITY_1, x);
return (
<HelpingSpotLight
userData={{ index }}
key={index}
position={position}
intensity={intensity}
penumbra={1}
castShadow={highGraphics}
decay={0}
color="#ffffff"
angle={0}
/>
);
})}
</group>
</>
);
}
interface AnimatorProps {
stop: () => void;
t0: RefObject<number>;
animationTime: RefObject<number>;
wrapper: RefObject<Group>;
}
function Animator({ stop, t0, animationTime, wrapper }: AnimatorProps) {
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
useFrame(({ invalidate }) => {
if (transition.current) {
transition.current = false;
t0.current = performance.now() / 1000;
const dt = performance.now() / 1e3 - t0.current;
const x = clamp(dt / animationTime.current, 0, 2);
if (x === 2) {
stop();
return;
}
const x = clamp(
(performance.now() / 1000 - t0.current) / animationTime.current,
0,
2,
);
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
if (t !== transitionEvent.last) {
transitionEvent.dispatch(t);
}
for (const child of wrapper.current.children) {
if (child instanceof SpotLight) {
child.angle = ANGLE * t;
@@ -66,53 +130,15 @@ export function Lighting() {
}
}
if (x < 2) invalidate();
transitionEvent.dispatch(t);
invalidate();
if (
x > 1 &&
Tankopedia.state.display !== Tankopedia.state.requestedDisplay
) {
if (x >= 1 && Tankopedia.state.display !== requestedDisplay) {
Tankopedia.mutate((draft) => {
draft.display = requestedDisplay;
});
}
});
useEffect(() => {
if (requestedDisplay === display) return;
transition.current = true;
animationTime.current = TRANSITION_ANIMATION_TIME;
}, [requestedDisplay]);
return (
<group ref={wrapper}>
<hemisphereLight intensity={0} color="#ffffff" groundColor="#afafaf" />
{times(LIGHTS_COUNT, (index) => {
const x = index / (LIGHTS_COUNT - 1);
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) + THETA_OFFSET;
const position = [
LIGHT_DISTANCE * Math.sin(theta),
lerp(LIGHT_HEIGHT_0, LIGHT_HEIGHT_1, x),
LIGHT_DISTANCE * Math.cos(theta),
] as const;
const intensity = lerp(INTENSITY_0, INTENSITY_1, x);
return (
<HelpingSpotLight
userData={{ index }}
key={index}
position={position}
intensity={intensity}
penumbra={1}
castShadow={highGraphics}
decay={0}
color="#ffffff"
angle={0}
/>
);
})}
</group>
);
return null;
}
@@ -1,10 +1,9 @@
import { invalidate, useFrame } from "@react-three/fiber";
import { clamp } from "lodash-es";
import { invalidate } from "@react-three/fiber";
import { Quicklime, type QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react";
import { Mesh, MeshStandardMaterial } from "three";
const SIZE = 2 ** 6;
const SIZE = 14;
export const screenshotReadyEvent = new Quicklime(false);
@@ -12,11 +11,6 @@ export function SceneProps() {
const mesh = useRef<Mesh>(null!);
const material = useRef<MeshStandardMaterial>(null);
useFrame(({ camera }) => {
if (!material.current) return;
material.current.opacity = clamp(0.5 * camera.position.y, 0, 0.5);
});
useEffect(() => {
function handleScreenshotReady(event: QuicklimeEvent<boolean>) {
mesh.current.visible = !event.data;
@@ -36,11 +30,9 @@ export function SceneProps() {
receiveShadow
visible={!screenshotReadyEvent.last!}
ref={mesh}
renderOrder={-1}
>
<planeGeometry args={[SIZE, SIZE]} />
<meshStandardMaterial
transparent
ref={material}
color="black"
roughness={1}
@@ -156,7 +156,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
localClippingEnabled: true,
preserveDrawingBuffer: true,
}}
shadows="soft"
shadows="basic"
onPointerDown={handlePointerDown}
onPointerMissed={() => {
Tankopedia.mutate((draft) => {
@@ -40,5 +40,5 @@ export const TankopediaPersistent = new Varuna<TankopediaPersistent>(
direction: "ascending",
},
},
"tankopedia-2"
"tankopedia-3",
);