Threejs中的WebGPU实践(1-2)

news2024/9/26 3:29:14

更多精彩内容尽在 dt.sim3d.cn ,关注公众号【sky的数孪技术】,技术交流、源码下载请添加VX:digital_twin123

此处接上文:Threejs中的WebGPU实践(1-1)

顶点着色器设置

现在我们已经对材质系统和 TSL 着色器有了一点熟悉,接下来我们再创建一个更有趣的场景。我们只使用基本的立方体和两个灯光,但利用顶点着色器的强大功能可以将这个基本的立方体转变为由色彩缤纷的旋转立方体组成的场景。
1_s30uvKKPjmRP4yM01t8lyg.gif
首先,我们需要定义一些常量,例如我们想要创建多少个同心圆,以及我们想要在场景中添加多少个立方体。本次实验我们来使用放置在四个同心圆内的八十个立方体网格实例填充场景。

// 要创建的立方体网格实例的数量
const instanceCount = 80;
// 场景中同心圆的数量
const numCircles = 4;
// 将实例数量平均分配到各个圆圈中
const meshesPerCircle = instanceCount / numCircles

const material = new THREE.MeshStandardNodeMaterial();

接下来,缩小立方体几何体的比例,删除网格的默认旋转,并将网格从标准网格切换为实例化网格。实例化网格利用 WebGPU 图形 API 中的特定功能,允许应用程序在一次绘制调用中绘制同一网格的多个实例,从而提高整体渲染性能。

// const geometry = new THREE.BoxGeometry( 1, 1, 1);
// mesh = new THREE.Mesh( geometry, material );
const geometry = new Three.BoxGeometry( 0.1, 0.1, 0.1 );
mesh = new THREE.InstancedMesh( geometry, material, instanceCount );
// mesh.rotation.y += MATH.PI / 4;

最后,我们将从 Three.js 库中导入所需的所有必需的节点功能到我们的项目中,创建一组uniform,并使这些uniform可供 GUI 访问。

import * as THREE from 'three';
// 创建本实验中出现的效果所需的所有节点功能
import { positionGeometry, cameraProjectionMatrix, modelViewProjection, modelScale, positionView, modelViewMatrix, storage, attribute, float, timerLocal, uniform, tslFn, vec3, vec4, rotate, PI2, sin, cos, instanceIndex, negate, texture, uv, vec2, positionLocal, int } from 'three/tsl';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

import GUI from 'three/addons/libs/lil-gui.module.min.js';

function init() {
  
  const effectController = {
    // uniform() 函数创建一个保存统一值的 UniformNode
    uCircleRadius: uniform( 1.0 ),
    uCircleSpeed: uniform( 0.5 ),
    uSeparationStart: uniform( 1.0 ),
    uSeparationEnd: uniform( 2.0 ),
    uCircleBounce: uniform( 0.02 ),
  };

  // ...

  const gui = new GUI();
  gui.add( effectController.uCircleRadius, 'value', 0.1, 3.0, 0.1 ).name( 'Circle Radius' );
  gui.add( effectController.uCircleSpeed, 'value', 0.1, 3.0, 0.1 ).name( 'Circle Speed' );
  gui.add( effectController.uSeparationStart, 'value', 0.5, 4, 0.1 ).name( 'Separation Start' );
  gui.add( effectController.uSeparationEnd, 'value', 1.0, 5.0, 0.1 ).name( 'Separation End' );
  gui.add( effectController.uCircleBounce, 'value', 0.01, 0.2, 0.001 ).name( 'Circle Bounce' );

}

编写顶点着色器

使用 Three.js WebGPURenderer 时,用户可以通过将 TSL 函数分配给材质的 positionNodevertexNode 属性,将顶点着色器应用到网格的 NodeMaterial。为 positionNode 编写TSL 函数仍将遵循已应用于网格的标准模型-视图-投影(MVP) 转换。本质上,这意味着对网格体顶点或网格体整体位置的复杂转换可以像在 Javascript 中执行一样执行。此外,由于这些操作将在材质的顶点着色器中并行执行,因此它们的性能将比同等的 CPU 操作高得多。

使用 vertexNode 时,函数的行为略有不同。该节点将绕过标准 MVP 转换,将 TSL 函数返回的原始值直接输出到顶点着色器。因此,如果将 TSL 函数分配给 vertexNode,则必须在该函数内手动应用 MVP 转换。

