yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
78d34f3b3
master
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}