Compare commits

..

3 Commits

Author SHA1 Message Date
SpecialX
bd6e0f4771 优化顶点数据布局:从多数组改为单数组 2025-11-19 11:49:41 +08:00
SpecialX
14c01ea55a 添加缓冲区工具函数和优化几何体处理 2025-11-19 11:35:51 +08:00
SpecialX
7d5265bced 添加WebGPU游戏基本功能:几何体、纹理和着色器支持 2025-11-19 11:15:29 +08:00
7 changed files with 192 additions and 61 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

30
src/buffer-util.ts Normal file
View File

@@ -0,0 +1,30 @@
export class BufferUtil {
public static createVertexBuffer(device: GPUDevice, data: Float32Array ): GPUBuffer {
const buffer = device.createBuffer({
size: data.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX,
mappedAtCreation: true,
});
new Float32Array(buffer.getMappedRange()).set(data);
buffer.unmap();
return buffer;
}
public static createIndexBuffer(device: GPUDevice, data: Uint16Array): GPUBuffer {
const buffer = device.createBuffer({
size: data.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.INDEX,
mappedAtCreation: true,
});
new Uint16Array(buffer.getMappedRange()).set(data);
buffer.unmap();
return buffer;
}
}

18
src/geometry.ts Normal file
View File

@@ -0,0 +1,18 @@
export class QuadGeometry {
public vertices: number[];
public indices: number[];
constructor() {
this.vertices = [
-0.5, -0.5, 0.0, 1.0, 1.0, 0.0, 0.0,
0.5, -0.5, 1.0, 1.0, 0.0, 1.0, 0.0,
-0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 1.0,
0.5, 0.5, 1.0, 0.0, 1.0, 1.0, 1.0
]
this.indices = [
0, 1, 2,
1, 2, 3,
]
}
}

View File

@@ -1,11 +1,16 @@
import shaderSource from './shaders/shader.wgsl?raw';
import { QuadGeometry } from './geometry';
import { Texture } from './texture';
import { BufferUtil } from './buffer-util';
class Renderer{
private device! : GPUDevice;
private context! : GPUCanvasContext;
private pipeline! : GPURenderPipeline;
private postitionBuffer! : GPUBuffer;
private colorBuffer! : GPUBuffer;
private verticesBuffer! : GPUBuffer;
private indexBuffer! : GPUBuffer;
private textureBindGroup! : GPUBindGroup;
private testTexture! : Texture;
public async initialize()
{
@@ -37,77 +42,52 @@ class Renderer{
format: navigator.gpu.getPreferredCanvasFormat()
})
this.testTexture = await Texture.createTextureFromUrl(this.device, 'assets/UVEditorColorGrid.png');
this.prepareModel();
this.postitionBuffer = this.CreateBuffer(new Float32Array([
-0.5, -0.5,
0.5, -0.5,
-0.5, 0.5,
-0.5, 0.5,
0.5, 0.5,
0.5, -0.5
]));
this.colorBuffer = this.CreateBuffer(new Float32Array([
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0
]));
const geometry = new QuadGeometry();
this.verticesBuffer = BufferUtil.createVertexBuffer(this.device,new Float32Array(geometry.vertices));
this.indexBuffer = BufferUtil.createIndexBuffer(this.device, new Uint16Array(geometry.indices));
}
private CreateBuffer(data: Float32Array)
{
const buffer = this.device.createBuffer({
size: data.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
});
new Float32Array(buffer.getMappedRange()).set(data);
buffer.unmap();
return buffer;
}
private prepareModel() {
const shaderModule = this.device.createShaderModule({code: shaderSource});
const postitionBufferLayout : GPUVertexBufferLayout =
const BufferLayout : GPUVertexBufferLayout =
{
arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT,
arrayStride: 7 * Float32Array.BYTES_PER_ELEMENT,
attributes: [
{
shaderLocation: 0,
offset: 0,
format: 'float32x2'
}
],
stepMode: 'vertex'
}
const colorBufferLayout : GPUVertexBufferLayout =
{
arrayStride: 3 * Float32Array.BYTES_PER_ELEMENT,
attributes: [
} ,
{
shaderLocation: 1,
offset: 0,
offset: 2 * Float32Array.BYTES_PER_ELEMENT,
format: 'float32x2'
},
{
shaderLocation: 2,
offset: 4 * Float32Array.BYTES_PER_ELEMENT,
format: 'float32x3'
}]
}
],
stepMode: 'vertex'
}
const vertexState: GPUVertexState = {
module : shaderModule,
entryPoint: "VertexMain",
buffers:
[postitionBufferLayout,
colorBufferLayout]
[
BufferLayout
]
}
const fragmentState: GPUFragmentState = {
@@ -115,18 +95,65 @@ class Renderer{
entryPoint: "FragmentMain",
targets: [
{
format: navigator.gpu.getPreferredCanvasFormat()
format: navigator.gpu.getPreferredCanvasFormat(),
blend: {
color: {
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
operation: 'add'
},
alpha: {
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
operation: 'add'
}
}
}
]
}
const textureBindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
sampler: {}
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
texture: {}
}
]
});
const pipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [
textureBindGroupLayout
]
});
this.textureBindGroup = this.device.createBindGroup({
layout: textureBindGroupLayout,
entries: [
{
binding: 0,
resource: this.testTexture.sampler
},
{
binding: 1,
resource: this.testTexture.texture.createView()
}
]
});
this.pipeline = this.device.createRenderPipeline({
vertex: vertexState,
fragment: fragmentState,
primitive:{
topology: 'triangle-list',
},
layout: "auto"
layout: pipelineLayout
});
@@ -152,12 +179,14 @@ class Renderer{
//Draw here
passEncoder.setPipeline(this.pipeline );
passEncoder.setVertexBuffer(0, this.postitionBuffer);
passEncoder.setVertexBuffer(1, this.colorBuffer);
passEncoder.draw(6);
passEncoder.setIndexBuffer(this.indexBuffer, 'uint16');
passEncoder.setVertexBuffer(0, this.verticesBuffer);
passEncoder.setBindGroup(0, this.textureBindGroup)
passEncoder.drawIndexed(6, 1, 0, 0, 0);
passEncoder.end();
this.device.queue.submit([commandEncoder.finish()]);
}

