This guide will help you create your first application using the Vienna-WebGPU-Engine.
Applications use the GameEngine directly and create scenes via SceneManager:
#include "engine/EngineMain.h"
// ^ Must be first to define SDL_MAIN_HANDLED ^
#include <engine/GameEngine.h>
int main(int argc, char** argv)
{
// Configure engine options
engine::GameEngineOptions options;
options.windowWidth = 1280;
options.windowHeight = 720;
options.enableVSync = false;
// Initialize engine
engine::GameEngine engine;
engine.initialize(options);
// Get managers
auto sceneManager = engine.getSceneManager();
auto resourceManager = engine.getResourceManager();
// Create and setup scene
auto mainScene = sceneManager->createScene("Main");
auto rootNode = mainScene->getRoot();
// Setup camera
auto camera = mainScene->getMainCamera();
camera->getTransform().setLocalPosition(glm::vec3(0, 2, 5));
camera->getTransform().lookAt(glm::vec3(0, 0, 0));
// Load scene and run
sceneManager->loadScene("Main");
engine.run();
return 0;
}
Path behavior differs between Debug and Release builds:
PathProvider uses the project root directory as basePath
E:\Projects\MyGame\ (where CMakeLists.txt is)PathProvider uses the executable directory as basePath
E:\Projects\MyGame\build\Release\assets folder existRecommendation: Always use PathProvider to abstract path differences between build configurations.
Your Application Structure:
YourApp/
├── assets/ # Application-specific assets
│ ├── models/ # Your 3D models
│ ├── textures/ # Your textures
│ ├── shaders/ # Your custom shaders
│ ├── materials/ # Material definitions
│ └── audio/ # Audio files
├── configs/ # Configuration files
└── logs/ # Log output
Engine Structure:
Vienna-WebGPU-Engine/
└── resources/ # Engine resources (shaders, default assets)
├── PBR_Lit_Shader.wgsl
├── shadow2d.wgsl
├── debug.wgsl
└── ...
Always use PathProvider to avoid hardcoded paths:
auto resourceManager = engine.getResourceManager();
// ✅ RECOMMENDED: Use PathProvider for application assets
auto modelPath = PathProvider::resolve("models") / "my_model.obj";
auto maybeModel = resourceManager->m_modelManager->createModel(modelPath.string());
// ✅ RECOMMENDED: Use PathProvider for engine resources
auto engineTexture = PathProvider::getResource("cobblestone_floor_08_diff_2k.jpg");
auto texture = resourceManager->m_textureManager->createTextureFromFile(engineTexture);
// ⚠️ AVOID: Hardcoded paths won't work across Debug/Release
auto maybeModel2 = resourceManager->m_modelManager->createModel("C:/Models/my_model.obj");
// ⚠️ AVOID: Relative paths depend on working directory
auto maybeModel3 = resourceManager->m_modelManager->createModel("my_model.obj");
if (!maybeModel.has_value()) {
spdlog::error("Failed to load model");
return -1;
}
PathProvider resolves paths based on two roots:
basePath) - Application root directory
libraryRoot) - Engine installation directory
⚠️ Critical: Default behavior uses basePath for resources
// By default, resources/ is resolved relative to basePath (NOT libraryRoot)
// This means in Debug: project_root/resources/
// in Release: executable_dir/resources/
Access application assets (recommended):
// Load from assets/models/ (relative to basePath)
auto modelPath = PathProvider::resolve("models") / "my_model.obj";
auto model = resourceManager->m_modelManager->createModel(modelPath.string());
// Load from assets/textures/ (relative to basePath)
auto texturePath = PathProvider::resolve("textures") / "albedo.png";
auto texture = resourceManager->m_textureManager->createTextureFromFile(texturePath.string());
// Shorthand for common paths (all relative to basePath)
auto modelsDir = PathProvider::resolve("models"); // -> {basePath}/assets/models/
auto texturesDir = PathProvider::resolve("textures"); // -> {basePath}/assets/textures/
auto shadersDir = PathProvider::resolve("shaders"); // -> {basePath}/assets/shaders/
| **Function | Resolves To | Debug | Release | Use Case | |
|---|---|---|---|---|---|
resolve("models") |
{basePath}/assets/models/ |
ProjectRoot/assets/models/ |
ExeDir/assets/models/ |
Your 3D models | |
resolve("textures") |
{basePath}/assets/textures/ |
ProjectRoot/assets/textures/ |
ExeDir/assets/textures/ |
Your textures | |
resolve("shaders") |
{basePath}/assets/shaders/ |
ProjectRoot/assets/shaders/ |
ExeDir/assets/shaders/ |
Your custom shaders | |
resolve("materials") |
{basePath}/assets/materials/ |
ProjectRoot/assets/materials/ |
ExeDir/assets/materials/ |
Material files | |
resolve("scenes") |
{basePath}/assets/scenes/ |
ProjectRoot/assets/scenes/ |
ExeDir/assets/scenes/ |
Scene files | |
resolve("configs") |
{basePath}/configs/ |
ProjectRoot/configs/ |
ExeDir/configs/ |
Config files | |
resolve("logs") |
{basePath}/logs/ |
ProjectRoot/logs/ |
ExeDir/logs/ |
Log output | |
getResource(file) |
{libraryRoot}/resources/{file} |
EngineRoot/resources/{file} |
EngineRoot/resources/{file} |
Engine resources |
Important Notes:
basePath changes between Debug/Release buildslibraryRoot always points to engine directorygetResource() for engine resources, not resolve("resources")Key Distinction:
resolve(key) → Application assets in {basePath}/assets/{key}/getResource(file) → Engine resources in {libraryRoot}/resources/{file}| Key | Resolves To | Example |
|---|---|---|
"models" |
{exe}/assets/models/ |
Load your 3D models |
"textures" |
{exe}/assets/textures/ |
Load your textures |
"shaders" |
{exe}/assets/shaders/ |
Load custom shaders |
"materials" |
{exe}/assets/materials/ |
Load material files |
"scenes" |
{exe}/assets/scenes/ |
Load scene files |
"configs" |
{exe}/configs/ |
Configuration files |
"logs" |
{exe}/logs/ |
Log output |
getResource(file) |
{engine}/resources/{file} |
Engine resources |
Override default paths programmatically:
// Override models directory to a custom location
PathProvider::overridePath("models", "C:/MyProject/CustomModels");
// Now resolves to C:/MyProject/CustomModels/
auto modelPath = PathProvider::resolve("models") / "model.obj";
int main(int argc, char** argv)
{
engine::GameEngine engine;
engine.initialize(options);
auto resourceManager = engine.getResourceManager();
// Load model from assets/models/
auto modelPath = PathProvider::resolve("models") / "robot.obj";
auto maybeModel = resourceManager->m_modelManager->createModel(modelPath.string());
// Load texture from assets/textures/
auto texturePath = PathProvider::resolve("textures") / "robot_diffuse.png";
auto maybeTexture = resourceManager->m_textureManager->createTextureFromFile(texturePath.string());
// Load engine resource (from Vienna-WebGPU-Engine/resources/)
auto floorTexture = PathProvider::getResource("cobblestone_floor_08_diff_2k.jpg");
auto maybeFloorTex = resourceManager->m_textureManager->createTextureFromFile(floorTexture);
// ... rest of setup
}
auto resourceManager = engine.getResourceManager();
// Load OBJ model (recommended format)
auto maybeModel = resourceManager->m_modelManager->createModel("my_model.obj");
if (!maybeModel.has_value()) {
spdlog::error("Failed to load model");
return -1;
}
// Create a model render node
auto modelNode = std::make_shared<engine::scene::nodes::ModelRenderNode>(
maybeModel.value()
);
// Position the model
modelNode->getTransform().setLocalPosition(glm::vec3(0, 1, 0));
modelNode->getTransform().setLocalRotation(glm::quat(glm::radians(glm::vec3(0, 45, 0))));
modelNode->getTransform().setLocalScale(glm::vec3(2, 2, 2));
// Add to scene
rootNode->addChild(modelNode);
Multiple nodes can share the same model (GPU memory is shared):
// First instance
auto instance1 = std::make_shared<engine::scene::nodes::ModelRenderNode>(maybeModel.value());
instance1->getTransform().setLocalPosition(glm::vec3(0, 1, 0));
rootNode->addChild(instance1);
// Second instance (shares GPU data)
auto instance2 = std::make_shared<engine::scene::nodes::ModelRenderNode>(maybeModel.value());
instance2->getTransform().setLocalPosition(glm::vec3(5, 1, 0));
rootNode->addChild(instance2);
The engine uses a scene graph system with specialized node types.
auto sceneManager = engine.getSceneManager();
auto mainScene = sceneManager->createScene("MyScene");
auto rootNode = mainScene->getRoot();
// Every scene has:
// - Root node (container for all scene objects)
// - Main camera (automatically created)
ModelRenderNode - Renders a 3D modelLightNode - Provides lightingUpdateNode - Custom update logic nodeNode - Transform hierarchy onlyauto transform = node->getTransform();
// Local space (relative to parent)
transform.setLocalPosition(glm::vec3(0, 1, 0));
transform.setLocalRotation(glm::quat(glm::radians(glm::vec3(0, 45, 0))));
transform.setLocalScale(glm::vec3(2, 2, 2));
// World space (absolute)
transform.setWorldPosition(glm::vec3(5, 0, 0));
// Look at target
transform.lookAt(glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
Child transforms are relative to parent:
auto parent = std::make_shared<engine::scene::nodes::Node>();
parent->getTransform().setLocalPosition(glm::vec3(5, 0, 0));
auto child = std::make_shared<engine::scene::nodes::ModelRenderNode>(model);
child->getTransform().setLocalPosition(glm::vec3(0, 1, 0));
parent->addChild(child); // Child is now at world position (5, 1, 0)
rootNode->addChild(parent);
The engine supports multiple light types via LightNode and the light data structs in engine::rendering.
For complete examples (directional/point/spot, shadows, day-night cycle), see examples/main_demo/main.cpp.
auto ambientLight = std::make_shared<engine::scene::nodes::LightNode>();
engine::rendering::AmbientLight ambientData{};
ambientData.color = glm::vec3(0.2f, 0.2f, 0.2f);
ambientData.intensity = 1.0f;
ambientLight->getLight().setData(ambientData);
rootNode->addChild(ambientLight->asNode());
auto mainCamera = mainScene->getMainCamera();
// Configure camera
mainCamera->setFov(45.0f);
mainCamera->setNearFar(0.1f, 100.0f);
mainCamera->setPerspective(true);
mainCamera->setBackgroundColor(glm::vec4(0.1f, 0.1f, 0.1f, 1.0f));
// Position camera
mainCamera->getTransform().setLocalPosition(glm::vec3(0, 2, 5));
mainCamera->getTransform().lookAt(glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
For a working controller, see examples/main_demo/OrbitCamera.h and examples/main_demo/OrbitCamera.cpp.
Create an UpdateNode to handle input each frame:
class MyInputHandler : public engine::scene::nodes::UpdateNode
{
public:
void update(float deltaTime) override {
auto input = engine()->input();
if (!input)
return;
// Keyboard
if (input->isKey(SDL_SCANCODE_W)) {
// Move forward
}
if (input->isKeyDown(SDL_SCANCODE_SPACE)) {
// Jump (triggered once per press)
}
// Mouse
if (input->isMouse(SDL_BUTTON_LEFT)) {
auto mousePos = input->getMousePosition();
auto mouseDelta = input->getMouseDelta();
// Handle mouse input
}
// Mouse wheel
auto wheel = input->getMouseWheel();
if (wheel.y != 0.0f) {
// Zoom or scroll
}
}
};
auto inputHandler = std::make_shared<MyInputHandler>();
rootNode->addChild(inputHandler);
auto imguiManager = engine.getImGuiManager();
// Add a simple window
imguiManager->addFrame([]() {
ImGui::Begin("My Window");
ImGui::Text("Hello from ImGui!");
ImGui::End();
});
For a larger UI setup, see examples/main_demo/MainDemoImGuiUI.cpp.
Explore the complete example:
| Task | Code |
|---|---|
| Initialize engine | engine.initialize(options) |
| Create scene | sceneManager->createScene("Name") |
| Get root node | mainScene->getRoot() |
| Get camera | mainScene->getMainCamera() |
| Load model | resourceManager->m_modelManager->createModel("file.obj") |
| Create model node | std::make_shared<ModelRenderNode>(model) |
| Add to scene | rootNode->addChild(node) |
| Set position | node->getTransform().setLocalPosition(pos) |
| Create light | std::make_shared<LightNode>() |
| Load scene | sceneManager->loadScene("Name") |
| Run engine | engine.run() |
class MyCustomNode : public engine::scene::nodes::UpdateNode
{
public:
void update(float deltaTime) override {
// Access engine systems
auto input = engine()->input();
auto gpu = engine()->gpu();
auto resources = engine()->resources();
// Your logic here
}
};
resources/*.wgslIf you get stuck, start from examples/main_demo/main.cpp.