学习笔记 | Orillusion-WebGPU小白入门(四)

3D矩阵变换

理论介绍

标准设备坐标

学习笔记 | Orillusion-WebGPU小白入门(四)_第1张图片

学习笔记 | Orillusion-WebGPU小白入门(四)_第2张图片

3D空间转换为2D图形的过程

学习笔记 | Orillusion-WebGPU小白入门(四)_第3张图片

模型变换

学习笔记 | Orillusion-WebGPU小白入门(四)_第4张图片

矩阵计算

学习笔记 | Orillusion-WebGPU小白入门(四)_第5张图片

齐次坐标

Vertex shader的裁剪空间又称为齐次空间

学习笔记 | Orillusion-WebGPU小白入门(四)_第6张图片

正交投影、透视投影

学习笔记 | Orillusion-WebGPU小白入门(四)_第7张图片

正视图、后视图

学习笔记 | Orillusion-WebGPU小白入门(四)_第8张图片

视锥

学习笔记 | Orillusion-WebGPU小白入门(四)_第9张图片

从前发散、从后发散

学习笔记 | Orillusion-WebGPU小白入门(四)_第10张图片

投影矩阵

学习笔记 | Orillusion-WebGPU小白入门(四)_第11张图片

学习笔记 | Orillusion-WebGPU小白入门(四)_第12张图片

显示一个完整的3D图形经过的步骤

  1. 以坐标为原点,建立整个图形的顶点坐标信息

  2. 利用矩阵变换,将一个图形在世界空间坐标中进行坐标变换

  3. 利用投影矩阵,用摄像机和视锥模拟不同视角

  4. 将裁剪空间进行统一的归一化处理,抛弃NDC空间之外的图形

  5. 把整个NDC空间通过内部的视图变换,得到二维平面结果

最后的两步是由GPU驱动自动完成,不需要开发者的参与。我们需要考虑的主要是(第三步)在Vertex Shader中返回裁剪空间的计算结果。前两步主要是在用户的逻辑代码中进行的。

学习笔记 | Orillusion-WebGPU小白入门(四)_第13张图片

注意:

  • 世界空间的坐标系可以和裁剪空间不同。因为我们引入了摄像机和视锥的概念,所以可以自由安排视锥体对应的坐标系结构。可以将整个空间的方向和大小按照我们的需求自定义修改。不需要强行的规范坐标系的范围和坐标轴的方向。所以一般的场景中我们会使用一个独立的坐标系来构建整个世界。

  • 具体的顶点数据的处理过程。不推荐改变原始顶点数据的方法变换图形(不高效:数据写入、管线切换)。推荐如果模型没有变化就不改变顶点坐标,再额外准备一个GPU的Buffer专门存储矩阵的数据。可以将需要变换的ModelView Matrix和Projection Matrix独立的相乘计算出结果,形成一个统一的ModelViewProjection矩阵,把它通过BindGroup作为全局参数传入到Vertex Shader中。然后可以在Vertex Shader中将顶点的坐标信息和MVP矩阵相乘得到最终的结果。好处是原始数据没有改变,可以复用一个模型的顶点数据。JS矩阵计算是用CPU,把矩阵放到Shader中利用GPU的并行计算效率会好很多。

学习笔记 | Orillusion-WebGPU小白入门(四)_第14张图片

Demo

http://github.com/Orillusion/orillusion-webgpu-samples

/src/rotatingCube.ts

注:在triangle.ts的基础上进行的教学

triangle.ts(实际上是cube.ts)

1个cube需要6个矩形面进行组合,1个矩形至少需要2个三角面进行组合,1个三角面如果不是strip模式或者用index来绘制,则需要3个独立的点来构成,1个cube至少需要36个点位信息。

const vertex = new Float32Array([
    // float3 position, float2 uv
    // face1
    +1, -1, +1,
    -1, -1, +1,
    -1, -1, -1,
    +1, -1, -1,
    +1, -1, +1,
    -1, -1, -1,
    // face
    +1, +1, +1,
    +1, -1, +1,
    +1, -1, -1,
    +1, +1, -1,
    +1, +1, +1,
    +1, -1, -1,
    // face
    -1, +1, +1,
    +1, +1, +1,
    +1, +1, -1,
    -1, +1, -1,
    -1, +1, +1,
    +1, +1, -1,
    // face
    -1, -1, +1,
    -1, +1, +1,
    -1, +1, -1,
    -1, -1, -1,
    -1, -1, +1,
    -1, +1, -1,
    // face
    +1, +1, +1,
    -1, +1, +1,
    -1, -1, +1,
    -1, -1, +1,
    +1, -1, +1,
    +1, +1, +1,
    // face
    +1, -1, -1,
    -1, -1, -1,
    -1, +1, -1,
    +1, +1, -1,
    +1, -1, -1,
    -1, +1, -1,
])