View File

@@ -1,24 +1,33 @@
struct VertexOut {
@builtin(position) pos: vec4<f32>,
@location(0) color: vec4<f32>,
@location(0) texcoord: vec2<f32>,
@location(1) color: vec4<f32>,
}
@vertex
fn VertexMain(
@location(0) pos: vec2<f32>,
@location(1) color: vec3<f32>,
@builtin(vertex_index) vertexIndex: u32,
@location(1) texcoord: vec2<f32>,
@location(2) color: vec3<f32>,
) -> VertexOut
{
var output : VertexOut;
output.pos = vec4<f32>(pos, 0.0, 1.0);
output.color = vec4<f32>(color, 1.0);
output.texcoord = vec2<f32>(texcoord);
return output;
}
@group(0) @binding(0)
var texSampler : sampler;
@group(0) @binding(1)
var tex : texture_2d<f32>;
@fragment
fn FragmentMain(fragData: VertexOut) -> @location(0) vec4<f32>
{
return fragData.color;
var texColor = textureSample(tex, texSampler, fragData.texcoord);
return fragData.color * texColor;
}

45
src/texture.ts Normal file
View File

@@ -0,0 +1,45 @@
export class Texture{
constructor(public texture: GPUTexture, public sampler: GPUSampler){}
public static async createTexture(device: GPUDevice, image: HTMLImageElement)
: Promise<Texture>
{
const texture = device.createTexture({
size: { width: image.width, height: image.height},
format: "rgba8unorm",
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
});
const data = await createImageBitmap(image );
device.queue.copyExternalImageToTexture(
{ source: data },
{ texture: texture },
{ width: image.width, height: image.height }
);
const sampler = device.createSampler({
magFilter: "linear",
minFilter: "linear",
});
return new Texture(texture, sampler);
}
public static async createTextureFromUrl(device: GPUDevice, url: string): Promise<Texture>
{
const imagePromise = new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
img.src = url;
img.onload = () => resolve(img);
img.onerror = () => {
console.error('Failed to load image from url: ' + url);
reject();
}
});
const image = await imagePromise;
return await Texture.createTexture(device, image);
}
}

View File

@@ -18,7 +18,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},