feat: overhead lighting

This commit is contained in:
TrèsAbhi committed 2025-11-27 13:05:08 -06:00
1 parent fced1e96ef
commit 344e7caa81
3 files changed
+55 -34

No files matched your search

@@ -12,6 +12,7 @@ import {
EyeOpenIcon, EyeOpenIcon,
ShadowInnerIcon, ShadowInnerIcon,
ShadowNoneIcon, ShadowNoneIcon,
SunIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { import {
Button, Button,
@@ -73,6 +74,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const { strings, unwrap } = useLocale(); const { strings, unwrap } = useLocale();
const revealed = Tankopedia.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
return ( return (
<> <>
@@ -349,6 +351,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
<IconButton
size="2"
highContrast
onClick={() => {
TankopediaPersistent.mutate((draft) => {
draft.highGraphics = !draft.highGraphics;
});
}}
color={highGraphics ? undefined : "gray"}
variant={highGraphics ? "solid" : "surface"}
>
<SunIcon />
</IconButton>
<ScreenshotButton <ScreenshotButton
color="gray" color="gray"
size="2" size="2"
@@ -1,15 +1,24 @@
import { useFrame, useThree } from "@react-three/fiber"; import { useFrame, useThree } from "@react-three/fiber";
import { times } from "lodash-es";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { HemisphereLight, SpotLight, type Group } from "three"; import { SpotLight, type Group } from "three";
import { clamp } from "three/src/math/MathUtils.js"; import { clamp, degToRad, lerp } from "three/src/math/MathUtils.js";
import { Tankopedia } from "../../../../../../stores/tankopedia"; import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
import { HelpingSpotLight } from "../../../../../HelpingSpotLight"; import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
const ANGLE = Math.PI / 3; const ANGLE = Math.PI / 3;
const HEMISPHERE_INTENSITY = 4; const REVEAL_ANIMATION_TIME = 3;
const REVEAL_ANIMATION_TIME = 5;
const TRANSITION_ANIMATION_TIME = 0.5; const TRANSITION_ANIMATION_TIME = 0.5;
const LIGHTS_COUNT = 4;
const THETA_OFFSET = degToRad(-150);
const LIGHT_DISTANCE = 13;
const LIGHT_HEIGHT_0 = 4;
const LIGHT_HEIGHT_1 = 8;
const INTENSITY_0 = 70;
const INTENSITY_1 = 40;
export function Lighting() { export function Lighting() {
const wrapper = useRef<Group>(null!); const wrapper = useRef<Group>(null!);
const t0 = useRef(0); const t0 = useRef(0);
@@ -17,6 +26,7 @@ export function Lighting() {
const display = Tankopedia.use((state) => state.display); const display = Tankopedia.use((state) => state.display);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay); const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const animationTime = useRef(REVEAL_ANIMATION_TIME); const animationTime = useRef(REVEAL_ANIMATION_TIME);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
useEffect(() => { useEffect(() => {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
@@ -35,9 +45,7 @@ export function Lighting() {
const t = 0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5; const t = 0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5;
for (const child of wrapper.current.children) { for (const child of wrapper.current.children) {
if (child instanceof HemisphereLight) { if (child instanceof SpotLight) {
child.intensity = HEMISPHERE_INTENSITY * t;
} else if (child instanceof SpotLight) {
child.angle = ANGLE * t; child.angle = ANGLE * t;
} }
} }
@@ -60,34 +68,29 @@ export function Lighting() {
return ( return (
<group ref={wrapper}> <group ref={wrapper}>
<hemisphereLight {times(LIGHTS_COUNT, (index) => {
position={[0, 1, 0]} const x = index / (LIGHTS_COUNT - 1);
color="#dadaff" const theta = 2 * Math.PI * (index / LIGHTS_COUNT) + THETA_OFFSET;
groundColor="#856331" const position = [
intensity={0} 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);
<HelpingSpotLight return (
debug <HelpingSpotLight
position={[-6, 6, -10]} userData={{ index }}
intensity={4} key={index}
penumbra={1} position={position}
castShadow intensity={intensity}
color="#ffffff" penumbra={1}
decay={0} castShadow={highGraphics}
angle={0} decay={1}
/> color="#ffffff"
/>
<HelpingSpotLight );
debug })}
position={[6, 6, -10]}
intensity={1}
penumbra={1}
castShadow
color="#ffffff"
decay={0}
angle={0}
/>
</group> </group>
); );
} }
@@ -15,6 +15,7 @@ export interface TankopediaPersistent {
showPrimaryArmor: boolean; showPrimaryArmor: boolean;
recentlyViewed: number[]; recentlyViewed: number[];
hideTankModelUnderArmor: boolean; hideTankModelUnderArmor: boolean;
highGraphics: boolean;
sort: { sort: {
by: TankopediaSortBy; by: TankopediaSortBy;
@@ -33,6 +34,7 @@ export const TankopediaPersistent = new Varuna<TankopediaPersistent>(
showExternalModules: true, showExternalModules: true,
showPrimaryArmor: true, showPrimaryArmor: true,
recentlyViewed: [], recentlyViewed: [],
highGraphics: true,
sort: { sort: {
by: "meta.none", by: "meta.none",
direction: "ascending", direction: "ascending",