为了演示其中的差异,我在下面编写了两个函数,一个用于 vertexNode,另一个用于 positionNode。每个着色器执行相同的操作:沿 x 轴移动网格的位置。

const material = new THREE.MeshStandardNodeMaterial();

// Position 节点方法
material.positionNode = tslFn(() => {

  const position = positionLocal;

  // 沿x轴来回摆动
  const moveX = sin( timerLocal() );

  // 相当于普通 JavaScript 中的 mesh.position.x += Math.sin(time)
  position.x.addAssign( moveX );

  return positionLocal;

})(); 

// Vertex 节点方法
material.vertexNode = tslFn(() => {

  const position = positionLocal;

  position.x.addAssign( sin( timerLocal() ) );

  // 需要应用变换矩阵以使输出相同
  return cameraProjectionMatrix.mul( modelViewMatrix ).mul( position );

})();

1_demqe_jWbVlgmszDpVZsbg.gif
请注意,在两个着色器中,我们如何使用 positionLocal 来访问局部空间中的网格顶点。有多个方便的属性可以访问网格顶点的预转换版本,包括

  • **positionWorld:**由 modelWorldMatrix 转换的网格几何体的位置,它可以缩放、旋转和平移网格顶点。
  • **positionView:**由 modelViewMatrix 转换的网格几何体的位置,它将网格带入视图空间。
  • **modelViewProjection:**对作为参数传递的位置执行标准 MVP 转换。

有了这些额外的节点,我们可以修改 vertexNode 着色器以输出无数网格的顶点,而无需对着色器的视觉输出进行任何更改。

// 用于渲染网格的顶点节点方法
material.vertexNode = tslFn(() => {
  // 方法 1
  return cameraProjectionMatrix.mul( modelViewMatrix ).mul( positionLocal );
  // 方法 2
  return cameraProjectionMatrix.mul( positionWorld );
  // 方法 3
  return modelViewProjection( positionLocal );
})();

由于我们不想扰乱标准 MVP 投影过程,因此我们将为材质的 positionNode 编写顶点着色器。让我们首先删除我们创建的任何示例位置或顶点着色器,然后创建一个新的着色器,稍后将其分配给我们的positionNode。在这个着色器中,让我们提取uniform并创建一些我们将在着色器中重复使用的变量。

const positionTSL = tslFn(() => {

  // uniform可以被解构,因为它们只是 Javascript 中代表uniform的对象变量
  const { uCircleRadius, uCircleSpeed, uSeparationStart, uSeparationEnd, uCircleBounce } = effectController;

  // 访问自着色器创建以来经过的时间
  const time = timerLocal();
  const circleSpeed = time.mul( uCircleSpeed );
  
})

然后,我们需要访问position着色器中的一些实例数据,以正确协调立方体网格每个实例的移动。这意味着我们必须访问当前顶点所属的实例索引。为此,我们所要做的就是访问之前导入的 instanceIndex 值。在分配给positionNodevertexNode的功能块中,instanceIndex值将表示当前顶点所属的网格实例的索引。如果你想知道为什么我明确区分这是它在顶点着色器上下文中的值,那是因为 instanceIndex 是一个上下文节点,其值根据使用它的上下文而变化。虽然目前了解并不重要,但 instanceIndex 可以表示的其他值在以后将变得至关重要。现在,让我们继续将 instanceIndex 添加到我们的position着色器中,并从它的值中导出其他索引。

const positionTSL = tslFn(() => {
  const { uCircleRadius, uCircleSpeed, uSeparationStart, uSeparationEnd, uCircleBounce } = effectController;
  const time = timerLocal();
  const circleSpeed = time.mul( uCircleSpeed );

  // 立方体在其各自同心圆内的索引。
  // 注意:instanceWithinCircle 使用从 0 开始的索引。
  const instanceWithinCircle = instanceIndex.remainder( meshesPerCircle );

  // 立方体网格所属圆的索引。
  // 注意:circleIndex 使用从 1 开始的索引。
  const circleIndex = instanceIndex.div( meshesPerCircle ).add( 1 );

  // Example Values when meshesPerCircle === 20
  //   instanceIndex: 0 ---> instance is cube 0 of circle 1.
  //   instanceIndex: 16 --> instance is cube 16 of circle 1.
  //   instanceIndex: 22 --> instance is cube 2 of circle 1.
  //   instanceIndex: 47 --> instance is cube 7 of circle 2.
})

