用 React-Three-Fiber 實現雪花下落與堆積效果:從零開始的 3D 雪景模擬

在 Web3D 開發中,自然現象模擬一直是極具吸引力的主題。本文將基于 React-Three-Fiber(R3F)框架,詳解如何實現一個包含雪花下落、地面堆積的完整雪景效果。我們會從基礎粒子系統入手,逐步完善物理交互邏輯,最終得到一個兼具視覺美感與性能優化的 3D 雪景組件。

?

為什么選擇 React-Three-Fiber?

在開始之前,先簡單介紹一下技術棧選擇的原因:

  • Three.js:作為 WebGL 的封裝庫,提供了豐富的 3D 圖形 API
  • React-Three-Fiber:將 Three.js 與 React 的聲明式編程模式結合,簡化了 3D 場景的狀態管理
  • @react-three/drei:提供了 Points 和 PointMaterial 等高層組件,大幅簡化粒子系統開發

這種組合讓我們能夠用熟悉的 React 語法編寫 3D 應用,同時享受聲明式編程帶來的狀態管理便利。

核心需求分析

我們要實現的雪景效果包含兩個核心部分:

  1. 動態下落的雪花:從空中隨機位置生成,受重力影響下落
  2. 地面堆積效果:雪花接觸地面后停留,形成積雪
  3. 性能平衡:在視覺效果與瀏覽器渲染性能間找到平衡點

接下來,我們將基于這些需求,逐步構建完整的實現方案。

基礎粒子系統:實現雪花下落

首先,我們需要創建一個能夠渲染大量雪花粒子的系統。在 Three.js 中,Points(點精靈)是實現粒子效果的理想選擇,它比 Mesh 更輕量,適合渲染大量簡單元素。

初始化雪花粒子

