webgpu triangle

This commit is contained in:
Jorijn van der Graaf 2026-05-18 18:43:30 +02:00
commit 5553ded476
22 changed files with 2107 additions and 42 deletions

View file

@ -0,0 +1,12 @@
// WebGPU port of closesthit.glsl. Library concatenates this BEFORE the
// library helpers, so `Payload` declared here is visible to traceRay,
// runClosestHit, the mega-switch, and the user's raygen source.
struct Payload {
color: vec3<f32>,
};
fn closesthit_main(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) {
let bary = vec3<f32>(1.0 - hit.attribs.x - hit.attribs.y, hit.attribs.x, hit.attribs.y);
(*payload).color = bary;
}

View file

@ -1,4 +1,6 @@
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif
#include <cassert>
import Crafter.Graphics;
@ -7,7 +9,7 @@ import std;
import Crafter.Event;
import Crafter.Math;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
int main() {
Device::Initialize();
Window window(1280, 720, "HelloVulkan");
@ -89,7 +91,7 @@ int main() {
RenderingElement3D::elements.emplace_back(&renderer);
MatrixRowMajor<float, 4, 3, 1> transform = MatrixRowMajor<float, 4, 3, 1>::Identity();
std::memcpy(renderer.instance.transform.matrix, transform.m, sizeof(transform.m));
transform.Store(reinterpret_cast<float*>(renderer.instance.transform.matrix));
RenderingElement3D::BuildTLAS(cmd, 0);
RenderingElement3D::BuildTLAS(cmd, 1);
RenderingElement3D::BuildTLAS(cmd, 2);
@ -202,3 +204,65 @@ int main() {
window.Render();
window.StartSync();
}
#else
// DOM-mode port. Same scene (one triangle), software-emulated raytracing
// via compute. Shaders are read from .wgsl files shipped as static
// assets (see project.cpp). Renders barycentric colors via the
// hit/miss/raygen mega-switch in PipelineRTWebGPU.
int main() {
Device::Initialize();
static Window window(1280, 720, "HelloVulkan");
auto cmd = window.StartInit();
DescriptorHeapWebGPU heap;
heap.Initialize(/*images*/ 4, /*buffers*/ 4, /*samplers*/ 2);
std::array<WebGPUShader, 3> shaders {{
WebGPUShader(std::filesystem::path("raygen.wgsl"), "raygen_main", WebGPURTStage::Raygen),
WebGPUShader(std::filesystem::path("miss.wgsl"), "miss_main", WebGPURTStage::Miss),
WebGPUShader(std::filesystem::path("closesthit.wgsl"), "closesthit_main", WebGPURTStage::ClosestHit),
}};
ShaderBindingTableWebGPU sbt;
sbt.Init(shaders);
std::array<RTShaderGroup, 1> raygenGroups {{
{ .type = RTShaderGroupType::General, .generalShader = 0 },
}};
std::array<RTShaderGroup, 1> missGroups {{
{ .type = RTShaderGroupType::General, .generalShader = 1 },
}};
std::array<RTShaderGroup, 1> hitGroups {{
{ .type = RTShaderGroupType::TrianglesHitGroup, .closestHitShader = 2 },
}};
PipelineRTWebGPU pipeline;
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, sbt);
Mesh triangleMesh;
std::array<Vector<float, 3, 3>, 3> verts {{{-150, -150, 100}, {0, 150, 100}, {150, -150, 100}}};
std::array<std::uint32_t, 3> index {{2, 1, 0}};
triangleMesh.Build(verts, index, cmd);
static RenderingElement3D renderer;
renderer.instance.transform.matrix[0][0] = 1; renderer.instance.transform.matrix[0][1] = 0; renderer.instance.transform.matrix[0][2] = 0; renderer.instance.transform.matrix[0][3] = 0;
renderer.instance.transform.matrix[1][0] = 0; renderer.instance.transform.matrix[1][1] = 1; renderer.instance.transform.matrix[1][2] = 0; renderer.instance.transform.matrix[1][3] = 0;
renderer.instance.transform.matrix[2][0] = 0; renderer.instance.transform.matrix[2][1] = 0; renderer.instance.transform.matrix[2][2] = 1; renderer.instance.transform.matrix[2][3] = 0;
renderer.instance.instanceCustomIndex = 0;
renderer.instance.mask = 0xFF;
renderer.instance.instanceShaderBindingTableRecordOffset = 0;
renderer.instance.flags = kRTGeometryInstanceForceOpaque;
renderer.instance.accelerationStructureReference = triangleMesh.blasAddr;
RenderingElement3D::Add(&renderer);
RenderingElement3D::BuildTLAS(cmd, 0);
window.descriptorHeap = &heap;
window.FinishInit();
RTPass rtPass(&pipeline);
window.passes.push_back(&rtPass);
window.Render();
window.StartUpdate();
window.StartSync();
}
#endif

View file

@ -0,0 +1,5 @@
// WebGPU port of miss.glsl.
fn miss_main(ray: RayDesc, payload: ptr<function, Payload>) {
(*payload).color = vec3<f32>(1.0, 1.0, 1.0);
}

View file

@ -4,6 +4,14 @@ namespace fs = std::filesystem;
using namespace Crafter;
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
bool isWasm = false;
for (std::string_view a : args) {
if (a.starts_with("--target=") && a.find("wasm") != std::string_view::npos) {
isWasm = true;
break;
}
}
std::vector<std::string> graphicsArgs(args.begin(), args.end());
Configuration* graphics = LocalProject({
.projectFile = "../../project.cpp",
@ -14,6 +22,12 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.path = "./";
cfg.name = "VulkanTriangle";
cfg.outputName = "VulkanTriangle";
cfg.type = ConfigurationType::Executable;
if (isWasm) {
cfg.target = "wasm32-wasip1";
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_DOM", ""});
cfg.compileFlags.push_back("-msimd128");
}
ApplyStandardArgs(cfg, args);
cfg.dependencies = { graphics };
@ -21,8 +35,15 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
std::array<fs::path, 1> impls = { "main" };
cfg.GetInterfacesAndImplementations(ifaces, impls);
cfg.shaders.emplace_back(fs::path("raygen.glsl"), std::string("main"), ShaderType::RayGen);
cfg.shaders.emplace_back(fs::path("closesthit.glsl"), std::string("main"), ShaderType::ClosestHit);
cfg.shaders.emplace_back(fs::path("miss.glsl"), std::string("main"), ShaderType::Miss);
if (isWasm) {
cfg.files.emplace_back(fs::path("raygen.wgsl"));
cfg.files.emplace_back(fs::path("closesthit.wgsl"));
cfg.files.emplace_back(fs::path("miss.wgsl"));
EnableWasiBrowserRuntime(cfg);
} else {
cfg.shaders.emplace_back(fs::path("raygen.glsl"), std::string("main"), ShaderType::RayGen);
cfg.shaders.emplace_back(fs::path("closesthit.glsl"), std::string("main"), ShaderType::ClosestHit);
cfg.shaders.emplace_back(fs::path("miss.glsl"), std::string("main"), ShaderType::Miss);
}
return cfg;
}

View file

@ -34,17 +34,17 @@ void main() {
1.0
));
// traceRayEXT(
// topLevelAS[bufferStart],
// gl_RayFlagsNoneEXT,
// 0xff,
// 0, 0, 0,
// origin,
// 0.001,
// direction,
// 10000.0,
// 0
// );
traceRayEXT(
topLevelAS[bufferStart],
gl_RayFlagsNoneEXT,
0xff,
0, 0, 0,
origin,
0.001,
direction,
10000.0,
0
);
imageStore(image[0], ivec2(pixel), vec4(hitValue, 1));
}

