WebGPU加载Wavefront .OBJ模型文件

在开发布料模拟之前,我想使用 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

为C# Console应用化个妆

说到Windows的cmd&#xff0c;刻板印象就是黑底白字的命令行界面。跟Linux花花绿绿的界面比&#xff0c;似乎单调了许多。但其实C#开发的Console应用也可以摆脱单调非黑即白的UI。 最近遇到个需求&#xff0c;要在一堆纯文本文件里找指定的关键字&#xff08;后续还要人肉判断…

m4s格式转换mp4

先安装 ffmpeg&#xff0c;具体从官网可以查到&#xff0c;https://ffmpeg.org&#xff0c;按流程走。 转换代码如下&#xff0c;可以任意选择格式导出 import subprocess import osdef merge_audio_video(input_audio_path, input_video_path, output_mp4_path):# 构建 FFmpe…

bootloader串口更新程序[瑕疵学习板]

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、储备知识二、程序步骤2.程序展示1.bootloader2.然后是主运行函数总结前言 很久没有更新文章了。最近工作太忙,没有学习很多的知识,然后这两天不忙了,就学习了一下bootloader的程序升级…

***一种改版后检查硬件PCB生产资料的方法***,简单实用,且不容易出错

一、前言 硬件电路设计改版是常有的事,不管小的实物,还是需求变更经常会遇到要增加或者减少器件,修改走线这些。在第一版已经做了生产资料投板的情况下,可以和第一板对比一下就知道改了哪些地方,怎么才能快速的定位出来改动点并检查是否更改呢。 有的人是通过PCB文件来检…

【ES】笔记-Promise基本使用

笔记-基本使用 一、初始Promise1. 抽象表达:2. 具体表达:为什么要用 Promise?promise的基本流程 二、fs读取文件三、AJAX请求四、Promise封装fs模块五、util.promisify方法六、Promise封装AJAX操作 一、初始Promise 1. 抽象表达: 1. Promise 是一门新的技术(ES6 规范) 2. Pr…

Live800:在线沟通有这些新趋势

近年来&#xff0c;随着互联网技术的快速发展&#xff0c;越来越多的企业开始采用在线客服系统&#xff0c;以解决与客户沟通的问题。这项技术的出现&#xff0c;不仅改变了企业与客户之间沟通的方式&#xff0c;也为未来在线沟通提供了新的方向。 在线客服系统的特点主要有以下…

【UE5】给模型指定面添加自定义材质

实现步骤 1. 首先我们向UE中导入一个简单的模型&#xff0c;可以看到目前该模型的材质插槽只有一个&#xff0c;当我们修改材质时会使得模型整体的材质全部改变&#xff0c;如果我们只想改变模型的某些面的材质就需要继续做后续操作。 2. 选择建模模式 3. 在模式工具栏中点击…

防御网络攻击风险的4个步骤

如今&#xff0c;人们正在做大量工作来保护 IT 系统免受网络犯罪的侵害。令人担忧的是&#xff0c;对于运营技术系统来说&#xff0c;情况却并非如此&#xff0c;运营技术系统用于运行从工厂到石油管道再到发电厂的所有业务。 组织应该强化其网络安全策略&#xff0c;因为针对…

【业务功能篇82】微服务SpringCloud-ElasticSearch-Kibanan-docke安装-进阶实战

四、ElasticSearch进阶 https://www.elastic.co/guide/en/elasticsearch/reference/7.4/getting-started-search.html 1.ES中的检索方式 在ElasticSearch中支持两种检索方式 通过使用REST request URL 发送检索参数(uri检索参数)通过使用 REST request body 来发送检索参数…

LeetCode第16~20题解

CONTENTS LeetCode 16. 最接近的三数之和&#xff08;中等&#xff09;LeetCode 17. 电话号码的字母组合&#xff08;中等&#xff09;LeetCode 18. 四数之和&#xff08;中等&#xff09; LeetCode 16. 最接近的三数之和&#xff08;中等&#xff09; 【题目描述】 给你一个…

