diff options
| author | Tim Foley <tfoleyNV@users.noreply.github.com> | 2018-08-10 22:21:44 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-08-10 22:21:44 -0700 |
| commit | 56d8a752d84e984afab20de5980edf10fe6c06f5 (patch) | |
| tree | eb1491b940daebc6afd200202347191d77f76112 /examples/model-viewer/shaders.slang | |
| parent | 73ff6907d723003d30e400f661876e7960de574f (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.slang | 413 |
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); } |
