Redian新闻
>
欢迎来到 WebGPU 的世界

欢迎来到 WebGPU 的世界

科技


WebGPU是一门神奇的技术,在浏览器支持率0%,标准还没有定稿的情况下,就已经被Three.js和Babylon.js等主流3D和游戏框架支持了。而且被Tensorflow.js用来加速手机端的深度学习,比起WebGL能带来20~30倍的显著提升。


在主流框架中 WebGPU 的例子


1、在Three.js中使用WebGPU

使用Three.js的封装,我们可以直接生成WebGPU的调用。

我们照猫画虎引入WebGPU相关的库:

   import * as THREE from 'three';
   import * as Nodes from 'three-nodes/Nodes.js';
   import { add, mul } from 'three-nodes/ShaderNode.js';

            import WebGPU from './jsm/capabilities/WebGPU.js';
   import WebGPURenderer from './jsm/renderers/webgpu/WebGPURenderer.js';
...

剩下就跟普通的WebGL代码写起来差不多:

   async function init({

    if ( WebGPU.isAvailable() === false ) {
     document.body.appendChild( WebGPU.getErrorMessage() );
     throw new Error'No WebGPU support' );
    }

    const container = document.createElement( 'div' );
    document.body.appendChild( container );

    camera = new THREE.PerspectiveCamera( 45window.innerWidth / window.innerHeight, 14000 );
    camera.position.set( 02001200 );

    scene = new THREE.Scene();
...

只不过渲染器使用WebGPURenderer:

    renderer = new WebGPURenderer();
    renderer.setPixelRatio( window.devicePixelRatio );
    renderer.setSize( window.innerWidth, window.innerHeight );
    container.appendChild( renderer.domElement );
...

如果封装的不能满足需求了,我们可以使用WGSL语言进行扩展:

    material = new Nodes.MeshBasicNodeMaterial();
    material.colorNode = desaturateWGSLNode.call( { colornew Nodes.TextureNode( texture ) } );
    materials.push( material );

    const getWGSLTextureSample = new Nodes.FunctionNode( `
     fn getWGSLTextureSample( tex: texture_2d<f32>, tex_sampler: sampler, uv:vec2<f32> ) -> vec4<f32> {
      return textureSample( tex, tex_sampler, uv ) * vec4<f32>( 0.0, 1.0, 0.0, 1.0 );
     }
    `
 );

    const textureNode = new Nodes.TextureNode( texture );

    material = new Nodes.MeshBasicNodeMaterial();
    material.colorNode = getWGSLTextureSample.call( { tex: textureNode, tex_sampler: textureNode, uvnew Nodes.UVNode() } );
    materials.push( material );

WGSL是WebGPU进行GPU指令编程的语言。类似于OpenGL的GLSL, Direct3D的HLSL。

我们来看一个完整的例子,显示一个跳舞的小人,也不过100多行代码:

<!DOCTYPE html>
<html lang="en">
 <head>
  <title>three.js - WebGPU - Skinning</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  <link type="text/css" rel="stylesheet" href="main.css">
  <meta http-equiv="origin-trial" content="AoS1pSJwCV3KRe73TO0YgJkK9FZ/qhmvKeafztp0ofiE8uoGrnKzfxGVKKICvoBfL8dgE0zpkp2g/oEJNS0fDgkAAABeeyJvcmlnaW4iOiJodHRwczovL3RocmVlanMub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJHUFUiLCJleHBpcnkiOjE2NTI4MzE5OTksImlzU3ViZG9tYWluIjp0cnVlfQ==">
 </head>
 <body>
  <div id="info">
   <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> WebGPU - Skinning
  </div>
  <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
  <script type="importmap">
   {
    "imports": {
     "three""../build/three.module.js",
     "three-nodes/""./jsm/nodes/"
    }
   }
  
</script>

  <script type="module">
   import * as THREE from 'three';
   import * as Nodes from 'three-nodes/Nodes.js';
   import { FBXLoader } from './jsm/loaders/FBXLoader.js';
   import WebGPU from './jsm/capabilities/WebGPU.js';
   import WebGPURenderer from './jsm/renderers/webgpu/WebGPURenderer.js';
   import LightsNode from 'three-nodes/lights/LightsNode.js';

   let camera, scene, renderer;
   let mixer, clock;
   init().then( animate ).catch( error );

   async function init({
    if ( WebGPU.isAvailable() === false ) {
     document.body.appendChild( WebGPU.getErrorMessage() );
     throw new Error'No WebGPU support' );
    }
    camera = new THREE.PerspectiveCamera( 50window.innerWidth / window.innerHeight, 11000 );
    camera.position.set( 100200300 );
    scene = new THREE.Scene();
    camera.lookAt( 01000 );
    clock = new THREE.Clock();

    // 光照
    const light = new THREE.PointLight( 0xffffff );
    camera.add( light );
    scene.add( camera );
    const lightNode = new LightsNode().fromLights( [ light ] );
    const loader = new FBXLoader();
    loader.load( 'models/fbx/Samba Dancing.fbx'function ( object {
     mixer = new THREE.AnimationMixer( object );
     const action = mixer.clipAction( object.animations[ 0 ] );
     action.play();
     object.traverse( function ( child {
      if ( child.isMesh ) {
       child.material = new Nodes.MeshStandardNodeMaterial();
       child.material.lightNode = lightNode;
      }
     } );
     scene.add( object );
    } );

    // 渲染
    renderer = new WebGPURenderer();
    renderer.setPixelRatio( window.devicePixelRatio );
    renderer.setSize( window.innerWidth, window.innerHeight );
    document.body.appendChild( renderer.domElement );
    window.addEventListener( 'resize', onWindowResize );
    return renderer.init();
   }

   function onWindowResize({
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize( window.innerWidth, window.innerHeight );
   }

   function animate({
    requestAnimationFrame( animate );
    const delta = clock.getDelta();
    if ( mixer ) mixer.update( delta );
    renderer.render( scene, camera );
   }

   function error( error {
    console.error( error );
   }
  
</script>
 </body>
</html>


2、在Babylon.js中使用WebGPU

Babylon.js的封装与Three.js大同小异,我们来看个PlayGround的效果:

不同之处在于处理WebGPU的支持情况时,Babylon.js并不判断整体上支不支持WebGPU,而是只看具体功能。

比如上面的例子,只判断是不是支持计算着色器。

    const supportCS = engine.getCaps().supportComputeShaders;

不过目前在macOS上,只有WebGPU支持计算着色器。

如果我们把环境切换成WebGL2,就变成下面这样了:

顺便说一句,Babylon.js判断WebGL2和WebGL时也是同样的逻辑,有高就用高。

如果对于着色器不熟悉,Babylon.js提供了练习Vertex Shader和Pixel Shader的环境:https://cyos.babylonjs.com/ , 带语法高亮和预览。

针对需要通过写手机应用的场景,Babylon.js提供了与React Native结合的能力:


3、用WebGPU进行深度学习加速

除了3D界面和游戏,深度学习的推理器也是GPU的重度用户。所以Tensorflow.js也在还落不了地的时候就支持了WebGPU。实在是计算着色器太重要了。

写出来的加速代码就像下面一样,很多算子的实现最终是由WGSL代码来实现的,最终会转换成GPU的指令。

  getUserCode(): string {
    const rank = this.xShape.length;
    const type = getCoordsDataType(rank);
    const start = this.xShape.map((_, i) => `uniforms.pad${i}[0]`).join(',');
    const end = this.xShape
                    .map(
                        (_, i) => `uniforms.pad${i}[0] + uniforms.xShape${
                            rank > 1 ? `[${i}]` : ''}
`
)
                    .join(',');
    const startValue = rank > 1 ? `${type}(${start})` : `${start}`;
    const endValue = rank > 1 ? `${type}(${end})` : `${end}`;

    const leftPadCondition = rank > 1 ? `any(outC < start)` : `outC < start`;
    const rightPadCondition = rank > 1 ? `any(outC >= end)` : `outC >= end`;

    const unpackedCoords = rank > 1 ?
        ['coords[0]''coords[1]''coords[2]''coords[3]'].slice(0, rank) :
        'coords';

    const userCode = `
      ${getMainHeaderAndGlobalIndexString()}
        if (index < uniforms.size) {
          let start = ${startValue};
          let end = ${endValue};
          let outC = getCoordsFromIndex(index);
          if (${leftPadCondition} || ${rightPadCondition}) {
            setOutputAtIndex(index, uniforms.constantValue);
          } else {
            let coords = outC - start;
            setOutputAtIndex(index, getX(${unpackedCoords}));
          }
        }
      }
    `
;
    return userCode;
  }



无框架手写WebGPU代码



通过框架,我们可以迅速地跟上技术的前沿。但是,框架的封装也容易让我们迷失对于技术本质的把握。

现在我们来看看如何手写WebGPU代码。

1、从Canvas说起

不管是WebGL还是WebGPU,都是对于Canvas的扩展。做为HTML 5的重要新增功能,大家对于2D的Canvas应该都不陌生。

比如我们要画一个三角形,就可以调用lineTo API来实现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Canvas</title>
</head>
<body>
    <canvas id="webcanvas" width="200" height="200" style="background-color: #eee"></canvas>
    <script>
        const canvas=document.getElementById('webcanvas');
        const ctx=canvas.getContext('2d');

        ctx.beginPath();
        ctx.moveTo(75,50);
        ctx.lineTo(100,75);
        ctx.lineTo(100,25);
        ctx.fill();
    
</script>
</body>

画出来的结果如下:

我们要修改画出来的图的颜色怎么办?
ctx有fillStyle属性,支持CSS的颜色字符串。

比如我们设成红色,可以这么写:

ctx.fillStyle = 'red';

也可以这么写:

ctx.fillStyle = '#F00';

还可以这么写:

ctx.fillStyle = 'rgb(255,0,0,1)';


2、从2D到3D

从2D Canvas到3D WebGL的最大跨越,就是从调用API,到完全不同于JavaScript的新语言GLSL的出场。

第一步的步子我们迈得小一点,不画三角形了,只画一个点。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Test OpenGL for a point</title>
</head>

<body>
    <canvas id="webgl" width="500" height="500" style="background-color: blue"></canvas>
    <script>
        const canvas = document.getElementById('webgl');
        const gl = canvas.getContext('webgl');

        const program = gl.createProgram();

        const vertexShaderSource = `
           void main(){
              gl_PointSize=sqrt(20.0);
              gl_Position =vec4(0.0,0.0,0.0,1.0);
           }`
;

        const vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vertexShaderSource);
        gl.compileShader(vertexShader);
        gl.attachShader(program, vertexShader);

        const fragShaderSource = `
          void main(){
            gl_FragColor = vec4(1.0,0.0,0.0,1.0);
          }
        `
;

        const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShader, fragShaderSource);
        gl.compileShader(fragmentShader);
        gl.attachShader(program, fragmentShader);

        gl.linkProgram(program);
        gl.useProgram(program);

        gl.drawArrays(gl.POINTS, 01);

    
</script>

</body>

</html>

getContext时将2d换成webgl。
我们可以加一行console.log(gl)来看下gl是什么东西:

我们可以看到,它是一个WebGLRenderingContext对象。
顺便说一句,之前我们拿到的2D的Context是CanvasRenderingContext2D。

下面就引入了两段程序中的程序,第一段叫做顶点着色器,用于顶点的坐标信息。第二段叫做片元着色器,用于配置如何进行一些属性的操作,在本例中我们做一个最基本的操作,改颜色。

我们先看顶点着色器的代码:

           void main(){
gl_PointSize=sqrt(20.0);
gl_Position =vec4(0.0,0.0,0.0,1.0);
}

像其他语言一样,glsl中的代码也需要一个入口函数。

gl_PointSize是一个系统变量,用于存储点的大小。我特意给大小加个了sqrt函数,给大家展示glsl的库函数。

gl_Position用于存储起点的位置。vec4是由4个元素构成的向量。

GLSL的数据类型很丰富,包括标量、向量、数组、矩阵、结构体和采样器等。

标量有布尔型bool, 有符号整数int, 无符号整数uint和浮点数float 4种类型。

类型的使用方式跟C语言一样,比如我们用float来定义浮点变量。

                float pointSize = sqrt(20.0);
gl_PointSize=pointSize;

GLSL没有double这样表示双精度的类型。在顶点着色器中是没有精度设置的。
但是在片元着色器中有精度的设置,需要指定低精度lowp, 中精度mediump和高精度highp. 一般采用中精度:

            void main(){    
mediump vec4 pointColor;
pointColor.r = 1.0;
pointColor.a = 1.0;
gl_FragColor = pointColor;
}

GLSL因为是基于C语言设计的,不支持泛型,所以每种向量同时有4种子类型的。

以四元组vec4为例,有4种类型:

  • vec4: 浮点型向量
  • ivec4: 整数型向量
  • uvec4: 无符号整数向量
  • bvec4: 布尔型向量。

另外还有vec2, vec3各有4种子类型,以此类推。

在GLSL里面,四元向量最常用的用途有两种,在顶点着色器里充当坐标,和在片元着色器里充当颜色。

当vec4作为坐标使用时,我们可以用x,y,z,w属性来对应4个维度。

我们来看个例子:

                vec4 pos;
pos.x = 0.0;
pos.y = 0.0;
pos.z = 0.0;
pos.w = 1.0;
gl_Position = pos;

同样,我们在片元着色器里面表示红色的时候只用指令r和a两个属性,g,b让它们默认是0:

            void main(){    
mediump vec4 pointColor;
pointColor.r = 1.0;
pointColor.a = 1.0;
gl_FragColor = pointColor;
}

有了顶点着色器和片元着色器的GLSL代码之后,我们将其进行编程,并attach到program上面。

最后再link和use这个program,就可以调用drawArrays来进行绘制了。


3、更现代的GPU编程方法

跨越了从 Canvas API到GLSL的鸿沟了之后,最后到WebGPU这一步相对就容易一些了。

我们要熟悉的是以Vulkan为代表的更现代的GPU的编程方法。

渲染管线不再是唯一,我们可以使用更通用的计算管线了。也不再有顶点着色器和片元着色器那么严格的限制。

另外最重要的一点是,为了提升GPU执行效率,WebGPU不再是像WebGL一样基本每一步都要由CPU来控制,我们使用commandEncoder将所有GPU指令打包在一起,一次性执行。

我们先看一下完整代码有个印象:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Test WebGPU</title>
</head>

<body>
    <canvas id="webgpu" width="500" height="500" style="background-color: blue"></canvas>
    <script>
        async function testGPU({
            const canvas = document.getElementById('webgpu');
            const gpuContext = canvas.getContext('webgpu');

            const adapter = await navigator.gpu.requestAdapter();
            const device = await adapter.requestDevice();

            presentationFormat = gpuContext.getPreferredFormat(adapter);

            gpuContext.configure({
                device,
                format: presentationFormat
            });

            const triangleVertWGSL = `
            @stage(vertex)
            fn main(@builtin(vertex_index) VertexIndex : u32)
             -> @builtin(position) vec4<f32> {
                var pos = array<vec2<f32>, 3>(
                vec2<f32>(0.0, 0.5),
                vec2<f32>(-0.5, -0.5),
                vec2<f32>(0.5, -0.5));

                return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
            }
            `
;

            const redFragWGSL = `
            @stage(fragment)
                fn main() -> @location(0) vec4<f32> {
                return vec4<f32>(1.0, 0.0, 0.0, 1.0);
            }
            `


            const commandEncoder = device.createCommandEncoder();
            const textureView = gpuContext.getCurrentTexture().createView();

            const pipeline = device.createRenderPipeline({
                vertex: {
                    module: device.createShaderModule({
                        code: triangleVertWGSL,
                    }),
                    entryPoint'main',
                },
                fragment: {
                    module: device.createShaderModule({
                        code: redFragWGSL,
                    }),
                    entryPoint'main',
                    targets: [
                        {
                            format: presentationFormat,
                        },
                    ],
                },
                primitive: {
                    topology'triangle-list',
                },
            });

            const renderPassDescriptor = {
                colorAttachments: [
                    {
                        view: textureView,
                        loadValue: { r1.0g1.0b1.0a1.0 }, 
                        storeOp'store',
                    },
                ],
            };
            const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
            console.log(passEncoder);
            passEncoder.setPipeline(pipeline);
            passEncoder.draw(3100);
            passEncoder.end();

            device.queue.submit([commandEncoder.finish()]);
        }

        testGPU();

    
</script>

</body>

</html>

因为浏览器还没有支持,所以我们需要像Chrome Canary这样的支持最新技术的浏览器。而且还要打开支持的开关,比如在Chrome Canary里是enable-unsafe-webgpu.

三角形画出来的结果如下:

现在的Context从WebGL的WebGLRenderingContext变成了GPUCanvasContext。

WGSL语言的语法更像Rust,vec4这样的容器可以用泛型的写法绑定类型:

            @stage(vertex)
fn main(@builtin(vertex_index) VertexIndex : u32)
-> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>(0.0, 0.5),
vec2<f32>(-0.5, -0.5),
vec2<f32>(0.5, -0.5));

return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}

对比下Rust的代码看看像不像:

fn fib2(n: i32) -> i64 {
    if n <= 2 {
        return 1i64
    } else {
        return fib2(n - 1) + fib2(n - 2)
    }
}

WGSL是为了规避知识产权问题发明的新语言,本质上它和GLSL,HLSL等语言一样,都可以编译成Vulkan的SPIR-V二进制格式:
.

Vulkan不限制使用什么样的语言,既可以使用GLSL, HLSL,也可以使用Open CL或者是Open CL的高级封装SYCL。

转换成SPIR-V格式之后,可以转成iOS上的Metal Shading Language,也可以转成Windows Direct 12上用的DXIL。

WebGPU没有这么自由,发明了一门新语言WGSL,不过其思想都是基于SPIR-V的。

在WebGPU和WGSL还未定版,资料还比较缺乏的情况下,我们可以先学习Vulkan相关的知识,然后迁移到WebGPU上来。本质上是同样的东西,只是封装略有不同。

我们之前学习的GLSL的知识同样用得上,而且在这种类Rust风格中可以写得更爽一些。

比如同样是给片元用的颜色值,在保留了vec4可以继续使用r,g,b,a分量的好处之外,因为指定了f32的精度,就不需要mediump了。而且,类型可以自动推断,我们直接给个var就好了:

            @stage(fragment)
fn main() -> @location(0) vec4<f32> {
var triColor = vec4<f32>(0.0,0.0,0.0,0.0);
triColor.r = 1.0;
triColor.a = 1.0;
return triColor;
}

有了作为功能核心的WGSL,剩下的工作主要就是组装了。

我们把指令打包在 CommandEncoder中,然后通过beginRenderPass来创建一个渲染Pass,再给这个Pass设置一个渲染的流水线,添加相应的draw操作,最后提交到GPU设备的队列中,就大功告成了。



小结



相对于基于OpenGL ES 2.0的WebGL 1.0,WebGPU更接近于Vulkan这样更能发挥GPU能力的新API,可以更有效地发挥出新的GPU的能力。就像渲染上Three.js和Babylon.js给我们展示的那样和计算上Tensorflow.js的飞跃一样。

虽然浏览器还不支持,但是不成熟的主要是封装,底层的Vulkan和Metal技术已经非常成熟,并且广泛被客户端所使用了。

WebGPU这个能力暴露给H5和小程序之后,将给元宇宙等热门应用插上性能倍增的翅膀。结合WebXR等支持率更成问题的新技术一起,成为未来几年前端的主要工具。




关注「Alibaba F2E」微信公众号把握阿里巴巴前端新动向



微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
​别的简报|Lo-fi Girl 停播了,全球第一学习 BGM 电台被 YouTube 下架“欢迎来到奥克兰,真是生活的‘好地方’!”LA警报拉响!最高法院泄密,全美禁止堕胎?!州长喊话:欢迎来加州,费用全免!EGG宝可梦“穿梭在银河的火箭队”:一款自带台词+BGM的雕像!Alexei Efros 团队发布 BlobGAN:灵活组合物体的布局与外形特征Web3夹在了新旧世界之间英特尔收购GPU企业:创始人曾奠定高通移动GPU根基欢迎来到一个受精卵比母亲重要的世界拿下 Twitter 的马斯克,掐住了 Web3 的喉舌“阳春白雪”与书法审美反战之反战你可以错过Web3,但不要错过Web5中西方医疗拔罐对比Kubernetes No CPU Limit:不限制 CPU 可能会更好Web3没玩明白,Web5来了?! Jack Dorsey发布新概念,马斯克第一个表示支持最左倾的大法官金斯伯格RBG也不喜欢Roe v. WadeCABG术后患者心绞痛,根源竟不在冠脉上?从Web2弄潮至今,这位互联网先知再度抢占Web3高地他拍的世界,震撼了世界Web2游戏大佬纷纷「下凡」,谁能笑傲Web3江湖?从Web3「调头」Web2,FTX到底在图什么?看完演讲想说几句新世界大门已开, Web3的入场券你挑哪张?欢迎光临平凡的世界Web3革命:逃离、信仰、大迁徙千万毕业生走出校门,酷狗为他们打造了专属BGM建设下一代 Web 开放技术——WebContainerGroupOn 上购买 Costco 会员优惠:$40 \bGift Card + $40 网购优惠券【05/23:活动又有了!】Web3是什么?资深Web3行业大佬Mark为大家解读强吞推特为哪般,醉翁之意在Web3邻居家爱意满满的小花园最新!打砸抢来到华人区,工业市Puente Hills商场内珠宝店遭到抢劫!VR游戏周报 | 吃鸡游戏《Population: One》迎来大更新Shanghai Reopening Diary: Why Don’t We Always Let the Weeds Grow这才刚开始学Web3,Web5就已经来了
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。