summaryrefslogtreecommitdiffstats
path: root/examples/model-viewer
diff options
context:
space:
mode:
authorTim Foley <tfoleyNV@users.noreply.github.com>2018-08-10 22:21:44 -0700
committerGitHub <noreply@github.com>2018-08-10 22:21:44 -0700
commit56d8a752d84e984afab20de5980edf10fe6c06f5 (patch)
treeeb1491b940daebc6afd200202347191d77f76112 /examples/model-viewer
parent73ff6907d723003d30e400f661876e7960de574f (diff)
Improve model-viewer support for lights (#626)
* Improve model-viewer support for lights The main visible change here is that the model-viewer example supports multiple light sources, with a basic UI for adding new light sources to the scene, and for manipulating the ones that are there. Along the way I also refactored the `IMaterial` decomposition to be a bit less naive, while still only including a completely naive Blinn-Phong implementation. I also went ahead and spruced up the `cube.obj` file so that it has multiple materials, although it is still a completely uninteresting asset. * Fixup: Windows SDK version
Diffstat (limited to 'examples/model-viewer')
-rw-r--r--examples/model-viewer/cube.mtl35
-rw-r--r--examples/model-viewer/cube.obj33
-rw-r--r--examples/model-viewer/main.cpp1216
-rw-r--r--examples/model-viewer/shaders.slang413
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);
}