WebGPU图形编程(5):构建一个线框球体<学习引自徐博士教程>


一、前提

在代码开始之前,你需要自行准备配置环境文件或者你可以去GitHub下载简单配置好的文件:https://github.com/zhiwenhao-0807/webgpu.git

如果你对刚开始如何配置环境疑惑,可以去跟着第一篇博客去学习:

1.1认知

本节要实现一个球体线框三维实体,就需要对球坐标系有一个初步的认知,如果你有图形基础,可以看出图中采用的是右手坐标系;

创建球体线框,可以使用UV球体方法来创建,u代表经度(纵向)v代表纬度(横向),u和v在球体表面形成了网格,因为我们是创建线框,所以先要创建一个单位网格,一个单位网格由四条线段组成,但你只需要绘制两条实线段就可以了,你可以想象一下,遍历图中两条实线段,按照球体排列平铺就可以形成线框球体,如果你把四条都绘制出来,当然也可以生成线框球体,但会有重叠部分,并且多余绘制会浪费资源消耗;

好的,前提的一些基础概念说完了,接下来开始编程部分!

 二、编程部分

在编程开始之前,我想要简单一一说明下src下各工程文件的含义,相比之前对比,本节多了wireframe.ts和math-func.ts两个文件,并且代码结构有了相应的变化

helper.ts:和之前代码一致,没有改变,主要用途是封装一些基本调用GPU的方法,gl-matrix转换方法,供调用,它是通用的;

main.ts:主要代码文件,主要实现三维的功能渲染,本节main代码中,你可以看到代码量其实非常少,三维对象创建和矩阵转换被封装为wireframe和math-func两个文件

math-func.ts:用来做球体点位的矩阵转换;

shader.ts:着色器代码,<但着色器代码是文本类形式,没有提示代码块的功能,所以我们之前都把着色器代码转移到pipeline描述>

vertex_data.ts:描述封装单元格线段函数,在main调用;

wireframe.ts:三维对象创建,主要描述buffer、pipeline、bindgroup、renderpass,<这个文件包含了大量着色器和渲染代码>

大致流程,你可以理解为:矩阵转换球体顶点数据(math-func)—>绘制单元格线段、并遍历球体顶点数据(vertex_data)—>缓冲区描述及着色器设计和渲染(wireframe)—>调用封装好的顶点数据和着色器渲染动画(main)

 

 2.1.创建math-func.ts<矩阵转换为球体顶点数据>

1 import { vec3 } from "gl-matrix";
2 
3 export const SpherePosition = (radius:number,theta:number,phi:number,center:vec3 = [0,0,0]) =>{
4     const snt = Math.sin(theta*Math.PI/180);
5     const cnt = Math.cos(theta*Math.PI/180);
6     const snp = Math.sin(phi*Math.PI/180);
7     const cnp = Math.cos(phi*Math.PI/180);
8     return vec3.fromValues(radius*snt*cnp+center[0],radius*cnt+center[1],-radius*snt*snp+center[2]);
9 }

 2.2.创建vertex_data.ts<绘制单元块线段,并按照球体顶点数据遍历线段>

 1 import { SpherePosition } from "./math-func";
 2 import { vec3 } from "gl-matrix";
 3 
 4 export const SphereWireframeData = (radius:number,u:number,v:number,center:vec3 =[0,0,1]) =>{
 5     if(u<2 || v<2)return;
 6     let pts =[];
 7     let pt:vec3;
 8     for(let i=0;i){
 9         let pt1:vec3[]=[];
10         for(let j=0;j){
11             pt =SpherePosition(radius,i*180/(u-1),j*360/(v-1),center);
12             pt1.push(pt);
13         }
14         pts.push(pt1);
15     }
16    
17 
18     let p = [] as any;
19 let p0,p1,p2,p3;
20 for(let i=0;i){
21     for(let j=0;j){
22         p0=pts[i][j];
23         p1=pts[i+1][j];
24         p3=pts[i][j+1];
25         p.push([
26             p0[0],p0[1],p0[2],p1[0],p1[1],p1[2],
27             p0[0],p0[1],p0[2],p3[0],p3[1],p3[2]
28         ]);
29     }
30    
31 }
32 return new Float32Array(p.flat());
33 }

