首页 > 基础资料 博客日记

从分形到森林——使用 Three.js 创建逼真的 3D 树木

2026-04-06 23:00:02基础资料围观1

极客资料网推荐从分形到森林——使用 Three.js 创建逼真的 3D 树木这篇文章给大家,欢迎收藏极客资料网享受知识的乐趣

本文是由 EZ-Tree 作者撰写的一篇文章的译文。EZ-Tree 是一款基于 three.js 的插件,能够生成高度逼真的树木模型。本文详细阐述了作者在创作 EZ-Tree 过程中的一些实践经历与核心思路,读者可从中汲取相关技术知识,获取有益的创作灵感。

探索 EZ-Tree 如何利用程序生成和 Three.js 创建逼真的 3D 树模型背后的算法。

自从14岁开始学习编程以来,我就一直对如何用代码模拟现实世界着迷。大学二年级时,我挑战自己,尝试编写一个能够生成树的3D模型的算法。这是一个有趣的实验,也取得了一些有趣的成果,但最终代码还是被束之高阁,尘封在了我的移动硬盘深处。

几年后,我重新发现了那段原始代码,并决定将其移植到 JavaScript(并进行了一些改进!),以便可以在 Web 上运行它。

最终成果是EZ-Tree,一个基于 Web 的应用程序,您可以在其中设计自己的 3D 树,并将其导出为 GLB 或 PNG 格式,以便在您自己的 2D/3D 项目中使用。您可以在这里找到 GitHub 代码库。EZ-Tree 使用Three.js进行 3D 渲染,Three.js 是一个基于 WebGL 的流行库。

在本文中,我将详细介绍我用来生成这些树的算法,并解释每个部分如何对最终的树模型做出贡献。

什么是程序生成?

首先,了解什么是程序生成可能会有所帮助。

程序生成本质上就是根据一组数学规则创建“某物”。以树为例,我们首先观察到的是,树干会分叉成一个或多个树枝,每个树枝又会分叉成一个或多个树枝,以此类推,最终形成一片树叶。从数学/计算机科学的角度来看,我们可以将其建模为一个递归过程。

让我们继续以这个例子为例。

如果我们观察自然界中树木的一根树枝,我们会发现一些事情。

  • 分支的半径和长度都比它所连接的分支要小。
  • 树枝的粗细向末端逐渐变细。
  • 根据树的种类,树枝可以是笔直的,也可以是扭曲的,向各个方向弯曲。
  • 枝条往往会朝着阳光的方向生长。
  • 树枝从树干水平伸展时,重力会将它们向下拉向地面。这种拉力的大小取决于树枝的粗细和树叶的数量。

所有这些观察结果都可以被归纳成各自的数学规则。然后,我们可以将所有规则组合起来,创造出类似树枝的形状。这就是所谓的涌现行为,它指的是许多简单的规则可以组合在一起,创造出比各个部分更复杂的事物。

L系统

数学中有一个领域试图将这类自然过程形式化,称为林登迈尔系统,或更常见的L系统。L系统是一种创建复杂模式的简单方法,常用于模拟植物、树木和其他自然现象的生长。它们从一个初始字符串(称为公理)开始,并反复应用一组规则来重写该字符串。这些规则定义了字符串的每个部分如何转换为新的序列。然后,可以使用绘图指令将生成的字符串转换为视觉模式。

虽然我即将向您展示的代码没有使用 L 系统(当时我根本不知道它们),但原理非常相似,两者都基于递归过程。

使用 L 系统生成的树的示例(来源:维基百科)

理论就说到这里,让我们直接来看代码吧!

树生成过程

树的生成过程始于该<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">generate()</font>方法。该方法初始化用于存储分支和叶子几何形状的数据结构,设置随机数生成器(RNG),并通过将树干添加到分支队列来启动该过程。