const vertexCount = 36

export {vertex, vertexCount}

colorTriangle.ts

本系列教程使用主流的gl-matrix作为矩阵的计算工具

在项目目录下的terminal通过npm install gl-matrix进行安装。

import positionVert from './shaders/position.vert.wgsl?raw'
import colorFrag from './shaders/color.frag.wgsl?raw'
import * as triangle from './util/triangle'
import {mat4,vec3} from 'gl-matrix'

// initialize webgpu device & config canvas context
async function initWebGPU(canvas: HTMLCanvasElement) {
    if(!navigator.gpu)
        throw new Error('Not Support WebGPU')
    const adapter = await navigator.gpu.requestAdapter()
    if (!adapter)
        throw new Error('No Adapter Found')
    const device = await adapter.requestDevice()
    const context = canvas.getContext('webgpu') as GPUCanvasContext
    const format = navigator.gpu.getPreferredCanvasFormat ? navigator.gpu.getPreferredCanvasFormat() : context.getPreferredFormat(adapter)
    const devicePixelRatio = window.devicePixelRatio || 1
    canvas.width = canvas.clientWidth * devicePixelRatio
    canvas.height = canvas.clientHeight * devicePixelRatio
    const size = {width: canvas.width, height: canvas.height}
    context.configure({
        device, format,
        // prevent chrome warning after v102
        alphaMode: 'opaque'
    })
    return {device, context, format, size}
}

// create a simple pipiline & buffers(暂时不动)
async function initPipeline(device: GPUDevice, format: GPUTextureFormat,size:{width:number,height:number}) {
    const pipeline = await device.createRenderPipelineAsync({
        label: 'Basic Pipline',
        layout: 'auto',
        vertex: {
            module: device.createShaderModule({
                code: positionVert,
            }),
            entryPoint: 'main',
            buffers: [{
                arrayStride: 3 * 4, // 3 float32,
                attributes: [
                    {
                        // position xyz
                        shaderLocation: 0,
                        offset: 0,
                        format: 'float32x3',
                    }
                ]
            }]
        },
        fragment: {
            module: device.createShaderModule({
                code: colorFrag,
            }),
            entryPoint: 'main',
            targets: [
                {
                    format: format
                }
            ]
        },
        primitive: {
            topology: 'triangle-list', // try point-list, line-list, line-strip, triangle-strip?
            cullMode:'back' //剔除背面
        },
        
        // 深度测试
        // Enable depth testing since we have z-level positions
        // Fragment closest to the camera is rendered in front
        depthStencil: {
            depthWriteEnabled: true,
            depthCompare: 'less',
            format: 'depth24plus', //深度贴图的数据格式,指深度存储的精度
        }
    } as GPURenderPipelineDescriptor)
    
    // 创建深度贴图
    // create depthTexture for renderPass
    const depthTexture = device.createTexture({
        size, format: 'depth24plus',//贴图的大小应该和画布大小一致
        usage: GPUTextureUsage.RENDER_ATTACHMENT,
    })
    
    
    // create vertex buffer(可以保留)
    const vertexBuffer = device.createBuffer({
        label: 'GPUBuffer store vertex',
        size: triangle.vertex.byteLength,
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
        //mappedAtCreation: true
    })
    device.queue.writeBuffer(vertexBuffer, 0, triangle.vertex)
    // create color buffer(可以保留)
    const colorBuffer = device.createBuffer({
        label: 'GPUBuffer store rgba color',
        size: 4 * 4, // 4 * float32
        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    })
    device.queue.writeBuffer(colorBuffer, 0, new Float32Array([1,1,0,1]))
    
    //新建一个GPU Buffer用于存储MVP矩阵
    const mvpMatrix = device.createBuffer({
        size:4*4*4 //4*4的矩阵,外加矩阵可以是小数 16 float32 
    	usage:GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
    })
    
    // create a uniform group for color
    const uniformGroup = device.createBindGroup({
        label: 'Uniform Group with colorBuffer',
        layout: pipeline.getBindGroupLayout(0),
        entries: [
            {
                binding: 0,
                resource: {
                    buffer: colorBuffer
                }
            },
            {
                binding: 1,
                resource: {
                    buffer: mvpMatrix
                }
            }, // 这两个可以交换顺序
        ]
    })
    // return all vars
    return {pipeline, vertexBuffer, colorBuffer, uniformGroup,mvpMatrix,depthTexture}
}