// 下落雪花的位置初始化
const [fallingPositions] = useState(() => {const pos = new Float32Array(particleCount * 3);for (let i = 0; i < particleCount; i++) {const index = i * 3;pos[index] = (Math.random() - 0.5) * 20; // X軸范圍:-10~10pos[index + 1] = Math.random() * 15 + 5; // Y軸范圍:5~20(從高空下落)pos[index + 2] = (Math.random() - 0.5) * 20; // Z軸范圍:-10~10}return pos;
});

這段代碼初始化了一個 Float32Array 數組,存儲所有雪花的 3D 坐標。每個雪花粒子需要 3 個值(X、Y、Z),因此數組長度是粒子數量的 3 倍。通過Math.random()我們讓雪花在指定范圍內隨機分布。

雪花材質設置

<PointMaterialtransparentcolor="#F0F8FF" // 柔和的雪花白sizeAttenuation={true}depthWrite={false}opacity={0.9}size={0.08}
/>

材質參數說明:

  • transparent:啟用透明效果,讓雪花有半透明質感
  • sizeAttenuation:開啟透視縮放,遠處的雪花看起來更小
  • depthWrite:關閉深度寫入,避免粒子間互相遮擋導致的視覺錯誤
  • color:選擇帶輕微藍色調的白色(#F0F8FF),更符合自然雪花的視覺感受

實現雪花下落物理邏輯

有了基礎粒子系統后,我們需要通過 R3F 的useFrame鉤子實現雪花的下落動畫。useFrame會在每幀渲染前執行,非常適合處理動畫邏輯。

useFrame((_, delta) => {if (!fallingRef.current) return;// 創建位置數組的副本以便修改const newPositions = new Float32Array(fallingPositions);for (let i = 0; i < particleCount; i++) {const index = i * 3;const x = newPositions[index];let y = newPositions[index + 1];const z = newPositions[index + 2];// 更新下落位置(乘以2加快下落速度)y -= speeds[i] * delta * 2;// 檢查是否接觸地面if (y <= 0) {// 添加到堆積雪花addAccumulatedSnow(x, z);// 重置雪花位置(重新從頂部下落)newPositions[index] = (Math.random() - 0.5) * 20;newPositions[index + 1] = Math.random() * 15 + 5;newPositions[index + 2] = (Math.random() - 0.5) * 20;} else {// 否則繼續下落newPositions[index + 1] = y;}}// 更新位置并通知Three.js需要重新渲染fallingPositions.set(newPositions);fallingRef.current.geometry.attributes.position.needsUpdate = true;
});

這段代碼的核心邏輯是:

  1. 遍歷所有雪花粒子,更新 Y 軸位置(模擬下落)
  2. 當雪花接觸地面(Y≤0)時,執行兩個操作:
    • 調用addAccumulatedSnow將雪花添加到地面堆積
    • 重置該雪花的位置,使其從空中重新下落
  3. 通過needsUpdate = true通知 Three.js 位置數據已更新

為了讓雪花下落更自然,我們還為每個雪花設置了隨機速度:

const [speeds] = useState(() =>Array.from({ length: particleCount }, () => 0.3 + Math.random() * 1.5),
);

通過0.3 + Math.random() * 1.5讓雪花速度在 0.3~1.8 之間隨機分布,避免機械感的同步下落。

地面堆積效果實現

雪花堆積效果是通過維護第二個粒子系統實現的。當下落的雪花接觸地面時,我們將其位置添加到地面粒子系統的位置數組中。

// 添加新堆積的雪花
const addAccumulatedSnow = (x: number, z: number) => {// 創建新的堆積雪花位置數組(長度+3)const newPosition = new Float32Array(groundPositions.length + 3);newPosition.set(groundPositions); // 復制原有位置// 添加新位置(Y=0.1避免與地面完全重合導致的閃爍)newPosition[groundPositions.length] = x;newPosition[groundPositions.length + 1] = 0.1;newPosition[groundPositions.length + 2] = z;setGroundPositions(newPosition);
};

地面堆積的雪花使用單獨的 Points 組件渲染,與下落的雪花相比有幾點不同:

  • 關閉sizeAttenuation,確保地面雪花大小一致
  • 增大size值(示例中為 3),讓堆積效果更明顯
  • 固定 Y 坐標為 0.1,略微高于地面避免 Z 軸沖突
<Pointsref={groundRef}positions={groundPositions}stride={3}frustumCulled={false}
><PointMaterialtransparentcolor="#F0F8FF"sizeAttenuation={false}depthWrite={false}opacity={0.9}size={3}/>
</Points>

性能優化技巧

在處理大量粒子(示例中使用 5000 個)時,性能優化至關重要:

  1. 使用 Float32Array:相比普通數組,TypedArray 在 WebGL 中處理效率更高
  2. 減少狀態更新:通過直接操作數組副本減少 React 渲染次數
  3. 關閉 frustumCulledfrustumCulled={false}避免雪花在視口邊緣被錯誤剔除
  4. 控制粒子數量:根據目標設備性能調整particleCount,移動端建議 2000-3000 個

如果需要進一步優化,可以考慮:

  • 實現視口剔除,只更新可見區域的粒子
  • 使用實例化渲染(InstancedMesh)替代 Points
  • 添加粒子生命周期限制,避免地面雪花無限累積

擴展方向

這個基礎實現可以通過以下方式擴展,獲得更豐富的效果:

  1. 添加風場效果:在 X/Z 軸方向添加隨機偏移,模擬風吹效果

    // 在更新Y軸位置的同時添加X/Z偏移
    newPositions[index] += (Math.random() - 0.5) * delta * 0.5;
    newPositions[index + 2] += (Math.random() - 0.5) * delta * 0.5;
    

  2. 實現積雪消融:為地面雪花添加生命周期,隨時間減小大小和透明度

  3. 碰撞檢測:結合模型的碰撞體,讓雪花堆積在物體表面而非穿透

  4. 雪花大小隨機化:通過自定義屬性為每個雪花設置隨機大小

完整代碼?

import React, { useState, useRef } from 'react';
import {  useFrame } from '@react-three/fiber';
import { Points, PointMaterial } from '@react-three/drei';
import * as THREE from 'three';// 雪花粒子組件
export const Snowfall = ({ particleCount = 5000 }) => {// 下落雪花的引用const fallingRef = useRef<THREE.Points>(null);// 堆積雪花的引用const groundRef = useRef<THREE.Points>(null);// 下落雪花的位置和速度const [fallingPositions] = useState(() => {const pos = new Float32Array(particleCount * 3);for (let i = 0; i < particleCount; i++) {const index = i * 3;pos[index] = (Math.random() - 0.5) * 20; // X范圍pos[index + 1] = Math.random() * 15 + 5; // Y范圍(高度)pos[index + 2] = (Math.random() - 0.5) * 20; // Z范圍}return pos;});// 堆積雪花的位置const [groundPositions, setGroundPositions] = useState<Float32Array>(() => {return new Float32Array(0);});// 下落速度const [speeds] = useState(() =>Array.from({ length: particleCount }, () => 0.3 + Math.random() * 1.5),);// 添加新堆積的雪花const addAccumulatedSnow = (x: number, z: number) => {// 創建新的堆積雪花位置(稍微高于地面避免閃爍)const newPosition = new Float32Array(groundPositions.length + 3);newPosition.set(groundPositions);newPosition[groundPositions.length] = x;newPosition[groundPositions.length + 1] = 0.1; // 稍微高于地面newPosition[groundPositions.length + 2] = z;setGroundPositions(newPosition);};// 每一幀更新雪花位置useFrame((_, delta) => {if (!fallingRef.current) return;// 創建位置數組的副本以便修改const newPositions = new Float32Array(fallingPositions);for (let i = 0; i < particleCount; i++) {const index = i * 3;const x = newPositions[index];let y = newPositions[index + 1];const z = newPositions[index + 2];// 更新下落位置y -= speeds[i] * delta * 2;// 檢查是否接觸地面if (y <= 0) {// 添加到堆積雪花addAccumulatedSnow(x, z);// 重置雪花位置newPositions[index] = (Math.random() - 0.5) * 20;newPositions[index + 1] = Math.random() * 15 + 5;newPositions[index + 2] = (Math.random() - 0.5) * 20;} else {// 否則繼續下落newPositions[index + 1] = y;}}// 更新狀態fallingPositions.set(newPositions);fallingRef.current.geometry.attributes.position.needsUpdate = true;});return (<>{/* 下落的雪花 */}<Pointsref={fallingRef}positions={fallingPositions}stride={3}frustumCulled={false}><PointMaterialtransparentcolor="#F0F8FF" // 雪花顏色sizeAttenuation={true}depthWrite={false}opacity={0.9}size={0.08}/></Points>{/* 堆積的雪花 */}<Pointsref={groundRef}positions={groundPositions}stride={3}frustumCulled={false}><PointMaterialtransparentcolor="#F0F8FF" // 雪花顏色sizeAttenuation={false}depthWrite={false}opacity={0.9}size={3}/></Points></>);
};

總結

通過本文的實現,我們展示了如何用 React-Three-Fiber 構建一個包含粒子系統、物理模擬和狀態管理的 3D 雪景效果。核心思路是將復雜效果分解為簡單模塊:下落粒子系統負責動態效果,地面粒子系統負責靜態堆積,通過useFrame實現兩者的聯動。

這種基于粒子系統的方法不僅適用于雪景模擬,還可擴展到雨滴、火焰、煙霧等多種自然現象。希望本文能為你的 3D 開發提供一些啟發,讓 Web3D 世界更加生動多彩。

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

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

相關文章

從抓包GitHub Copilot認證請求,認識OAuth 2.0技術

引言 在現代開發工具中&#xff0c;GitHub Copilot 以智能、嵌入式的人工智能代碼補全能力著稱。作為一項涉及用戶敏感數據和付費授權的服務&#xff0c;其認證授權流程尤為值得技術研究。本文基于實際抓包 VS Code 中的 Copilot 登錄認證請求&#xff0c;系統梳理其 OAuth 2.…

Linux操作系統之線程:分頁式存儲管理

目錄 前言&#xff1a; 一、分頁式存儲管理 二、二級頁表的地址轉化 三、缺頁中斷 總結 前言&#xff1a; 我們上篇文章簡單介紹了線程的一些知識點&#xff0c;但是還有很多坑沒有給大家填上&#xff0c;包括頁表部分我們還沒為大家說明。 本篇文章我將會繼續為大家講解…

xss1-8

Level-1<script>alert()</script>基礎反射型 無任何過濾Level-2"> <script>alert()</script> <"閉合屬性&#xff1a;">用來閉合當前標簽的value屬性注入新標簽&#xff1a;閉合屬性后&#xff0c;插入獨立的<script>…

51c嵌入式~單片機~合集1

自己的原文哦~ https://blog.51cto.com/whaosoft/11897656 一、STM32的啟動模式配置與應用 三種BOOT模式 所謂啟動&#xff0c;一般來說就是指我們下好程序后&#xff0c;重啟芯片時&#xff0c;SYSCLK的第4個上升沿&#xff0c;BOOT引腳的值將被鎖存。用戶可以通過設…

Typecho分類導航欄開發指南:從基礎到高級實現

文章目錄 Typecho分類導航欄深度解析:父分類與子分類的完美呈現 引言 一、Typecho分類系統基礎 1.1 Typecho分類結構 1.2 獲取分類數據的基本方法 二、基礎分類導航輸出 2.1 簡單的平鋪式導航 2.2 帶計數器的分類導航 三、層級分類導航實現 3.1 遞歸輸出父子分類 3.2 使用Type…

C++異步編程工具 async promise-future packaged_task等

深入探討 C11 中引入的四個核心異步編程工具&#xff1a;std::async, std::future, std::promise, 和 std::packaged_task。它們共同構成了 C 現代并發編程的基礎。 為了更好地理解&#xff0c;我們可以使用一個餐廳點餐的類比&#xff1a; std::future (取餐憑證)&#xff1…

Linux-網絡管理

網絡管理1. 網絡基礎1.1 TCP/IP 協議棧&#xff08;四層模型&#xff09;1.2 網絡設備配置與基礎概念1.3 網絡接口命名規則1.4 網絡配置文件位置2. 常用網絡配置命令2.1 查看網絡接口信息2.2 配置 IP 地址2.3 啟用/禁用網卡2.4 修改網卡 MAC 地址2.5 配置網卡的 MTU&#xff08…

Linux鎖的概念及線程同步

目錄 1.常見鎖概念 死鎖 死鎖四個必要條件 避免死鎖 避免死鎖算法 2. Linux線程同步 條件變量 同步概念與競態條件 條件變量函數 初始化 銷毀 等待條件滿足 喚醒等待 簡單案例&#xff1a; 條件變量使用規范 1.常見鎖概念 死鎖 死鎖是指在一組進程中的各個進程均占有不會釋放的…

docker更換國內加速器-更換華為加速器2025-717親測可用docker 拉取鏡像出錯

[rootlocalhost ~]# docker pull nginx Using default tag: latest Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)報錯原因就是…

Unity VR多人手術模擬恢復2:客戶端移動同步問題分析與解決方案

Unity VR多人手術模擬恢復2&#xff1a;客戶端移動同步問題分析與解決方案 &#x1f3af; 問題背景 在開發基于Unity Mirror網絡架構的VR多人手術模擬系統時&#xff0c;我們遇到了一個復雜的客戶端移動同步問題&#xff1a; 主要操作者&#xff08;第一個客戶端&#xff09;&a…

uni-app開發的頁面跳轉全局加載中

uni-app開發的頁面跳轉全局加載中首先需要下載插件創建加載中頁面組件app.vue頁面中監聽跳轉首先需要下載插件 https://ext.dcloud.net.cn/plugin?id20613 創建加載中頁面組件 <!-- 全局自定義加載中 --> <template><view v-if"visible" class&qu…

XXE漏洞4-XXE無回顯文件讀取-PentesterLab靶場搭建

一.PentesterLab靶場搭建(實驗環境搭建)介紹&#xff1a;PentesterLab 是一個全面的漏洞演示平臺&#xff0c;但是它是收費的&#xff0c;我們這里只使用它的 xxe 演示案例。安裝 PentesterLab 虛擬機:下載好鏡像&#xff1a; 1.打開VMware新建虛擬機&#xff0c;選擇典型就行。…

【機器學習】圖片分類中增強常用方式詳解以及效果展示

圖片增強常用方式詳解 引言 圖片數據的質量和多樣性對模型的訓練效果起著至關重要的作用。然而&#xff0c;實際獲取的圖片數據往往存在數量不足、分布不均衡等問題。圖片增強技術應運而生&#xff0c;它通過對原始圖片進行一系列變換&#xff0c;生成更多具有多樣性的圖片&…

【URL 轉換為PDF】HTML轉換為PDF

1、方法1 pdfkit 安裝依賴 # 安裝 wkhtmltopdf&#xff08;系統級&#xff09; # Ubuntu/Debian sudo apt install wkhtmltopdf# macOS brew install wkhtmltopdf# Windows 下載安裝&#xff1a;https://wkhtmltopdf.org/downloads.html# 安裝 Python 庫 pip install pdfkitimp…

單鏈表的定義、插入和刪除

一、定義一個單鏈表 struct LNode{ //定義單鏈表節點類型ElemType data; //存放節點數據元素struct LNode *next; //指針指向下一個結點 }; //增加一個新節點&#xff1a;在內存中申請一個結點所需空間&#xff0c;并用指針p指向這個結點 struct LNode * p (struc…

Nextjs官方文檔異疑惑

第一個區別&#xff1a;不同的頁面對應的路由器設定&#xff01; 繼續用 app 路由器&#xff08;推薦&#xff0c;Next.js 未來主流&#xff09; 路由規則&#xff1a;app 目錄下&#xff0c;文件夾 page.tsx 對應路由。例如&#xff1a; app/page.tsx → 對應 / 路由&#xf…

突破AI模型訪問的“光標牢籠”:長上下文處理與智能環境隔離實戰

> 當AI模型面對浩瀚文檔卻只能處理零星片段,當關鍵信息散落各處而模型“視而不見”,我們該如何打破這堵無形的墻? 在自然語言處理領域,**輸入長度限制**(常被稱為“光標區域限制”)如同一個無形的牢籠,嚴重制約了大型語言模型(LLM)在真實場景中的應用潛力。無論是分…

AI 智能質檢系統在汽車制造企業的應用?

某知名汽車制造企業在其龐大且復雜的生產流程中&#xff0c;正面臨著棘手的汽車零部件質檢難題。傳統的人工質檢方式&#xff0c;完全依賴人工的肉眼觀察與簡單工具測量。質檢員們長時間處于高強度的工作狀態&#xff0c;精神高度集中&#xff0c;即便如此&#xff0c;由于人工…

設計模式》》門面模式 適配器模式 區別

// 復雜子系統 class CPU {start() { console.log("CPU啟動"); } } class Memory {load() { console.log("內存加載"); } } class HardDrive {read() { console.log("硬盤讀取"); } }// 門面 class ComputerFacade {constructor() {this.cpu ne…

windows內核研究(驅動開發 第一個驅動程序和調試環境搭建)

驅動開發 第一個驅動程序 驅動的開發流程 1.編寫代碼 -> 生成.sys文件 -> 部署 -> 啟動 -> 停止 ->卸載 // 編寫我們的第一個驅動程序 #include<ntddk.h>// 卸載函數 VOID DrvUnload(PDRIVER_OBJECT DriverObject) {DbgPrint("我被卸載了\n"…