// The starting point for the tree generation process
generate() {
  // Initialize geometry data
  this.branches = { };
  this.leaves = { };

  // Initialize RNG
  this.rng = new RNG(this.options.seed);

  // Start with the trunk
  this.branchQueue.push(
    new Branch(
      new THREE.Vector3(),              // Origin
      new THREE.Euler(),                // Orientation
      this.options.branch.length[0],    // Length
      this.options.branch.radius[0],    // Radius
      0,                                // Recursion level
      this.options.branch.sections[0],  // # of sections
      this.options.branch.segments[0],  // # of segments
    ),
  );

  // Process branches in the queue
  while (this.branchQueue.length > 0) {
    const branch = this.branchQueue.shift();
    this.generateBranch(branch);
  }
}

<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">Branch</font>数据结构

<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">Branch</font>数据结构保存了生成分支所需的输入参数。每个分支都使用以下参数表示:

  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">origin</font>– 定义三维空间中分支的起始点<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">(x, y, z)</font>
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">orientation</font>– 使用欧拉角指定分支的旋转<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">(pitch, yaw, roll)</font>
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">length</font>– 树枝从根部到顶端的总长度
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">radius</font>– 设置树枝的粗细
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">level</font> – 表示递归深度,主干从第 0 层开始。
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">sectionCount</font>– 定义树干沿其长度方向被分割的次数。
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">segmentCount</font>– 通过设置树干周长周围的分段数来控制平滑度。

了解分支队列

<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">branchQueue</font>是树生成过程中至关重要的一部分。它保存着所有待生成的分支。第一个分支从队列中取出,并生成其几何形状。然后,我们递归地生成<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">Branch</font>子分支的对象,并将它们添加到队列中以便稍后处理。这个过程会一直持续到队列被填满为止。

生成分支

<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">generateBranch()</font>函数是树生成过程的核心。它包含了根据<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">Branch</font>对象中包含的输入创建单个分支几何形状所需的所有规则。

让我们来看一下这个函数的关键部分。

三维几何入门

在生成树枝之前,我们首先需要了解 Three.js 中是如何存储 3D 几何体的。

在表示三维物体时,我们通常使用索引几何体,它通过减少冗余来优化渲染。几何体由四个主要部分组成:

  1. 顶点——三维空间中定义物体形状的点列表。每个顶点都由一个<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">THREE.Vector3</font>包含其 x、y 和 z 坐标的数组表示。这些点构成了几何体的“基本组成单元”。
  2. 索引——一个整数列表,用于定义顶点如何连接形成面(通常是三角形)。索引引用已有的顶点,而不是为每个面存储重复的顶点,从而显著降低内存使用量。例如,三个索引 [0, 1, 2] 使用顶点列表中的第一个、第二个和第三个顶点构成一个三角形。
  3. 法线——“法线”向量描述了顶点在三维空间中的方向;简而言之,就是表面指向的方向。法线对于光照计算至关重要,因为它们决定了光线如何与表面相互作用,从而产生逼真的阴影和高光。
  4. UV坐标——一组二维坐标,用于将纹理映射到几何体上。每个顶点都被赋予一对介于0.0和1.0之间的UV值,这些值决定了图像或材质如何包裹物体表面。这些坐标使纹理能够与几何体的形状正确对齐。

<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">generateBranch()</font>函数逐节生成分支顶点、索引、法线和 UV 坐标,并将结果附加到各自的数组中。

this.branches = {
  verts: [],
  indices: [],
  normals: [],
  uvs: []
};

几何体全部生成后,将这些数组组合成一个网格,该网格完整地表示了树的几何形状及其材质。

                  _<font style="color:rgb(38, 39, 46);background-color:rgb(247, 247, 247);">左图:单个树枝的线框图;右图:应用了简单平面光照模型后的同一树枝。</font>_

从上图可以看出,树枝沿其长度方向由 10 个独立的节段组成,每个节段又有 5 个边(或线段)。我们可以调整树枝的节段数和线段数,从而控制最终模型的细节程度。数值越高,模型越平滑,但性能也会相应降低。

既然如此,让我们深入了解一下树生成算法吧!