// create & submit device commands
function draw(device: GPUDevice, context: GPUCanvasContext, pipelineObj: {
    pipeline: GPURenderPipeline,
    vertexBuffer: GPUBuffer,
    colorBuffer: GPUBuffer,
    uniformGroup: GPUBindGroup,
    depthTexture: GPUTexture,
}) {
    const commandEncoder = device.createCommandEncoder()
    const view = context.getCurrentTexture().createView()
    const renderPassDescriptor: GPURenderPassDescriptor = {
        colorAttachments: [
            {
                view: view,
                clearValue: { r: 0, g: 0, b: 0, a: 1.0 },
                loadOp: 'clear',
                storeOp: 'store'
            }
        ],
        //深度模板附件
            depthStencilAttachment: {
            view: pipelineObj.depthView.createView(),
            depthClearValue: 1.0,//全部清空
            depthLoadOp: 'clear',
            depthStoreOp: 'store',
        }
    }
    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor)
    passEncoder.setPipeline(pipelineObj.pipeline)
    // set uniformGroup
    passEncoder.setBindGroup(0, pipelineObj.uniformGroup)
    // set vertex
    passEncoder.setVertexBuffer(0, pipelineObj.vertexBuffer)
    // 3 vertex form a triangle
    passEncoder.draw(triangle.vertexCount)
    passEncoder.end()
    // webgpu run in a separate process, all the commands will be executed after submit
    device.queue.submit([commandEncoder.finish()])
}

