WebGPU加载Wavefront .OBJ模型文件

news2025/1/17 23:57:41

在开发布料模拟之前,我想使用 WebGPU 开发强大的代码基础。 这就是为什么我想从 Wavefront .OBJ 文件加载器开始渲染 3D 模型。 这样,我们可以快速渲染 3D 模型,并构建一个简单而强大的渲染引擎来完成此任务。 一旦我们有了扎实的基础,我们就可以轻松实现布料模拟部分了。

在这里插入图片描述

推荐:用 NSDT编辑器 快速搭建可编程3D场景

1、Wavefront .OBJ 文件

.OBJ 是一种文件格式,包含由 Wavefront Technologies 公司创建的 3D 几何图形的描述。 典型的 .OBJ 文件的结构包含一组:

  • 顶点
  • 法线
  • 纹理坐标

让我们看一个例子。 .obj 金字塔定义如下:

v  0  0  0
v  1  0  0
v  1  1  0
v  0  1  0
v  0.5  0.5  1.6
​
f  4// 1// 2//
f  3// 4// 2//
f  5// 2// 1//
f  4// 5// 1//
f  3// 5// 4//
f  5// 3// 2//

渲染此文件,我们可能会看到一个如下图所示的金字塔:

在这里插入图片描述

那么问题出现了🤔:

我们如何将这种文件格式加载到我们的程序中?

我们将了解如何使用现成的 .OBJ 文件来执行此操作,以渲染复杂的几何图形。 利用别人的劳动来免除我们的辛苦工作的美妙之处。 💅

如果你手头有FBX、GLTF等其他格式的模型,可以使用NSDT 3DConvert这个在线3D格式转换工具将其转换为.OBJ文件:
在这里插入图片描述

2、我们如何找到 .OBJ 文件?

我使用 Google 查找 .OBJ 文件。 也就是说,如果我找到一个我喜欢的文件,我必须将其加载到 Blender 等软件中,原因如下:

  • 格式一致性:当使用 Google 查找 .OBJs 文件时,它们都有一些小的格式特性。 例如,他们可以定义带或不带斜线的顶点和面。 我想用Blender加载和导出以保证文件内容的格式。 📁
  • 几何体的定位:有时,模型的定位方式是我们不想要的。 纠正 Blender 中的初始位置可以节省我们一些时间和代码。

让我们看一个例子。 对于这个项目,我想使用著名的斯坦福兔子。 该文件可以在这里找到:
在这里插入图片描述

3、如何在 Blender 中准备几何体

下载文件后,我们需要在Blender等3D软件中打开它进行检查。 我们立刻就可以看到这个位置有问题:
在这里插入图片描述

我想将兔子居中,使其身体位于原点。 要做到这一点非常简单。 以下是这些步骤:

将原点放在兔子上。 右键单击,然后导航到“设置原点”>“原点到几何体”。
在这里插入图片描述

将兔子移动到场景原点。 右键单击,然后导航至“捕捉”>“光标选择”。

在这里插入图片描述

兔子现在以原点为中心🎯:

在这里插入图片描述

最后,一个好主意是检查与模型相关的法线。 我们可以通过在 Blender 中进入编辑模式(按 TAB 键)并导航到“网格”>“法线”>“重新计算外部”来重做计算:

在这里插入图片描述

导航到文件 > 导出 > Wavefront (.obj)。 可以使用以下设置导出文件:

✅ 应用修饰符
✅ 写法线
✅ 包括 UV
✅ 撰写材质
✅ 三角面

在这里插入图片描述

之后,你应该准备好使用 WebGPU 在浏览器中渲染的 .OBJ 文件。 🥳

4、WebGPU 代码

💡代码现在假设该文件是由 Blender 准备的。 如果没有,请参阅上一节。

目标是将 .OBJ 文件中的所有数据存储在缓冲区中。 幸运的是,这些数据很容易读取。 我将系统设计为两部分:

  • 加载器 - 我们加载文件并将其文本存储在内存中,以便我们可以处理它。
  • 解析器 - 将文本存储在内存中,我们可以解析文本行并将它们存储在缓冲区中。
