Rendu de Primitives Triangulaires avec WebGPU

Configuration de la Structure HTML

Pour ce tutoriel, nous avons besoin d'une intreface permettant de sélectionner entre deux modes de topologie primitive: triangle-list et triangle-strip. Ces modes définissent comment les sommets sont interprétés pour former des triangles. La documentation WebGPU détaille ces concepts dans la section concernant l'état primitif (Primitive State).


<html lang="fr">
<head>
    <meta charset="utf-8">
    <title>WebGPU - Rendu Triangulaire</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <div>
        <h1>Rendu de Primitives Triangulaires</h1>
        <label><b>Mode de topologie:</b></label>
        <select id="topologySelector">
            <option value="triangle-list">triangle-list</option>
            <option value="triangle-strip">triangle-strip</option>
        </select>
        <br><br>
        <canvas id="gpuCanvas" width="640" height="480"></canvas>
    </div>
    <script src="main.bundle.js"></script>
</body>
</html>

Implémentation des Shaders WGSL

Le shader de sommets définit neuf positions et neuf couleurs pour créer plusieurs triangles connectés. La structure de sortie transmet la position clip et la couleur interpolée vers le fragment shader.

export const ShaderModule = () => {
    const vertexSource = `
    struct VertexOutput {
        [[builtin(position)]] clipPos : vec4<f32>;
        [[location(0)]] fragColor : vec4<f32>;
    };

    [[stage(vertex)]]
    fn main([[builtin(vertex_index)]] idx: u32) -> VertexOutput {
        let positions : array<vec2<f32>, 9> = array<vec2<f32>, 9>(
            vec2<f32>(-0.63,  0.80),
            vec2<f32>(-0.65,  0.20),
            vec2<f32>(-0.20,  0.60),
            vec2<f32>(-0.37, -0.07),
            vec2<f32>( 0.05,  0.18),
            vec2<f32>(-0.13, -0.40),
            vec2<f32>( 0.30, -0.13),
            vec2<f32>( 0.13, -0.64),
            vec2<f32>( 0.70, -0.30)
        );
        
        let colors : array<vec3<f32>, 9> = array<vec3<f32>, 9>(
            vec3<f32>(1.0, 0.0, 0.0),
            vec3<f32>(0.0, 1.0, 0.0),
            vec3<f32>(0.0, 0.0, 1.0),
            vec3<f32>(1.0, 0.0, 0.0),
            vec3<f32>(0.0, 1.0, 0.0),
            vec3<f32>(0.0, 0.0, 1.0),
            vec3<f32>(1.0, 0.0, 0.0),
            vec3<f32>(0.0, 1.0, 0.0),
            vec3<f32>(0.0, 0.0, 1.0)
        );

        var out: VertexOutput;
        out.clipPos = vec4<f32>(positions[idx], 0.0, 1.0);
        out.fragColor = vec4<f32>(colors[idx], 1.0);
        return out;
    }`;

    const fragmentSource = `
    [[stage(fragment)]]
    fn main([[location(0)]] fragColor: vec4<f32>) -> [[location(0)]] vec4<f32> {
        return fragColor;
    }
    `;
    
    return { vertexSource, fragmentSource };
};

Logique Principale de Rendu

Le fichier TypeScript gère l'initialisation WebGPU et configure le pipeilne de rendu. Le mode triangle-list génère un triangle par triplet de sommets, tandis que triangle-strip crée une bande continue où chaque nouveau sommet complète un triangle adjacent.

import $ from 'jquery';
import { verifyWebGPUSupport } from './utils';
import { ShaderModule } from './shaders';

const initializeRenderer = async (topologyMode = 'triangle-list') => {
    const supportMessage = verifyWebGPUSupport();
    if (!supportMessage.includes('supported')) {
        console.error(supportMessage);
        throw new Error('WebGPU non supporté');
    }

    let stripFormat = undefined;
    if (topologyMode === 'triangle-strip') {
        stripFormat = 'uint32';
    }

    const canvasElement = document.getElementById('gpuCanvas') as HTMLCanvasElement;
    const gpuAdapter = await navigator.gpu.requestAdapter() as GPUAdapter;
    const gpuDevice = await gpuAdapter.requestDevice() as GPUDevice;
    const gpuContext = canvasElement.getContext('webgpu') as unknown as GPUCanvasContext;
    const textureFormat = 'bgra8unorm';

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

    const shaderCode = ShaderModule();
    const renderPipeline = gpuDevice.createRenderPipeline({
        vertex: {
            module: gpuDevice.createShaderModule({
                code: shaderCode.vertexSource
            }),
            entryPoint: "main"
        },
        fragment: {
            module: gpuDevice.createShaderModule({
                code: shaderCode.fragmentSource
            }),
            entryPoint: "main",
            targets: [{
                format: textureFormat as GPUTextureFormat
            }]
        },
        primitive: {
            topology: topologyMode as GPUPrimitiveTopology,
            stripIndexFormat: stripFormat as GPUIndexFormat
        }
    });

    const encoder = gpuDevice.createCommandEncoder();
    const textureView = gpuContext.getCurrentTexture().createView();
    
    const renderPassDesc = {
        colorAttachments: [{
            view: textureView,
            loadValue: { r: 0.5, g: 0.5, b: 0.8, a: 1.0 },
            storeOp: 'store'
        }]
    };
    
    const passEncoder = encoder.beginRenderPass(renderPassDesc);
    passEncoder.setPipeline(renderPipeline);
    passEncoder.draw(9, 1, 0, 0);
    passEncoder.endPass();

    gpuDevice.queue.submit([encoder.finish()]);
};

initializeRenderer();

$('#topologySelector').on('change', () => {
    const selectedTopology = $('#topologySelector').val() as string;
    initializeRenderer(selectedTopology);
});

Compilation et Exécution

Pour générer le bundle de production:

npm run prod

Chargez ensuite le fichier HTML dans un navigateur comptaible WebGPU pour observer le rendu. La différence entre triangle-list (trois triangles indépendants) et triangle-strip (bande triangulaire connectée) devient visible en changeant la sélection dans le menu déroulant.

Étiquettes: WebGPU wgsl WebGL 3D Graphics Browser API

Publié le 9 octobre à 23h28