# 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.