This tutorial demonstrates how to use the NVIDIA VK_NV_ray_tracing_linear_swept_spheres extension to efficiently render grass fields, standalone spheres, and multi-segment chains using specialized ray tracing primitives. The extension introduces two new geometric primitives—Spheres and Linear Swept Spheres (LSS)—that provide compact representation and hardware-accelerated intersection for sphere-based geometry.
Modified: 18_swept_spheres.cpp (main function)
- Added
VK_NV_RAY_TRACING_LINEAR_SWEPT_SPHERES_EXTENSION_NAMEto device extensions - Enabled
VkPhysicalDeviceRayTracingLinearSweptSpheresFeaturesNVwith both.linearSweptSpheresand.spheresfeatures - Added
VK_PIPELINE_CREATE_2_RAY_TRACING_ALLOW_SPHERES_AND_LINEAR_SWEPT_SPHERES_BIT_NVflag to ray tracing pipeline
VkPhysicalDeviceRayTracingLinearSweptSpheresFeaturesNV linearSweptSpheresFeature{
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_LINEAR_SWEPT_SPHERES_FEATURES_NV
};
nvvk::ContextInitInfo vkSetup{
// ...
.deviceExtensions = {
// ... other extensions
{VK_NV_RAY_TRACING_LINEAR_SWEPT_SPHERES_EXTENSION_NAME, &linearSweptSpheresFeature, false},
},
};New: Scene Generation Functions
The tutorial creates three types of swept sphere geometries:
- many grass blades randomly scattered in a 10×10 unit field
- Each blade represented by two vertices (root and tip) with corresponding radii
- Random height variations (0.2-0.5 units) and position jitter for natural appearance
- Radii taper from base (0.003-0.008) to tip (40% of base) for realistic grass blade shape
- Uses
VK_RAY_TRACING_LSS_INDEXING_MODE_LIST_NV- no index buffer needed - Vertices stored sequentially: pairs (0,1), (2,3), (4,5), etc. automatically form LSS primitives
void createGrassField(uint32_t grassBlades, glm::vec2 fieldSize) {
for(uint32_t i = 0; i < grassBlades; i++) {
glm::vec3 root(xPos, 0.0f, zPos);
glm::vec3 tip(xPos + tilt, height, zPos + tilt);
m_grassLSSVertices.push_back(root); // Even index
m_grassLSSVertices.push_back(tip); // Odd index
m_grassLSSRadii.push_back(radius); // Base
m_grassLSSRadii.push_back(radius * 0.4f); // Tip
}
}- 10 spheres with varying radii (0.15-0.45 units)
- Randomly positioned throughout the scene
- Uses
VK_GEOMETRY_TYPE_SPHERES_NVgeometry type - Each sphere defined by a center point and radius
- Used for decorative elements with colorful materials
- 20 chains with 4 segments each (80 total segments)
- Simulate reeds, stalks, or tall grass stems
- Uses
VK_RAY_TRACING_LSS_INDEXING_MODE_SUCCESSIVE_NVwith index buffer - Efficient vertex storage: N+1 vertices for N segments per chain (no duplication)
- Connected segments form curved paths with seamless endcaps
- Chain separation detected by gaps in index sequence
// Chain with 4 segments uses 5 vertices (not 8):
// Vertices: [v0, v1, v2, v3, v4] for chain 0
// [v5, v6, v7, v8, v9] for chain 1
// Indices: [0, 1, 2, 3, 5, 6, 7, 8]
// └─ gap → new chain startsModified: createBottomLevelAS()
Three new BLAS structures are created using extension-specific geometry types:
Grass LSS BLAS (LIST Mode):
VkAccelerationStructureGeometryLinearSweptSpheresDataNV grassLSSData{};
grassLSSData.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_LINEAR_SWEPT_SPHERES_DATA_NV;
grassLSSData.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT;
grassLSSData.vertexData = {.deviceAddress = m_grassLSSVertexBuffer.address};
grassLSSData.vertexStride = sizeof(glm::vec3);
grassLSSData.radiusFormat = VK_FORMAT_R32_SFLOAT;
grassLSSData.radiusData = {.deviceAddress = m_grassLSSRadiusBuffer.address};
grassLSSData.radiusStride = sizeof(float);
grassLSSData.indexType = VK_INDEX_TYPE_NONE_KHR; // No index buffer
grassLSSData.indexingMode = VK_RAY_TRACING_LSS_INDEXING_MODE_LIST_NV;
grassLSSData.endCapsMode = VK_RAY_TRACING_LSS_PRIMITIVE_END_CAPS_MODE_CHAINED_NV;
VkAccelerationStructureGeometryKHR geometry{};
geometry.geometryType = VK_GEOMETRY_TYPE_LINEAR_SWEPT_SPHERES_NV;
geometry.pNext = &grassLSSData;Chains LSS BLAS (SUCCESSIVE Mode):
VkAccelerationStructureGeometryLinearSweptSpheresDataNV chainsLSSData{};
chainsLSSData.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_LINEAR_SWEPT_SPHERES_DATA_NV;
chainsLSSData.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT;
chainsLSSData.vertexData = {.deviceAddress = m_chainsLSSVertexBuffer.address};
chainsLSSData.vertexStride = sizeof(glm::vec3);
chainsLSSData.radiusFormat = VK_FORMAT_R32_SFLOAT;
chainsLSSData.radiusData = {.deviceAddress = m_chainsLSSRadiusBuffer.address};
chainsLSSData.radiusStride = sizeof(float);
chainsLSSData.indexType = VK_INDEX_TYPE_UINT32; // Index buffer required
chainsLSSData.indexData = {.deviceAddress = m_chainsLSSIndexBuffer.address};
chainsLSSData.indexStride = sizeof(uint32_t);
chainsLSSData.indexingMode = VK_RAY_TRACING_LSS_INDEXING_MODE_SUCCESSIVE_NV;
chainsLSSData.endCapsMode = VK_RAY_TRACING_LSS_PRIMITIVE_END_CAPS_MODE_CHAINED_NV;
VkAccelerationStructureGeometryKHR geometry{};
geometry.geometryType = VK_GEOMETRY_TYPE_LINEAR_SWEPT_SPHERES_NV;
geometry.pNext = &chainsLSSData;Spheres BLAS:
VkAccelerationStructureGeometrySpheresDataNV sphereData{};
sphereData.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_SPHERES_DATA_NV;
sphereData.centerFormat = VK_FORMAT_R32G32B32_SFLOAT;
sphereData.centerData = {.deviceAddress = m_spheresDataBuffer.address};
sphereData.radiusFormat = VK_FORMAT_R32_SFLOAT;
sphereData.radiusData = {.deviceAddress = m_spheresRadiusBuffer.address};
VkAccelerationStructureGeometryKHR geometry{};
geometry.geometryType = VK_GEOMETRY_TYPE_SPHERES_NV;
geometry.pNext = &sphereData;New: rtsweptspheres.slang
Added four closest hit shaders for different primitive types:
rchitMain- Ground plane (triangles)rchitMainGrass- Grass blades (LSS)rchitMainSpheres- Standalone spheresrchitMainChains- Multi-segment chains (LSS)
LSS Normal Calculation:
// Get LSS endpoints and radii from Slang's built-in function
// Returns float2x4: row 0 = [pos0.xyz, radius0], row 1 = [pos1.xyz, radius1]
float2x4 posAndRadii = GetLssPositionsAndRadii();
float3 vertex0 = float3(mul(float4(posAndRadii[0].xyz, 1.0), ObjectToWorld4x3()));
float3 vertex1 = float3(mul(float4(posAndRadii[1].xyz, 1.0), ObjectToWorld4x3()));
// Calculate normal perpendicular to LSS axis
float3 lssAxis = normalize(vertex1 - vertex0);
float3 toHit = worldPos - vertex0;
float t = dot(toHit, lssAxis);
float3 closest = vertex0 + lssAxis * t;
float3 normal = normalize(worldPos - closest);Sphere Normal Calculation:
float3 worldPos = WorldRayOrigin() + WorldRayDirection() * RayTCurrent();
// Get sphere center and radius from Slang's built-in function
// Returns float4: [center.xyz, radius]
float3 sphereCenterWorld = float3(mul(float4(GetSpherePositionAndRadius().xyz, 1.0), ObjectToWorld4x3()));
float3 normal = normalize(worldPos - sphereCenterWorld);New: Texture Mapping Feature
Added UI-controlled image-based coloring for grass:
if(pushConst.useImageColoring && textures.count() > 0) {
float2 uv = (worldPos.xz + float2(5.0, 5.0)) / 10.0;
float3 texColor = textures[0].SampleLevel(uv, 0).xyz;
albedo = lerp(albedo, texColor * albedo, pushConst.colorIntensity);
}| Primitive Type | Geometry Type | Indexing Mode | Use Case | Vertices per Primitive |
|---|---|---|---|---|
| Grass Blades | LSS | LIST (no index buffer) | Independent grass strands | 2N for N blades |
| Standalone Spheres | Spheres | N/A | Decorative objects | 1 per sphere |
| Multi-Segment Chains | LSS | SUCCESSIVE (indexed) | Connected strands (reeds/stalks) | N+1 per N-segment chain |
A Linear Swept Sphere is defined by two points in space with associated radii. The primitive represents the volume swept by a sphere as it moves linearly from the first point to the second, with the radius interpolating between the two endpoint radii. This creates a capsule-like shape perfect for representing thin cylindrical objects like grass, hair, or fur.
Advantages:
- Compact representation (2 vertices + 2 radii vs. many triangles)
- Smooth silhouettes at any distance
- Efficient hardware-accelerated intersection
- Natural representation for strand-based geometry
Simple sphere primitives defined by a center point and radius. These are a subset of LSS where both endpoints coincide, but are optimized specifically for sphere intersection.
When using VK_RAY_TRACING_LSS_INDEXING_MODE_SUCCESSIVE_NV with VK_RAY_TRACING_LSS_PRIMITIVE_END_CAPS_MODE_CHAINED_NV, the extension automatically detects chain boundaries by analyzing the index sequence.
How it works:
- Each index K creates an LSS segment from vertex K to vertex K+1
- Consecutive indices (e.g., 5, 6, 7) indicate segments in the same chain
- Non-consecutive indices (e.g., 7 → 10) indicate a new chain starting
Example with 2 chains of 3 segments each:
Vertices: [v0, v1, v2, v3, v4, v5, v6, v7]
└─ Chain 0 ─┘ └─ Chain 1 ─┘
Index buffer: [0, 1, 2, 4, 5, 6]
└─────┘ └─────┘
Chain 0 Chain 1
↑
Gap (2 → 4) starts new chain
Chain 0 segments:
- Segment 0: v0→v1 (both endcaps - first in chain)
- Segment 1: v1→v2 (trailing endcap only)
- Segment 2: v2→v3 (trailing endcap only)
Chain 1 segments:
- Segment 3: v4→v5 (both endcaps - new chain detected)
- Segment 4: v5→v6 (trailing endcap only)
- Segment 5: v6→v7 (trailing endcap only)
Memory efficiency:
- LIST mode: 2N vertices for N segments (duplicate shared vertices)
- SUCCESSIVE mode: N+1 vertices per chain (shared vertices, 37.5% memory savings for 4-segment chains)
The extension supports different end cap modes for LSS:
VK_RAY_TRACING_LSS_PRIMITIVE_END_CAPS_MODE_CHAINED_NV: When used with SUCCESSIVE mode, the first primitive in each chain has both endcaps enabled, while subsequent primitives only have the trailing endcap. This creates seamless connected chains without gaps.VK_RAY_TRACING_LSS_PRIMITIVE_END_CAPS_MODE_NONE_NV: All endcaps disabled (open-ended capsules)
-
VK_RAY_TRACING_LSS_INDEXING_MODE_LIST_NV: Sequential vertex pairs without an index buffer- Primitives formed from consecutive vertex pairs: (0,1), (2,3), (4,5), etc.
- Used by grass field - simple and efficient for disconnected strands
- Requires 2N vertices for N primitives
-
VK_RAY_TRACING_LSS_INDEXING_MODE_SUCCESSIVE_NV: Indexed mode where each index K defines a segment from vertex K to vertex K+1- Requires an index buffer with one index per primitive
- Used by LSS chains - efficient for multi-segment chains
- Requires N+1 vertices for N segments (shared vertices between segments)
- Chain separation: When indices are non-consecutive (e.g., [0,1,2, 5,6,7]), the gap indicates a new chain starts
- With CHAINED endcaps mode, new chains automatically get both endcaps on their first segment
Pipeline Requirements:
VK_PIPELINE_CREATE_2_RAY_TRACING_ALLOW_SPHERES_AND_LINEAR_SWEPT_SPHERES_BIT_NVflag must be set- Separate hit groups for each primitive type
- Standard
VK_RAY_TRACING_SHADER_GROUP_TYPE_TRIANGLES_HIT_GROUP_KHRtype works for all
Built-In Functions (from Slang Standard Library): The extension's built-in variables are accessed through Slang's standard library functions:
GetSpherePositionAndRadius()- Returnsfloat4containing sphere center (xyz) and radius (w)GetLssPositionsAndRadii()- Returnsfloat2x4matrix with two rows: [position0.xyz, radius0] and [position1.xyz, radius1]IsSphereHit()- Returnsbooltrue if hit is on a sphereIsLssHit()- Returnsbooltrue if hit is on an LSS
These functions are defined in hlsl.meta.slang and provide HLSL, CUDA/OptiX, and SPIRV implementations automatically. They map directly to the extension's SPIRV built-ins (HitSpherePositionNV, HitLSSPositionsNV, etc.).
Normal Calculation:
- For spheres:
normal = normalize(hitPoint - sphereCenter) - For LSS: Project hit point onto LSS axis, then calculate perpendicular normal
Limitations:
- Extension is NVIDIA-specific
- Requires RTX-capable hardware (Blackwell and newer)
Extension Support: If the extension is not available on your hardware, the tutorial will display an error message and show only the ground plane.