interface Mesh {
  positions: Float32Array;
  uvs: Float32Array;
  normals: Float32Array;
  indices: Uint16Array;
}

type ObjFile = string
type FilePath = string

type CachePosition = number
type CacheFace = string
type CacheNormal = number
type CacheUv = number
type CacheArray<T> = T[][]

type toBeFloat32 = number
type toBeUInt16 = number

/**
 * ObjLoader to load in .obj files. This has only been tested on Blender .obj exports that have been UV unwrapped
 * and you may need to throw out certain returned fields if the .OBJ is missing them (ie. uvs or normals)
 */
export default class ObjLoader {
  constructor() {}
  /**
   * Fetch the contents of a file, located at a filePath.
   */
  async load(filePath: FilePath): Promise<ObjFile> {
    const resp = await fetch(filePath)
    if (!resp.ok) {
      throw new Error(
        `ObjLoader could not fine file at ${filePath}. Please check your path.`
      )
    }
    const file = await resp.text()

    if (file.length === 0) {
      throw new Error(`${filePath} File is empty.`)
    }

    return file
  }

  /**
   * Parse a given obj file into a Mesh
   */
  parse(file: ObjFile): Mesh {
    const lines = file?.split("\n")

    // Store what's in the object file here
    const cachedPositions: CacheArray<CachePosition> = []
    const cachedFaces: CacheArray<CacheFace> = []
    const cachedNormals: CacheArray<CacheNormal> = []
    const cachedUvs: CacheArray<CacheUv> = []

    // Read out data from file and store into appropriate source buckets
    {
      for (const untrimmedLine of lines) {
        const line = untrimmedLine.trim() // remove whitespace
        const [startingChar, ...data] = line.split(" ")
        switch (startingChar) {
          case "v":
            cachedPositions.push(data.map(parseFloat))
            break
          case "vt":
            cachedUvs.push(data.map(Number))
            break
          case "vn":
            cachedNormals.push(data.map(parseFloat))
            break
          case "f":
            cachedFaces.push(data)
            break
        }
      }
    }

    // Use these intermediate arrays to leverage Array API (.push)
    const finalPositions: toBeFloat32[] = []
    const finalNormals: toBeFloat32[] = []
    const finalUvs: toBeFloat32[] = []
    const finalIndices: toBeUInt16[] = []

    // Loop through faces, and return the buffers that will be sent to GPU for rendering
    {
      const cache: Record<string, number> = {}
      let i = 0
      for (const faces of cachedFaces) {
        for (const faceString of faces) {
          // If we already saw this, add to indices list.
          if (cache[faceString] !== undefined) {
            finalIndices.push(cache[faceString])
            continue
          }

          cache[faceString] = i
          finalIndices.push(i)

          // Need to convert strings to integers, and subtract by 1 to get to zero index.
          const [vI, uvI, nI] = faceString
            .split("/")
            .map((s: string) => Number(s) - 1)

          vI > -1 && finalPositions.push(...cachedPositions[vI])
          uvI > -1 && finalUvs.push(...cachedUvs[uvI])
          nI > -1 && finalNormals.push(...cachedNormals[nI])

          i += 1
        }
      }
    }

    return {
      positions: new Float32Array(finalPositions),
      uvs: new Float32Array(finalUvs),
      normals: new Float32Array(finalNormals),
      indices: new Uint16Array(finalIndices),
    }
  }
}

5、加载器

让我们看一下 load() 函数:

async function load(filePath: FilePath): Promise<ObjFile> {
    const resp = await fetch(filePath);
    if (!resp.ok) {
      throw new Error(
        `ObjLoader could not fine file at ${filePath}. Please check your path.`
      );
    }
    const file = await resp.text();
​
    if (file.length === 0) {
      throw new Error(`${filePath} File is empty.`);
    }
​
    return file;
}

