2020 Graphics & Games
WWDC20 · 21 min · Graphics & Games
Discover ray tracing with Metal
Achieve photorealistic 3D scenes in your apps and games through ray tracing — a core part of the Metal graphics framework and Shading Language. Discover the fundamentals of the Metal ray tracing API and Shading Language extensions for ray tracing, find out how to use them in your graphics apps and games, and learn how to take control of your kernels and combine them into a single compute kernel for optimal performance.
Watch at developer.apple.com ↗Code shown on screen · 17 snippets
Ray tracing with Metal
[[kernel]]
void rtKernel(primitive_acceleration_structure accelerationStructure [[buffer(0)]],
/* ... */)
{
// Generate ray
ray r = generateCameraRay(tid);
// Create an intersector
intersector<triangle_data> intersector;
// Intersect with scene
intersection_result<triangle_data> intersection;
intersection = intersector.intersect(r, accelerationStructure);
// shading...
} Create an acceleration structure descriptor
let accelerationStructureDescriptor = MTLPrimitiveAccelerationStructureDescriptor()
// Create geometry descriptor(s)
let geometryDescriptor = MTLAccelerationStructureTriangleGeometryDescriptor()
geometryDescriptor.vertexBuffer = vertexBuffer
geometryDescriptor.triangleCount = triangleCount
accelerationStructureDescriptor.geometryDescriptors = [ geometryDescriptor ] Allocate acceleration storage
// Query for acceleration structure sizes
let sizes = device.accelerationStructureSizes(descriptor: accelerationStructureDescriptor)
// Allocate acceleration structure
let accelerationStructure =
device.makeAccelerationStructure(size: sizes.accelerationStructureSize)!
// Allocate scratch buffer
let scratchBuffer = device.makeBuffer(length: sizes.buildScratchBufferSize,
options: .storageModePrivate)! Build acceleration structure
// Create command buffer/encoder
let commandBuffer = commandQueue.makeCommandBuffer()!
let commandEncoder = commandBuffer.makeAccelerationStructureCommandEncoder()!
// Encode acceleration structure build
commandEncoder.build(accelerationStructure: accelerationStructure,
descriptor: accelerationStructureDescriptor,
scratchBuffer: scratchBuffer,
scratchBufferOffset: 0)
// Commit command buffer
commandEncoder.endEncoding()
commandBuffer.commit() Pass acceleration structure to ray intersector
[[kernel]]
void rtKernel(primitive_acceleration_structure accelerationStructure [[buffer(0)]],
/* ... */)
{
// generate ray, create intersector...
intersection = intersector.intersect(r, accelerationStructure);
// shading...
} Bind acceleration structure with compute command encoder
computeEncoder.setAccelerationStructure(accelerationStructure, bufferIndex: 0) Triangle intersection functions
[[intersection(triangle, triangle_data)]]
bool alphaTestIntersectionFunction(uint primitiveIndex [[primitive_id]],
uint geometryIndex [[geometry_id]],
float2 barycentricCoords [[barycentric_coord]],
device Material *materials [[buffer(0)]])
{
texture2d<float> alphaTexture = materials[geometryIndex].alphaTexture;
float2 UV = interpolateUVs(materials[geometryIndex].UVs,
primitiveIndex, barycentricCoords);
float alpha = alphaTexture.sample(sampler, UV).x;
return alpha > 0.5f;
} Creating a bounding box acceleration structure
// Create a primitive acceleration structure descriptor
let accelerationStructureDescriptor = MTLPrimitiveAccelerationStructureDescriptor()
// Create one or more bounding box geometry descriptors:
let geometryDescriptor = MTLAccelerationStructureBoundingBoxGeometryDescriptor()
geometryDescriptor.boundingBoxBuffer = boundingBoxBuffer
geometryDescriptor.boundingBoxCount = boundingBoxCount
accelerationStructureDescriptor.geometryDescriptors = [ geometryDescriptor ] Bounding Box Result
struct BoundingBoxResult {
bool accept [[accept_intersection]];
float distance [[distance]];
}; Bounding box intersection functions
[[intersection(bounding_box)]]
BoundingBoxResult sphereIntersectionFunction(float3 origin [[origin]],
float3 direction [[direction]],
float minDistance [[min_distance]],
float maxDistance [[max_distance]],
uint primitiveIndex [[primitive_id]],
device Sphere *spheres [[buffer(0)]])
{
float distance;
if (!intersectRaySphere(origin, direction, spheres[primitiveIndex], &distance))
return { false, 0.0f };
if (distance < minDistance || distance > maxDistance)
return { false, 0.0f };
return { true, distance };
} Ray payload
[[intersection(bounding_box)]]
BoundingBoxResult sphereIntersectionFunction(/* ... */,
ray_data float3 & normal [[payload]])
{
// ...
if (distance < minDistance || distance > maxDistance)
return { false, 0.0f };
float3 intersectionPoint = origin + direction * distance;
normal = normalize(intersectionPoint - spheres[primitiveIndex].origin);
return { true, distance };
} Ray payload 2
[[kernel]]
void rtKernel(/* ... */)
{
// generate ray, create intersector...
float3 normal;
intersection = intersector.intersect(r, accelerationStructure, functionTable, normal);
// shading...
} Linking intersection functions
// Load functions from Metal library
let sphereIntersectionFunction = library.makeFunction(name: “sphereIntersectionFunction”)!
// other functions...
// Attach functions to ray tracing compute pipeline descriptor
let linkedFunctions = MTLLinkedFunctions()
linkedFunctions.functions = [ sphereIntersectionFunction, alphaTestFunction, ... ]
computePipelineDescriptor.linkedFunctions = linkedFunctions
// Compile and link ray tracing compute pipeline
let computePipeline = try device.makeComputePipeline(descriptor: computePipelineDescriptor,
options: [],
reflection: nil) Intersection function table offsets
class MTLAccelerationStructureGeometryDescriptor : NSObject {
var intersectionFunctionTableOffset: Int
// ...
}
struct MTLAccelerationStructureInstanceDescriptor {
var intersectionFunctionTableOffset: UInt32
// ...
}; Creating an intersection function table
// Allocate intersection function table
let descriptor = MTLIntersectionFunctionTableDescriptor()
descriptor.functionCount = intersectionFunctions.count
let functionTable = computePipeline.makeIntersectionFunctionTable(descriptor: descriptor)
for i in 0 ..< intersectionFunctions.count {
// Get a handle to the linked intersection function in the pipeline state
let functionHandle = computePipeline.functionHandle(function: intersectionFunctions[i])
// Insert the function handle into the table
functionTable.setFunction(functionHandle, index: i)
}
// Bind intersection function resources
functionTable.setBuffer(sphereBuffer, offset: 0, index: 0) Pass intersection function table to ray intersector
[[kernel]]
void rtKernel(primitive_acceleration_structure accelerationStructure [[buffer(0)]],
intersection_function_table<triangle_data> functionTable [[buffer(1)]],
/* ... */)
{
// generate ray, create intersector...
intersection = intersector.intersect(r, accelerationStructure, functionTable);
// shading...
} Bind intersection function table with compute command encoder
encoder.setIntersectionFunctionTable(functionTable, bufferIndex: 1) Resources
Related sessions
-
32 min -
40 min