初始化

let sectionOrigin = branch.origin.clone();
let sectionOrientation = branch.orientation.clone();
let sectionLength = branch.length / branch.sectionCount;

let sections = [];

for (let i = 0; i <= branch.sectionCount; i++) {
  // Calculate section radius
  // Build section geometry
}

首先,我们初始化分支的起点、方向和长度。接下来,我们定义一个数组来存储分支的各个部分。最后,我们遍历每个部分并生成其几何数据。在遍历每个部分的过程中, x<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">sectionOrigin</font>y 变量都会更新。<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">sectionOrientation</font>

分支截面半径

为了计算树枝的半径,我们首先要计算树枝的整体半径。如果是树枝的最后一节,我们将半径设为零,因为我们希望树枝末端呈尖状。对于其他所有树枝节,我们根据其在树枝上的位置计算出需要收缩的程度(越靠近末端,收缩程度越大),然后将该值乘以前一节的半径。

let sectionRadius = branch.radius;

// If last section, set radius to effectively zero
if (i === branch.sectionCount) {
  sectionRadius = 0.001;
} else {
  sectionRadius *=
    1 - this.options.branch.taper[branch.level] * (i / branch.sectionCount);
}

构建截面几何形状

           <font style="color:rgb(38, 39, 46);background-color:rgb(247, 247, 247);">单个截面的几何体线框图。以下代码为圆柱体的每一面构建三角形对。</font>

由于圆柱体的端部被遮挡,因此保持开放状态。

现在我们已经掌握了足够的信息来构建截面几何体。接下来数学计算会变得稍微复杂一些!你只需要知道,我们使用之前计算出的截面原点、方向和半径来创建每个顶点、法线和UV坐标。

// Create the segments that make up this section.
for (let j = 0; j < branch.segmentCount; j++) {
  let angle = (2.0 * Math.PI * j) / branch.segmentCount;

  const vertex = new THREE.Vector3(Math.cos(angle), 0, Math.sin(angle))
    .multiplyScalar(sectionRadius)
    .applyEuler(sectionOrientation)
    .add(sectionOrigin);

  const normal = new THREE.Vector3(Math.cos(angle), 0, Math.sin(angle))
    .applyEuler(sectionOrientation)
    .normalize();

  const uv = new THREE.Vector2(
    j / branch.segmentCount,
    (i % 2 === 0) ? 0 : 1,
  );

  this.branches.verts.push(...Object.values(vertex));
  this.branches.normals.push(...Object.values(normal));
  this.branches.uvs.push(...Object.values(uv));
}

sections.push({
  origin: sectionOrigin.clone(),
  orientation: sectionOrientation.clone(),
  radius: sectionRadius,
});

这就是创建单个分支几何形状的方法!

然而,单凭这一点并不能生成一棵非常有趣的树。程序生成的树之所以看起来美观,很大程度上取决于各部分之间的相对方向以及整体分支的布局规则。

让我们来看看使分支看起来更有趣的两个参数。

太棒了,老兄!

我最喜欢的参数之一是树干的弯曲程度(gnarliness)。它控制着树枝的扭曲和弯曲程度。在我看来,这对于赋予树木“生命力”而非使其显得死气沉沉、毫无生气至关重要。


左边,低弯曲 右边,高弯曲

树枝的弯曲程度可以用数学方法来表示,即通过控制树枝某一部分与前一部分方向的偏差程度。这些偏差会沿着树枝的长度累积,从而产生一些有趣的现象。

const gnarliness =
  Math.max(1, 1 / Math.sqrt(sectionRadius)) *
  this.options.branch.gnarliness[branch.level];

sectionOrientation.x += this.rng.random(gnarliness, -gnarliness);
sectionOrientation.z += this.rng.random(gnarliness, -gnarliness);

从上面的表达式可以看出,树枝的弯曲程度与树枝的半径成反比。这反映了树木在自然界中的生长规律:较小的树枝比较大的树枝更容易卷曲和扭曲。

