React Three Fiber 實現晝夜循環:從光照過渡到日月聯動的技術拆解

在 3D 場景中用 React Three Fiber 實現自然的晝夜循環,核心難點在于光照的平滑過渡日月運動的聯動邏輯晝夜狀態下的光影差異處理,以及性能與視覺效果的平衡。本文以一個 React+Three.js 的實現為例,詳細解析如何通過三角函數計算日月位置、用插值函數實現光照漸變、區分晝夜光影特性,最終讓場景從日出到月光的每一刻都自然流暢。

晝夜循環讓 3D 場景 “活” 起來

玩過《我的世界》或《塞爾達傳說》的同學一定有體會:晝夜交替不僅是視覺效果的變化,更是場景 “生命力” 的體現 —— 朝陽的暖光、正午的強光、夕陽的余暉、月光的冷寂,每一種光影都在悄悄改變場景的氛圍。

但實現這一效果并不簡單:太陽和月亮怎么 “走” 才自然?光照從亮到暗怎么過渡才不生硬?白天的陽光和夜晚的月光,光影特性差異該怎么體現?今天我們就以一個基于 React Three Fiber(R3F,Three.js 的 React 封裝)的實現為例,拆一拆這些問題的解決思路。

基礎架構:用 React Three Fiber 搭起骨架

先簡單看一下整體實現的 “骨架”。這個組件叫DayNightCycle,核心功能是通過時間驅動太陽、月亮、光照和天空背景的變化,技術棧以 React Three Fiber 為核心,搭配 Three.js 的原生 API。

核心鉤子

  • useFrame:R3F 的幀更新鉤子,類似 Three.js 的requestAnimationFrame,負責每幀更新動畫狀態(如時間、光照)。
  • useThree:R3F 提供的上下文鉤子,用于獲取場景(scene)、相機(camera)等 Three.js 核心對象。
  • useCallback/useMemo:React 的性能優化鉤子,緩存計算結果(如月亮形狀、更新函數),避免重復計算。

狀態管理:用useView獲取timeOfDay(0-1 之間的時間值,0 和 1 對應午夜,0.25 是日出,0.5 是正午,0.75 是日落)和isPaused(是否暫停動畫),通過setTimeOfDay更新時間,驅動整個循環。?

太陽和月亮的 “聯動舞步”

要讓日月運動自然,關鍵是位置計算邏輯—— 它們的運動既要符合 “東升西落” 的直覺,又要保持反向聯動(太陽升則月亮落)。

1. 太陽位置:用三角函數 “畫” 出軌跡

太陽的運動軌跡是一個圓形(簡化為 2D 平面運動),代碼中用三角函數計算位置:

const calculateSunPosition = (time: number) => {const angle = time * Math.PI * 2; // 時間0-1映射為角度0-2π(360度)const x = Math.sin(angle) * SUN_RADIUS; // x坐標由正弦函數決定(左右運動)const y = Math.cos(angle) * SUN_MAX_HEIGHT; // y坐標由余弦函數決定(上下運動)return { x, y: -Math.max(y, -5), z: 0 };
};

?這里回顧高中學的正弦、余弦曲線,在我這個組件中,太陽的z值為0,日出日落是太陽在xy平面上的圓周運動

?

  • 原理:時間time從 0 到 1 循環,對應角度從 0 到 2π(360 度)。sin(angle)控制左右(x 軸),cos(angle)控制上下(y 軸),剛好形成一個圓形軌跡。

舉例

time=0.25(日出):angle=π/2sin(π/2)=1(x 最大,東邊),cos(π/2)=0(y=0,地平線)→ 太陽在東方地平線。

time=0.5(正午):angle=πsin(π)=0(x=0,中間),cos(π)=-1(y=SUN_MAX_HEIGHT,最高點)→ 太陽在頭頂。

2. 月亮位置:與太陽 “反向同步”

月亮的運動方向與太陽相反,代碼中直接基于太陽位置計算(x 同方向,y 反方向):

const calculateMoonPosition = (time: number) => {const sunPos = calculateSunPosition(time);return { x: sunPos.x, y: -sunPos.y, z: 0 }; // x同方向,y反方向
};
  • 效果:太陽在東邊時,月亮在西邊;太陽升到最高點(正午),月亮落到最低點(地下),完美實現 “日月交替” 的視覺效果。

光照的 “平滑過渡術”

光照是晝夜循環的靈魂。白天靠陽光,夜晚靠月光,過渡時的 “柔和感” 是關鍵 —— 不能突然變亮或變暗,顏色也得自然切換。

1. 光照類型:Three.js 光源的 “分工”

Three.js 中有多種光源,這里用了兩類核心光源,分工明確:

  • 方向光(DirectionalLight)

    • 模擬平行光(如太陽、月亮),光線方向平行,能產生清晰陰影;
    • 適合表現 “直射光”,比如陽光照在物體上形成的影子。
  • 環境光(AmbientLight)

    • 無方向的 “基礎光”,不產生陰影,作用是讓場景暗處不黑屏;
    • 適合表現 “散射光”,比如白天天空反射的陽光、夜晚大氣散射的月光。

