feat: new lighting

This commit is contained in:
TrèsAbhi committed 2025-11-26 20:48:25 -06:00
1 parent 2559782a28
commit b67fd5b58c
5 files changed
+39 -153

No files matched your search

@@ -1,3 +1,5 @@
import { Box, Text } from "@radix-ui/themes";
import { Html } from "@react-three/drei";
import { memo, type ComponentProps } from "react"; import { memo, type ComponentProps } from "react";
interface Props extends ComponentProps<"spotLight"> { interface Props extends ComponentProps<"spotLight"> {
@@ -8,10 +10,20 @@ export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
return ( return (
<> <>
{debug && ( {debug && (
<mesh position={props.position}> <group position={props.position}>
<mesh>
<icosahedronGeometry args={[0.1, 2]} /> <icosahedronGeometry args={[0.1, 2]} />
<meshBasicMaterial color={props.color} /> <meshBasicMaterial color={props.color} />
</mesh> </mesh>
<Html center>
<Box pb="9">
<Text wrap="nowrap">
{props.intensity} @ {JSON.stringify(props.userData)}
</Text>
</Box>
</Html>
</group>
)} )}
<spotLight {...props} /> <spotLight {...props} />
@@ -1,40 +0,0 @@
import { invalidate, useThree } from "@react-three/fiber";
import { useEffect } from "react";
import type { Fog } from "three";
import { fogAnimationTime, fogFar0, fogFar1, forNear0, forNear1 } from "..";
import { Tankopedia } from "../../../../../../stores/tankopedia";
export function InitialFogReveal() {
const fog = useThree((state) => state.scene.fog as Fog | null);
useEffect(() => {
Tankopedia.mutate((draft) => {
draft.revealed = true;
});
if (!fog) return;
const t0 = Date.now();
const interval = setInterval(() => {
const t = (Date.now() - t0) / 1000;
const x = t / fogAnimationTime;
const near = forNear1 + (forNear0 - forNear1) * x;
const far = fogFar1 + (fogFar0 - fogFar1) * x;
fog.near = near;
fog.far = far;
invalidate();
if (x >= 1) {
fog.near = forNear0;
fog.far = fogFar0;
clearInterval(interval);
}
}, 1000 / 60);
return () => clearInterval(interval);
}, []);
return null;
}
@@ -1,82 +1,34 @@
import { useHelper } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { times } from "lodash-es";
import { useRef, useState } from "react";
import { Color, SpotLight, SpotLightHelper } from "three";
import { degToRad } from "three/src/math/MathUtils.js";
import { isHalloween } from "../../../../../../core/blitzkit/isHalloween";
import { HelpingSpotLight } from "../../../../../HelpingSpotLight"; import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
const LIGHTS_COUNT = 5;
const LIGHT_DISTANCE = 13;
const LIGHT_HEIGHT_0 = 4;
const LIGHT_HEIGHT_1 = 2;
const INTENSITY = 50;
const N = 128;
const TARGET_FRAME_RATE = 30;
export function Lighting() { export function Lighting() {
const [cast, setCast] = useState(true);
const t0 = useRef(0);
const topLight = useRef<SpotLight>(null!);
const frontLight = useRef<SpotLight>(null!);
useHelper(topLight, SpotLightHelper, "red");
useHelper(frontLight, SpotLightHelper, "green");
const last = useRef<number>(0);
const samples = useRef<number[]>([]);
useFrame(({ clock, frameloop }) => {
if (frameloop === "never") {
t0.current = clock.elapsedTime;
samples.current = [];
return;
}
if (!cast || clock.elapsedTime - t0.current < TARGET_FRAME_RATE * N) {
return;
}
const dt = clock.elapsedTime - last.current;
last.current = clock.elapsedTime;
const fps = 1 / dt;
samples.current.push(fps);
if (samples.current.length > N) samples.current.shift();
const avg =
samples.current.reduce((a, b) => a + b, 0) / samples.current.length;
if (avg < TARGET_FRAME_RATE) setCast(false);
});
return ( return (
<> <>
{times(LIGHTS_COUNT, (index) => { <hemisphereLight
const x = index / (LIGHTS_COUNT - 1); position={[0, 1, 0]}
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) - degToRad(152); color="#dadaff"
const position = [ groundColor="#856331"
LIGHT_DISTANCE * Math.sin(theta), intensity={4}
LIGHT_HEIGHT_0 * (1 - x) + LIGHT_HEIGHT_1 * x, />
LIGHT_DISTANCE * Math.cos(theta),
] as const;
const color = new Color();
return (
<HelpingSpotLight <HelpingSpotLight
key={index} debug
position={position} position={[-6, 6, -10]}
intensity={INTENSITY} intensity={4}
penumbra={1} penumbra={1}
castShadow={cast} castShadow
decay={1} color="#ffffff"
color={color} decay={0}
/>
<HelpingSpotLight
debug
position={[6, 6, -10]}
intensity={1}
penumbra={1}
castShadow
color="#ffffff"
decay={0}
/> />
);
})}
</> </>
); );
} }
@@ -4,7 +4,7 @@ import { Quicklime, type QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { Mesh, MeshStandardMaterial } from "three"; import { Mesh, MeshStandardMaterial } from "three";
const SIZE = 2 ** 5; const SIZE = 2 ** 6;
export const screenshotReadyEvent = new Quicklime(false); export const screenshotReadyEvent = new Quicklime(false);
@@ -14,7 +14,7 @@ export function SceneProps() {
useFrame(({ camera }) => { useFrame(({ camera }) => {
if (!material.current) return; if (!material.current) return;
material.current.opacity = clamp(0.5 * camera.position.y, 0, 0.25); material.current.opacity = clamp(0.5 * camera.position.y, 0, 0.5);
}); });
useEffect(() => { useEffect(() => {
@@ -44,7 +44,7 @@ export function SceneProps() {
ref={material} ref={material}
color="black" color="black"
roughness={1} roughness={1}
metalness={1} metalness={0}
/> />
</mesh> </mesh>
); );
@@ -1,5 +1,4 @@
import { BLITZKIT_TANK_ICON_SIZE } from "@blitzkit/core"; import { BLITZKIT_TANK_ICON_SIZE } from "@blitzkit/core";
import { invalidate } from "@react-three/fiber";
import { import {
forwardRef, forwardRef,
Suspense, Suspense,
@@ -29,7 +28,6 @@ import { SmartCanvas } from "../../../../SmartCanvas";
import { AutoClear } from "./components/AutoClear"; import { AutoClear } from "./components/AutoClear";
import { Controls } from "./components/Control"; import { Controls } from "./components/Control";
import { InitialAligner } from "./components/InitialAligner"; import { InitialAligner } from "./components/InitialAligner";
import { InitialFogReveal } from "./components/InitialFogReveal";
import { Lighting } from "./components/Lighting"; import { Lighting } from "./components/Lighting";
import { SceneProps } from "./components/SceneProps"; import { SceneProps } from "./components/SceneProps";
import { TankModel } from "./components/TankModel"; import { TankModel } from "./components/TankModel";
@@ -40,15 +38,8 @@ interface TankSandboxProps {
naked?: boolean; naked?: boolean;
} }
export const forNear0 = 30;
export const fogFar0 = 50;
export const forNear1 = 0;
export const fogFar1 = 0;
export const fogAnimationTime = 2.5;
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>( export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
({ thicknessRange, naked }, ref) => { ({ thicknessRange, naked }, ref) => {
const fog = useRef(new Fog("black", forNear1, fogFar1));
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122); const hasImprovedVerticalStabilizer = useEquipment(122);
const hasDownImprovedVerticalStabilizer = useEquipment(124); const hasDownImprovedVerticalStabilizer = useEquipment(124);
@@ -64,34 +55,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
(state) => state.hideTankModelUnderArmor (state) => state.hideTankModelUnderArmor
); );
useEffect(() => {
if (rawDisplay === display) return;
const t0 = Date.now();
const interval = setInterval(() => {
const t = (Date.now() - t0) / 1000;
const x = t / fogAnimationTime;
const y = Math.sin(Math.PI * x);
const near = forNear0 + (forNear1 - forNear0) * y;
const far = fogFar0 + (fogFar1 - fogFar0) * y;
fog.current.near = near;
fog.current.far = far;
invalidate();
if (rawDisplay !== display && x >= 0.5) setDisplay(rawDisplay);
if (x >= 1) {
fog.current.near = forNear0;
fog.current.far = fogFar0;
clearInterval(interval);
}
}, 1000 / 60);
return () => clearInterval(interval);
}, [rawDisplay]);
useImperativeHandle(ref, () => canvas.current!, []); useImperativeHandle(ref, () => canvas.current!, []);
function handlePointerDown() { function handlePointerDown() {
@@ -191,7 +154,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
return ( return (
<SmartCanvas <SmartCanvas
ref={canvas} ref={canvas}
scene={{ fog: naked ? undefined : fog.current }} scene={{ fog: naked ? undefined : new Fog("black", 30, 50) }}
gl={{ gl={{
clippingPlanes: [], clippingPlanes: [],
localClippingEnabled: true, localClippingEnabled: true,
@@ -224,7 +187,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
<Suspense> <Suspense>
{/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */} {/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */}
<Controls naked={naked} /> <Controls naked={naked} />
<InitialFogReveal />
{display === TankopediaDisplay.DynamicArmor && <Armor />} {display === TankopediaDisplay.DynamicArmor && <Armor />}
{display === TankopediaDisplay.StaticArmor && ( {display === TankopediaDisplay.StaticArmor && (