这段代码的想法只是获取位于 filePath 的文件的内容。 我将文件存储在硬盘上,但可以通过 HTTP 请求数据。

6、解析器

这是代码的第一部分:

parse(file: ObjFile): Mesh {
    const lines = file?.split("\n");
​
    // Store what's in the object file here
    const cachedVertices: CacheArray<CacheVertice> = [];
    const cachedFaces: CacheArray<CacheFace> = [];
    const cachedNormals: CacheArray<CacheNormal> = [];
    const cachedUvs: CacheArray<CacheUv> = [];
​
    // Read out data from file and store into appropriate source buckets
    {
      for (const untrimmedLine of lines) {
        const line = untrimmedLine.trim(); // remove whitespace
        const [startingChar, ...data] = line.split(" ");
        switch (startingChar) {
          case "v":
            cachedVertices.push(data.map(parseFloat));
            break;
          case "vt":
            cachedUvs.push(data.map(Number));
            break;
          case "vn":
            cachedNormals.push(data.map(parseFloat));
            break;
          case "f":
            cachedFaces.push(data);
            break;
        }
      }
    }
​
... Rest of code
}

这部分包括简单地从内存中读取数据并将它们存储在相应的数组中。 幸运的是,每行文本都标有其关联的类型:

  • v - 顶点的位置
  • vt - 纹理坐标(uv)
  • vn - 法线向量(法线)
  • f - 面(形成三角形的三个顶点)

这是其余的代码:

... the code before
    // Use these intermediate arrays to leverage Array API (.push)
    const finalVertices: toBeFloat32[] = [];
    const finalNormals: toBeFloat32[] = [];
    const finalUvs: toBeFloat32[] = [];
    const finalIndices: toBeUInt16[] = [];
​
    // Loop through faces, and return the buffers that will be sent to GPU for rendering
    {
      const cache: Record<string, number> = {};
      let i = 0;
      for (const faces of cachedFaces) {
        for (const faceString of faces) {
          // If we already saw this, add to indices list.
          if (cache[faceString] !== undefined) {
            finalIndices.push(cache[faceString]);
            continue;
          }
​
          cache[faceString] = i;
          finalIndices.push(i);
​
          // Need to convert strings to integers, and subtract by 1 to get to zero index.
          const [vI, uvI, nI] = faceString
            .split("/")
            .map((s: string) => Number(s) - 1);
​
          vI > -1 && finalVertices.push(...cachedVertices[vI]);
          uvI > -1 && finalUvs.push(...cachedUvs[uvI]);
          nI > -1 && finalNormals.push(...cachedNormals[nI]);
​
          i += 1;
        }
      }
    }
​
    return {
      vertices: new Float32Array(finalVertices),
      uvs: new Float32Array(finalUvs),
      normals: new Float32Array(finalNormals),
      indices: new Uint16Array(finalIndices),
    };
  }

接下来,我们迭代面部以创建数据并将其存储在最终缓冲区中。 我们使用称为索引缓冲区的东西,这是避免存储重复数据的一种方法。 我们稍后会看到如何进行。

7、缓冲器类型

正如我们在讨论中看到的,我们在 WebGPU 中渲染了一个三角形,我们使用缓冲区来存储每个顶点的属性。

更具体地,使用一个或多个顶点缓冲对象(VBO)和索引缓冲对象(IBO)。 我们使用 IBO 中的索引来索引 VBO 以避免存储重复数据。

让我们看一个例子:
在这里插入图片描述

标签为 2 的顶点位于两个三角形中(一个由顶点 1、2、3 形成,另一个由顶点 3、2、4 形成)。 我们将数据定义如下:

position_vbo = [
     -1, 0, 0, #v1
     1, 0, 0, #v2
     0, 1, 0, #v3
     2, 1, 0, #v4
 ]
​
 color_vbo = [
     1, 0, 0, #v1
     0, 0, 1, #v2
     1, 1, 0, #v3
     2, 1, 0, #v4
 ]