我们在一定范围内生成一个随机倾斜角度<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">[-gnarliness, gnarliness]</font>,然后将其应用于下一节的方向。

运用力量!

下一个参数是生长力,它模拟树木如何朝着阳光生长以最大化光合作用(也可以用来模拟重力作用于树枝,使其向地面下坠)。这对于模拟白杨树等树木尤其有用,因为它们的树枝往往笔直向上生长,而不是远离树干。


      左:生长力禁用								右: 设置生长力 + Y 方向

我们定义了一个生长方向(可以想象成指向太阳的光线)和一个生长强度 因子。每个枝条都会进行微小的旋转,使其沿着生长方向排列。旋转的幅度与生长强度因子成正比,与枝条半径成反比,因此较小的枝条受到的影响更大。

const qSection = new THREE.Quaternion().setFromEuler(sectionOrientation);

const qTwist = new THREE.Quaternion().setFromAxisAngle(
  new THREE.Vector3(0, 1, 0),
  this.options.branch.twist[branch.level],
);

const qForce = new THREE.Quaternion().setFromUnitVectors(
  new THREE.Vector3(0, 1, 0),
  new THREE.Vector3().copy(this.options.branch.force.direction),
);

qSection.multiply(qTwist);
qSection.rotateTowards(
  qForce,
  this.options.branch.force.strength / sectionRadius,
);

sectionOrientation.setFromQuaternion(qSection);

上述代码使用四元数来表示旋转,从而避免了使用更常用的欧拉角(俯仰角、偏航角、滚转角)时出现的一些问题。四元数超出了本文的讨论范围,但您只需知道它是一种表示物体在空间中方向的不同方法即可。

附加参数

树干的弯曲度和生长力并非唯二可调参数。以下列出了其他可用于控制树木生长的可调参数。

  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">angle</font>此参数设置子枝相对于父枝的生长角度。通过调整此值,您可以控制子枝是陡峭向上生长(几乎与父枝平行),还是以较大角度向外扇形生长,从而模拟不同类型的树木。
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">children</font>此参数控制从单个父分支生成的子分支数量。增加此值会生成更茂密、更复杂的树状结构,分支密度更高;而减小此值则会生成稀疏、极简的树状结构。
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">start</font>此参数决定子枝从父枝的哪个位置开始生长。数值越低,子枝越靠近父枝的基部生长;数值越高,子枝越靠近父枝的顶端生长,从而形成不同的生长模式。
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">twist</font>– “扭曲”参数会对树枝的几何形状进行绕其轴线的旋转调整。通过修改此值,您可以引入螺旋效果,为树枝增添动态、自然的视觉效果,模拟树木扭曲或弯曲的生长形态。

你还能想到其他需要补充的吗?

生成子分支

分支几何形状生成完毕后,接下来就要生成它的子分支了。

if (branch.level === this.options.branch.levels) {
  this.generateLeaves(sections);
} else if (branch.level < this.options.branch.levels) {
  this.generateChildBranches(
    this.options.branch.children[branch.level],
    branch.level + 1,
    sections);
}

如果我们已经到了递归的最后一层,那么我们生成的是叶节点而不是分支。稍后我们会详细介绍叶节点的生成过程,但它与生成子分支的方式其实差别不大。

如果尚未到达递归的最后一层,则调用该<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">generateChildBranches()</font>函数。

generateChildBranches(count, level, sections) {
  for (let i = 0; i < count; i++) {

    // Calculate the child branch parameters...

    this.branchQueue.push(
      new Branch(
        childBranchOrigin,
        childBranchOrientation,
        childBranchLength,
        childBranchRadius,
        level,
        this.options.branch.sections[level],
        this.options.branch.segments[level],
      ),
    );
  }
}

该函数遍历每个子分支,生成填充<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">Branch</font>数据结构所需的值,并将结果附加到该分支<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">branchQueue</font>,然后由<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">generateBranch()</font>我们在上一节中讨论的函数进行处理。

