summaryrefslogtreecommitdiff
path: root/examples/model-viewer/shaders.slang
diff options
context:
space:
mode:
authorTim Foley <tfoleyNV@users.noreply.github.com>2018-08-10 22:21:44 -0700
committerGitHub <noreply@github.com>2018-08-10 22:21:44 -0700
commit56d8a752d84e984afab20de5980edf10fe6c06f5 (patch)
treeeb1491b940daebc6afd200202347191d77f76112 /examples/model-viewer/shaders.slang
parent73ff6907d723003d30e400f661876e7960de574f (diff)
Improve model-viewer support for lights (#626)
* Improve model-viewer support for lights The main visible change here is that the model-viewer example supports multiple light sources, with a basic UI for adding new light sources to the scene, and for manipulating the ones that are there. Along the way I also refactored the `IMaterial` decomposition to be a bit less naive, while still only including a completely naive Blinn-Phong implementation. I also went ahead and spruced up the `cube.obj` file so that it has multiple materials, although it is still a completely uninteresting asset. * Fixup: Windows SDK version
Diffstat (limited to 'examples/model-viewer/shaders.slang')
-rw-r--r--examples/model-viewer/shaders.slang413
1 files changed, 360 insertions, 53 deletions
diff --git a/examples/model-viewer/shaders.slang b/examples/model-viewer/shaders.slang
index b79636d15..9b6538577 100644
--- a/examples/model-viewer/shaders.slang
+++ b/examples/model-viewer/shaders.slang
@@ -11,76 +11,165 @@
// on the C preprocessor manual parameter-binding decorations.
//
-// We will start with a `struct` for per-view parameters that
-// will be allocated into a `ParameterBlock`.
+// We are going to define a simple model for surface material shading.
//
-// As written, this isn't very different from using an HLSL
-// `cbuffer` declaration, but importantly this code will
-// continue to work if we add one or more resources (e.g.,
-// an enironment map texture) to the `PerView` type.
+// The first building block in our model will be the representation of
+// the geometry attributes of a surface as fed into the material.
//
-struct PerView
+struct SurfaceGeometry
{
- float4x4 viewProjection;
+ float3 position;
+ float3 normal;
- float3 lightDir;
- float3 lightColor;
-};
-ParameterBlock<PerView> gViewParams;
+ // TODO: tangent vectors would be the natural next thing to add here,
+ // and would be required for anisotropic materials. However, the
+ // simplistic model loading code we are currently using doesn't
+ // produce tangents...
+ //
+ // float3 tangentU;
+ // float3 tangentV;
-// Declaring a block for per-model parameter data is
-// similarly simple.
+ // We store a single UV parameterization in these geometry attributes.
+ // A more complex renderer might need support for multiple UV sets,
+ // and indeed it might choose to use interfaces and generics to capture
+ // the different requirements that different materials impose on
+ // the available surface attributes. We won't go to that kind of
+ // trouble for such a simple example.
+ //
+ float2 uv;
+};
//
-struct PerModel
+// Next, we want to define the fundamental concept of a refletance
+// function, so that we can use it as a building block for other
+// parts of the system. This is a case where we are trying to
+// show how a proper physically-based renderer (PBR) might
+// decompose the problem using Slang, even though our simple
+// example is *not* physically based.
+//
+interface IBRDF
{
- float4x4 modelTransform;
- float4x4 inverseTransposeModelTransform;
+ // Technically, a BRDF is only a function of the incident
+ // (`wi`) and exitant (`wo`) directions, but for simplicity
+ // we are passing in the surface normal (`N`) as well.
+ //
+ float3 evaluate(float3 wo, float3 wi, float3 N);
};
-ParameterBlock<PerModel> gModelParams;
+//
+// We can now define various implemntations of the `IBRDF` interface
+// that represent different reflectance functions we want to support.
+// For now we keep things simple by defining about the simplest
+// reflectance function we can think of: the Blinn-Phong reflectance
+// model:
+//
+struct BlinnPhong : IBRDF
+{
+ // Blinn-Phong needs diffuse and specular reflectances, plus
+ // a specular exponent value (which relates to "roughness"
+ // in more modern physically-based models).
+ //
+ float3 kd;
+ float3 ks;
+ float specularity;
+ // Here we implement the one requirement of the `IBRDF` interface
+ // for our concrete implementation, using a textbook definition
+ // of Blinng-Phong shading.
+ //
+ // Note: our "BRDF" definition here folds the N-dot-L term into
+ // the evlauation of the reflectance function in case there are
+ // useful algebraic simplifications this enables.
+ //
+ float3 evaluate(float3 V, float3 L, float3 N)
+ {
+ float nDotL = saturate(dot(N, L));
+ float3 H = normalize(L + V);
+ float nDotH = saturate(dot(N, H));
-// Next, we are going to demonstrate a simplistic interface
-// for surface materials. As written, materials can only
-// determine how to compute the diffuse color component
-// of a surface; a more advanced example would fold
-// the entire BRDF into the material interface.
+ return kd*nDotL + ks*pow(nDotH, specularity);
+ }
+};
+//
+// It is important to note that a reflectance function is *not*
+// a "material." In most cases, a material will have spatially-varying
+// properties so that it cannot be summarized as a single `IBRDF`
+// instance.
+//
+// Thus a "material" is a value that can produce a BRDF for any point
+// on a surface (e.g., by sampling texture maps, etc.).
//
interface IMaterial
{
- float3 getDiffuseColor();
+ // Different concrete material implementations might yield BRDF
+ // values with different types. E.g., one material might yield
+ // reflectance functions using `BlinnPhong` while another uses
+ // a much more complicated/accurate representation.
+ //
+ // We encapsulate the choice of BRDF parameters/evaluation in
+ // our material interface with an "associated type." In the
+ // simplest terms, think of this as an interface requirement
+ // that is a type, instead of a method.
+ //
+ // (If you are C++-minded, you might think of this as akin to
+ // how every container provided an `iterator` type, but different
+ // containers may have different types of iterators)
+ //
+ associatedtype BRDF : IBRDF;
+
+ // For our simple example program, it is enough for a material to
+ // be able to return a BRDF given a point on the surface.
+ //
+ // A more complex implementation of material shading might also
+ // have the material return updated surface geometry to reflect
+ // the result of normal mapping, occlusion mapping, etc. or
+ // return an opacity/coverage value for partially transparent
+ // surfaces.
+ //
+ BRDF prepare(SurfaceGeometry geometry);
};
-// In order for our shader to be able to take a material
-// as a parameter, we need to declare a `ParameterBlock<M>`
-// for some material type `M`. Rather than hard-code the
-// specific material type to use, or select one via the
-// preprocessor, we will use Slang's support for generics,
-// by defining a "global type parameter":
-//
-type_param TMaterial : IMaterial;
-//
-// This declaration declares a shader parameter `TMaterial`
-// that is a to-be-determined *type*. The `TMaterial`
-// type parameter is *constrained* to only support types
-// that implement our `IMaterial` interface.
+// We will now define a trivial first implementation of the material
+// interface, which uses our Blinn-Phong BRDF with uniform values
+// for its parameters.
//
-// With the `TMaterial` parameter declared, we can
-// declare that our shader takes as input a parameter block
-// containing material data:
+// Note that this implemetnation is being provided *after* the
+// shader parameter `gMaterial` is declared, so that there is no
+// assumption in the shader code that `gMaterial` will be plugged
+// in using an instance of `SimpleMaterial`
//
-ParameterBlock<TMaterial> gMaterial;
-
-// For now, we will define only a single implementation
-// of the `IMaterial` interface, which is a simple material
-// with a uniform diffuse color:
//
struct SimpleMaterial : IMaterial
{
+ // We declare the properties we need as fields of the material type.
+ // When `SimpleMaterial` is used for `TMaterial` above, then
+ // `gMaterial` will be a `ParameterBlock<SimpleMaterial>`, and these
+ // parameters will be allocated to a constant buffer that is part of
+ // that parameter block.
+ //
+ // TODO: A future version of this example will include texture parameters
+ // here to show that they are declared just like simple uniforms.
+ //
float3 diffuseColor;
+ float3 specularColor;
+ float specularity;
- float3 getDiffuseColor()
+ // To satisfy the requirements of the `IMaterial` interface, our
+ // material type needs to provide a suitable `BRDF` type. We
+ // do this by using a simple `typedef`, although a nested
+ // `struct` type can also satisfy an assocaited type requirement.
+ //
+ // A future version of the Slang compiler may allow the "right"
+ // associated type definition to be inferred from the signature
+ // of the `prepare()` method below.
+ //
+ typedef BlinnPhong BRDF;
+
+ BlinnPhong prepare(SurfaceGeometry geometry)
{
- return diffuseColor;
+ BlinnPhong brdf;
+ brdf.kd = diffuseColor;
+ brdf.ks = specularColor;
+ brdf.specularity = specularity;
+ return brdf;
}
};
//
@@ -91,6 +180,191 @@ struct SimpleMaterial : IMaterial
// `TMaterial` parameter.
//
+// A light, or an entire lighting *environment* is an object
+// that can illuminate a surface using some BRDF implemented
+// with our abstractions above.
+//
+interface ILightEnv
+{
+ // The `illuminate` method is intended to integrate incoming
+ // illumination from this light (environment) incident at the
+ // surface point given by `g` (which has the reflectance function
+ // `brdf`) and reflected into the outgoing direction `wo`.
+ //
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo);
+ //
+ // Note that the `illuminate()` method is allowed as an interface
+ // requirement in Slang even though it is a generic. Constract that
+ // with C++ where a `template` method cannot be `virtual`.
+};
+
+// Given the `ILightEnv` interface, we can write up almost textbook
+// definition of directional and point lights.
+
+struct DirectionalLight : ILightEnv
+{
+ float3 direction;
+ float3 intensity;
+
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
+ {
+ return intensity * brdf.evaluate(wo, direction, g.normal);
+ }
+};
+struct PointLight : ILightEnv
+{
+ float3 position;
+ float3 intensity;
+
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
+ {
+ float3 delta = position - g.position;
+ float d = length(delta);
+ float3 direction = normalize(delta);
+ float3 illuminance = intensity / (d*d);
+ return illuminance * brdf.evaluate(wo, direction, g.normal);
+ }
+};
+
+// In most cases, a shader entry point will only be specialized for a single
+// material, but interesting rendering almost always needs multiple lights.
+// For that reason we will next define types to represent *composite* lighting
+// environment with multiple lights.
+//
+// A naive approach might be to have a single undifferntiated list of lights
+// where any type of light may appear at any index, but this would lose all
+// of the benefits of static specialization: we would have to perform dynamic
+// branching to determine what kind of light is stored at each index.
+//
+// Instead, we will start with a type for *homogeneous* arrays of lights:
+//
+struct LightArray<L : ILightEnv, let N : int> : ILightEnv
+{
+ // The `LightArray` type has two generic parameters:
+ //
+ // - `L` is a type parameter, representing the type of lights that will be in our array
+ // - `N` is a generic *value* parameter, representing the maximum number of lights allowed
+ //
+ // Slang's support for generic value parameters is currently experimental,
+ // and the syntax might change.
+
+ int count;
+ L lights[N];
+
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
+ {
+ // Our light array integrates illumination by naively summing
+ // contributions from all the lights in the array (up to `count`).
+ //
+ // A more advanced renderer might try apply sampling techniques
+ // to pick a subset of lights to sample.
+ //
+ float3 sum = 0;
+ for( int ii = 0; ii < count; ++ii )
+ {
+ sum += lights[ii].illuminate(g, brdf, wo);
+ }
+ return sum;
+ }
+};
+
+// `LightArray` can handle multiple lights as long as they have the
+// same type, but we need a way to have a scene with multiple lights
+// of different types *without* losing static specialization.
+//
+// The `LightPair<T,U>` type supports this in about the simplest way
+// possible, by aggregating a light (environment) of type `T` and
+// one of type `U`. Those light environments might themselves be
+// `LightArray`s or `LightPair`s, so that arbitrarily complex
+// environments can be created from just these two composite types.
+//
+// This is probably a good place to insert a reminder the Slang's
+// generics are *not* C++ templates, so that the error messages
+// produced when working with these types are in general reasonable,
+// and this is *not* any form of "template metaprogramming."
+//
+// That said, we expect that future versions of Slang will make
+// defining composite types light this a bit less cumbersome.
+//
+struct LightPair<T : ILightEnv, U : ILightEnv> : ILightEnv
+{
+ T first;
+ U second;
+
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
+ {
+ return first.illuminate(g, brdf, wo)
+ + second.illuminate(g, brdf, wo);
+ }
+};
+
+// As a final (degenerate) case, we will define a light
+// environment with *no* lights, which contributes no illumination.
+//
+struct EmptyLightEnv : ILightEnv
+{
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
+ {
+ return 0;
+ }
+};
+
+// The code above constitutes the "shader library" for our
+// application, while the code below this point is the
+// implementation of a simple forward rendering pass
+// using that library.
+//
+// While the shader library has used many of Slang's advanced
+// mechanisms, the vertex and fragment shaders will be
+// much more modest, and hopefully easier to follow.
+
+
+// We will start with a `struct` for per-view parameters that
+// will be allocated into a `ParameterBlock`.
+//
+// As written, this isn't very different from using an HLSL
+// `cbuffer` declaration, but importantly this code will
+// continue to work if we add one or more resources (e.g.,
+// an enironment map texture) to the `PerView` type.
+//
+struct PerView
+{
+ float4x4 viewProjection;
+ float3 eyePosition;
+};
+ParameterBlock<PerView> gViewParams;
+
+// Declaring a block for per-model parameter data is
+// similarly simple.
+//
+struct PerModel
+{
+ float4x4 modelTransform;
+ float4x4 inverseTransposeModelTransform;
+};
+ParameterBlock<PerModel> gModelParams;
+
+// We want our shader to work with any kind of lighting environment
+// - that is, and type that implements `ILightEnv`. Furthermore,
+// we want the parameters of that lighting environment to be passed
+// as parameter block - `ParameterBlock<L>` for some type `L`.
+//
+// We handle this by defining a global generic type parameter for
+// our shader, and constrainting it to implement `ILightEnv`...
+//
+type_param TLightEnv : ILightEnv;
+//
+// ... and then defining a parameter block that uses that type
+// parameter as the "element type" of the block:
+//
+ParameterBlock<TLightEnv> gLightEnv;
+
+// Our handling of the material parameter for our shader
+// is quite similar to the case for the lighting environment:
+//
+type_param TMaterial : IMaterial;
+ParameterBlock<TMaterial> gMaterial;
+
// Our vertex shader entry point is only marginally more
// complicated than the Hello World example. We will
// start by declaring the various "connector" `struct`s.
@@ -167,12 +441,45 @@ VertexStageOutput vertexMain(
float4 fragmentMain(
CoarseVertex coarseVertex : CoarseVertex) : SV_Target
{
- float3 N = normalize(coarseVertex.worldNormal);
- float3 L = normalize(gViewParams.lightDir);
+ // We start by using our interpolated vertex attributes
+ // to construct the local surface geometry that we will
+ // use for material evaluation.
+ //
+ SurfaceGeometry g;
+ g.position = coarseVertex.worldPosition;
+ g.normal = normalize(coarseVertex.worldNormal);
+ g.uv = coarseVertex.uv;
+
+ float3 V = normalize(gViewParams.eyePosition - g.position);
+
+ // Next we prepare the material, which involves running
+ // any "pattern generation" logic of the material (e.g.,
+ // sampling and blending texture layers), to produce
+ // a BRDF suitable for evaluating under illumination
+ // from different light sources.
+ //
+ // Note that the return type here is `TMaterial.BRDF`,
+ // which is the `BRDF` type *assocaited* with the (unknown)
+ // `TMaterial` type. When `TMaterial` gets substituted for
+ // a concrete type later (e.g., `SimpleMaterial`) this
+ // will resolve to a concrete type too (e.g., `SimpleMaterial.BRDF`
+ // which is an alias for `BlinnPhong`).
+ //
+ TMaterial.BRDF brdf = gMaterial.prepare(g);
+
+ // Now that we've done the first step of material evaluation
+ // and sampled texture maps, etc., it is time to start
+ // integrating incident light at our surface point.
+ //
+ // Because we've wrapped up the lighting environment as
+ // a single (composite) object, this is as simple as calling
+ // its `illuminate()` method. Our particular fragment shader
+ // is thus abstracted from how the renderer chooses to structure
+ // this integration step, somewhat similar to how an
+ // `illuminance` loop in RenderMan Shading Language works.
+ //
- float4 color;
- color.xyz = gMaterial.getDiffuseColor() * max(0, dot(N, L));
- color.w = 1.0f;
+ float3 color = gLightEnv.illuminate(g, brdf, V);
- return color;
+ return float4(color, 1);
}