当前位置: 首页 > news >正文

Three.js 草地着色器教程

草地着色器 ·Grass Shader· ▶ 在线运行案例

  • 案例合集:三维可视化功能案例(threehub.cn)
  • 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
  • 400个案例代码:网盘链接

你将学到什么

  • 程序化生成草叶三角面(非模型导入)
  • 自定义GrassGeometry extends BufferGeometry
  • 顶点着色器sin 风摆+ 片元云影采样
  • gl_VertexID % 5区分草尖与草身

效果说明

半径 50 的圆盘上10 万根草叶随风摇摆,云纹理在草面缓慢漂移,底部圆形地面共用同一 shader。

核心概念

草叶几何

每根草5 顶点 / 3 三角面:底边两角 + 中段两角 + 尖端。随机:

  • yaw:绕 Y 轴朝向
  • bend:尖端弯曲方向
  • height:高度 ± 随机变化
class GrassGeometry extends THREE.BufferGeometry { constructor(size, count) { for (let i = 0; i < count; i++) { const radius = (size / 2) * Math.random(); const theta = Math.random()2Math.PI; const x = radius * Math.cos(theta); const y = radius * Math.sin(theta); const blade = this.computeBlade([x, 0, y], i); // push positions, uvs, indices } } }

顶点风动

bool isTip = (gl_VertexID + 1) % 5 == 0;

float waveDistance = isTip ? 0.3 : 0.1; vPosition.x += sin((uTime / 500.0) + uv.x10.0)waveDistance;

草尖摆动幅度更大,视觉更自然。

片元着色

  • vPosition.y混深浅绿
  • texture2D(uCloud, vUv)叠云影(UV 随 uTime 平移)
  • 简单法线点乘做明暗

实现步骤

  • 定义BLADE_WIDTH/HEIGHT等常量
  • GrassGeometry生成 position / uv / index
  • ShaderMaterial写 vertex + fragment,uniform:uTimeuCloud
  • Grass类继承 Mesh,update(time)更新 uTime
  • 同材质CircleGeometry作地面
  • 代码要点

    import * as THREE from 'three'

    import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

    const box = document.getElementById('box') const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera(75, box.clientWidth / box.clientHeight, 0.1, 1000) camera.position.set(0, 10, 10) const renderer = new THREE.WebGLRenderer({ antialias: true , alpha: true, logarithmicDepthBuffer: true}) renderer.setSize(box.clientWidth, box.clientHeight) box.appendChild(renderer.domElement) new OrbitControls(camera, renderer.domElement) window.onresize = () => { renderer.setSize(box.clientWidth, box.clientHeight) camera.aspect = box.clientWidth / box.clientHeight camera.updateProjectionMatrix() } scene.background = new THREE.CubeTextureLoader().load([0, 1, 2, 3, 4, 5].map(k => (FILE_HOST + 'files/sky/skyBox0/' + (k + 1) + '.png')));

    let grass = null animate() function animate(time) { if(grass) grass.update(time); requestAnimationFrame(animate) renderer.render(scene, camera) }

    const BLADE_WIDTH = 0.1 const BLADE_HEIGHT = 0.8 const BLADE_HEIGHT_VARIATION = 0.6 const BLADE_VERTEX_COUNT = 5 const BLADE_TIP_OFFSET = 0.1

    function interpolate(val, oldMin, oldMax, newMin, newMax) { return ((val - oldMin) * (newMax - newMin)) / (oldMax - oldMin) + newMin }

    class GrassGeometry extends THREE.BufferGeometry { constructor(size, count) { super()

    const positions = [] const uvs = [] const indices = []

    for (let i = 0; i < count; i++) { const surfaceMin = (size / 2) * -1 const surfaceMax = size / 2 const radius = (size / 2) * Math.random() const theta = Math.random()2Math.PI

    const x = radius * Math.cos(theta) const y = radius * Math.sin(theta)

    uvs.push( ...Array.from({ length: BLADE_VERTEX_COUNT }).flatMap(() => [ interpolate(x, surfaceMin, surfaceMax, 0, 1), interpolate(y, surfaceMin, surfaceMax, 0, 1) ]) )

    const blade = this.computeBlade([x, 0, y], i) positions.push(...blade.positions) indices.push(...blade.indices) }

    this.setAttribute( 'position', new THREE.BufferAttribute(new Float32Array(positions), 3) ) this.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2)) this.setIndex(indices) this.computeVertexNormals() }

    // Grass blade generation, covered in https://smythdesign.com/blog/stylized-grass-webgl // TODO: reduce vertex count, optimize & possibly move to GPU computeBlade(center, index = 0) { const height = BLADE_HEIGHT + Math.random() * BLADE_HEIGHT_VARIATION const vIndex = index * BLADE_VERTEX_COUNT

    // Randomize blade orientation and tip angle const yaw = Math.random()Math.PI2 const yawVec = [Math.sin(yaw), 0, -Math.cos(yaw)] const bend = Math.random()Math.PI2 const bendVec = [Math.sin(bend), 0, -Math.cos(bend)]

    // Calc bottom, middle, and tip vertices const bl = yawVec.map((n, i) => n(BLADE_WIDTH / 2)1 + center[i]) const br = yawVec.map((n, i) => n(BLADE_WIDTH / 2)-1 + center[i]) const tl = yawVec.map((n, i) => n(BLADE_WIDTH / 4)1 + center[i]) const tr = yawVec.map((n, i) => n(BLADE_WIDTH / 4)-1 + center[i]) const tc = bendVec.map((n, i) => n * BLADE_TIP_OFFSET + center[i])

