.. _customize_shaders:
Customizing Shaders
====================================
SAPIEN renderer compiles GLSL shaders on the fly. One may provide new shader
files to change the behavior of SAPIEN renderer completely.
This tutorial covers the design of SAPIEN's shader packs, and demonstrates how
to customize the renderer with 2 examples:
* Add a depth-of-field effect to the rasterization pipeline (coming soon)
* Add a per-pixel sampling variance measure to the ray-tracing pipeline
.. note::
This tutorial assumes knowledge of GPU pipelines and GLSL. If you are not
familiar with the them, we recommend reading `Vulkan tutorial
`_ and `OpenGL tutorial
`_ first.
.. warning::
The current design for renderer customization is experimental. You may expect
frequent breaking changes in future versions.
Shader pack
----------------
A shader pack in SAPIEN is a directory containing glsl files. SAPIEN parses the
file names and file contents under this directory to determine rendering
bahavior.
If `gbuffer.frag` exists, SAPIEN recognizes the directory as a rasterization
shader pack, if `camera.rgen` exists, SAPIEN recognizes the directory as a
ray-tracing shader pack.
The best way to understand SAPIEN's shaders is by reading them. The default
rasterization shader pack directory can be found by shell command
.. code-block:: shell
python -c 'import os,sapien; print(os.path.dirname(sapien.__file__) + "/vulkan_shader/default")'
The default ray traacing shader pack directory can be found by shell command
.. code-block:: shell
python -c 'import os,sapien; print(os.path.dirname(sapien.__file__) + "/vulkan_shader/rt")'
Rasterization pipeline
-----------------------------
SAPIEN adopts a multi-pass deferred rendering pipeline for rasterization. The
passes are determined by files in the shader pack.
Render passes
^^^^^^^^^^^^^^^^^^
A render pass is identified by files describing its shader stages. To enable
render pass `A`, you need to create `A.vert`, `A.geom`, and/or `A.frag` under
the shader pack directory.
SAPIEN rasterization pipeline **requires** a `gbuffer` pass. At the minimum, a
rasterization shader pack contains `gbuffer.vert` and `gbuffer.frag` files. All
other files are optional to describe more complex pipelines.
If `gbuffer1.vert` and `gbuffer1.frag` exist. The `gbuffer` pass only processes
opaque objects and `gbuffer1` pass processes transparent objects. More over if
`gbuffer{x}.vert` and `gbuffer{x}.frag` exist where `{x}` is consecutive
integers starting from 2. SAPIEN will render objects marked with render mode
`{x}` with this pass (this functionality has not been exposed to Python).
2 other special gbuffer passes are the `point` pass and `line` pass, which
processes point and line primitives.
`shadow` pass describes shadow map generation and only contains the vertex
shader stage `shadow.vert`. Multiple render passes will be created during
rendering depending on the number of lights added to the scene.
`deferred` pass describes the deferred lighting stage. `deferred.vert` is almost
always the same code that draws a full-screen triangle. `deferred.frag` should
compute lighting from rendering results of the `gbuffer` pass.
`composite` passes are additional `deferred` passes for multi-pass
post-processing. They share a `composite.vert` that is identical to
`deferred.vert`. Their fragment shaders are named `composite{x}.frag` where
`{x}` are consecutive integers starting from 0.
Geometry input
^^^^^^^^^^^^^^^^^^^
Geometries (mesh, point clouds, etc.) are specified as input parameters to
vertex shader of gbuffer passes (`gbuffer{x}.vert`, `point.vert`, `line.vert`).
SAPIEN has 6 built-in input attributes ``vec3 position``, ``vec3 normal``,
``vec2 uv``, ``vec3 tangent``, ``vec3 bitangent``, ``vec4 color``. ``position``
is the required attribute and must be placed at `location=0`. All other
attributes are optional and SAPIEN automatically identifies them and loads them
to the GPU. They can be placed in any order, but their locations must be
consecutive integers. Here is an example of a valid input layout.
.. code-block:: glsl
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec2 uv;
layout(location = 3) in vec3 tangent;
layout(location = 4) in vec3 bitangent;
.. note::
All `gbuffer{x}` passes must use the same vertex layout, even across
different shaders.
All `point` and `line` passes must use the same vertex layout, even across
different shaders.
Uniform input
^^^^^^^^^^^^^^^^^^^^^^^
Property of objects, the camera, and the scene are bound as uniform buffers and
textures in descriptor sets. Each descriptor set can use any set number. SAPIEN
recognizes these sets by their names.
Camera set
""""""""""""
The camera set should have the following buffer at binding 0. The name
``CameraBuffer`` and ``cameraBuffer`` are mandatory.
.. code-block:: glsl
layout(set = 0, binding = 0) uniform CameraBuffer {
mat4 viewMatrix; // view matrix of the current frame
mat4 projectionMatrix; // projection matrix of this camera
mat4 viewMatrixInverse;
mat4 projectionMatrixInverse;
float width; // render target width
float height; // render target height
} cameraBuffer;
Object set
""""""""""""
The object set should have the following buffer at binding 0 and 1. The name
``ObjectTransformBuffer`` and ``ObjectDataBuffer`` are mandatory.
.. code-block:: glsl
layout(set = 1, binding = 0) uniform ObjectTransformBuffer {
mat4 modelMatrix;
} objectTransformBuffer;
layout(set = 1, binding = 1) uniform ObjectDataBuffer {
uvec4 segmentation;
float transparency;
int shadeFlat;
} objectDataBuffer;
Material set
""""""""""""""
The material set should always be the same.
.. code-block:: glsl
layout(set = 2, binding = 0) uniform MaterialBuffer {
vec4 emission;
vec4 baseColor;
float fresnel;
float roughness;
float metallic;
float transmission;
float ior;
float transmissionRoughness;
int textureMask;
int padding1;
vec4 textureTransforms[6];
} materialBuffer;
Scene set
""""""""""""
The scene set should have the following buffer at binding 0 and 1. The name
``SceneBuffer``, ``sceneBuffer``, ``LightBuffer``, ``ShadowBuffer``, and
``shadowBuffer`` are mandatory.
.. code-block:: glsl
layout(set = 0, binding = 0) uniform SceneBuffer {
vec4 ambientLight;
DirectionalLight directionalLights[3];
SpotLight spotLights[10];
PointLight pointLights[10];
SpotLight texturedLights[1];
} sceneBuffer;
struct LightBuffer {
mat4 viewMatrix;
mat4 viewMatrixInverse;
mat4 projectionMatrix;
mat4 projectionMatrixInverse;
int width;
int height;
};
layout(set = 0, binding = 1) uniform ShadowBuffer {
LightBuffer directionalLightBuffers[3];
LightBuffer spotLightBuffers[10];
LightBuffer pointLightBuffers[60]; // 1 point light requires 6 buffers
LightBuffer texturedLightBuffers[1];
} shadowBuffer;
If shadow map is enabled, the next few bindings are used for the depth maps.
.. code-block:: glsl
layout(set = 3, binding = 2) uniform samplerCube samplerPointLightDepths[3];
layout(set = 3, binding = 3) uniform sampler2D samplerDirectionalLightDepths[1];
layout(set = 3, binding = 4) uniform sampler2D samplerTexturedLightDepths[1];
layout(set = 3, binding = 5) uniform sampler2D samplerSpotLightDepths[10];
Additionally, if textured light (active light) is enabled, a texture light
texture is required. Currently only 1 textured light texture is supported.
.. code-block:: glsl
layout(set = 3, binding = 6) uniform sampler2D samplerTexturedLightTextures[1];
You are also required to declare the following specialization constants that
match the numbers used in ``SceneBuffer`` and ``ShadowBuffer``. The order does
not matter. These numbers also correspond to the maximum number of lights and
shadows allowed for this shader.
.. code-block:: glsl
layout (constant_id = 0) const int NUM_DIRECTIONAL_LIGHTS = 3;
layout (constant_id = 1) const int NUM_POINT_LIGHTS = 10;
layout (constant_id = 2) const int NUM_DIRECTIONAL_LIGHT_SHADOWS = 1;
layout (constant_id = 3) const int NUM_POINT_LIGHT_SHADOWS = 3;
layout (constant_id = 4) const int NUM_TEXTURED_LIGHT_SHADOWS = 1;
layout (constant_id = 5) const int NUM_SPOT_LIGHT_SHADOWS = 10;
layout (constant_id = 6) const int NUM_SPOT_LIGHTS = 10;
Additionally, the scene set has 2 more recognized textures ``sampleEnvironment``
and ``sampleBRDFLUT``
.. code-block:: glsl
layout(set = 3, binding = 7) uniform samplerCube samplerEnvironment;
layout(set = 3, binding = 8) uniform sampler2D samplerBRDFLUT;
``sampleEnvironment`` is the texture specified by SAPIEN's ``set_environment_map``
functions. ``sampleBRDFLUT`` is a fixed texture used in `image based lighting `_.
Render targets
^^^^^^^^^^^^^^^^
Render targets are textures written by the renderer. These textures are
eventually retrieved by ``camera.get_picture`` and ``camera.get_picture_cuda``.
Output
""""""""""
When render targets are written as a color attachment, you need to add an `out`
as prefix to its name. For example in `gbuffer` shader, one may do
.. code-block:: glsl
layout(location = 0) out vec4 outAlbedo;
layout(location = 1) out vec4 outSpecular;
layout(location = 2) out vec4 outNormal;
layout(location = 3) out uvec4 outSegmentation;
SAPIEN recognizes these names and create float textures named `Albedo`,
`Specular`, and `Normal`, as well as uint texture `Segmentation`.
Input
""""""""""
When render targets need to be used as input to `deferred` and `composite`
passes, you need to add a `sampler` as prefix to its name. And use a separate
descriptor set to bind them. For example in `deferred.frag`, you may do
.. code-block:: glsl
layout(set = 2, binding = 0) uniform sampler2D samplerAlbedo;
layout(set = 2, binding = 1) uniform sampler2D samplerNormal;
layout(set = 2, binding = 2) uniform sampler2D samplerSpecular;
layout(location = 0) out vec4 outLighting;
This tells SAPIEN renderer to use the `Albedo`, `Normal`, and `Specular`
textures generated from a previous pass, and this pass generates the `Lighting`
texture.
Ray tracing pipeline
-----------------------------
Uniform input
^^^^^^^^^^^^^^^^^^^^^^
Similar to the rasterization pipeline, information about the camera and the
scene are provided through descriptor sets. However, object and material
information are part of the scene as all obejcts and material information must
be available at once for ray-tracing.
The camera set is identical to the rasterization pipeline. The scene set is
described as follows.
.. code-block:: glsl
struct GeometryInstance {
uint geometryIndex;
uint materialIndex;
int padding0;
int padding1;
};
struct Material {
vec4 emission;
vec4 baseColor;
float fresnel;
float roughness;
float metallic;
float transmission;
float ior;
float transmissionRoughness;
int textureMask;
int padding1;
vec4 textureTransforms[6];
};
struct TextureIndex {
int diffuse;
int metallic;
int roughness;
int emission;
int normal;
int occlusion;
int padding0;
int padding1;
};
struct Object {
uvec4 segmentation;
float transparency;
int shadeFlat;
int padding0;
int padding1;
};
layout(set = 1, binding = 0) uniform accelerationStructureEXT tlas;
layout(set = 1, binding = 1) readonly buffer GeometryInstances
{
GeometryInstance i[];
} geometryInstances;
layout(set = 1, binding = 2) readonly buffer Materials
{
Material m;
} materials[];
layout(set = 1, binding = 3) readonly buffer TextureIndices
{
TextureIndex t[];
} textureIndices;
layout(set = 1, binding = 4) uniform sampler2D textures[];
layout(set = 1, binding = 5) readonly buffer PointLights
{
PointLight l[];
} pointLights;
layout(set = 1, binding = 6) readonly buffer DirectionalLights
{
DirectionalLight l[];
} directionalLights;
layout(set = 1, binding = 7) readonly buffer SpotLights
{
SpotLight l[];
} spotLights;
layout(std430, set = 1, binding = 8) readonly buffer Vertices {
Vertex v[];
} vertices[];
layout(set = 1, binding = 9) readonly buffer Indices {
uint i[];
} indices[];
layout(set = 1, binding = 10) uniform samplerCube samplerEnvironment;
layout(set = 1, binding = 11) readonly buffer Objects {
Object o[];
} objects;
The set number and binding order does not matter. the ``Vertices`` buffer must
be using `std430` so that it is compatible with the rasterization pipeline if
you want to use both shader pipelines in SAPIEN.
Ray generation
^^^^^^^^^^^^^^^^^^
Ray tracing's entry point is always the ray generation shader `camera.rgen`.
The render targets must be all specified in this stage. Unlike the rasterization
pipeline, render targets are specified as stroage images instead of color
attachments. For example,
.. code-block:: glsl
layout(set = 0, binding = 0, rgba32f) uniform image2D outHdrColor;
layout(set = 0, binding = 1, rgba32f) uniform image2D outAlbedo;
layout(set = 0, binding = 2, rgba32f) uniform image2D outNormal;
layout(set = 0, binding = 3, rgba32ui) uniform uimage2D outSegmentation;
layout(set = 0, binding = 4, rgba32f) uniform image2D outRadiance;
Ray miss
^^^^^^^^^^^^^^^^^^
SAPIEN renderer expects `camera.rmiss` and `shadow.rmiss` files for camera ray
miss and shadow ray miss respectively.
Ray hit
^^^^^^^^^^^
SAPIEN renderer expects `camera.rahit` for any hit and `camera.rchit` for
closest hit. Shadow ray tracing, and next ray sampling should happen in
`camera.rchit`.
Denoising
^^^^^^^^^^^^^^^^^^
If hardware allows, SAPIEN renderer uses the OpenImageDenoise or OptiX denoiser
to perform denoising on `HdrColor` texture with `Albedo` and `Normal` textures.
Post processing
^^^^^^^^^^^^^^^^^^
After ray tracing and optionally denoising, a
full-screen `postprocessing.comp` compute shader is invoked to process the
render targets (e.g., doing gamma correction). This shader expects storage
images as input.
.. code-block:: glsl
layout(set = 0, binding = 0, rgba32f) uniform readonly image2D HdrColor;
layout(set = 0, binding = 1, rgba32f) uniform writeonly image2D Color;
Parameters
^^^^^^^^^^^^^
Parameters to the ray-tracing pipeline is specified through push constants.
.. code-block:: glsl
layout(push_constant) uniform Constants {
vec3 ambientLight;
int frameCount; // current accumulated frame
int spp; // samples per pixel
int maxDepth; // max camera ray bounces
int russianRoulette; // whether to use Russian Roulette
int russianRouletteMinBounces; // starting point for Russian Roulette
int pointLightCount; // number of point lights
int directionalLightCount; // number of directional lights
int spotLightCount; // number of spot lights (including textured lights)
int envmap; // whether to use envmap or ambient light
};
Example: ray tracing variance
---------------------------------
Create a new shader pack
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Copy the default `rt` shader pack. For example, name it `rt-variance`.
Modify the camera and viewer shader directory to the new `rt-variance` folder.
.. code-block:: python
sapien.render.set_camera_shader_dir("./rt-variance")
sapien.render.set_viewer_shader_dir("./rt-variance")
Add a new output texture
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Edit `camera.rgen` file and declare the variance texture.
.. highlight:: glsl
.. literalinclude:: scripts/rt-variance/camera.rgen
:dedent: 0
:lines: 11-16
Compute variance
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In addition to accumulating the radiance, we also accumulate its square.
.. literalinclude:: scripts/rt-variance/camera.rgen
:dedent: 0
:lines: 38-39
.. literalinclude:: scripts/rt-variance/camera.rgen
:dedent: 0
:lines: 84-85
.. literalinclude:: scripts/rt-variance/camera.rgen
:dedent: 0
:lines: 88-89
Finally, compute incremental variance across frames
.. literalinclude:: scripts/rt-variance/camera.rgen
:dedent: 0
:lines: 91-106
Now the `Variance` and `Color` texture on 32 samples per pixel looks like
.. |pic1| image:: assets/variance.png
:width: 45%
.. |pic2| image:: assets/variance-color.png
:width: 45%
|pic1| |pic2|