​
 indices_ibo = [
    0, 1, 2, # triangle 1
    2, 1, 3  # triangle 2
]

原文链接:WebGPU加载.OBJ模型 — BimAnt

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

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

相关文章

山西电力市场日前价格预测【2023-08-30】

日前价格预测 预测明日&#xff08;2023-08-30&#xff09;山西电力市场全天平均日前电价为317.95元/MWh。其中&#xff0c;最高日前电价为373.07元/MWh&#xff0c;预计出现在19: 45。最低日前电价为248.17元/MWh&#xff0c;预计出现在13: 30。 价差方向预测 1&#xff1a; 实…

手机云控设计思路

本系统为任务分发系统,上游发布任务或者接受其他平台系统分发的任务,对任务进行规则引擎处理后分类,由核心分发系统部分进行对存活的空闲终端进行分发任务,终端做完任务后进行反馈给任务系统. 核心要处理的点是终端存活与空闲的统计、任务平均分布下发给终端的算法,保证分布的…

RK3562 VS A40i 性能对比

RK3562作为瑞芯微新推出的低功耗、高性价比的通用SOC&#xff0c;在智能商显和工业控制领域又为深圳触觉智能增加了一款强有力的高性价比产品。RK3562是一款专为智能显示设备设计的高性能、低功耗四核应用处理器&#xff0c;其工规版本RK3562J还具备CAN FD接口&#xff0c;工作…

基于SSM的旅游管理系统jsp房源信息java源代码Mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 基于SSM的旅游管理系统 系统有2权限&#xff1a;管理…

创作与合作:AI绘画API与艺术家的共舞

引言 在数字时代&#xff0c;技术和艺术之间的界限正在变得越来越模糊。人工智能&#xff08;AI&#xff09;绘画API是一个典型的例子&#xff0c;它已经开始改变我们对创作和艺术的理解方式。然而&#xff0c;与传统艺术手法相比&#xff0c;AI绘画API并不是要取代艺术家&…

JavaScript原型链污染

前言 在浏览某个论坛的时候&#xff0c;第一次看到了JavaScript原型链污染漏洞。当时非常的好奇&#xff0c;当时我一直以为js作为一种前端语言&#xff0c;就算存在漏洞也是针对前端&#xff0c;不会危害到后端&#xff0c;因此我以为这种漏洞危害应该不大。可当我看到他的漏…

Feign在进行序列化时遇到泛型类型的擦除,导致反序列化时成了LinkedHashMap

Feign在进行序列化时遇到泛型类型的擦除&#xff0c;导致反序列化时成了LinkedHashMap 故障背景问题分析修复方案修复方案一 避免使用泛型修复方案二 解析data泛型的时候处理 故障背景 假设我们有一个Feign接口 import org.springframework.cloud.openfeign.FeignClient; imp…

深度学习卷积神经网络识别光学字符验证码,及captcha使用简单案例

深度学习卷积神经网络识别验证码 文章目录 深度学习卷积神经网络识别验证码一、引言二、导入必要的库三、防止 tensorflow 占用所有显存四、定义数据生成器并测试五、定义网络结构六、训练模型七、测试模型 一、引言 验证码识别&#xff0c;本身使用来判断访问网站的用户是不是…

更新netframe报错,更新.netFrame 到4.8

背景&#xff1a; 更新了Java程序后&#xff0c;启动提示我更新netframe 手动更新.netFrame 到4.8 ndp48-x86-x64-allos-enu.exe 发现报错&#xff1a; 继续百度解决报错。方案为&#xff1a; https://blog.csdn.net/i2blue/article/details/88821583 产生阻滞的问题: 你…

快手Java一面,全是基础

现在已经到了面试招聘比较火热的时候&#xff0c;准备面试的过程中&#xff0c;一定要多看面经&#xff0c;多自测&#xff01; 今天分享的是一位贵州大学的同学分享的快手一面面经。 快手一面主要会问一些基础问题&#xff0c;也就是比较简单且容易准备的常规八股&#xff0…