有了这些索引,我们将使用它们根据这些值来分离和偏移每个网格实例。下面,我们将在函数中添加一小段代码来演示这些值的工作原理。

const positionTSL = tslFn(() => {
  // ...
  const newPosition = positionLocal;

  // 将范围 [0, meshesPerCircle) 的 instanceWithinCircle 归一化到范围 [-1, 1)
  const range = float( instanceWithinCircle ).sub( meshesPerCircle / 2 ).div( meshesPerCircle / 2 )

  // 偏移网格 x.
  newPosition.x.addAssign( range.mul( 2 ) );
  
  // 按circleIndex 偏移网格 y
  newPosition.y.addAssign( int(circleIndex).sub( 2 ) );
})

material.positionNode = positionTSL();

image.png
从结果上看试验成功,那么接下来我们就要完成自己的着色器了。首先,将场景的透视相机的位置向后设置为 15 个单位。然后,删除上面块中的示例代码,并将其替换为这一行,该行根据数字的奇偶校验返回负值或正值。

// 圆索引偶数 = 1,圆索引奇数 = -1.
// Examples:
//   0 -> 0 % 2 = 0 * 2 = 0 - 1 = -1
//   3 -> 3 % 2 = 1 * 2 = 2 - 1 =  1
const evenOdd = circleIndex.remainder( 2 ).mul( 2 ).oneMinus();

接下来,创建一个代表同心圆之一的半径的变量。随着circleIndex 的增加,每个连续圆的半径也会增加。它增加的程度是由我们之前创建并应用于 GUI 的圆半径统一驱动的。

// 当我们进入下一个圆时增加半径
const circleRadius = uCircleRadius.mul( circleIndex );

我们现在需要将立方体网格的每个实例移动到其各自的位置。为此,需要计算从立方体中心到其圆周的每个可能的角度,并将立方体沿着该角度移动到其圆中。此外,我们将缩放外圈中的立方体,使它们逐渐变得比内圈中的立方体更大。

material.positionNode = Fn(() => {
  // ...

  // 将 instanceWithinCircle 置于范围 [0, 2*PI] 以获得 'meshesPerCircle' 从原点到圆周长的角度数
  const angle = float( instanceWithinCircle ).div( meshesPerCircle ).mul( PI2 ).add( circleSpeed );

  // 圆的半径是从位于原点的圆心到它的边缘的距离。
  // 我们所要做的就是用这个半径来缩放角度的x和y方向,将网格实例放置在圆的圆周上。

  // 相反方向旋转偶数圈和奇数圈。
  const circleX = sin( angle ).mul( circleRadius ).mul( evenOdd );
  const circleY = cos( angle ).mul( circleRadius );

  // 将后面的同心圆中的立方体缩放得更大.
  const scalePosition = positionLocal.mul( circleIndex );

  const newPosition = scalePosition.add( vec3( circleX, circleY, 0.0 ));
  return newPosition;
 })(); 

1_eMatDIEQnvIkQ8zYfw8FEg.gif
缩放操作后,让我们随着时间的推移旋转每个单独的立方体。

// 将后面的同心圆中的立方体缩放得更大.
const scalePosition = positionLocal.mul( circleIndex );

// 旋转形成同心圆的各个立方体.
const rotatePosition = rotate( scalePosition, vec3( time, time, time ) );

const newPosition = rotatePosition.add( vec3( circleX, circleY, 0.0 ) );

最后,我们可以通过向每个立方体的位置添加额外的偏移来完成position着色器的完善。

// 最终的 Postion Shader