<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">generateChildBranches()</font>函数需要几个参数

  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">count</font>– 要生成的子分支数量
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">level</font>– 当前的递归级别,以便我们知道是否需要<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">generateChildBranches()</font>再次调用,或者是否应该就此停止。
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">sections</font>这是父分支的节数据数组。它包含节的起点和方向,这些信息将用于帮助确定子分支的放置位置。

计算子分支参数

让我们来详细分析一下每个子分支参数是如何计算的。

起源

// Determine how far along the length of the parent branch the child
// branch should originate from (0 to 1)
let childBranchStart = this.rng.random(1.0, this.options.branch.start[level]);

// Find which sections are on either side of the child branch origin point
// so we can determine the origin, orientation and radius of the branch
const sectionIndex = Math.floor(childBranchStart * (sections.length - 1));
let sectionA, sectionB;
sectionA = sections[sectionIndex];
if (sectionIndex === sections.length - 1) {
  sectionB = sectionA;
} else {
  sectionB = sections[sectionIndex + 1];
}

// Find normalized distance from section A to section B (0 to 1)
const alpha =
  (childBranchStart - sectionIndex / (sections.length - 1)) /
  (1 / (sections.length - 1));

// Linearly interpolate origin from section A to section B
const childBranchOrigin = new THREE.Vector3().lerpVectors(
  sectionA.origin,
  sectionB.origin,
  alpha,
);

这个方法其实并不像看起来那么复杂。在确定分支位置时,我们首先生成一个介于 0.0 和 1.0 之间的随机数,该随机数表示子分支应该放置在父分支长度的哪个位置。然后,我们找到该点两侧的节段,并通过插值法确定子分支的起始点。

半径

const childBranchRadius =
  this.options.branch.radius[level] *
  ((1 - alpha) * sectionA.radius + alpha * sectionB.radius);

半径的计算逻辑与原点相同。我们查看子分支两侧各段的半径,并对这些值进行插值,从而得到子分支的半径。

方向

// Linearlly interpolate the orientation
const qA = new THREE.Quaternion().setFromEuler(sectionA.orientation);
const qB = new THREE.Quaternion().setFromEuler(sectionB.orientation);
const parentOrientation = new THREE.Euler().setFromQuaternion(
  qB.slerp(qA, alpha),
);

// Calculate the angle offset from the parent branch and the radial angle
const radialAngle = 2.0 * Math.PI * (radialOffset + i / count);
const q1 = new THREE.Quaternion().setFromAxisAngle(
  new THREE.Vector3(1, 0, 0),
  this.options.branch.angle[level] / (180 / Math.PI),
);
const q2 = new THREE.Quaternion().setFromAxisAngle(
  new THREE.Vector3(0, 1, 0),
  radialAngle,
);
const q3 = new THREE.Quaternion().setFromEuler(parentOrientation);

const childBranchOrientation = new THREE.Euler().setFromQuaternion(
  q3.multiply(q2.multiply(q1)),
);

四元数再次发挥作用!在确定子分支方向时,我们需要考虑两个角度。

  • 父分支周围的径向角度。我们希望子分支均匀分布在主分支的圆周上,而不是都指向同一个方向。
  • 子分支与父分支之间的角度。该角度是参数化的,可以进行调整以获得所需的特定效果。

分支角和径向角示意图

这两个角度与父分支的方向结合起来,确定分支在三维空间中的最终方向。

长度

let childBranchLength = this.options.branch.length[level];

最后,分支的长度由用户界面上设置的参数决定。

计算完所有这些值后,我们就拥有了生成子分支所需的足够信息。我们对每个子分支重复此过程,直到生成所有子分支为止。<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">branchQueue</font>现在,该对象已填充了所有子分支数据,这些数据将按顺序处理并传递给<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">generateBranch()</font>函数。

生成叶子

