阅读 161

Hello WebGPU —— 加载3D模型 & 基础光照模型

前言

今天让我们继续回到渲染的话题当中来。今天我们要学习的内容是如何加载一个3D模型并且编写一个基本的光照模型shader程序。

加载3D模型

首先,我们需要一个3D模型,这里我们采用了一个很出名的模型:它叫做 stanford-dragon,并且它还是一个NPM包。我们可以通过 yarn add stanford-dragon 在项目中引入这个3D模型,注意这里,它不是一个常见的3D模型格式,它是封装好的数据。常见的3D模型格式有: .obj, .gltf, fbx 等,你如果要加载这些类型的3D模型,那么你需要一个分析3D文件的解析器。如何解析3D文件中的数据不在本文的讨论范围之内就不过多赘述了。

这里,我们对数据进行一下简单的封装:

export const mesh = {   positions: dragonRawData.positions as [number, number, number][],   triangles: dragonRawData.cells as [number, number, number][],   normals: [] as [number, number, number][], }; // Compute surface normals mesh.normals = computeSurfaceNormals(mesh.positions, mesh.triangles); export function computeSurfaceNormals(   positions: [number, number, number][],   triangles: [number, number, number][] ): [number, number, number][] {   const normals: [number, number, number][] = positions.map(() => {     // Initialize to zero.     return [0, 0, 0];   });   triangles.forEach(([i0, i1, i2]) => {     const p0 = positions[i0];     const p1 = positions[i1];     const p2 = positions[i2];     const v0 = vec3.subtract(vec3.create(), p1, p0);     const v1 = vec3.subtract(vec3.create(), p2, p0);     vec3.normalize(v0, v0);     vec3.normalize(v1, v1);     const norm = vec3.cross(vec3.create(), v0, v1);     // Accumulate the normals.     vec3.add(normals[i0], normals[i0], norm);     vec3.add(normals[i1], normals[i1], norm);     vec3.add(normals[i2], normals[i2], norm);   });   normals.forEach((n) => {     // Normalize accumulated normals.     vec3.normalize(n, n);   });   return normals; } 复制代码

由于原始数据中没有提供法线信息,所以这里我们需要手动的计算一下模型的法线信息(计算方式:计算相邻两条边的叉积再归一化即可)。

接在为3D模型的数据创建Buffer用于传递顶点数据:

  // Create the model vertex buffer.   const vertexBuffer = device.createBuffer({     size: mesh.positions.length * 3 * 2 * Float32Array.BYTES_PER_ELEMENT,     usage: GPUBufferUsage.VERTEX,     mappedAtCreation: true,   });   {     const mapping = new Float32Array(vertexBuffer.getMappedRange());     for (let i = 0; i < mesh.positions.length; ++i) {       mapping.set(mesh.positions[i], 6 * i);       mapping.set(mesh.normals[i], 6 * i + 3);     }     vertexBuffer.unmap();   } 复制代码

接着,为顶点的索引创建Buffer

   // Create the model index buffer.   const indexCount = mesh.triangles.length * 3;   const indexBuffer = device.createBuffer({     size: indexCount * Uint16Array.BYTES_PER_ELEMENT,     usage: GPUBufferUsage.INDEX,     mappedAtCreation: true,   });   {     const mapping = new Uint16Array(indexBuffer.getMappedRange());     for (let i = 0; i < mesh.triangles.length; ++i) {       mapping.set(mesh.triangles[i], 3 * i);     }     indexBuffer.unmap();   } 复制代码

这里,为 indexBuffer 作出一些说明。后续我们不会使用 renderPass.draw 来进行绘制,而是采用 renderPass.drawIndexed 来进行绘制。那么这两者之间有什么区别呢?

image.png 如上图所示,如果我们需要绘制一个正方形,一个正方形是由两个三角形拼接而成。draw 的API要求我们提供两个三角形的顶点,那我们需要传入6个顶点信息。我们可以发现,这种方式,会有2个顶点的信息是一样的,这样就造成了顶点信息的冗余。

drawIndexed 的意思是根据顶点的索引来进行绘制。所以这里我们只需要传入4个顶点信息,然后再提供另外一个Buffer,告诉GPU按照 (0, 1, 3, 1, 2, 3) 的顺序来读取顶点信息进行绘制即可。这样我们可以减少一些顶点信息。

往WebGPU中传入 VertexBuffer 的部分此处就不再赘述了,如果你已经忘记了大致的渲染流程,可以回到之前的文章进行复习。

基础光照模型

现在我们来实现一个最为基本的光照模型。我们现在只为物体增加一个漫反射的效果。

image.png

如上图所示,一束光线打在一个物体上发生漫反射时,出射光线会向四面八方散开。不管我们从哪个角度去观察物体,物体的亮度应该都是不变的。

我们不难想象,如果一束光打向物体的角度约接近于垂直,则物体接受到的能量越多,物体也就越亮。

还有,如果物体距离光源越近,接受的能量越多,物体也就越亮。

所以,综上所述,我们的着色模型跟光线入射角度和光源距离物体的位置有关。此处为了简单起见,我们只考虑光线的入射角度。

那么,我们在着色器中需要一些场景的信息,还需要往片元着色器中传入每个片元的位置信息、法线信息。

struct Scene {   cameraViewProjMatrix: mat4x4<f32>;   lightPos: vec3<f32>; }; struct VertexOutput {   [[location(0)]] fragPos: vec3<f32>;   [[location(1)]] fragNorm: vec3<f32>;   [[builtin(position)]] Position: vec4<f32>; }; [[stage(vertex)]] fn main([[location(0)]] position: vec3<f32>, [[location(1)]] normal: vec3<f32>) -> VertexOutput {   var output : VertexOutput;   output.Position = scene.cameraViewProjMatrix * model.modelMatrix * vec4<f32>(position, 1.0);   output.fragPos = output.Position.xyz;   output.fragNorm = normal;   return output; } 复制代码

对于片元着色器,我们同样需要场景的信息,然后就是刚刚顶点着色器中计算好的信息:

 struct Scene {   cameraViewProjMatrix : mat4x4<f32>;   lightPos : vec3<f32>; }; let ambientFactor : f32 = 0.2; [[stage(fragment)]] fn main(input : FragmentInput) -> [[location(0)]] vec4<f32> {   let lambertFactor : f32 = max(dot(normalize(scene.lightPos - input.fragPos), input.fragNorm), 0.0);   let lightingFactor : f32 = min(ambientFactor + lambertFactor, 1.0);   return vec4<f32>(lightingFactor, 1.0); } 复制代码

这里其中的核心代码就在于: dot(normalize(scene.lightPos - input.fragPos), input.fragNorm)

这里 dot 表示求入射光线与物体法线的投影,也就是表示入射光线与物体法线的相近程度。后续的 max 表示,如果是物体背面则为0。

这里我们还使用了一个常量 ambientFactor 表示环境光照,这样可以使物体的暗部看起来没那么的黑。

最终的效果如下:

image.png

总结

今天我们学习了:

  1. 如何加载一个3D模型

  2. 学习了使用drawIndexed这个API来进行绘制可以减少冗余的顶点数据。

  3. 学习了一个基本的光照模型。

总的来说,今天的内容还是比较少的。希望你能够自己动手实践一番~ 如果你觉得本文有用,不妨点个赞~ 你的支持就是作者更新的动力。感谢阅读,Happy Coding~!


作者:玄玄子
链接:https://juejin.cn/post/7062672237924450311


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