自然语言处理(三):基于跳元模型的word2vec实现

跳元模型 回顾一下第一节讲过的跳元模型 跳元模型&#xff08;Skip-gram Model&#xff09;是一种用于学习词向量的模型&#xff0c;属于Word2Vec算法中的一种。它的目标是通过给定一个中心词语来预测其周围的上下文词语。 这节我们以跳元模型为例&#xff0c;讲解word2vec的…

如何通过内网穿透实现外部网络对Spring Boot服务端接口的HTTP监听和调试?

文章目录 前言1. 本地环境搭建1.1 环境参数1.2 搭建springboot服务项目 2. 内网穿透2.1 安装配置cpolar内网穿透2.1.1 windows系统2.1.2 linux系统 2.2 创建隧道映射本地端口2.3 测试公网地址 3. 固定公网地址3.1 保留一个二级子域名3.2 配置二级子域名3.2 测试使用固定公网地址…

【VLDB 2023】基于预测的云资源弹性伸缩框架MagicScaler,实现“高QoS,低成本”双丰收

开篇 近日&#xff0c;由阿里云计算平台大数据基础工程技术团队主导&#xff0c;与计算平台MaxCompute团队、华东师范大学数据科学与工程学院、达摩院合作&#xff0c;基于预测的云计算平台资源弹性伸缩框架论文《MagicScaler: Uncertainty-aware, Predictive Autoscaling 》被…

linux操作系统的权限的深入学习

1.Linux权限的概念 Linux下有两种用户&#xff1a;超级用户&#xff08;root&#xff09;、普通用户。 超级用户&#xff1a;可以再linux系统下做任何事情&#xff0c;不受限制 普通用户&#xff1a;在linux下做有限的事情。 超级用户的命令提示符是“#”&#xff0c;普通用户…

架构师日记-软件工程里的组织文化 | 京东云技术团队

一 引言 本文是京东到家自动化测试体系建设过程中的一些回顾和总结&#xff0c;删减了部分系统设计与实践的章节&#xff0c;保留了组织与文化相关的内容&#xff0c;整理成文&#xff0c;以飨读者。 下面就以QA&#xff08;Quality Assurance&#xff09;的视角来探讨工作中经…

Git分支机制

一、分支机制简述 要想真正理解Git的分支机制&#xff0c;我们要首先回过头来看一下Git是如何存储数据的。 Git并没有采用多个变更集( changeset )或是差异的方式存储数据&#xff0c;而是采用一系列快照的方式。当你发起提交时&#xff0c;Git存储的是提交对象( commi…

fastadmin iis伪静态应用入口文件index.php

<?xml version"1.0" encoding"UTF-8"?> <configuration><system.webServer><rewrite><rules><rule name"OrgPage" stopProcessing"true"><match url"^(.*)$" /><conditions…

开始MySQL之路——外键关联和多表联合查询详细概述

多表查询和外键关联 实际开发中&#xff0c;一个项目通常需要很多张表才能完成。例如&#xff0c;一个商城项目就需要分类表&#xff0c;商品表&#xff0c;订单表等多张表。且这些表的数据之间存在一定的关系&#xff0c;接下来我们将在单表的基础上&#xff0c;一起学习多表…

抖音seo矩阵系统源代码开发部署分享

一、 开发步骤分享 抖音SEO矩阵系统源代码开发部署分享&#xff0c;需要经验丰富的开发人员和服务器管理人员&#xff0c;以下是大致的步骤&#xff1a; 确定你需要的功能和设计&#xff0c;确定开发人员和设计师的角色和任务分配&#xff0c;以及开发进度和计划。 确定服务器…

java+ssm+mysql农场信息管理系统

项目介绍&#xff1a; 本系统为基于jspssmmysql的农场信息管理系统&#xff0c;功能如下&#xff1a; 用户&#xff1a;注册登录系统&#xff0c;菜地信息管理&#xff0c;农作物信息管理&#xff0c;种植信息管理&#xff0c;客户信息管理&#xff0c;商家信息管理&#xff…