叶片的生成过程与子枝的生成过程几乎完全相同。主要区别在于,叶片是以纹理的形式渲染到四边形(即矩形平面)上的,因此我们不是生成枝条,而是生成一个四边形,并以与枝条相同的方式定位和调整其方向。

为了增加树叶的茂盛度,使树叶从各个角度都能被看到,我们使用了两个四边形而不是一个,并将它们彼此垂直放置。


              透明度禁用                                                                      透明度启用

控制叶片外观的参数有很多。

  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">type</font>我找到了几种不同的叶子纹理,因此可以生成各种不同类型的叶子。
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">size</font>– 控制叶片四边形的整体大小,使叶片变大或变小。
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">count</font> 每个分支要生成的叶子数量
  • <font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">angle</font>– 叶片相对于枝条的角度(类似于枝条<font style="color:rgb(101, 117, 128);background-color:rgb(237, 240, 243);">angle</font>参数)

环境设计

美丽的树需要一个美丽的家,所以我投入了大量精力为 EZ-Tree 打造一个逼真的环境。虽然这并非本文的主题,但我还是想重点介绍一下我添加的一些环境元素,它们让场景更加生动。

如果您想了解更多关于我如何创建该环境的信息,本文顶部/底部提供了源代码链接。

地面

第一步是添加地面。我使用了平滑噪波函数,使其在泥土纹理和草地纹理之间切换。在模拟自然界的任何事物时,始终要注意那些瑕疵;正是这些瑕疵让场景感觉自然真实,而不是生硬虚假。

接下来,我添加了一些云。这些云实际上只是另一种噪波纹理(看出规律了吗?),我将其应用到一个巨大的四边形上,并将该四边形放置在场景上方。为了让云看起来“生动”,我调整了纹理随时间的变化,使其呈现出云朵移动的效果。我选择了一个非常柔和、略带阴天的天空,以免分散人们对场景焦点——那棵树的注意力。

树叶和岩石

为了让地面更丰富多彩,我添加了一些草、石头和花朵。为了提升性能,靠近树的草比较茂密,远离树的草则比较稀疏。我选择了一些带有苔藓的石头模型,这样它们就能更好地融入地面。花朵也为单调的绿色增添了斑斓的色彩。

森林

我们的树感觉有点孤单,所以在应用程序加载时,我生成了 100 棵树(从预设列表中选择),并将它们放置在主树周围。

自然界时刻处于运动状态,因此在树木和草地的建模中模拟这种运动至关重要。我编写了自定义着色器,用于实现草、树枝和树叶的几何动画效果。我定义了风向,然后将几个不同频率和振幅的正弦函数相加,再将结果应用于几何体的每个顶点,从而获得所需的效果。

转存失败,建议直接上传图片文件

下面摘录一段 GLSL 着色器代码,用于控制应用于顶点位置的风偏移量。

vec4 mvPosition = vec4(transformed, 1.0);

float windOffset = 2.0 * 3.14 * simplex3(mvPosition.xyz / uWindScale);
vec3 windSway = uv.y * uWindStrength * (
  0.5 * sin(uTime * uWindFrequency + windOffset) +
  0.3 * sin(2.0 * uTime * uWindFrequency + 1.3 * windOffset) +
  0.2 * sin(5.0 * uTime * uWindFrequency + 1.5 * windOffset)
);
mvPosition.xyz += windSway;

mvPosition = modelViewMatrix * mvPosition;
gl_Position = projectionMatrix * mvPosition;

结论

希望您喜欢我对程序化树木生成器的详细介绍!程序化生成是一个非常棒的领域,它能将艺术和科学结合起来,创造出既美观又实用的东西。

参考链接

https://github.com/dgreenheck/ez-tree

https://www.eztree.dev/

最后,关注公号“ITMan彪叔” 可以添加作者微信进行交流,及时收到更多有价值的文章。

最后,关注公号“ITMan彪叔” 可以添加作者微信进行交流,及时收到更多有价值的文章。


文章来源:https://www.cnblogs.com/flyfox1982/p/19827151
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云