You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WebGPU绘制矩形异常:仅显示三角形的问题求助

WebGPU绘制矩形仅显示三角形的问题解决

问题描述

我是WebGPU与3D渲染领域的新手,尝试使用TypeScript和WGSL绘制矩形。我了解网格由顶点构成,3个顶点可绘制三角形,但设置4个顶点后浏览器仍仅显示三角形,最后一个顶点未被识别。

相关代码

rectangle_mesh.ts

export class RectangleMesh {

    public buffer: GPUBuffer;
    public bufferLayout: GPUVertexBufferLayout;

    constructor(public device: GPUDevice) {
        const vertices: Float32Array = new Float32Array(
            [
                -0.5,      0.5,    1,  1,  1,
                0.5,       0.5,    0,  1,  0,
                0.5,       -0.5,   1,  0,  1,
                -0.5,      -0.5,   0,  0,  0, // 这个顶点未被使用
            ]
        );

        const usage: GPUBufferUsageFlags = GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST;

        const descriptor: GPUBufferDescriptor = {
            size: vertices.byteLength,
            usage: usage,
            mappedAtCreation: true
        };

        this.buffer = device.createBuffer(descriptor);
        new Float32Array(this.buffer.getMappedRange()).set(vertices);
        this.buffer.unmap();

        this.bufferLayout = {
            arrayStride: 20, // 每个顶点5个float,5*4=20字节
            attributes: [
                {
                    shaderLocation: 0,
                    format: "float32x2",
                    offset: 0
                },
                {
                    shaderLocation: 1,
                    format: "float32x3",
                    offset: 8
                }
            ]
        }
    }
}

shader.wgsl

struct Fragment {
    @builtin(position) Position : vec4<f32>,
    @location(0) Color : vec4<f32>
};

@vertex
fn vs_main(@location(0) vertexPosition: vec2<f32>, @location(1) vertexColor: vec3<f32>) -> Fragment {

    var output : Fragment;
    output.Position = vec4<f32>(vertexPosition, 0.0, 1.0);
    output.Color = vec4<f32>(vertexColor, 1.0);

    return output;
}

@fragment
fn fs_main(@location(0) Color: vec4<f32>) -> @location(0) vec4<f32> {
    return Color;
}

main.ts

import { RectangleMesh } from "./rectangle_mesh";
import shader from "./shaders/shader.wgsl";
import { TriangleMesh } from "./triangle_mesh";

