# Bella Engine SDK - Complete Developer Guide
## Welcome to the Bella Engine SDK
This comprehensive guide will take you from zero to rendering with the Bella Engine SDK. Whether you're building a plugin for a 3D application, creating a custom renderer, or implementing procedural textures, this guide provides practical examples and clear explanations to make your development journey joyous.
If you're new to 3D rendering or nodal scene graphs, don't worry—we'll start with the fundamentals and build up to advanced topics. If you have questions along the way, join us on [our Discord server](https://discord.gg/DqCAvAXH6C).
## Quick Start - Your First Render
Let's start with something that works right away. Here's a complete example that creates a simple scene and renders it:
```c++
#include "bella_sdk/bella_engine.h"
#include "bella_sdk/bella_scene.h"
#include "dl_core/dl_logging.h"
using namespace dl;
using namespace dl::bella_sdk;
// Observer to receive render updates
struct MyEngineObserver : public EngineObserver {
void onStarted(String pass) override {
logInfo("Rendering started: %s", pass.buf());
}
void onProgress(String pass, Progress progress) override {
logInfo("Progress: %.1f%% - %s",
progress.progress() * 100.0, progress.toString().buf());
}
void onImage(String pass, Image image) override {
logInfo("Got image: %dx%d", image.width(), image.height());
// In a real app, you'd display this image
}
void onStopped(String pass) override {
logInfo("Rendering stopped: %s", pass.buf());
}
};
int main() {
// Create engine and load node definitions
Engine engine;
Scene scene = engine.scene();
scene.loadDefs();
// Create observer
MyEngineObserver observer;
engine.subscribe(&observer);
// Create a simple scene with a sphere
Node world = scene.world();
Node sphere = scene.createNode("sphere");
Node material = scene.createNode("pbr");
// Set up the sphere
sphere["radius"] = 1.0;
sphere["material"] = material;
sphere.parentTo(world);
// Set material properties
material["base"]["color"] = Rgba{0.8, 0.2, 0.2, 1.0}; // Red
material["base"]["metallic"] = 0.0;
material["base"]["roughness"] = 0.3;
// Configure camera
Node camera = scene.camera();
camera["resolution"] = Vec2{800, 600};
camera["fov"] = 45.0;
// Position camera to look at sphere
Node cameraXform = scene.findNode("camera_xform");
if (!cameraXform) {
cameraXform = scene.createNode("xform", "camera_xform");
camera.parentTo(cameraXform);
cameraXform.parentTo(world);
}
cameraXform["transform"]["pos"] = Pos3{0, 0, 3};
// Start rendering
engine.start();
// In a real application, you'd have a message loop here
logInfo("Rendering... Press Ctrl+C to stop.");
return 0;
}
```
This example demonstrates the core concepts you'll use throughout the SDK:
- **Engine**: Manages the rendering process
- **Scene**: Contains all the objects and their relationships
- **Nodes**: Individual objects like spheres, materials, cameras
- **Attributes**: Properties of nodes accessed with `node["property"]`
- **Hierarchy**: Objects parented to form a scene graph
- **Observers**: Get notified of rendering progress and results
## Understanding the Architecture
### The Big Picture
Bella Engine uses a **nodal scene graph** architecture. Think of it like a visual programming environment where everything is a node with inputs and outputs:
************************************************
*
* +------------------+ +------------------+
* | Camera | | Sphere |
* +------------------+ +------------------+
* o resolution | o radius |
* +------------------+ +------------------+
* o fov | o material -----> [PBR Material]
* +------------------+ +------------------+
* o transform -----> [Transform]
* +------------------+
*
************************************************
[Figure [diagram]: Nodes with inputs and outputs forming a scene graph.]
Each node is like a specialized computer that:
- Has **inputs** (parameters you can set)
- Has **outputs** (computed results)
- Can be **connected** to other nodes
- Contains **data** and possibly **code** to process that data
### The Foundation - DL Core Library
Before diving into rendering, let's understand the foundation. The `dl_core` library provides essential utilities:
```c++
#include "dl_core/dl_string.h"
#include "dl_core/dl_vector.h"
#include "dl_core/dl_math.h"
using namespace dl;
// Strings are immutable and reference-counted
String name = "MyNode";
String fullPath = name + "_001";
// Vectors are dynamic arrays with mathematical operations
Vector values = {1.0, 2.0, 3.0};
values.push_back(4.0);
// Math types with intuitive operations
Vec3 position = {1.0, 2.0, 3.0};
Vec3 direction = {0.0, 0.0, 1.0};
Vec3 newPos = position + direction * 2.0;
// Colors with alpha
Rgba red = {1.0, 0.0, 0.0, 1.0};
Rgba transparent = red;
transparent.a = 0.5;
// 4x4 transformation matrices
Mat4 translation = Mat4::translation(Vec3{10, 0, 0});
Mat4 rotation = Mat4::rotationZ(math::pi / 4);
Mat4 transform = translation * rotation;
```
### Type Safety and Conversions
Bella's type system prevents errors while allowing natural conversions:
```c++
// These conversions work automatically:
Real scalar = 5.0;
Vec3 vector = scalar; // Becomes {5.0, 5.0, 5.0}
Vec4 vec4 = vector; // Becomes {5.0, 5.0, 5.0, 0.0}
// Matrix conversions preserve data:
Mat3 small = Mat3::identity;
Mat4 large = small; // Embeds 3x3 in upper-left of 4x4
// The system warns about narrowing:
Vec4 source = {1.0, 2.0, 3.0, 4.0};
Vec3 result = source; // Becomes {1.0, 2.0, 3.0} - warns about lost data
```
## Working with Scenes and Nodes
### Creating Your First Scene
```c++
// Every scene needs node definitions first
Scene scene;
scene.loadDefs(); // Loads built-in Bella node types
// Now you can create nodes
Node sphere = scene.createNode("sphere");
Node material = scene.createNode("pbr");
Node light = scene.createNode("physicalSky");
// Nodes have a type and unique name
logInfo("Created %s node named '%s'",
sphere.type().buf(), sphere.id().buf());
```
### Understanding Node Hierarchy
Bella scenes are organized as hierarchies rooted at the **world** transform:
```c++
Node world = scene.world();
// Create transform nodes to position objects
Node roomXform = scene.createNode("xform", "room");
Node tableXform = scene.createNode("xform", "table");
Node cupXform = scene.createNode("xform", "cup");
// Build hierarchy: world -> room -> table -> cup
roomXform.parentTo(world);
tableXform.parentTo(roomXform);
cupXform.parentTo(tableXform);
// Position each transform
roomXform["transform"]["pos"] = Pos3{0, 0, 0};
tableXform["transform"]["pos"] = Pos3{0, 0, 1}; // Table height
cupXform["transform"]["pos"] = Pos3{0.5, 0.3, 0.1}; // On table
// Add geometry to the cup transform
Node cupMesh = scene.createNode("cylinder");
cupMesh["height"] = 0.15;
cupMesh["radius"] = 0.05;
cupMesh.parentTo(cupXform);
```
This creates a hierarchy where transformations accumulate down the tree. The cup's final position will be the combination of all parent transforms.
### Working with Attributes
Nodes have **inputs** (properties you set) and **outputs** (computed values):
```c++
Node material = scene.createNode("pbr");
// Set simple values
material["base"]["color"] = Rgba{0.8, 0.1, 0.1, 1.0};
material["base"]["metallic"] = 0.0;
material["base"]["roughness"] = 0.4;
// Read values back
Rgba color = material["base"]["color"].asRgba();
Real metallic = material["base"]["metallic"].asReal();
// Some inputs accept other nodes
Node texture = scene.createNode("fileTexture");
texture["path"] = String("/path/to/texture.jpg");
material["base"]["color"] = texture; // Node reference
// Or connect outputs to inputs
material["base"]["color"] |= texture.output("outColor");
```
### Arrays and Complex Attributes
Some attributes are arrays or complex objects:
```c++
Node mesh = scene.createNode("mesh");
// Create vertex positions (Vec3f buffer)
Vector positions = {
{-1, -1, 0}, {1, -1, 0}, {1, 1, 0}, {-1, 1, 0} // Quad
};
mesh["steps"][0]["points"] = BufRef(positions);
// Create face indices (Vec4u buffer for quads)
Vector faces = {
{0, 1, 2, 3} // Single quad face
};
mesh["polygons"] = BufRef(faces);
// UV coordinates (Vec2f buffer)
Vector uvs = {
{0, 0}, {1, 0}, {1, 1}, {0, 1}
};
mesh["steps"][0]["uvs"] = BufRef(uvs);
```
### Node Connections and Dependencies
The power of nodal architecture comes from connecting nodes:
```c++
// Create a procedural texture network
Node checker = scene.createNode("checker");
Node colorRamp = scene.createNode("colorRamp");
Node noise = scene.createNode("noise");
// Set up the checker pattern
checker["color1"] = Rgba{1.0, 1.0, 1.0, 1.0}; // White
checker["color2"] = Rgba{0.2, 0.2, 0.2, 1.0}; // Dark gray
checker["scale"] = 10.0;
// Add noise to the UV coordinates
noise["scale"] = 5.0;
noise["detail"] = 3.0;
checker["uvCoord"] |= noise.output("outVector");
// Use color ramp to adjust the result
colorRamp["interpolation"] = String("linear");
colorRamp["input"] |= checker.output("outColor");
// Finally connect to material
Node material = scene.createNode("pbr");
material["base"]["color"] |= colorRamp.output("outColor");
```
This creates a network: `noise -> checker -> colorRamp -> material`, where each node processes the output of the previous one.
## File I/O and Scene Management
### Loading and Saving Scenes
```c++
Scene scene;
scene.loadDefs();
// Load a scene from file
if (scene.read("my_scene.bsa")) {
logInfo("Loaded scene with %d nodes", scene.nodeCount());
} else {
logError("Failed to load scene");
}
// Modify the scene
Node camera = scene.camera();
camera["resolution"] = Vec2{1920, 1080};
// Save in different formats
scene.write("output.bsa"); // Text format (human readable)
scene.write("output.bsx"); // Binary format (faster)
scene.write("output.bsz"); // Compressed archive with resources
// Check what file types are supported
StringVector supported = scene.supportedFileTypes();
for (String ext : supported) {
logInfo("Supported: %s", ext.buf());
}
```
### Working with External Resources
```c++
// Find all resources used by the scene
StringVector resources = scene.referencedResources();
StringVector missing = scene.missingResources();
if (missing.size() > 0) {
logWarning("Missing %d resources:", missing.size());
for (String path : missing) {
logWarning(" %s", path.buf());
}
}
// Gather all resources into a local directory
if (scene.gatherResources()) {
logInfo("Resources copied to ./res/");
}
// Resolve path variables
Node texture = scene.findNode("myTexture");
if (texture && texture.isTypeOf("fileTexture")) {
String resolved = scene.resolveInputPath(texture);
logInfo("Texture path: %s", resolved.buf());
}
```
### Scene Organization and Querying
```c++
// Find nodes by type
NodeVector lights = scene.nodes("light");
NodeVector cameras = scene.nodes("camera");
// Find specific nodes
Node worldNode = scene.world();
Node settings = scene.settings();
Node beautyPass = scene.beautyPass();
// Search by name pattern
Node myLight = scene.findNode("key_light_001");
// Query node properties
for (Node node : scene.nodes()) {
logInfo("Node: %s (type: %s)",
node.displayName().buf(), node.type().buf());
if (node.isTypeOf("light")) {
Real intensity = node["intensity"].asReal();
logInfo(" Light intensity: %f", intensity);
}
}
// Get dependency information
NodeVector deps = worldNode.dependencies(true, true); // Include outputs, topologically sorted
logInfo("World depends on %d nodes", deps.size());
```
## Rendering with the Engine
### Basic Engine Setup
```c++
Engine engine;
Scene scene = engine.scene();
scene.loadDefs();
// Configure rendering settings
Node settings = scene.settings();
settings["outputDir"] = String("/path/to/output");
settings["outputName"] = String("my_render");
Node beautyPass = scene.beautyPass();
beautyPass["solver"] = String("pt"); // Path tracer
beautyPass["maxTime"] = 300.0; // 5 minutes
beautyPass["targetNoise"] = 0.01; // 1% noise
```
### Engine Observers - Getting Results
The observer pattern is how you receive rendering updates:
```c++
struct RenderWindow : public EngineObserver {
// Called when rendering starts
void onStarted(String pass) override {
updateUI("Rendering started...");
}
// Progress updates during rendering
void onProgress(String pass, Progress progress) override {
float percent = progress.progress() * 100.0f;
int timeRemaining = progress.remainingEnd();
updateProgressBar(percent);
updateTimeEstimate(timeRemaining);
// Show detailed progress info
logInfo("Level %d: %.1f%% complete, %s remaining",
progress.level(), percent,
progress.remainingToString().buf());
}
// New image available
void onImage(String pass, Image image) override {
// Image data is only valid during this call!
// Copy it if you need to keep it
displayImage(image.rgba8(), image.width(), image.height());
// Or save to file
saveImageToFile(image, "current_render.png");
}
// Error handling
void onError(String pass, String msg) override {
showErrorDialog(msg.buf());
}
// Rendering finished
void onStopped(String pass) override {
updateUI("Rendering complete");
enableUI(true);
}
private:
void updateUI(const char* status) { /* Update your GUI */ }
void updateProgressBar(float percent) { /* Update progress bar */ }
void displayImage(Rgba8* pixels, int w, int h) { /* Show image */ }
};
// Use the observer
RenderWindow window;
engine.subscribe(&window);
engine.start();
```
### Interactive Rendering (IPR)
For real-time preview while editing:
```c++
// Enable interactive mode
engine.enableInteractiveMode();
engine.start();
// Now any scene changes automatically trigger re-rendering
Node sphere = scene.findNode("mySphere");
sphere["radius"] = 2.0; // Render updates automatically
// Group changes for better performance
{
Scene::EventScope group(scene); // Batch updates
sphere["radius"] = 3.0;
sphere["material"]["base"]["color"] = Rgba{0, 1, 0, 1};
// Updates applied when scope ends
}
// Interactive camera controls
void onMouseDrag(int deltaX, int deltaY) {
Path cameraPath = scene.cameraPath();
if (isOrbiting) {
Mat4 newTransform = orbitCamera(cameraPath, Vec2{deltaX, deltaY});
// Camera automatically updates
} else if (isPanning) {
Mat4 newTransform = panCamera(cameraPath, Vec2{deltaX, deltaY}, true);
}
}
```
### Advanced Engine Features
```c++
// Multiple render passes
Node alphaPass = scene.createNode("alphaPass");
Node normalPass = scene.createNode("normalPass");
Node depthPass = scene.createNode("depthPass");
Node settings = scene.settings();
settings["extraPasses"].appendElement() = alphaPass;
settings["extraPasses"].appendElement() = normalPass;
settings["extraPasses"].appendElement() = depthPass;
// Render regions (for preview or tiled rendering)
Node camera = scene.camera();
camera["region"] = Region{100, 100, 400, 300}; // x, y, width, height
// Licensing information
if (Engine::isLicensed()) {
logInfo("Licensed version: %s", Engine::licenseType().buf());
} else {
logInfo("Demo mode - resolution limited to 1080p");
Vec2u actualRes = renderedResolution({1920, 1080}, false);
logInfo("Actual resolution: %dx%d", actualRes.x, actualRes.y);
}
```
## Material and Lighting Workflows
### Creating Physically Based Materials
```c++
Node material = scene.createNode("pbr");
// Base color and properties
material["base"]["color"] = Rgba{0.8, 0.3, 0.1, 1.0}; // Orange
material["base"]["metallic"] = 0.0; // Non-metallic
material["base"]["roughness"] = 0.4; // Slightly rough
material["base"]["anisotropy"] = 0.0; // Isotropic
// Subsurface scattering for organic materials
material["sss"]["weight"] = 0.1;
material["sss"]["color"] = Rgba{1.0, 0.8, 0.6, 1.0};
material["sss"]["scale"] = 1.0;
// Emission for glowing materials
material["emission"]["weight"] = 0.0; // No emission
material["emission"]["color"] = Rgba{1.0, 1.0, 1.0, 1.0};
// Transparency
material["transmission"]["weight"] = 0.0; // Opaque
material["opacity"] = 1.0;
```
### Working with Textures
```c++
// Load texture from file
Node diffuseTexture = scene.createNode("fileTexture");
diffuseTexture["path"] = String("/textures/wood_diffuse.jpg");
diffuseTexture["colorSpace"] = String("sRGB");
// Apply to material
material["base"]["color"] |= diffuseTexture.output("outColor");
// Normal mapping
Node normalTexture = scene.createNode("fileTexture");
normalTexture["path"] = String("/textures/wood_normal.jpg");
normalTexture["colorSpace"] = String("linear");
Node normalMap = scene.createNode("normalMap");
normalMap["input"] |= normalTexture.output("outColor");
normalMap["strength"] = 1.0;
material["base"]["normal"] |= normalMap.output("outNormal");
// UV transformations
Node uvTransform = scene.createNode("texform");
uvTransform["scale"] = Vec2{4.0, 4.0}; // Tile 4x
uvTransform["rotation"] = 45.0; // Rotate 45 degrees
// Connect to textures
diffuseTexture["uvCoord"] |= uvTransform.output("outUV");
normalTexture["uvCoord"] |= uvTransform.output("outUV");
```
### PBR Texture Sets
Bella can automatically handle PBR texture sets:
```c++
PbrTextureSet textureSet;
// Point to any texture in the set or a zip file
UInt found = textureSet.resolve("/textures/metal_PBR.zip");
logInfo("Found %d textures in set", found);
// Check what was found
if (textureSet.hasColor())
logInfo("Diffuse: %s", textureSet.color().buf());
if (textureSet.hasRoughness())
logInfo("Roughness: %s", textureSet.roughness().buf());
if (textureSet.hasMetallic())
logInfo("Metallic: %s", textureSet.metallic().buf());
if (textureSet.hasNormal())
logInfo("Normal: %s", textureSet.normal().buf());
// Apply to material automatically
Node material = scene.createNode("pbr");
textureSet.applyTo(material);
```
### Lighting Setup
```c++
// Physical sky for outdoor scenes
Node sky = scene.createNode("physicalSky");
Node sun = scene.createNode("sun");
// Configure sun position
Node dateTime = scene.createNode("dateTime");
dateTime["year"] = 2024;
dateTime["month"] = 6; // June
dateTime["day"] = 21; // Summer solstice
dateTime["hour"] = 14.0; // 2 PM
Node location = scene.createNode("location");
location["latitude"] = 40.7128; // New York
location["longitude"] = -74.0060;
sun["dateTime"] = dateTime;
sun["location"] = location;
sky["sun"] = sun;
sky["turbidity"] = 3.0; // Clear sky
// Set as environment
scene.settings()["environment"] = sky;
// Studio lighting with area lights
Node keyLight = scene.createNode("light");
keyLight["type"] = String("area");
keyLight["width"] = 2.0;
keyLight["height"] = 1.0;
keyLight["intensity"] = 100.0;
keyLight["color"] = Rgba{1.0, 0.95, 0.8, 1.0}; // Warm white
// Position the light
Node keyLightXform = scene.createNode("xform", "key_light_xform");
keyLightXform["transform"]["pos"] = Pos3{2, 1, 2};
keyLightXform["transform"]["rot"] = Vec3{-30, 45, 0}; // Euler angles
keyLight.parentTo(keyLightXform);
keyLightXform.parentTo(scene.world());
// Fill light (softer, opposite side)
Node fillLight = scene.createNode("light");
fillLight["type"] = String("area");
fillLight["width"] = 3.0;
fillLight["height"] = 2.0;
fillLight["intensity"] = 30.0;
fillLight["color"] = Rgba{0.8, 0.9, 1.0, 1.0}; // Cool white
Node fillLightXform = scene.createNode("xform", "fill_light_xform");
fillLightXform["transform"]["pos"] = Pos3{-1.5, 0.5, 1.5};
fillLight.parentTo(fillLightXform);
fillLightXform.parentTo(scene.world());
```
## Geometry and Modeling
### Procedural Geometry
```c++
// Basic primitives
Node sphere = scene.createNode("sphere");
sphere["radius"] = 1.0;
sphere["subdivisions"] = 3;
Node cube = scene.createNode("box");
cube["sizeX"] = 2.0;
cube["sizeY"] = 1.0;
cube["sizeZ"] = 0.5;
Node cylinder = scene.createNode("cylinder");
cylinder["radius"] = 0.5;
cylinder["height"] = 2.0;
cylinder["subdivisions"] = 32;
// Ground plane
Node plane = scene.createNode("plane");
plane["sizeX"] = 10.0;
plane["sizeY"] = 10.0;
plane["subdivisionsX"] = 10;
plane["subdivisionsY"] = 10;
```
### Custom Mesh Creation
```c++
Node mesh = scene.createNode("mesh");
// Create a tetrahedron
Vector vertices = {
{0, 0, 0}, // Bottom center
{1, 0, 0}, // Bottom right
{0.5, 0.866, 0}, // Bottom back
{0.5, 0.289, 0.816} // Top
};
Vector faces = {
{0, 1, 2, 2}, // Bottom face (triangle, so repeat last index)
{0, 3, 1, 1}, // Side face 1
{1, 3, 2, 2}, // Side face 2
{2, 3, 0, 0} // Side face 3
};
// UV coordinates for each vertex
Vector uvs = {
{0, 0}, {1, 0}, {0.5, 1}, {0.5, 0.5}
};
// Assign to mesh
mesh["polygons"] = BufRef(faces);
mesh["steps"][0]["points"] = BufRef(vertices);
mesh["steps"][0]["uvs"] = BufRef(uvs);
// Auto-generate normals and tangents
mesh["autoNormals"] = true;
mesh["autoTangents"] = true;
// Validate the mesh
String validation = mesh.checkMesh();
if (!validation.isEmpty()) {
logWarning("Mesh issues: %s", validation.buf());
}
```
### Motion Blur and Animation
```c++
Node mesh = scene.createNode("mesh");
// Multiple time steps for motion blur
Vector frame0 = {{0,0,0}, {1,0,0}, {1,1,0}, {0,1,0}};
Vector frame1 = {{0,0,1}, {1,0,1}, {1,1,1}, {0,1,1}};
mesh["steps"][0]["time"] = 0.0;
mesh["steps"][0]["points"] = BufRef(frame0);
mesh["steps"][1]["time"] = 1.0;
mesh["steps"][1]["points"] = BufRef(frame1);
// Same faces for all frames
Vector faces = {{0, 1, 2, 3}};
mesh["polygons"] = BufRef(faces);
// Configure motion blur in camera
Node camera = scene.camera();
camera["shutterOpen"] = 0.0;
camera["shutterClose"] = 1.0;
camera["motionBlur"] = true;
```
### Instancing and Copying
```c++
// Create a base object
Node originalSphere = scene.createNode("sphere", "prototype");
originalSphere["radius"] = 0.5;
// Create many instances efficiently
Node world = scene.world();
for (int i = 0; i < 100; i++) {
// Clone creates a copy with dependencies
Node instance = scene.cloneNode(originalSphere, false); // Don't clone deps
// Position each instance
Node xform = scene.createNode("xform", String::format("sphere_xform_%d", i));
Real x = (i % 10) * 2.0;
Real z = (i / 10) * 2.0;
xform["transform"]["pos"] = Pos3{x, 0, z};
instance.parentTo(xform);
xform.parentTo(world);
}
// Or use proper instancing for better performance
Node instancer = scene.createNode("instancer");
instancer["object"] = originalSphere;
// Define instance positions
Vector positions;
for (int i = 0; i < 100; i++) {
Real x = (i % 10) * 2.0;
Real z = (i / 10) * 2.0;
positions.push_back({x, 0, z});
}
instancer["positions"] = BufRef(positions);
```
## Advanced Features
### Custom Node Implementation
While most users won't need to implement custom nodes, here's how the system works:
```c++
// Node definition in .bnd file:
/*
"myTexture": {
"bases": ["texture"],
"inputs": {
"scale": {
"type": "real",
"value": 1.0,
"help": {"en": "Pattern scale factor"}
},
"seed": {
"type": "int",
"value": 42,
"help": {"en": "Random seed"}
}
},
"outputs": {
"outColor": {"type": "rgba"}
}
}
*/
// Implementation structure
struct MyTextureData {
DL_MAKE_IINFO(Real, scale);
DL_MAKE_IINFO(Int, seed);
DL_MAKE_OINFO(Rgba, outColor);
};
DL_C_EXPORT bool myTextureInit(const INode* inode, void** data) {
auto info = initNodeInfo(inode, data);
if (!info) return false;
DL_INIT_IINFO(scale);
DL_INIT_IINFO(seed);
DL_INIT_OINFO(outColor);
return true;
}
DL_C_EXPORT bool myTexturePrep(const INode* inode, void* data) {
auto info = getNodeInfo(inode, data);
if (!info) return false;
DL_PREP_IINFO(scale);
DL_PREP_IINFO(seed);
DL_PREP_OINFO(outColor);
return true;
}
DL_C_EXPORT bool myTextureEval(EvalCtx* ctx, void* output) {
auto info = getNodeInfo(ctx);
if (!info) return false;
// Get input values
DL_EVAL_IINFO(scale);
DL_EVAL_IINFO(seed);
// Get UV coordinates from context
Vec2 uv = {0, 0};
if (ctx->numUVs() > 0) {
const Vec2f* uvs = ctx->uvs();
uv = Vec2{uvs[0].x, uvs[0].y};
}
// Compute pattern (example: checkered pattern)
uv *= scale;
bool checker = ((int)floor(uv.x) + (int)floor(uv.y)) % 2;
Rgba result = checker ? Rgba{1,1,1,1} : Rgba{0,0,0,1};
// Write result if this output matches
if (info->outColor.matches(ctx)) {
return writeOutput(result, output);
}
return false;
}
DL_C_EXPORT bool myTextureFree(const INode* inode, void** data) {
return freeNodeInfo(inode, data);
}
```
### Performance Optimization
```c++
// Use event groups for bulk changes
{
Scene::EventScope batch(scene);
// All these changes are batched
for (Node mesh : scene.nodes("mesh")) {
mesh["subdivisions"] = 2;
mesh["smooth"] = true;
}
// Engine processes all changes at once when scope ends
}
// Disable events during major scene building
{
Scene::DisableEvents pause(scene);
// Build complex scene without notifications
for (int i = 0; i < 1000; i++) {
Node instance = scene.createNode("sphere");
// ... setup instance
}
// Events re-enabled when scope ends
}
// Use topological sorting for dependency order
NodeVector ordered = scene.nodes("", true); // Topologically sorted
for (Node node : ordered) {
node.nodePrep(); // Process in correct order
}
// Optimize memory usage
scene.clearNodes(true); // Remove unreferenced nodes
```
### Error Handling and Debugging
```c++
// Check for common issues
String validation = Engine::checkMesh(myMesh);
if (!validation.isEmpty()) {
logError("Mesh problems: %s", validation.buf());
}
// Validate entire scene
bool isDirty = scene.isDirty();
if (isDirty) {
logInfo("Scene has unsaved changes");
}
// Handle licensing issues
if (!Engine::isLicensed()) {
logWarning("Running in demo mode");
// Adjust expectations
Vec2u requestedRes = {1920, 1080};
Vec2u actualRes = renderedResolution(requestedRes, false);
if (actualRes != requestedRes) {
logInfo("Resolution limited to %dx%d", actualRes.x, actualRes.y);
}
}
// Debug node connections
void debugNode(Node node) {
logInfo("Node: %s (%s)", node.id().buf(), node.type().buf());
for (UInt i = 0; i < node.inputCount(); i++) {
Input input = node.input(i);
if (input.connected()) {
Output output = input.output();
logInfo(" %s connected to %s.%s",
input.name().buf(),
output.node().id().buf(),
output.name().buf());
} else {
logInfo(" %s = %s",
input.name().buf(),
input.valueToString().buf());
}
}
}
```
### Integration with Host Applications
```c++
// Example: Maya plugin integration
struct MayaRenderer {
Engine engine;
Scene scene;
MyEngineObserver observer;
bool initialize() {
scene = engine.scene();
scene.loadDefs();
engine.subscribe(&observer);
engine.enableInteractiveMode();
return true;
}
void translateMayaScene() {
// Convert Maya scene to Bella
MItDag dagIter;
for (; !dagIter.isDone(); dagIter.next()) {
MDagPath dagPath;
dagIter.getPath(dagPath);
if (dagPath.hasFn(MFn::kMesh)) {
translateMesh(dagPath);
} else if (dagPath.hasFn(MFn::kCamera)) {
translateCamera(dagPath);
} else if (dagPath.hasFn(MFn::kLight)) {
translateLight(dagPath);
}
}
}
void translateMesh(const MDagPath& dagPath) {
MFnMesh mayaMesh(dagPath);
// Get Maya mesh data
MPointArray points;
MIntArray polygonCounts;
MIntArray polygonIndices;
mayaMesh.getPoints(points);
mayaMesh.getVertices(polygonCounts, polygonIndices);
// Convert to Bella format
Vector bellaPoints;
for (UInt i = 0; i < points.length(); i++) {
MPoint p = points[i];
bellaPoints.push_back({(float)p.x, (float)p.y, (float)p.z});
}
Vector bellaFaces;
UInt polyIndex = 0;
for (UInt i = 0; i < polygonCounts.length(); i++) {
int count = polygonCounts[i];
if (count == 3) {
// Triangle
Vec4u face = {
(UInt)polygonIndices[polyIndex],
(UInt)polygonIndices[polyIndex + 1],
(UInt)polygonIndices[polyIndex + 2],
(UInt)polygonIndices[polyIndex + 2] // Repeat last
};
bellaFaces.push_back(face);
} else if (count == 4) {
// Quad
Vec4u face = {
(UInt)polygonIndices[polyIndex],
(UInt)polygonIndices[polyIndex + 1],
(UInt)polygonIndices[polyIndex + 2],
(UInt)polygonIndices[polyIndex + 3]
};
bellaFaces.push_back(face);
}
polyIndex += count;
}
// Create Bella mesh
String meshName = String(dagPath.partialPathName().asChar());
Node bellaMesh = scene.createNode("mesh", meshName);
bellaMesh["polygons"] = BufRef(bellaFaces);
bellaMesh["steps"][0]["points"] = BufRef(bellaPoints);
// Handle transform hierarchy
MDagPath parentPath = dagPath;
parentPath.pop();
Node parentXform = findOrCreateTransform(parentPath);
bellaMesh.parentTo(parentXform);
}
void startIPR() {
translateMayaScene();
engine.start();
}
void stopIPR() {
engine.stop();
}
// Handle Maya scene changes
void onMayaNodeChanged(MObject& node) {
if (node.hasFn(MFn::kMesh)) {
Scene::EventScope batch(scene);
retranslateMesh(node);
}
}
};
```
## Best Practices and Tips
### Scene Organization
- **Use meaningful names**: `hero_car_xform` instead of `xform_001`
- **Group related objects**: Use transform hierarchies to organize
- **Minimize dependencies**: Avoid circular references
- **Use node libraries**: Create reusable material and lighting setups
### Performance
- **Batch scene changes**: Always use `Scene::EventScope` for multiple operations
- **Avoid tiny textures**: Use appropriate resolution for render size
- **Optimize geometry**: Remove unnecessary subdivisions and vertices
- **Use instancing**: For repeated objects, use proper instancing
### Memory Management
- **Reference counting is automatic**: Just use the objects normally
- **Pass objects by value**: `Node`, `Input`, etc. are designed for this
- **Don't hold raw pointers**: Always use the wrapper classes
- **Clean up unused nodes**: Use `scene.clearNodes(true)` periodically
### Error Handling
- **Check return values**: File operations and engine calls can fail
- **Validate geometry**: Use `checkMesh()` before rendering
- **Handle licensing**: Gracefully handle demo mode limitations
- **Use observers**: Monitor for engine errors and warnings
### Debugging
- **Use logging**: The `dl_core` logging system is your friend
- **Inspect node graphs**: Print connections and values
- **Validate scenes**: Check for dirty state and missing resources
- **Monitor performance**: Use progress callbacks to identify bottlenecks
## Common Patterns and Recipes
### Material Library System
```c++
class MaterialLibrary {
Scene& scene;
HashMap materials;
public:
MaterialLibrary(Scene& s) : scene(s) {}
Node createMetal(String name, Rgba color, Real roughness) {
Node material = scene.createNode("pbr", name);
material["base"]["color"] = color;
material["base"]["metallic"] = 1.0;
material["base"]["roughness"] = roughness;
materials[name] = material;
return material;
}
Node createGlass(String name, Rgba color, Real roughness, Real ior) {
Node material = scene.createNode("pbr", name);
material["base"]["color"] = color;
material["base"]["metallic"] = 0.0;
material["base"]["roughness"] = roughness;
material["transmission"]["weight"] = 1.0;
material["transmission"]["ior"] = ior;
materials[name] = material;
return material;
}
Node get(String name) {
return materials.contains_key(name) ? materials[name] : Node();
}
};
// Usage
MaterialLibrary lib(scene);
Node gold = lib.createMetal("gold", Rgba{1.0, 0.766, 0.336, 1.0}, 0.1);
Node glass = lib.createGlass("window_glass", Rgba{0.9, 0.9, 0.9, 1.0}, 0.0, 1.52);
```
### Camera Animation System
```c++
class CameraAnimator {
Scene& scene;
Node cameraXform;
Vector keyframes;
Vector times;
public:
CameraAnimator(Scene& s) : scene(s) {
cameraXform = scene.findNode("camera_xform");
if (!cameraXform) {
cameraXform = scene.createNode("xform", "camera_xform");
scene.camera().parentTo(cameraXform);
cameraXform.parentTo(scene.world());
}
}
void addKeyframe(Real time, Pos3 position, Vec3 target, Vec3 up = {0,0,1}) {
Mat4 transform = Mat4::lookAt(position, target, up);
keyframes.push_back(transform);
times.push_back(time);
}
void setTime(Real t) {
if (keyframes.size() < 2) return;
// Find surrounding keyframes
UInt index = 0;
for (UInt i = 0; i < times.size() - 1; i++) {
if (t >= times[i] && t <= times[i + 1]) {
index = i;
break;
}
}
// Interpolate
Real alpha = (t - times[index]) / (times[index + 1] - times[index]);
Mat4 result = lerp(keyframes[index], keyframes[index + 1], alpha);
cameraXform["transform"]["matrix"] = result;
}
private:
Mat4 lerp(const Mat4& a, const Mat4& b, Real t) {
// Simple linear interpolation (you'd want proper slerp for rotations)
Mat4 result;
for (int i = 0; i < 16; i++) {
result.m[i] = a.m[i] + t * (b.m[i] - a.m[i]);
}
return result;
}
};
```
### Procedural Scene Generator
```c++
class ProceduralCity {
Scene& scene;
Node world;
public:
ProceduralCity(Scene& s) : scene(s), world(s.world()) {}
void generate(UInt width, UInt height, Real blockSize) {
for (UInt x = 0; x < width; x++) {
for (UInt z = 0; z < height; z++) {
generateBlock(x, z, blockSize);
}
}
generateStreets(width, height, blockSize);
}
private:
void generateBlock(UInt x, UInt z, Real blockSize) {
// Random building height
Real height = 10.0 + (rand() % 20);
Node building = scene.createNode("box",
String::format("building_%d_%d", x, z));
building["sizeX"] = blockSize * 0.8;
building["sizeY"] = height;
building["sizeZ"] = blockSize * 0.8;
// Position
Node xform = scene.createNode("xform",
String::format("building_xform_%d_%d", x, z));
xform["transform"]["pos"] = Pos3{
x * blockSize, height * 0.5, z * blockSize
};
building.parentTo(xform);
xform.parentTo(world);
// Random material
Node material = createRandomMaterial(x, z);
building["material"] = material;
}
void generateStreets(UInt width, UInt height, Real blockSize) {
// Generate street grid
for (UInt x = 0; x <= width; x++) {
createStreet(x * blockSize, 0, x * blockSize, height * blockSize,
2.0, String::format("street_ns_%d", x));
}
for (UInt z = 0; z <= height; z++) {
createStreet(0, z * blockSize, width * blockSize, z * blockSize,
2.0, String::format("street_ew_%d", z));
}
}
void createStreet(Real x1, Real z1, Real x2, Real z2, Real width, String name) {
Node street = scene.createNode("plane", name);
Real length = sqrt((x2-x1)*(x2-x1) + (z2-z1)*(z2-z1));
street["sizeX"] = length;
street["sizeY"] = width;
Node xform = scene.createNode("xform", name + "_xform");
xform["transform"]["pos"] = Pos3{(x1+x2)*0.5, 0, (z1+z2)*0.5};
if (x1 != x2) {
Real angle = atan2(z2-z1, x2-x1) * 180.0 / math::pi;
xform["transform"]["rot"] = Vec3{0, angle, 0};
}
street.parentTo(xform);
xform.parentTo(world);
// Asphalt material
Node material = scene.createNode("pbr", name + "_material");
material["base"]["color"] = Rgba{0.1, 0.1, 0.1, 1.0};
material["base"]["roughness"] = 0.8;
street["material"] = material;
}
Node createRandomMaterial(UInt x, UInt z) {
// Pseudo-random but deterministic
UInt seed = x * 1000 + z;
srand(seed);
Node material = scene.createNode("pbr",
String::format("material_%d_%d", x, z));
// Random color
Real r = 0.5 + (rand() % 50) / 100.0;
Real g = 0.5 + (rand() % 50) / 100.0;
Real b = 0.5 + (rand() % 50) / 100.0;
material["base"]["color"] = Rgba{r, g, b, 1.0};
material["base"]["metallic"] = (rand() % 100) < 20 ? 0.8 : 0.0;
material["base"]["roughness"] = 0.2 + (rand() % 60) / 100.0;
return material;
}
};
```
## Troubleshooting Guide
### Common Issues and Solutions
**Problem**: Render appears black or empty
```c++
// Check these common issues:
// 1. No geometry visible to camera
NodeVector meshes = scene.nodes("geometry");
if (meshes.empty()) {
logError("No geometry in scene");
}
// 2. Camera looking in wrong direction
Node camera = scene.camera();
Path cameraPath = scene.cameraPath();
Mat4 transform = cameraPath.transform();
logInfo("Camera position: %s", String::format(transform.translation()).buf());
// 3. No lighting
Node environment = scene.environment();
if (!environment) {
logWarning("No environment lighting set");
}
// 4. Objects not in hierarchy
Node world = scene.world();
PathVector paths = scene.paths("geometry");
if (paths.empty()) {
logError("No geometry paths found - check hierarchy");
}
```
**Problem**: Poor render performance
```c++
// Performance diagnostics:
// 1. Check geometry complexity
for (Node mesh : scene.nodes("mesh")) {
UInt pointCount = mesh.pointCloudCount();
if (pointCount > 100000) {
logWarning("High-poly mesh: %s (%d points)",
mesh.id().buf(), pointCount);
}
}
// 2. Check texture sizes
for (Node texture : scene.nodes("fileTexture")) {
String path = texture["path"].asString();
// Check file size, recommend appropriate resolution
}
// 3. Monitor solver settings
Node beautyPass = scene.beautyPass();
Real maxTime = beautyPass["maxTime"].asReal();
if (maxTime < 0) {
logInfo("No time limit set - render may run indefinitely");
}
```
**Problem**: Memory usage growing
```c++
// Memory management:
// 1. Clean up unused nodes
UInt removed = scene.clearNodes(true);
logInfo("Removed %d unreferenced nodes", removed);
// 2. Check for leaks in observers
class LeakCheckObserver : public EngineObserver {
static UInt instanceCount;
public:
LeakCheckObserver() { instanceCount++; }
~LeakCheckObserver() { instanceCount--; }
static UInt getInstanceCount() { return instanceCount; }
};
// 3. Monitor node counts
logInfo("Scene has %d nodes", scene.nodeCount());
```
## Conclusion
The Bella Engine SDK provides a powerful, flexible foundation for 3D rendering applications. Its nodal architecture makes complex scenes manageable, while the clean C++ API ensures your code remains readable and maintainable.
Key takeaways for successful development:
1. **Start simple**: Begin with basic scenes and gradually add complexity
2. **Use the type system**: Let the SDK prevent errors with its type safety
3. **Embrace the nodal paradigm**: Think in terms of connected nodes, not monolithic objects
4. **Monitor performance**: Use observers and profiling to keep renders responsive
5. **Leverage the foundation**: The `dl_core` library provides robust utilities
The SDK's design philosophy of "optimistic programming" means you can write clean, chainable code without constant null checks. Combined with automatic memory management and a comprehensive type system, this makes development both faster and more reliable.
Whether you're building the next great 3D application or adding rendering capabilities to existing software, the Bella Engine SDK provides the tools you need to create stunning visuals with clean, maintainable code.
Happy rendering!
---
Copyright © 2024 Diffuse Logic, all rights reserved.