2. 太陽光:從日出到日落的 “強度 + 顏色” 漸變

太陽光的變化分兩步:強度隨太陽高度變化顏色隨時間切換

強度計算:太陽越高(y 坐標越大),強度越強;太陽在地平線以下時,強度為 0:

const calculateSunIntensity = (time: number) => {const sunPos = calculateSunPosition(time);const normalizedHeight = sunPos.y / SUN_MAX_HEIGHT; // 歸一化高度(-1到1)if (normalizedHeight < -0.05) return 0; // 太陽在地平線以下時無光照return Math.pow(Math.max(0, normalizedHeight + 0.05), 0.8) * 2.0;
};

顏色過渡:日出 / 日落偏暖(橙紅),正午偏亮(黃白),用Color.lerp(線性插值)實現漸變:

const calculateLightColor = (time: number) => {const sunriseColor = new THREE.Color(1.0, 0.5, 0.2); // 日出橙紅const noonColor = new THREE.Color(1.0, 0.95, 0.85); // 正午黃白const sunsetColor = new THREE.Color(1.0, 0.4, 0.1); // 日落橙紅const nightColor = new THREE.Color(0.05, 0.05, 0.2); // 夜晚深藍if (time < 0.25) {// 從夜晚到日出:nightColor → sunriseColorconst factor = smoothstep(0.15, 0.25, time); // 0-1的過渡因子return nightColor.clone().lerp(sunriseColor, factor);} else if (time < 0.35) {// 從日出到正午:sunriseColor → noonColorconst factor = smoothstep(0.25, 0.35, time);return sunriseColor.clone().lerp(noonColor, factor);}// ... 其他時間段邏輯
};

關鍵函數smoothstep:讓過渡不是線性的,而是 “先慢后快再慢”,更接近自然光影變化(比如日出時亮度增長先慢后快)。

3. 月光:夜晚的 “冷色調” 與低強度

月光與太陽光相反:只在夜晚生效,強度更低,顏色偏冷(藍白)。

