WebGPU图形编程(5):构建一个线框球体<学习引自徐博士教程>
一、前提
在代码开始之前,你需要自行准备配置环境文件或者你可以去GitHub下载简单配置好的文件:https://github.com/zhiwenhao-0807/webgpu.git
如果你对刚开始如何配置环境疑惑,可以去跟着第一篇博客去学习:https://www.cnblogs.com/wenhao0807/p/15806359.html
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<u;i++){ 9 let pt1:vec3[]=[]; 10 for(let j=0;j<v;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<u-1;i++){ 21 for(let j=0;j<v-1;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 <!DOCTYPE html> 2 <head> 3 <meta charset="utf-8"> 4 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 5 <title>WebGPU Step-by-Step 13</title> 6 <meta name="description" content=""> 7 <meta name="viewport" content="width=device-width, initial-scale=1"> 8 </head> 9 10 <body> 11 <style> 12 .grid { 13 display: grid; 14 height: 100%; 15 grid-template-columns: repeat(8, 1fr); 16 grid-template-rows: 100%; 17 } 18 .grid1 { 19 display: grid; 20 height: 35px; 21 grid-template-columns: repeat(8, 1fr); 22 grid-template-rows: 35px; 23 } 24 .item1 { 25 grid-column: 1/3; 26 } 27 .item2 { 28 grid-column: 3/9; 29 } 30 .item3 { 31 grid-column: 1/3; 32 } 33 .item4 { 34 grid-column: 3/8; 35 } 36 37 </style> 38 <div style="margin-left:20px;"> 39 <h1>Sphere Wireframe</h1><br> 40 41 <div class="grid"> 42 <div class="item1"> 43 <h2>Motion Control</h2> 44 <div id="id-radio"> 45 <label><input type="radio" name="options" value="animation" checked>Animation</label> 46 <label style="margin-left:30px;"><input type="radio" name="options" value="camera">Camera Control</label> 47 </div> 48 <br> 49 <h2>Set Parameters</h2> 50 <div class="grid1"> 51 <div class="item3">center</div> 52 <div class="item4"> 53 <input id="id-center" type="text" value="0, 0, 0" /> 54 </div> 55 </div> 56 <div class="grid1"> 57 <div class="item3">radius</div> 58 <div class="item4"> 59 <input id="id-radius" type="text" value="2" /> 60 </div> 61 </div> 62 <div class="grid1"> 63 <div class="item3">u</div> 64 <div class="item4"> 65 <input id="id-u" type="text" value="20" /> 66 </div> 67 </div> 68 <div class="grid1"> 69 <div class="item3">v</div> 70 <div class="item4"> 71 <input id="id-v" type="text" value="15" /> 72 </div> 73 </div> 74 <br><button type="button" id="btn-redraw"><b>Redraw</b></button> 75 </div> 76 <div class="item2"> 77 <canvas id="canvas-webgpu" width="640" height="480"></canvas> 78 </div> 79 </div> 80 </div> 81 82 <script src="main.bundle.js"></script> 83 </body> 84 </html>