零代码,使用 Dify 和 Laf 两分钟接入企业微信 AI 机器人

Dify 允许创建 AI 应用&#xff0c;并提供二次开发的能力。这里我将演示创建一个法律问答助手的 AI 应用&#xff0c;称作“知法”。在本篇教程中&#xff0c;我将指导你为“知法”接入企业微信。 前置准备 企业微信的管理员权限一个 Dify 的帐号一个 Laf 云的帐号&#xff0…

【Linux】序列化与反序列化

目录 前言 什么是应用层&#xff1f; 再谈"协议" 什么是序列化和反序列化 网络版计算器 整体流程实现 Sock.hpp的实现 TcpServer.hpp的实现 Protocol.hpp的实现 CalServer.cc的编写 CalClient.cc的编写 整体代码 前言 本章是属于TCP/UDP四层模型中的第一层…

opencv的使用(Ubuntu linux环境,AS jni,AS java)

最近要完成一个功能&#xff0c;就是把四个视频合成左右上下分布的一个视频。尝试很多方法&#xff0c;最终使用opencv来实现该功能。&#xff08;通过opencv实现的视频好像没有声音。&#xff09;研究的步骤&#xff0c;首先在Ubuntu环境测试&#xff0c;该功能是否实现。然后…

13.搬砖

目录 题目 Description Input Output 思路&#xff08;归并排序&#xff09; 具体步骤如下 C整体代码&#xff08;含详细注释&#xff09; 归并排序总结 核心步骤 代码模板 题目 Description 小张在暑假时间来到工地搬砖挣钱。包工头交给他一项艰巨的任务&#xff0…

Mavan进阶之父子模块(继承)

文章目录 Mavan 父子模块&#xff08;继承&#xff09;1. 父项目2. 子项目3. 父子项目的使用 Mavan 父子模块&#xff08;继承&#xff09; 「继承」是 Maven 中很强大的一种功能&#xff0c;继承可以使得子 pom 可以获得 parent 中的各项配置&#xff0c;可以对子 pom 进行统…

深度学习之反卷积

具体推理可以参考https://blog.csdn.net/zhsmkxy/article/details/107073350

uniapp微信小程序使用stomp.js实现STOMP传输协议的实时聊天

简介&#xff1a; 原生微信小程序中使用 本来使用websocket&#xff0c;后端同事使用了stomp协议&#xff0c;导致前端也需要对应修改。 如何使用 1.yarn add stompjs 2.版本 “stompjs”: “^2.3.3” 3.在static/js中新建stomp.js和websocket.js&#xff0c;然后在需要使用…

一文讲透超宽带(UWB)前世今生

►►►UWB大火与巨头入局 传闻已久的蔚来手机可能即将要发布了。据工信部官网显示&#xff1a;申请单位为蔚来移动科技有限公司、型号为N2301的手机已正式完成入网。相关认证信息显示&#xff0c;N2301支持UWB&#xff0c;可以被用作蔚来汽车的数字钥匙。 图 1 蔚来手机概念图 …

第十四课 定语从句

文章目录 前言 所有定语从句的连接词是没有意思的一、定语从句的定义和结构二、关系代词引导的定语从句1、whowho谓语&#xff08;宾语&#xff09;状语who系动词表语状语who助动词及物动词的过去分词 2、whomwhom主语及物动词&#xff08;状语&#xff09;whom主语谓语to及物动…

2023京东咖啡机行业数据分析(京东数据分析平台)

如今咖啡的渗透率越来越高&#xff0c;养成咖啡饮用习惯的消费者越来越多&#xff0c;尤其是一二线城市。同时&#xff0c;随着人们收入水平的提高&#xff0c;精致生活理念使人们对咖啡的态度从提神需求逐渐转变为社交需求&#xff0c;国内咖啡机市场的发展空间也逐步增大。 …