# GenericOSGB **Repository Path**: xarray/generic-osgb ## Basic Information - **Project Name**: GenericOSGB - **Description**: A completely dependency-free way to read the classic .osgb format (oblique photography model data), which can then be used in all 3D engines and web-based engines. - **Primary Language**: C++ - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-10-13 - **Last Updated**: 2026-10-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # generic_wrappers A **zero-OSG-dependency** re-implementation of the OpenSceneGraph serialization subsystem (the `osgDB` "wrappers"). It reads `.osgt` (ASCII) and `.osgb` (binary) files and reconstructs the object graph **without linking against OpenSceneGraph**. The `generic_osg/` directory contains one translation unit per OSG class, adapted from OSG's own serializer sources so that the same read logic can be driven standalone. --- ## 1. How it works (simulating OSG serialize wrappers) OSG stores objects through a per-class *wrapper*: `REGISTER_OBJECT_WRAPPER(name, prototype, class, associates)` declares a list of serializable properties using serializer macros (`ADD_INT_SERIALIZER`, `ADD_OBJECT_SERIALIZER`, `ADD_VEC3F_SERIALIZER`, `BEGIN_ENUM_SERIALIZER`, ...). When loading, OSG uses the class name read from the stream to find the wrapper and then reads properties in the declared order. This project re-implements that mechanism standalone: * **Registry** — `Rewrapper` / `RewrapperManager` (in `GenericReserializer.h`) hold, per class name, an ordered list of `(propertyName, BaseSerializer)` pairs, mirroring the OSG wrapper layout. * **Serializer kinds** — `ValueSerializer`, `BoolSerializer`, `StringSerializer`, `ObjectSerializer`, `ImageSerializer`, `VectorSerializer`, `ListSerializer`, `EnumSerializer`, `BitFlagSerializer`, `GLenumSerializer`, and `UserSerializer` (a custom read callback). Each knows how to pull its value off the stream. * **Registration** — the `REGISTER_OBJECT_WRAPPER` macro expands into a static `RegisterWrapperProxy` that registers the wrapper into `RewrapperManager` **before `main()` runs**. Because those registrars are never referenced explicitly, the archive is linked with *whole-archive* (`--whole-archive` / `/WHOLEARCHIVE`) by default; with `-DENABLE_STATIC_LIBRARY=ON` the executable instead keeps them alive by explicitly activating each library through `GenericStaticWrappers.h` (see §3). * **Dispatch** — `SerializerVisitor` walks a wrapper and routes every serializer to its read path (`readSerializerValue`), keeping the reader generic. * **Math / base layer** — `GenericMath.h` defines a minimal `osg` namespace (`Referenced`/`ref_ptr`, `Vec` and the `Vec2*/Vec3*/Vec4*` families, `Quat`, `Plane`, `Matrix`, `BoundingBox*`, `BoundingSphere*`, GL typedefs) so the adapted `generic_osg/*.cpp` sources compile with no `libosg`. * **I/O layer** — `GenericInputStream.h/.cpp` provide `InputIterator` (`BinaryInputIterator` / `AsciiInputIterator`) and `InputStream`. The format is detected from the first 8 header bytes; `.osgt` is parsed as text, `.osgb` is read as raw binary (byte order handled via `setByteSwap`). * **Output** — reading does **not** construct real OSG objects. `readObject()` walks the stream and records the result into `InputUserData`: a scene tree (`TreeNode` = class name, id, file offset, parent/child links) plus explicit commands (`ud.add("setXxx", ...)` / `ud.add("addChild", ...)`) emitted by the user-serializer `read*` functions. Those commands are forwarded to the optional `SerializerVisitor` and/or recorded into a neutral `SceneIR` (see `SceneIR.h`) that a host adapter can replay to rebuild the graph in another engine. Entry point (`GenericReader.h`): ```cpp osgVerse::loadRewrappers(); osgVerse::InputUserData ud; osgVerse::ObjectTypeAndID oid = osgVerse::readObjectFile("model.osgb", ud); // oid._type is the top-level class name, e.g. "osg::MatrixTransform" ``` ### Current format support * `.osgb` — **uncompressed** binary and `.osgt` — ASCII text. * zlib-compressed `.osgb` streams are **not** supported yet (`InputStream::decompress()` rejects any compressor other than `"0"`). --- ## 2. Why dependency-free `.osgb` reading matters `.osgb` is OSG's compact, widely used runtime format, but consuming it normally means building and linking the full OpenSceneGraph library — a large, platform- and toolchain-sensitive C++ dependency with ABI constraints. Reading `.osgb` without OSG makes it possible to: * **Inspect and validate** assets (structure, versions, class usage) on any machine, in CI, or in offline tooling. * **Convert / migrate** OSG assets into other renderers and engines without an OSG build step. * **Ship a lightweight decoder** into environments where `libosg` cannot be used: browsers/WASM, mobile, embedded, sandboxes, and server-side pipelines. * **Own the format layer** — custom memory handling, streaming, and re-serialization strategies are under direct control instead of being tied to OSG internals. The whole library depends only on the C++17 standard library. --- ## 3. Build Requirements: a C++17 compiler and CMake >= 3.10. No third-party libraries. ```bash cmake -S . -B build cmake --build build ``` This produces the dependency-free static library `osgVerseGenericReserializer` and two executables that depend only on it: * `reserializing_test` — parses a file and prints the object scene tree. * `null_integration` — rebuilds the neutral `SceneIR` and prints everything the IR describes (object/command histograms, data payloads, full scene tree). Run them on any `.osgt` / `.osgb` file: ```bash ./build/reserializing_test [--force-ascii] ./build/null_integration ``` Output of `reserializing_test` reports the number of registered rewrappers, the top-level object class, and the parsed scene tree: ``` Rewrappers: 461 Top object : osg::MatrixTransform (id=1) Scene tree : 519 objects read - osg::MatrixTransform (id=1, @0x19) ... Done. ``` `--force-ascii` skips binary detection and parses the input as ASCII text. ### Static library (`ENABLE_STATIC_LIBRARY`) By default the core archive is linked with *whole-archive* so the static registrars survive. `-DENABLE_STATIC_LIBRARY=ON` replaces that with an explicit, plain static link: nothing is force-included, and each executable keeps the registrars alive by including `GenericStaticWrappers.h` — guarded by the `OSGVERSE_GENERIC_USE_STATIC_ACTIVATION` define the option adds to it: ```bash cmake -S . -B build-static -DENABLE_STATIC_LIBRARY=ON cmake --build build-static ``` `GenericStaticWrappers.h` mirrors osgDB's `USE_SERIALIZER_WRAPPER_LIBRARY`: it references one `wrapper_serializer_library_()` anchor per ported library (defined in each `generic_osg*/LibraryWrapper.cpp`), which is what makes the linker keep those members and, transitively, every per-class registrar. Both modes register the same **461** wrappers — `reserializing_test` prints the count (`Rewrappers: 461`) and both produce byte-identical `null_integration` output. ### WebAssembly (Emscripten) The core and the dependency-free executables (`reserializing_test`, `null_integration`) build to WASM unchanged, since they use only the standard library. With Emscripten installed (this tree uses `~/Public/emsdk`): ```bash source ~/Public/emsdk/emsdk_env.sh emcmake cmake -S . -B build-wasm -DENABLE_STATIC_LIBRARY=ON \ -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXE_LINKER_FLAGS="-sNODERAWFS=1 -sEXIT_RUNTIME=1" cmake --build build-wasm ``` This emits `build-wasm/reserializing_test.js` + `.wasm` and `null_integration.js` + `.wasm`. Run them under node — `-sNODERAWFS=1` lets the module read files from the host filesystem and `-sEXIT_RUNTIME=1` makes the process exit after `main()`: ```bash node build-wasm/reserializing_test.js ~/Public/Assets/osg-data/cessna.osgt node build-wasm/null_integration.js ~/Public/Assets/osg-data/clock.osgt ``` `ENABLE_STATIC_LIBRARY=ON` is recommended for WASM (the explicit activation avoids depending on whole-archive); the default whole-archive path also links under Emscripten. `-sNODERAWFS=1` is only needed to read host files under node — in a browser, mount the asset (or embed it with `--preload-file`) instead. ### Engine examples Concrete engine adapters live under `examples/`; each one pulls in its own engine SDK and is disabled by default. All of them read the same neutral `SceneIR`, so they share one implementation strategy (create engine objects, then replay the recorded commands). Engines are enabled with `-DENABLE_=ON` and their `-D_ROOT=`. **OpenSceneGraph** — rebuilds a real osg scene through `osgDB::ObjectWrapperManager` and renders it with `osgViewer`: ```bash cmake -S . -B build -DENABLE_OSG=ON -DOSG_ROOT=/path/to/osg-sdk # include/ + lib/ cmake --build build ./build/osg_integration # render ./build/osg_integration --list # rebuild + report only ``` **VulkanSceneGraph (VSG)** — rebuilds the IR into VSG's default scene graph (`vsg::Group` / `vsg::MatrixTransform` / `vsg::LOD` / `vsg::Switch`, with `vsg::VertexIndexDraw` / `vsg::VertexDraw` and a Phong graphics pipeline built per geometry) and renders it with `vsg::Viewer`. Textures are loaded through `vsgXchange` (SGI `.rgb` textures, which `vsgXchange` has no reader for, are decoded locally), and the camera is framed from the computed scene bounds: ```bash cmake -S . -B build -DENABLE_VSG=ON -DVSG_ROOT=/path/to/vsg-sdk # include/ + lib/cmake/ cmake --build build ./build/vsg_integration # render ./build/vsg_integration --list # rebuild + report only ``` The adapter itself lives in `examples/vsg_scene_builder.h`, so it can also be registered as a `vsg::ReaderWriter`. `vsg_plugin_integration` does exactly that, which turns `osg::PagedLOD` / `osg::ProxyNode` into `vsg::PagedLOD` and lets VSG's own `DatabasePager` stream the child `.osgb` files through the generic reader: ```bash ./build/vsg_plugin_integration --list # report only ./build/vsg_plugin_integration --load-levels 2 # preload 2 levels ./build/vsg_plugin_integration --lod-ratio 0.25 # pager switch ratio ./build/vsg_plugin_integration -f 300 # run 300 frames ``` Images embedded inside an `.osgb` (osg's "inline file" storage) are decoded from memory through `vsgXchange`, so databases that carry their JPEG/PNG textures inside the tile files render without any texture files on disk. See §4 for how the two VSG examples differ. ### Directory layout ``` . dependency-free core (reader, IR, math layer) generic_osg/ one wrapper per osg core class generic_osgterrain/ osgTerrain generic_osganimation/ osgAnimation generic_osgparticle/ osgParticle generic_osgvolume/ osgVolume generic_osgsim/ osgSim generic_osgmanipulator/ osgManipulator generic_osgshadow/ osgShadow generic_osgui/ osgUI generic_osgga/ osgGA generic_osgfx/ osgFX generic_osgviewer/ osgViewer generic_osgtext/ osgText generic_osgutil/ osgUtil examples/ example programs (null_integration, osg_integration, ...) ``` Every osg library shipped with OpenSceneGraph has been ported this way: create `generic_osg/`, take the wrappers from `OpenSceneGraph/src/osgWrappers/serializers/osg/`, include `GenericReserializer.h`, turn the `read*` functions into the `ud.add("setXxx", ...)` form, drop the `check*`/`write*` helpers, then add the directory to the library sources in `CMakeLists.txt`. --- ## 4. Embedding and rendering in 3D engines An engine adapter is a single class implementing `ISceneBuilder` (`SceneIR.h`): ```cpp struct ISceneBuilder { virtual void onObject (const IRObject&, int index) = 0; // create the engine object virtual void onCommand(const IRObject&, int index, const IRCommand&) = 0; // apply one property / command virtual void finish() {} }; ``` `buildScene(ir, builder)` replays the recorded read order into it. An adapter never sees the file format — only object class names, typed arguments and object references — so supporting a new engine means writing one of these classes. `examples/` currently contains: * **OSG (echo back)** — `osg_integration.cpp`: rebuilds real osg objects through `osgDB::ObjectWrapperManager` and renders with `osgViewer`. Handy as the reference for what osg itself would produce from the same file. * **VulkanSceneGraph** — the adapter lives in `vsg_scene_builder.h` and is shared by two front ends: `vsg_integration.cpp` and `vsg_plugin_integration.cpp`. * **Null** — `null_integration.cpp`: zero-dependency, prints what the IR contains (class/command histograms, scene tree, data payload summaries). ### VSG: `vsg_integration` vs `vsg_plugin_integration` Both front ends use the same adapter (`examples/vsg_scene_builder.h` — geometry, material, texture, `vsg::LOD` and `vsg::PagedLOD` conversion), so they produce the same scene graph and the same image. The difference is **how the file is reached**: * `vsg_integration` reads the file itself: `readSceneIR()` → `VsgSceneBuilder` → `buildScene()`, and hands the result straight to `vsg::Viewer`. VSG's IO layer is never involved, so `vsg::read()` has no reader for `.osgb`/`.osgt`. A `PagedLOD`/`ProxyNode` is still rebuilt as a `vsg::PagedLOD`, but VSG's `DatabasePager` cannot resolve the child format and logs `Failed to read ... Tile_1_L15_0.osgb`. * `vsg_plugin_integration` wraps that same adapter in a `vsg::ReaderWriter` (`OsgbReaderWriter`) and adds it to `vsg::Options`, then loads the root through `vsg::read()`. Because VSG's own IO can resolve the format now, its `DatabasePager` streams the children in on demand: ``` osg::PagedLOD / osg::ProxyNode -> vsg::PagedLOD{ filename = child .osgb, children[1] = in-memory fallback, options = ... } -> DatabasePager: vsg::read(filename, plod->options) -> OsgbReaderWriter::read() -> readSceneIR() -> VsgSceneBuilder -> merged back in as children[0] ``` | Aspect | `vsg_integration` | `vsg_plugin_integration` | |---|---|---| | Reads via | `readSceneIR()` directly | `vsg::read()` through the registered `vsg::ReaderWriter` | | PagedLOD / ProxyNode | node is built, but the pager cannot load the child format | children are streamed in by VSG's `DatabasePager`, or synchronously with `--load-levels N` | | Extra options | — | `--load-levels N`, `--lod-ratio R`, `--max-paged-lod N`, `-f/--frames` | | Reports | IR objects/events, rebuilt objects, draws, paged LODs, missing images, unhandled properties | root, paged LODs, the child `FileName` list discovered per file, preloaded count | | Render graph | `createRenderGraphForView` + explicit `CommandGraph` | `createCommandGraphForView` + `ResourceHints` on the root (pre-sizes Vulkan resources for the paged database) | `--list` on a paged database shows the child `FileName` list that each `osg::PagedLOD` / `osg::ProxyNode` exposed (see §3 for the build/run commands): ``` $ vsg_plugin_integration Tile_1.osgb --list --load-levels 4 Root : vsg::PagedLOD PagedLODs : 6 Paging tree (file -> child FileName list): Tile_1.osgb -> Tile_1_L15_0.osgb Tile_1_L15_0.osgb -> Tile_1_L16_00.osgb Tile_1_L16_00.osgb -> Tile_1_L17_000.osgb Tile_1_L17_000.osgb -> Tile_1_L18_0000t3.osgb Tile_1_L18_0000t3.osgb -> Tile_1_L19_00000t3.osgb Tile_1_L19_00010t3.osgb ``` Adapter notes / current gaps (the full list of osg classes that are *not* converted, with the reason for each, is in the header comment of `examples/vsg_scene_builder.h`): * `osg::Program`/`Shader`/`Uniform` (custom GLSL), `osg::TexGen`, `TexEnv`/`TexEnvCombine`, scene-graph lights, clip planes, cube maps and `osg::ShapeDrawable` are not translated, so those assets keep the default Phong look (or draw nothing, for parametric shapes). * Textures follow osg's row layout: osg stores image rows bottom-up (`osg::Image`'s default `BOTTOM_LEFT`, and its jpeg/png plugins flip the file rows on load), while `vsgXchange` returns top-down rows. Decoded images are flipped back (`toOsgRowOrder()`) so osg's texture coordinates land on the same texels instead of every texture coming out mirrored. * `osg::TexGen` (e.g. `SPHERE_MAP`), cube maps and custom `Shader`/`Program` assets are not converted yet, so those few assets (cow's environment map, `cube_mapped_torus`, the `glsl_*` samples) render without their special effect. * The Phong pipeline is created per geometry, so the first pass ignores `StateSet` inheritance and per-geometry state overrides. Planned / to be filled in: * Web (three.js / WebGL / WebGPU) — * Babylon.js — * Godot — * Unity — * Unreal Engine —