View file

@ -0,0 +1,39 @@
// WebGPU port of raygen.glsl. Mirrors the pinhole camera setup the
// Payload type is declared in closesthit.wgsl (concatenated earlier).
fn raygen_main(gid: vec3<u32>) {
if (gid.x >= hdr.surfaceW || gid.y >= hdr.surfaceH) { return; }
let pixel = vec2<f32>(f32(gid.x), f32(gid.y));
let resolution = vec2<f32>(f32(hdr.surfaceW), f32(hdr.surfaceH));
let uv = (pixel + vec2<f32>(0.5)) / resolution;
let ndc = uv * 2.0 - vec2<f32>(1.0);
let origin = vec3<f32>(0.0, 0.0, -300.0);
let aspect = resolution.x / resolution.y;
let fov = 60.0 * 3.14159265 / 180.0;
let tanHalfFov = tan(fov * 0.5);
let direction = normalize(vec3<f32>(
ndc.x * aspect * tanHalfFov,
-ndc.y * tanHalfFov,
1.0,
));
var payload: Payload;
payload.color = vec3<f32>(0.0);
traceRay(
0u, // tlasIdx (unused)
0u, // ray flags
0xFFu, // cull mask
0u, 0u, 0u, // sbtRecordOffset, sbtRecordStride, missIndex
origin, 0.001,
direction, 10000.0,
&payload,
);
textureStore(outImage,
vec2<i32>(i32(gid.x), i32(gid.y)),
vec4<f32>(payload.color, 1.0));
}