async function run(){
    const canvas = document.querySelector('canvas')
    if (!canvas)
        throw new Error('No Canvas')
    const {device, context, format,size} = await initWebGPU(canvas)
    const pipelineObj = await initPipeline(device, format,size)
    
    const position = {x:0, y:0, z:-8},
    const rotation = {x:0.5, y:0, z:0},
    const scale = {x:1, y:1, z:1}

    // first draw
    function frame(){
        rotation.x += 0.001
        rotation.y += 0.001
    
    	const modelViewMatrix = mat4.create()
    	// translate position
    	mat4.translate(modelViewMatrix, modelViewMatrix, vec3.fromValues(position.x, position.y, position.z))
    	// rotate
    	mat4.rotateX(modelViewMatrix, modelViewMatrix, rotation.x)
    	mat4.rotateY(modelViewMatrix, modelViewMatrix, rotation.y)
    	mat4.rotateZ(modelViewMatrix, modelViewMatrix, rotation.z)
    	// scale
    	mat4.scale(modelViewMatrix, modelViewMatrix, vec3.fromValues(scale.x, scale.y, scale.z))
    
    	//设置projection matrix的API
    	const projectionMatrix = mat4.create()
    	mat4.perspective(projectionMatrix,Math.PI/2,size.width/size.height,1,100)
    
    	const mvpMatrix = mat4.create()
    	mat.multiply(mvpMatrix,projectionMatrix,modelViewMatrix)
    	device.queue.writeBuffer(pipelineObj.mvpMatrix,0,mvpMatrix as Float32Array)
    
    	/* ---- 注意以下顺序都不能打乱 ----
         * MVP的顺序是projection乘以modelView
     	* ModelView是按照平移旋转缩放的顺序进行的
     	* shader里的计算是MVP乘以坐标
     	*/
    
        draw(device, context, pipelineObj)
    } // 间隔小于40ms就比较流畅了,过小不推荐(CPU&GPU计算、其他异步程序,主流显示器16.6,漏帧错帧
    

    // update colorBuffer if color changed
    document.querySelector('input[type="color"]')?.addEventListener('input', (e:Event) => {
        // get hex color string
        const color = (e.target as HTMLInputElement).value
        console.log(color)
        // parse hex color into rgb
        const r = +('0x' + color.slice(1, 3)) / 255
        const g = +('0x' + color.slice(3, 5)) / 255
        const b = +('0x' + color.slice(5, 7)) / 255
        // write colorBuffer with new color
        device.queue.writeBuffer(pipelineObj.colorBuffer, 0, new Float32Array([r, g, b, 1]))
        draw(device, context, pipelineObj)
        //开启动画循环
        requestAnimationFrame(frame)
    }
    //浏览器UI刷新之前进行一次回调
    requestAnimationFrame(frame)
    
    // update vertexBuffer
    document.querySelector('input[type="range"]')?.addEventListener('input', (e:Event) => {
        // get input value
        const value = +(e.target as HTMLInputElement).value
        console.log(value)
        // chagne vertex 0/3/6
        triangle.vertex[0] = 0 + value
        triangle.vertex[3] = -0.5 + value
        triangle.vertex[6] = 0.5 + value
        // write vertexBuffer with new vertex
        device.queue.writeBuffer(pipelineObj.vertexBuffer, 0, triangle.vertex)
        draw(device, context, pipelineObj)
    })
    // re-configure context on resize
    window.addEventListener('resize', ()=>{
        canvas.width = canvas.clientWidth * devicePixelRatio
        canvas.height = canvas.clientHeight * devicePixelRatio
        // don't need to recall context.configure() after v104
        draw(device, context, pipelineObj)
    })
}
run()

Vertex Shader : prosition.vert.wgsl

//使用color的形式在Vertex Shader中通过@group(0)@binding(1)的形式来获取MVP矩阵
@group(0) @binding(1) var<uniform>mvp : mat4x4<f32>;

//自定义返回结构
struct VertexOutput{
    @builin(posotion) Position : vec4<f32>,
    @location(0) fragPosition: vec4<f32>
};

@stage(vertex)
fn main(@location(0) position : vec4<f32>) -> VertexOutput {
    var out: VertexOutput;
    out.position = mvp * position;
    out.fragPosition = 0.5 * (position + vec4(1.0,1.0,1.0,1.0); //避免数值太多小于0的,所以有大片的黑色
    return out;
}

/* 注意:
 * 传入的position是一个1*3的数组,是不能和4*4的矩阵直接相乘的
 * 法1:return mvp * vec4(position, 1.0);
 * 法2:把cube.ts所有的坐标数据都+1变成vec4,但需要去调整管线的相关设置
 * 法3:把position直接写为vec4【@location(0) position : vec4】
 * mvp * position顺序不能颠倒(矩阵左乘右乘的区别)
 

通过mat4.perspective API创建符合透视空间的投影矩阵

学习笔记 | Orillusion-WebGPU小白入门(四)_第15张图片

调整物体位置

学习笔记 | Orillusion-WebGPU小白入门(四)_第16张图片

fragment shader : color.frag.wgsl

@group(0) @binding(0) var<uniform> color : vec4<f32>;

@stage(fragment)
fn main(@location(0) fragPosition:vec4<f32>) -> @location(0) vec4<f32> {
    
    //WGSL中引入的group变量必须使用,否则会报错
    var a = color;
    
    return fragPosition;
}
/* Box的颜色变成了RGB的过渡区间
 * Vertex Shader会把数据传给fragment shader
 * Vertex Shader是根据顶点数量进行的并行运行,而fragment shader的是根据片源(像素点)数量
 * 每个三角面的3个顶点的数据可以理解为是vertex shader的直接返回的
 * 被三角面框起来的中间这些点的数据:GPU默认会采取的插值方法来处理每个片源对应的数据;3个点之间的数据会根据3个顶点的数值做线性插值
 * 返回的每个点的坐标对应到fragment shader里其实就是每个片源或者像素点在空间里对应的插值坐标(范围-1~1)
 * 所以如果我们直接将fragPosition返回作为颜色,自然就可以得到一个这样的过度颜色区间
 */

×:可以手动的调节整个顶点的结构,优化绘制顺序,以保证整个图形是按照深度的顺序来进行绘制图形(不高效)

√:开启深度测试。深度测试属于渲染管线中的一个环节,默认是不开启的。如果开启管线会在传入fragment shader之前,将各个片元进行z轴的深度对比。如果有相互遮挡的片元,深度较大的片源(离屏幕相对远的片源将会被舍弃)。没有开启深度测试情况下,没有被遮挡的面是内部面。

你可能感兴趣的:(WebGPU学习笔记,javascript)