yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

Matthew MoultonImprove documentation and example formatting consistency (#4299)78d34f3b3

master
17.0 KiB478 linesraw
1// shaders.slang
2
3//
4// This example builds on the simplistic shaders presented in the
5// "Hello, World" example by adding support for (intentionally
6// simplistic) surface materil and light shading.
7//
8// The code here is not meant to exemplify state-of-the-art material
9// and lighting techniques, but rather to show how a shader
10// library can be developed in a modular fashion without reliance
11// on the C preprocessor manual parameter-binding decorations.
12//
13
14// We are going to define a simple model for surface material shading.
15//
16// The first building block in our model will be the representation of
17// the geometry attributes of a surface as fed into the material.
18//
19struct SurfaceGeometry
20{
21    float3 position;
22    float3 normal;
23
24    // TODO: tangent vectors would be the natural next thing to add here,
25    // and would be required for anisotropic materials. However, the
26    // simplistic model loading code we are currently using doesn't
27    // produce tangents...
28    //
29    //      float3 tangentU;
30    //      float3 tangentV;
31
32    // We store a single UV parameterization in these geometry attributes.
33    // A more complex renderer might need support for multiple UV sets,
34    // and indeed it might choose to use interfaces and generics to capture
35    // the different requirements that different materials impose on
36    // the available surface attributes. We won't go to that kind of
37    // trouble for such a simple example.
38    //
39    float2 uv;
40};
41//
42// Next, we want to define the fundamental concept of a refletance
43// function, so that we can use it as a building block for other
44// parts of the system. This is a case where we are trying to
45// show how a proper physically-based renderer (PBR) might
46// decompose the problem using Slang, even though our simple
47// example is *not* physically based.
48//
49interface IBRDF
50{
51    // Technically, a BRDF is only a function of the incident
52    // (`wi`) and exitant (`wo`) directions, but for simplicity
53    // we are passing in the surface normal (`N`) as well.
54    //
55    float3 evaluate(float3 wo, float3 wi, float3 N);
56};
57//
58// We can now define various implemntations of the `IBRDF` interface
59// that represent different reflectance functions we want to support.
60// For now we keep things simple by defining about the simplest
61// reflectance function we can think of: the Blinn-Phong reflectance
62// model:
63//
64struct BlinnPhong : IBRDF
65{
66    // Blinn-Phong needs diffuse and specular reflectances, plus
67    // a specular exponent value (which relates to "roughness"
68    // in more modern physically-based models).
69    //
70    float3 kd;
71    float3 ks;
72    float specularity;
73
74    // Here we implement the one requirement of the `IBRDF` interface
75    // for our concrete implementation, using a textbook definition
76    // of Blinng-Phong shading.
77    //
78    // Note: our "BRDF" definition here folds the N-dot-L term into
79    // the evlauation of the reflectance function in case there are
80    // useful algebraic simplifications this enables.
81    //
82    float3 evaluate(float3 V, float3 L, float3 N)
83    {
84        float nDotL = saturate(dot(N, L));
85        float3 H = normalize(L + V);
86        float nDotH = saturate(dot(N, H));
87
88        // TODO: The current model loading has a bug that is leading
89        // to the `ks` and `specularity` fields being invalid garbage
90        // for our example cube, and the result is a non-finite value
91        // coming out of `evaluate()` if we include the specular term.
92
93        // return kd*nDotL + ks*pow(nDotH, specularity);
94        return kd*nDotL;
95    }
96};
97//
98// It is important to note that a reflectance function is *not*
99// a "material." In most cases, a material will have spatially-varying
100// properties so that it cannot be summarized as a single `IBRDF`
101// instance.
102//
103// Thus a "material" is a value that can produce a BRDF for any point
104// on a surface (e.g., by sampling texture maps, etc.).
105//
106interface IMaterial
107{
108    // Different concrete material implementations might yield BRDF
109    // values with different types. E.g., one material might yield
110    // reflectance functions using `BlinnPhong` while another uses
111    // a much more complicated/accurate representation.
112    //
113    // We encapsulate the choice of BRDF parameters/evaluation in
114    // our material interface with an "associated type." In the
115    // simplest terms, think of this as an interface requirement
116    // that is a type, instead of a method.
117    //
118    // (If you are C++-minded, you might think of this as akin to
119    // how every container provided an `iterator` type, but different
120    // containers may have different types of iterators)
121    //
122    associatedtype BRDF : IBRDF;
123
124    // For our simple example program, it is enough for a material to
125    // be able to return a BRDF given a point on the surface.
126    //
127    // A more complex implementation of material shading might also
128    // have the material return updated surface geometry to reflect
129    // the result of normal mapping, occlusion mapping, etc. or
130    // return an opacity/coverage value for partially transparent
131    // surfaces.
132    //
133    BRDF prepare(SurfaceGeometry geometry);
134};
135
136// We will now define a trivial first implementation of the material
137// interface, which uses our Blinn-Phong BRDF with uniform values
138// for its parameters.
139//
140// Note that this implemetnation is being provided *after* the
141// shader parameter `gMaterial` is declared, so that there is no
142// assumption in the shader code that `gMaterial` will be plugged
143// in using an instance of `SimpleMaterial`
144//
145struct SimpleMaterial : IMaterial
146{
147    // We declare the properties we need as fields of the material type.
148    // When `SimpleMaterial` is used for `TMaterial` above, then
149    // `gMaterial` will be a `ParameterBlock<SimpleMaterial>`, and these
150    // parameters will be allocated to a constant buffer that is part of
151    // that parameter block.
152    //
153    // TODO: A future version of this example will include texture parameters
154    // here to show that they are declared just like simple uniforms.
155    //
156    float3 diffuseColor;
157    float3 specularColor;
158    float specularity;
159
160    // To satisfy the requirements of the `IMaterial` interface, our
161    // material type needs to provide a suitable `BRDF` type. We
162    // do this by using a simple `typedef`, although a nested
163    // `struct` type can also satisfy an associated type requirement.
164    //
165    // A future version of the Slang compiler may allow the "right"
166    // associated type definition to be inferred from the signature
167    // of the `prepare()` method below.
168    //
169    typedef BlinnPhong BRDF;
170
171    BlinnPhong prepare(SurfaceGeometry geometry)
172    {
173        BlinnPhong brdf;
174        brdf.kd = diffuseColor;
175        brdf.ks = specularColor;
176        brdf.specularity = specularity;
177        return brdf;
178    }
179};
180//
181// Note that no other code in this file statically
182// references the `SimpleMaterial` type, and instead
183// it is up to the application to "plug in" this type,
184// or another `IMaterial` implementation for the
185// `TMaterial` parameter.
186//
187
188// A light, or an entire lighting *environment* is an object
189// that can illuminate a surface using some BRDF implemented
190// with our abstractions above.
191//
192interface ILightEnv
193{
194    // The `illuminate` method is intended to integrate incoming
195    // illumination from this light (environment) incident at the
196    // surface point given by `g` (which has the reflectance function
197    // `brdf`) and reflected into the outgoing direction `wo`.
198    //
199    float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo);
200    //
201    // Note that the `illuminate()` method is allowed as an interface
202    // requirement in Slang even though it is a generic. Contrast that
203    // with C++ where a `template` method cannot be `virtual`.
204};
205
206// Given the `ILightEnv` interface, we can write up almost textbook
207// definition of directional and point lights.
208
209struct DirectionalLight : ILightEnv
210{
211    float3 direction;
212    float3 intensity;
213
214    float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
215    {
216        return intensity * brdf.evaluate(wo, direction, g.normal);
217    }
218};
219struct PointLight : ILightEnv
220{
221    float3 position;
222    float3 intensity;
223
224    float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
225    {
226        float3 delta = position - g.position;
227        float d = length(delta);
228        float3 direction = normalize(delta);
229        float3 illuminance = intensity / (d*d);
230        return illuminance * brdf.evaluate(wo, direction, g.normal);
231    }
232};
233
234// In most cases, a shader entry point will only be specialized for a single
235// material, but interesting rendering almost always needs multiple lights.
236// For that reason we will next define types to represent *composite* lighting
237// environment with multiple lights.
238//
239// A naive approach might be to have a single undifferntiated list of lights
240// where any type of light may appear at any index, but this would lose all
241// of the benefits of static specialization: we would have to perform dynamic
242// branching to determine what kind of light is stored at each index.
243//
244// Instead, we will start with a type for *homogeneous* arrays of lights:
245//
246struct LightArray<L : ILightEnv, let N : int> : ILightEnv
247{
248    // The `LightArray` type has two generic parameters:
249    //
250    // - `L` is a type parameter, representing the type of lights that will be in our array
251    // - `N` is a generic *value* parameter, representing the maximum number of lights allowed
252    //
253    // Slang's support for generic value parameters is currently experimental,
254    // and the syntax might change.
255
256    int count;
257    L lights[N];
258
259    float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
260    {
261        // Our light array integrates illumination by naively summing
262        // contributions from all the lights in the array (up to `count`).
263        //
264        // A more advanced renderer might try apply sampling techniques
265        // to pick a subset of lights to sample.
266        //
267        float3 sum = 0;
268        for( int ii = 0; ii < count; ++ii )
269        {
270            sum += lights[ii].illuminate(g, brdf, wo);
271        }
272        return sum;
273    }
274};
275
276// `LightArray` can handle multiple lights as long as they have the
277// same type, but we need a way to have a scene with multiple lights
278// of different types *without* losing static specialization.
279//
280// The `LightPair<T,U>` type supports this in about the simplest way
281// possible, by aggregating a light (environment) of type `T` and
282// one of type `U`. Those light environments might themselves be
283// `LightArray`s or `LightPair`s, so that arbitrarily complex
284// environments can be created from just these two composite types.
285//
286// This is probably a good place to insert a reminder the Slang's
287// generics are *not* C++ templates, so that the error messages
288// produced when working with these types are in general reasonable,
289// and this is *not* any form of "template metaprogramming."
290//
291// That said, we expect that future versions of Slang will make
292// defining composite types light this a bit less cumbersome.
293//
294struct LightPair<T : ILightEnv, U : ILightEnv> : ILightEnv
295{
296    T first;
297    U second;
298
299    float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
300    {
301        return first.illuminate(g, brdf, wo)
302            + second.illuminate(g, brdf, wo);
303    }
304};
305
306// As a final (degenerate) case, we will define a light
307// environment with *no* lights, which contributes no illumination.
308//
309struct EmptyLightEnv : ILightEnv
310{
311    float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
312    {
313        return 0;
314    }
315};
316
317// The code above constitutes the "shader library" for our
318// application, while the code below this point is the
319// implementation of a simple forward rendering pass
320// using that library.
321//
322// While the shader library has used many of Slang's advanced
323// mechanisms, the vertex and fragment shaders will be
324// much more modest, and hopefully easier to follow.
325
326
327// We will start with a `struct` for per-view parameters that
328// will be allocated into a `ParameterBlock`.
329//
330// As written, this isn't very different from using an HLSL
331// `cbuffer` declaration, but importantly this code will
332// continue to work if we add one or more resources (e.g.,
333// an enironment map texture) to the `PerView` type.
334//
335struct PerView
336{
337    float4x4    viewProjection;
338    float3      eyePosition;
339};
340ParameterBlock<PerView>     gViewParams;
341
342// Declaring a block for per-model parameter data is
343// similarly simple.
344//
345struct PerModel
346{
347    float4x4    modelTransform;
348    float4x4    inverseTransposeModelTransform;
349};
350ParameterBlock<PerModel>    gModelParams;
351
352// We want our shader to work with any kind of lighting environment
353// - that is, and type that implements `ILightEnv`.
354
355ILightEnv gLightEnv;
356
357// Our handling of the material parameter for our shader
358// is quite similar to the case for the lighting environment:
359//
360IMaterial gMaterial;
361
362// Our vertex shader entry point is only marginally more
363// complicated than the Hello World example. We will
364// start by declaring the various "connector" `struct`s.
365//
366struct AssembledVertex
367{
368    float3 position : POSITION;
369    float3 normal   : NORMAL;
370    float2 uv       : UV;
371};
372struct CoarseVertex
373{
374    float3 worldPosition;
375    float3 worldNormal;
376    float2 uv;
377};
378struct VertexStageOutput
379{
380    CoarseVertex    coarseVertex    : CoarseVertex;
381    float4          sv_position     : SV_Position;
382};
383
384// Perhaps most interesting new feature of the entry
385// point decalrations is that we use a `[shader(...)]`
386// attribute (as introduced in HLSL Shader Model 6.x)
387// in order to tag our entry points.
388//
389// This attribute informs the Slang compiler which
390// functions are intended to be compiled as shader
391// entry points (and what stage they target), so that
392// the programmer no longer needs to specify the
393// entry point name/stage through the API (or on
394// the command line when using `slangc`).
395//
396// While HLSL added this feature only in newer versions,
397// the Slang compiler supports this attribute across
398// *all* targets, so that it is okay to use whether you
399// want DXBC, DXIL, or SPIR-V output.
400//
401[shader("vertex")]
402VertexStageOutput vertexMain(
403    AssembledVertex assembledVertex)
404{
405    VertexStageOutput output;
406
407    float3 position = assembledVertex.position;
408    float3 normal   = assembledVertex.normal;
409    float2 uv       = assembledVertex.uv;
410
411    float3 worldPosition = mul(gModelParams.modelTransform, float4(position, 1.0)).xyz;
412    float3 worldNormal = mul(gModelParams.inverseTransposeModelTransform, float4(normal, 0.0)).xyz;
413
414    output.coarseVertex.worldPosition = worldPosition;
415    output.coarseVertex.worldNormal   = worldNormal;
416    output.coarseVertex.uv            = uv;
417
418    output.sv_position = mul(gViewParams.viewProjection, float4(worldPosition, 1.0));
419
420    return output;
421}
422
423// Our fragment shader is almost trivial, with the most interesting
424// thing being how it uses the `TMaterial` type parameter (through the
425// value stored in the `gMaterial` parameter block) to dispatch to
426// the correct implementation of the `getDiffuseColor()` method
427// in the `IMaterial` interface.
428//
429// The `gMaterial` parameter block declaration thus serves not only
430// to group certain shader parameters for efficient CPU-to-GPU
431// communication, but also to select the code that will execute
432// in specialized versions of the `fragmentMain` entry point.
433//
434[shader("fragment")]
435float4 fragmentMain(
436    CoarseVertex coarseVertex : CoarseVertex) : SV_Target
437{
438    // We start by using our interpolated vertex attributes
439    // to construct the local surface geometry that we will
440    // use for material evaluation.
441    //
442    SurfaceGeometry g;
443    g.position = coarseVertex.worldPosition;
444    g.normal = normalize(coarseVertex.worldNormal);
445    g.uv = coarseVertex.uv;
446
447    float3 V = normalize(gViewParams.eyePosition - g.position);
448
449    // Next we prepare the material, which involves running
450    // any "pattern generation" logic of the material (e.g.,
451    // sampling and blending texture layers), to produce
452    // a BRDF suitable for evaluating under illumination
453    // from different light sources.
454    //
455    // Note that the return type here is `gMaterial.BRDF`,
456    // which is the `BRDF` type *associated* with the (unknown)
457    // `TMaterial` type. When `TMaterial` gets substituted for
458    // a concrete type later (e.g., `SimpleMaterial`) this
459    // will resolve to a concrete type too (e.g., `SimpleMaterial.BRDF`
460    // which is an alias for `BlinnPhong`).
461    //
462    let brdf = gMaterial.prepare(g);
463
464    // Now that we've done the first step of material evaluation
465    // and sampled texture maps, etc., it is time to start
466    // integrating incident light at our surface point.
467    //
468    // Because we've wrapped up the lighting environment as
469    // a single (composite) object, this is as simple as calling
470    // its `illuminate()` method. Our particular fragment shader
471    // is thus abstracted from how the renderer chooses to structure
472    // this integration step, somewhat similar to how an
473    // `illuminance` loop in RenderMan Shading Language works.
474    //
475
476    float3 color = saturate(gLightEnv.illuminate(g, brdf, V) + float3(0.3));
477    return float4(color, 1);
478}