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 | |
| 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')
| -rw-r--r-- | examples/model-viewer/cube.mtl | 35 | ||||
| -rw-r--r-- | examples/model-viewer/cube.obj | 33 | ||||
| -rw-r--r-- | examples/model-viewer/main.cpp | 1216 | ||||
| -rw-r--r-- | examples/model-viewer/shaders.slang | 413 |
4 files changed, 1393 insertions, 304 deletions
diff --git a/examples/model-viewer/cube.mtl b/examples/model-viewer/cube.mtl index 6634af823..6c8eeb10b 100644 --- a/examples/model-viewer/cube.mtl +++ b/examples/model-viewer/cube.mtl @@ -1,8 +1,35 @@ -newmtl Material -Ns 96.078431 +newmtl Red +Ns 95 Ka 0.000000 0.000000 0.000000 -Kd 0.640000 0.640000 0.640000 -Ks 0.500000 0.500000 0.500000 +Kd 0.640000 0.30000 0.30000 +Ks 0.500000 0.200000 0.200000 +Ni 1.000000 +d 1.000000 +illum 2 + +newmtl Green +Ns 20 +Ka 0.000000 0.000000 0.000000 +Kd 0.20000 0.640000 0.20000 +Ks 0.100000 0.500000 0.100000 +Ni 1.000000 +d 1.000000 +illum 2 + +newmtl Blue +Ns 200 +Ka 0.000000 0.000000 0.000000 +Kd 0.10000 0.10000 0.20000 +Ks 0.200000 0.200000 0.700000 +Ni 1.000000 +d 1.000000 +illum 2 + +newmtl Ground +Ns 10 +Ka 0.000000 0.000000 0.000000 +Kd 0.25 0.25 0.25 +Ks 0.1 0.1 0.1 Ni 1.000000 d 1.000000 illum 2 diff --git a/examples/model-viewer/cube.obj b/examples/model-viewer/cube.obj index 7226aaa77..2f7de8a92 100644 --- a/examples/model-viewer/cube.obj +++ b/examples/model-viewer/cube.obj @@ -14,11 +14,30 @@ vn 1.000000 0.000000 0.000000 vn 0.000000 0.000000 1.000000 vn -1.000000 0.000000 0.000000 vn 0.000000 0.000000 -1.000000 -usemtl Material + +v -10 -1 -10 +v 10 -1 -10 +v 10 -1 10 +v -10 -1 10 +vn 0 1 0 + +usemtl Red s off -f 1//1 2//1 3//1 4//1 -f 5//2 8//2 7//2 6//2 -f 1//3 5//3 6//3 2//3 -f 2//4 6//4 7//4 3//4 -f 3//5 7//5 8//5 4//5 -f 5//6 1//6 4//6 8//6 +f 2//3 6//3 5//3 1//3 +f 4//5 8//5 7//5 3//5 + +usemtl Green +s off +f 4//1 3//1 2//1 1//1 +f 6//2 7//2 8//2 5//2 + +usemtl Blue +s off +f 3//4 7//4 6//4 2//4 +f 8//6 4//6 1//6 5//6 + +o Ground +usemtl Ground +s off +f 9//7 10//7 11//7 12//7 + diff --git a/examples/model-viewer/main.cpp b/examples/model-viewer/main.cpp index b32d4166c..be2e6878f 100644 --- a/examples/model-viewer/main.cpp +++ b/examples/model-viewer/main.cpp @@ -28,7 +28,10 @@ using namespace gfx; // just to keep the code short. Note that the Slang API does // not use or require any C++ standard library features. // +#include <map> #include <memory> +#include <string> +#include <sstream> #include <vector> // A larger application will typically want to load/compile @@ -347,6 +350,9 @@ RefPtr<Program> loadProgram(ShaderModule* module, char const* entryPoint0, char // similar to those in Vulkan, and then maps these down to efficient alternatives // on other APIs including D3D12, D3D11, and OpenGL. // +// Before we dive into the definition of the application's `ParameterBlock` type, +// we will start with some underlying types. +// // Every parameter block is allocated based on a particular layout, and we // can share the same layout across multiple blocks: // @@ -357,6 +363,10 @@ struct ParameterBlockLayout : RefObject // RefPtr<gfx::Renderer> renderer; + // The name of the type, as it appears in Slang code. + // + std::string typeName; + // The Slang type layout information that will be used to decide // how much space is needed in instances of this layout. // @@ -452,11 +462,178 @@ RefPtr<ParameterBlockLayout> getParameterBlockLayout( RefPtr<ParameterBlockLayout> parameterBlockLayout = new ParameterBlockLayout(); parameterBlockLayout->renderer = renderer; parameterBlockLayout->primaryConstantBufferSize = primaryConstantBufferSize; + parameterBlockLayout->typeName = name; parameterBlockLayout->slangTypeLayout = typeLayout; parameterBlockLayout->descriptorSetLayout = descriptorSetLayout; return parameterBlockLayout; } +// +// In some cases, we may want to create a parameter block based +// on a *generic* type in the shader code (e.g., `LightPair<A,B>`). +// +// The current Slang API re-uses the `findTypeByName()` operation to +// support specialization of types, by allowing the user to pass in +// the string name of a sepcialized type and have the Slang runtime +// system parse it. +// +// Note: a future version of the Slang API may streamline this operation +// so that less application code is needed. +// +// In order to construct the string name of a type like `LightArray<X,3>` +// we need a uniform encoding of the generic *arguments* `X` and `3`. +// We use the `SpecializationArg` for this: +// +struct SpecializationArg +{ + // A `SpecializationArg` is just a thing wrapper around a string, + // with support for implicit conversions from the values we might + // use as specialization arguments. + + SpecializationArg(Int val) + { + str = std::to_string(val); + } + SpecializationArg(RefPtr<ParameterBlockLayout> layout) + { + str = layout->typeName; + } + + std::string str; +}; +// +// Now, given the name of a type to specialize and its specialization +// arguments, we can easily construct the string name of the specialized +// type and defer to the existing `getParameterBlockLayout()`. +// +RefPtr<ParameterBlockLayout> getSpecializedParameterBlockLayout( + ShaderModule* module, + char const* name, + Int argCount, + SpecializationArg const* args) +{ + std::stringstream stream; + stream << name << "<"; + for (Int aa = 0; aa < argCount; ++aa) + { + if (aa != 0) stream << ","; + stream << args[aa].str; + } + stream << ">"; + + std::string specializedName = stream.str(); + return getParameterBlockLayout(module, specializedName.c_str()); +} +RefPtr<ParameterBlockLayout> getSpecializedParameterBlockLayout( + ShaderModule* module, + char const* name, + SpecializationArg const& arg0, + SpecializationArg const& arg1) +{ + SpecializationArg args[] = { arg0, arg1 }; + return getSpecializedParameterBlockLayout(module, name, 2, args); +} + +// In order to allow parameter blocks to be filled in conveniently, +// we will introduce a helper type for "encoding" parameter blocks +// (those familiar with the Metal API may recognize a similarity +// to the `MTLArgumentEncoder` type). +// +struct ParameterBlockEncoder +{ + // The parameter block being filled in (if this is + // a "top-level" encoder. + // + struct ParameterBlock* parameterBlock = nullptr; + // A top-level encoder will unmap the underlying constant + // buffer (if any) when it goes out of scope. + // + ~ParameterBlockEncoder(); + + // The underlying descriptor set being filled in. + // + gfx::DescriptorSet* descriptorSet = nullptr; + + // The Slang type information for the part of the + // block that we are filling in. This might be the + // type stored in the whole block, the type of a single + // field, or anything in between. + // + slang::TypeLayoutReflection* slangTypeLayout = nullptr; + + // A pointer to the uniform data for the (sub)block + // being filled in, as well as offsets for the resource + // binding ranges. + // + char* uniformData = nullptr; + Int rangeOffset = 0; + Int rangeArrayIndex = 0; + + // Assuming we have an encoder for a `struct` type, + // return an encoder for a single field by its index. + // + ParameterBlockEncoder beginField(Int fieldIndex) + { + assert(slangTypeLayout->getKind() == slang::TypeReflection::Kind::Struct); + + auto slangField = slangTypeLayout->getFieldByIndex(fieldIndex); + auto fieldUniformOffset = slangField->getOffset(); + + // TODO: this type needs to be extended to handle resource fields. + size_t fieldRangeOffset = 0; + + ParameterBlockEncoder subEncoder; + subEncoder.descriptorSet = descriptorSet; + subEncoder.slangTypeLayout = slangField->getTypeLayout(); + subEncoder.uniformData = uniformData + fieldUniformOffset; + subEncoder.rangeOffset = rangeOffset + fieldRangeOffset; + subEncoder.rangeArrayIndex = rangeArrayIndex; + return subEncoder; + } + + // Assuming we have an encoder for an array type, return an + // encoder for an element of that array. + // + ParameterBlockEncoder beginArrayElement(Int index) + { + assert(slangTypeLayout->getKind() == slang::TypeReflection::Kind::Array); + + auto uniformStride = slangTypeLayout->getElementStride(slang::ParameterCategory::Uniform); + auto slangElementTypeLayout = slangTypeLayout->getElementTypeLayout(); + + ParameterBlockEncoder subEncoder; + subEncoder.descriptorSet = descriptorSet; + subEncoder.slangTypeLayout = slangElementTypeLayout; + subEncoder.uniformData = uniformData + index * uniformStride; + subEncoder.rangeOffset = rangeOffset; + subEncoder.rangeArrayIndex = index; + return subEncoder; + } + + // Write uniform data into this encoder. + // + void writeUniform(const void* data, size_t dataSize) + { + memcpy(uniformData, data, dataSize); + } + template<typename T> + void write(T const& value) + { + writeUniform(&value, sizeof(value)); + } + + // As a convenience, create a sub-encoder for a single field, + // and write a single value into it. + // + template<typename T> + void writeField(Int fieldIndex, T const& value) + { + beginField(fieldIndex).write(value); + } +}; + +// With the layout and encoder types dealt with, we are now +// prepared to // A `ParameterBlock` abstracts over the allocated storage // for a descriptor set, based on some `ParameterBlockLayout` // @@ -477,15 +654,7 @@ struct ParameterBlock : RefObject // for any resource fields. RefPtr<gfx::DescriptorSet> descriptorSet; - // Map/unmap operations are provided to access the - // contents of the primary constant buffer. - void* map(); - void unmap(); - - // A a convenience, `pb->mapAs<X>()` map be used as - // a declaration of intent, instead of `(X*) pb->map()` - template<typename T> - T* mapAs() { return (T*)map(); } + ParameterBlockEncoder beginEncoding(); }; // Allocating a parameter block is mostly a matter of allocating @@ -554,182 +723,36 @@ RefPtr<ParameterBlock> allocateTransientParameterBlock( return allocateParameterBlockImpl(layout); } -// As described earlier, it is convenient to be able -// to easily map the primary constant buffer of a parameter -// block, since this will hold the values for any ordinary fields. +// In order to fill in a parameter block, the application +// will create an encoder pointing at the mapped uniform +// data for the block: // -void* ParameterBlock::map() +ParameterBlockEncoder ParameterBlock::beginEncoding() { - return renderer->map( - primaryConstantBuffer, - MapFlavor::WriteDiscard); + ParameterBlockEncoder encoder; + encoder.parameterBlock = this; + encoder.descriptorSet = descriptorSet; + encoder.slangTypeLayout = layout->slangTypeLayout; + encoder.uniformData = primaryConstantBuffer ? + (char*) renderer->map( + primaryConstantBuffer, + MapFlavor::WriteDiscard) + : nullptr; + encoder.rangeOffset = 0; + encoder.rangeArrayIndex = 0; + return encoder; } -void ParameterBlock::unmap() -{ - renderer->unmap(primaryConstantBuffer); -} - -// Our application code has a rudimentary material system, -// to match the `IMaterial` abstraction used in the shade code. -// -struct Material : RefObject -{ - // The key feature of a matrial in our application is that - // it can provide a parameter block that describes it and - // its parameters. The contents of the parameter block will - // be any colors, textures, etc. that the material needs, - // while the Slang type that was used to allocate the - // block will be an implementation of `IMaterial` that - // provides the evaluation logic for the material. - - // Each subclass of `Material` will provide a routine to - // create a parameter block of its chosen type/layout. - virtual RefPtr<ParameterBlock> createParameterBlock() = 0; - // The parameter block for a material will be stashed here - // after it is created. - RefPtr<ParameterBlock> parameterBlock; -}; - -// For now we have only a single implementation of `Material`, -// which corresponds to the `SimpleMaterial` type in our shader -// code. +// When a "top-level" encoder goes out of scope, +// we need to unmap the parameter block. // -struct SimpleMaterial : Material +ParameterBlockEncoder::~ParameterBlockEncoder() { - // The `SimpleMaterial` shader type has only uniform data, - // so we declare a `struct` type for that data here. - struct Uniforms - { - glm::vec3 diffuseColor; - float pad; - }; - Uniforms uniforms; - - // When asked to create a parameter block, the `SimpleMaterial` - // type will allocate a block based on the corresponding - // shader type, and fill it in based on the data in the C++ - // object. - // - RefPtr<ParameterBlock> createParameterBlock() override + if (parameterBlock && uniformData) { - auto parameterBlockLayout = gParameterBlockLayout; - auto parameterBlock = allocatePersistentParameterBlock( - parameterBlockLayout); - - if(auto u = parameterBlock->mapAs<Uniforms>()) - { - *u = uniforms; - parameterBlock->unmap(); - } - - return parameterBlock; + parameterBlock->renderer->unmap( + parameterBlock->primaryConstantBuffer); } - - // We cache the corresponding parameter block layout for - // `SimpleMaterial` in a static variable so that we don't - // load it more than once. - // - static RefPtr<ParameterBlockLayout> gParameterBlockLayout; -}; -RefPtr<ParameterBlockLayout> SimpleMaterial::gParameterBlockLayout; - -// With the `Material` abstraction defined, we can go on to define -// the representation for loaded models that we will use. -// -// A `Model` will own vertex/index buffers, along with a list of meshes, -// while each `Mesh` will own a material and a range of indices. -// For this example we will be loading models from `.obj` files, but -// that is just a simple lowest-common-denominator choice. -// -struct Mesh : RefObject -{ - RefPtr<Material> material; - int firstIndex; - int indexCount; -}; -struct Model : RefObject -{ - typedef ModelLoader::Vertex Vertex; - - RefPtr<BufferResource> vertexBuffer; - RefPtr<BufferResource> indexBuffer; - PrimitiveTopology primitiveTopology; - int vertexCount; - int indexCount; - std::vector<RefPtr<Mesh>> meshes; -}; -// -// Loading a model from disk is done with the help of some utility -// code for parsing the `.obj` file format, so that the application -// mostly just registers some callbacks to allocate the objects -// used for its representation. -// -RefPtr<Model> loadModel( - Renderer* renderer, - char const* inputPath, - ModelLoader::LoadFlags loadFlags = 0, - float scale = 1.0f) -{ - // The model loading interface using a C++ interface of - // callback functions to handle creating the application-specific - // representation of meshes, materials, etc. - // - struct Callbacks : ModelLoader::ICallbacks - { - void* createMaterial(MaterialData const& data) override - { - SimpleMaterial* material = new SimpleMaterial(); - material->uniforms.diffuseColor = data.diffuseColor; - - material->parameterBlock = material->createParameterBlock(); - - return material; - } - - void* createMesh(MeshData const& data) override - { - Mesh* mesh = new Mesh(); - mesh->firstIndex = data.firstIndex; - mesh->indexCount = data.indexCount; - mesh->material = (Material*)data.material; - return mesh; - } - - void* createModel(ModelData const& data) override - { - Model* model = new Model(); - model->vertexBuffer = data.vertexBuffer; - model->indexBuffer = data.indexBuffer; - model->primitiveTopology = data.primitiveTopology; - model->vertexCount = data.vertexCount; - model->indexCount = data.indexCount; - - int meshCount = data.meshCount; - for(int ii = 0; ii < meshCount; ++ii) - model->meshes.push_back((Mesh*)data.meshes[ii]); - - return model; - } - }; - Callbacks callbacks; - - // We instantiate a model loader object and then use it to - // try and load a model from the chosen path. - // - ModelLoader loader; - loader.renderer = renderer; - loader.loadFlags = loadFlags; - loader.scale = scale; - loader.callbacks = &callbacks; - Model* model = nullptr; - if(SLANG_FAILED(loader.load(inputPath, (void**)&model))) - { - log("failed to load '%s'\n", inputPath); - return nullptr; - } - - return model; } // The core of our application's rendering abstraction is @@ -841,7 +864,7 @@ RefPtr<EffectVariant> createEffectVaraint( for(auto gp : program->genericParams) { int parameterBlockIndex = gp.parameterBlockIndex; - auto typeName = parameterBlockLayouts[parameterBlockIndex]->slangTypeLayout->getName(); + auto typeName = parameterBlockLayouts[parameterBlockIndex]->typeName.c_str(); genericArgs.push_back(typeName); } @@ -1005,13 +1028,6 @@ struct ShaderCache : RefObject // The shader cache is mostly just a dictionary mapping // variant keys to the associated variant, generated on-demand. // - // TODO: A more advanced application might support removing - // entries from the shader cache when effects get unloaded, - // or in order to respond to operations like a "hot reload" - // key in a development build (e.g., just clear the - // cache of variants and allow the ordinary loading logic - // to re-populate it). - // Dictionary<VariantKey, RefPtr<EffectVariant> > variants; // Getting a variant is just a matter of looking for an @@ -1033,6 +1049,16 @@ struct ShaderCache : RefObject variants.Add(key, variant); return variant; } + + // We support clearign the shader cache, which can serve + // as a kind of "hot reload" action, because subsequent + // rendering work will need to re-compile shader variants + // from scratch. + // + void clear() + { + variants.Clear(); + } }; @@ -1255,6 +1281,623 @@ public: } }; +// +// The above types represent a core set of abstractions for working +// with rendering effects and their parameters, while performing +// static specialization to maintain GPU efficiency. +// +// We will now turn our attention to application-side abstractions +// for lights and materials that will match up with our shader-side +// interface definitions. +// +// For example, our application code has a rudimentary material system, +// to match the `IMaterial` abstraction used in the shade code. +// +struct Material : RefObject +{ + // The key feature of a matrial in our application is that + // it can provide a parameter block that describes it and + // its parameters. The contents of the parameter block will + // be any colors, textures, etc. that the material needs, + // while the Slang type that was used to allocate the + // block will be an implementation of `IMaterial` that + // provides the evaluation logic for the material. + + // Each subclass of `Material` will provide a routine to + // create a parameter block of its chosen type/layout. + virtual RefPtr<ParameterBlock> createParameterBlock() = 0; + + // The parameter block for a material will be stashed here + // after it is created. + RefPtr<ParameterBlock> parameterBlock; +}; + +// For now we have only a single implementation of `Material`, +// which corresponds to the `SimpleMaterial` type in our shader +// code. +// +struct SimpleMaterial : Material +{ + glm::vec3 diffuseColor; + glm::vec3 specularColor; + float specularity; + + // When asked to create a parameter block, the `SimpleMaterial` + // type will allocate a block based on the corresponding + // shader type, and fill it in based on the data in the C++ + // object. + // + RefPtr<ParameterBlock> createParameterBlock() override + { + auto parameterBlockLayout = gParameterBlockLayout; + auto parameterBlock = allocatePersistentParameterBlock( + parameterBlockLayout); + + ParameterBlockEncoder encoder = parameterBlock->beginEncoding(); + encoder.writeField(0, diffuseColor); + encoder.writeField(1, specularColor); + encoder.writeField(2, specularity); + + return parameterBlock; + } + + // We cache the corresponding parameter block layout for + // `SimpleMaterial` in a static variable so that we don't + // load it more than once. + // + static RefPtr<ParameterBlockLayout> gParameterBlockLayout; +}; +RefPtr<ParameterBlockLayout> SimpleMaterial::gParameterBlockLayout; + +// With the `Material` abstraction defined, we can go on to define +// the representation for loaded models that we will use. +// +// A `Model` will own vertex/index buffers, along with a list of meshes, +// while each `Mesh` will own a material and a range of indices. +// For this example we will be loading models from `.obj` files, but +// that is just a simple lowest-common-denominator choice. +// +struct Mesh : RefObject +{ + RefPtr<Material> material; + int firstIndex; + int indexCount; +}; +struct Model : RefObject +{ + typedef ModelLoader::Vertex Vertex; + + RefPtr<BufferResource> vertexBuffer; + RefPtr<BufferResource> indexBuffer; + PrimitiveTopology primitiveTopology; + int vertexCount; + int indexCount; + std::vector<RefPtr<Mesh>> meshes; +}; +// +// Loading a model from disk is done with the help of some utility +// code for parsing the `.obj` file format, so that the application +// mostly just registers some callbacks to allocate the objects +// used for its representation. +// +RefPtr<Model> loadModel( + Renderer* renderer, + char const* inputPath, + ModelLoader::LoadFlags loadFlags = 0, + float scale = 1.0f) +{ + // The model loading interface using a C++ interface of + // callback functions to handle creating the application-specific + // representation of meshes, materials, etc. + // + struct Callbacks : ModelLoader::ICallbacks + { + void* createMaterial(MaterialData const& data) override + { + SimpleMaterial* material = new SimpleMaterial(); + material->diffuseColor = data.diffuseColor; + material->specularColor = data.specularColor; + material->specularity = data.specularity; + + material->parameterBlock = material->createParameterBlock(); + + return material; + } + + void* createMesh(MeshData const& data) override + { + Mesh* mesh = new Mesh(); + mesh->firstIndex = data.firstIndex; + mesh->indexCount = data.indexCount; + mesh->material = (Material*)data.material; + return mesh; + } + + void* createModel(ModelData const& data) override + { + Model* model = new Model(); + model->vertexBuffer = data.vertexBuffer; + model->indexBuffer = data.indexBuffer; + model->primitiveTopology = data.primitiveTopology; + model->vertexCount = data.vertexCount; + model->indexCount = data.indexCount; + + int meshCount = data.meshCount; + for (int ii = 0; ii < meshCount; ++ii) + model->meshes.push_back((Mesh*)data.meshes[ii]); + + return model; + } + }; + Callbacks callbacks; + + // We instantiate a model loader object and then use it to + // try and load a model from the chosen path. + // + ModelLoader loader; + loader.renderer = renderer; + loader.loadFlags = loadFlags; + loader.scale = scale; + loader.callbacks = &callbacks; + Model* model = nullptr; + if (SLANG_FAILED(loader.load(inputPath, (void**)&model))) + { + log("failed to load '%s'\n", inputPath); + return nullptr; + } + + return model; +} + +// Along with materials, our application needs to be able to represent +// multiple light sources in the scene. For this task we will use a C++ +// inheritance hierarchy rooted at `Light` to match the `ILight` +// interface in Slang. +// +// Unlike how materials are currently being handled, we will use a +// quick-and-dirty "RTTI" system for lights to allow some of the application +// code to abstract over particular light types. +// +struct Light; +struct LightType +{ + // A light type needs to know both the name of the type (e.g., so that + // we can load shader code), and must also provide a factory function + // to create lights on demand (e.g., when the user requests that one + // be added in a UI). + // + char const* name; + Light* (*createLight)(); +}; +// +// The following is some crud to bootstrap the rudimentary RTTI system +// for lights. Each concrete subclass of `Light` needs to use the +// `DEFINE_LIGHT_TYPE` macro to set up its RTTI info. +// +template<typename T> +struct LightTypeImpl +{ + static LightType type; + static Light* create() { return (Light*)(new T); } +}; +#define DEFINE_LIGHT_TYPE(NAME) \ + LightType LightTypeImpl<NAME>::type = { #NAME, &LightTypeImpl<NAME>::create }; +template<typename T> +LightType* getLightType() +{ + return &LightTypeImpl<T>::type; +} + +struct Light : RefObject +{ + // A light must be able to return its type information. + virtual LightType* getType() = 0; + + // A light must be able to write a representation of itself into + // a parameter block, or a part of one. + virtual void fillInParameterBlock(ParameterBlockEncoder& encoder) = 0; + + // For this application, a light must be able to present a user + // interface for people to modify its properties. + virtual void doUI() = 0; +}; + +// We will provide two nearly trivial implementations of `Light` for now, +// to show the kind of application code needed to line up with the corresponding +// types defined in the Slang shader code for this application. + +struct DirectionalLight : Light +{ + glm::vec3 direction = normalize(glm::vec3(1)); + glm::vec3 color = glm::vec3(1); + float intensity = 1; + + LightType* getType() override { return getLightType<DirectionalLight>(); }; + + void fillInParameterBlock(ParameterBlockEncoder& encoder) override + { + encoder.writeField(0, direction); + encoder.writeField(1, color*intensity); + } + + void doUI() override + { + if (ImGui::SliderFloat3("direction", &direction[0], -1, 1)) + { + direction = normalize(direction); + } + ImGui::ColorEdit3("color", &color[0]); + ImGui::DragFloat("intensity", &intensity, 1.0f, 0.0f, 10000.0f, "%.3f", 2.0f); + } +}; +DEFINE_LIGHT_TYPE(DirectionalLight); + +struct PointLight : Light +{ + glm::vec3 position = glm::vec3(0); + glm::vec3 color = glm::vec3(1); + float intensity = 10; + + LightType* getType() override { return getLightType<PointLight>(); }; + + void fillInParameterBlock(ParameterBlockEncoder& encoder) override + { + encoder.writeField(0, position); + encoder.writeField(1, color*intensity); + } + + void doUI() override + { + ImGui::DragFloat3("position", &position[0], 0.1f); + ImGui::ColorEdit3("color", &color[0]); + ImGui::DragFloat("intensity", &intensity, 1.0f, 0.0f, 10000.0f, "%.3f", 2.0f); + } +}; +DEFINE_LIGHT_TYPE(PointLight); + +// Rendering is usually done with collections of lights rather than single +// lights. This application will use a concept of "light environments" to +// group together lights for rendering. +// +// We want to be *able* to specialize our shader code based on the particular +// types of lights in a scene, but we also do not want to over-specialize +// and, e.g., use differnt specialized shaders for a scene with 99 point +// lights vs. 100. +// +// This particular application will use a notion of a "layout" for a lighting +// environment, which specifies the allowed types of lights, and the maximum +// number of lights of each type. Different lighting environment layouts +// will yield different specialized code. + +struct LightEnvLayout : public RefObject +{ + // Our lighting environment layout will track layout + // information for several different arrays: one + // for each supported light type. + // + struct LightArrayLayout : RefObject + { + LightType* type; + RefPtr<ParameterBlockLayout> lightLayout; + RefPtr<ParameterBlockLayout> arrayLayout; + Int maximumCount = 0; + }; + RefPtr<ShaderModule> module; + std::vector<RefPtr<LightArrayLayout>> lightArrayLayouts; + std::map<LightType*, Int> mapLightTypeToArrayIndex; + + LightEnvLayout(ShaderModule* module) + : module(module) + {} + + void addLightType(LightType* type, Int maximumCount) + { + Int arrayIndex = (Int)lightArrayLayouts.size(); + RefPtr<LightArrayLayout> layout = new LightArrayLayout(); + layout->type = type; + layout->lightLayout = ::getParameterBlockLayout(module, type->name); + layout->maximumCount = maximumCount; + + // When the user adds a light type `X` to a light-env layout, + // we need to compute the corresponding Slang type and + // layout information to use. If only a single light is + // supported, this will just be the type `X`, while for + // any other count this will be a `LightArray<X, maximumCount>` + // + if (maximumCount <= 1) + { + layout->arrayLayout = layout->lightLayout; + } + else + { + layout->arrayLayout = getSpecializedParameterBlockLayout( + module, "LightArray", layout->lightLayout, maximumCount); + } + + lightArrayLayouts.push_back(layout); + mapLightTypeToArrayIndex.insert(std::make_pair(type, arrayIndex)); + } + template<typename T> + void addLightType(Int maximumCount) + { + addLightType(getLightType<T>(), maximumCount); + } + + Int getArrayIndexForType(LightType* type) + { + auto iter = mapLightTypeToArrayIndex.find(type); + if (iter != mapLightTypeToArrayIndex.end()) + return iter->second; + + return -1; + } + + // We will compute a parameter block layout for the + // whole lighting environment on demand, and then + // cache it thereafter. + // + RefPtr<ParameterBlockLayout> parameterBlockLayout; + RefPtr<ParameterBlockLayout> getParameterBlockLayout() + { + if (!parameterBlockLayout) + { + parameterBlockLayout = computeParameterBlockLayout(); + } + return parameterBlockLayout; + } + + RefPtr<ParameterBlockLayout> computeParameterBlockLayout() + { + // Given a lighting environment with N light types: + // + // L0, L1, ... LN + // + // We want to compute the Slang type: + // + // LightPair<L0, LightPair<L1, ... LightPair<LN-1, LN>>> + // + // This is most easily accomplished by doing a "fold" while + // walking the array in reverse order. + + RefPtr<ParameterBlockLayout> envLayout; + + auto arrayCount = lightArrayLayouts.size(); + for (size_t ii = arrayCount; ii--;) + { + auto arrayInfo = lightArrayLayouts[ii]; + RefPtr<ParameterBlockLayout> arrayLayout = arrayInfo->arrayLayout; + + if (!envLayout) + { + // The is the right-most entry, so it is the base case for our "fold" + envLayout = arrayLayout; + } + else + { + // Fold one entry: `envLayout = LightPair<a, envLayout>` + envLayout = getSpecializedParameterBlockLayout( + module, "LightPair", arrayLayout, envLayout); + } + } + + if (!envLayout) + { + // Handle the special case of *zero* light types. + envLayout = ::getParameterBlockLayout(module, "EmptyLightEnv"); + } + + return envLayout; + } +}; + +// A `LightEnv` follows the structure of a `LightEnvLayout`, +// and provides storage for zero or more lights of various +// different types (up to the limits imposed by the layout). +// +struct LightEnv : public RefObject +{ + // A light environment is always created from a fixed layout + // in this application, so the constructor allocates an array + // for the per-light-type data. + // + // A more complex example might dynamically determine the + // layout based on the number of lights of each type active + // in the scene, with some quantization applied to avoid + // generating too many shader specializations. + // + // Note: the kind of specialization going on here would also + // be applicable to a deferred or "forward+" renderer, insofar + // as it sets the bounds on the total set of lights for + // a scene/frame, while per-tile/-cluster light lists would + // probably just be indices into the global structure. + // + RefPtr<LightEnvLayout> layout; + LightEnv(RefPtr<LightEnvLayout> layout) + : layout(layout) + { + for (auto arrayLayout : layout->lightArrayLayouts) + { + RefPtr<LightArray> lightArray = new LightArray(); + lightArray->layout = arrayLayout; + lightArrays.push_back(lightArray); + } + } + + // For each light type, we track the layout information, + // plus the list of active lights of that type. + // + struct LightArray : RefObject + { + RefPtr<LightEnvLayout::LightArrayLayout> layout; + std::vector<RefPtr<Light>> lights; + }; + std::vector<RefPtr<LightArray>> lightArrays; + + RefPtr<LightArray> getArrayForType(LightType* type) + { + auto index = layout->getArrayIndexForType(type); + return lightArrays[index]; + } + + void add(RefPtr<Light> light) + { + auto array = getArrayForType(light->getType()); + array->lights.push_back(light); + } + + virtual void doUI() + { + if (ImGui::Button("Add")) + { + ImGui::OpenPopup("AddLight"); + } + if (ImGui::BeginPopup("AddLight")) + { + for (auto array : lightArrays) + { + if (ImGui::MenuItem( + array->layout->type->name, + nullptr, + nullptr, + array->lights.size() < (size_t)array->layout->maximumCount)) + { + auto light = array->layout->type->createLight(); + array->lights.push_back(light); + } + } + ImGui::EndPopup(); + } + + for (auto array : lightArrays) + { + auto lightCount = array->lights.size(); + auto maxLightCount = array->layout->maximumCount; + if (ImGui::TreeNode( + array.Ptr(), + "%s (%d/%d)", + array->layout->type->name, + (int)lightCount, + (int)maxLightCount)) + { + size_t lightCounter = 0; + for (auto light : array->lights) + { + size_t lightIndex = lightCounter++; + if (ImGui::TreeNode(light.Ptr(), "%d", (int)lightIndex)) + { + light->doUI(); + ImGui::TreePop(); + } + } + ImGui::TreePop(); + } + } + } + + // Because the lighting environment will often change between frames, + // we will not try to optimize for the case where it doesn't change, + // and will instead fill in a "transient" parameter block from + // scratch every frame. + // + RefPtr<ParameterBlock> createParameterBlock() + { + auto parameterBlockLayout = layout->getParameterBlockLayout(); + auto parameterBlock = allocateTransientParameterBlock(parameterBlockLayout); + + ParameterBlockEncoder encoder = parameterBlock->beginEncoding(); + fillInParameterBlock(encoder); + + return parameterBlock; + } + void fillInParameterBlock(ParameterBlockEncoder& inEncoder) + { + // When filling in the parameter block for a lighting + // environment, we mostly follow the structure of + // the type that was computed by the `LightEnvLayout`: + // + // LightPair<A, LightPair<B, ... LightPair<Y, Z>>> + // + // we will keep `encoder` pointed at the "spine" of this + // structure (so at an element that represents a `LightPair`, + // except for the special case of the last item like `Z` above). + // + // For each light type, we will then encode the data as + // needed for the light type (`A` then `B` then ...) + // + auto encoder = inEncoder; + size_t lightTypeCount = lightArrays.size(); + for (size_t tt = 0; tt < lightTypeCount; ++tt) + { + // The encoder for the very last item will + // just be the one on the "spine" of the list. + auto lightTypeEncoder = encoder; + if (tt != lightTypeCount - 1) + { + // In the common case `encoder` is set up + // for writing to a `LightPair<X, Y>` so + // we ant to set up the `lightTypeEncoder` + // for writing to an `X` (which is the first + // field of `LightPair`, and then have + // `encoder` move on to the `Y` (the rest + // of the list of light types). + // + lightTypeEncoder = encoder.beginField(0); + encoder = encoder.beginField(1); + } + + auto& lightTypeArray = lightArrays[tt]; + size_t lightCount = lightTypeArray->lights.size(); + size_t maxLightCount = lightTypeArray->layout->maximumCount; + + // Recall that we are representing the data for a single + // light type `L` as either an instance of type `L` (if + // only a single light is supported), or as an instance + // of the type `LightArray<L,N>`. + // + if (maxLightCount == 1) + { + // This is the case where the maximu number of lights of + // the given type was set as one, so we just have a value + // of type `L`, and can tell the first light in our application-side + // array to encode itself into that location. + + if (lightCount > 0) + { + lightTypeArray->lights[0]->fillInParameterBlock(lightTypeEncoder); + } + else + { + // We really ought to zero out the entry in this case + // (under the assumption that all zeros will represent + // an inactive light). + } + } + else + { + // The more interesting case is when we have a `LightArray<L,N>`, + // in which case we need to encode the first field (the light count)... + // + lightTypeEncoder.writeField<int32_t>(0, lightTypeArray->lights.size()); + // + // ... followed by an array of values of type `L` in the second field. + // We will only write to the first `lightCount` entries, which may be + // less than `N`. We will rely on dynamic looping in the shader to + // not access the entries past that point. + // + ParameterBlockEncoder arrayEncoder = lightTypeEncoder.beginField(1); + for (size_t ii = 0; ii < lightCount; ++ii) + { + lightTypeArray->lights[ii]->fillInParameterBlock(arrayEncoder.beginArrayElement(ii)); + } + } + } + } +}; + +// Now that we've written all the required infrastructure code for +// the application's renderer and shader library, we can move on +// to the main logic. +// // We will again structure our example application as a C++ `struct`, // so that we can scope its allocations for easy cleanup, rather than // use global variables. @@ -1276,9 +1919,12 @@ RefPtr<ParameterBlockLayout> gPerModelParameterBlockLayout; RefPtr<ShaderCache> shaderCache; RefPtr<GUI> gui; -// Most of the application state is stored in the list of loaded models. +// Most of the application state is stored in the list of loaded models, +// as well as the active light source (a single light for now). // -std::vector<RefPtr<Model>> gModels; +std::vector<RefPtr<Model>> gModels; +RefPtr<LightEnv> lightEnv; + // During startup the application will load one or more models and // add them to the `gModels` list. @@ -1296,26 +1942,92 @@ void loadAndAddModel( int gWindowWidth = 1024; int gWindowHeight = 768; -// For this more complex example we will be passing multiple -// parameter blocks into the shader code, and each will -// need its own `struct` type the define the layout of the -// uniform data. +// Our "simulation" state consists of just a few values. // -struct PerView +uint64_t lastTime = 0; + +//glm::vec3 lightDir = normalize(glm::vec3(10, 10, 10)); +//glm::vec3 lightColor = glm::vec3(1, 1, 1); + +glm::vec3 cameraPosition = glm::vec3(1.75, 1.25, 5); +glm::quat cameraOrientation = glm::quat(1, glm::vec3(0)); + +float translationScale = 0.5f; +float rotationScale = 0.025f; + + +// In order to control camera movement, we will +// use good old WASD +bool wPressed = false; +bool aPressed = false; +bool sPressed = false; +bool dPressed = false; + +bool isMouseDown = false; +float lastMouseX; +float lastMouseY; + +void handleEvent(Event const& event) { - glm::mat4x4 viewProjection; + switch( event.code ) + { + case EventCode::KeyDown: + case EventCode::KeyUp: + { + bool isDown = event.code == EventCode::KeyDown; + switch(event.u.key) + { + default: + break; - glm::vec3 lightDir; - float pad0; + case KeyCode::W: wPressed = isDown; break; + case KeyCode::A: aPressed = isDown; break; + case KeyCode::S: sPressed = isDown; break; + case KeyCode::D: dPressed = isDown; break; + } + } + break; - glm::vec3 lightColor; - float pad1; -}; -struct PerModel + case EventCode::MouseDown: + { + isMouseDown = true; + lastMouseX = event.u.mouse.x; + lastMouseY = event.u.mouse.y; + } + break; + + case EventCode::MouseMoved: + { + if( isMouseDown ) + { + float deltaX = event.u.mouse.x - lastMouseX; + float deltaY = event.u.mouse.y - lastMouseY; + + cameraOrientation = glm::rotate(cameraOrientation, -deltaX * rotationScale, glm::vec3(0,1,0)); + cameraOrientation = glm::rotate(cameraOrientation, -deltaY * rotationScale, glm::vec3(1,0,0)); + + cameraOrientation = normalize(cameraOrientation); + + lastMouseX = event.u.mouse.x; + lastMouseY = event.u.mouse.y; + } + } + break; + + case EventCode::MouseUp: + isMouseDown = false; + break; + + default: + break; + } +} + +static void _handleEvent(Event const& event) { - glm::mat4x4 modelTransform; - glm::mat4x4 inverseTransposeModelTransform; -}; + ModelViewer* app = (ModelViewer*) getUserData(event.window); + app->handleEvent(event); +} // The overall initialization logic is quite similar to // the earlier example. The biggest difference is that we @@ -1329,6 +2041,8 @@ Result initialize() windowDesc.title = "Model Viewer"; windowDesc.width = gWindowWidth; windowDesc.height = gWindowHeight; + windowDesc.eventHandler = &_handleEvent; + windowDesc.userData = this; gWindow = createWindow(windowDesc); gRenderer = createD3D11Renderer(); @@ -1415,6 +2129,17 @@ Result initialize() // shaderCache = new ShaderCache(); + // We will create a lighting environment layout that can hold a few point + // and directional lights, and then initialize a lighting environment + // with just a single point light. + // + RefPtr<LightEnvLayout> lightEnvLayout = new LightEnvLayout(shaderModule); + lightEnvLayout->addLightType<PointLight>(10); + lightEnvLayout->addLightType<DirectionalLight>(2); + + lightEnv = new LightEnv(lightEnvLayout); + lightEnv->add(new PointLight()); + // Once we have created all our graphcis API and application resources, // we can start to load models. For now we are keeping things extremely // simple by using a trivial `.obj` file that can be checked into source @@ -1423,7 +2148,7 @@ Result initialize() // Support for loading more interesting/complex models will be added // to this example over time (although model loading is *not* the focus). // - loadAndAddModel("cube.obj", ModelLoader::LoadFlag::FlipWinding); + loadAndAddModel("cube.obj"); // We will do some GUI rendering in this app, using "Dear, IMGUI", // so we need to do the appropriate initialization work here. @@ -1445,47 +2170,48 @@ void renderFrame() // In order to see that things are rendering properly we need some // kind of animation, so we will compute a crude delta-time value here. // - static uint64_t lastTime = getCurrentTime(); + if(!lastTime) lastTime = getCurrentTime(); uint64_t currentTime = getCurrentTime(); - float deltaTime = float(currentTime - lastTime) / float(getTimerFrequency()); + float deltaTime = float(double(currentTime - lastTime) / double(getTimerFrequency())); lastTime = currentTime; // We will use the GLM library to do the matrix math required // to set up our various transformation matrices. // glm::mat4x4 identity = glm::mat4x4(1.0f); - glm::mat4x4 projection = glm::perspective( glm::radians(60.0f), float(gWindowWidth) / float(gWindowHeight), 0.1f, 1000.0f); + // We are implementing a *very* basic 6DOF first-person + // camera movement model. + // + glm::mat3x3 cameraOrientationMat(cameraOrientation); + glm::vec3 forward = -cameraOrientationMat[2]; + glm::vec3 right = cameraOrientationMat[0]; + + glm::vec3 movement = glm::vec3(0); + if(wPressed) movement += forward; + if(sPressed) movement -= forward; + if(aPressed) movement -= right; + if(dPressed) movement += right; + + cameraPosition += deltaTime * translationScale * movement; + glm::mat4x4 view = identity; - view = glm::lookAt( - glm::vec3(4, 5, -5), - glm::vec3(0), - glm::vec3(0, 1, 0)); + view *= glm::mat4x4(inverse(cameraOrientation)); + view = glm::translate(view, -cameraPosition); glm::mat4x4 viewProjection = projection * view; - // We set up a light source with a simple animation applied - // to its direction. - // - glm::vec3 lightDir = normalize(glm::vec3(10, 10, -10)); - glm::vec3 lightColor = glm::vec3(1, 1, 1); - static float angle = 0.0f; - angle += 0.5f * deltaTime; - glm::mat4x4 lightTransform = identity; - lightTransform = rotate(lightTransform, angle, glm::vec3(0, 1, 0)); - lightDir = glm::vec3(lightTransform * glm::vec4(lightDir, 0)); - // Some of the basic rendering setup is identical to the previous example. // + gRenderer->setDepthStencilTarget(gDepthTarget); static const float kClearColor[] = { 0.25, 0.25, 0.25, 1.0 }; gRenderer->setClearColor(kClearColor); gRenderer->clearFrame(); - gRenderer->setDepthStencilTarget(gDepthTarget); gRenderer->setPrimitiveTopology(PrimitiveTopology::TriangleList); // Now we will start in on the more interesting rendering logic, @@ -1510,13 +2236,10 @@ void renderFrame() // auto viewParameterBlock = allocateTransientParameterBlock( gPerViewParameterBlockLayout); - if(auto perView = viewParameterBlock->mapAs<PerView>()) { - perView->viewProjection = viewProjection; - perView->lightDir = lightDir; - perView->lightColor = lightColor; - - viewParameterBlock->unmap(); + auto encoder = viewParameterBlock->beginEncoding(); + encoder.writeField(0, viewProjection); + encoder.writeField(1, cameraPosition); } // // Note: the assignment of indices to parameter blocks is driven @@ -1527,6 +2250,12 @@ void renderFrame() // context.setParameterBlock(0, viewParameterBlock); + // Our `LightEnv` type knows how to turn itself into a parameter + // block, so we just create and bind it here. + // + auto lightEnvParameterBlock = lightEnv->createParameterBlock(); + context.setParameterBlock(2, lightEnvParameterBlock); + // The majority of our rendering logic is handled as a loop // over the models in the scene, and their meshes. // @@ -1550,12 +2279,10 @@ void renderFrame() auto modelParameterBlock = allocateTransientParameterBlock( gPerModelParameterBlockLayout); - if(auto perModel = modelParameterBlock->mapAs<PerModel>()) { - perModel->modelTransform = modelTransform; - perModel->inverseTransposeModelTransform = inverseTransposeModelTransform; - - modelParameterBlock->unmap(); + auto encoder = modelParameterBlock->beginEncoding(); + encoder.writeField(0, modelTransform); + encoder.writeField(1, inverseTransposeModelTransform); } context.setParameterBlock(1, modelParameterBlock); @@ -1580,7 +2307,7 @@ void renderFrame() // is implementing the `IMaterial` interface). // context.setParameterBlock( - 2, + 3, mesh->material->parameterBlock); // Once we've set up all the parameter blocks needed @@ -1595,16 +2322,25 @@ void renderFrame() } } -#if 0 - ImGui::Begin("Model Viewer"); + ImGui::Begin("Slang Model Viewer Example"); ImGui::Text("Average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); - if (ImGui::Button("Load Model")) + if (ImGui::Button("Reload Shaders")) { - // need to do a file open box, and then try to open the resulting file - + shaderCache->clear(); + } + if( ImGui::CollapsingHeader("Lights") ) + { + lightEnv->doUI(); + } + if (ImGui::CollapsingHeader("Camera")) + { + ImGui::InputFloat3("position", &cameraPosition[0]); + ImGui::InputFloat3("orientation[0]", &cameraOrientationMat[0][0]); + ImGui::InputFloat3("orientation[1]", &cameraOrientationMat[1][0]); + ImGui::InputFloat3("orientation[2]", &cameraOrientationMat[2][0]); } + ImGui::End(); -#endif gui->endFrame(); gRenderer->presentFrame(); 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); } |
