diff options
| author | Yong He <yonghe@outlook.com> | 2021-04-16 10:34:26 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-04-16 10:34:26 -0700 |
| commit | 79e92395f8ce3d92c446e3bb3250d19ce33decd5 (patch) | |
| tree | 2ac277fa299200da72cf03a2b5b96338f66aee5d /examples/model-viewer | |
| parent | bad484d838590d0a2aaf1b5b8ac820634af2decb (diff) | |
Update `model-viewer` example and fixing compiler bugs. (#1795)
Diffstat (limited to 'examples/model-viewer')
| -rw-r--r-- | examples/model-viewer/README.md | 25 | ||||
| -rw-r--r-- | examples/model-viewer/cube.mtl | 35 | ||||
| -rw-r--r-- | examples/model-viewer/cube.obj | 31 | ||||
| -rw-r--r-- | examples/model-viewer/main.cpp | 922 | ||||
| -rw-r--r-- | examples/model-viewer/shaders.slang | 474 |
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); +} |