const Initialize = async() => {

    const canvas : HTMLCanvasElement = <HTMLCanvasElement> document.getElementById("game_window");

    //adapter: 物理GPU的包装,描述特性和限制
    const adapter : GPUAdapter = <GPUAdapter> await navigator.gpu?.requestAdapter();

    //device: GPU功能的操作入口
    const device : GPUDevice = <GPUDevice> await adapter?.requestDevice();

    //context: 类似Vulkan实例或OpenGL上下文
    const context : GPUCanvasContext = <GPUCanvasContext><unknown>canvas.getContext("webgpu");
    const format : GPUTextureFormat = "bgra8unorm";
    context.configure({
        device: device,
        format: format,
        alphaMode: "opaque"
    });

    const bindGroupLayout = device.createBindGroupLayout({
        entries: [],
    });

    const bindGroup = device.createBindGroup({
        layout: bindGroupLayout,
        entries: []
    });
    
    const pipelineLayout = device.createPipelineLayout({
        bindGroupLayouts: [bindGroupLayout]
    });

    // const triangleMesh = new TriangleMesh(device);
    const rectangleMesh = new RectangleMesh(device);

    const pipeline = device.createRenderPipeline({
        vertex : {
            module : device.createShaderModule({
                code : shader
            }),
            entryPoint : "vs_main",
            buffers: [rectangleMesh.bufferLayout, ]
        },

        fragment : {
            module : device.createShaderModule({
                code : shader
            }),
            entryPoint : "fs_main",
            targets : [{
                format : format
            }]
        },

        primitive : {
            topology : "triangle-list"
        },

        layout: pipelineLayout
    });

    //command encoder: 记录绘制命令用于提交
    const commandEncoder : GPUCommandEncoder = device.createCommandEncoder();

    //texture view: 颜色缓冲区的视图
    const textureView : GPUTextureView = context.getCurrentTexture().createView();

    //renderpass: 包含绘制命令,从command encoder创建
    const renderpass : GPURenderPassEncoder = commandEncoder.beginRenderPass({
        // 设置背景色
        colorAttachments: [{
            view: textureView,
            clearValue: {r: 52/255, g: 124/255, b: 235/255, a: 1.0},
            loadOp: "clear",
            storeOp: "store"
        }]
    });
    renderpass.setPipeline(pipeline);
    renderpass.setBindGroup(0, bindGroup)
    renderpass.setVertexBuffer(0, rectangleMesh.buffer)
    renderpass.draw(4, 1, 0, 0);
    renderpass.end();

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

function InitializeScreen() {
    console.log("Width = ", window.innerWidth.toString());
    console.log("Height = ", window.innerHeight.toString());
    
    document.getElementById("game_window")?.setAttribute("width", window.innerWidth.toString());
    document.getElementById("game_window")?.setAttribute("height", window.innerHeight.toString());
}

window.onresize = () => {
    console.log("Resizing ...");
    InitializeScreen();
    Initialize();
}

InitializeScreen();
Initialize();

渲染效果

仅显示一个三角形,第四个顶点未被识别。

问题原因

当前渲染管线使用的是triangle-list拓扑,这种模式下GPU会每3个连续顶点组成一个独立三角形。你提供了4个顶点,GPU只会用前3个顶点绘制第一个三角形,第四个顶点因不足3个的数量要求被直接忽略,因此只显示单个三角形。

解决方案

以下三种方案都可以解决这个问题,你可以根据需求选择:

方案1:改用triangle-strip拓扑(最简单)

triangle-strip拓扑会自动复用相邻顶点绘制连续三角形,4个顶点即可组成两个三角形(矩形)。

修改main.ts中渲染管线的primitive配置:

primitive : {
    topology : "triangle-strip" // 替换原有的"triangle-list"
}

draw调用的顶点数量保持4不变(renderpass.draw(4, 1, 0, 0);),修改后即可渲染出完整矩形。

方案2:使用两个三角形的完整顶点数据

直接提供两个三角形的6个顶点(包含重复顶点),保持triangle-list拓扑:

修改rectangle_mesh.ts中的顶点数组:

const vertices: Float32Array = new Float32Array(
    [
        // 第一个三角形
        -0.5,      0.5,    1,  1,  1,
        0.5,       0.5,    0,  1,  0,
        0.5,       -0.5,   1,  0,  1,
        // 第二个三角形
        -0.5,      0.5,    1,  1,  1,
        0.5,       -0.5,   1,  0,  1,
        -0.5,      -0.5,   0,  0,  0,
    ]
);

然后修改main.ts中的draw调用,将顶点数量改为6:

renderpass.draw(6, 1, 0, 0);

方案3:使用索引缓冲区(推荐,减少数据冗余)

创建索引缓冲区存储三角形的顶点索引,用4个顶点+6个索引绘制两个三角形,既节省内存又灵活:

步骤1:修改rectangle_mesh.ts,添加索引缓冲区

export class RectangleMesh {
    public vertexBuffer: GPUBuffer;
    public indexBuffer: GPUBuffer;
    public bufferLayout: GPUVertexBufferLayout;
    public indexCount: number;

    constructor(public device: GPUDevice) {
        // 原始4个顶点
        const vertices: Float32Array = new Float32Array(
            [
                -0.5,      0.5,    1,  1,  1,
                0.5,       0.5,    0,  1,  0,
                0.5,       -0.5,   1,  0,  1,
                -0.5,      -0.5,   0,  0,  0,
            ]
        );

        // 索引:两个三角形,对应顶点索引0-1-2和0-2-3
        const indices: Uint16Array = new Uint16Array([0, 1, 2, 0, 2, 3]);
        this.indexCount = indices.length;

        // 创建顶点缓冲区(逻辑不变)
        const vertexUsage: GPUBufferUsageFlags = GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST;
        const vertexDescriptor: GPUBufferDescriptor = {
            size: vertices.byteLength,
            usage: vertexUsage,
            mappedAtCreation: true
        };
        this.vertexBuffer = device.createBuffer(vertexDescriptor);
        new Float32Array(this.vertexBuffer.getMappedRange()).set(vertices);
        this.vertexBuffer.unmap();

        // 创建索引缓冲区
        const indexUsage: GPUBufferUsageFlags = GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST;
        const indexDescriptor: GPUBufferDescriptor = {
            size: indices.byteLength,
            usage: indexUsage,
            mappedAtCreation: true
        };
        this.indexBuffer = device.createBuffer(indexDescriptor);
        new Uint16Array(this.indexBuffer.getMappedRange()).set(indices);
        this.indexBuffer.unmap();

        // 顶点布局保持不变
        this.bufferLayout = {
            arrayStride: 20,
            attributes: [
                {
                    shaderLocation: 0,
                    format: "float32x2",
                    offset: 0
                },
                {
                    shaderLocation: 1,
                    format: "float32x3",
                    offset: 8
                }
            ]
        }
    }
}

步骤2:修改main.ts中的绘制逻辑

替换原有的setVertexBuffer和draw调用:

renderpass.setVertexBuffer(0, rectangleMesh.vertexBuffer);
renderpass.setIndexBuffer(rectangleMesh.indexBuffer, "uint16"); // 设置索引缓冲区
renderpass.drawIndexed(rectangleMesh.indexCount, 1, 0, 0, 0); // 使用索引绘制

内容的提问来源于stack exchange,提问作者Jean-Paul Abi-Ghosn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 12:32:12