強度控制:月亮越高(y 坐標越大),強度越強,但最大強度只有太陽光的一半(更符合現實):

  const calculateMoonIntensity = (time: number) => {const isNight = time < 0.25 || time > 0.75if (!isNight) return 0// 根據月亮高度調整強度const moonPos = calculateMoonPosition(time)const heightFactor = Math.max(0, (moonPos.y + 0.5) / 1.5) // 0-1范圍return heightFactor * 0.5 // 最大強度為0.5}

顏色差異:月光偏冷(藍白),與太陽光的暖色調形成對比,且隨月亮高度變亮:

  // 計算月光顏色const calculateMoonColor = (time: number) => {const baseColor = new THREE.Color(0.7, 0.7, 1.0) // 冷色調藍色const isNight = time < 0.25 || time > 0.75if (!isNight) return baseColor// 根據月亮高度調整顏色const moonPos = calculateMoonPosition(time)const heightFactor = Math.max(0, (moonPos.y + 0.5) / 1.5)return baseColor.clone().lerp(new THREE.Color(0.9, 0.9, 1.0), heightFactor)}

4. 環境光:晝夜通用的 “基礎亮度”

環境光強度隨晝夜變化:白天強(太陽光散射多),夜晚弱(只有月光散射):

const calculateAmbientIntensity = (time: number) => {const sunHeight = Math.sin(time * Math.PI * 2); // 太陽高度因子const dayFactor = smoothstep(-0.2, 0.1, sunHeight); // 白天強度因子const nightFactor = smoothstep(0.8, -0.2, Math.abs(sunHeight)); // 夜晚強度因子return 0.1 + dayFactor * 0.4 + nightFactor * 0.1; // 基礎亮度+晝夜補償
};

晝夜狀態的 “智能切換”

太陽和月亮不能同時 “工作”,需要通過時間判斷晝夜狀態,自動切換光照源。

狀態劃分time < 0.25time > 0.75為夜晚,其余為白天

      // 方向光(太陽光)const isDaytime = time > 0.25 && time < 0.75if (directionalRef.current) {directionalRef.current.visible = isDaytimedirectionalRef.current.position.copy(sunPosition)directionalRef.current.intensity = isDaytime? calculateSunIntensity(time): 0directionalRef.current.color.copy(calculateLightColor(time))}// 月光方向光if (moonDirectionalRef.current) {const moonLightIntensity = calculateMoonLightIntensity(time)moonDirectionalRef.current.visible = isNight && moonLightIntensity > 0moonDirectionalRef.current.position.copy(moonPosition)moonDirectionalRef.current.intensity = moonLightIntensitymoonDirectionalRef.current.color.copy(calculateMoonColor(time))// Softer shadows for moonlightmoonDirectionalRef.current.shadow.mapSize.width = 1024moonDirectionalRef.current.shadow.mapSize.height = 1024moonDirectionalRef.current.shadow.camera.far = 50moonDirectionalRef.current.shadow.bias = -0.0005moonDirectionalRef.current.shadow.normalBias = 0.05}

?完整代碼

// src/components/DayNightCycle.jsx
import React, { useCallback, useEffect,  useRef } from 'react'
import { useFrame, useThree } from '@react-three/fiber'
import * as THREE from 'three'
import { useView } from '../ViewContext'export const DayNightCycle = ({ speed = 0.1 }) => {const { timeOfDay, setTimeOfDay, isPaused } = useView()const { scene, camera } = useThree()const sunRef = useRef<THREE.Mesh>(null)const ambientRef = useRef<THREE.AmbientLight>(null)const directionalRef = useRef<THREE.DirectionalLight>(null)const moonDirectionalRef = useRef<THREE.DirectionalLight>(null) // Add this for moonlightconst skyRef = useRef<THREE.Color>(null)const moonRef = useRef<THREE.Mesh>(null)const wasPaused = useRef(false)// 太陽參數const SUN_RADIUS = 2 // 太陽運動半徑const SUN_MAX_HEIGHT = 1 // 太陽最大高度(正午時的高度)const SUN_SIZE = 0.05 // 減小太陽大小// 月亮參數const MOON_SIZE = 0.04 // 月亮比太陽稍小// 計算月光強度const calculateMoonIntensity = (time: number) => {const isNight = time < 0.25 || time > 0.75if (!isNight) return 0// 根據月亮高度調整強度const moonPos = calculateMoonPosition(time)const heightFactor = Math.max(0, (moonPos.y + 0.5) / 1.5) // 0-1范圍return heightFactor * 0.5 // 最大強度為0.5}// 計算月光顏色const calculateMoonColor = (time: number) => {const baseColor = new THREE.Color(0.7, 0.7, 1.0) // 冷色調藍色const isNight = time < 0.25 || time > 0.75if (!isNight) return baseColor// 根據月亮高度調整顏色const moonPos = calculateMoonPosition(time)const heightFactor = Math.max(0, (moonPos.y + 0.5) / 1.5)return baseColor.clone().lerp(new THREE.Color(0.9, 0.9, 1.0), heightFactor)}// 計算月光方向光強度const calculateMoonLightIntensity = (time: number) => {const isNight = time < 0.25 || time > 0.75if (!isNight) return 0const moonPos = calculateMoonPosition(time)const heightFactor = Math.max(0, (moonPos.y + 0.5) / 1.5)return heightFactor * 0.8 // 比環境月光更強一些}// 計算月亮位置(與太陽相反)const calculateMoonPosition = (time: number) => {const sunPos = calculateSunPosition(time)return {x: sunPos.x, // 月亮與太陽X軸同向y: -sunPos.y, // 月亮與太陽Y軸對稱z: 0,}}// 太陽位置計算const calculateSunPosition = (time: number) => {const angle = time * Math.PI * 2const x = Math.sin(angle) * SUN_RADIUSconst y = Math.cos(angle) * SUN_MAX_HEIGHTreturn {x: x,y: -Math.max(y, -5),z: 0,}}// 平滑過渡函數const smoothstep = (min: number, max: number, value: number) => {const x = Math.max(0, Math.min(1, (value - min) / (max - min)))return x * x * (3 - 2 * x)}// 計算光照顏色const calculateLightColor = (time: number) => {const sunriseColor = new THREE.Color(1.0, 0.5, 0.2)const noonColor = new THREE.Color(1.0, 0.95, 0.85)const sunsetColor = new THREE.Color(1.0, 0.4, 0.1)const nightColor = new THREE.Color(0.05, 0.05, 0.2)let color = new THREE.Color()if (time < 0.25) {const factor = smoothstep(0.15, 0.25, time)color.copy(nightColor).lerp(sunriseColor, factor)} else if (time < 0.35) {const factor = smoothstep(0.25, 0.35, time)color.copy(sunriseColor).lerp(noonColor, factor)} else if (time < 0.65) {color.copy(noonColor)} else if (time < 0.75) {const factor = smoothstep(0.65, 0.75, time)color.copy(noonColor).lerp(sunsetColor, factor)} else {const factor = smoothstep(0.75, 0.85, time)color.copy(sunsetColor).lerp(nightColor, factor)}return color}// 計算環境光強度const calculateAmbientIntensity = (time: number) => {const sunHeight = Math.sin(time * Math.PI * 2)const dayFactor = smoothstep(-0.2, 0.1, sunHeight)const nightFactor = smoothstep(0.8, -0.2, Math.abs(sunHeight))return 0.1 + dayFactor * 0.4 + nightFactor * 0.1}// 計算太陽光強度const calculateSunIntensity = (time: number) => {const sunPos = calculateSunPosition(time)const normalizedHeight = sunPos.y / SUN_MAX_HEIGHTif (normalizedHeight < -0.05) return 0return Math.pow(Math.max(0, normalizedHeight + 0.05), 0.8) * 2.0}// 計算天空顏色const calculateSkyColor = (time: number) => {const sunriseColor = new THREE.Color(0.9, 0.5, 0.3)const dayColor = new THREE.Color(0.5, 0.7, 1.0)const nightColor = new THREE.Color(0.05, 0.05, 0.15)let colorif (time >= 0.2 && time <= 0.3) {const factor = (time - 0.2) * 10color = nightColor.clone().lerp(sunriseColor, factor)} else if (time >= 0.3 && time <= 0.4) {const factor = (time - 0.3) * 10color = sunriseColor.clone().lerp(dayColor, factor)} else if (time >= 0.4 && time <= 0.6) {color = dayColor} else if (time >= 0.6 && time <= 0.7) {const factor = (time - 0.6) * 10color = dayColor.clone().lerp(sunriseColor, factor)} else if (time >= 0.7 && time <= 0.8) {const factor = (time - 0.7) * 10color = sunriseColor.clone().lerp(nightColor, factor)} else {color = nightColor}return color}// 提取的光照更新函數const updateLighting = useCallback((time) => {// 太陽位置const sunPosition = calculateSunPosition(time)sunRef.current?.position.set(sunPosition.x, sunPosition.y, sunPosition.z)// 月亮位置const moonPosition = calculateMoonPosition(time)moonRef.current?.position.set(moonPosition.x, moonPosition.y, moonPosition.z)moonRef.current?.lookAt(camera.position)// 月光設置const isNight = time < 0.25 || time > 0.75const moonEmissiveIntensity = isNight ? calculateMoonIntensity(time) * 2 : 0if (moonRef.current) {(moonRef.current.material as THREE.MeshStandardMaterial).emissiveIntensity = moonEmissiveIntensity}// 方向光(太陽光)const isDaytime = time > 0.25 && time < 0.75if (directionalRef.current) {directionalRef.current.visible = isDaytimedirectionalRef.current.position.copy(sunPosition)directionalRef.current.intensity = isDaytime? calculateSunIntensity(time): 0directionalRef.current.color.copy(calculateLightColor(time))}// 月光方向光if (moonDirectionalRef.current) {const moonLightIntensity = calculateMoonLightIntensity(time)moonDirectionalRef.current.visible = isNight && moonLightIntensity > 0moonDirectionalRef.current.position.copy(moonPosition)moonDirectionalRef.current.intensity = moonLightIntensitymoonDirectionalRef.current.color.copy(calculateMoonColor(time))// Softer shadows for moonlightmoonDirectionalRef.current.shadow.mapSize.width = 1024moonDirectionalRef.current.shadow.mapSize.height = 1024moonDirectionalRef.current.shadow.camera.far = 50moonDirectionalRef.current.shadow.bias = -0.0005moonDirectionalRef.current.shadow.normalBias = 0.05}// 天空背景scene.background = calculateSkyColor(time)},[camera, scene],)useFrame((state, delta) => {if (!isPaused) {const newTime = (timeOfDay + delta * speed) % 1setTimeOfDay(newTime)updateLighting(newTime)wasPaused.current = false} else if (!wasPaused.current) {updateLighting(timeOfDay)wasPaused.current = true}})useEffect(() => {if (isPaused) {updateLighting(timeOfDay)}}, [timeOfDay, isPaused, updateLighting])return (<group>{/* 太陽(可視化) */}<mesh ref={sunRef}><sphereGeometry args={[SUN_SIZE, 32, 32]} /><meshBasicMaterial color="#ffcc33" /></mesh>{/* 月亮(可視化) */}<mesh ref={moonRef}><sphereGeometry args={[MOON_SIZE, 32, 32]} /><meshStandardMaterialcolor="#e0e0ff"emissive="#b0b0ff"emissiveIntensity={0}side={THREE.DoubleSide}metalness={0.3}roughness={0.5}/></mesh>{/* 方向光(太陽光) */}<directionalLightref={directionalRef}castShadow={true}shadow-mapSize-width={2048}shadow-mapSize-height={2048}shadow-camera-far={100}shadow-camera-left={-30}shadow-camera-right={30}shadow-camera-top={30}shadow-camera-bottom={-30}shadow-bias={-0.0001}shadow-normalBias={0.05}intensity={calculateSunIntensity(timeOfDay)}color={calculateLightColor(timeOfDay)}/>{/* 方向光(月光) */}<directionalLightref={moonDirectionalRef}castShadow={true}shadow-mapSize-width={1024}shadow-mapSize-height={1024}shadow-camera-far={50}shadow-camera-left={-20}shadow-camera-right={20}shadow-camera-top={20}shadow-camera-bottom={-20}shadow-bias={-0.0005}shadow-normalBias={0.05}intensity={calculateMoonLightIntensity(timeOfDay)}color={calculateMoonColor(timeOfDay)}/>{/* 環境光 */}<ambientLightref={ambientRef}intensity={calculateAmbientIntensity(timeOfDay)}color={0xffffff}/>{/* 天空背景 */}<colorref={skyRef}attach="background"args={[calculateSkyColor(timeOfDay)]}/></group>)
}

?在組件中調用

import { OrbitControls } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'
import { CityModal } from '../Models/CityModel'
import { Snowfall } from '../Example/Snow'
import { useView } from '../ViewContext'
import { DayNightCycle } from '../Example/DayNightCycle'
import * as THREE from 'three'
// 存放所有的model加載,公用一個Canvas
export const ModalView = () => {const { playAnimation } = useView()return (<Canvas className="w-full h-full " gl={{ alpha: false }} shadows>{/* 控制器 */}<OrbitControls enableZoom={true} enablePan={true} />{/* 添加日出日落組件 */}<DayNightCycle speed={0.05} /><CityModal />{playAnimation && <Snowfall particleCount={8000} />}{/* 地面 */}<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow><planeGeometry args={[100, 100]} /><meshStandardMaterialcolor="#2c3e50"roughness={0.5}metalness={0.1}side={THREE.DoubleSide}shadowSide={THREE.FrontSide}/></mesh></Canvas>)
}

場景中的CityModal組件

?要讓模型跟隨平行光進行陰影的變化,需要開啟陰影屬性。遍歷所有的子對象,開啟陰影。child.castShadow = true;? ?child.receiveShadow = true;

import { useGLTF } from '@react-three/drei'
import { useEffect, useMemo, useRef, useState } from 'react'
import * as THREE from 'three'
import { useModelManager } from '../../utils/viewHelper/viewContext'
import { useFrame, useThree } from '@react-three/fiber'
import { useView } from '../ViewContext'export const CityModal = () => {const { scene } = useGLTF('/models/city-_shanghai-sandboxie.glb')const modelRef = useRef<THREE.Group>(null)const helper = useModelManager()const { scene: CanvasScene, camera, size } = useThree()const { gl } = useThree()gl.shadowMap.enabled = truegl.shadowMap.type = THREE.PCFSoftShadowMap // 更好的陰影質量const { cameraPosition, cameraTarget, boundaryStatus, timeOfDay } = useView()const boxHelperRef = useRef<THREE.Box3Helper>(null)const modelSize = useRef(new THREE.Vector3()) // 存儲模型尺寸const [isCameraMoving, setIsCameraMoving] = useState(false)const [targetPosition, setTargetPosition] = useState<THREE.Vector3 | null>(null,)const [targetLookAt, setTargetLookAt] = useState<THREE.Vector3 | null>(null)const mouse = useRef(new THREE.Vector2())const MOVE_DURATION = 1500// 夜間材質const nightMaterial = useMemo(() => {return new THREE.MeshStandardMaterial({color: '#0a0a1a',emissive: '#040410',emissiveIntensity: 0.2,metalness: 0.7,roughness: 0.8,})}, [])// 白天材質const dayMaterial = useMemo(() => {return new THREE.MeshStandardMaterial({color: '#0a1a3a',metalness: 0.3,roughness: 0.6,})}, [])const [lastDayNightState, setLastDayNightState] = useState<'day' | 'night'>();const nightMaterialInstance = useMemo(() => nightMaterial.clone(), [nightMaterial]);const dayMaterialInstance = useMemo(() => dayMaterial.clone(), [dayMaterial]);//處理光照對模型的影響// 獲取當前時間標簽const currentHour = useMemo(() => {const hour = Math.floor(timeOfDay * 24)const displayHour = hour % 24 || 24return displayHour}, [timeOfDay])useEffect(() => {addModel()initBoxBorder()calculateModelSize() // 計算模型尺寸alignModelToWorldCenterAndBaseToXZ()}, [])useEffect(() => {if (boxHelperRef.current) {if (boundaryStatus) {scene.add(boxHelperRef.current)} else {scene.remove(boxHelperRef.current)}}}, [boundaryStatus])useEffect(() => {if (!isCameraMoving && cameraPosition && cameraTarget) {camera.position.copy(cameraPosition)camera.lookAt(cameraTarget)}}, [cameraPosition, cameraTarget, isCameraMoving])useFrame(() => {const currentState = currentHour <= 5 || currentHour >= 18 ? 'night' : 'day';if (currentState !== lastDayNightState) {setLastDayNightState(currentState);if (modelRef.current) {modelRef.current.traverse((child) => {if (child instanceof THREE.Mesh) {child.castShadow = true;child.receiveShadow = true;if (!child.userData.originalMaterial) {child.userData.originalMaterial = child.material;}child.material = currentState === 'night' ? nightMaterialInstance : child.userData.originalMaterial || dayMaterialInstance;updateHighlight(child);}});}}});useEffect(() => {if (!isCameraMoving || !targetPosition || !targetLookAt) return// 相機移動動畫const startPosition = new THREE.Vector3().copy(camera.position)const startTime = Date.now()const animate = () => {if (!isCameraMoving) returnconst elapsed = Date.now() - startTimeconst progress = Math.min(elapsed / MOVE_DURATION, 1)const easeProgress = easeInOutCubic(progress)// 更新相機位置camera.position.lerpVectors(startPosition, targetPosition, easeProgress)camera.lookAt(targetLookAt)if (progress < 1) {requestAnimationFrame(animate)} else {setIsCameraMoving(false)}}animate()return () => {setIsCameraMoving(false)}}, [isCameraMoving, targetPosition, targetLookAt])// 計算模型尺寸const calculateModelSize = () => {if (modelRef.current) {const box = new THREE.Box3().setFromObject(modelRef.current)box.getSize(modelSize.current)}}// 更新高亮邊緣const updateHighlight = (mesh: THREE.Mesh) => {const oldHighlight = mesh.getObjectByName('surroundLine')if (oldHighlight) mesh.remove(oldHighlight)if (currentHour <= 5 || currentHour >= 18) {const geometry = new THREE.EdgesGeometry(mesh.geometry)const material = new THREE.LineBasicMaterial({color: 0x4c8bf5,linewidth: 2,})const line = new THREE.LineSegments(geometry, material)line.name = 'surroundLine'line.position.copy(mesh.position)line.rotation.copy(mesh.rotation)line.scale.copy(mesh.scale)mesh.add(line)}}//模型對齊世界中心const alignModelToWorldCenterAndBaseToXZ = () => {if (modelRef.current) {// 計算模型的包圍盒(包含所有頂點的最小立方體)const box = new THREE.Box3().setFromObject(modelRef.current)// 1. 計算模型中心點(用于XZ平面居中)const center = new THREE.Vector3()box.getCenter(center)// 2. 計算模型底部的Y坐標(包圍盒最低點的Y值)const baseY = box.min.y// 3. 先將模型在XZ平面居中,再將底部對齊到Y=0modelRef.current.position.set(-center.x, // X軸居中(減去中心點X坐標)-baseY, // Y軸對齊底部到XZ平面(減去底部Y坐標)-center.z, // Z軸居中(減去中心點Z坐標))}}const addModel = () => {if (!helper.getScene()) {helper.init(CanvasScene)}if (modelRef.current) {if (helper.getScene()) {helper.addModelToScene(modelRef.current)}helper.addModel({id: '模型1',name: '模型1',model: modelRef.current,})}camera.position.copy(new THREE.Vector3(1, 1, 1.5))}const initBoxBorder = () => {if (modelRef.current) {const box = new THREE.Box3().setFromObject(modelRef.current)boxHelperRef.current = new THREE.Box3Helper(box, 0xffff00)}}const handleMouseMove = (event: React.MouseEvent<HTMLCanvasElement>) => {// 獲取畫布尺寸const { width, height } = size// 計算鼠標在標準化設備坐標中的位置 (-1 to +1)mouse.current.x = (event.clientX / width) * 2 - 1mouse.current.y = -(event.clientY / height) * 2 + 1}const handleClick = (event) => {event.stopPropagation()if (isCameraMoving || !modelRef.current || !modelSize.current) returnhandleMouseMove(event)const intersects = event.intersectionsif (intersects.length > 0) {const clickedPoint = intersects[0].point// 根據模型大小動態計算偏移量const maxDimension = Math.max(modelSize.current.x,modelSize.current.y,modelSize.current.z,)const offsetDistance = maxDimension * 0.1 // 使用模型最大尺寸的1.5倍作為偏移距離// 計算相機位置 - 從點擊點向相機當前位置的反方向偏移const direction = new THREE.Vector3().subVectors(camera.position, clickedPoint).normalize()const targetPos = new THREE.Vector3().copy(clickedPoint).addScaledVector(direction, offsetDistance)setTargetPosition(targetPos)setTargetLookAt(clickedPoint)setIsCameraMoving(true)}}const easeInOutCubic = (t: number) => {return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2}return <primitive object={scene} ref={modelRef} onClick={handleClick} />
}

控制日出日落組件?

import { useView } from '../ViewContext'export const SunControl = () => {const { timeOfDay, setTimeOfDay, isPaused, setIsPaused } = useView()// 獲取當前時間標簽
const getTimeLabel = () => {const hour = Math.floor(timeOfDay * 24);const minute = Math.floor((timeOfDay * 24 - hour) * 60);// 判斷上午/下午const period = hour < 12 ? '上午' : '下午';// 處理小時顯示:// 1. 0點顯示為12// 2. 12點顯示為12// 3. 其他時間保持原樣const displayHour = hour % 12 === 0 ? 12 : hour % 12;return `${period} ${displayHour}:${minute.toString().padStart(2, '0')}`;
}return (<><divstyle={{position: 'absolute',top: '20px',right: '20px',background: 'rgba(0, 0, 0, 0.7)',color: 'white',padding: '10px 15px',borderRadius: '8px',fontFamily: 'Arial, sans-serif',zIndex: 100,display: 'flex',alignItems: 'center',gap: '10px',}}><span>{getTimeLabel()}</span><buttononClick={() => setIsPaused(!isPaused)}style={{background: isPaused ? '#4CAF50' : '#f44336',border: 'none',color: 'white',padding: '5px 10px',borderRadius: '4px',cursor: 'pointer',}}>{isPaused ? '?' : '?'}</button><inputtype="range"min="0"max="100"value={timeOfDay * 100}onChange={(e) => setTimeOfDay(e.target.value / 100)}style={{ width: '100px' }}/></div></>)
}

總結:React Three Fiber 實現晝夜循環的關鍵點

  1. 技術棧結合:用 R3F 的useFrame驅動幀更新,useThree獲取場景對象,React 的useMemo優化性能;
  2. 運動邏輯:三角函數計算日月位置,實現反向聯動;
  3. 光照過渡smoothstepColor.lerp實現強度、顏色的平滑漸變,避免生硬切換;
  4. 細節差異:區分晝夜狀態,讓太陽光和月光 “各司其職”,陰影根據光源特性調整清晰度。

其實,3D 場景的真實感往往藏在細節里 —— 太陽高度與光照強度的對應、月光的冷色調、陰影的清晰度差異…… 這些 “小調整” 加起來,就構成了從日到夜的自然過渡。如果你也想用 React Three Fiber 實現類似效果,不妨從這些細節入手試試~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/90587.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/90587.shtml
英文地址,請注明出處:http://en.pswp.cn/web/90587.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

進階向:基于Python的簡易屏幕畫筆工具

用Python打造你的專屬屏幕畫筆工具&#xff1a;零基礎也能輕松實現你是否曾在觀看網課或參加遠程會議時&#xff0c;想要直接在屏幕上標注重點&#xff1f;或者作為設計師&#xff0c;需要快速繪制創意草圖&#xff1f;現在&#xff0c;只需幾行Python代碼&#xff0c;你就能輕…

Elasticsearch-ik分析器

CLI 安裝步驟 1、停止 Elasticsearch&#xff08;如果正在運行&#xff09;&#xff1a; 在安裝插件之前&#xff0c;確保 Elasticsearch 沒有在運行。 命令&#xff1a; systemctl stop elasticsearch2、安裝插件&#xff1a; 使用 elasticsearch-plugin 命令安裝 IK 插件。進…

MySQL八股篇

查詢關鍵字執行先后順序FROM&#xff08;及 JOIN)WHEREGROUP BYHAVINGSELECTDISTINCTORDER BYLIMIT / OFFSETCHAR 和 VARCHAR 的區別&#xff1f;使用場景&#xff1f;特性CHARVARCHAR?存儲方式??定長&#xff0c;存儲時填充空格至定義長度變長&#xff0c;存儲實際數據 長…

QT RCC 文件

RCC (Qt Resource Compiler) 是 Qt 框架中的一個工具&#xff0c;用于將資源文件&#xff08;如圖像、音頻、翻譯文件等&#xff09;編譯成二進制格式&#xff0c;并嵌入到應用程序可執行文件中。RCC 文件基本概念作用&#xff1a;將應用程序所需的資源文件編譯成 C 代碼&#…

數據湖典型架構解析:2025 年湖倉一體化解決方案

數據湖架構概述&#xff1a;從傳統模型到 2025 年新范式數據湖作為存儲海量異構數據的中央倉庫&#xff0c;其架構設計直接影響企業數據價值的釋放效率。傳統數據湖架構主要關注數據的存儲和管理&#xff0c;而 2025 年的數據湖架構已經演變為更加智能化、自動化的綜合性數據平…

繪圖庫 Matplotlib Search

關于Pathon的繪圖庫的認識和基本操作的學習 這里學習了兩款常用便捷的繪圖庫去學習使用Matplotlib介紹是最受歡迎的一種數據可視化包 是常用的2D繪圖庫 一般常于Numpy和Pandas使用 是數據分析中非常重要的工具可以自定義XY軸 繪制線形圖 柱狀圖 直方圖 密度圖 散點圖 更清晰的展…

Docker詳解及實戰

&#x1f389; Docker 簡介和安裝 - Docker 快速入門 Docker 簡介 Docker是一個開源的平臺&#xff0c;用于開發、交付和運行應用程序。它能夠在Windows&#xff0c;macOS&#xff0c;Linux計算機上運行&#xff0c;并將某一應用程序及其依賴項打包至一個容器中&#xff0c;這…

嵌入式學習的第三十三天-進程間通信-UDP

一、網絡1.定義不同主機間進程通信主機間在硬件層面互聯互通主機在軟件層面互聯互通2.國際網絡體系結構OSI模型&#xff08;7層&#xff09;: open system interconnect -------理論模型------定義了網絡通信中不同層的協議1977 國際標準化組織各種不同體系結構的計算機能在世…

4、Spring AI_DeepSeek模型_結構化輸出

一、前言 Spring AI 提供跨 AI 供應商&#xff08;如 OpenAI、Hugging Face 等&#xff09;的一致性 API, 通過分裝的ChatModel或ChatClient即可輕松調動LLM進行流式或非流式對話。 本專欄主要圍繞著通過OpenAI兼容接口調用各種大語言模型展開學習&#xff08;因為大部分模型…

Spring Data Redis 從入門到精通:原理與實戰指南

一、Redis 基礎概念 Redis&#xff08;Remote Dictionary Server&#xff09;是開源的內存鍵值對數據庫&#xff0c;以高性能著稱。它支持多種數據結構&#xff08;String、Hash、List、Set、ZSet&#xff09;&#xff0c;并提供持久化機制&#xff08;RDB、AOF&#xff09;。 …

免費版酒店押金原路退回系統——仙盟創夢IDE

項目介紹?東方仙盟開源酒店押金管理系統是一款面向中小型酒店、民宿、客棧的輕量級前臺管理工具&#xff0c;專注于簡化房態管理、訂單處理和押金跟蹤流程。作為完全開源的解決方案&#xff0c;它無需依賴任何第三方服務&#xff0c;所有數據存儲在本地瀏覽器中&#xff0c;確…

10. isaacsim4.2教程-RTX Lidar 傳感器

1. 前言RTX Lidar 傳感器Isaac Sim的RTX或光線追蹤Lidar支持通過JSON配置文件設置固態和旋轉Lidar配置。每個RTX傳感器必須附加到自己的視口或渲染產品&#xff0c;以確保正確模擬。重要提示&#xff1a; 在運行RTX Lidar仿真時&#xff0c;如果你在Isaac Sim UI中停靠窗口&…

QT6 源,七章對話框與多窗體(14)棧式窗體 QStackedWidget:本類里代碼很少。舉例,以及源代碼帶注釋。

&#xff08;1&#xff09;這不是本章節要用到的窗體組件&#xff0c;只是跟著標簽窗體 QTabWidget 一起學了。這也是 QT 的 UI 界面里的最后幾個容器了。而且本類也很簡單。就了解一下它。 本類的繼承關系如下 &#xff1a; UI 設計界面 &#xff1a;運行效果 &#xff1a;&…

魔百和M401H_國科GK6323V100C_安卓9_不分地區免拆卡刷固件包

魔百和M401H_國科GK6323V100C_安卓9_不分地區免拆卡刷固件包刷機說明&#xff1a;1&#xff0c;進機頂盒設置&#xff08;密碼10086&#xff09;&#xff0c;在其他里&#xff0c;一直按左鍵約32下&#xff0c;打開調試模式2&#xff0c;進網絡設置&#xff0c;查看IP地址。3&a…

MySQL基礎02

一. 函數在 MySQL 中&#xff0c;函數是用于對數據進行特定處理或計算的工具&#xff0c;根據作用范圍和返回結果的不同&#xff0c;主要分為單行函數和聚合函數&#xff08;又稱分組函數&#xff09;。以下是詳細介紹&#xff1a;1.單行函數單行函數對每一行數據單獨處理&…

LabVIEW 視覺檢測SIM卡槽

針對SIM 卡槽生產中人工檢測效率低、漏檢誤檢率高的問題&#xff0c;設計了基于 LabVIEW 機器視覺的缺陷檢測系統。該系統通過光學采集與圖像處理算法&#xff0c;實現對卡槽引腳折彎、變形、漏銅等缺陷的自動檢測&#xff0c;誤報率為 0&#xff0c;平均檢測時間小于 750ms&am…

RocketMQ5.3.1的安裝

1、下載安裝 RocketMQ 的安裝包分為兩種&#xff0c;二進制包和源碼包。1 下載 Apache RocketMQ 5.3.1的源碼包后上傳到linux https://dist.apache.org/repos/dist/release/rocketmq/5.3.1/rocketmq-all-5.3.1-source-release.zip2 解壓編譯 $ unzip rocketmq-all-5.3.1-source…

FunASR實時多人對話語音識別、分析、端點檢測

核心功能&#xff1a;FunASR是一個基礎語音識別工具包&#xff0c;提供多種功能&#xff0c;包括語音識別&#xff08;ASR&#xff09;、語音端點檢測&#xff08;VAD&#xff09;、標點恢復、語言模型、說話人驗證、說話人分離和多人對話語音識別等。FunASR提供了便捷的腳本和…

opencv--day01--opencv基礎知識及基礎操作

文章目錄前言一、opencv基礎知識1.opencv相關概念1.1背景1.2特點1.3主要功能與應用1.4.opencv-python2.計算機中的圖像概念2.1圖像表示2.2圖像存儲彩色圖像二、opencv基礎操作1.圖像的讀取2.圖像的顯示3.保存圖像4.創建黑白圖及隨機像素彩圖5. 圖像切片&#xff08;圖片剪裁&am…

如何撤銷Git提交誤操作

要撤銷在主分支上的 git add . 和 git commit 操作&#xff0c;可以按照以下步驟安全回退&#xff1a; 完整回退步驟&#xff1a; # 1. 查看提交歷史&#xff0c;確認要回退的commit git log --oneline# 示例輸出&#xff1a; # d3f4g7h (HEAD -> main) 誤操作提交 # a1b2c3…