うねる円周上に並ぶテクスチャ(アニメーション)
import { createSphereInstanceBuffer, createSquareBuffer } from "./buffer"import { createWebGPUTextureFromImageUrl } from "./image"
import shaderCode from "./shader.wgsl?raw"import imageUrl from "../assets/sakura.png?url"
const adapter = await navigator.gpu.requestAdapter()if (!adapter) { throw new Error("WebGPU cannot be initialized - Adapter not found")}
const device = await adapter.requestDevice()device.lost.then(() => { throw new Error("WebGPU cannot be initialized - Device has been lost")})
const canvas = document.querySelector("canvas")const context = canvas!.getContext("webgpu")if (!context) { throw new Error("WebGPU cannot be initialized - Canvas does not support WebGPU")}
const canvasFormat = navigator.gpu.getPreferredCanvasFormat()context.configure({ device, format: canvasFormat })
const { vertexBuffer, indexBuffer, indexCount } = createSquareBuffer(device, 0.1)
const instanceCount = 70const instanceBuffer = createSphereInstanceBuffer(device, instanceCount)
const texture = await createWebGPUTextureFromImageUrl(device, imageUrl)const sampler = device.createSampler({ magFilter: "linear", minFilter: "linear"})
const uTime = 0.0const frameUniform = new Float32Array([uTime])const frameUniformBuffer = device.createBuffer({ size: frameUniform.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST})device.queue.writeBuffer(frameUniformBuffer, 0, frameUniform)
const shaderModule = device.createShaderModule({ code: shaderCode })
const renderPipeline = device.createRenderPipeline({ layout: "auto", vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [ { arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT, attributes: [ { shaderLocation: 0, offset: 0, format: "float32x2" }, { shaderLocation: 1, offset: 2 * Float32Array.BYTES_PER_ELEMENT, format: "float32x2" } ], stepMode: "vertex" }, { arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, attributes: [ { shaderLocation: 2, offset: 0, format: "float32x2" } ], stepMode: "instance" } ] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [ { format: canvasFormat, blend: { color: { srcFactor: "one", // RGBはそのまま dstFactor: "one-minus-src-alpha", // 背景に1-αを掛ける operation: "add" }, alpha: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" } } } ] }})
const uniformBindGroup = device.createBindGroup({ layout: renderPipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: texture.createView() }, { binding: 1, resource: sampler } ]})
const frameUniformBindGroup = device.createBindGroup({ layout: renderPipeline.getBindGroupLayout(1), entries: [ { binding: 0, resource: { buffer: frameUniformBuffer, offset: 0, size: frameUniform.byteLength } } ]})
const startTime = new Date().getTime()
const frame = () => { const elapsed = new Date().getTime() - startTime
const uTime = elapsed * 0.001 // 秒に変換 const frameUniform = new Float32Array([uTime]) device.queue.writeBuffer(frameUniformBuffer, 0, frameUniform)
const commandEncoder = device.createCommandEncoder()
const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [ { view: context.getCurrentTexture().createView(), loadOp: "clear", clearValue: { r: 0.824, g: 0.878, b: 0.984, a: 1 }, storeOp: "store" } ] }) renderPass.setPipeline(renderPipeline) renderPass.setBindGroup(0, uniformBindGroup) renderPass.setBindGroup(1, frameUniformBindGroup) renderPass.setVertexBuffer(0, vertexBuffer) renderPass.setVertexBuffer(1, instanceBuffer) renderPass.setIndexBuffer(indexBuffer, "uint16") renderPass.drawIndexed(indexCount, instanceCount) renderPass.end()
device.queue.submit([commandEncoder.finish()])
requestAnimationFrame(frame)}
frame()
export const createWebGPUTextureFromImageUrl = async (device: GPUDevice, url: string) => { const response = await fetch(url) const blob = await response.blob() const imgBitmap = await createImageBitmap(blob)
const textureDescriptor: GPUTextureDescriptor = { size: { width: imgBitmap.width, height: imgBitmap.height }, format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT } const texture = device.createTexture(textureDescriptor) device.queue.copyExternalImageToTexture({ source: imgBitmap }, { texture }, textureDescriptor.size)
return texture}
export const createSquareBuffer = (device: GPUDevice, size: number) => { const half = size * 0.5
// prettier-ignore const vertices = new Float32Array([ // x, y, u, v -half, half, 0.0, 1.0, // 0: 左上 half, half, 1.0, 1.0, // 1: 右上 half, -half, 1.0, 0.0, // 2: 右下 -half, -half, 0.0, 0.0 // 3: 左下 ])
const vertexBuffer = device.createBuffer({ size: vertices.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST }) device.queue.writeBuffer(vertexBuffer, 0, vertices)
// prettier-ignore const indices = new Uint16Array([ 0, 3, 2, // 第1三角形(左上 → 左下 → 右下) 0, 2, 1 // 第2三角形(左上 → 右下 → 右上) ]);
const indexBuffer = device.createBuffer({ size: indices.byteLength, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST }) device.queue.writeBuffer(indexBuffer, 0, indices)
return { vertexBuffer, indexBuffer, indexCount: indices.length }}
export const createSphereInstanceBuffer = (device: GPUDevice, count: number) => { const instanceBuffer = device.createBuffer({ size: count * Float32Array.BYTES_PER_ELEMENT * 2, usage: GPUBufferUsage.VERTEX, mappedAtCreation: true })
const instanceOffsets = new Float32Array(instanceBuffer.getMappedRange())
for (let i = 0; i < count; i++) { const theta = (Math.PI * 2 * i) / count instanceOffsets[i * 2 + 0] = Math.cos(theta) instanceOffsets[i * 2 + 1] = Math.sin(theta) }
instanceBuffer.unmap()
return instanceBuffer}
@group(0) @binding(0) var image_texture: texture_2d<f32>;@group(0) @binding(1) var image_sampler: sampler;
@group(1) @binding(0) var<uniform> time: f32;
struct VertexInput { @builtin(instance_index) InstanceIndex: u32, @location(0) position: vec3f, @location(1) uv: vec2f, @location(2) offset: vec2f,};
struct VertexOutput { @builtin(position) Position: vec4f, @location(0) uv: vec2f,};
const PI: f32 = 3.1415926;
@vertexfn vs_main(in: VertexInput) -> VertexOutput { let theta = f32(in.InstanceIndex) * PI * 2.0; let instance_radius = sin(theta * 0.9 + time) + 1.4; let pos = in.position.xy * instance_radius + in.offset * 0.7;
var out: VertexOutput; out.Position = vec4f(pos, 0.0, 1.0); out.uv = in.uv; return out;}
@fragmentfn fs_main(in: VertexOutput) -> @location(0) vec4f { return textureSample(image_texture, image_sampler, in.uv);}