在 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"…