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 5WebGPU Step-by-Step 13 6 7 8 9 10 11 383981 82 83 84Sphere Wireframe
40 4142804376Motion Control
4445 46 4748
49Set Parameters
505156center5253 54555762radius5859 60616368u6465 66676974v7071 7273
7577 7879