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/main.cpp | |
| parent | bad484d838590d0a2aaf1b5b8ac820634af2decb (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.cpp | 922 |
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>) |
