news 2026/7/8 4:54:34

Three.js 草地着色器教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 开源仓库
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/8 4:54:06

Frida-Trace实战:从动态追踪到精准Hook的Android逆向进阶指南

1. 项目概述&#xff1a;从追踪到精准Hook的进阶之路在Android逆向与安全分析的实战中&#xff0c;Frida早已成为我们手中的“瑞士军刀”。然而&#xff0c;很多朋友在熟练使用frida -U -f com.example.app -l script.js这类基础操作后&#xff0c;往往会遇到一个瓶颈&#xff…

作者头像 李华
网站建设 2026/7/8 4:49:27

Claude正版使用指南:从环境配置到代码实战完整方案

&#x1f680; 30款热门AI模型一站整合&#xff0c;DeepSeek/GLM/Qwen 随心用&#xff0c;限时 5 折。 &#x1f449; 点击领海量免费额度 如果你正在寻找使用 Claude 的可靠方法&#xff0c;可能已经发现了一个尴尬的现实&#xff1a;官方渠道限制严格&#xff0c;而各种&q…

作者头像 李华
网站建设 2026/7/8 4:47:26

赋能增安型7/8多芯防爆连接器适用范围

一、危险区域适配范围 气体环境‌&#xff1a;防爆标志多为Ex eb IIC Gb&#xff0c;可安全用于‌Zone 1、Zone 2‌爆炸性气体场所&#xff0c;兼容ⅡA、ⅡB、ⅡC类易燃气体混合物&#xff0c; 覆盖绝大多数常规油气、化工类气体爆炸场景。 粉尘环境‌&#xff1a;符合粉尘防爆…

作者头像 李华