summaryrefslogtreecommitdiffstats
path: root/examples/model-viewer
diff options
context:
space:
mode:
authorYong He <yonghe@outlook.com>2021-04-16 10:34:26 -0700
committerGitHub <noreply@github.com>2021-04-16 10:34:26 -0700
commit79e92395f8ce3d92c446e3bb3250d19ce33decd5 (patch)
tree2ac277fa299200da72cf03a2b5b96338f66aee5d /examples/model-viewer
parentbad484d838590d0a2aaf1b5b8ac820634af2decb (diff)
Update `model-viewer` example and fixing compiler bugs. (#1795)
Diffstat (limited to 'examples/model-viewer')
-rw-r--r--examples/model-viewer/README.md25
-rw-r--r--examples/model-viewer/cube.mtl35
-rw-r--r--examples/model-viewer/cube.obj31
-rw-r--r--examples/model-viewer/main.cpp922
-rw-r--r--examples/model-viewer/shaders.slang474
5 files changed, 1487 insertions, 0 deletions
diff --git a/examples/model-viewer/README.md b/examples/model-viewer/README.md
new file mode 100644
index 000000000..a350a48a2
--- /dev/null
+++ b/examples/model-viewer/README.md
@@ -0,0 +1,25 @@
+Model Viewer Example
+====================
+
+This example expands on the simple Slang API integration from the "Hello, World" example by actually loading and rendering model data with extremely basic surface and light shading.
+
+This time, the shader code is making use of various Slang language features, so readers may want to read through `shaders.slang` to see an example of how the various mechanisms can be used to build out a more complicated shader library.
+While the shader code in this example is still simplistic, it shows examples of:
+
+* Using multiple Slang `ParameterBlock`s to manage the space of shader parameter bindings in a graphics-API-independent fashion, while still taking advantage of the performance opportunities afforded by D3D12 and Vulkan.
+
+* Using `interface`s and generics to express multiple variations of a feature with static specialization, in place of more traditional preprocessor techniques.
+
+The application code in `main.cpp` also shows a more advanced integration of the Slang API than that in the "Hello, World" example, including examples of:
+
+* Loading a library of Slang shader code to perform reflection on its types *without* specifying a particular entry point to generate code for
+
+* Using Slang's reflection information to allocate graphics-API objects to implement parameter blocks (e.g., D3D12/Vulkan descriptor tables/sets)
+
+* Performing on-demand specialization of Slang's generics using type information from parameter blocks to achieve simple shader specialization
+
+It is perhaps worth taking note of the two things this example intentionally does *not* do:
+
+* There is no use of the C-style preprocessor in the shader code presented, in order to demonstrate that shader specialization can be achieved without preprocessor techniques.
+
+* There is no use of explicit parameter binding decorations (e.g., HLSL `regsiter` or GLSL `layout` modifiers), in order to demonstrate that these are not needed in order to achieve high-performance shader parameter binding.
diff --git a/examples/model-viewer/cube.mtl b/examples/model-viewer/cube.mtl
new file mode 100644
index 000000000..6c8eeb10b
--- /dev/null
+++ b/examples/model-viewer/cube.mtl
@@ -0,0 +1,35 @@
+newmtl Red
+Ns 95
+Ka 0.000000 0.000000 0.000000
+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
new file mode 100644
index 000000000..9213e178b
--- /dev/null
+++ b/examples/model-viewer/cube.obj
@@ -0,0 +1,31 @@
+mtllib cube.mtl
+
+v 0.000000 2.000000 2.000000
+v 0.000000 0.000000 2.000000
+v 2.000000 0.000000 2.000000
+v 2.000000 2.000000 2.000000
+v 0.000000 2.000000 0.000000
+v 0.000000 0.000000 0.000000
+v 2.000000 0.000000 0.000000
+v 2.000000 2.000000 0.000000
+# 8 vertices
+
+g front cube
+usemtl white
+f 1 2 3 4
+g back cube
+# expects white material
+f 8 7 6 5
+g right cube
+usemtl red
+f 4 3 7 8
+g top cube
+usemtl white
+f 5 1 4 8
+g left cube
+usemtl green
+f 5 6 2 1
+g bottom cube
+usemtl white
+f 2 6 7 3
+# 6 elements
diff --git a/examples/model-viewer/main.cpp b/examples/model-viewer/main.cpp
new file mode 100644
index 000000000..3d7a9fe34
--- /dev/null
+++ b/examples/model-viewer/main.cpp
@@ -0,0 +1,922 @@
+// This example is out of date and currently disabled from build.
+// The `gfx` layer has been refactored with a new shader-object model
+// that will greatly simplify shader binding and specialization.
+// This example should be updated to use the shader-object API in `gfx`.
+
+// main.cpp
+
+//
+// This example is much more involved than the `hello-world` example,
+// so readers are encouraged to work through the simpler code first
+// before diving into this application. We will gloss over parts of
+// the code that are similar to the code in `hello-world`, and
+// instead focus on the new code that is required to use Slang in
+// more advanced ways.
+//
+
+// We still need to include the Slang header to use the Slang API
+//
+#include <slang.h>
+#include "slang-com-helper.h"
+
+// We will again make use of a graphics API abstraction
+// layer that implements the shader-object idiom based on Slang's
+// `ParameterBlock` and `interface` features to simplify shader specialization
+// and parameter binding.
+//
+#include "slang-gfx.h"
+#include "tools/gfx-util/shader-cursor.h"
+#include "tools/platform/model.h"
+#include "tools/platform/vector-math.h"
+#include "tools/platform/window.h"
+#include "tools/platform/gui.h"
+#include "examples/example-base/example-base.h"
+
+#include <map>
+#include <sstream>
+
+using namespace gfx;
+using Slang::RefObject;
+using Slang::RefPtr;
+
+struct RendererContext
+{
+ IDevice* device;
+ slang::IModule* shaderModule;
+ slang::ShaderReflection* slangReflection;
+ ComPtr<IShaderProgram> shaderProgram;
+
+ slang::TypeReflection* perViewShaderType;
+ slang::TypeReflection* perModelShaderType;
+
+ Result init(IDevice* inDevice)
+ {
+ device = inDevice;
+ ComPtr<ISlangBlob> diagnostic;
+ shaderModule = device->getSlangSession()->loadModule("shaders", diagnostic.writeRef());
+ diagnoseIfNeeded(diagnostic);
+
+ // Compose the shader program for drawing models by combining the shader module
+ // and entry points ("vertexMain" and "fragmentMain").
+ char const* vertexEntryPointName = "vertexMain";
+ ComPtr<slang::IEntryPoint> vertexEntryPoint;
+ SLANG_RETURN_ON_FAIL(
+ shaderModule->findEntryPointByName(vertexEntryPointName, vertexEntryPoint.writeRef()));
+
+ char const* fragEntryPointName = "fragmentMain";
+ ComPtr<slang::IEntryPoint> fragEntryPoint;
+ SLANG_RETURN_ON_FAIL(
+ shaderModule->findEntryPointByName(fragEntryPointName, fragEntryPoint.writeRef()));
+
+ // At this point we have a few different Slang API objects that represent
+ // pieces of our code: `module`, `vertexEntryPoint`, and `fragmentEntryPoint`.
+ //
+ // A single Slang module could contain many different entry points (e.g.,
+ // four vertex entry points, three fragment entry points, and two compute
+ // shaders), and before we try to generate output code for our target API
+ // we need to identify which entry points we plan to use together.
+ //
+ // Modules and entry points are both examples of *component types* in the
+ // Slang API. The API also provides a way to build a *composite* out of
+ // other pieces, and that is what we are going to do with our module
+ // and entry points.
+ //
+ Slang::List<slang::IComponentType*> componentTypes;
+ componentTypes.add(shaderModule);
+ componentTypes.add(vertexEntryPoint);
+ componentTypes.add(fragEntryPoint);
+
+ // Actually creating the composite component type is a single operation
+ // on the Slang session, but the operation could potentially fail if
+ // something about the composite was invalid (e.g., you are trying to
+ // combine multiple copies of the same module), so we need to deal
+ // with the possibility of diagnostic output.
+ //
+ ComPtr<slang::IComponentType> composedProgram;
+ ComPtr<ISlangBlob> diagnosticsBlob;
+ SlangResult result = device->getSlangSession()->createCompositeComponentType(
+ componentTypes.getBuffer(),
+ componentTypes.getCount(),
+ composedProgram.writeRef(),
+ diagnosticsBlob.writeRef());
+ diagnoseIfNeeded(diagnosticsBlob);
+ SLANG_RETURN_ON_FAIL(result);
+ slangReflection = composedProgram->getLayout();
+
+ // At this point, `composedProgram` represents the shader program
+ // we want to run, and the compute shader there have been checked.
+ // We can create a `gfx::IShaderProgram` object from `composedProgram`
+ // so it may be used by the graphics layer.
+ gfx::IShaderProgram::Desc programDesc = {};
+ programDesc.pipelineType = gfx::PipelineType::Graphics;
+ programDesc.slangProgram = composedProgram.get();
+
+ shaderProgram = device->createProgram(programDesc);
+
+ // Get other shader types that we will use for creating shader objects.
+ perViewShaderType = slangReflection->findTypeByName("PerView");
+ perModelShaderType = slangReflection->findTypeByName("PerModel");
+
+ return SLANG_OK;
+ }
+};
+
+// 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 shader object that describes it and
+ // its parameters. The contents of the shader object 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 shader object that stores its shader parameters.
+ virtual IShaderObject* createShaderObject(RendererContext* context) = 0;
+
+ // The shader object for a material will be stashed here
+ // after it is created.
+ ComPtr<IShaderObject> shaderObject;
+};
+
+// 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;
+
+ // Create a shader object that contains the type info and parameter values
+ // that represent an instance of `SimpleMaterial`.
+ IShaderObject* createShaderObject(RendererContext* context) override
+ {
+ auto program = context->slangReflection;
+ auto shaderType = program->findTypeByName("SimpleMaterial");
+ shaderObject = context->device->createShaderObject(shaderType);
+ gfx::ShaderCursor cursor(shaderObject);
+ cursor["diffuseColor"].setData(&diffuseColor, sizeof(diffuseColor));
+ cursor["specularColor"].setData(&specularColor, sizeof(specularColor));
+ cursor["specularity"].setData(&specularity, sizeof(specularity));
+ return shaderObject.get();
+ }
+};
+
+// 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 platform::ModelLoader::Vertex Vertex;
+
+ ComPtr<IBufferResource> vertexBuffer;
+ ComPtr<IBufferResource> 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(
+ RendererContext* context,
+ char const* inputPath,
+ platform::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 : platform::ModelLoader::ICallbacks
+ {
+ RendererContext* context;
+ void* createMaterial(MaterialData const& data) override
+ {
+ SimpleMaterial* material = new SimpleMaterial();
+ material->diffuseColor = data.diffuseColor;
+ material->specularColor = data.specularColor;
+ material->specularity = data.specularity;
+ material->createShaderObject(context);
+ 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;
+ callbacks.context = context;
+
+ // We instantiate a model loader object and then use it to
+ // try and load a model from the chosen path.
+ //
+ platform::ModelLoader loader;
+ loader.device = context->device;
+ 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.
+
+struct Light : RefObject
+{
+ // A light must be able to create a shader object defining its
+ // corresponding shader type and parameter values.
+ virtual IShaderObject* createShaderObject(RendererContext* context) = 0;
+
+ // Retrieves the shader type for this light object.
+ virtual slang::TypeReflection* getShaderType(RendererContext* context) = 0;
+
+ // The shader object for a light will be stashed here
+ // after it is created.
+ ComPtr<IShaderObject> shaderObject;
+};
+
+// Helper function to retrieve the underlying shader type of `T`.
+template<typename T>
+slang::TypeReflection* getShaderType(RendererContext* context)
+{
+ auto program = context->slangReflection;
+ auto shaderType = program->findTypeByName(T::getTypeName());
+ return shaderType;
+}
+
+// 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 intensity = glm::vec3(1);
+
+ static const char* getTypeName() { return "DirectionalLight"; }
+
+ virtual IShaderObject* createShaderObject(RendererContext* context) override
+ {
+ auto shaderType = ::getShaderType<DirectionalLight>(context);
+ shaderObject = context->device->createShaderObject(shaderType);
+ gfx::ShaderCursor cursor(shaderObject);
+ cursor["direction"].setData(&direction, sizeof(direction));
+ cursor["intensity"].setData(&intensity, sizeof(intensity));
+ return shaderObject.get();
+ }
+
+ virtual slang::TypeReflection* getShaderType(RendererContext* context) override
+ {
+ return ::getShaderType<DirectionalLight>(context);
+ }
+};
+
+struct PointLight : Light
+{
+ glm::vec3 position = glm::vec3(0);
+ glm::vec3 intensity = glm::vec3(1);
+
+ static const char* getTypeName() { return "PointLight"; }
+
+ virtual IShaderObject* createShaderObject(RendererContext* context) override
+ {
+ auto shaderType = ::getShaderType<PointLight>(context);
+ shaderObject = context->device->createShaderObject(shaderType);
+ gfx::ShaderCursor cursor(shaderObject);
+ cursor["position"].setData(&position, sizeof(position));
+ cursor["intensity"].setData(&intensity, sizeof(intensity));
+ return shaderObject.get();
+ }
+
+ virtual slang::TypeReflection* getShaderType(RendererContext* context) override
+ {
+ return ::getShaderType<PointLight>(context);
+ }
+};
+
+// 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
+ {
+ Int maximumCount = 0;
+ std::string typeName;
+ };
+ std::vector<LightArrayLayout> lightArrayLayouts;
+ std::map<slang::TypeReflection*, Int> mapLightTypeToArrayIndex;
+ slang::TypeReflection* shaderType = nullptr;
+
+ void addLightType(RendererContext* context, slang::TypeReflection* lightType, Int maximumCount)
+ {
+ Int arrayIndex = (Int)lightArrayLayouts.size();
+ LightArrayLayout layout;
+ 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.typeName = lightType->getName();
+ }
+ else
+ {
+ auto program = context->slangReflection;
+ std::stringstream typeNameBuilder;
+ typeNameBuilder << "LightArray<" << lightType->getName() << "," << maximumCount
+ << ">";
+ layout.typeName = typeNameBuilder.str();
+ }
+
+ lightArrayLayouts.push_back(layout);
+ mapLightTypeToArrayIndex.insert(std::make_pair(lightType, arrayIndex));
+ }
+
+ template<typename T> void addLightType(RendererContext* context, Int maximumCount)
+ {
+ addLightType(context, getShaderType<T>(context), maximumCount);
+ }
+
+ Int getArrayIndexForType(slang::TypeReflection* lightType)
+ {
+ auto iter = mapLightTypeToArrayIndex.find(lightType);
+ if (iter != mapLightTypeToArrayIndex.end())
+ return iter->second;
+
+ return -1;
+ }
+};
+
+// 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;
+ RendererContext* context;
+ LightEnv(RefPtr<LightEnvLayout> layout, RendererContext* inContext)
+ : layout(layout)
+ , context(inContext)
+ {
+ 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
+ {
+ LightEnvLayout::LightArrayLayout layout;
+ std::vector<RefPtr<Light>> lights;
+ };
+ std::vector<RefPtr<LightArray>> lightArrays;
+
+ RefPtr<LightArray> getArrayForType(slang::TypeReflection* type)
+ {
+ auto index = layout->getArrayIndexForType(type);
+ return lightArrays[index];
+ }
+
+ void add(RefPtr<Light> light)
+ {
+ auto array = getArrayForType(light->getShaderType(context));
+ array->lights.push_back(light);
+ }
+
+ // Get the proper shader type that represents this lighting environment.
+ slang::TypeReflection* getShaderType()
+ {
+ // 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.
+
+ std::string currentEnvTypeName;
+ auto arrayCount = layout->lightArrayLayouts.size();
+ for (size_t ii = arrayCount; ii--;)
+ {
+ auto arrayInfo = layout->lightArrayLayouts[ii];
+
+ if (!currentEnvTypeName.size())
+ {
+ // The is the right-most entry, so it is the base case for our "fold".
+ currentEnvTypeName = arrayInfo.typeName;
+ }
+ else
+ {
+ // Fold one entry: `envLayout = LightPair<a, envLayout>`
+ std::stringstream typeBuilder;
+ typeBuilder << "LightPair<" << arrayInfo.typeName << "," << currentEnvTypeName
+ << ">";
+ currentEnvTypeName = typeBuilder.str();
+ }
+ }
+
+ if (!currentEnvTypeName.size())
+ {
+ // Handle the special case of *zero* light types.
+ currentEnvTypeName = "EmptyLightEnv";
+ }
+ return context->slangReflection->findTypeByName(currentEnvTypeName.c_str());
+ }
+
+ // 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 create a "transient" shader object from
+ // scratch every frame.
+ //
+ ComPtr<IShaderObject> createShaderObject()
+ {
+ auto specializedType = getShaderType();
+
+ auto shaderObject = context->device->createShaderObject(specializedType);
+ ShaderCursor cursor(shaderObject);
+ // When filling in the shader object for a lighting
+ // environment, we mostly follow the structure of
+ // the type that was computed by the `LightEnv::getShaderType`:
+ //
+ // 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 ...)
+ //
+ 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 lightTypeCursor = cursor;
+ 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).
+ //
+ lightTypeCursor = cursor["first"];
+ cursor = cursor["second"];
+ }
+
+ 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)
+ {
+ lightTypeCursor.setObject(
+ lightTypeArray->lights[0]->createShaderObject(context));
+ }
+ 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 fill in the first field (the light count)...
+ //
+ uint32_t lightCount = uint32_t(lightTypeArray->lights.size());
+ lightTypeCursor["count"].setData(&lightCount, sizeof(lightCount));
+ //
+ // ... 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.
+ //
+ auto arrayCursor = lightTypeCursor["lights"];
+ for (size_t ii = 0; ii < lightCount; ++ii)
+ {
+ arrayCursor[ii].setObject(
+ lightTypeArray->lights[ii]->createShaderObject(context));
+ }
+ }
+ }
+ return shaderObject;
+ }
+};
+
+// 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.
+//
+struct ModelViewer : WindowedAppBase
+{
+
+RendererContext context;
+
+// 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;
+RefPtr<LightEnv> lightEnv;
+
+// The pipeline state object we will use to draw models.
+ComPtr<IPipelineState> gPipelineState;
+
+// During startup the application will load one or more models and
+// add them to the `gModels` list.
+//
+void loadAndAddModel(
+ char const* inputPath,
+ platform::ModelLoader::LoadFlags loadFlags = 0,
+ float scale = 1.0f)
+{
+ auto model = loadModel(&context, inputPath, loadFlags, scale);
+ if(!model) return;
+ gModels.push_back(model);
+}
+
+// Our "simulation" state consists of just a few values.
+//
+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 = 0.0f;
+float lastMouseY = 0.0f;
+
+void setKeyState(platform::KeyCode key, bool state)
+{
+ switch (key)
+ {
+ default:
+ break;
+ case platform::KeyCode::W:
+ wPressed = state;
+ break;
+ case platform::KeyCode::A:
+ aPressed = state;
+ break;
+ case platform::KeyCode::S:
+ sPressed = state;
+ break;
+ case platform::KeyCode::D:
+ dPressed = state;
+ break;
+ }
+}
+void onKeyDown(platform::KeyEventArgs args) { setKeyState(args.key, true); }
+void onKeyUp(platform::KeyEventArgs args) { setKeyState(args.key, false); }
+
+void onMouseDown(platform::MouseEventArgs args)
+{
+ isMouseDown = true;
+ lastMouseX = (float)args.x;
+ lastMouseY = (float)args.y;
+}
+
+void onMouseMove(platform::MouseEventArgs args)
+{
+ if (isMouseDown)
+ {
+ float deltaX = args.x - lastMouseX;
+ float deltaY = args.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 = (float)args.x;
+ lastMouseY = (float)args.y;
+ }
+}
+void onMouseUp(platform::MouseEventArgs args)
+{
+ isMouseDown = false;
+}
+
+// The overall initialization logic is quite similar to
+// the earlier example. The biggest difference is that we
+// create instances of our application-specific parameter
+// block layout and effect types instead of just creating
+// raw graphics API objects.
+//
+Result initialize()
+{
+ initializeBase("Model Viewer", 1024, 768);
+ gWindow->events.mouseMove = [this](const platform::MouseEventArgs& e) { onMouseMove(e); };
+ gWindow->events.mouseUp = [this](const platform::MouseEventArgs& e) { onMouseUp(e); };
+ gWindow->events.mouseDown = [this](const platform::MouseEventArgs& e) { onMouseDown(e); };
+ gWindow->events.keyDown = [this](const platform::KeyEventArgs& e) { onKeyDown(e); };
+ gWindow->events.keyUp = [this](const platform::KeyEventArgs& e) { onKeyUp(e); };
+
+ // Initialize `RendererContext`, which loads the shader module from file.
+ SLANG_RETURN_ON_FAIL(context.init(gDevice));
+
+ InputElementDesc inputElements[] = {
+ {"POSITION", 0, Format::RGB_Float32, offsetof(Model::Vertex, position) },
+ {"NORMAL", 0, Format::RGB_Float32, offsetof(Model::Vertex, normal) },
+ {"UV", 0, Format::RG_Float32, offsetof(Model::Vertex, uv) },
+ };
+ auto inputLayout = gDevice->createInputLayout(
+ &inputElements[0],
+ 3);
+ if(!inputLayout) return SLANG_FAIL;
+
+ // Create the pipeline state object for drawing models.
+ GraphicsPipelineStateDesc pipelineStateDesc = {};
+ pipelineStateDesc.program = context.shaderProgram;
+ pipelineStateDesc.framebufferLayout = gFramebufferLayout;
+ pipelineStateDesc.inputLayout = inputLayout;
+ pipelineStateDesc.primitiveType = PrimitiveType::Triangle;
+ pipelineStateDesc.depthStencil.depthFunc = ComparisonFunc::LessEqual;
+ pipelineStateDesc.depthStencil.depthTestEnable = true;
+ gPipelineState = gDevice->createGraphicsPipelineState(pipelineStateDesc);
+
+ // 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();
+ lightEnvLayout->addLightType<PointLight>(&context, 10);
+ lightEnvLayout->addLightType<DirectionalLight>(&context, 2);
+
+ lightEnv = new LightEnv(lightEnvLayout, &context);
+ 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
+ // control.
+ //
+ // 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");
+
+ return SLANG_OK;
+}
+
+// With the setup work done, we can look at the per-frame rendering
+// logic to see how the application will drive the `RenderContext`
+// type to perform both shader parameter binding and code specialization.
+//
+void renderFrame(int frameIndex) override
+{
+ // 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.
+ //
+ if(!lastTime) lastTime = getCurrentTime();
+ uint64_t currentTime = getCurrentTime();
+ 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);
+ auto clientRect = getWindow()->getClientRect();
+ glm::mat4x4 projection = glm::perspective(
+ glm::radians(60.0f), float(clientRect.width) / float(clientRect.height),
+ 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::mat4x4(inverse(cameraOrientation));
+ view = glm::translate(view, -cameraPosition);
+
+ glm::mat4x4 viewProjection = projection * view;
+
+ auto drawCommandBuffer = gTransientHeaps[frameIndex]->createCommandBuffer();
+ auto drawCommandEncoder =
+ drawCommandBuffer->encodeRenderCommands(gRenderPass, gFramebuffers[frameIndex]);
+ gfx::Viewport viewport = {};
+ viewport.maxZ = 1.0f;
+ viewport.extentX = (float)clientRect.width;
+ viewport.extentY = (float)clientRect.height;
+ drawCommandEncoder->setViewportAndScissor(viewport);
+ drawCommandEncoder->setPrimitiveTopology(PrimitiveTopology::TriangleList);
+
+ // We are only rendering one view, so we can fill in a per-view
+ // shader object once and use it across all draw calls.
+ //
+ auto viewShaderObject = gDevice->createShaderObject(context.perViewShaderType);
+ {
+ ShaderCursor cursor(viewShaderObject);
+ cursor["viewProjection"].setData(&viewProjection, sizeof(viewProjection));
+ cursor["eyePosition"].setData(&cameraPosition, sizeof(cameraPosition));
+ }
+
+ // The majority of our rendering logic is handled as a loop
+ // over the models in the scene, and their meshes.
+ //
+ for(auto& model : gModels)
+ {
+ auto rootObject = drawCommandEncoder->bindPipeline(gPipelineState);
+ ShaderCursor rootCursor(rootObject);
+ rootCursor["gViewParams"].setObject(viewShaderObject);
+ drawCommandEncoder->setVertexBuffer(0, model->vertexBuffer, sizeof(Model::Vertex));
+ drawCommandEncoder->setIndexBuffer(model->indexBuffer, Format::R_UInt32);
+
+ // For each model we provide a parameter
+ // block that holds the per-model transformation
+ // parameters, corresponding to the `PerModel` type
+ // in the shader code.
+ glm::mat4x4 modelTransform = identity;
+ glm::mat4x4 inverseTransposeModelTransform = inverse(transpose(modelTransform));
+ auto modelShaderObject = gDevice->createShaderObject(context.perModelShaderType);
+ {
+ ShaderCursor cursor(modelShaderObject);
+ cursor["modelTransform"].setData(&modelTransform, sizeof(modelTransform));
+ cursor["inverseTransposeModelTransform"].setData(
+ &inverseTransposeModelTransform, sizeof(inverseTransposeModelTransform));
+ }
+ rootCursor["gModelParams"].setObject(modelShaderObject);
+
+ auto lightShaderObject = lightEnv->createShaderObject();
+ rootCursor["gLightEnv"].setObject(lightShaderObject);
+
+ // Now we loop over the meshes in the model.
+ //
+ // A more advanced rendering loop would sort things by material
+ // rather than by model, to avoid overly frequent state changes.
+ // We are just doing something simple for the purposes of an
+ // exmple program.
+ //
+ for(auto& mesh : model->meshes)
+ {
+ // Each mesh has a material, and each material has its own
+ // parameter block that was created at load time, so we
+ // can just re-use the persistent parameter block for the
+ // chosen material.
+ //
+ // Note that binding the material parameter block here is
+ // both selecting the values to use for various material
+ // parameters as well as the *code* to use for material
+ // evaluation (based on the concrete shader type that
+ // is implementing the `IMaterial` interface).
+ //
+ rootCursor["gMaterial"].setObject(mesh->material->shaderObject);
+
+ // All the shader parameters and pipeline states have been set up,
+ // we can now issue a draw call for the mesh.
+ drawCommandEncoder->drawIndexed(mesh->indexCount, mesh->firstIndex);
+ }
+ }
+
+ gSwapchain->present();
+}
+
+};
+
+// This macro instantiates an appropriate main function to
+// run the application defined above.
+PLATFORM_UI_MAIN(innerMain<ModelViewer>)
diff --git a/examples/model-viewer/shaders.slang b/examples/model-viewer/shaders.slang
new file mode 100644
index 000000000..0cc0d802f
--- /dev/null
+++ b/examples/model-viewer/shaders.slang
@@ -0,0 +1,474 @@
+// shaders.slang
+
+//
+// This example builds on the simplistic shaders presented in the
+// "Hello, World" example by adding support for (intentionally
+// simplistic) surface materil and light shading.
+//
+// The code here is not meant to exemplify state-of-the-art material
+// and lighting techniques, but rather to show how a shader
+// library can be developed in a modular fashion without reliance
+// on the C preprocessor manual parameter-binding decorations.
+//
+
+// We are going to define a simple model for surface material shading.
+//
+// The first building block in our model will be the representation of
+// the geometry attributes of a surface as fed into the material.
+//
+struct SurfaceGeometry
+{
+ float3 position;
+ float3 normal;
+
+ // 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;
+
+ // 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;
+};
+//
+// 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
+{
+ // 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);
+};
+//
+// 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));
+
+ 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
+{
+ // 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);
+};
+
+// We will now define a trivial first implementation of the material
+// interface, which uses our Blinn-Phong BRDF with uniform values
+// for its parameters.
+//
+// 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`
+//
+//
+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;
+
+ // 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 associated 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)
+ {
+ BlinnPhong brdf;
+ brdf.kd = diffuseColor;
+ brdf.ks = specularColor;
+ brdf.specularity = specularity;
+ return brdf;
+ }
+};
+//
+// Note that no other code in this file statically
+// references the `SimpleMaterial` type, and instead
+// it is up to the application to "plug in" this type,
+// or another `IMaterial` implementation for the
+// `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`.
+
+ILightEnv gLightEnv;
+
+// Our handling of the material parameter for our shader
+// is quite similar to the case for the lighting environment:
+//
+IMaterial 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.
+//
+struct AssembledVertex
+{
+ float3 position : POSITION;
+ float3 normal : NORMAL;
+ float2 uv : UV;
+};
+struct CoarseVertex
+{
+ float3 worldPosition;
+ float3 worldNormal;
+ float2 uv;
+};
+struct VertexStageOutput
+{
+ CoarseVertex coarseVertex : CoarseVertex;
+ float4 sv_position : SV_Position;
+};
+
+// Perhaps most interesting new feature of the entry
+// point decalrations is that we use a `[shader(...)]`
+// attribute (as introduced in HLSL Shader Model 6.x)
+// in order to tag our entry points.
+//
+// This attribute informs the Slang compiler which
+// functions are intended to be compiled as shader
+// entry points (and what stage they target), so that
+// the programmer no longer needs to specify the
+// entry point name/stage through the API (or on
+// the command line when using `slangc`).
+//
+// While HLSL added this feature only in newer versions,
+// the Slang compiler supports this attribute across
+// *all* targets, so that it is okay to use whether you
+// want DXBC, DXIL, or SPIR-V output.
+//
+[shader("vertex")]
+VertexStageOutput vertexMain(
+ AssembledVertex assembledVertex)
+{
+ VertexStageOutput output;
+
+ float3 position = assembledVertex.position;
+ float3 normal = assembledVertex.normal;
+ float2 uv = assembledVertex.uv;
+
+ float3 worldPosition = mul(gModelParams.modelTransform, float4(position, 1.0)).xyz;
+ float3 worldNormal = mul(gModelParams.inverseTransposeModelTransform, float4(normal, 0.0)).xyz;
+
+ output.coarseVertex.worldPosition = worldPosition;
+ output.coarseVertex.worldNormal = worldNormal;
+ output.coarseVertex.uv = uv;
+
+ output.sv_position = mul(gViewParams.viewProjection, float4(worldPosition, 1.0));
+
+ return output;
+}
+
+// Our fragment shader is almost trivial, with the most interesting
+// thing being how it uses the `TMaterial` type parameter (through the
+// value stored in the `gMaterial` parameter block) to dispatch to
+// the correct implementation of the `getDiffuseColor()` method
+// in the `IMaterial` interface.
+//
+// The `gMaterial` parameter block declaration thus serves not only
+// to group certain shader parameters for efficient CPU-to-GPU
+// communication, but also to select the code that will execute
+// in specialized versions of the `fragmentMain` entry point.
+//
+[shader("fragment")]
+float4 fragmentMain(
+ CoarseVertex coarseVertex : CoarseVertex) : SV_Target
+{
+ // 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 `gMaterial.BRDF`,
+ // which is the `BRDF` type *associated* 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`).
+ //
+ let 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.
+ //
+
+ float3 color = gLightEnv.illuminate(g, brdf, V);
+
+ return float4(color, 1);
+}