const positionTSL = tslFn(() => {
  const { uCircleRadius, uCircleSpeed, uSeparationStart, uSeparationEnd, uCircleBounce } = effectController;
  const time = timerLocal();
  const circleSpeed = time.mul( uCircleSpeed );

  const instanceWithinCircle = instanceIndex.remainder( meshesPerCircle );
  const circleIndex = instanceIndex.div( meshesPerCircle ).add( 1 );
  const evenOdd = circleIndex.remainder( 2 ).mul( 2 ).oneMinus();

  const circleRadius = uCircleRadius.mul( circleIndex );
  const angle = float( instanceWithinCircle ).div( meshesPerCircle ).mul( PI2 ).add( circleSpeed );
  const circleX = sin( angle ).mul( circleRadius ).mul( evenOdd );
  const circleY = cos( angle ).mul( circleRadius );

  const scalePosition = positionLocal.mul( circleIndex );
  const rotatePosition = rotate( scalePosition, vec3( time, time, time ) );

  // 控制圆圈垂直弹跳的程度. 
  const bounceOffset = cos( time.mul( 10 ) ).mul( uCircleBounce );

  const bounce = circleIndex.remainder( 2 ).equal( 0 ).cond( bounceOffset, negate( bounceOffset ) );

  const separationDistance = uSeparationEnd.sub( uSeparationStart );

  const sinRange = ( sin( time ).add( 1 ) ).mul( 0.5 );

  const separation = uSeparationStart.add( sinRange.mul( separationDistance ) );

  const newPosition = rotatePosition.add( vec3( circleX, circleY.add( bounce ), float( circleIndex ).mul( separation ) ) );
  return newPosition;

});

material.positionNode = positionTSL();

现在剩下要做的就是将随机颜色应用于网格的每个实例,我们的效果就完成了!

material.positionNode = positionTSL();
//material.colorNode = texture( crateTexture, uv().add( vec2( timerLocal(), negate( timerLocal()) ) ));
const r = sin( timerLocal().add( instanceIndex ) );
const g = cos( timerLocal().add( instanceIndex ) );
const b = sin( timerLocal() );
material.fragmentNode = vec4( r, g, b, 1.0 );

1_s30uvKKPjmRP4yM01t8lyg.gif

结论

现在我们算是刚刚迈出了进入 WebGPURenderer 世界的第一步。在下一个教程中,我们将探索 Three.js 的新计算功能,编写一个计算着色器来并行计算多个粒子的速度。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2053018.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《框架封装 · 优雅接口限流方案》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗 🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数…

权限审批也能这么人性化?没错,可道云teamOS让团队关系更和谐

作为一位企业管理者,我深知权限审批在企业管理中的重要性。它不仅仅是一个简单的流程,更是保障企业信息安全、提升团队协作效率的关键环节。 然而,过去我们常常面临权限审批流程繁琐、效率低下的问题,这不仅影响了我们的工作效率…

如何在 Odoo 16 Studio 模块中自定义视图和报告

为了有效地运营公司,需要定制的软件系统。Odoo 平台提供针对单个应用程序量身定制的管理解决方案和用户友好的界面,以便开发应用程序,而无需更复杂的后端功能。该平台支持使用简单的拖放功能和内置工具创建和修改更多定制的 Odoo 应用程序。企…

ubuntu如何监控Xvfb虚拟显示器

在Ubuntu中监控Xvfb显示器主要涉及到使用VNC服务器来远程访问这个环境。以下是一些基本步骤: 安装Xvfb和相关工具: 使用apt安装Xvfb和x11vnc,x11vnc是一个VNC服务器,可以远程访问Xvfb创建的虚拟桌面环境。 sudo apt-get install xvfb sudo ap…

Ciallo~(∠・ω・ )⌒☆第十九篇 mysql windows、Ubuntu安装与远程连接配置

一、安装windows版本的mysql (一)、安装mysql 1. 2. 3. 4. 5. (二)、测试mysql 这些步骤完成后记得去配置环境变量,path为mysql的安装目录这里我选择的是默认路径: C:\Program Files\MySQL\MySQL Serve…

零基础学习Redis(5) -- redis单线程模型介绍

前面我们提到过,redis是单线程的,这期我们详细介绍一下redis的单线程模型 1. redis单线程模型 redis只使用一个线程处理所有的请求,并不是redis服务器进程内部只有一个线程,其实也存在多个线程,只不过多个线程是在处…

MySQL常用函数、语法案例