2.3.创建wifeframe.ts<缓冲区创建和着色器描述>

如果你按照前面的教程一步一步学习过来的,你去看代码结构并不会懵,如果你看不懂没关系,我的建议是慢慢来,先把代码copy,完成实现;之后你再详细去看webgpu的API。

我简单说一下这个代码的流程,import(调用其他已经实现的类/方法)—>CreateWireframe(定义一个异步函数)—>shader\pipeline(进行着色器和管线函数声明创建)—>uniform data(虽然叫统一数据,但这个代码块里包含模型和透视矩阵的设计)—>rotation\camera(声明对象旋转和相机)—>uniform buffer and layout(创建buffer并且进行资源绑定)—>draw(渲染管线、绘制顶点数据)

  1 //这是一个通用文件,可以为不同的三维对象创建线框,例如:球体、圆柱体、圆环
  2 import { InitGPU,CreateGPUBuffer,CreateGPUBufferUint,CreateTransforms,CreateViewProjection,CreateAnimation } from "./helper";
  3 import { Shaders } from "./shaders";
  4 import { vec3,mat4 } from "gl-matrix";
  5 const createCamera=require('3d-view-controls');
  6 
  7 export const CreateWireframe =async (wireframeData:Float32Array,isAnimation=true)=> {  //wireframeData是一个输入变量,来自mian第7行
  8 
  9     const gpu =await InitGPU();
 10     const device =gpu.device;
 11 //create vertex buffers
 12 const numberOfVertices=wireframeData.length/3;
 13 const vertexBuffer = CreateGPUBuffer(device,wireframeData);
 14 
 15 const shader =Shaders();  //引用Shaders文件,在当前文件描述着色器代码
 16 const pipeline = device.createRenderPipeline({ //创建控制顶点和片段着色器阶段管线代码块
 17     vertex:{
 18         module:device.createShaderModule({
 19             code:shader.vertex
 20         }),
 21         entryPoint:"main",
 22         buffers:[{
 23             arrayStride:12,
 24             attributes:[{
 25                 shaderLocation:0,
 26                 format:"float32x3",
 27                 offset:0
 28         }]
 29     }]  
 30 },
 31         fragment:{
 32             module:device.createShaderModule({
 33                 code:shader.fragment
 34             }),
 35             entryPoint:"main",
 36             targets:[
 37                 {
 38                     format:gpu.format as GPUTextureFormat
 39                 }
 40             ]
 41         },
 42         primitive:{
 43             topology:"line-list",
 44         }
 45 });    
 46         
 47 //create uniform data
 48 const modelMatrix = mat4.create();
 49 const mvpMatrix = mat4.create();
 50 let vMatrix = mat4.create();
 51 let vpMatrix = mat4.create();
 52 const vp =CreateViewProjection(gpu.canvas.width/gpu.canvas.height);
 53 vpMatrix=vp.viewProjectionMatrix;
 54 
 55 //add rotation and camera
 56 let rotation =vec3.fromValues(0,0,0);
 57 var camera = createCamera(gpu.canvas,vp.cameraOption);
 58 
 59 //create uniform buffer and layout
 60 const uniformBuffer = device.createBuffer({
 61     size:64,
 62     usage:GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
 63 });
 64 
 65 const uniformBindGroup =device.createBindGroup({
 66     layout:pipeline.getBindGroupLayout(0),
 67     entries:[{
 68         binding:0,
 69         resource:{
 70             buffer:uniformBuffer,
 71             offset:0,
 72             size:64
 73         }
 74     }]
 75 });
 76 
 77 let textureView = gpu.context.getCurrentTexture().createView();
 78 const renderPassDescription = {
 79     colorAttachments:[{
 80         view:textureView,
 81         loadValue:{r:0.2,g:0.247,b:0.314,a:1.0}, //backgroud color
 82         storeOp:'store'
 83     }]
 84 };
 85 
 86 function draw(){
 87     if(!isAnimation){
 88         if(camera.tick()){
 89             const pMatrix = vp.projectionMatrix;
 90             vMatrix = camera.matrix;
 91             mat4.multiply(vpMatrix,pMatrix,vMatrix);
 92         }
 93     }
 94     CreateTransforms(modelMatrix,[0,0,0],rotation);
 95     mat4.multiply(mvpMatrix,vpMatrix,modelMatrix);
 96     device.queue.writeBuffer(uniformBuffer,0,mvpMatrix as ArrayBuffer);
 97 
 98     textureView = gpu.context.getCurrentTexture().createView();
 99     renderPassDescription.colorAttachments[0].view = textureView;
100     const commandEncoder = device.createCommandEncoder();
101     const renderPass = commandEncoder.beginRenderPass(renderPassDescription as GPURenderPassDescriptor)
102     
103     renderPass.setPipeline(pipeline);    //渲染管线
104     renderPass.setVertexBuffer(0,vertexBuffer);  //渲染顶点缓冲区
105     renderPass.setBindGroup(0,uniformBindGroup); //渲染资源绑定组
106     renderPass.draw(numberOfVertices);  //渲染顶点
107     renderPass.endPass();  //结束渲染通道编码器
108 
109     device.queue.submit([commandEncoder.finish()]);
110 }
111 CreateAnimation(draw,rotation,isAnimation);
112 }

 2.4.main.ts文件<这是src最后一步,调用前面所有的函数方法实现>

 1 import { CreateWireframe } from './wireframe';
 2 import { SphereWireframeData } from './vertex_data';
 3 import { vec3 } from 'gl-matrix';
 4 import $ from 'jquery';
 5 
 6 const Create3DObject = async (radius:number, u:number, v:number, center:vec3, isAnimation:boolean) => {
 7     const wireframeData = SphereWireframeData(radius, u, v, center) as Float32Array;
 8     await CreateWireframe(wireframeData, isAnimation);
 9 }
