Skip to content

Commit 89b79d9

Browse files
authored
Replace material/primitive index with generic meta::T field (#9)
* glass and cleanup * allow complex material lookup * fix tests + tutorials * remove display * try to reduce docs size * try to make plots smaller * fix bvh rendering
1 parent 343599a commit 89b79d9

14 files changed

+223
-183
lines changed

docs/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008"
1616

1717
[sources]
1818
BonitoBook = {url = "https://github.com/SimonDanisch/BonitoBook.jl"}
19-
Raycore = {path = "../"}
19+
Raycore = {path = ".."}
2020

2121
[compat]
2222
Documenter = "1.5"

docs/src/bvh_hit_tests_content.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ end
9191

9292
complex_bvh = Raycore.BVH(complex_spheres)
9393
# Test rays to find cases where any_hit differs from closest_hit
94-
test_rays = map(rand(Point2f, 200)) do p
94+
test_rays = map(rand(Point2f, 20)) do p
9595
p = (p .* 14f0) .- 8f0
9696
Raycore.Ray(o=Point3f(p..., -5), d=Vec3f(0, 0, 1))
9797
end
@@ -175,4 +175,3 @@ This document demonstrated:
175175
6. `any_hit` is typically faster than `closest_hit` due to early termination
176176

177177
All tests passed! ✓
178-

docs/src/gpu_raytracing_tutorial.md

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,7 @@ Let's use the exact same scene as the CPU tutorial - the Makie cat with room geo
3838
# Load and prepare the cat model
3939
include("raytracing-core.jl")
4040
bvh, ctx = example_scene()
41-
# We have a Makie extension for plotting the scene graph
42-
f, ax, pl = plot(bvh; axis=(; show_axis=false))
43-
f
44-
```
45-
```julia (editor=true, logging=false, output=true)
46-
cam = cameracontrols(ax.scene)
47-
cam.eyeposition[] = [0, 1.0, -4]
48-
cam.lookat[] = [0, 0, 2]
49-
cam.upvector[] = [0.0, 1, 0.0]
50-
cam.fov[] = 45.0
41+
md"**Scene loaded: $(length(bvh.primitives)) triangles, $(length(ctx.materials)) materials**"
5142
```
5243
## Part 2: GPU Kernel Version 1 - Basic Naive Approach
5344

docs/src/raytracing-core.jl

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,33 +47,12 @@ function Raycore.to_gpu(Arr, ctx::RenderContext)
4747
return RenderContext(to_gpu(Arr, ctx.lights), to_gpu(Arr, ctx.materials), ctx.ambient)
4848
end
4949

50-
function render_context(; glass_cat=false)
51-
# Create lights and materials
52-
lights = [
50+
function default_lights()
51+
return [
5352
PointLight(Point3f(3, 4, -2), 50.0f0, RGB(1.0f0, 0.9f0, 0.8f0)),
5453
PointLight(Point3f(-3, 2, 0), 20.0f0, RGB(0.7f0, 0.8f0, 1.0f0)),
5554
PointLight(Point3f(0, 5, 5), 15.0f0, RGB(1.0f0, 1.0f0, 1.0f0))
5655
]
57-
58-
# Material: base_color, metallic, roughness, ior, transmission
59-
cat_material = if glass_cat
60-
# Glass cat: clear glass with slight green tint, ior=1.5
61-
Material(RGB(0.95f0, 1.0f0, 0.95f0), 0.0f0, 0.0f0, 1.5f0, 1.0f0)
62-
else
63-
# Original diffuse cat
64-
Material(RGB(0.8f0, 0.6f0, 0.4f0), 0.0f0, 0.8f0, 1.0f0, 0.0f0)
65-
end
66-
67-
materials = [
68-
cat_material, # cat (index 1)
69-
Material(RGB(0.3f0, 0.5f0, 0.3f0), 0.0f0, 0.9f0, 1.0f0, 0.0f0), # floor - diffuse
70-
Material(RGB(0.8f0, 0.6f0, 0.5f0), 0.8f0, 0.05f0, 1.0f0, 0.0f0), # back wall - metallic
71-
Material(RGB(0.7f0, 0.7f0, 0.8f0), 0.0f0, 0.8f0, 1.0f0, 0.0f0), # left wall - diffuse
72-
Material(RGB(0.9f0, 0.9f0, 0.9f0), 0.8f0, 0.02f0, 1.0f0, 0.0f0), # sphere1 - metallic
73-
Material(RGB(0.3f0, 0.6f0, 0.9f0), 0.5f0, 0.3f0, 1.0f0, 0.0f0), # sphere2 - semi-metallic
74-
]
75-
76-
return RenderContext(lights, materials, 0.1f0)
7756
end
7857

7958
function compute_light(
@@ -151,7 +130,7 @@ end
151130
function reflective_kernel(bvh, ctx, tri, dist, bary, ray, sky_color, shadow_samples=8)
152131
hit_point = ray.o + ray.d * dist
153132
normal = compute_normal(tri, bary)
154-
mat = ctx.materials[tri.material_idx]
133+
mat = ctx.materials[tri.metadata]
155134

156135
# Direct lighting with soft shadows
157136
direct_color = compute_multi_light(bvh, ctx, hit_point, normal, mat, shadow_samples=shadow_samples)
@@ -174,7 +153,7 @@ function reflective_kernel(bvh, ctx, tri, dist, bary, ray, sky_color, shadow_sam
174153
reflection_color = if refl_hit
175154
refl_point = reflect_ray.o + reflect_ray.d * refl_dist
176155
refl_normal = compute_normal(refl_tri, refl_bary)
177-
refl_mat = ctx.materials[refl_tri.material_idx]
156+
refl_mat = ctx.materials[refl_tri.metadata]
178157
compute_multi_light(bvh, ctx, refl_point, refl_normal, refl_mat, shadow_samples=shadow_samples)
179158
else
180159
to_vec3f(sky_color)
@@ -211,9 +190,29 @@ function example_scene(; glass_cat=false)
211190
sphere1 = Tesselation(Sphere(Point3f(-2, -1.5 + 0.8, 2), 0.8f0), 64)
212191
sphere2 = Tesselation(Sphere(Point3f(2, -1.5 + 0.6, 1), 0.6f0), 64)
213192

214-
# Build our BVH acceleration structure
215-
scene_geometry = [cat_mesh, floor, back_wall, left_wall, sphere1, sphere2]
216-
return scene_geometry, render_context(glass_cat=glass_cat)
193+
# Material: base_color, metallic, roughness, ior, transmission
194+
cat_material = if glass_cat
195+
Material(RGB(0.95f0, 1.0f0, 0.95f0), 0.0f0, 0.0f0, 1.5f0, 1.0f0)
196+
else
197+
Material(RGB(0.8f0, 0.6f0, 0.4f0), 0.0f0, 0.8f0, 1.0f0, 0.0f0)
198+
end
199+
200+
# (geometry, material) pairs
201+
scene = [
202+
(cat_mesh, cat_material),
203+
(floor, Material(RGB(0.3f0, 0.5f0, 0.3f0), 0.0f0, 0.9f0, 1.0f0, 0.0f0)),
204+
(back_wall, Material(RGB(0.8f0, 0.6f0, 0.5f0), 0.8f0, 0.05f0, 1.0f0, 0.0f0)),
205+
(left_wall, Material(RGB(0.7f0, 0.7f0, 0.8f0), 0.0f0, 0.8f0, 1.0f0, 0.0f0)),
206+
(sphere1, Material(RGB(0.9f0, 0.9f0, 0.9f0), 0.8f0, 0.02f0, 1.0f0, 0.0f0)),
207+
(sphere2, Material(RGB(0.3f0, 0.6f0, 0.9f0), 0.5f0, 0.3f0, 1.0f0, 0.0f0)),
208+
]
209+
210+
geometries = [g for (g, _) in scene]
211+
materials = [m for (_, m) in scene]
212+
bvh = Raycore.BVH(geometries, (mesh_idx, tri_idx) -> UInt32(mesh_idx))
213+
lights = default_lights()
214+
ctx = RenderContext(lights, materials, 0.1f0)
215+
return bvh, ctx
217216
end
218217

219218
function example_scene_glass_cat()

docs/src/raytracing_tutorial_content.md

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,7 @@ sphere2 = Tesselation(Sphere(Point3f(2, -1.5 + 0.6, 1), 0.6f0), 64)
5050
# Build our BVH acceleration structure
5151
scene_geometry = [cat_mesh, floor, back_wall, left_wall, sphere1, sphere2]
5252
bvh = Raycore.BVH(scene_geometry)
53-
f, ax, pl = plot(bvh; axis=(; show_axis=false))
54-
```
55-
Set the camera to something better:
56-
57-
```julia (editor=true, logging=false, output=true)
58-
cam = cameracontrols(ax.scene)
59-
cam.eyeposition[] = [0, 1.0, -4]
60-
cam.lookat[] = [0, 0, 2]
61-
cam.upvector[] = [0.0, 1, 0.0]
62-
cam.fov[] = 45.0
63-
update_cam!(ax.scene, cam)
64-
nothing
53+
md"**BVH built with $(length(bvh.primitives)) triangles**"
6554
```
6655
## Part 2: Helper Functions - Building Blocks
6756

@@ -212,7 +201,11 @@ trace((args...)-> shadow_kernel(args...; shadow_samples=8), bvh, samples=8)
212201

213202
## Part 7: Materials and Multiple Lights
214203

215-
Time to add color and multiple lights:
204+
Time to add color and multiple lights! To associate materials with geometry, we need to rebuild the BVH with **metadata** that links each triangle to its material.
205+
206+
### Triangle Metadata
207+
208+
When building a BVH, you can pass a `metadata_fn(mesh_idx, tri_idx)` that assigns custom data to each triangle. This metadata is stored in `triangle.metadata` and returned with every ray hit. For materials, we use the mesh index as metadata:
216209

217210
```julia (editor=true, logging=false, output=true)
218211
struct PointLight
@@ -233,25 +226,36 @@ struct RenderContext
233226
ambient::Float32
234227
end
235228

236-
# Create lights and materials
229+
# Define materials for each mesh (order matches scene_geometry)
230+
materials = [
231+
Material(RGB(0.8f0, 0.6f0, 0.4f0), 0.0f0, 0.8f0), # 1: cat
232+
Material(RGB(0.3f0, 0.5f0, 0.3f0), 0.0f0, 0.9f0), # 2: floor
233+
Material(RGB(0.8f0, 0.6f0, 0.5f0), 0.8f0, 0.05f0), # 3: back wall
234+
Material(RGB(0.7f0, 0.7f0, 0.8f0), 0.0f0, 0.8f0), # 4: left wall
235+
Material(RGB(0.9f0, 0.9f0, 0.9f0), 0.8f0, 0.02f0), # 5: sphere1 - metallic
236+
Material(RGB(0.3f0, 0.6f0, 0.9f0), 0.5f0, 0.3f0), # 6: sphere2 - semi-metallic
237+
]
238+
239+
# Rebuild BVH with material indices as metadata
240+
# The metadata_fn receives (mesh_idx, tri_idx) and returns data stored per-triangle
241+
bvh = Raycore.BVH(scene_geometry, (mesh_idx, tri_idx) -> UInt32(mesh_idx))
242+
243+
# Create lights
237244
lights = [
238245
PointLight(Point3f(3, 4, -2), 50.0f0, RGB(1.0f0, 0.9f0, 0.8f0)),
239246
PointLight(Point3f(-3, 2, 0), 20.0f0, RGB(0.7f0, 0.8f0, 1.0f0)),
240247
PointLight(Point3f(0, 5, 5), 15.0f0, RGB(1.0f0, 1.0f0, 1.0f0))
241248
]
242249

243-
materials = [
244-
Material(RGB(0.8f0, 0.6f0, 0.4f0), 0.0f0, 0.8f0), # cat
245-
Material(RGB(0.3f0, 0.5f0, 0.3f0), 0.0f0, 0.9f0), # floor
246-
Material(RGB(0.8f0, 0.6f0, 0.5f0), 0.8f0, 0.05f0), # back wall
247-
Material(RGB(0.7f0, 0.7f0, 0.8f0), 0.0f0, 0.8f0), # left wall
248-
Material(RGB(0.9f0, 0.9f0, 0.9f0), 0.8f0, 0.02f0), # sphere1 - metallic
249-
Material(RGB(0.3f0, 0.6f0, 0.9f0), 0.5f0, 0.3f0), # sphere2 - semi-metallic
250-
]
251-
252250
ctx = RenderContext(lights, materials, 0.1f0)
253251
nothing
254252
```
253+
Now when a ray hits a triangle, we can look up its material using `triangle.metadata`:
254+
255+
```julia
256+
mat = ctx.materials[triangle.metadata] # Get material for hit triangle
257+
```
258+
255259
```julia (editor=true, logging=false, output=true)
256260
# Compute lighting from all lights - reusing our compute_light function!
257261
function compute_multi_light(bvh, ctx, point, normal, mat; shadow_samples=1)
@@ -269,7 +273,8 @@ end
269273
function material_kernel(bvh, ctx, tri, dist, bary, ray)
270274
hit_point = ray.o + ray.d * dist
271275
normal = compute_normal(tri, bary)
272-
mat = ctx.materials[tri.material_idx]
276+
# Look up material using triangle's metadata (mesh index we stored during BVH construction)
277+
mat = ctx.materials[tri.metadata]
273278

274279
color = compute_multi_light(bvh, ctx, hit_point, normal, mat, shadow_samples=2)
275280
return to_rgb(color)
@@ -287,7 +292,7 @@ Add simple reflections for metallic surfaces:
287292
function reflective_kernel(bvh, ctx, tri, dist, bary, ray, sky_color)
288293
hit_point = ray.o + ray.d * dist
289294
normal = compute_normal(tri, bary)
290-
mat = ctx.materials[tri.material_idx]
295+
mat = ctx.materials[tri.metadata]
291296

292297
# Direct lighting with soft shadows
293298
direct_color = compute_multi_light(bvh, ctx, hit_point, normal, mat, shadow_samples=8)
@@ -310,7 +315,7 @@ function reflective_kernel(bvh, ctx, tri, dist, bary, ray, sky_color)
310315
reflection_color = if refl_hit
311316
refl_point = reflect_ray.o + reflect_ray.d * refl_dist
312317
refl_normal = compute_normal(refl_tri, refl_bary)
313-
refl_mat = ctx.materials[refl_tri.material_idx]
318+
refl_mat = ctx.materials[refl_tri.metadata]
314319
compute_multi_light(bvh, ctx, refl_point, refl_normal, refl_mat, shadow_samples=1)
315320
else
316321
to_vec3f(sky_color)
@@ -387,16 +392,30 @@ We built a complete ray tracer with:
387392

388393
**Key Raycore Functions:**
389394

390-
* `Raycore.BVH(meshes)` - Build acceleration structure
395+
* `Raycore.BVH(meshes)` - Build acceleration structure (default metadata = primitive index)
396+
* `Raycore.BVH(meshes, metadata_fn)` - Build with custom per-triangle metadata
391397
* `Raycore.Ray(o=origin, d=direction)` - Create ray
392-
* `Raycore.closest_hit(bvh, ray)` - Find nearest intersection
393-
* `Raycore.any_hit(bvh, ray)` - Test for any intersection
398+
* `Raycore.closest_hit(bvh, ray)` - Find nearest intersection, returns `(hit, triangle, distance, bary_coords)`
399+
* `Raycore.any_hit(bvh, ray)` - Test for any intersection (fast shadow test)
394400
* `Raycore.reflect(wo, normal)` - Compute reflection direction
401+
* `triangle.metadata` - Access custom data stored per-triangle
402+
403+
**Key Patterns:**
404+
405+
1. **Material Scene Pattern** - Associate materials with geometry using metadata:
406+
407+
```julia
408+
# Build BVH with mesh index as metadata
409+
bvh = Raycore.BVH(meshes, (mesh_idx, tri_idx) -> UInt32(mesh_idx))
410+
411+
# In your shader, look up material from hit triangle
412+
mat = materials[triangle.metadata]
413+
```
395414

396-
**Key Pattern:** The `compute_light` function is reusable across the entire tutorial:
415+
2. **Reusable Lighting** - The `compute_light` function handles both hard and soft shadows:
397416

398417
* `shadow_samples=1` → hard shadows
399-
* `shadow_samples=4` → soft shadows
418+
* `shadow_samples>1` → soft shadows
400419

401-
This shows how a well-designed function can handle multiple use cases cleanly!
420+
This shows how well-designed functions can handle multiple use cases cleanly!
402421

docs/src/test_wavefront.jl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ using BenchmarkTools
1414
include("raytracing-core.jl")
1515
include("wavefront-renderer.jl")
1616

17-
geom, ctx = example_scene()
18-
bvh = BVH(geom)
17+
bvh, ctx = example_scene()
1918
# ibvh = Raycore.InstancedBVH(geom)
2019
begin
2120
img = fill(RGBf(0, 0, 0), 400, 720)
@@ -28,13 +27,16 @@ renderer_instanced.framebuffer
2827
begin
2928
img = fill(RGBf(0, 0, 0), 400, 720)
3029
renderer_instanced = WavefrontRenderer(
31-
img, ibvh, ctx;
30+
img, bvh, ctx;
3231
camera_pos=Point3f(0, -0.9, -2.5),
3332
fov=45.0f0,
3433
sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0),
3534
samples_per_pixel=4
3635
)
3736
@btime render!(renderer_instanced)
37+
# on windows + ryzen 395 max
38+
# 381.034 ms (1200456 allocations: 90.13 MiB)
39+
3840
nothing
3941
end
4042
using ImageShow
@@ -45,6 +47,7 @@ save("wavefront.png", map(col -> mapc(c -> clamp(c, 0f0, 1f0), col), renderer.fr
4547
using AMDGPU
4648
amd_renderer = to_gpu(ROCArray, renderer);
4749
Array(@btime render!(amd_renderer))
50+
# 36ms on windows + amd 8060s
4851

4952
using pocl_jll, OpenCL
5053
amd_renderer = to_gpu(CLArray, renderer);

docs/src/viewfactors_content.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,22 @@ This example demonstrates Raycore's analysis capabilities for radiosity and illu
66

77
```julia (editor=true, logging=false, output=true)
88
using Raycore, GeometryBasics, LinearAlgebra
9-
using WGLMakie, FileIO
9+
using WGLMakie
1010

11-
function LowSphere(radius, contact=Point3f(0); ntriangles=10)
11+
function LowSphere(radius, contact=Point3f(0); ntriangles=6)
1212
return Tesselation(Sphere(contact .+ Point3f(0, 0, radius), radius), ntriangles)
1313
end
1414

15-
# Create scene with multiple objects
16-
ntriangles = 10
15+
# Create scene with multiple objects (using low triangle counts to keep docs small)
16+
ntriangles = 6
1717
s1 = LowSphere(0.5f0, Point3f(-0.5, 0.0, 0); ntriangles)
1818
s2 = LowSphere(0.3f0, Point3f(1, 0.5, 0); ntriangles)
1919
s3 = LowSphere(0.3f0, Point3f(-0.5, 1, 0); ntriangles)
2020
s4 = LowSphere(0.4f0, Point3f(0, 1.0, 0); ntriangles)
21-
cat = load(Makie.assetpath("cat.obj"))
21+
s5 = LowSphere(0.35f0, Point3f(0.5, 0.0, 0); ntriangles)
2222

2323
# Build BVH acceleration structure
24-
bvh = BVH([s1, s2, s3, s4, cat])
24+
bvh = BVH([s1, s2, s3, s4, s5])
2525
world_mesh = GeometryBasics.Mesh(bvh)
2626

2727
# Visualize the scene
@@ -35,7 +35,7 @@ View factors quantify how much each surface "sees" every other surface - essenti
3535

3636
```julia (editor=true, logging=false, output=true)
3737
# Calculate view factors between all faces
38-
viewf_matrix = view_factors(bvh, rays_per_triangle=1000)
38+
viewf_matrix = view_factors(bvh, rays_per_triangle=20)
3939

4040
# Sum up total view factor per face
4141
viewfacts = map(i -> Float32(sum(view(viewf_matrix, :, i))), 1:length(bvh.primitives))
@@ -59,7 +59,7 @@ Calculate how much each face is exposed to rays from a specific viewing directio
5959
viewdir = normalize(ax.scene.camera.view_direction[])
6060

6161
# Compute illumination
62-
illum = get_illumination(bvh, viewdir)
62+
illum = get_illumination(bvh, viewdir; grid_size=10)
6363

6464
# Visualize
6565
pf = FaceView(illum, [GLTriangleFace(i) for i in 1:N])
@@ -75,7 +75,7 @@ Find the average position of visible surface points from a given direction.
7575

7676
```julia (editor=true, logging=false, output=true)
7777
# Calculate centroid
78-
hitpoints, centroid = get_centroid(bvh, viewdir)
78+
hitpoints, centroid = get_centroid(bvh, viewdir; grid_size=10)
7979

8080
# Visualize
8181
f, ax, pl = mesh(world_mesh, color=(:blue, 0.5), transparency=true, axis=(show_axis=false,))
@@ -86,4 +86,3 @@ meshscatter!(ax, [centroid], color=:red, markersize=0.05)
8686
f
8787
```
8888
The red sphere marks the centroid - useful for camera placement and focus calculations.
89-

0 commit comments

Comments
 (0)