summaryrefslogtreecommitdiffstats
path: root/examples/model-viewer/main.cpp
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/main.cpp
parentbad484d838590d0a2aaf1b5b8ac820634af2decb (diff)
Update `model-viewer` example and fixing compiler bugs. (#1795)
Diffstat (limited to 'examples/model-viewer/main.cpp')
-rw-r--r--examples/model-viewer/main.cpp922
1 files changed, 922 insertions, 0 deletions
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>)