    // Attenuate height tl[1] += height / 2 tr[1] += height / 2 tc[1] += height

    return { positions: [...bl, ...br, ...tr, ...tl, ...tc], indices: [ vIndex, vIndex + 1, vIndex + 2, vIndex + 2, vIndex + 4, vIndex + 3, vIndex + 3, vIndex, vIndex + 2 ] } } }

    const cloudTexture = new THREE.TextureLoader().load(FILE_HOST + 'threeExamples/shader/cloud.jpg') cloudTexture.wrapS = cloudTexture.wrapT = THREE.RepeatWrapping

    class Grass extends THREE.Mesh { constructor(size, count) { const geometry = new GrassGeometry(size, count) const material = new THREE.ShaderMaterial({ uniforms: { uCloud: { value: cloudTexture }, offsetX: { value: 0.5 }, offsetY: { value: 0.3 }, uTime: { value: 0 }, }, side: THREE.DoubleSide, vertexShader:uniform float uTime; uniform float offsetX; uniform float offsetY; varying vec3 vPosition; varying vec2 vUv; varying vec3 vNormal; float wave(float waveSize, float tipDistance, float centerDistance) { // Tip is the fifth vertex drawn per blade bool isTip = (gl_VertexID + 1) % 5 == 0; float waveDistance = isTip ? tipDistance : centerDistance; return sin((uTime / 500.0) + waveSize) * waveDistance; } void main() { vPosition = position; vUv = uv; // Cloud shadow move vUv.x += uTime0.0001offsetX; vUv.y += uTime0.0001offsetY; vNormal = normalize(normalMatrix * normal); if (vPosition.y < 0.0) { vPosition.y = 0.0; } else { vPosition.x += wave(uv.x * 10.0, 0.3, 0.1); } gl_Position = projectionMatrixmodelViewMatrixvec4(vPosition, 1.0); }, fragmentShader:uniform sampler2D uCloud; uniform float uTime; varying vec3 vPosition; varying vec2 vUv; varying vec3 vNormal; vec3 green = vec3(0.2, 0.6, 0.3); void main() { vec3 color = mix(green * 0.7, green, vPosition.y); color = mix(color, texture2D(uCloud, vUv).rgb, 0.4); float lighting = normalize(dot(vNormal, vec3(10))); gl_FragColor = vec4(color + lighting * 0.03, 1.0); }, }) super(geometry, material) const floor = new THREE.Mesh( new THREE.CircleGeometry(size / 2, 8).rotateX(Math.PI / 2), material ) floor.position.y = -Number.EPSILON this.add(floor)

    } update(time) { this.material.uniforms.uTime.value = time } }

    grass = new Grass(50, 100000); scene.add(grass);

    完整源码:GitHub

    小结

    • 本文提供草地着色器完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
    • 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库
http://www.cnnetsun.cn/news/3215127.html

相关文章:

  • Frida-Trace实战:从动态追踪到精准Hook的Android逆向进阶指南
  • 2026大模型API聚合平台横向评测:企业与开发者如何选择稳定的AI网关方案
  • Claude正版使用指南:从环境配置到代码实战完整方案
  • 智推时代完成数千万元天使轮融资,GEO市场2030年规模或突破500亿!
  • 赋能增安型7/8多芯防爆连接器适用范围
  • 暗黑破坏神2现代化补丁D2DX:让经典游戏在现代电脑上焕然新生
  • 国内首场Houdini 22技术大会落地深圳
  • 标记接口和深拷贝
  • 电商站点内嵌伪造支付页钓鱼攻击机理与全域防御体系研究
  • 重新定义分子智能:ChemBERTa如何颠覆化学研究的AI范式
  • 具身智能体在线归因系统:让机器人实时诊断导航异常
  • 数字人本地部署实战:从零搭建免费可控的数字人生成系统
  • AI 不会彻底取代程序员,但一定会淘汰原地踏步的普通程序员
  • 正版Claude完整使用指南:从注册到API集成实战
  • Linux环境变量学习心得:弄懂变量、PATH与数组
  • 论人类原生系统实证科学的东方源头 —— 以《墨经》为核心证据解构古希腊人造科学叙事
  • Calibre NoTrans插件:彻底解决中文路径乱码问题的完美方案
  • 一台两年没洗的空调,三天吹出“大白肺“——洗滤网≠洗空调
  • Linux 提示 No space left on device 怎么快速清理?
  • HALCON vs OpenCV 亚像素轮廓提取对比:4类工业零件实测精度与速度
  • Java毕设项目:基于 SpringBoot 的在线学习笔记与答疑系统的设计与实现 基于 SpringBoot 的课程资源发布与学习系统 (源码+文档,讲解、调试运行,定制等)
  • AI文档翻译版面还原技术:从OCR到排版重建全解析
  • AI Search × ES Agent Builder 最佳实践:企业智能助手落地指南
  • Sub2API 云服务器部署完整教程
  • AI绘画:2025年主流AI绘画工具全景盘点
  • GHelper终极指南:华硕笔记本性能调校的轻量化解决方案
  • ChatGPT分析Excel时,我常用的5种提问方式
  • MQTT-Proxy部署架构:从单节点到大规模集群的演进路径
  • 3 种汽车电动车窗电机驱动方案对比:继电器 vs H桥 vs LIN总线
  • 摩托车发动机重油污扫码PDA选型:海雅达Model 4 DPM手持终端评估