Three.js实时竹林渲染:几何建模、光影氛围与风动系统实现

发布时间:2026/11/2 18:13:08
Three.js实时竹林渲染:几何建模、光影氛围与风动系统实现 1. 项目缘起与整体设计思路1.1 为什么是竹林而不是别的看到这个标题的时候我第一反应是又是一个“情怀驱动”的3D场景项目。但仔细一想竹林这个选题其实非常刁钻。它不像森林、沙漠、雪山那样有大量现成的素材库和教程竹子的形态、光影、风动效果在3D领域里属于“看起来简单、做起来要命”的类型。我小时候也在一片竹林边上长大那种感觉很难用语言描述光线从密密麻麻的竹叶缝隙里漏下来地上是厚厚一层枯黄的竹叶风一吹整片竹林会发出“沙沙”的摩擦声而且竹子不是一棵一棵独立摇晃的是整片像波浪一样此起彼伏。这个项目要还原的就是这种记忆中的氛围感。从技术选型角度来说这个项目本质上是一个实时3D场景渲染项目核心要解决三个问题竹子的几何建模、竹林的光影氛围、以及风动动画。这三个问题分别对应了建模管线、渲染管线和动画系统任何一个环节拉胯整个场景的“味道”就没了。1.2 技术栈选型背后的取舍逻辑虽然原始描述里没有明确说用了什么引擎但基于这类个人情怀项目的常见实践大概率是在Three.js或者Unity之间做选择。我两种方案都试过这里说说我的判断逻辑。选Three.js的理由很直接浏览器打开就能看分享给朋友不需要装任何东西而且WebGL的着色器编写非常灵活适合做竹叶透光、竹竿次表面散射这类效果。但坑也很明显——性能优化全靠自己没有现成的LOD系统、没有内置的植被工具每一棵竹子都得自己写实例化逻辑。选Unity的理由是工具链成熟Terrain系统、Wind Zone、LOD Group都是现成的做植被场景效率高很多。但问题是打包出来是独立应用分享门槛高而且Unity的默认渲染管线做这种“小而美”的场景有点杀鸡用牛刀的感觉。我最终倾向于Three.js 自定义着色器的方案。原因很简单这个场景的核心不是“大”而是“精”。竹林不需要几万棵树可能就几百棵竹子但每一棵的形态、每一片叶子的透光都要经得起细看。这种需求下自己写着色器比用现成工具更可控。1.3 场景的整体架构拆解整个场景我把它拆成四个层次来构建从下往上依次是地面层铺满枯竹叶的地面需要做纹理混合和法线扰动让地面看起来不是一块平板竹竿层竹子的主体结构包括竹节、竹枝需要做实例化渲染和风动骨骼竹叶层最复杂的部分需要处理透光、双面渲染、风动波浪氛围层体积光、雾气、粒子飘落的竹叶这是“记忆感”的关键这个分层逻辑的好处是每一层可以独立调试。比如竹叶的透光效果没调好不会影响竹竿的渲染地面的纹理不满意单独换就行。实际开发中我建议先把地面和竹竿搭起来确认整体比例和构图没问题再往上加竹叶和氛围否则很容易陷入“叶子调了半天发现竹子位置不对”的尴尬。提示不要一上来就追求照片级真实。竹林的美感在于“氛围”而不是“细节”先把光影和风动做对比把每片叶子都建模出来重要得多。2. 竹子的几何建模与实例化策略2.1 单棵竹子的建模要点竹子和普通树木最大的区别在于它是空心圆柱体而且有明显的竹节。这两个特征决定了建模方式。我试过三种方案第一种是用圆柱体加环形切槽简单粗暴但竹节看起来像“套了一圈橡皮筋”不自然。第二种是用曲线放样沿着一条略微弯曲的路径生成管状几何体然后在节点处做半径变化。第三种是程序化生成用噪声函数控制竹竿的弯曲和竹节的凸起。最终我采用的是第二种和第三种结合的方案先用曲线定义竹竿的整体形态每棵竹子都不一样然后在曲线采样点上根据位置计算半径——竹节处半径略微凸起节间略微收缩。这个半径变化不需要太大大概5%到8%就够了太大了像糖葫芦太小了看不出竹节。竹竿的弯曲也很关键。现实中的竹子不是笔直的而是有轻微的S形弯曲而且越往顶部弯曲越明显。我在曲线控制点上加了一个基于高度的偏移函数// 竹竿弯曲控制点生成 function generateBambooCurve(height, bendFactor) { const points []; const segments 12; for (let i 0; i segments; i) { const t i / segments; const y t * height; // 底部弯曲小顶部弯曲大 const bend Math.sin(t * Math.PI) * bendFactor * t; const x bend (Math.random() - 0.5) * 0.1; const z (Math.random() - 0.5) * 0.1; points.push(new THREE.Vector3(x, y, z)); } return new THREE.CatmullRomCurve3(points); }这段代码里bendFactor控制弯曲程度我实测下来0.3到0.6之间比较自然。太小了像塑料管太大了像被风吹弯了腰。2.2 竹叶的几何处理竹叶是整个项目里最折磨人的部分。一片竹叶大概是一个细长的披针形长度10到15厘米宽度1到2厘米。如果用高精度模型一片叶子几百个面一棵竹子几百片叶子整个场景直接卡死。我的做法是用两个交叉的四边形面片来模拟一片叶子每个面片只有4个顶点。这样一片叶子8个顶点、4个三角形一棵竹子按200片叶子算也才1600个三角形。几百棵竹子加起来三角形数量控制在50万以内现代GPU完全扛得住。但低模的代价是叶子看起来“扁”。解决办法是在着色器里做法线扰动让叶子表面看起来有起伏。具体来说在片元着色器里根据UV坐标计算一个伪法线// 竹叶法线扰动 vec3 perturbNormal(vec3 normal, vec2 uv) { float noise sin(uv.x * 20.0) * cos(uv.y * 15.0) * 0.1; return normalize(normal vec3(noise, noise * 0.5, noise)); }这个扰动幅度不能大0.1左右就够了目的是让叶子在光照下有一点点明暗变化看起来不是完全平的。2.3 实例化渲染的性能账几百棵竹子如果每棵都单独draw call浏览器直接跪。必须用InstancedMesh。Three.js的InstancedMesh允许你用一次draw call渲染大量相同几何体的不同实例。但这里有个问题每棵竹子的弯曲形态不一样如果几何体完全相同看起来会很假。我的解决方案是准备5到8种不同弯曲形态的竹竿几何体然后用InstancedMesh分别实例化。这样虽然draw call从1变成了8但相比几百次还是天壤之别。而且8种形态随机分布后视觉上几乎看不出重复。竹叶也是同样的逻辑。我准备了大概20种不同大小、不同朝向的叶子簇每簇包含5到10片叶子然后用InstancedMesh实例化到每棵竹子的枝干上。方案Draw Call数量视觉重复感实现复杂度每棵独立Mesh300无低单一InstancedMesh1极强低多形态InstancedMesh8-10弱中多形态叶子簇30-40几乎无高实测下来30到40个draw call在现代浏览器上跑60帧毫无压力视觉上也没有明显的重复感。这个平衡点我觉得找得比较准。注意InstancedMesh的矩阵更新是有开销的。如果每帧都要更新所有实例的矩阵比如做风动性能会急剧下降。我的做法是把风动逻辑放到顶点着色器里用uniform传入时间和风力参数这样CPU端完全不需要更新矩阵。3. 光影氛围与风动系统的实现细节3.1 竹林光影的核心透光与阴影竹林的光影和普通树林完全不同。普通树林的光影是“斑驳”的竹林的光影是“丝缕”的——因为竹叶是细长的光线穿过竹叶缝隙后形成的是细长的光带而不是圆形的光斑。要还原这种效果阴影贴图的分辨率很关键。分辨率太低光带糊成一团分辨率太高性能吃不消。我试过1024、2048、4096三档最终选了2048。4096在移动端直接卡死1024时光带边缘全是锯齿。但光有阴影还不够。竹叶的透光性是竹林氛围的灵魂。现实中的竹叶在逆光下是半透明的能看到叶脉的轮廓。这个效果在着色器里用次表面散射的简化模型来实现// 简化的竹叶透光计算 float translucency(vec3 lightDir, vec3 viewDir, vec3 normal) { float backLight max(0.0, dot(viewDir, -lightDir)); float trans pow(backLight, 3.0) * 0.6; return trans; }这个公式的意思是当视线方向和光线方向相反时也就是逆光看叶子透光最强。pow(backLight, 3.0)让透光集中在逆光角度0.6是透光强度系数。实测下来这个系数在0.4到0.7之间比较自然太高了叶子像塑料片太低了没有透光感。3.2 风动系统的分层设计竹林的风动不是简单的“左右摇摆”。我观察过真实的竹林风动至少有三个层次第一层是竹竿的整体弯曲。风大的时候整棵竹子会向一个方向倾斜然后弹回来。这个用顶点着色器里的正弦函数就能模拟// 竹竿风动 float windOffset sin(time * windSpeed position.y * 0.5) * windStrength * pow(position.y / height, 2.0);注意pow(position.y / height, 2.0)这一项——它让竹竿底部的偏移接近零顶部的偏移最大。这符合物理规律竹子根部是固定的。第二层是竹叶的独立摆动。竹叶比竹竿轻得多风一吹就翻飞。这个用更高频率的正弦波叠加随机相位来实现// 竹叶风动 float leafWind sin(time * 3.0 instanceId * 1.7) * 0.3 sin(time * 5.0 instanceId * 2.3) * 0.2;两个不同频率的正弦波叠加产生一种“不规律”的摆动感。instanceId作为相位偏移让每片叶子的摆动不同步。第三层是整片竹林的波浪效果。这个是最微妙的。当风吹过整片竹林时会形成一个从远到近的波浪。实现方式是在顶点着色器里根据竹子的世界坐标计算一个延迟// 竹林波浪 float waveDelay worldPosition.x * 0.1 worldPosition.z * 0.15; float wave sin(time * windSpeed - waveDelay) * windStrength;这个waveDelay让不同位置的竹子在不同时间开始摆动形成波浪推进的效果。系数0.1和0.15是我反复调试出来的太大了波浪太快像多米诺骨牌太小了看不出波浪。3.3 体积光与雾气的氛围加持竹林里那种“光线从竹叶缝隙中射下来”的效果靠普通的平行光阴影是做不出来的。必须加体积光。Three.js里做体积光有几种方案。最简单的是用God Ray后处理在屏幕空间做径向模糊。但这种方法的问题是它不区分光源整个屏幕都会泛光。我最终用的是基于射线步进的体积光在片元着色器里沿着视线方向采样阴影贴图累加透光量。这个方案的计算量不小但竹林场景的深度范围不大大概20到30米步进次数控制在32步以内性能可以接受。关键是降采样——体积光计算在半分辨率下进行然后上采样混合性能直接翻倍视觉上几乎看不出差别。雾气方面我用的是指数雾而不是线性雾。指数雾的公式是float fogFactor 1.0 - exp(-fogDensity * fogDensity * depth * depth);fogDensity控制雾的浓度我设在0.02到0.05之间。指数雾的好处是近处清晰、远处快速模糊符合竹林里那种“深处看不清”的感觉。线性雾太均匀了看起来像一层纱不自然。实操心得体积光和雾气的颜色要统一。我一开始体积光用暖色、雾气用冷色结果画面看起来像“冰火两重天”。后来统一成偏青绿的色调整个场景的氛围感立刻对了。竹林的色调应该是“冷绿中带一点暖黄”这是竹叶透光和地面枯叶反射共同作用的结果。4. 地面细节与粒子系统的补充4.1 枯叶地面的纹理混合竹林的地面不是泥土是厚厚一层枯黄的竹叶。这个细节如果做不好整个场景的“记忆感”就丢了一半。我的做法是用三张纹理混合一张是枯叶的漫反射贴图一张是法线贴图一张是粗糙度贴图。然后在着色器里根据高度和噪声做混合// 地面纹理混合 float blend smoothstep(0.3, 0.7, noise(uv * 5.0)); vec3 albedo mix(leafColor1, leafColor2, blend); float roughness mix(0.8, 0.95, blend);枯叶的颜色不能太鲜艳否则看起来像秋天的枫叶。我用的色值是#8B7355和#6B5B3E偏灰偏暗符合腐殖质的色调。法线贴图的作用是让地面看起来有“厚度”。枯叶堆积的地面不是平的踩上去会陷下去。法线贴图的强度我设在0.5左右太强了像石头地面太弱了像一张纸。4.2 飘落竹叶的粒子系统氛围感的最后一环是飘落的竹叶。这个用Three.js的Points或者InstancedMesh都能做我选的是InstancedMesh因为每片叶子需要独立的旋转。粒子的运动逻辑是从竹叶层随机位置生成受重力和风力影响下落落地后消失并重新生成。关键是下落速度要慢竹叶不是石头它是飘着下来的// 飘落竹叶的运动 leaf.position.y - 0.5 * deltaTime; // 下落速度 leaf.position.x Math.sin(time leaf.phase) * 0.3 * deltaTime; // 左右飘 leaf.rotation.x 0.5 * deltaTime; // 翻转 leaf.rotation.z 0.3 * deltaTime;下落速度0.5米/秒左右左右飘的幅度0.3翻转速度0.5弧度/秒。这些参数我调了很多次核心原则是“慢”和“不规则”。太快了像下雪太规则了像机器人在撒叶子。粒子的数量控制在50到100片之间。太少了没有氛围太多了性能吃不消而且看起来像暴风雪。4.3 后期处理的克制使用后期处理是双刃剑。用好了画龙点睛用多了画蛇添足。我最终只保留了三个后期效果Bloom强度0.3半径0.5。让透光的部分微微泛光模拟眼睛在暗环境下的光晕感色调映射用ACES filmic让高光过渡更自然暗角强度0.2让视线集中在画面中心没有用景深因为竹林场景的深度层次已经很丰富了再加景深反而让远处的竹子糊成一片。也没有用运动模糊静态场景不需要。注意Bloom的阈值要设高一点只让最亮的部分泛光。我一开始阈值设太低整个画面像蒙了一层雾后来调到0.8才正常。5. 常见问题与排查实录5.1 性能断崖式下跌的排查思路这个问题我踩过最大的坑。场景搭好后在台式机上跑60帧很稳一到笔记本上直接掉到15帧。排查过程分享出来希望能帮你省点时间。第一步确认瓶颈在CPU还是GPU。打开Chrome的Performance面板看帧时间分布。如果GPU时间远大于CPU时间说明是渲染瓶颈反之则是逻辑瓶颈。第二步如果是GPU瓶颈逐层禁用渲染。先关粒子再关体积光再关竹叶最后关竹竿。每关一层看帧率变化。我最后定位到是体积光的步进次数太多从64步降到32步后帧率直接翻倍。第三步如果是CPU瓶颈检查矩阵更新和draw call。用renderer.info查看draw call数量。如果超过100说明实例化没做好。如果矩阵更新耗时太长把风动逻辑移到着色器里。症状可能原因解决方案帧率突然从60掉到20体积光步进过多降到32步半分辨率渲染移动端直接白屏纹理分辨率过高降到1024用压缩纹理竹子看起来在“抖动”顶点着色器精度不够用highp精度竹叶边缘有黑边双面渲染透明混合用alpha test代替alpha blend阴影边缘锯齿严重阴影贴图分辨率低提到2048加PCF软阴影5.2 竹叶渲染的透明排序问题竹叶是半透明的必须开透明混合。但透明物体在3D渲染里有个经典问题排序。如果排序不对前面的叶子会被后面的叶子遮挡看起来像“鬼影”。Three.js默认按物体中心点排序但竹叶是实例化渲染的所有叶子在同一个InstancedMesh里排序完全失效。我的解决方案是用alpha test代替alpha blend。设置一个阈值比如0.5低于阈值的像素直接丢弃高于阈值的完全不透明。这样就不需要排序了而且性能更好。代价是叶子边缘会有硬边。解决办法是在纹理的alpha通道里做一点羽化让边缘过渡稍微柔和一点。虽然不如真正的透明混合完美但在竹林这种密集场景里几乎看不出区别。5.3 风动效果的“塑料感”怎么破我一开始做的风动效果竹子像塑料管一样整体摇摆完全没有竹子的“韧性”。后来发现问题出在频率和阻尼上。真实的竹子在风停之后不会立刻停止摆动而是会继续振荡几秒钟振幅逐渐衰减。这个叫阻尼振荡。在着色器里模拟阻尼振荡// 阻尼振荡 float dampedOsc(float t, float freq, float damping) { return sin(t * freq) * exp(-damping * t); }damping控制衰减速度我设在0.5到1.0之间。太小了竹子会一直晃太大了像浸在水里。另外竹子的摆动不是单一频率的。我叠加了两个频率一个低频0.5Hz控制整体弯曲一个高频3Hz控制竹叶的颤动。两个频率的振幅比大概是3:1低频主导高频点缀。5.4 色彩管理的坑这个问题很隐蔽但影响巨大。Three.js默认的颜色空间是sRGB但如果你在着色器里直接输出颜色不做转换整个画面会偏暗偏灰。正确的做法是在着色器里用线性空间计算最后输出时转sRGB。Three.js的outputEncoding设为sRGBEncoding然后在自定义着色器的最后加上gl_FragColor linearToOutputTexel(gl_FragColor);这个转换不做的话竹叶的绿色会看起来“脏脏的”地面的枯黄色会偏暗。我调了整整一个下午才发现是颜色空间的问题。实操心得如果你觉得场景“哪里不对但说不上来”先检查颜色空间。这个问题太常见了而且一旦对了整个画面的质感会提升一个档次。6. 从记忆到场景的转化经验6.1 记忆中的竹林和现实中的竹林做这个项目最大的挑战不是技术是把记忆中的感觉翻译成技术参数。我记忆中的竹林是“幽暗”的但实际测量现实竹林的亮度其实并不暗只是对比度高——竹叶缝隙透下来的光很亮但竹叶本身和地面很暗。这种高对比度在视觉上产生了“幽暗”的感觉。所以我在调光照的时候没有降低整体亮度而是拉高了对比度。主光源强度设到2.0环境光降到0.3让亮的地方过曝、暗的地方接近黑。这样出来的画面才有“记忆中的感觉”。另一个例子是颜色。我记忆中的竹林是“翠绿”的但实际取色发现竹叶的绿色其实偏黄偏灰饱和度并不高。如果按记忆中的“翠绿”来调画面会像塑料假树。最终我用的绿色饱和度只有30%左右明度中等看起来反而更“真”。6.2 声音设计的缺失与补充这个项目是纯视觉的没有声音。但竹林的声音是氛围感的重要组成部分。如果后续要扩展我建议加三层声音底噪层竹叶摩擦的沙沙声循环播放音量随风力参数变化事件层偶尔的竹竿碰撞声竹子互相摩擦会发出“吱呀”声环境层远处的鸟鸣或虫鸣增加空间感声音的实现可以用Web Audio API把风力参数映射到音量和滤波频率上。风大的时候沙沙声更响、高频更多风小的时候声音低沉、稀疏。6.3 这个项目还能怎么扩展如果你也想做类似的项目或者想在这个基础上继续发展我有几个方向可以分享方向一交互式探索。加一个第一人称控制器让用户可以在竹林里走动。这需要处理碰撞检测和LOD切换复杂度会上升不少但沉浸感会强很多。方向二时间变化。加一个昼夜循环不同时间段的光影完全不同。清晨的竹林有雾气正午的竹林光影最锐利傍晚的竹林偏暖色调。这个用uniform控制光源方向和颜色就能实现。方向三季节变化。春天的竹林有竹笋夏天的竹林最茂密秋天的竹林有枯叶冬天的竹林有积雪。每个季节需要不同的纹理和粒子效果工作量不小但很有意思。方向四程序化生成。现在的竹子位置是手动摆放的如果改成程序化生成可以用噪声函数控制竹子的密度分布做出“林间空地”或者“密林深处”的效果。这个在游戏开发里很常用。我个人最想做的其实是方向二因为竹林的光影变化太丰富了固定一个时间点有点浪费。而且昼夜循环的技术难度不高主要是调色和光源方向的插值性价比很高。6.4 给后来者的几句实在话如果你也想做一个“记忆中的场景”我的建议是先做减法再做加法。一开始不要想着把所有的细节都做出来。先把最核心的几样东西做对光影的方向、颜色的基调、风动的节奏。这三样对了场景的“魂”就有了。剩下的竹节、叶脉、地面纹理都是锦上添花。另外多拍参考照片。不要靠记忆调参数记忆会骗人。去公园找一片竹林拍几张照片在PS里取色看看真实的竹叶到底是什么颜色。我敢保证和你记忆中的颜色差很多。最后不要追求完美。这个项目我做了三个星期到现在还有不满意的地方。但每次打开浏览器看到那片竹林在风中摇曳那种感觉就对了。技术是为感觉服务的感觉对了技术上的小瑕疵都可以接受。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询