10 
11 let radius = 2;  //半径
12 let u = 20;   //u横向等分
13 let v = 15;   //v纵向等分
14 let center:vec3 = [0,0,0];  //中心位置
15 let isAnimation = true;   //球体自动旋转
16 
17 Create3DObject(radius, u, v, center, isAnimation);
18 
19 $('#id-radio input:radio').on('click', function(){
20     let val = $('input[name="options"]:checked').val();
21     if(val === 'animation') isAnimation = true;
22     else isAnimation = false;
23     Create3DObject(radius, u, v, center, isAnimation);
24 });
25 
26 $('#btn-redraw').on('click', function(){
27     const val = $('#id-center').val();
28     center = val?.toString().split(',').map(Number) as vec3;
29     radius = parseFloat($('#id-radius').val()?.toString() as string);
30     u = parseInt($('#id-u').val()?.toString() as string);
31     v = parseInt($('#id-v').val()?.toString() as string);
32     Create3DObject(radius, u, v, center, isAnimation);  
33 });

2.5.index.html<最后就写你的网页布局和调用打包的bundle.js>

 1 
 2 
 3    
 4    
 5    WebGPU Step-by-Step 13
 6    
 7    
 8 
 9 
10 
11    
38    
39

Sphere Wireframe


40 41
42
43

Motion Control

44
45 46 47
48
49

Set Parameters

50
51
center
52
53 54
55
56
57
radius
58
59 60
61
62
63
u
64
65 66
67
68
69
v
70
71 72
73
74
75
76
77 78
79
80
81 82 83 84

三、 至此你的所有代码完毕,最后展示出来应该是下图