Skip to content

Latest commit

 

History

History
151 lines (114 loc) · 4.85 KB

pointcloud.md

File metadata and controls

151 lines (114 loc) · 4.85 KB

PointCloud Plugin

Hardware-rasterized particle rendering into the G-buffer with self-shadowing via voxel grid DDA.

Usage

#include "newtype/plugin/PointCloud.h"
#include "newtype/core/Pipeline.h"

// Create a point cloud with up to 1M particles
auto cloud = std::make_unique<newtype::plugin::PointCloud>(
    NewTypeRenderer::device(),
    1'000'000u,                       // max particles
    materialId,                       // material index (must be added to Pipeline first)
    float3{-100.f, -10.f, -100.f},   // bounds_min (world space)
    float3{ 100.f, 200.f,  100.f},   // bounds_max (world space)
    256u                              // voxel grid resolution
);

_cloud = cloud.get();
mPipeline->addFeature(std::move(cloud));  // must be added BEFORE buildScene()

mPipeline->buildScene();  // queries IVoxelGridUser config from PointCloud

// Each frame — upload particle data
luisa::vector<luisa::float4> positions(count);  // xyz = world pos, w = particle size
luisa::vector<luisa::float3> velocities(count);
// ... fill positions/velocities ...
_cloud->upload_positions(stream, positions, velocities, count);

Architecture

Pipeline::buildScene()
  └─ discovers IVoxelGridUser features → creates VoxelGrid with correct bounds

Each frame:
  PointCloud::onExecute()
    ├─ VoxelGrid::voxelize(positions) on main COMPUTE stream
    ├─ synchronize()
    ├─ RasterStream: clear RTVs + depth buffer
    ├─ RasterStream: draw instanced billboards (1 quad × N particles)
    └─ RasterStream: merge shader → write into G-buffer (depth, vis, bary)

G-buffer vis encoding for points

Bits Field
.x material_id (passed as pixel shader argument)
.y bit 31 glass flag (0 for points)
.y bit 30 is_point flag (1 for points)
.y bits 0-29 particle_id

The shade shader detects is_point and does an early return with material albedo, bypassing instance_buffer lookup (which would be out-of-bounds since inst_id = material_id, not a real instance index).

Circle-shaped Points (Discard)

The DSL doesn't expose discard as a named function, but you can call it via FunctionBuilder:

#include <luisa/ast/function_builder.h>
#include <luisa/ast/op.h>

// In pixel shader lambda:
Float2 corner_uv = vary.corner_uv;
Float dist_sq = corner_uv.x * corner_uv.x + corner_uv.y * corner_uv.y;
$if(dist_sq > 1.0f) {
    luisa::compute::detail::FunctionBuilder::current()->call(
        luisa::compute::CallOp::RASTER_DISCARD, {});
};

corner_uv is in [-1, +1] — discarding outside length > 1 trims the square billboard to a circle.

Replacing the Quad with a Custom Mesh

The billboard quad can be replaced with any mesh (e.g., low-poly sphere):

1. Update MeshFormat

_meshFormat = {};
VertexAttribute attrs[] = {
    {VertexAttributeType::Position, PixelFormat::RGBA32F},
    {VertexAttributeType::Normal,   PixelFormat::RGBA32F},
    {VertexAttributeType::UV0,      PixelFormat::RG32F},
};
_meshFormat.emplace_vertex_stream(attrs);

2. Replace Vertex Buffer

// Instead of 6 billboard quad verts, fill with your mesh vertices
// Vertex stride must match sum of attribute sizes in MeshFormat
_meshVB = device.create_buffer<VertexType>(vertexCount);
// ... upload mesh data ...

3. Update RasterMesh

VertexBufferView vbv{_meshVB};
RasterMesh mesh(
    luisa::span<const VertexBufferView, 1>{&vbv, 1},
    meshVertexCount,       // was 6 for quad
    _particleCount,        // instance count (unchanged)
    0u
);

For indexed meshes, add an index buffer as a second vertex stream view.

4. Update Vertex Shader

The vertex shader reads mesh vertex positions instead of billboard corner offsets:

Float4 particle = particles.read(app.instance_id);
Float3 center = particle.xyz();
Float  size   = particle.w;
Float3 world_pos = center + app.position.xyz() * size;  // mesh vertex offset × size

Semantic Mapping (DX12 Input Layout)

VertexAttributeType HLSL Semantic Index
Position POSITION 0
Normal NORMAL 0
Tangent TANGENT 0
Color COLOR 0
UV0 TEXCOORD 0
UV1 TEXCOORD 1
UV2 TEXCOORD 2
UV3 TEXCOORD 3

SV_InstanceID is system-generated by the GPU — not in the vertex buffer.

VoxelGrid

The VoxelGrid uses per-axis voxel coordinate mapping for non-cubic bounds:

cell_f = (pos - bounds_min) * resolution / (bounds_max - bounds_min)

This avoids the old single-scalar cell_size (diagonal-based) which produced out-of-bounds indices for non-cubic bounds, causing GPU memory corruption that broke DI/GI passes.