本人MySQL5.7版本 表结构 假设有一个名为 order_summary 的表,其字段如下: order_id (INT): 订单的唯一标识符 customer_id (VARCHAR): 顾客的唯一标识符 order_date (DATETIME): 订单创建时间 total_amount (DECIMAL): 订单总金额 payment_status (E…

贪心+多维度dp

前言:处理简单版本的时候,想到了贪心,以及暴力求解顺便剪枝一下,要注意边界问题 haed版本的时候,完全行不通了,m的范围到了200,这是不可以暴力求解的 但是我不知道如何定义状态转移方程&#…

Hutool糊涂包JSON相关方法汇总

目录 1. JSON 对象 (JSONObject) 的创建 2. 向 JSONObject 添加键值对 3. 从 JSONObject 获取值 4. JSON 对象与字符串之间的转换 5. JSON 对象与 Java Bean(POJO)之间的转换 6. JSON 数组 (JSONArray) 的使用 7. JSON 数组与 Java List 之间的转…

Unity的UI设计

目录 创建和布局 布局与交互 性能优化 最佳实践 学习资源 Unity UI Toolkit与uGUI和IMGUI之间的具体区别和适用场景是什么? Unity UI Toolkit uGUI IMGUI 如何在Unity中实现响应式UI设计以适应不同设备尺寸? Unity UI性能优化的最新技术和方法…

8.MySQL知识巩固-牛客网练习题

目录 SQL228 批量插入数据 描述 SQL202 找出所有员工当前薪水salary情况 描述 示例1 SQL195 查找最晚入职员工的所有信息描述 示例1 SQL196 查找入职员工时间排名倒数第三的员工所有信息描述 SQL201查找薪水记录超过15条的员工号emp_no以及其对应的记录次数t 描述 SQL…

后端Web之数据库多表设计

1.概述 项目开发中,在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在着各种联系,基本上分为三种:一对多、多对多、一对一。 数据库的多表设计是关…

JavaWeb——MVC架构模式

一、概述: MVC(Model View Controller)是软件工程中的一种 软件架构模式 ,它把软件系统分为模型、视图和控制器三个基本部分。用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户…

字符串专题——字符串相乘

1、题目解析 2、算法解析 1、解法一 使用模拟的方法:模拟小学的列竖式运算 细节1:高位相乘的时候要补上“0” 细节2:处理前导“0” 细节3:注意计算结果的顺序 2、解法二 对比解法一做优化->无进位相乘然后相加&#xff0…

Huawei Matebook e 2022 安装 archlinux 双系统

本文同步发布于我的网站 安装之前 wifi 名称修改为英文数字的,以防之后没法联网 准备好 U 盘并使用 GPT 分区表写入最新的 arch 镜像。 基础安装 开机按 F2 进入 UEFI/BIOS 设置,将 Secure Boot(安全启动)关闭,按…

AI学习记录 - transformers 的 linear 词映射层的详细分析, CrossEntropyLoss 函数解析

创作不易,有用的话点个赞。。。。。。 1. 假设条件 词汇表:假设词汇表包含四个词汇:[token_0, token_1, token_2, token_3]。 模型的输出概率分布:模型的输出经过 Softmax 转换后,得到概率分布:[0.1,0.5,…

JavaScript - Api学习 Day1(WebApi、操作DOM对象)

应用编程接口 (API) 是编程语言中提供的结构,允许开发者更轻松地创建复杂的功能。、 webapi 是一套 操作网页内容(DOM) 与 浏览器窗口(BOM) 的对象Js由ECMAScript、DOM、BOM三个部分组成。 文章目录 零、前言0.1 变量声明 壹、WebAPI的认识1.1 作用1.2 什么是DOM1…

【AI大模型】解锁AI智能:从注意力机制到Transformer,再到BERT与GPT的较量

文章目录 前言一、揭秘注意力机制:AI的焦点如何塑造智能1.什么是注意力机制?2.为什么需要注意力机制? 二、变革先锋:Transformer的突破与影响力1.什么是Transformer?2.为什么Transformer如此重要? 三、路径…

《给所有人的生成式 AI 课》学习笔记(一)

前言 本文是吴恩达(Andrew Ng)的视频课程《Generative AI for Everyone》(给所有人的生成式 AI 课)的学习笔记。由于原课程为全英文视频课程(时长约 3 个小时),且国内访问较慢,阅读…

零基础转行学网络安全怎么样?

在当今数字化飞速发展的时代,网络安全已成为备受瞩目的领域。那么,对于零基础的人来说,转行学习网络安全究竟怎么样呢? 网络安全行业正处于蓬勃发展的阶段。随着互联网的普及和信息技术的不断进步,网络安全问题日益凸显。政企单位…