diff --git a/.gitignore b/.gitignore index fe08127d..50664d6b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,9 @@ /.vscode/compile_commands.json /compile_commands.json /.cache/ -/build/ /.idea/ +/.trae/ .DS_Store data -output \ No newline at end of file +compile_commands.json \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 3987fe50..eb7a810b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,8 @@ endif() if (WIN32) target_compile_definitions(_3dtile PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN) + target_compile_options(_3dtile PRIVATE /EHsc) + target_compile_options(_3dtile PRIVATE /bigobj) target_compile_options(_3dtile PRIVATE $<$:/Zc:preprocessor>) target_precompile_headers(_3dtile PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/osg_fix.h") endif() diff --git a/Cargo.toml b/Cargo.toml index 910713f6..c742a5b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "_3dtile" version = "0.1.0" -authors = ["fanzhenhua "] +authors = ["fanzhenhua ", "wallance "] edition = "2021" [dependencies] @@ -21,6 +21,8 @@ env_logger = "0.11.8" # byteorder = "1.2" +thiserror = "2.0.18" + [build-dependencies] cmake = "0.1" pkg-config = "0.3" diff --git a/build.rs b/build.rs index d27160be..e8cb1bee 100644 --- a/build.rs +++ b/build.rs @@ -1,731 +1,55 @@ extern crate cmake; -use std::{env, fs, io, path::Path}; -use cmake::Config; +use std::path::Path; -// Copy CMake's compile_commands.json into a stable workspace path for editors. -fn export_compile_commands(out_dir: &Path) { - let cmake_build_dir = out_dir.join("build"); - let src = cmake_build_dir.join("compile_commands.json"); - if !src.exists() { - println!("cargo:warning=compile_commands.json not found at {}", src.display()); - return; - } - - let dst_dir = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join("build"); - if let Err(err) = fs::create_dir_all(&dst_dir) { - println!("cargo:warning=failed to create build dir {}: {}", dst_dir.display(), err); - return; - } - - let dst = dst_dir.join("compile_commands.json"); - match fs::copy(&src, &dst) { - Ok(_) => println!("cargo:warning=exported compile_commands.json to {}", dst.display()), - Err(err) => println!( - "cargo:warning=failed to copy compile_commands.json to {}: {}", - dst.display(), - err - ), - } -} - -fn create_vcpkg_installed_dir_symlink() -> std::io::Result<()> { - let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR") - .expect("Failed to get CARGO_MANIFEST_DIR environment variable"); - let root_vcpkg_installed_dir = Path::new(&cargo_manifest_dir).join("vcpkg_installed"); - if root_vcpkg_installed_dir.exists() { - let out_dir = env::var("OUT_DIR").unwrap(); - let build_vcpkg_installed_dir = Path::new(&out_dir).join("build").join("vcpkg_installed"); - - // ensure all parent dirs exists - let parent_build_vcpkg_installed_dir = build_vcpkg_installed_dir.parent().expect(&format!("there is no parent directory in {}", build_vcpkg_installed_dir.display())); - fs::create_dir_all(parent_build_vcpkg_installed_dir).expect("Failed to create build dir"); - - println!("cargo:warning=build_vcpkg_installed_dir: {}", build_vcpkg_installed_dir.display()); - - if build_vcpkg_installed_dir.exists() { - println!( - "cargo:warning=build_vcpkg_installed_dir already exists, so there is no need to create it again.: {:?}", - build_vcpkg_installed_dir - ); - return Ok(()); - } - - #[cfg(target_family = "unix")] - { - std::os::unix::fs::symlink(&root_vcpkg_installed_dir, &build_vcpkg_installed_dir) - .expect(&format!("Failed to create symlink for Unix-like os from {} -> {}", root_vcpkg_installed_dir.display(), build_vcpkg_installed_dir.display())); - } - - #[cfg(target_family = "windows")] - { - std::os::windows::fs::symlink_dir(&root_vcpkg_installed_dir, &build_vcpkg_installed_dir) - .expect(&format!("Failed to create symlink for windows os from {} -> {}", root_vcpkg_installed_dir.display(), build_vcpkg_installed_dir.display())); - } - } - - Ok(()) -} - -fn build_win_msvc() { - // Get VCPKG_ROOT environment variable - let vcpkg_root = std::env::var("VCPKG_ROOT").expect("VCPKG_ROOT environment variable is not set"); - - let vcpkg_has_been_installed = env::var("VCPKG_HAS_BEEN_INSTALLED").unwrap_or_default() == "1"; - if vcpkg_has_been_installed { - create_vcpkg_installed_dir_symlink().expect("create_dir_symlink fail"); - } - - // Check if strict mode is enabled via environment variable - let enable_strict = env::var("ENABLE_STRICT_CHECKS").unwrap_or_default() == "1"; - - let mut config = Config::new("."); - config - .define("CMAKE_TOOLCHAIN_FILE", format!("{}/scripts/buildsystems/vcpkg.cmake", vcpkg_root)) - .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON") - .very_verbose(true); - - if enable_strict { - println!("cargo:warning=Building with STRICT CHECKS enabled (CI mode)"); - config.define("ENABLE_STRICT_CHECKS", "ON"); - } - - let dst = config.build(); - println!("cmake dst = {}", dst.display()); - // Link Search Path for C++ Implementation - println!("cargo:rustc-link-search=native={}/lib", dst.display()); - // for FFI C++ static library - println!("cargo:rustc-link-lib=static=_3dtile"); - - // Link Search Path for Draco library - let source_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR environment variable is not set"); - // Link Search Path for Draco library - println!("cargo:rustc-link-search=native={}/thirdparty/draco", Path::new(&source_dir).display()); - println!("cargo:rustc-link-lib=static=draco"); - - let out_dir = env::var("OUT_DIR").unwrap(); - println!("cargo:warning=out_dir = {}", &out_dir); - export_compile_commands(Path::new(&out_dir)); - print_vcpkg_tree(Path::new(&out_dir)).unwrap(); - // vcpkg_installed path - let vcpkg_installed_dir = Path::new(&out_dir) - .join("build") - .join("vcpkg_installed") - .join("x64-windows"); - - // Link Search Path for third party library - let vcpkg_installed_lib_dir = vcpkg_installed_dir.join("lib"); - println!("cargo:rustc-link-search=native={}", vcpkg_installed_lib_dir.display()); - - // Determine if building in debug or release mode - let profile = env::var("PROFILE").unwrap_or("release".to_string()); - let is_debug = profile == "debug"; - let geolib_lib_name = if is_debug { - "GeographicLib_d-i" - } else { - "GeographicLib-i" - }; - println!("cargo:warning=Building in {} mode, linking GeographicLib as: {}", profile, geolib_lib_name); - - // 1. FFI static - println!("cargo:rustc-link-lib=static=_3dtile"); - println!("cargo:rustc-link-lib=static=ufbx"); - - // 2. OSG - println!("cargo:rustc-link-lib=osgUtil"); - println!("cargo:rustc-link-lib=osgDB"); - println!("cargo:rustc-link-lib=osg"); - //let osg_plugins_lib = vcpkg_installed_lib_dir.join("osgPlugins-3.6.5"); - //println!("cargo:rustc-link-search=native={}", osg_plugins_lib.display()); - - // 3. OpenThreads (依赖 _3dtile) - println!("cargo:rustc-link-lib=OpenThreads"); - - // 4. GDAL dependencies - println!("cargo:rustc-link-lib=gdal"); - println!("cargo:rustc-link-lib=basisu_encoder"); - println!("cargo:rustc-link-lib=meshoptimizer"); - // zstd is required by gdal/basisu when building KTX2 (supercompression) and some GDAL drivers - println!("cargo:rustc-link-lib=zstd"); - - // GeographicLib for geoid height calculation - println!("cargo:rustc-link-lib={}", geolib_lib_name); - - // 5. sqlite - println!("cargo:rustc-link-lib=sqlite3"); - - // copy gdal and proj data - let vcpkg_share_dir = vcpkg_installed_dir.join("share"); - copy_gdal_data(vcpkg_share_dir.to_str().unwrap()); - copy_proj_data(vcpkg_share_dir.to_str().unwrap()); - - // copy OSG plugins - let osg_plugins_src = vcpkg_installed_dir.join("plugins").join("osgPlugins-3.6.5"); - copy_osg_plugins(osg_plugins_src.to_str().unwrap()); -} - -fn get_target_dir() -> std::path::PathBuf { - let profile = env::var("PROFILE").unwrap(); - let target_dir = - Path::new(&env::var("CARGO_TARGET_DIR").unwrap_or("target".into())).join(&profile); - target_dir -} - -fn build_linux_unknown() { - let vcpkg_root = std::env::var("VCPKG_ROOT").expect("VCPKG_ROOT environment variable is not set"); - - let vcpkg_has_been_installed = env::var("VCPKG_HAS_BEEN_INSTALLED").unwrap_or_default() == "1"; - if vcpkg_has_been_installed { - create_vcpkg_installed_dir_symlink().expect("create_dir_symlink fail"); - } - - // Check if strict mode is enabled via environment variable - let enable_strict = env::var("ENABLE_STRICT_CHECKS").unwrap_or_default() == "1"; - - // Get VCPKG_ROOT environment variable - let mut config = Config::new("."); - config - .define("CMAKE_TOOLCHAIN_FILE",format!("{}/scripts/buildsystems/vcpkg.cmake", vcpkg_root)) - .define("CMAKE_C_COMPILER", "/usr/bin/gcc") - .define("CMAKE_CXX_COMPILER", "/usr/bin/g++") - .define("CMAKE_MAKE_PROGRAM", "/usr/bin/make") - .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON") - .very_verbose(true); - - if enable_strict { - println!("cargo:warning=Building with STRICT CHECKS enabled (CI mode)"); - config.define("ENABLE_STRICT_CHECKS", "ON"); - } - - let dst = config.build(); - println!("cmake dst = {}", dst.display()); - // Link Search Path for C++ Implementation - println!("cargo:rustc-link-search=native={}/lib", dst.display()); - - // Link Search Path for Draco library - let source_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - // Link Search Path for Draco library - println!("cargo:rustc-link-search=native={}/thirdparty/draco", Path::new(&source_dir).display()); - println!("cargo:rustc-link-lib=static=draco"); - - let out_dir = env::var("OUT_DIR").unwrap(); - println!("cargo:warning=out_dir = {}", &out_dir); - export_compile_commands(Path::new(&out_dir)); - // print_vcpkg_tree(Path::new(&out_dir)).unwrap(); - // vcpkg_installed path - let vcpkg_installed_dir = Path::new(&out_dir) - .join("build") - .join("vcpkg_installed") - .join("x64-linux"); - // Link Search Path for third party library - let vcpkg_installed_lib_dir = vcpkg_installed_dir.join("lib"); - println!("cargo:rustc-link-search=native={}", vcpkg_installed_lib_dir.display()); - let osg_plugins_lib = vcpkg_installed_lib_dir.join("osgPlugins-3.6.5"); - println!("cargo:rustc-link-search=native={}", osg_plugins_lib.display()); - - // 0. System C++ library first - println!("cargo:rustc-link-lib=stdc++"); - println!("cargo:rustc-link-lib=z"); - - // 1. FFI static - println!("cargo:rustc-link-lib=static=_3dtile"); - println!("cargo:rustc-link-lib=static=ufbx"); - - // 2. OSG - // println!("cargo:rustc-link-lib=osgdb_jp2"); - println!("cargo:rustc-link-lib=GL"); - println!("cargo:rustc-link-lib=X11"); - println!("cargo:rustc-link-lib=Xi"); - println!("cargo:rustc-link-lib=Xrandr"); - println!("cargo:rustc-link-lib=dl"); - println!("cargo:rustc-link-lib=pthread"); - - // Note: On Linux, OSG plugins are static libraries (.a) that must be linked at build time - println!("cargo:rustc-link-lib=osgdb_jpeg"); - println!("cargo:rustc-link-lib=osgdb_tga"); - println!("cargo:rustc-link-lib=osgdb_rgb"); - println!("cargo:rustc-link-lib=osgdb_png"); - println!("cargo:rustc-link-lib=osgdb_osg"); - println!("cargo:rustc-link-lib=osgdb_serializers_osg"); - println!("cargo:rustc-link-lib=osgUtil"); - println!("cargo:rustc-link-lib=osgDB"); - println!("cargo:rustc-link-lib=osg"); - - // 3. OpenThreads (依赖 _3dtile) - println!("cargo:rustc-link-lib=OpenThreads"); - - // 4. GDAL dependencies - println!("cargo:rustc-link-lib=gdal"); - println!("cargo:rustc-link-lib=geos_c"); - println!("cargo:rustc-link-lib=geos"); - println!("cargo:rustc-link-lib=proj"); - println!("cargo:rustc-link-lib=sqlite3"); - println!("cargo:rustc-link-lib=expat"); - println!("cargo:rustc-link-lib=curl"); - println!("cargo:rustc-link-lib=ssl"); - println!("cargo:rustc-link-lib=crypto"); - println!("cargo:rustc-link-lib=uriparser"); - println!("cargo:rustc-link-lib=kmlbase"); - println!("cargo:rustc-link-lib=kmlengine"); - println!("cargo:rustc-link-lib=kmldom"); - println!("cargo:rustc-link-lib=kmlconvenience"); - // println!("cargo:rustc-link-lib=qhullcpp"); - println!("cargo:rustc-link-lib=Lerc"); - - // 5. Other dependencies - // println!("cargo:rustc-link-lib=hdf5_hl"); - // println!("cargo:rustc-link-lib=hdf5"); - println!("cargo:rustc-link-lib=json-c"); - // println!("cargo:rustc-link-lib=netcdf"); - println!("cargo:rustc-link-lib=sharpyuv"); - - // 6. Image / compression libraries - println!("cargo:rustc-link-lib=geotiff"); - println!("cargo:rustc-link-lib=gif"); - println!("cargo:rustc-link-lib=jpeg"); - println!("cargo:rustc-link-lib=png"); - println!("cargo:rustc-link-lib=tiff"); - println!("cargo:rustc-link-lib=webp"); - println!("cargo:rustc-link-lib=xml2"); - println!("cargo:rustc-link-lib=lzma"); - println!("cargo:rustc-link-lib=openjp2"); - println!("cargo:rustc-link-lib=qhullstatic_r"); - println!("cargo:rustc-link-lib=minizip"); - println!("cargo:rustc-link-lib=spatialite"); - println!("cargo:rustc-link-lib=freexl"); - println!("cargo:rustc-link-lib=basisu_encoder"); - println!("cargo:rustc-link-lib=meshoptimizer"); - // zstd is required by gdal/basisu when building KTX2 (supercompression) and some GDAL drivers - println!("cargo:rustc-link-lib=zstd"); - - // GeographicLib for geoid height calculation - println!("cargo:rustc-link-lib=GeographicLib"); - - let vcpkg_share_dir = vcpkg_installed_dir.join("share"); - copy_gdal_data(vcpkg_share_dir.to_str().unwrap()); - copy_proj_data(vcpkg_share_dir.to_str().unwrap()); - - // copy OSG plugins - let osg_plugins_src = vcpkg_installed_lib_dir.join("osgPlugins-3.6.5"); - copy_osg_plugins(osg_plugins_src.to_str().unwrap()); -} - -fn build_macos() { - let vcpkg_root = std::env::var("VCPKG_ROOT").expect("VCPKG_ROOT environment variable is not set"); - - let vcpkg_has_been_installed = env::var("VCPKG_HAS_BEEN_INSTALLED").unwrap_or_default() == "1"; - if vcpkg_has_been_installed { - create_vcpkg_installed_dir_symlink().expect("create_dir_symlink fail"); - } +// 引入构建模块 +#[path = "build/mod.rs"] +mod build; - // Check if strict mode is enabled via environment variable - let enable_strict = env::var("ENABLE_STRICT_CHECKS").unwrap_or_default() == "1"; +use build::{ + copy_gdal_data, copy_osg_plugins, copy_proj_data, create_vcpkg_symlink, + export_compile_commands, link_linux, link_macos, link_windows, BuildConfig, BuildError, + CMakeBuilder, +}; - // Get VCPKG_ROOT environment variable - let mut config = Config::new("."); - config - .define("CMAKE_TOOLCHAIN_FILE",format!("{}/scripts/buildsystems/vcpkg.cmake", vcpkg_root)) - .define("VCPKG_INSTALL_OPTIONS", "--allow-unsupported") - .define("CMAKE_C_COMPILER", "/usr/bin/clang") - .define("CMAKE_CXX_COMPILER", "/usr/bin/clang++") - .define("CMAKE_MAKE_PROGRAM", "/usr/bin/make") - .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON") - .very_verbose(true); - - if enable_strict { - println!("cargo:warning=Building with STRICT CHECKS enabled (CI mode)"); - config.define("ENABLE_STRICT_CHECKS", "ON"); - } - - let dst = config.build(); - println!("cmake dst = {}", dst.display()); - // Link Search Path for C++ Implementation - println!("cargo:rustc-link-search=native={}/lib", dst.display()); - - let source_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - // Link Search Path for Draco library - println!("cargo:rustc-link-search=native={}/thirdparty/draco", Path::new(&source_dir).display()); - println!("cargo:rustc-link-lib=static=draco"); - - let out_dir = env::var("OUT_DIR").unwrap(); - println!("cargo:warning=out_dir = {}", &out_dir); - export_compile_commands(Path::new(&out_dir)); - // print_vcpkg_tree(Path::new(&out_dir)).unwrap(); - // vcpkg_installed path - let vcpkg_installed_dir = Path::new(&out_dir) - .join("build") - .join("vcpkg_installed") - .join("arm64-osx"); - // Link Search Path for third party library - let vcpkg_installed_lib_dir = vcpkg_installed_dir.join("lib"); - println!("cargo:rustc-link-search=native={}", vcpkg_installed_lib_dir.display()); - let osg_plugins_lib = vcpkg_installed_lib_dir.join("osgPlugins-3.6.5"); - println!("cargo:rustc-link-search=native={}", osg_plugins_lib.display()); - - // 0. System C++ library first - println!("cargo:rustc-link-lib=c++"); - println!("cargo:rustc-link-lib=z"); - - // 1. FFI static - println!("cargo:rustc-link-lib=static=_3dtile"); - println!("cargo:rustc-link-lib=static=ufbx"); - - // 2. OSG - // Note: On macOS ARM64, OSG plugins are static libraries (.a) that must be linked at build time - println!("cargo:rustc-link-lib=osgdb_jpeg"); - println!("cargo:rustc-link-lib=osgdb_tga"); - println!("cargo:rustc-link-lib=osgdb_rgb"); - println!("cargo:rustc-link-lib=osgdb_png"); - println!("cargo:rustc-link-lib=osgdb_osg"); - println!("cargo:rustc-link-lib=osgdb_serializers_osg"); - println!("cargo:rustc-link-lib=osgUtil"); - println!("cargo:rustc-link-lib=osgDB"); - println!("cargo:rustc-link-lib=osg"); - - // 3. OpenThreads (依赖 _3dtile) - println!("cargo:rustc-link-lib=OpenThreads"); - - // 4. GDAL dependencies - println!("cargo:rustc-link-lib=gdal"); - println!("cargo:rustc-link-lib=geos_c"); - println!("cargo:rustc-link-lib=geos"); - println!("cargo:rustc-link-lib=proj"); - println!("cargo:rustc-link-lib=sqlite3"); - println!("cargo:rustc-link-lib=expat"); - println!("cargo:rustc-link-lib=curl"); - println!("cargo:rustc-link-lib=ssl"); - println!("cargo:rustc-link-lib=crypto"); - println!("cargo:rustc-link-lib=kmlbase"); - println!("cargo:rustc-link-lib=kmlengine"); - println!("cargo:rustc-link-lib=kmldom"); - println!("cargo:rustc-link-lib=kmlconvenience"); - // println!("cargo:rustc-link-lib=qhullcpp"); - println!("cargo:rustc-link-lib=Lerc"); - - // 5. Other dependencies - // println!("cargo:rustc-link-lib=hdf5_hl"); - // println!("cargo:rustc-link-lib=hdf5"); - println!("cargo:rustc-link-lib=json-c"); - // println!("cargo:rustc-link-lib=netcdf"); - println!("cargo:rustc-link-lib=sharpyuv"); - - // 6. Image / compression libraries - println!("cargo:rustc-link-lib=geotiff"); - println!("cargo:rustc-link-lib=gif"); - println!("cargo:rustc-link-lib=jpeg"); - println!("cargo:rustc-link-lib=png"); - println!("cargo:rustc-link-lib=tiff"); - println!("cargo:rustc-link-lib=webp"); - println!("cargo:rustc-link-lib=xml2"); - println!("cargo:rustc-link-lib=lzma"); - println!("cargo:rustc-link-lib=openjp2"); - println!("cargo:rustc-link-lib=qhullstatic_r"); - println!("cargo:rustc-link-lib=minizip"); - println!("cargo:rustc-link-lib=spatialite"); - println!("cargo:rustc-link-lib=freexl"); - println!("cargo:rustc-link-lib=basisu_encoder"); - println!("cargo:rustc-link-lib=meshoptimizer"); - // zstd is required by gdal/basisu when building KTX2 (supercompression) and some GDAL drivers - println!("cargo:rustc-link-lib=zstd"); - - // GeographicLib for geoid height calculation - println!("cargo:rustc-link-lib=GeographicLib"); - - // 7. System libraries / frameworks - println!("cargo:rustc-link-lib=c++"); - println!("cargo:rustc-link-lib=z"); - println!("cargo:rustc-link-lib=framework=Security"); - println!("cargo:rustc-link-lib=framework=CoreFoundation"); - println!("cargo:rustc-link-lib=framework=Foundation"); - println!("cargo:rustc-link-lib=framework=OpenGL"); - println!("cargo:rustc-link-lib=framework=AppKit"); - println!("cargo:rustc-link-lib=framework=SystemConfiguration"); - - // ----------------------------- - // Additional linker flags for Objective-C / Boost symbols in static libs - // ----------------------------- - println!("cargo:rustc-link-arg=-ObjC"); - println!("cargo:rustc-link-arg=-all_load"); - - let vcpkg_share_dir = vcpkg_installed_dir.join("share"); - copy_gdal_data(vcpkg_share_dir.to_str().unwrap()); - copy_proj_data(vcpkg_share_dir.to_str().unwrap()); - - // copy OSG plugins for macOS x86_64 - let osg_plugins_src = vcpkg_installed_lib_dir.join("osgPlugins-3.6.5"); - copy_osg_plugins(osg_plugins_src.to_str().unwrap()); -} - -fn build_macos_x86_64() { - let vcpkg_root = std::env::var("VCPKG_ROOT").expect("VCPKG_ROOT environment variable is not set"); - - let vcpkg_has_been_installed = env::var("VCPKG_HAS_BEEN_INSTALLED").unwrap_or_default() == "1"; - if vcpkg_has_been_installed { - create_vcpkg_installed_dir_symlink().expect("create_dir_symlink fail"); - } - - // Check if strict mode is enabled via environment variable - let enable_strict = env::var("ENABLE_STRICT_CHECKS").unwrap_or_default() == "1"; - - // Get VCPKG_ROOT environment variable - let mut config = Config::new("."); - config - .define("CMAKE_TOOLCHAIN_FILE",format!("{}/scripts/buildsystems/vcpkg.cmake", vcpkg_root)) - .define("CMAKE_C_COMPILER", "/usr/bin/clang") - .define("CMAKE_CXX_COMPILER", "/usr/bin/clang++") - .define("CMAKE_MAKE_PROGRAM", "/usr/bin/make") - .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON") - .very_verbose(true); - - if enable_strict { - println!("cargo:warning=Building with STRICT CHECKS enabled (CI mode)"); - config.define("ENABLE_STRICT_CHECKS", "ON"); - } - - let dst = config.build(); - println!("cmake dst = {}", dst.display()); - // Link Search Path for C++ Implementation - println!("cargo:rustc-link-search=native={}/lib", dst.display()); - - // Link Search Path for Draco library - let source_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - // Link Search Path for Draco library - println!("cargo:rustc-link-search=native={}/thirdparty/draco", Path::new(&source_dir).display()); - println!("cargo:rustc-link-lib=static=draco"); - - let out_dir = env::var("OUT_DIR").unwrap(); - println!("cargo:warning=out_dir = {}", &out_dir); - export_compile_commands(Path::new(&out_dir)); - // print_vcpkg_tree(Path::new(&out_dir)).unwrap(); - // vcpkg_installed path - let vcpkg_installed_dir = Path::new(&out_dir) - .join("build") - .join("vcpkg_installed") - .join("x64-osx"); - // Link Search Path for third party library - let vcpkg_installed_lib_dir = vcpkg_installed_dir.join("lib"); - println!( "cargo:rustc-link-search=native={}", vcpkg_installed_lib_dir.display()); - let osg_plugins_lib = vcpkg_installed_lib_dir.join("osgPlugins-3.6.5"); - println!("cargo:rustc-link-search=native={}", osg_plugins_lib.display()); - - // 0. System C++ library first - println!("cargo:rustc-link-lib=c++"); - println!("cargo:rustc-link-lib=z"); - - // 1. FFI static - println!("cargo:rustc-link-lib=static=_3dtile"); - println!("cargo:rustc-link-lib=static=ufbx"); - - // 2. OSG - // Note: On macOS x86_64, OSG plugins are static libraries (.a) that must be linked at build time - println!("cargo:rustc-link-lib=osgdb_jpeg"); - println!("cargo:rustc-link-lib=osgdb_tga"); - println!("cargo:rustc-link-lib=osgdb_rgb"); - println!("cargo:rustc-link-lib=osgdb_png"); - println!("cargo:rustc-link-lib=osgdb_osg"); - println!("cargo:rustc-link-lib=osgdb_serializers_osg"); - println!("cargo:rustc-link-lib=osgUtil"); - println!("cargo:rustc-link-lib=osgDB"); - println!("cargo:rustc-link-lib=osg"); - - // 3. OpenThreads (依赖 _3dtile) - println!("cargo:rustc-link-lib=OpenThreads"); - - // 4. GDAL dependencies - println!("cargo:rustc-link-lib=gdal"); - println!("cargo:rustc-link-lib=geos_c"); - println!("cargo:rustc-link-lib=geos"); - println!("cargo:rustc-link-lib=proj"); - println!("cargo:rustc-link-lib=sqlite3"); - println!("cargo:rustc-link-lib=expat"); - println!("cargo:rustc-link-lib=curl"); - println!("cargo:rustc-link-lib=ssl"); - println!("cargo:rustc-link-lib=crypto"); - println!("cargo:rustc-link-lib=kmlbase"); - println!("cargo:rustc-link-lib=kmlengine"); - println!("cargo:rustc-link-lib=kmldom"); - println!("cargo:rustc-link-lib=kmlconvenience"); - // println!("cargo:rustc-link-lib=qhullcpp"); - println!("cargo:rustc-link-lib=Lerc"); - - // 5. Other dependencies - // println!("cargo:rustc-link-lib=hdf5_hl"); - // println!("cargo:rustc-link-lib=hdf5"); - println!("cargo:rustc-link-lib=json-c"); - // println!("cargo:rustc-link-lib=netcdf"); - println!("cargo:rustc-link-lib=sharpyuv"); - - // 6. Image / compression libraries - println!("cargo:rustc-link-lib=geotiff"); - println!("cargo:rustc-link-lib=gif"); - println!("cargo:rustc-link-lib=jpeg"); - println!("cargo:rustc-link-lib=png"); - println!("cargo:rustc-link-lib=tiff"); - println!("cargo:rustc-link-lib=webp"); - println!("cargo:rustc-link-lib=xml2"); - println!("cargo:rustc-link-lib=lzma"); - println!("cargo:rustc-link-lib=openjp2"); - println!("cargo:rustc-link-lib=qhullstatic_r"); - println!("cargo:rustc-link-lib=minizip"); - println!("cargo:rustc-link-lib=spatialite"); - println!("cargo:rustc-link-lib=freexl"); - println!("cargo:rustc-link-lib=basisu_encoder"); - println!("cargo:rustc-link-lib=meshoptimizer"); - // zstd is required by gdal/basisu when building KTX2 (supercompression) and some GDAL drivers - println!("cargo:rustc-link-lib=zstd"); - - // GeographicLib for geoid height calculation - println!("cargo:rustc-link-lib=GeographicLib"); - - // 7. System libraries / frameworks - println!("cargo:rustc-link-lib=c++"); - println!("cargo:rustc-link-lib=z"); - println!("cargo:rustc-link-lib=framework=Security"); - println!("cargo:rustc-link-lib=framework=CoreFoundation"); - println!("cargo:rustc-link-lib=framework=Foundation"); - println!("cargo:rustc-link-lib=framework=OpenGL"); - println!("cargo:rustc-link-lib=framework=AppKit"); - println!("cargo:rustc-link-lib=framework=SystemConfiguration"); - - // ----------------------------- - // Additional linker flags for Objective-C / Boost symbols in static libs - // ----------------------------- - println!("cargo:rustc-link-arg=-ObjC"); - println!("cargo:rustc-link-arg=-all_load"); - - let vcpkg_share_dir = vcpkg_installed_dir.join("share"); - copy_gdal_data(vcpkg_share_dir.to_str().unwrap()); - copy_proj_data(vcpkg_share_dir.to_str().unwrap()); - - // copy OSG plugins for macOS x86_64 - let osg_plugins_src = vcpkg_installed_lib_dir.join("osgPlugins-3.6.5"); - copy_osg_plugins(osg_plugins_src.to_str().unwrap()); -} +fn main() -> Result<(), BuildError> { + // 启用完整回溯信息 + std::env::set_var("RUST_BACKTRACE", "full"); -fn copy_gdal_data(share: &str) { - let gdal_data = Path::new(share).join("gdal"); - let out_dir = get_target_dir().join("gdal"); + // 1. 从环境变量加载配置 + let config = BuildConfig::from_env()?; println!( - "gdal_data -> {}, out_dir -> {}", - gdal_data.display(), - out_dir.display() + "cargo:warning=Building for platform: {:?}", + config.platform ); - copy_dir_recursive(&gdal_data, &out_dir).unwrap(); -} + println!("cargo:warning=VCPKG_ROOT: {}", config.vcpkg_root.display()); -fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> { - if !dst.exists() { - fs::create_dir_all(dst)?; - } + // 2. 创建vcpkg符号链接(如果需要) + create_vcpkg_symlink(&config)?; - for entry in fs::read_dir(src)? { - let entry = entry?; - let path = entry.path(); - let dest_path = dst.join(entry.file_name()); + // 3. 执行CMake构建 + let mut cmake = CMakeBuilder::new(&config)?; + let _build_dir = cmake.build(); - if path.is_dir() { - copy_dir_recursive(&path, &dest_path)?; - } else { - fs::copy(&path, &dest_path)?; + // 4. 导出compile_commands.json + export_compile_commands(Path::new(&config.out_dir)); + + // 5. 平台特定的库链接 + match config.platform { + build::config::Platform::WindowsMsvc => link_windows(&config), + build::config::Platform::LinuxGnu => link_linux(&config), + build::config::Platform::MacOsArm64 | build::config::Platform::MacOsX86_64 => { + link_macos(&config) } } - Ok(()) -} -fn copy_proj_data(share: &str) { - let proj_data = Path::new(share).join("proj"); - let out_dir = get_target_dir().join("proj"); - println!( - "proj_data -> {}, out_dir -> {}", - proj_data.display(), - out_dir.display() - ); + // 6. 复制数据文件 + copy_gdal_data(&config); + copy_proj_data(&config); + copy_osg_plugins(&config); - copy_dir_recursive(&proj_data, &out_dir).unwrap(); -} + println!("cargo:warning=Build completed successfully!"); -fn copy_osg_plugins(plugins_dir: &str) { - let plugins_src = Path::new(plugins_dir); - let out_dir = get_target_dir().join("osgPlugins-3.6.5"); - - println!( - "osg_plugins -> {}, out_dir -> {}", - plugins_src.display(), - out_dir.display() - ); - - if plugins_src.exists() { - copy_dir_recursive(&plugins_src, &out_dir).unwrap(); - } else { - println!( - "cargo:warning=OSG plugins directory not found: {}", - plugins_dir - ); - } -} - -/// 打印 ./vcpkg_installed 下的目录树,用 cargo:warning 输出,这样能在 cargo build 输出中看到 -#[allow(dead_code)] -fn print_vcpkg_tree(root: &Path) -> io::Result<()> { - if !root.exists() { - println!("cargo:warning=path '{}' does not exist", root.display()); - return Ok(()); - } - fn walk(path: &Path, prefix: &str) -> io::Result<()> { - let meta = fs::metadata(path)?; - if meta.is_dir() { - println!( - "cargo:warning={}{}", - prefix, - path.file_name() - .map(|s| s.to_string_lossy()) - .unwrap_or_else(|| path.display().to_string().into()) - ); - let mut entries: Vec<_> = fs::read_dir(path)?.collect(); - entries.sort_by_key(|e| e.as_ref().map(|e| e.file_name()).ok()); - for entry in entries { - let entry = entry?; - let p = entry.path(); - if p.is_dir() { - walk(&p, &format!("{} ", prefix))?; - } else { - println!( - "cargo:warning={} {}", - prefix, - entry.file_name().to_string_lossy() - ); - } - } - } else { - println!("cargo:warning={} (file)", path.display()); - } - Ok(()) - } - // 根节点打印特殊处理 - println!("cargo:warning=Listing {}", root.display()); - for entry in fs::read_dir(root)? { - let entry = entry?; - let p = entry.path(); - if p.is_dir() { - walk(&p, "")?; - } else { - println!("cargo:warning= {}", entry.file_name().to_string_lossy()); - } - } Ok(()) } - -fn main() { - std::env::set_var("RUST_BACKTRACE", "full"); - match env::var("TARGET") { - Ok(val) => match val.as_str() { - "x86_64-unknown-linux-gnu" => build_linux_unknown(), - "x86_64-pc-windows-msvc" => build_win_msvc(), - "aarch64-apple-darwin" => build_macos(), - "x86_64-apple-darwin" => build_macos_x86_64(), - &_ => {} - }, - _ => {} - } -} diff --git a/build/cmake.rs b/build/cmake.rs new file mode 100644 index 00000000..cd304e65 --- /dev/null +++ b/build/cmake.rs @@ -0,0 +1,73 @@ +//! CMake构建抽象 + +use crate::build::config::{BuildConfig, BuildError, Platform}; +use cmake::Config; +use std::path::PathBuf; + +/// CMake构建器 +pub struct CMakeBuilder { + config: Config, +} + +impl CMakeBuilder { + /// 创建新的CMake构建器 + pub fn new(build_config: &BuildConfig) -> Result { + let mut config = Config::new(&build_config.source_dir); + + // 配置基础选项 + config + .define( + "CMAKE_TOOLCHAIN_FILE", + format!( + "{}/scripts/buildsystems/vcpkg.cmake", + build_config.vcpkg_root.display() + ), + ) + .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON") + .very_verbose(true); + + // 平台特定编译器配置 + Self::configure_compiler(&mut config, &build_config.platform); + + // 严格模式 + if build_config.enable_strict { + println!("cargo:warning=Building with STRICT CHECKS enabled (CI mode)"); + config.define("ENABLE_STRICT_CHECKS", "ON"); + } + + // macOS特殊配置 + if build_config.platform.is_macos() { + config.define("VCPKG_INSTALL_OPTIONS", "--allow-unsupported"); + } + + Ok(Self { config }) + } + + /// 配置平台特定编译器 + fn configure_compiler(config: &mut Config, platform: &Platform) { + match platform { + Platform::LinuxGnu => { + config + .define("CMAKE_C_COMPILER", "/usr/bin/gcc") + .define("CMAKE_CXX_COMPILER", "/usr/bin/g++") + .define("CMAKE_MAKE_PROGRAM", "/usr/bin/make"); + } + Platform::MacOsArm64 | Platform::MacOsX86_64 => { + config + .define("CMAKE_C_COMPILER", "/usr/bin/clang") + .define("CMAKE_CXX_COMPILER", "/usr/bin/clang++") + .define("CMAKE_MAKE_PROGRAM", "/usr/bin/make"); + } + Platform::WindowsMsvc => { + // Windows使用默认MSVC编译器 + } + } + } + + /// 执行构建 + pub fn build(&mut self) -> PathBuf { + let dst = self.config.build(); + println!("cmake dst = {}", dst.display()); + dst + } +} diff --git a/build/config.rs b/build/config.rs new file mode 100644 index 00000000..1d9b22ff --- /dev/null +++ b/build/config.rs @@ -0,0 +1,149 @@ +//! 构建配置定义 + +use std::path::PathBuf; + +/// 构建错误类型 +#[derive(Debug)] +pub enum BuildError { + EnvVarMissing(String), + VcpkgRootMissing, + Io(std::io::Error), +} + +impl std::fmt::Display for BuildError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BuildError::EnvVarMissing(var) => write!(f, "环境变量未设置: {}", var), + BuildError::VcpkgRootMissing => write!(f, "VCPKG_ROOT未设置"), + BuildError::Io(e) => write!(f, "IO错误: {}", e), + } + } +} + +impl std::error::Error for BuildError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + BuildError::Io(e) => Some(e), + _ => None, + } + } +} + +impl From for BuildError { + fn from(e: std::io::Error) -> Self { + BuildError::Io(e) + } +} + +/// 支持的目标平台 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Platform { + WindowsMsvc, + LinuxGnu, + MacOsArm64, + MacOsX86_64, +} + +impl Platform { + /// 从TARGET环境变量解析平台 + pub fn from_target(target: &str) -> Option { + match target { + "x86_64-pc-windows-msvc" => Some(Self::WindowsMsvc), + "x86_64-unknown-linux-gnu" => Some(Self::LinuxGnu), + "aarch64-apple-darwin" => Some(Self::MacOsArm64), + "x86_64-apple-darwin" => Some(Self::MacOsX86_64), + _ => None, + } + } + + /// 获取vcpkg triplet名称 + pub fn vcpkg_triplet(&self) -> &'static str { + match self { + Self::WindowsMsvc => "x64-windows", + Self::LinuxGnu => "x64-linux", + Self::MacOsArm64 => "arm64-osx", + Self::MacOsX86_64 => "x64-osx", + } + } + + /// 是否为macOS平台 + pub fn is_macos(&self) -> bool { + matches!(self, Self::MacOsArm64 | Self::MacOsX86_64) + } +} + +/// 通用构建配置 +#[derive(Debug, Clone)] +pub struct BuildConfig { + pub platform: Platform, + pub vcpkg_root: PathBuf, + pub source_dir: PathBuf, + pub out_dir: PathBuf, + pub target_dir: PathBuf, + pub enable_strict: bool, + pub vcpkg_has_been_installed: bool, +} + +impl BuildConfig { + /// 从环境变量创建配置 + pub fn from_env() -> Result { + let target = std::env::var("TARGET") + .map_err(|_| BuildError::EnvVarMissing("TARGET".into()))?; + + let platform = Platform::from_target(&target) + .ok_or_else(|| BuildError::EnvVarMissing(format!("未知目标平台: {}", target)))?; + + let vcpkg_root = std::env::var("VCPKG_ROOT") + .map_err(|_| BuildError::VcpkgRootMissing)?; + + let source_dir = std::env::var("CARGO_MANIFEST_DIR") + .map_err(|_| BuildError::EnvVarMissing("CARGO_MANIFEST_DIR".into()))? + .into(); + + let out_dir = std::env::var("OUT_DIR") + .map_err(|_| BuildError::EnvVarMissing("OUT_DIR".into()))? + .into(); + + let profile = std::env::var("PROFILE").unwrap_or_else(|_| "release".into()); + let target_dir = std::env::var("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("target")) + .join(&profile); + + let enable_strict = std::env::var("ENABLE_STRICT_CHECKS") + .map(|v| v == "1") + .unwrap_or(false); + + let vcpkg_has_been_installed = std::env::var("VCPKG_HAS_BEEN_INSTALLED") + .map(|v| v == "1") + .unwrap_or(false); + + Ok(Self { + platform, + vcpkg_root: vcpkg_root.into(), + source_dir, + out_dir, + target_dir, + enable_strict, + vcpkg_has_been_installed, + }) + } + + /// 获取vcpkg安装目录 + pub fn vcpkg_installed_dir(&self) -> PathBuf { + self.out_dir + .join("build") + .join("vcpkg_installed") + .join(self.platform.vcpkg_triplet()) + } + + /// 获取vcpkg lib目录 + pub fn vcpkg_lib_dir(&self) -> PathBuf { + self.vcpkg_installed_dir().join("lib") + } + + /// 获取vcpkg share目录 + pub fn vcpkg_share_dir(&self) -> PathBuf { + self.vcpkg_installed_dir().join("share") + } +} diff --git a/build/link/linux.rs b/build/link/linux.rs new file mode 100644 index 00000000..d755c21b --- /dev/null +++ b/build/link/linux.rs @@ -0,0 +1,91 @@ +//! Linux平台库链接配置 + +use crate::build::config::BuildConfig; + +/// Linux平台库链接 +pub fn link_libraries(config: &BuildConfig) { + // CMake构建输出 + println!("cargo:rustc-link-search=native={}/lib", config.out_dir.display()); + + // Draco库路径 + println!( + "cargo:rustc-link-search=native={}/thirdparty/draco", + config.source_dir.display() + ); + + // vcpkg库路径 + let vcpkg_installed_dir = config.vcpkg_installed_dir(); + let vcpkg_installed_lib_dir = vcpkg_installed_dir.join("lib"); + println!("cargo:rustc-link-search=native={}", vcpkg_installed_lib_dir.display()); + + // OSG插件路径 + let osg_plugins_lib = vcpkg_installed_lib_dir.join("osgPlugins-3.6.5"); + println!("cargo:rustc-link-search=native={}", osg_plugins_lib.display()); + + // 0. System C++ library first + println!("cargo:rustc-link-lib=stdc++"); + println!("cargo:rustc-link-lib=z"); + + // 1. FFI static + println!("cargo:rustc-link-lib=static=_3dtile"); + println!("cargo:rustc-link-lib=static=ufbx"); + println!("cargo:rustc-link-lib=static=draco"); + + // 2. OSG + println!("cargo:rustc-link-lib=GL"); + println!("cargo:rustc-link-lib=X11"); + println!("cargo:rustc-link-lib=Xi"); + println!("cargo:rustc-link-lib=Xrandr"); + println!("cargo:rustc-link-lib=dl"); + println!("cargo:rustc-link-lib=pthread"); + println!("cargo:rustc-link-lib=osgdb_jpeg"); + println!("cargo:rustc-link-lib=osgdb_tga"); + println!("cargo:rustc-link-lib=osgdb_rgb"); + println!("cargo:rustc-link-lib=osgdb_png"); + println!("cargo:rustc-link-lib=osgdb_osg"); + println!("cargo:rustc-link-lib=osgdb_serializers_osg"); + println!("cargo:rustc-link-lib=osgUtil"); + println!("cargo:rustc-link-lib=osgDB"); + println!("cargo:rustc-link-lib=osg"); + println!("cargo:rustc-link-lib=OpenThreads"); + + // 3. GDAL dependencies + println!("cargo:rustc-link-lib=gdal"); + println!("cargo:rustc-link-lib=geos_c"); + println!("cargo:rustc-link-lib=geos"); + println!("cargo:rustc-link-lib=proj"); + println!("cargo:rustc-link-lib=sqlite3"); + println!("cargo:rustc-link-lib=expat"); + println!("cargo:rustc-link-lib=curl"); + println!("cargo:rustc-link-lib=ssl"); + println!("cargo:rustc-link-lib=crypto"); + println!("cargo:rustc-link-lib=uriparser"); + println!("cargo:rustc-link-lib=kmlbase"); + println!("cargo:rustc-link-lib=kmlengine"); + println!("cargo:rustc-link-lib=kmldom"); + println!("cargo:rustc-link-lib=kmlconvenience"); + println!("cargo:rustc-link-lib=Lerc"); + println!("cargo:rustc-link-lib=json-c"); + println!("cargo:rustc-link-lib=sharpyuv"); + + // 4. Image / compression libraries + println!("cargo:rustc-link-lib=geotiff"); + println!("cargo:rustc-link-lib=gif"); + println!("cargo:rustc-link-lib=jpeg"); + println!("cargo:rustc-link-lib=png"); + println!("cargo:rustc-link-lib=tiff"); + println!("cargo:rustc-link-lib=webp"); + println!("cargo:rustc-link-lib=xml2"); + println!("cargo:rustc-link-lib=lzma"); + println!("cargo:rustc-link-lib=openjp2"); + println!("cargo:rustc-link-lib=qhullstatic_r"); + println!("cargo:rustc-link-lib=minizip"); + println!("cargo:rustc-link-lib=spatialite"); + println!("cargo:rustc-link-lib=freexl"); + println!("cargo:rustc-link-lib=basisu_encoder"); + println!("cargo:rustc-link-lib=meshoptimizer"); + println!("cargo:rustc-link-lib=zstd"); + + // 5. GeographicLib + println!("cargo:rustc-link-lib=GeographicLib"); +} diff --git a/build/link/macos.rs b/build/link/macos.rs new file mode 100644 index 00000000..573b5f16 --- /dev/null +++ b/build/link/macos.rs @@ -0,0 +1,96 @@ +//! macOS平台库链接配置 + +use crate::build::config::BuildConfig; + +/// macOS平台库链接 +pub fn link_libraries(config: &BuildConfig) { + // CMake构建输出 + println!("cargo:rustc-link-search=native={}/lib", config.out_dir.display()); + + // Draco库路径 + println!( + "cargo:rustc-link-search=native={}/thirdparty/draco", + config.source_dir.display() + ); + + // vcpkg库路径 + let vcpkg_installed_dir = config.vcpkg_installed_dir(); + let vcpkg_installed_lib_dir = vcpkg_installed_dir.join("lib"); + println!("cargo:rustc-link-search=native={}", vcpkg_installed_lib_dir.display()); + + // OSG插件路径 + let osg_plugins_lib = vcpkg_installed_lib_dir.join("osgPlugins-3.6.5"); + println!("cargo:rustc-link-search=native={}", osg_plugins_lib.display()); + + // 0. System C++ library first + println!("cargo:rustc-link-lib=c++"); + println!("cargo:rustc-link-lib=z"); + + // 1. FFI static + println!("cargo:rustc-link-lib=static=_3dtile"); + println!("cargo:rustc-link-lib=static=ufbx"); + println!("cargo:rustc-link-lib=static=draco"); + + // 2. OSG + println!("cargo:rustc-link-lib=osgdb_jpeg"); + println!("cargo:rustc-link-lib=osgdb_tga"); + println!("cargo:rustc-link-lib=osgdb_rgb"); + println!("cargo:rustc-link-lib=osgdb_png"); + println!("cargo:rustc-link-lib=osgdb_osg"); + println!("cargo:rustc-link-lib=osgdb_serializers_osg"); + println!("cargo:rustc-link-lib=osgUtil"); + println!("cargo:rustc-link-lib=osgDB"); + println!("cargo:rustc-link-lib=osg"); + println!("cargo:rustc-link-lib=OpenThreads"); + + // 3. GDAL dependencies + println!("cargo:rustc-link-lib=gdal"); + println!("cargo:rustc-link-lib=geos_c"); + println!("cargo:rustc-link-lib=geos"); + println!("cargo:rustc-link-lib=proj"); + println!("cargo:rustc-link-lib=sqlite3"); + println!("cargo:rustc-link-lib=expat"); + println!("cargo:rustc-link-lib=curl"); + println!("cargo:rustc-link-lib=ssl"); + println!("cargo:rustc-link-lib=crypto"); + println!("cargo:rustc-link-lib=kmlbase"); + println!("cargo:rustc-link-lib=kmlengine"); + println!("cargo:rustc-link-lib=kmldom"); + println!("cargo:rustc-link-lib=kmlconvenience"); + println!("cargo:rustc-link-lib=Lerc"); + println!("cargo:rustc-link-lib=json-c"); + println!("cargo:rustc-link-lib=sharpyuv"); + + // 4. Image / compression libraries + println!("cargo:rustc-link-lib=geotiff"); + println!("cargo:rustc-link-lib=gif"); + println!("cargo:rustc-link-lib=jpeg"); + println!("cargo:rustc-link-lib=png"); + println!("cargo:rustc-link-lib=tiff"); + println!("cargo:rustc-link-lib=webp"); + println!("cargo:rustc-link-lib=xml2"); + println!("cargo:rustc-link-lib=lzma"); + println!("cargo:rustc-link-lib=openjp2"); + println!("cargo:rustc-link-lib=qhullstatic_r"); + println!("cargo:rustc-link-lib=minizip"); + println!("cargo:rustc-link-lib=spatialite"); + println!("cargo:rustc-link-lib=freexl"); + println!("cargo:rustc-link-lib=basisu_encoder"); + println!("cargo:rustc-link-lib=meshoptimizer"); + println!("cargo:rustc-link-lib=zstd"); + + // 5. GeographicLib + println!("cargo:rustc-link-lib=GeographicLib"); + + // 6. System frameworks + println!("cargo:rustc-link-lib=framework=Security"); + println!("cargo:rustc-link-lib=framework=CoreFoundation"); + println!("cargo:rustc-link-lib=framework=Foundation"); + println!("cargo:rustc-link-lib=framework=OpenGL"); + println!("cargo:rustc-link-lib=framework=AppKit"); + println!("cargo:rustc-link-lib=framework=SystemConfiguration"); + + // 7. Additional linker flags + println!("cargo:rustc-link-arg=-ObjC"); + println!("cargo:rustc-link-arg=-all_load"); +} diff --git a/build/link/mod.rs b/build/link/mod.rs new file mode 100644 index 00000000..01f19e09 --- /dev/null +++ b/build/link/mod.rs @@ -0,0 +1,9 @@ +//! 平台特定的库链接配置 + +pub mod linux; +pub mod macos; +pub mod windows; + +pub use linux::link_libraries as link_linux; +pub use macos::link_libraries as link_macos; +pub use windows::link_libraries as link_windows; diff --git a/build/link/windows.rs b/build/link/windows.rs new file mode 100644 index 00000000..179526ad --- /dev/null +++ b/build/link/windows.rs @@ -0,0 +1,57 @@ +//! Windows平台库链接配置 + +use crate::build::config::BuildConfig; +use std::env; + +/// Windows平台库链接 +pub fn link_libraries(config: &BuildConfig) { + // CMake构建输出 + println!("cargo:rustc-link-search=native={}/lib", config.out_dir.display()); + + // Draco库路径 + println!( + "cargo:rustc-link-search=native={}/thirdparty/draco", + config.source_dir.display() + ); + + // vcpkg库路径 + let vcpkg_installed_dir = config.vcpkg_installed_dir(); + let vcpkg_installed_lib_dir = vcpkg_installed_dir.join("lib"); + println!("cargo:rustc-link-search=native={}", vcpkg_installed_lib_dir.display()); + + // 1. FFI static + println!("cargo:rustc-link-lib=static=_3dtile"); + println!("cargo:rustc-link-lib=static=ufbx"); + println!("cargo:rustc-link-lib=static=draco"); + + // 2. OSG + println!("cargo:rustc-link-lib=osgUtil"); + println!("cargo:rustc-link-lib=osgDB"); + println!("cargo:rustc-link-lib=osg"); + + // 3. OpenThreads + println!("cargo:rustc-link-lib=OpenThreads"); + + // 4. GDAL dependencies + println!("cargo:rustc-link-lib=gdal"); + println!("cargo:rustc-link-lib=basisu_encoder"); + println!("cargo:rustc-link-lib=meshoptimizer"); + println!("cargo:rustc-link-lib=zstd"); + + // 5. sqlite + println!("cargo:rustc-link-lib=sqlite3"); + + // GeographicLib (debug/release区分) + let profile = env::var("PROFILE").unwrap_or_else(|_| "release".into()); + let is_debug = profile == "debug"; + let geolib_name = if is_debug { + "GeographicLib_d-i" + } else { + "GeographicLib-i" + }; + println!( + "cargo:warning=Building in {} mode, linking GeographicLib as: {}", + profile, geolib_name + ); + println!("cargo:rustc-link-lib={}", geolib_name); +} diff --git a/build/mod.rs b/build/mod.rs new file mode 100644 index 00000000..07279348 --- /dev/null +++ b/build/mod.rs @@ -0,0 +1,15 @@ +//! Build script helper modules +//! +//! 提供跨平台C++构建的抽象和工具函数 + +pub mod cmake; +pub mod config; +pub mod link; +pub mod utils; + +pub use cmake::CMakeBuilder; +pub use config::{BuildConfig, BuildError}; +pub use link::{link_linux, link_macos, link_windows}; +pub use utils::{ + copy_gdal_data, copy_osg_plugins, copy_proj_data, create_vcpkg_symlink, export_compile_commands, +}; diff --git a/build/utils.rs b/build/utils.rs new file mode 100644 index 00000000..46187e13 --- /dev/null +++ b/build/utils.rs @@ -0,0 +1,180 @@ +//! 构建工具函数 + +use crate::build::config::BuildConfig; +use std::fs; +use std::io; +use std::path::Path; + +/// 递归复制目录 +pub fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> { + if !dst.exists() { + fs::create_dir_all(dst)?; + } + + for entry in fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + let dest_path = dst.join(entry.file_name()); + + if path.is_dir() { + copy_dir_recursive(&path, &dest_path)?; + } else { + fs::copy(&path, &dest_path)?; + } + } + Ok(()) +} + +/// 创建vcpkg_installed目录符号链接 +pub fn create_vcpkg_symlink(config: &BuildConfig) -> io::Result<()> { + if !config.vcpkg_has_been_installed { + return Ok(()); + } + + let root_vcpkg = config.source_dir.join("vcpkg_installed"); + if !root_vcpkg.exists() { + return Ok(()); + } + + let build_vcpkg = config.out_dir.join("build").join("vcpkg_installed"); + + // 确保父目录存在 + if let Some(parent) = build_vcpkg.parent() { + fs::create_dir_all(parent)?; + } + + if build_vcpkg.exists() { + println!( + "cargo:warning=vcpkg_installed symlink already exists, no need to create again.: {:?}", + build_vcpkg + ); + return Ok(()); + } + + #[cfg(target_family = "unix")] + { + std::os::unix::fs::symlink(&root_vcpkg, &build_vcpkg).map_err(|e| { + io::Error::other(format!( + "Failed to create symlink for Unix-like os from {} -> {}: {}", + root_vcpkg.display(), + build_vcpkg.display(), + e + )) + })?; + } + + #[cfg(target_family = "windows")] + { + std::os::windows::fs::symlink_dir(&root_vcpkg, &build_vcpkg).map_err(|e| { + io::Error::other(format!( + "Failed to create symlink for windows os from {} -> {}: {}", + root_vcpkg.display(), + build_vcpkg.display(), + e + )) + })?; + } + + println!( + "cargo:warning=Created vcpkg_installed symlink: {} -> {}", + build_vcpkg.display(), + root_vcpkg.display() + ); + + Ok(()) +} + +/// 导出compile_commands.json +pub fn export_compile_commands(out_dir: &Path) { + let cmake_build_dir = out_dir.join("build"); + let src = cmake_build_dir.join("compile_commands.json"); + + if !src.exists() { + println!( + "cargo:warning=compile_commands.json not found at {}", + src.display() + ); + return; + } + + let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into()); + let dst_dir = Path::new(&cargo_manifest_dir).join("build"); + + if let Err(err) = fs::create_dir_all(&dst_dir) { + println!( + "cargo:warning=failed to create build dir {}: {}", + dst_dir.display(), + err + ); + return; + } + + let dst = dst_dir.join("compile_commands.json"); + match fs::copy(&src, &dst) { + Ok(_) => println!( + "cargo:warning=exported compile_commands.json to {}", + dst.display() + ), + Err(err) => println!( + "cargo:warning=failed to copy compile_commands.json to {}: {}", + dst.display(), + err + ), + } +} + +/// 复制GDAL数据文件 +pub fn copy_gdal_data(config: &BuildConfig) { + let gdal_data = config.vcpkg_share_dir().join("gdal"); + let out_dir = &config.target_dir; + + println!( + "gdal_data -> {}, out_dir -> {}", + gdal_data.display(), + out_dir.display() + ); + + if let Err(e) = copy_dir_recursive(&gdal_data, &out_dir.join("gdal")) { + println!("cargo:warning=Failed to copy GDAL data: {}", e); + } +} + +/// 复制PROJ数据文件 +pub fn copy_proj_data(config: &BuildConfig) { + let proj_data = config.vcpkg_share_dir().join("proj"); + let out_dir = &config.target_dir; + + println!( + "proj_data -> {}, out_dir -> {}", + proj_data.display(), + out_dir.display() + ); + + if let Err(e) = copy_dir_recursive(&proj_data, &out_dir.join("proj")) { + println!("cargo:warning=Failed to copy PROJ data: {}", e); + } +} + +/// 复制OSG插件 +pub fn copy_osg_plugins(config: &BuildConfig) { + let plugins_src = config.vcpkg_lib_dir().join("osgPlugins-3.6.5"); + let out_dir = config.target_dir.join("osgPlugins-3.6.5"); + + println!( + "osg_plugins -> {}, out_dir -> {}", + plugins_src.display(), + out_dir.display() + ); + + if !plugins_src.exists() { + println!( + "cargo:warning=OSG plugins directory not found: {}", + plugins_src.display() + ); + return; + } + + if let Err(e) = copy_dir_recursive(&plugins_src, &out_dir) { + println!("cargo:warning=Failed to copy OSG plugins: {}", e); + } +} diff --git a/cmake-build.sh b/cmake-build.sh new file mode 100755 index 00000000..aebb73c9 --- /dev/null +++ b/cmake-build.sh @@ -0,0 +1,16 @@ +#!/bin/bash +cmake_build_root=cmake-build + +if [ -z "$VCPKG_ROOT" ]; then + echo "VCPKG_ROOT环境变量未设置" + exit 1 +fi + +vcpkg_triplet=arm64-osx + +rm -rf $cmake_build_root +mkdir $cmake_build_root +cmake -S . -B $cmake_build_root -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake \ +-DVCPKG_TARGET_TRIPLET=$vcpkg_triplet \ +-DVCPKG_INSTALLED_DIR=vcpkg_installed \ +-DVCPKG_MANIFEST_MODE=OFF diff --git a/docs/build_rs_refactor_plan.md b/docs/build_rs_refactor_plan.md new file mode 100644 index 00000000..fa2e0060 --- /dev/null +++ b/docs/build_rs_refactor_plan.md @@ -0,0 +1,898 @@ +# build.rs 重构优化方案 + +> 创建时间: 2026-02-26 +> 目标: 提取公共构建逻辑,解决C++单独编译问题 + +--- + +## 一、当前架构问题诊断 + +### 1.1 代码重复严重(DRY原则违反) + +四个平台构建函数存在大量重复代码: + +| 重复内容 | 出现次数 | 代码行数 | +|---------|---------|---------| +| vcpkg_root获取与symlink创建 | 4次 | ~15行×4 | +| CMake Config配置 | 4次 | ~10行×4 | +| strict模式检查 | 4次 | ~5行×4 | +| compile_commands导出 | 4次 | ~5行×4 | +| draco库链接 | 4次 | ~3行×4 | +| 数据复制调用 | 4次 | ~6行×4 | + +**重复代码占比估算**:约60-70%的代码是重复的。 + +### 1.2 平台特定逻辑混杂 + +```rust +// 当前:平台逻辑完全分散在独立函数中 +fn build_win_msvc() { /* 200+行,包含Windows特有逻辑 */ } +fn build_linux_unknown() { /* 200+行,包含Linux特有逻辑 */ } +fn build_macos() { /* 200+行,包含macOS特有逻辑 */ } +fn build_macos_x86_64() { /* 200+行,与macOS几乎相同 */ } +``` + +**问题**:macOS ARM64和x86_64的代码几乎完全相同,仅triplet不同(`arm64-osx` vs `x64-osx`)。 + +### 1.3 错误处理不一致 + +```rust +// 有些地方使用 expect +.expect(&format!("Failed to create symlink...")) + +// 有些地方使用 unwrap +.copy(&src, &dst).unwrap() + +// 有些地方手动处理错误 +if let Err(err) = fs::create_dir_all(&dst_dir) { + println!("cargo:warning=..."); +} +``` + +--- + +## 二、优化方案概述 + +### 2.1 目标架构 + +``` +build.rs (入口,~30行) + │ + ├──► build/ + │ ├── mod.rs (公共模块导出) + │ ├── config.rs (BuildConfig, PlatformConfig) + │ ├── cmake.rs (CMake配置抽象) + │ ├── link.rs (库链接管理) + │ ├── platform.rs (平台特定配置) + │ └── utils.rs (工具函数:copy, symlink等) + │ + └──► 原build.rs中的平台函数删除,逻辑迁移到上述模块 +``` + +### 2.2 推荐实施路线图 + +``` +Phase 1: 快速收益(1-2天) +├── 1.1 提取公共函数(copy_gdal_data等) +├── 1.2 合并macOS ARM64/x86_64构建函数 +└── 1.3 统一错误处理风格 + +Phase 2: 架构重构(3-5天) +├── 2.1 设计BuildConfig/LinkConfig结构 +├── 2.2 实现平台配置表 +├── 2.3 重构main函数使用新抽象 +└── 2.4 添加完整错误处理 + +Phase 3: 高级优化(1-2周) +├── 3.1 集成vcpkg crate自动链接 +├── 3.2 实现独立C++编译模式 +├── 3.3 CMake现代化改造 +└── 3.4 添加构建缓存支持 +``` + +--- + +## 三、详细设计方案 + +### 3.1 build/mod.rs - 模块入口 + +```rust +//! Build script helper modules +//! +//! 提供跨平台C++构建的抽象和工具函数 + +pub mod config; +pub mod cmake; +pub mod link; +pub mod platform; +pub mod utils; + +pub use config::{BuildConfig, BuildError, Platform}; +pub use cmake::CMakeBuilder; +pub use link::LinkManager; +pub use platform::PlatformConfig; +pub use utils::{copy_dir_recursive, create_vcpkg_symlink, export_compile_commands}; +``` + +### 3.2 build/config.rs - 配置结构体 + +```rust +//! 构建配置定义 + +use std::path::PathBuf; +use thiserror::Error; + +/// 构建错误类型 +#[derive(Debug, Error)] +pub enum BuildError { + #[error("环境变量未设置: {0}")] + EnvVarMissing(String), + + #[error("VCPKG_ROOT未设置")] + VcpkgRootMissing, + + #[error("CMake配置失败: {0}")] + CMakeConfigFailed(String), + + #[error("库链接失败: {name}")] + LibraryLinkFailed { name: String }, + + #[error("目录操作失败: {path}")] + DirectoryOperationFailed { path: PathBuf }, + + #[error("IO错误: {0}")] + Io(#[from] std::io::Error), +} + +/// 支持的目标平台 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Platform { + WindowsMsvc, + LinuxGnu, + MacOsArm64, + MacOsX86_64, +} + +impl Platform { + /// 从TARGET环境变量解析平台 + pub fn from_target(target: &str) -> Option { + match target { + "x86_64-pc-windows-msvc" => Some(Self::WindowsMsvc), + "x86_64-unknown-linux-gnu" => Some(Self::LinuxGnu), + "aarch64-apple-darwin" => Some(Self::MacOsArm64), + "x86_64-apple-darwin" => Some(Self::MacOsX86_64), + _ => None, + } + } + + /// 获取vcpkg triplet名称 + pub fn vcpkg_triplet(&self) -> &'static str { + match self { + Self::WindowsMsvc => "x64-windows", + Self::LinuxGnu => "x64-linux", + Self::MacOsArm64 => "arm64-osx", + Self::MacOsX86_64 => "x64-osx", + } + } + + /// 是否为Windows平台 + pub fn is_windows(&self) -> bool { + matches!(self, Self::WindowsMsvc) + } + + /// 是否为macOS平台 + pub fn is_macos(&self) -> bool { + matches!(self, Self::MacOsArm64 | Self::MacOsX86_64) + } + + /// 是否为Linux平台 + pub fn is_linux(&self) -> bool { + matches!(self, Self::LinuxGnu) + } +} + +/// 通用构建配置 +#[derive(Debug, Clone)] +pub struct BuildConfig { + pub platform: Platform, + pub vcpkg_root: PathBuf, + pub source_dir: PathBuf, + pub out_dir: PathBuf, + pub target_dir: PathBuf, + pub enable_strict: bool, + pub vcpkg_has_been_installed: bool, +} + +impl BuildConfig { + /// 从环境变量创建配置 + pub fn from_env() -> Result { + let platform = std::env::var("TARGET") + .map_err(|_| BuildError::EnvVarMissing("TARGET".into()))? + .parse()?; + + let vcpkg_root = std::env::var("VCPKG_ROOT") + .map_err(|_| BuildError::VcpkgRootMissing)?; + + let source_dir = std::env::var("CARGO_MANIFEST_DIR") + .map_err(|_| BuildError::EnvVarMissing("CARGO_MANIFEST_DIR".into()))? + .into(); + + let out_dir = std::env::var("OUT_DIR") + .map_err(|_| BuildError::EnvVarMissing("OUT_DIR".into()))? + .into(); + + let profile = std::env::var("PROFILE").unwrap_or_else(|_| "release".into()); + let target_dir = std::env::var("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("target")) + .join(&profile); + + let enable_strict = std::env::var("ENABLE_STRICT_CHECKS") + .map(|v| v == "1") + .unwrap_or(false); + + let vcpkg_has_been_installed = std::env::var("VCPKG_HAS_BEEN_INSTALLED") + .map(|v| v == "1") + .unwrap_or(false); + + Ok(Self { + platform, + vcpkg_root: vcpkg_root.into(), + source_dir, + out_dir, + target_dir, + enable_strict, + vcpkg_has_been_installed, + }) + } + + /// 获取vcpkg安装目录 + pub fn vcpkg_installed_dir(&self) -> PathBuf { + self.out_dir + .join("build") + .join("vcpkg_installed") + .join(self.platform.vcpkg_triplet()) + } + + /// 获取vcpkg lib目录 + pub fn vcpkg_lib_dir(&self) -> PathBuf { + self.vcpkg_installed_dir().join("lib") + } + + /// 获取vcpkg share目录 + pub fn vcpkg_share_dir(&self) -> PathBuf { + self.vcpkg_installed_dir().join("share") + } +} + +impl std::str::FromStr for Platform { + type Err = BuildError; + + fn from_str(s: &str) -> Result { + Self::from_target(s) + .ok_or_else(|| BuildError::EnvVarMissing(format!("未知目标平台: {}", s))) + } +} +``` + +### 3.3 build/cmake.rs - CMake配置抽象 + +```rust +//! CMake构建抽象 + +use cmake::Config; +use std::path::PathBuf; +use crate::config::{BuildConfig, BuildError, Platform}; + +/// CMake构建器 +pub struct CMakeBuilder { + config: Config, + build_config: BuildConfig, +} + +impl CMakeBuilder { + /// 创建新的CMake构建器 + pub fn new(build_config: &BuildConfig) -> Result { + let mut config = Config::new(&build_config.source_dir); + + // 配置基础选项 + config + .define("CMAKE_TOOLCHAIN_FILE", + format!("{}/scripts/buildsystems/vcpkg.cmake", + build_config.vcpkg_root.display())) + .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON") + .very_verbose(true); + + // 平台特定编译器配置 + Self::configure_compiler(&mut config, &build_config.platform); + + // 严格模式 + if build_config.enable_strict { + println!("cargo:warning=Building with STRICT CHECKS enabled (CI mode)"); + config.define("ENABLE_STRICT_CHECKS", "ON"); + } + + // macOS特殊配置 + if build_config.platform.is_macos() { + config.define("VCPKG_INSTALL_OPTIONS", "--allow-unsupported"); + } + + Ok(Self { + config, + build_config: build_config.clone(), + }) + } + + /// 配置平台特定编译器 + fn configure_compiler(config: &mut Config, platform: &Platform) { + match platform { + Platform::LinuxGnu => { + config + .define("CMAKE_C_COMPILER", "/usr/bin/gcc") + .define("CMAKE_CXX_COMPILER", "/usr/bin/g++") + .define("CMAKE_MAKE_PROGRAM", "/usr/bin/make"); + } + Platform::MacOsArm64 | Platform::MacOsX86_64 => { + config + .define("CMAKE_C_COMPILER", "/usr/bin/clang") + .define("CMAKE_CXX_COMPILER", "/usr/bin/clang++") + .define("CMAKE_MAKE_PROGRAM", "/usr/bin/make"); + } + Platform::WindowsMsvc => { + // Windows使用默认MSVC编译器 + } + } + } + + /// 执行构建 + pub fn build(&mut self) -> PathBuf { + let dst = self.config.build(); + println!("cargo:warning=CMake build directory: {}", dst.display()); + dst + } +} +``` + +### 3.4 build/utils.rs - 工具函数 + +```rust +//! 构建工具函数 + +use crate::config::BuildConfig; +use std::fs; +use std::io; +use std::path::Path; + +/// 递归复制目录 +pub fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> { + if !dst.exists() { + fs::create_dir_all(dst)?; + } + + for entry in fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + let dest_path = dst.join(entry.file_name()); + + if path.is_dir() { + copy_dir_recursive(&path, &dest_path)?; + } else { + fs::copy(&path, &dest_path)?; + } + } + Ok(()) +} + +/// 创建vcpkg_installed目录符号链接 +pub fn create_vcpkg_symlink(config: &BuildConfig) -> io::Result<()> { + if !config.vcpkg_has_been_installed { + return Ok(()); + } + + let root_vcpkg = config.source_dir.join("vcpkg_installed"); + if !root_vcpkg.exists() { + return Ok(()); + } + + let build_vcpkg = config.out_dir.join("build").join("vcpkg_installed"); + + // 确保父目录存在 + if let Some(parent) = build_vcpkg.parent() { + fs::create_dir_all(parent)?; + } + + if build_vcpkg.exists() { + println!("cargo:warning=vcpkg_installed symlink already exists"); + return Ok(()); + } + + #[cfg(target_family = "unix")] + { + std::os::unix::fs::symlink(&root_vcpkg, &build_vcpkg) + .map_err(|e| io::Error::new( + io::ErrorKind::Other, + format!("Failed to create Unix symlink: {}", e) + ))?; + } + + #[cfg(target_family = "windows")] + { + std::os::windows::fs::symlink_dir(&root_vcpkg, &build_vcpkg) + .map_err(|e| io::Error::new( + io::ErrorKind::Other, + format!("Failed to create Windows symlink: {}", e) + ))?; + } + + println!("cargo:warning=Created vcpkg_installed symlink: {} -> {}", + build_vcpkg.display(), root_vcpkg.display()); + + Ok(()) +} + +/// 导出compile_commands.json +pub fn export_compile_commands(out_dir: &Path) { + let cmake_build_dir = out_dir.join("build"); + let src = cmake_build_dir.join("compile_commands.json"); + + if !src.exists() { + println!("cargo:warning=compile_commands.json not found at {}", src.display()); + return; + } + + let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .unwrap_or_else(|_| ".".into()); + let dst_dir = Path::new(&cargo_manifest_dir).join("build"); + + if let Err(err) = fs::create_dir_all(&dst_dir) { + println!("cargo:warning=Failed to create build dir {}: {}", + dst_dir.display(), err); + return; + } + + let dst = dst_dir.join("compile_commands.json"); + match fs::copy(&src, &dst) { + Ok(_) => println!("cargo:warning=Exported compile_commands.json to {}", + dst.display()), + Err(err) => println!("cargo:warning=Failed to copy compile_commands.json: {}", + err), + } +} + +/// 复制GDAL数据文件 +pub fn copy_gdal_data(config: &BuildConfig) { + let gdal_data = config.vcpkg_share_dir().join("gdal"); + let out_dir = &config.target_dir; + + println!("cargo:warning=Copying GDAL data: {} -> {}", + gdal_data.display(), out_dir.display()); + + if let Err(e) = copy_dir_recursive(&gdal_data, &out_dir.join("gdal")) { + println!("cargo:warning=Failed to copy GDAL data: {}", e); + } +} + +/// 复制PROJ数据文件 +pub fn copy_proj_data(config: &BuildConfig) { + let proj_data = config.vcpkg_share_dir().join("proj"); + let out_dir = &config.target_dir; + + println!("cargo:warning=Copying PROJ data: {} -> {}", + proj_data.display(), out_dir.display()); + + if let Err(e) = copy_dir_recursive(&proj_data, &out_dir.join("proj")) { + println!("cargo:warning=Failed to copy PROJ data: {}", e); + } +} + +/// 复制OSG插件 +pub fn copy_osg_plugins(config: &BuildConfig) { + let plugins_src = config.vcpkg_lib_dir().join("osgPlugins-3.6.5"); + let out_dir = config.target_dir.join("osgPlugins-3.6.5"); + + println!("cargo:warning=Copying OSG plugins: {} -> {}", + plugins_src.display(), out_dir.display()); + + if !plugins_src.exists() { + println!("cargo:warning=OSG plugins directory not found: {}", + plugins_src.display()); + return; + } + + if let Err(e) = copy_dir_recursive(&plugins_src, &out_dir) { + println!("cargo:warning=Failed to copy OSG plugins: {}", e); + } +} +``` + +### 3.5 重构后的build.rs + +```rust +//! 3dtiles项目构建脚本 +//! +//! 重构后版本:使用模块化设计,消除平台特定代码重复 + +// 引入构建模块 +#[path = "build/mod.rs"] +mod build; + +use build::{ + CMakeBuilder, + BuildConfig, + BuildError, + copy_gdal_data, + copy_proj_data, + copy_osg_plugins, + create_vcpkg_symlink, + export_compile_commands, +}; + +fn main() -> Result<(), BuildError> { + // 启用完整回溯信息 + std::env::set_var("RUST_BACKTRACE", "full"); + + // 1. 从环境变量加载配置 + let config = BuildConfig::from_env()?; + + println!("cargo:warning=Building for platform: {:?}", config.platform); + println!("cargo:warning=VCPKG_ROOT: {}", config.vcpkg_root.display()); + + // 2. 创建vcpkg符号链接(如果需要) + create_vcpkg_symlink(&config)?; + + // 3. 执行CMake构建 + let mut cmake = CMakeBuilder::new(&config)?; + let _build_dir = cmake.build(); + + // 4. 导出compile_commands.json + export_compile_commands(&config.out_dir); + + // 5. 平台特定的库链接(保留原有println!方式) + match config.platform { + build::config::Platform::WindowsMsvc => link_windows(&config), + build::config::Platform::LinuxGnu => link_linux(&config), + build::config::Platform::MacOsArm64 | + build::config::Platform::MacOsX86_64 => link_macos(&config), + } + + // 6. 复制数据文件 + copy_gdal_data(&config); + copy_proj_data(&config); + copy_osg_plugins(&config); + + println!("cargo:warning=Build completed successfully!"); + + Ok(()) +} + +/// Windows平台库链接 +fn link_windows(config: &BuildConfig) { + // CMake构建输出 + println!("cargo:rustc-link-search=native={}/lib", config.out_dir.display()); + + // Draco库路径 + println!("cargo:rustc-link-search=native={}/thirdparty/draco", + config.source_dir.display()); + + // vcpkg库路径 + let vcpkg_lib = config.vcpkg_lib_dir(); + println!("cargo:rustc-link-search=native={}", vcpkg_lib.display()); + + // 1. FFI static + println!("cargo:rustc-link-lib=static=_3dtile"); + println!("cargo:rustc-link-lib=static=ufbx"); + println!("cargo:rustc-link-lib=static=draco"); + + // 2. OSG + println!("cargo:rustc-link-lib=osgUtil"); + println!("cargo:rustc-link-lib=osgDB"); + println!("cargo:rustc-link-lib=osg"); + + // 3. OpenThreads + println!("cargo:rustc-link-lib=OpenThreads"); + + // 4. GDAL dependencies + println!("cargo:rustc-link-lib=gdal"); + println!("cargo:rustc-link-lib=basisu_encoder"); + println!("cargo:rustc-link-lib=meshoptimizer"); + println!("cargo:rustc-link-lib=zstd"); + + // 5. sqlite + println!("cargo:rustc-link-lib=sqlite3"); + + // GeographicLib (debug/release区分) + let profile = std::env::var("PROFILE").unwrap_or_else(|_| "release".into()); + let is_debug = profile == "debug"; + let geolib_name = if is_debug { + "GeographicLib_d-i" + } else { + "GeographicLib-i" + }; + println!("cargo:warning=Building in {} mode, linking GeographicLib as: {}", + profile, geolib_name); + println!("cargo:rustc-link-lib={}", geolib_name); +} + +/// Linux平台库链接 +fn link_linux(config: &BuildConfig) { + // CMake构建输出 + println!("cargo:rustc-link-search=native={}/lib", config.out_dir.display()); + + // Draco库路径 + println!("cargo:rustc-link-search=native={}/thirdparty/draco", + config.source_dir.display()); + + // vcpkg库路径 + let vcpkg_lib = config.vcpkg_lib_dir(); + println!("cargo:rustc-link-search=native={}", vcpkg_lib.display()); + + // OSG插件路径 + let osg_plugins = vcpkg_lib.join("osgPlugins-3.6.5"); + println!("cargo:rustc-link-search=native={}", osg_plugins.display()); + + // 0. System C++ library first + println!("cargo:rustc-link-lib=stdc++"); + println!("cargo:rustc-link-lib=z"); + + // 1. FFI static + println!("cargo:rustc-link-lib=static=_3dtile"); + println!("cargo:rustc-link-lib=static=ufbx"); + println!("cargo:rustc-link-lib=static=draco"); + + // 2. OSG + println!("cargo:rustc-link-lib=GL"); + println!("cargo:rustc-link-lib=X11"); + println!("cargo:rustc-link-lib=Xi"); + println!("cargo:rustc-link-lib=Xrandr"); + println!("cargo:rustc-link-lib=dl"); + println!("cargo:rustc-link-lib=pthread"); + println!("cargo:rustc-link-lib=osgdb_jpeg"); + println!("cargo:rustc-link-lib=osgdb_tga"); + println!("cargo:rustc-link-lib=osgdb_rgb"); + println!("cargo:rustc-link-lib=osgdb_png"); + println!("cargo:rustc-link-lib=osgdb_osg"); + println!("cargo:rustc-link-lib=osgdb_serializers_osg"); + println!("cargo:rustc-link-lib=osgUtil"); + println!("cargo:rustc-link-lib=osgDB"); + println!("cargo:rustc-link-lib=osg"); + println!("cargo:rustc-link-lib=OpenThreads"); + + // 3. GDAL dependencies + println!("cargo:rustc-link-lib=gdal"); + println!("cargo:rustc-link-lib=geos_c"); + println!("cargo:rustc-link-lib=geos"); + println!("cargo:rustc-link-lib=proj"); + println!("cargo:rustc-link-lib=sqlite3"); + println!("cargo:rustc-link-lib=expat"); + println!("cargo:rustc-link-lib=curl"); + println!("cargo:rustc-link-lib=ssl"); + println!("cargo:rustc-link-lib=crypto"); + println!("cargo:rustc-link-lib=uriparser"); + println!("cargo:rustc-link-lib=kmlbase"); + println!("cargo:rustc-link-lib=kmlengine"); + println!("cargo:rustc-link-lib=kmldom"); + println!("cargo:rustc-link-lib=kmlconvenience"); + println!("cargo:rustc-link-lib=Lerc"); + println!("cargo:rustc-link-lib=json-c"); + println!("cargo:rustc-link-lib=sharpyuv"); + + // 4. Image / compression libraries + println!("cargo:rustc-link-lib=geotiff"); + println!("cargo:rustc-link-lib=gif"); + println!("cargo:rustc-link-lib=jpeg"); + println!("cargo:rustc-link-lib=png"); + println!("cargo:rustc-link-lib=tiff"); + println!("cargo:rustc-link-lib=webp"); + println!("cargo:rustc-link-lib=xml2"); + println!("cargo:rustc-link-lib=lzma"); + println!("cargo:rustc-link-lib=openjp2"); + println!("cargo:rustc-link-lib=qhullstatic_r"); + println!("cargo:rustc-link-lib=minizip"); + println!("cargo:rustc-link-lib=spatialite"); + println!("cargo:rustc-link-lib=freexl"); + println!("cargo:rustc-link-lib=basisu_encoder"); + println!("cargo:rustc-link-lib=meshoptimizer"); + println!("cargo:rustc-link-lib=zstd"); + + // 5. GeographicLib + println!("cargo:rustc-link-lib=GeographicLib"); +} + +/// macOS平台库链接 +fn link_macos(config: &BuildConfig) { + // CMake构建输出 + println!("cargo:rustc-link-search=native={}/lib", config.out_dir.display()); + + // Draco库路径 + println!("cargo:rustc-link-search=native={}/thirdparty/draco", + config.source_dir.display()); + + // vcpkg库路径 + let vcpkg_lib = config.vcpkg_lib_dir(); + println!("cargo:rustc-link-search=native={}", vcpkg_lib.display()); + + // OSG插件路径 + let osg_plugins = vcpkg_lib.join("osgPlugins-3.6.5"); + println!("cargo:rustc-link-search=native={}", osg_plugins.display()); + + // 0. System C++ library first + println!("cargo:rustc-link-lib=c++"); + println!("cargo:rustc-link-lib=z"); + + // 1. FFI static + println!("cargo:rustc-link-lib=static=_3dtile"); + println!("cargo:rustc-link-lib=static=ufbx"); + println!("cargo:rustc-link-lib=static=draco"); + + // 2. OSG + println!("cargo:rustc-link-lib=osgdb_jpeg"); + println!("cargo:rustc-link-lib=osgdb_tga"); + println!("cargo:rustc-link-lib=osgdb_rgb"); + println!("cargo:rustc-link-lib=osgdb_png"); + println!("cargo:rustc-link-lib=osgdb_osg"); + println!("cargo:rustc-link-lib=osgdb_serializers_osg"); + println!("cargo:rustc-link-lib=osgUtil"); + println!("cargo:rustc-link-lib=osgDB"); + println!("cargo:rustc-link-lib=osg"); + println!("cargo:rustc-link-lib=OpenThreads"); + + // 3. GDAL dependencies + println!("cargo:rustc-link-lib=gdal"); + println!("cargo:rustc-link-lib=geos_c"); + println!("cargo:rustc-link-lib=geos"); + println!("cargo:rustc-link-lib=proj"); + println!("cargo:rustc-link-lib=sqlite3"); + println!("cargo:rustc-link-lib=expat"); + println!("cargo:rustc-link-lib=curl"); + println!("cargo:rustc-link-lib=ssl"); + println!("cargo:rustc-link-lib=crypto"); + println!("cargo:rustc-link-lib=kmlbase"); + println!("cargo:rustc-link-lib=kmlengine"); + println!("cargo:rustc-link-lib=kmldom"); + println!("cargo:rustc-link-lib=kmlconvenience"); + println!("cargo:rustc-link-lib=Lerc"); + println!("cargo:rustc-link-lib=json-c"); + println!("cargo:rustc-link-lib=sharpyuv"); + + // 4. Image / compression libraries + println!("cargo:rustc-link-lib=geotiff"); + println!("cargo:rustc-link-lib=gif"); + println!("cargo:rustc-link-lib=jpeg"); + println!("cargo:rustc-link-lib=png"); + println!("cargo:rustc-link-lib=tiff"); + println!("cargo:rustc-link-lib=webp"); + println!("cargo:rustc-link-lib=xml2"); + println!("cargo:rustc-link-lib=lzma"); + println!("cargo:rustc-link-lib=openjp2"); + println!("cargo:rustc-link-lib=qhullstatic_r"); + println!("cargo:rustc-link-lib=minizip"); + println!("cargo:rustc-link-lib=spatialite"); + println!("cargo:rustc-link-lib=freexl"); + println!("cargo:rustc-link-lib=basisu_encoder"); + println!("cargo:rustc-link-lib=meshoptimizer"); + println!("cargo:rustc-link-lib=zstd"); + + // 5. GeographicLib + println!("cargo:rustc-link-lib=GeographicLib"); + + // 6. System frameworks + println!("cargo:rustc-link-lib=framework=Security"); + println!("cargo:rustc-link-lib=framework=CoreFoundation"); + println!("cargo:rustc-link-lib=framework=Foundation"); + println!("cargo:rustc-link-lib=framework=OpenGL"); + println!("cargo:rustc-link-lib=framework=AppKit"); + println!("cargo:rustc-link-lib=framework=SystemConfiguration"); + + // 7. Additional linker flags + println!("cargo:rustc-link-arg=-ObjC"); + println!("cargo:rustc-link-arg=-all_load"); +} +``` + +--- + +## 四、Cargo.toml更新 + +```toml +[package] +name = "_3dtile" +version = "0.1.0" +authors = ["fanzhenhua "] +edition = "2021" + +[dependencies] +libc = "0.2" +clap = "4.5.53" +chrono = "0.4" +rayon = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde-xml-rs = "0.8.2" +log = "0.4" +env_logger = "0.11.8" +byteorder = "1.2" +thiserror = "1.0" # 新增:用于错误处理 + +[build-dependencies] +cmake = "0.1" +pkg-config = "0.3" +thiserror = "1.0" # 新增 +``` + +--- + +## 五、目录结构 + +``` +/Users/wallance/Developer/cim/thirdparty/3dtiles/ +├── build.rs # 简化后的入口(~200行) +├── Cargo.toml # 添加thiserror依赖 +├── build/ # 新增:构建模块目录 +│ ├── mod.rs # 模块导出 +│ ├── config.rs # BuildConfig, Platform, BuildError +│ ├── cmake.rs # CMakeBuilder +│ └── utils.rs # 工具函数 +├── CMakeLists.txt # 不变 +└── src/ # 源代码目录 + └── ... +``` + +--- + +## 六、优化效果对比 + +| 指标 | 重构前 | 重构后 | 改善 | +|-----|-------|-------|-----| +| build.rs代码行数 | ~731行 | ~200行 | **减少73%** | +| 平台构建函数 | 4个(各~150行) | 3个链接函数(各~50行) | **消除重复** | +| 错误处理 | 混合unwrap/expect | 统一Result类型 | **类型安全** | +| 新增平台支持 | 需复制~150行代码 | 添加1个链接函数 | **维护性↑** | +| 可读性 | 低(逻辑分散) | 高(结构清晰) | **可维护性↑** | + +--- + +## 七、后续优化建议 + +### 7.1 独立C++编译模式 + +支持仅生成CMake配置,不执行构建,便于CI/CD分离构建: + +```rust +enum BuildMode { + Integrated, // 正常模式:Cargo驱动CMake + CppOnly, // 仅生成CMake配置 + Prebuilt(PathBuf), // 使用预构建的C++库 +} +``` + +### 7.2 vcpkg自动链接 + +考虑使用`vcpkg` crate自动处理依赖链接: + +```rust +// [build-dependencies] +// vcpkg = "0.2" + +let lib = vcpkg::Config::new() + .emit_includes(true) + .find_package("gdal")?; +``` + +### 7.3 构建缓存 + +添加构建缓存支持,避免重复编译未变更的C++代码。 + +--- + +## 八、实施检查清单 + +- [ ] 创建`build/`目录和模块文件 +- [ ] 更新`Cargo.toml`添加`thiserror`依赖 +- [ ] 迁移工具函数到`build/utils.rs` +- [ ] 实现`BuildConfig`和`Platform`枚举 +- [ ] 实现`CMakeBuilder` +- [ ] 重构`build.rs`主函数 +- [ ] 测试Windows构建 +- [ ] 测试Linux构建 +- [ ] 测试macOS ARM64构建 +- [ ] 测试macOS x86_64构建 diff --git a/docs/codebase_optimization_guide.md b/docs/codebase_optimization_guide.md new file mode 100644 index 00000000..300163e6 --- /dev/null +++ b/docs/codebase_optimization_guide.md @@ -0,0 +1,1153 @@ +# 3DTiles工程优化指南 + +> 创建时间: 2026-02-26 +> 目标: 从Rust和C++专业架构师角度,提供日志、错误处理、架构设计、代码格式化、lint、clippy、命名空间、目录结构等全方位优化建议 + +--- + +## 目录 + +1. [项目架构概览](#一项目架构概览) +2. [日志系统优化](#二日志系统优化) +3. [错误处理机制优化](#三错误处理机制优化) +4. [架构设计优化](#四架构设计优化) +5. [代码格式化与Lint配置](#五代码格式化与lint配置) +6. [C++命名空间规范](#六c命名空间规范) +7. [C++目录结构规范](#七c目录结构规范) +8. [Unsafe代码安全审查](#八unsafe代码安全审查) +9. [优化任务优先级](#九优化任务优先级) +10. [实施检查清单](#十实施检查清单) + +--- + +## 一、项目架构概览 + +### 1.1 当前架构 + +这是一个**Rust + C++混合架构**的3D Tiles转换工具: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Rust Layer │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ main.rs │ │ CLI Parser │ │ Thread Pool ( Rayon)│ │ +│ │ (774 lines)│ │ (clap) │ │ │ │ +│ └──────┬──────┘ └─────────────┘ └─────────────────────┘ │ +│ │ │ +│ ┌──────▼──────────────────────────────────────────────┐ │ +│ │ FFI Layer │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────────┐ │ │ +│ │ │ fbx.rs │ │ osgb.rs │ │ shape.rs│ │ fun_c.rs │ │ │ +│ │ └────┬────┘ └────┬────┘ └────┬────┘ └─────┬──────┘ │ │ +│ └───────┼───────────┼───────────┼────────────┼─────────┘ │ +└──────────┼───────────┼───────────┼────────────┼─────────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ C++ Layer │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ +│ │ FBX │ │ OSGB │ │ Shapefile │ │ +│ │ Pipeline │ │ Converter │ │ Converter │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ +│ │ GLTF │ │ B3DM │ │ Tileset │ │ +│ │ Builder │ │ Generator │ │ Builder │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 1.2 各层职责 + +| 层级 | 职责 | 主要文件 | +|------|------|----------| +| **Rust层** | CLI入口、参数解析、流程编排、并发处理 | `main.rs`, `osgb.rs`, `fbx.rs`, `shape.rs` | +| **FFI层** | Rust与C++的桥接、类型转换 | `fun_c.rs`, `extern.h` | +| **C++层** | 核心几何处理、FBX/OSGB解析、GLTF构建 | `src/*.cpp`, `src/*/*.cpp` | + +--- + +## 二、日志系统优化 + +### 2.1 当前实现分析 + +| 层级 | 实现方式 | 问题 | +|------|---------|------| +| Rust | `env_logger` + `log` crate | 功能基础,无结构化输出 | +| C++ | `spdlog` header-only | 缓冲区溢出风险,无类型安全 | + +**问题代码** (`src/extern.h:16-40`): + +```cpp +// 问题:固定1024字节缓冲区,可能溢出 +inline void log_printf_impl(spdlog::level::level_enum lvl, const char* format, ...) { + char buf[1024]; // 固定缓冲区 + va_list args; + va_start(args, format); + std::vsnprintf(buf, sizeof(buf), format, args); // 截断但无警告 + va_end(args); + spdlog::log(lvl, "{}", buf); +} +``` + +### 2.2 优化建议 + +#### 2.2.1 C++层使用类型安全格式化 + +```cpp +// include/tilecore/logging.h +#pragma once +#include +#include + +namespace tilecore { + +// 使用fmt的类型安全格式化,避免缓冲区问题 +template +void log_debug(fmt::format_string fmt, Args&&... args) { + spdlog::debug(fmt, std::forward(args)...); +} + +template +void log_info(fmt::format_string fmt, Args&&... args) { + spdlog::info(fmt, std::forward(args)...); +} + +template +void log_warn(fmt::format_string fmt, Args&&... args) { + spdlog::warn(fmt, std::forward(args)...); +} + +template +void log_error(fmt::format_string fmt, Args&&... args) { + spdlog::error(fmt, std::forward(args)...); +} + +} // namespace tilecore +``` + +#### 2.2.2 Rust层考虑使用`tracing` + +```toml +# Cargo.toml +[dependencies] +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } +``` + +```rust +// src/logging.rs +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +pub fn setup_logging(verbose: bool) { + let filter = if verbose { + "debug" + } else { + "info" + }; + + tracing_subscriber::registry() + .with(EnvFilter::new(filter)) + .with(fmt::layer().with_target(false)) + .init(); +} +``` + +### 2.3 迁移路径 + +```cpp +// 旧代码 +LOG_E("Failed to load file: %s, error: %d", filename.c_str(), errno); + +// 新代码 +tilecore::log_error("Failed to load file: {}, error: {}", filename, errno); +``` + +--- + +## 三、错误处理机制优化 + +### 3.1 当前问题 + +1. **过度使用`unwrap()`/`expect()`** - `main.rs`中发现**40+处** + +```rust +// src/main.rs:301 - 可能panic +let input = abs_input_buf.to_str().unwrap(); + +// src/main.rs:551 - 可能panic +let exe_dir = ::std::env::current_exe().unwrap(); +``` + +2. **错误类型不一致** + - Rust: `Box`(过于泛化) + - C++: 无统一错误码,使用bool返回值 + +3. **FFI边界错误处理薄弱** + +```rust +// src/fbx.rs:47 - 仅检查null,无详细错误信息 +unsafe { + let out_ptr = fbx23dtile(...); + if out_ptr.is_null() { + return Err(From::from(format!("FBX conversion failed"))); + } +} +``` + +### 3.2 优化建议 + +#### 3.2.1 Rust层定义结构化错误 + +```rust +// src/error.rs +use thiserror::Error; +use std::path::PathBuf; + +#[derive(Error, Debug)] +pub enum TileError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Invalid path: {path}")] + InvalidPath { path: String }, + + #[error("Path contains invalid UTF-8: {path}")] + InvalidUtf8 { path: PathBuf }, + + #[error("FFI call failed: {func} - {reason}")] + FfiError { func: &'static str, reason: String }, + + #[error("Conversion failed for format: {format}")] + ConversionFailed { format: String }, + + #[error("Required argument missing: {arg}")] + MissingArgument { arg: &'static str }, + + #[error("Invalid coordinate: {value}")] + InvalidCoordinate { value: String }, + + #[error("Geoid initialization failed: {model}")] + GeoidInitFailed { model: String }, +} + +pub type TileResult = Result; +``` + +#### 3.2.2 C++层引入错误码机制 + +```cpp +// include/tilecore/error.h +#pragma once +#include +#include + +namespace tilecore { + +enum class ErrorCode { + Success = 0, + InvalidInput, + FileNotFound, + ParseError, + OutOfMemory, + UnsupportedFormat, + ConversionFailed, + IoError, +}; + +class Error { +public: + Error(ErrorCode code, std::string message) + : code_(code), message_(std::move(message)) {} + + ErrorCode code() const { return code_; } + const std::string& message() const { return message_; } + + bool is_success() const { return code_ == ErrorCode::Success; } + +private: + ErrorCode code_; + std::string message_; +}; + +// Result类型模板 +template +class Result { +public: + static Result ok(T value) { + return Result(std::move(value), std::nullopt); + } + + static Result err(Error error) { + return Result(std::nullopt, std::move(error)); + } + + bool is_ok() const { return value_.has_value(); } + bool is_err() const { return error_.has_value(); } + + T& value() { return value_.value(); } + Error& error() { return error_.value(); } + +private: + Result(std::optional value, std::optional error) + : value_(std::move(value)), error_(std::move(error)) {} + + std::optional value_; + std::optional error_; +}; + +} // namespace tilecore +``` + +#### 3.2.3 FFI边界添加错误传播 + +```rust +// src/ffi/mod.rs +use crate::error::{TileError, TileResult}; +use std::ffi::CStr; + +// 从C++获取最后错误信息 +extern "C" { + fn get_last_error_message() -> *const libc::c_char; +} + +pub fn get_last_error() -> Option { + unsafe { + let ptr = get_last_error_message(); + if ptr.is_null() { + None + } else { + CStr::from_ptr(ptr).to_str().ok().map(|s| s.to_string()) + } + } +} + +// 封装FFI调用 +pub fn call_fbx_converter(args: &FbxArgs) -> TileResult<()> { + unsafe { + let result = fbx23dtile(/* args */); + if result.is_null() { + let reason = get_last_error() + .unwrap_or_else(|| "Unknown error".to_string()); + return Err(TileError::FfiError { + func: "fbx23dtile", + reason, + }); + } + libc::free(result); + Ok(()) + } +} +``` + +--- + +## 四、架构设计优化 + +### 4.1 当前架构问题 + +1. **模块职责不清晰** + - `main.rs`过长(774行),包含CLI解析、环境设置、业务逻辑 + - `fun_c.rs`命名不清晰,包含文件操作和地理计算 + +2. **FFI边界混乱** + - 多处`extern "C"`块分散在各模块 + - 无统一的FFI安全抽象层 + +3. **并发模型待优化** + +```rust +// src/osgb.rs:127 - 使用裸channel,可考虑Rayon parallel iterator +.map(|info| unsafe { ... }) +``` + +### 4.2 推荐模块结构 + +#### 4.2.1 Rust层重构 + +``` +src/ +├── main.rs # CLI入口,仅做参数解析和dispatch (~100行) +├── cli.rs # clap参数定义 +├── lib.rs # 库入口(支持作为库使用) +├── error.rs # 统一错误类型 +├── logging.rs # 日志配置 +│ +├── ffi/ # FFI抽象层 +│ ├── mod.rs # FFI模块导出 +│ ├── fbx.rs # FBX相关FFI +│ ├── osgb.rs # OSGB相关FFI +│ ├── shape.rs # Shapefile相关FFI +│ └── utils.rs # FFI工具函数 +│ +├── converters/ # 业务逻辑层 +│ ├── mod.rs +│ ├── fbx.rs # FBX转换器 +│ ├── osgb.rs # OSGB转换器 +│ ├── shape.rs # Shapefile转换器 +│ └── common.rs # 通用转换逻辑 +│ +└── utils.rs # 通用工具 +``` + +#### 4.2.2 FFI安全封装示例 + +```rust +// src/ffi/mod.rs +pub mod fbx; +pub mod osgb; +pub mod shape; + +use crate::error::TileResult; + +/// FFI调用安全封装trait +pub unsafe trait CApi { + type Output; + + fn call(&self) -> TileResult; +} + +/// 封装所有unsafe调用 +pub struct FbxConverter { + input_path: CString, + output_path: CString, + // ... +} + +impl FbxConverter { + pub fn new(input: &Path, output: &Path) -> TileResult { + Ok(Self { + input_path: path_to_cstring(input)?, + output_path: path_to_cstring(output)?, + }) + } + + pub fn convert(&self) -> TileResult<()> { + unsafe { + let result = fbx23dtile( + self.input_path.as_ptr(), + self.output_path.as_ptr(), + // ... + ); + + if result.is_null() { + return Err(TileError::FfiError { + func: "fbx23dtile", + reason: get_last_error_message(), + }); + } + + libc::free(result); + Ok(()) + } + } +} + +fn path_to_cstring(path: &Path) -> TileResult { + let s = path.to_str() + .ok_or_else(|| TileError::InvalidUtf8 { path: path.to_path_buf() })?; + CString::new(s) + .map_err(|_| TileError::InvalidPath { path: s.to_string() }) +} +``` + +--- + +## 五、代码格式化与Lint配置 + +### 5.1 当前状态 + +- ❌ 无`rustfmt.toml` - 使用默认配置 +- ❌ 无`clippy.toml` - 使用默认配置 +- ⚠️ `.cargo/config.toml` 中有警告配置但被注释 + +### 5.2 推荐配置 + +#### 5.2.1 rustfmt.toml + +```toml +# rustfmt.toml +edition = "2021" +max_width = 100 +tab_spaces = 4 +use_small_heuristics = "Default" + +# 导入格式化 +imports_granularity = "Crate" +group_imports = "StdExternalCrate" +reorder_imports = true + +# 代码风格 +fn_single_line = false +match_block_trailing_comma = true +trailing_comma = "Vertical" + +# 文档注释 +wrap_comments = true +comment_width = 80 +format_code_in_doc_comments = true +``` + +#### 5.2.2 clippy.toml + +```toml +# clippy.toml +avoid-breaking-exported-api = false +msrv = "1.70" + +# 允许的类型复杂度 +type-complexity-threshold = 300 + +# 大型枚举变体阈值 +enum-variant-size-threshold = 300 +``` + +#### 5.2.3 Cargo.toml Workspace Lints + +```toml +# Cargo.toml +[workspace.lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } + +# 必须修复 +unwrap_used = "deny" +expect_used = "deny" +panic = "deny" + +# 建议修复 +complexity = "warn" +perf = "warn" +style = "warn" +suspicious = "warn" + +# 允许(由于FFI需要) +unsafe_code = "allow" + +# 允许(根据实际情况调整) +too_many_lines = "allow" +type_complexity = "allow" +``` + +### 5.3 修复当前Clippy错误 + +```rust +// build/utils.rs:57 - 当前错误代码 +io::Error::new( + io::ErrorKind::Other, + format!("Failed to create symlink..."), +) + +// 修复后 - 使用io::Error::other (Rust 1.74+) +io::Error::other(format!("Failed to create symlink...")) +``` + +--- + +## 六、C++命名空间规范 + +### 6.1 当前命名空间分析 + +| 命名空间 | 使用位置 | 问题 | +|----------|---------|------| +| `gltf` | `src/gltf/*` | ✅ 良好 | +| `gltf::extensions` | `src/gltf/extensions/*` | ✅ 良好 | +| `fbx` | `src/fbx/*` | ✅ 良好 | +| `shapefile` | `src/shapefile/*` | ✅ 良好 | +| `spatial::strategy` | `src/spatial/strategy/*` | ✅ 良好 | +| `osg::utils` | `src/osg/utils/*` | ⚠️ 嵌套过深 | +| `b3dm` | `src/b3dm/*` | ✅ 良好 | +| `std` (特化) | `src/fbx.h:48` | ❌ 污染std命名空间 | +| `using namespace std` | `osgb23dtile.cpp`, `shp23dtile.cpp` | ❌ 全局污染 | + +### 6.2 问题代码示例 + +```cpp +// src/fbx.h:48 - 污染std命名空间 +namespace std { + template<> + struct hash { + size_t operator()(const MeshKey &k) const { + return hash()(k.geomHash) ^ (hash()(k.matHash) << 1); + } + }; +} + +// src/osgb23dtile.cpp:38 - 全局using namespace +using namespace std; +``` + +### 6.3 推荐命名空间规范 + +#### 6.3.1 根命名空间 + +所有代码应放在`tilecore`根命名空间下: + +```cpp +// include/tilecore/core.h +namespace tilecore { + +// 核心类型和常量 +constexpr int MAX_TILE_LEVEL = 20; + +} // namespace tilecore +``` + +#### 6.3.2 子模块命名空间 + +```cpp +// 推荐结构 +tilecore::io // 文件IO +tilecore::geometry // 几何处理 +tilecore::gltf // GLTF相关 +tilecore::gltf::ext // GLTF扩展(替代gltf::extensions) +tilecore::fbx // FBX处理 +tilecore::osgb // OSGB处理 +tilecore::shapefile // Shapefile处理 +tilecore::b3dm // B3DM格式 +tilecore::tileset // Tileset构建 +tilecore::spatial // 空间索引 +tilecore::coords // 坐标转换 +tilecore::math // 数学工具 +tilecore::logging // 日志 +tilecore::utils // 通用工具 +``` + +#### 6.3.3 修复std特化 + +```cpp +// include/tilecore/fbx/mesh_key.h +#pragma once +#include + +namespace tilecore { +namespace fbx { + +struct MeshKey { + std::string geomHash; + std::string matHash; + + bool operator==(const MeshKey& other) const { + return geomHash == other.geomHash && matHash == other.matHash; + } +}; + +} // namespace fbx +} // namespace tilecore + +// 在std命名空间特化(这是唯一允许的std扩展) +namespace std { + +template<> +struct hash { + size_t operator()(const tilecore::fbx::MeshKey& k) const { + return hash()(k.geomHash) ^ + (hash()(k.matHash) << 1); + } +}; + +} // namespace std +``` + +#### 6.3.4 禁止全局using namespace + +```cpp +// ❌ 禁止 +using namespace std; + +// ✅ 推荐 +using std::string; +using std::vector; +using std::filesystem::path; + +// 或在函数内部使用 +void process() { + using namespace std::literals::string_literals; + auto s = "hello"s; +} +``` + +### 6.4 命名空间别名 + +```cpp +// 长命名空间使用别名 +namespace tgf = tilecore::gltf; +namespace tfx = tilecore::fbx; +namespace tio = tilecore::io; +``` + +--- + +## 七、C++目录结构规范 + +### 7.1 当前目录结构问题 + +``` +src/ +├── fbx.h # ❌ 根目录头文件过多 +├── extern.h # ❌ 职责不清晰 +├── shape.h # ❌ 根目录头文件过多 +├── FBXPipeline.h # ❌ 命名不一致(大驼峰) +├── osg_fix.h # ⚠️ 平台相关代码 +├── ... +├── fbx/ # ✅ 良好 +├── gltf/ # ✅ 良好 +├── osg/ # ✅ 良好 +├── shapefile/ # ✅ 良好 +├── spatial/ # ✅ 良好 +├── tileset/ # ✅ 良好 +├── coords/ # ✅ 良好 +└── common/ # ✅ 良好 +``` + +### 7.2 推荐目录结构 + +``` +include/tilecore/ # 公共头文件(对外接口) +├── core.h # 核心类型、常量 +├── error.h # 错误处理 +├── logging.h # 日志接口 +├── math.h # 数学工具 +├── io/ +│ ├── file_utils.h +│ └── path_utils.h +├── geometry/ +│ ├── bounding_box.h +│ ├── mesh.h +│ └── transform.h +├── coords/ +│ ├── coordinate_system.h +│ └── coordinate_transformer.h +└── tileset/ + ├── tileset.h + ├── bounding_volume.h + └── geometric_error.h + +src/ # 实现文件 +├── main.cpp # 程序入口(如果是可执行文件) +├── core/ # 核心实现 +│ ├── error.cpp +│ └── logging.cpp +├── io/ +│ └── file_utils.cpp +├── geometry/ +│ └── mesh.cpp +├── fbx/ # FBX模块 +│ ├── fbx_importer.h # 内部头文件 +│ ├── fbx_importer.cpp +│ ├── fbx_geometry_extractor.h +│ ├── fbx_geometry_extractor.cpp +│ └── ... +├── osgb/ # OSGB模块(原osgb23dtile重构) +│ ├── osgb_importer.h +│ ├── osgb_importer.cpp +│ └── ... +├── gltf/ # GLTF模块 +│ ├── gltf_builder.h +│ ├── gltf_builder.cpp +│ ├── extensions/ +│ │ ├── draco.h +│ │ └── draco.cpp +│ └── ... +├── b3dm/ # B3DM模块 +│ ├── b3dm_generator.h +│ └── b3dm_generator.cpp +├── shapefile/ # Shapefile模块 +│ ├── shapefile_reader.h +│ └── shapefile_reader.cpp +├── tileset/ # Tileset模块 +│ └── tileset_builder.cpp +└── ffi/ # FFI接口层 + ├── ffi_bridge.h + └── ffi_bridge.cpp + +thirdparty/ # 第三方库 +├── ufbx/ +├── stb/ +└── ... + +tests/ # 测试代码 +├── unit/ +├── integration/ +└── e2e/ +``` + +### 7.3 头文件组织规范 + +#### 7.3.1 头文件命名 + +```cpp +// ✅ 使用小写+下划线 +mesh_processor.h +geometry_extractor.h +tileset_builder.h + +// ❌ 避免 +MeshProcessor.h +geometryExtractor.h +``` + +#### 7.3.2 头文件内容组织 + +```cpp +// include/tilecore/fbx/importer.h +#pragma once + +// 1. 标准库头文件 +#include +#include +#include + +// 2. 第三方库头文件 +#include +#include + +// 3. 项目内部头文件 +#include "tilecore/core.h" +#include "tilecore/geometry/mesh.h" +#include "tilecore/error.h" + +namespace tilecore { +namespace fbx { + +// 前向声明 +class MeshProcessor; + +// 类定义 +class Importer { +public: + explicit Importer(const std::string& filepath); + ~Importer(); + + // 禁止拷贝 + Importer(const Importer&) = delete; + Importer& operator=(const Importer&) = delete; + + // 允许移动 + Importer(Importer&&) noexcept; + Importer& operator=(Importer&&) noexcept; + + Result import(); + +private: + class Impl; // PIMPL模式 + std::unique_ptr pImpl; +}; + +} // namespace fbx +} // namespace tilecore +``` + +#### 7.3.3 内部vs公共头文件 + +```cpp +// 公共头文件 - include/tilecore/fbx/importer.h +// 只暴露必要接口 + +// 内部头文件 - src/fbx/fbx_internal.h +// 包含实现细节,不对外暴露 +#pragma once +#include "tilecore/fbx/importer.h" + +namespace tilecore { +namespace fbx { +namespace internal { + +// 内部辅助函数 +void process_mesh_data(/* ... */); + +} // namespace internal +} // namespace fbx +} // namespace tilecore +``` + +### 7.4 CMake组织 + +```cmake +# CMakeLists.txt + +# 公共接口库 +add_library(tilecore INTERFACE) +target_include_directories(tilecore INTERFACE + $ + $ +) + +# 核心实现库 +add_library(tilecore_internal STATIC + src/core/error.cpp + src/core/logging.cpp + src/io/file_utils.cpp + # ... +) +target_link_libraries(tilecore_internal PUBLIC tilecore) + +# 各模块库 +add_library(tilecore_fbx STATIC + src/fbx/fbx_importer.cpp + src/fbx/fbx_geometry_extractor.cpp + # ... +) +target_link_libraries(tilecore_fbx PUBLIC tilecore_internal) + +# 可执行文件 +add_executable(3dtile src/main.cpp) +target_link_libraries(3dtile PRIVATE + tilecore_fbx + tilecore_osgb + tilecore_gltf + # ... +) +``` + +--- + +## 八、Unsafe代码安全审查 + +### 8.1 当前Unsafe使用情况 + +共发现**22处**`unsafe`使用: + +| 类型 | 数量 | 位置 | 风险等级 | +|------|------|------|----------| +| FFI调用 | 15 | `fbx.rs`, `osgb.rs`, `shape.rs`, `fun_c.rs` | 中 | +| 环境变量设置 | 6 | `main.rs` | 低(单线程时安全) | +| 字符串转换 | 1 | `fun_c.rs` | 中 | + +### 8.2 风险点分析 + +#### 8.2.1 环境变量设置的线程安全问题 + +```rust +// src/main.rs:39 - unsafe设置环境变量 +unsafe { env::set_var("OSG_LIBRARY_PATH", &plugins_dir) } + +// 风险:如果在多线程环境下调用,可能导致未定义行为 +``` + +#### 8.2.2 C字符串转换无空检查 + +```rust +// src/shape.rs:136 - 如果包含null字节会panic +let source_vec = CString::new(from).unwrap(); +``` + +### 8.3 安全封装建议 + +#### 8.3.1 封装unsafe环境变量设置 + +```rust +// src/utils/env.rs +use std::path::Path; +use std::sync::Once; + +static ENV_INIT: Once = Once::new(); + +/// 在程序启动时(单线程)安全地设置环境变量 +pub fn init_environment() { + ENV_INIT.call_once(|| { + // 所有环境变量设置在这里完成 + unsafe { + std::env::set_var("RUST_BACKTRACE", "1"); + } + }); +} + +/// 路径类型安全的环境变量设置 +pub fn set_path_env(key: &str, path: &Path) -> TileResult<()> { + let path_str = path.to_str() + .ok_or_else(|| TileError::InvalidUtf8 { + path: path.to_path_buf() + })?; + + // 确保只在初始化阶段调用 + if ENV_INIT.is_completed() { + return Err(TileError::FfiError { + func: "set_path_env", + reason: "Environment already initialized".to_string(), + }); + } + + unsafe { + std::env::set_var(key, path_str); + } + Ok(()) +} +``` + +#### 8.3.2 安全的C字符串转换 + +```rust +// src/ffi/utils.rs +use std::ffi::CString; +use std::path::Path; + +/// 安全的Path到CString转换 +pub fn path_to_cstring(path: &Path) -> TileResult { + let s = path.to_str() + .ok_or_else(|| TileError::InvalidUtf8 { + path: path.to_path_buf() + })?; + + CString::new(s) + .map_err(|_| TileError::InvalidPath { + path: s.to_string() + }) +} + +/// 安全的String到CString转换 +pub fn string_to_cstring(s: String) -> TileResult { + CString::new(s.clone()) + .map_err(|_| TileError::InvalidPath { path: s }) +} +``` + +#### 8.3.3 FFI调用统一封装 + +```rust +// src/ffi/macros.rs + +/// 安全的FFI调用宏 +#[macro_export] +macro_rules! safe_ffi_call { + ($func:ident($($arg:expr),*)) => {{ + let result = unsafe { $func($($arg),*) }; + if result.is_null() { + Err(TileError::FfiError { + func: stringify!($func), + reason: $crate::ffi::get_last_error() + .unwrap_or_else(|| "Unknown error".to_string()), + }) + } else { + Ok(result) + } + }}; +} + +// 使用示例 +let ptr = safe_ffi_call!(fbx23dtile( + input_path.as_ptr(), + output_path.as_ptr(), + // ... +))?; +``` + +--- + +## 九、优化任务优先级 + +### 9.1 优先级矩阵 + +| 优先级 | 任务 | 影响 | 工作量 | 风险 | +|-------|------|------|--------|------| +| **P0** | 修复clippy错误 | 构建失败 | 5分钟 | 无 | +| **P0** | 减少`unwrap()`使用 | 稳定性 | 2小时 | 低 | +| **P1** | 统一错误处理类型 | 可维护性 | 4小时 | 中 | +| **P1** | FFI安全封装 | 安全性 | 6小时 | 中 | +| **P2** | 日志系统升级 | 可观测性 | 4小时 | 低 | +| **P2** | 添加rustfmt/clippy配置 | 代码质量 | 30分钟 | 无 | +| **P2** | C++命名空间规范化 | 可维护性 | 4小时 | 中 | +| **P3** | Rust模块重构 | 架构 | 2天 | 高 | +| **P3** | C++目录结构重构 | 架构 | 3天 | 高 | +| **P3** | C++现代化改造 | 性能/安全 | 3天 | 高 | + +### 9.2 实施路线图 + +``` +第1周:紧急修复 +├── Day 1: 修复clippy错误,启用CI检查 +├── Day 2-3: 替换关键unwrap调用 +└── Day 4-5: 统一错误处理类型 + +第2周:基础改进 +├── Day 1-2: FFI安全封装 +├── Day 3: 日志系统升级 +├── Day 4: 添加格式化配置 +└── Day 5: C++命名空间规范化 + +第3-4周:架构重构 +├── Week 3: Rust模块重构 +└── Week 4: C++目录结构重构 +``` + +--- + +## 十、实施检查清单 + +### 10.1 立即执行(P0) + +- [ ] 修复`build/utils.rs:57`的`io::Error::other`调用 +- [ ] 取消注释`.cargo/config.toml`中的`rustflags = ["-D", "warnings"]` +- [ ] 运行`cargo fix --allow-dirty`自动修复简单问题 +- [ ] 识别并替换所有关键路径上的`unwrap()`调用 + +### 10.2 短期优化(P1-P2) + +- [ ] 创建`src/error.rs`定义`TileError` +- [ ] 创建`src/ffi/mod.rs`封装FFI调用 +- [ ] 创建`rustfmt.toml`和`clippy.toml` +- [ ] 在`Cargo.toml`中添加workspace lints +- [ ] 创建C++ `tilecore`命名空间 +- [ ] 移动`std::hash`特化到正确位置 +- [ ] 移除所有`using namespace std` + +### 10.3 长期重构(P3) + +- [ ] 重构Rust模块结构 +- [ ] 重构C++目录结构 +- [ ] 添加完整单元测试 +- [ ] 集成代码覆盖率检查 +- [ ] 添加性能基准测试 + +--- + +## 附录 + +### A. 参考资源 + +- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) +- [C++ Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) +- [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) +- [Rust FFI Guidelines](https://doc.rust-lang.org/nomicon/ffi.html) + +### B. 工具推荐 + +| 工具 | 用途 | 配置 | +|------|------|------| +| rustfmt | 代码格式化 | `rustfmt.toml` | +| clippy | 静态分析 | `clippy.toml` + `Cargo.toml` | +| cargo-deny | 依赖检查 | `deny.toml` | +| cargo-audit | 安全审计 | CI集成 | +| clang-tidy | C++静态分析 | `.clang-tidy` | +| cppcheck | C++静态分析 | CI集成 | + +### C. CI/CD建议 + +```yaml +# .github/workflows/ci.yml +name: CI + +on: [push, pull_request] + +jobs: + rust-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-action@stable + - run: cargo fmt --check + - run: cargo clippy --all-targets --all-features -- -D warnings + - run: cargo test + + cpp-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + - run: clang-tidy -p build src/**/*.cpp + - run: cppcheck --enable=all --error-exitcode=1 src/ +``` + +--- + +*文档版本: 1.0* +*最后更新: 2026-02-26* diff --git a/docs/coordinate_system_refactor.md b/docs/coordinate_system_refactor.md index 127d73e1..c0db88b8 100644 --- a/docs/coordinate_system_refactor.md +++ b/docs/coordinate_system_refactor.md @@ -655,3 +655,112 @@ double CoordinateTransformer::ApplyGeoidCorrection(double lat, double lon, doubl .ConvertOrthometricToEllipsoidal(lat, lon, height); } ``` + +## 11. 实现状态 + +### 11.1 已完成 + +| 模块 | 文件 | 状态 | +|------|------|------| +| CoordinateSystem | `src/coordinate_system.h/cpp` | ✅ 已实现 | +| CoordinateTransformer | `src/coordinate_transformer.h/cpp` | ✅ 已实现 | +| coords 命名空间 | - | ✅ 已建立 | + +### 11.2 已迁移 + +| 模块 | 状态 | 说明 | +|------|------|------| +| `tileset.cpp` | ✅ 已完成 | 使用 `coords::CoordinateTransformer` 作为全局转换器 | +| `osgb23dtile.cpp` | ✅ 已完成 | 使用 `GetGlobalTransformer()` 获取转换器 | +| `shp23dtile.cpp` | ✅ 已完成 | 使用 `coords::CoordinateTransformer::CalcEnuToEcefMatrix` | +| `FBXPipeline.cpp` | ✅ 已完成 | 使用 `coords::CoordinateTransformer::CalcEnuToEcefMatrix` | +| `GeoTransform.h/cpp` | ✅ 已删除 | 旧代码已移除 | + +### 11.3 待清理 + +| 项目 | 说明 | +|------|------| +| Rust FFI 层注释 | `main.rs` 中残留的 "GeoTransform" 注释文本 | +| `extern.h` 注释 | 残留的 "GeoTransform" 注释文本 | +| `tileset.cpp` 日志 | `[GeoTransform]` 日志前缀可考虑更新 | + +## 12. 与 glTF Writer 重构方案的协调 + +### 12.1 模块关系 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Pipeline Layer │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ ShapefilePipeline│ │ OsgbPipeline │ │ FBXPipeline │ │ +│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ +│ │ │ │ │ +│ └────────────────────┼────────────────────┘ │ +│ │ │ +│ ┌────────────────────┼────────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │coords namespace │ │gltf_writer ns │ │mesh_processor │ │ +│ │CoordinateSystem │ │GltfBuilder │ │simplify_mesh │ │ +│ │CoordinateTrans- │ │B3dmWriter │ │compress_mesh │ │ +│ │former │ │TilesetWriter │ │process_texture │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### 12.2 职责划分 + +| 模块 | 职责 | 命名空间 | +|------|------|----------| +| **coords** | 坐标系定义、坐标转换、地理参考 | `coords::` | +| **gltf_writer** | glTF 模型构建、Extension 管理、B3DM 输出 | `gltf_writer::` | +| **mesh_processor** | 网格简化、Draco 压缩、纹理处理 | 全局函数 | + +### 12.3 协作示例 + +```cpp +#include "coordinate_transformer.h" +#include "gltf_writer/gltf_builder.h" +#include "mesh_processor.h" + +void convertFBXTo3DTiles(const FBXData& fbx_data, + const coords::GeoReference& geo_ref) { + // 1. 坐标转换 + coords::CoordinateSystem cs = coords::CoordinateSystem::LocalCartesian( + coords::UpAxis::Y_UP); + coords::CoordinateTransformer transformer(cs, geo_ref); + + std::vector local_points; + for (const auto& p : fbx_data.positions) { + local_points.push_back(transformer.ToLocalENU( + glm::dvec3(p.x, p.y, p.z))); + } + + // 2. 网格处理 + std::vector vertices; + std::vector indices; + SimplificationParams simp_params{.enable_simplification = true}; + optimize_and_simplify_mesh(vertices, vertex_count, indices, + index_count, simplified_indices, + simplified_count, simp_params); + + // 3. glTF 构建 + gltf_writer::GltfBuilderConfig config { + .enable_draco = true, + .enable_unlit = true + }; + gltf_writer::GltfBuilder builder(config); + + // ... 添加顶点、索引、材质等 + + // 4. B3DM 输出 + gltf_writer::B3dmWriter b3dm_writer; + b3dm_writer.setGlbData(builder.toGLB()); + b3dm_writer.writeToFile("output.b3dm"); +} +``` + +### 12.4 相关文档 + +- glTF Writer 重构方案: `docs/gltf_writer_refactor.md` +- 网格处理模块: `src/mesh_processor.h` diff --git a/docs/fbx_material_implementation_steps.md b/docs/fbx_material_implementation_steps.md new file mode 100644 index 00000000..fd98f6f1 --- /dev/null +++ b/docs/fbx_material_implementation_steps.md @@ -0,0 +1,731 @@ +# FBX材质移植实施步骤 + +本文档提供详细的实施步骤,指导开发者逐步完成FBX材质功能从master分支到refactor-master分支的移植。 + +## 前置条件 + +- 熟悉C++和OSG/GLTF基础 +- 了解FBX材质系统 +- 已阅读`fbx_material_migration.md` + +## 步骤1:扩展IGeometryExtractor接口 + +### 1.1 修改文件 +**文件路径:** `src/common/geometry_extractor.h` + +### 1.2 添加TextureTransformInfo结构 +```cpp +namespace common { + +/** + * @brief 纹理变换信息 + * + * 对应GLTF的KHR_texture_transform扩展 + */ +struct TextureTransformInfo { + float offset[2] = {0.0f, 0.0f}; // UV偏移 + float scale[2] = {1.0f, 1.0f}; // UV缩放 + float rotation = 0.0f; // 旋转(弧度) + int texCoord = 0; // 纹理坐标集 + bool hasTransform = false; // 是否有变换 +}; +``` + +### 1.3 添加MaterialInfo结构 +```cpp +/** + * @brief 材质信息 + * + * 包含完整的PBR材质参数和纹理信息 + */ +struct MaterialInfo { + // ===== 基础PBR参数 ===== + std::vector baseColor = {1.0, 1.0, 1.0, 1.0}; // 基础颜色 [r,g,b,a] + float roughnessFactor = 1.0f; // 粗糙度 [0,1] + float metallicFactor = 0.0f; // 金属度 [0,1] + std::vector emissiveColor = {0.0, 0.0, 0.0}; // 自发光颜色 [r,g,b] + float aoStrength = 1.0f; // 遮挡强度 + + // ===== 纹理对象 ===== + osg::ref_ptr baseColorTexture; // 基础颜色纹理(纹理单元0) + osg::ref_ptr normalTexture; // 法线纹理(纹理单元1) + osg::ref_ptr metallicRoughnessTexture; // 金属度/粗糙度纹理(纹理单元2) + osg::ref_ptr occlusionTexture; // 遮挡纹理(纹理单元3) + osg::ref_ptr emissiveTexture; // 自发光纹理(纹理单元4) + + // ===== 纹理变换 ===== + TextureTransformInfo baseColorTransform; + TextureTransformInfo normalTransform; + TextureTransformInfo metallicRoughnessTransform; + TextureTransformInfo occlusionTransform; + TextureTransformInfo emissiveTransform; + + // ===== Specular-Glossiness(传统材质) ===== + bool useSpecularGlossiness = false; // 是否使用Specular-Glossiness + std::vector diffuseFactor = {1.0, 1.0, 1.0, 1.0}; // 漫反射因子 + std::vector specularFactor = {1.0, 1.0, 1.0}; // 高光因子 + double glossinessFactor = 1.0; // 光泽度 + osg::ref_ptr specularGlossinessTexture; // Specular-Glossiness纹理 + osg::ref_ptr diffuseTexture; // 漫反射纹理 + + // ===== 其他属性 ===== + bool doubleSided = true; // 双面渲染 + std::string alphaMode = "OPAQUE"; // Alpha模式:OPAQUE/MASK/BLEND + float alphaCutoff = 0.5f; // Alpha裁剪值(MASK模式) +}; +``` + +### 1.4 扩展IGeometryExtractor接口 +```cpp +class IGeometryExtractor { +public: + virtual ~IGeometryExtractor() = default; + + /** + * @brief 从空间对象提取几何体 + */ + virtual std::vector> extract( + const spatial::core::SpatialItem* item) = 0; + + /** + * @brief 获取对象的唯一标识(用于BatchID) + */ + virtual std::string getId(const spatial::core::SpatialItem* item) = 0; + + /** + * @brief 获取对象的属性(用于BatchTable) + */ + virtual std::map getAttributes( + const spatial::core::SpatialItem* item) = 0; + + /** + * @brief 获取对象的材质信息(新增) + * + * @param item 空间对象 + * @return 材质信息,如果没有材质返回nullptr + */ + virtual std::shared_ptr getMaterial( + const spatial::core::SpatialItem* item) = 0; +}; + +} // namespace common +``` + +### 1.5 验证步骤 +- [ ] 编译通过,无语法错误 +- [ ] 其他提取器(如Shapefile)需要实现虚函数或保持默认行为 + +--- + +## 步骤2:实现FBXGeometryExtractor::getMaterial + +### 2.1 修改文件 +**文件路径:** `src/fbx/fbx_geometry_extractor.h` 和 `src/fbx/fbx_geometry_extractor.cpp` + +### 2.2 添加头文件包含 +```cpp +#include "../common/geometry_extractor.h" +#include "../osg/utils/material_utils.h" +#include "fbx_spatial_item_adapter.h" +``` + +### 2.3 在类定义中添加方法声明 +```cpp +class FBXGeometryExtractor : public common::IGeometryExtractor { +public: + // ... 现有方法 ... + + std::shared_ptr getMaterial( + const spatial::core::SpatialItem* item) override; +}; +``` + +### 2.4 实现getMaterial方法 +```cpp +std::shared_ptr FBXGeometryExtractor::getMaterial( + const spatial::core::SpatialItem* item) { + + const auto* fbxItem = dynamic_cast(item); + if (!fbxItem) { + LOG_W("FBXGeometryExtractor::getMaterial: item is not FBXSpatialItemAdapter"); + return nullptr; + } + + // 获取几何体的StateSet + const osg::Geometry* geom = fbxItem->getGeometry(); + if (!geom) { + return nullptr; + } + + const osg::StateSet* stateSet = geom->getStateSet(); + if (!stateSet) { + // 没有StateSet表示使用默认材质 + return std::make_shared(); + } + + auto materialInfo = std::make_shared(); + + // ===== 步骤2.4.1:提取基础PBR参数 ===== + osg::utils::PBRParams pbrParams; + osg::utils::MaterialUtils::extractPBRParams(stateSet, pbrParams); + + materialInfo->baseColor = pbrParams.baseColor; + materialInfo->roughnessFactor = pbrParams.roughnessFactor; + materialInfo->metallicFactor = pbrParams.metallicFactor; + materialInfo->emissiveColor = { + pbrParams.emissiveColor[0], + pbrParams.emissiveColor[1], + pbrParams.emissiveColor[2] + }; + materialInfo->aoStrength = pbrParams.aoStrength; + + // ===== 步骤2.4.2:提取纹理对象 ===== + materialInfo->baseColorTexture = + const_cast(osg::utils::MaterialUtils::getBaseColorTexture(stateSet)); + materialInfo->normalTexture = + const_cast(osg::utils::MaterialUtils::getNormalTexture(stateSet)); + materialInfo->emissiveTexture = + const_cast(osg::utils::MaterialUtils::getEmissiveTexture(stateSet)); + + // 金属度/粗糙度和遮挡纹理需要从其他纹理单元获取 + // 根据FBXLoader的实现,它们可能在特定的纹理单元 + if (stateSet->getTextureAttributeList().size() > 2) { + materialInfo->metallicRoughnessTexture = + const_cast(dynamic_cast( + stateSet->getTextureAttribute(2, osg::StateAttribute::TEXTURE))); + } + if (stateSet->getTextureAttributeList().size() > 3) { + materialInfo->occlusionTexture = + const_cast(dynamic_cast( + stateSet->getTextureAttribute(3, osg::StateAttribute::TEXTURE))); + } + + // ===== 步骤2.4.3:提取FBX特有的扩展数据 ===== + // 需要从FBXSpatialItemAdapter获取MaterialExtensionData + // 这需要在FBXSpatialItemAdapter中添加相应的方法 + const MaterialExtensionData* extData = fbxItem->getMaterialExtensionData(); + if (extData) { + // 复制纹理变换 + copyTextureTransform(extData->base_color_transform, materialInfo->baseColorTransform); + copyTextureTransform(extData->normal_transform, materialInfo->normalTransform); + copyTextureTransform(extData->metallic_roughness_transform, materialInfo->metallicRoughnessTransform); + copyTextureTransform(extData->occlusion_transform, materialInfo->occlusionTransform); + copyTextureTransform(extData->emissive_transform, materialInfo->emissiveTransform); + + // 复制Specular-Glossiness数据 + if (extData->specular_glossiness.use_specular_glossiness) { + materialInfo->useSpecularGlossiness = true; + materialInfo->diffuseFactor = { + extData->specular_glossiness.diffuse_factor[0], + extData->specular_glossiness.diffuse_factor[1], + extData->specular_glossiness.diffuse_factor[2], + extData->specular_glossiness.diffuse_factor[3] + }; + materialInfo->specularFactor = { + extData->specular_glossiness.specular_factor[0], + extData->specular_glossiness.specular_factor[1], + extData->specular_glossiness.specular_factor[2] + }; + materialInfo->glossinessFactor = extData->specular_glossiness.glossiness_factor; + } + } + + return materialInfo; +} +``` + +### 2.5 添加辅助函数 +```cpp +namespace { + void copyTextureTransform(const ::TextureTransformData& src, + common::TextureTransformInfo& dst) { + if (src.has_transform) { + dst.hasTransform = true; + dst.offset[0] = src.offset[0]; + dst.offset[1] = src.offset[1]; + dst.scale[0] = src.scale[0]; + dst.scale[1] = src.scale[1]; + dst.rotation = src.rotation; + dst.texCoord = src.tex_coord; + } + } +} +``` + +### 2.6 验证步骤 +- [ ] 编译通过 +- [ ] 单元测试:验证能从FBX正确提取材质信息 + +--- + +## 步骤3:扩展FBXSpatialItemAdapter + +### 3.1 修改文件 +**文件路径:** `src/fbx/fbx_spatial_item_adapter.h` 和 `src/fbx/fbx_spatial_item_adapter.cpp` + +### 3.2 添加方法声明 +```cpp +class FBXSpatialItemAdapter : public spatial::core::SpatialItem { +public: + // ... 现有方法 ... + + /** + * @brief 获取材质扩展数据 + * @return 材质扩展数据指针,如果没有返回nullptr + */ + const MaterialExtensionData* getMaterialExtensionData() const; + + /** + * @brief 设置材质扩展数据 + * @param data 材质扩展数据指针(由外部管理生命周期) + */ + void setMaterialExtensionData(const MaterialExtensionData* data); + +private: + // ... 现有成员 ... + const MaterialExtensionData* materialExtData_ = nullptr; +}; +``` + +### 3.3 实现方法 +```cpp +const MaterialExtensionData* FBXSpatialItemAdapter::getMaterialExtensionData() const { + return materialExtData_; +} + +void FBXSpatialItemAdapter::setMaterialExtensionData(const MaterialExtensionData* data) { + materialExtData_ = data; +} +``` + +--- + +## 步骤4:修改B3DMGenerator支持材质 + +### 4.1 分析现有代码 +**文件路径:** `src/b3dm/b3dm_generator.cpp` + +现有`buildGLTFModel`函数只处理几何体,不处理材质。 + +### 4.2 修改buildGLTFModel函数签名 +```cpp +void B3DMGenerator::buildGLTFModel( + osg::Geometry* mergedGeom, + const spatial::core::SpatialItemRefList& items, + bool enableDraco, + const DracoCompressionParams& dracoParams, + std::vector& glbData, + const std::vector>& materials); // 新增参数 +``` + +### 4.3 在函数内添加材质构建逻辑 +```cpp +void B3DMGenerator::buildGLTFModel( + osg::Geometry* mergedGeom, + const spatial::core::SpatialItemRefList& items, + bool enableDraco, + const DracoCompressionParams& dracoParams, + std::vector& glbData, + const std::vector>& materials) { + + // ... 现有几何体提取代码 ... + + // ===== 新增:构建材质 ===== + std::vector materialIndices; + if (!materials.empty()) { + // 去重:相同的材质只创建一次 + std::map materialCache; + + for (const auto& matInfo : materials) { + std::string matKey = computeMaterialKey(matInfo); + auto it = materialCache.find(matKey); + if (it != materialCache.end()) { + materialIndices.push_back(it->second); + } else { + int matIdx = buildMaterial(matInfo, model, buffer); + materialCache[matKey] = matIdx; + materialIndices.push_back(matIdx); + } + } + } + + // ... 创建primitive时关联材质 ... + tinygltf::Primitive primitive; + // ... 设置attributes ... + + // 关联材质 + if (!materialIndices.empty() && materialIndices[0] >= 0) { + primitive.material = materialIndices[0]; + } + + // ... 其余代码 ... +} +``` + +### 4.4 实现buildMaterial辅助函数 +```cpp +int B3DMGenerator::buildMaterial( + const std::shared_ptr& matInfo, + tinygltf::Model& model, + tinygltf::Buffer& buffer) { + + if (!matInfo) { + return -1; + } + + gltf::MaterialBuilder matBuilder; + gltf::ExtensionManager extMgr; + + // 设置基础PBR参数 + matBuilder.setBaseColor(matInfo->baseColor); + matBuilder.setPBRParams(matInfo->roughnessFactor, matInfo->metallicFactor); + matBuilder.setEmissiveColor(matInfo->emissiveColor); + matBuilder.setDoubleSided(matInfo->doubleSided); + matBuilder.setAlphaMode(matInfo->alphaMode); + + // 处理基础颜色纹理 + if (matInfo->baseColorTexture) { + processAndAddTexture(matInfo->baseColorTexture, + matInfo->baseColorTransform, + matBuilder, model, buffer, extMgr, + TextureType::BASE_COLOR); + } + + // 处理法线纹理 + if (matInfo->normalTexture) { + processAndAddTexture(matInfo->normalTexture, + matInfo->normalTransform, + matBuilder, model, buffer, extMgr, + TextureType::NORMAL); + } + + // 处理自发光纹理 + if (matInfo->emissiveTexture) { + processAndAddTexture(matInfo->emissiveTexture, + matInfo->emissiveTransform, + matBuilder, model, buffer, extMgr, + TextureType::EMISSIVE); + } + + // 处理Specular-Glossiness + if (matInfo->useSpecularGlossiness) { + gltf::extensions::SpecularGlossiness sg; + sg.diffuse_factor = { + matInfo->diffuseFactor[0], + matInfo->diffuseFactor[1], + matInfo->diffuseFactor[2], + matInfo->diffuseFactor[3] + }; + sg.specular_factor = { + matInfo->specularFactor[0], + matInfo->specularFactor[1], + matInfo->specularFactor[2] + }; + sg.glossiness_factor = matInfo->glossinessFactor; + + matBuilder.setSpecularGlossiness(sg); + } + + // 构建材质 + return matBuilder.build(model, extMgr); +} +``` + +### 4.5 实现纹理处理辅助函数 +```cpp +void B3DMGenerator::processAndAddTexture( + osg::Texture* texture, + const common::TextureTransformInfo& transform, + gltf::MaterialBuilder& matBuilder, + tinygltf::Model& model, + tinygltf::Buffer& buffer, + gltf::ExtensionManager& extMgr, + TextureType type) { + + // 处理纹理 + auto result = osg::utils::TextureUtils::processTexture( + texture, config_.enableTextureCompress); + + if (!result.success) { + LOG_W("Failed to process texture"); + return; + } + + // 添加到模型 + bool useBasisu = (result.mimeType == "image/ktx2"); + if (useBasisu) { + extMgr.useAndRequire("KHR_texture_basisu"); + } + + int texIdx = osg::utils::TextureUtils::addImageToModel( + model, buffer, result.data, result.mimeType, useBasisu); + + // 设置到材质构建器 + switch (type) { + case TextureType::BASE_COLOR: + matBuilder.setBaseColorTexture(texIdx); + if (transform.hasTransform) { + matBuilder.setBaseColorTextureTransform( + convertToGLTFTransform(transform)); + } + if (result.hasAlpha) { + matBuilder.setAlphaMode("BLEND"); + } + break; + case TextureType::NORMAL: + matBuilder.setNormalTexture(texIdx); + if (transform.hasTransform) { + matBuilder.setNormalTextureTransform( + convertToGLTFTransform(transform)); + } + break; + case TextureType::EMISSIVE: + matBuilder.setEmissiveTexture(texIdx); + if (transform.hasTransform) { + matBuilder.setEmissiveTextureTransform( + convertToGLTFTransform(transform)); + } + break; + } +} +``` + +--- + +## 步骤5:扩展MaterialBuilder + +### 5.1 修改文件 +**文件路径:** `src/gltf/material_builder.h` + +### 5.2 添加头文件包含 +```cpp +#include "extensions/texture_transform.h" +#include "extensions/specular_glossiness.h" +#include +``` + +### 5.3 添加新方法声明 +```cpp +class MaterialBuilder { +public: + // ... 现有方法 ... + + // 纹理变换设置 + void setBaseColorTextureTransform(const gltf::extensions::TextureTransform& transform); + void setNormalTextureTransform(const gltf::extensions::TextureTransform& transform); + void setEmissiveTextureTransform(const gltf::extensions::TextureTransform& transform); + void setMetallicRoughnessTextureTransform(const gltf::extensions::TextureTransform& transform); + void setOcclusionTextureTransform(const gltf::extensions::TextureTransform& transform); + + // Specular-Glossiness设置 + void setSpecularGlossiness(const gltf::extensions::SpecularGlossiness& sg); + +private: + // ... 现有成员 ... + std::optional baseColorTransform_; + std::optional normalTransform_; + std::optional emissiveTransform_; + std::optional metallicRoughnessTransform_; + std::optional occlusionTransform_; + std::optional specularGlossiness_; +}; +``` + +### 5.4 实现新方法 +**文件路径:** `src/gltf/material_builder.cpp` + +```cpp +void MaterialBuilder::setBaseColorTextureTransform( + const gltf::extensions::TextureTransform& transform) { + baseColorTransform_ = transform; +} + +void MaterialBuilder::setNormalTextureTransform( + const gltf::extensions::TextureTransform& transform) { + normalTransform_ = transform; +} + +void MaterialBuilder::setEmissiveTextureTransform( + const gltf::extensions::TextureTransform& transform) { + emissiveTransform_ = transform; +} + +void MaterialBuilder::setSpecularGlossiness( + const gltf::extensions::SpecularGlossiness& sg) { + specularGlossiness_ = sg; +} +``` + +### 5.5 修改build()方法 +```cpp +int MaterialBuilder::build(tinygltf::Model& model, ExtensionManager& extMgr) { + tinygltf::Material material; + material.name = "Material"; + material.doubleSided = doubleSided_; + material.alphaMode = alphaMode_; + + // 设置PBR参数 + material.pbrMetallicRoughness.baseColorFactor = baseColor_; + material.pbrMetallicRoughness.roughnessFactor = roughnessFactor_; + material.pbrMetallicRoughness.metallicFactor = metallicFactor_; + + // 设置基础颜色纹理 + if (baseColorTexture_ >= 0) { + material.pbrMetallicRoughness.baseColorTexture.index = baseColorTexture_; + + // 应用纹理变换 + if (baseColorTransform_) { + gltf::extensions::applyTextureTransform( + material, "baseColorTexture", *baseColorTransform_, extMgr); + } + } + + // 设置法线纹理 + if (normalTexture_ >= 0) { + material.normalTexture.index = normalTexture_; + + if (normalTransform_) { + gltf::extensions::applyTextureTransform( + material, "normalTexture", *normalTransform_, extMgr); + } + } + + // 设置自发光 + if (emissiveTexture_ >= 0) { + material.emissiveTexture.index = emissiveTexture_; + + if (emissiveTransform_) { + gltf::extensions::applyTextureTransform( + material, "emissiveTexture", *emissiveTransform_, extMgr); + } + } + + if (emissiveColor_[0] > 0.0 || emissiveColor_[1] > 0.0 || emissiveColor_[2] > 0.0) { + material.emissiveFactor = emissiveColor_; + } + + // 设置Unlit扩展 + if (unlit_) { + material.extensions["KHR_materials_unlit"] = tinygltf::Value(tinygltf::Value::Object()); + extMgr.use("KHR_materials_unlit"); + } + + // 应用Specular-Glossiness扩展 + if (specularGlossiness_) { + gltf::extensions::applySpecularGlossiness( + material, *specularGlossiness_, extMgr); + } + + int index = static_cast(model.materials.size()); + model.materials.push_back(material); + return index; +} +``` + +--- + +## 步骤6:修改generate函数 + +### 6.1 收集材质信息 +**文件路径:** `src/b3dm/b3dm_generator.cpp` + +```cpp +std::string B3DMGenerator::generate( + const spatial::core::SpatialItemRefList& items, + const LODLevelSettings& lodSettings) { + + if (items.empty()) { + return std::string(); + } + + // 提取并合并几何体 + osg::ref_ptr mergedGeom = extractAndMergeGeometries(items); + if (!mergedGeom.valid()) { + LOG_E("Failed to extract and merge geometries"); + return std::string(); + } + + // 应用简化 + if (lodSettings.enable_simplification) { + applySimplification(mergedGeom.get(), lodSettings.simplify); + } + + // ===== 新增:收集材质信息 ===== + std::vector> materials; + for (const auto& item : items) { + auto matInfo = config_.geometryExtractor->getMaterial(item.get()); + materials.push_back(matInfo); + } + + // 构建GLTF模型(传入材质信息) + std::vector glbData; + buildGLTFModel( + mergedGeom.get(), + items, + lodSettings.enable_draco, + lodSettings.draco, + glbData, + materials // 新增参数 + ); + + // ... 其余代码 ... +} +``` + +--- + +## 验证清单 + +### 编译验证 +- [ ] 所有修改的文件编译通过 +- [ ] 无警告(或警告在可接受范围内) +- [ ] 链接成功 + +### 功能验证 +- [ ] 基础颜色正确显示 +- [ ] 金属度/粗糙度参数生效 +- [ ] 自发光效果正确 +- [ ] 基础颜色纹理加载 +- [ ] 法线贴图效果 +- [ ] 自发光纹理 +- [ ] KTX2纹理压缩(如果启用) +- [ ] Alpha透明度检测 +- [ ] 纹理变换(偏移、缩放、旋转) +- [ ] Specular-Glossiness材质 + +### 回归测试 +- [ ] OSGB流程不受影响 +- [ ] Shapefile流程不受影响 +- [ ] 无材质模型正常显示 + +--- + +## 常见问题排查 + +### 问题1:纹理不显示 +**排查步骤:** +1. 检查`getMaterial`是否正确返回材质信息 +2. 检查纹理对象是否有效 +3. 检查`processTexture`是否成功 +4. 检查GLTF中是否正确引用纹理索引 + +### 问题2:材质颜色不正确 +**排查步骤:** +1. 检查`extractPBRParams`提取的参数 +2. 检查`MaterialBuilder`是否正确设置参数 +3. 检查GLTF中baseColorFactor的值 + +### 问题3:纹理变换不生效 +**排查步骤:** +1. 检查FBX中是否有纹理变换数据 +2. 检查`TextureTransformInfo`是否正确复制 +3. 检查`applyTextureTransform`是否被调用 +4. 检查GLTF扩展是否正确写入 + +--- + +## 性能优化建议 + +1. **材质缓存**:相同材质只创建一次,使用哈希表缓存 +2. **纹理缓存**:相同纹理只处理一次,避免重复编码 +3. **延迟加载**:大纹理可以考虑延迟加载或流式加载 diff --git a/docs/fbx_material_interface_design.md b/docs/fbx_material_interface_design.md new file mode 100644 index 00000000..758f7c5b --- /dev/null +++ b/docs/fbx_material_interface_design.md @@ -0,0 +1,827 @@ +# FBX材质移植接口设计文档 + +本文档详细描述FBX材质移植过程中涉及的接口设计,包括数据结构、类接口和模块间的交互关系。 + +## 1. 数据结构设计 + +### 1.1 TextureTransformInfo + +```cpp +namespace common { + +/** + * @brief 纹理变换信息 + * + * 对应GLTF KHR_texture_transform扩展的参数 + * 用于描述纹理坐标的变换(偏移、缩放、旋转) + */ +struct TextureTransformInfo { + float offset[2] = {0.0f, 0.0f}; // UV偏移 [u, v] + float scale[2] = {1.0f, 1.0f}; // UV缩放 [u, v] + float rotation = 0.0f; // 旋转角度(弧度) + int texCoord = 0; // 纹理坐标集索引 + bool hasTransform = false; // 标记是否有变换 + + /** + * @brief 创建默认变换(无变换) + */ + static TextureTransformInfo Identity() { + return {}; + } + + /** + * @brief 创建带偏移的变换 + */ + static TextureTransformInfo WithOffset(float u, float v) { + TextureTransformInfo t; + t.offset[0] = u; + t.offset[1] = v; + t.hasTransform = true; + return t; + } + + /** + * @brief 创建带缩放的变换 + */ + static TextureTransformInfo WithScale(float u, float v) { + TextureTransformInfo t; + t.scale[0] = u; + t.scale[1] = v; + t.hasTransform = true; + return t; + } +}; + +} // namespace common +``` + +### 1.2 MaterialInfo + +```cpp +namespace common { + +/** + * @brief 完整的材质信息 + * + * 包含PBR材质的所有参数,支持: + * - Metallic-Roughness工作流 + * - Specular-Glossiness工作流(传统FBX材质) + * - 纹理变换 + * - 各种GLTF材质扩展 + */ +struct MaterialInfo { + // ==================== 基础PBR参数 ==================== + + /** + * @brief 基础颜色 + * + * 线性颜色空间,RGBA格式 + * 默认: [1.0, 1.0, 1.0, 1.0](白色不透明) + */ + std::vector baseColor = {1.0, 1.0, 1.0, 1.0}; + + /** + * @brief 粗糙度 + * + * 范围: [0.0, 1.0] + * 0.0 = 完全光滑(镜面反射) + * 1.0 = 完全粗糙(漫反射) + * 默认: 1.0 + */ + float roughnessFactor = 1.0f; + + /** + * @brief 金属度 + * + * 范围: [0.0, 1.0] + * 0.0 = 非金属(介电质) + * 1.0 = 金属 + * 默认: 0.0 + */ + float metallicFactor = 0.0f; + + /** + * @brief 自发光颜色 + * + * 线性颜色空间,RGB格式 + * 默认: [0.0, 0.0, 0.0](无自发光) + */ + std::vector emissiveColor = {0.0, 0.0, 0.0}; + + /** + * @brief 遮挡强度 + * + * 范围: [0.0, 1.0] + * 仅当使用遮挡纹理时有效 + * 默认: 1.0 + */ + float aoStrength = 1.0f; + + // ==================== 纹理对象 ==================== + + /** + * @brief 基础颜色纹理(纹理单元0) + * + * 与baseColor相乘 + * 支持RGB或RGBA格式 + */ + osg::ref_ptr baseColorTexture; + + /** + * @brief 法线纹理(纹理单元1) + * + * 切线空间法线贴图 + * 通常使用RGB格式存储XYZ + */ + osg::ref_ptr normalTexture; + + /** + * @brief 金属度/粗糙度纹理(纹理单元2) + * + * 使用GB通道: + * - G通道: 粗糙度 + * - B通道: 金属度 + */ + osg::ref_ptr metallicRoughnessTexture; + + /** + * @brief 遮挡纹理(纹理单元3) + * + * 通常使用R通道存储AO值 + */ + osg::ref_ptr occlusionTexture; + + /** + * @brief 自发光纹理(纹理单元4) + * + * 与emissiveColor相乘 + */ + osg::ref_ptr emissiveTexture; + + // ==================== 纹理变换 ==================== + + TextureTransformInfo baseColorTransform; // 基础颜色纹理变换 + TextureTransformInfo normalTransform; // 法线纹理变换 + TextureTransformInfo metallicRoughnessTransform; // 金属度/粗糙度纹理变换 + TextureTransformInfo occlusionTransform; // 遮挡纹理变换 + TextureTransformInfo emissiveTransform; // 自发光纹理变换 + + // ==================== Specular-Glossiness ==================== + + /** + * @brief 是否使用Specular-Glossiness工作流 + * + * 用于支持传统FBX材质 + * 启用时使用KHR_materials_pbrSpecularGlossiness扩展 + */ + bool useSpecularGlossiness = false; + + /** + * @brief 漫反射因子(Specular-Glossiness) + * + * 对应Metallic-Roughness的baseColor + * 默认: [1.0, 1.0, 1.0, 1.0] + */ + std::vector diffuseFactor = {1.0, 1.0, 1.0, 1.0}; + + /** + * @brief 高光因子(Specular-Glossiness) + * + * 范围: [0.0, 1.0] per channel + * 默认: [1.0, 1.0, 1.0] + */ + std::vector specularFactor = {1.0, 1.0, 1.0}; + + /** + * @brief 光泽度(Specular-Glossiness) + * + * 范围: [0.0, 1.0] + * 0.0 = 粗糙 + * 1.0 = 光滑 + * 默认: 1.0 + */ + double glossinessFactor = 1.0; + + /** + * @brief Specular-Glossiness纹理 + * + * RGB通道存储高光颜色,A通道存储光泽度 + */ + osg::ref_ptr specularGlossinessTexture; + + /** + * @brief 漫反射纹理(Specular-Glossiness) + */ + osg::ref_ptr diffuseTexture; + + // ==================== 其他属性 ==================== + + /** + * @brief 双面渲染 + * + * true = 渲染正面和背面 + * false = 只渲染正面(背面剔除) + * 默认: true + */ + bool doubleSided = true; + + /** + * @brief Alpha模式 + * + * - "OPAQUE": 不透明(忽略Alpha) + * - "MASK": Alpha裁剪(使用alphaCutoff) + * - "BLEND": Alpha混合(半透明) + * 默认: "OPAQUE" + */ + std::string alphaMode = "OPAQUE"; + + /** + * @brief Alpha裁剪值 + * + * 仅当alphaMode为"MASK"时有效 + * 范围: [0.0, 1.0] + * 默认: 0.5 + */ + float alphaCutoff = 0.5f; + + // ==================== 辅助方法 ==================== + + /** + * @brief 检查是否有任何纹理 + */ + bool hasAnyTexture() const { + return baseColorTexture || normalTexture || metallicRoughnessTexture || + occlusionTexture || emissiveTexture || specularGlossinessTexture || + diffuseTexture; + } + + /** + * @brief 检查是否有纹理变换 + */ + bool hasAnyTextureTransform() const { + return baseColorTransform.hasTransform || normalTransform.hasTransform || + metallicRoughnessTransform.hasTransform || occlusionTransform.hasTransform || + emissiveTransform.hasTransform; + } +}; + +} // namespace common +``` + +## 2. 接口设计 + +### 2.1 IGeometryExtractor(扩展) + +```cpp +namespace common { + +/** + * @brief 几何体提取器接口 + * + * 抽象不同数据源(FBX/Shapefile/OSGB)的几何体和材质提取逻辑 + */ +class IGeometryExtractor { +public: + virtual ~IGeometryExtractor() = default; + + /** + * @brief 从空间对象提取几何体 + * + * @param item 空间对象 + * @return 几何体列表(可能包含多个子几何体) + */ + virtual std::vector> extract( + const spatial::core::SpatialItem* item) = 0; + + /** + * @brief 获取对象的唯一标识 + * + * 用于生成BatchID,在3D Tiles中标识单个要素 + * + * @param item 空间对象 + * @return 唯一标识字符串 + */ + virtual std::string getId(const spatial::core::SpatialItem* item) = 0; + + /** + * @brief 获取对象的属性 + * + * 用于生成BatchTable,存储要素的属性数据 + * + * @param item 空间对象 + * @return 属性键值对 + */ + virtual std::map getAttributes( + const spatial::core::SpatialItem* item) = 0; + + /** + * @brief 获取对象的材质信息(新增) + * + * 提取空间对象的完整材质信息,包括: + * - PBR参数 + * - 纹理对象 + * - 纹理变换 + * - 扩展数据 + * + * @param item 空间对象 + * @return 材质信息,如果没有材质返回nullptr或默认材质 + */ + virtual std::shared_ptr getMaterial( + const spatial::core::SpatialItem* item) = 0; +}; + +} // namespace common +``` + +### 2.2 FBXGeometryExtractor + +```cpp +namespace fbx { + +/** + * @brief FBX几何体提取器 + * + * 从FBXSpatialItemAdapter提取几何体和材质信息 + */ +class FBXGeometryExtractor : public common::IGeometryExtractor { +public: + FBXGeometryExtractor() = default; + ~FBXGeometryExtractor() override = default; + + // 禁用拷贝 + FBXGeometryExtractor(const FBXGeometryExtractor&) = delete; + FBXGeometryExtractor& operator=(const FBXGeometryExtractor&) = delete; + + /** + * @brief 提取几何体 + * + * 从FBX空间对象提取OSG几何体,应用坐标变换(Y-up到Z-up) + */ + std::vector> extract( + const spatial::core::SpatialItem* item) override; + + /** + * @brief 获取节点名称作为ID + */ + std::string getId(const spatial::core::SpatialItem* item) override; + + /** + * @brief 获取节点属性 + */ + std::map getAttributes( + const spatial::core::SpatialItem* item) override; + + /** + * @brief 获取材质信息(新增) + * + * 提取流程: + * 1. 获取几何体的StateSet + * 2. 使用MaterialUtils提取PBR参数 + * 3. 提取纹理对象(从各个纹理单元) + * 4. 从FBX扩展数据提取纹理变换和Specular-Glossiness参数 + * + * @param item FBX空间对象 + * @return 完整的材质信息 + */ + std::shared_ptr getMaterial( + const spatial::core::SpatialItem* item) override; + +private: + /** + * @brief 复制纹理变换数据 + */ + void copyTextureTransform(const ::TextureTransformData& src, + common::TextureTransformInfo& dst); +}; + +} // namespace fbx +``` + +### 2.3 MaterialBuilder(扩展) + +```cpp +namespace gltf { + +/** + * @brief GLTF材质构建器 + * + * 简化GLTF Material的创建,支持各种扩展 + */ +class MaterialBuilder { +public: + MaterialBuilder(); + ~MaterialBuilder() = default; + + // ==================== 基础参数设置 ==================== + + void setBaseColor(const std::vector& color); + void setPBRParams(float roughness, float metallic); + void setEmissiveColor(const std::vector& color); + void setDoubleSided(bool doubleSided); + void setAlphaMode(const std::string& alphaMode); + void setAlphaCutoff(float cutoff); + void setUnlit(bool unlit); + + // ==================== 纹理设置 ==================== + + void setBaseColorTexture(int textureIndex); + void setNormalTexture(int textureIndex); + void setEmissiveTexture(int textureIndex); + void setMetallicRoughnessTexture(int textureIndex); + void setOcclusionTexture(int textureIndex); + + // ==================== 纹理变换(新增) ==================== + + /** + * @brief 设置基础颜色纹理变换 + * @param transform 纹理变换参数 + */ + void setBaseColorTextureTransform(const extensions::TextureTransform& transform); + + /** + * @brief 设置法线纹理变换 + * @param transform 纹理变换参数 + */ + void setNormalTextureTransform(const extensions::TextureTransform& transform); + + /** + * @brief 设置自发光纹理变换 + * @param transform 纹理变换参数 + */ + void setEmissiveTextureTransform(const extensions::TextureTransform& transform); + + /** + * @brief 设置金属度/粗糙度纹理变换 + * @param transform 纹理变换参数 + */ + void setMetallicRoughnessTextureTransform(const extensions::TextureTransform& transform); + + /** + * @brief 设置遮挡纹理变换 + * @param transform 纹理变换参数 + */ + void setOcclusionTextureTransform(const extensions::TextureTransform& transform); + + // ==================== Specular-Glossiness(新增) ==================== + + /** + * @brief 设置Specular-Glossiness参数 + * @param sg Specular-Glossiness数据 + */ + void setSpecularGlossiness(const extensions::SpecularGlossiness& sg); + + // ==================== 构建 ==================== + + /** + * @brief 构建GLTF材质 + * + * 构建流程: + * 1. 创建tinygltf::Material + * 2. 设置PBR参数 + * 3. 设置纹理引用 + * 4. 应用纹理变换扩展(如果有) + * 5. 应用Specular-Glossiness扩展(如果有) + * 6. 应用Unlit扩展(如果有) + * 7. 添加到模型并返回索引 + * + * @param model GLTF模型 + * @param extMgr 扩展管理器(记录使用的扩展) + * @return 材质索引 + */ + int build(tinygltf::Model& model, ExtensionManager& extMgr); + + /** + * @brief 清空所有设置 + */ + void clear(); + +private: + // 基础参数 + std::vector baseColor_; + float roughnessFactor_; + float metallicFactor_; + std::vector emissiveColor_; + bool doubleSided_; + std::string alphaMode_; + float alphaCutoff_; + bool unlit_; + + // 纹理索引 + int baseColorTexture_; + int normalTexture_; + int emissiveTexture_; + int metallicRoughnessTexture_; + int occlusionTexture_; + + // 纹理变换(新增) + std::optional baseColorTransform_; + std::optional normalTransform_; + std::optional emissiveTransform_; + std::optional metallicRoughnessTransform_; + std::optional occlusionTransform_; + + // Specular-Glossiness(新增) + std::optional specularGlossiness_; +}; + +} // namespace gltf +``` + +### 2.4 B3DMGenerator(扩展) + +```cpp +namespace b3dm { + +/** + * @brief B3DM内容生成器 + * + * 生成支持材质的B3DM数据 + */ +class B3DMGenerator { +public: + explicit B3DMGenerator(const B3DMGeneratorConfig& config); + ~B3DMGenerator() = default; + + /** + * @brief 生成单LOD级别的B3DM + */ + std::string generate( + const spatial::core::SpatialItemRefList& items, + const LODLevelSettings& lodSettings); + + /** + * @brief 生成多LOD级别的B3DM文件 + */ + std::vector generateLODFiles( + const spatial::core::SpatialItemRefList& items, + const std::string& outputDir, + const std::string& baseFilename, + const std::vector& lodLevels); + +private: + // 纹理类型枚举(新增) + enum class TextureType { + BASE_COLOR, + NORMAL, + EMISSIVE, + METALLIC_ROUGHNESS, + OCCLUSION + }; + + /** + * @brief 提取并合并几何体(现有) + */ + osg::ref_ptr extractAndMergeGeometries( + const spatial::core::SpatialItemRefList& items); + + /** + * @brief 应用几何简化(现有) + */ + void applySimplification(osg::Geometry* geometry, + const SimplificationParams& params); + + /** + * @brief 构建Batch数据(现有) + */ + BatchData buildBatchData(const spatial::core::SpatialItemRefList& items); + + /** + * @brief 构建GLTF模型(扩展) + * + * @param mergedGeom 合并后的几何体 + * @param items 空间对象列表 + * @param enableDraco 是否启用Draco压缩 + * @param dracoParams Draco参数 + * @param glbData 输出的GLB数据 + * @param materials 材质信息列表(新增) + */ + void buildGLTFModel( + osg::Geometry* mergedGeom, + const spatial::core::SpatialItemRefList& items, + bool enableDraco, + const DracoCompressionParams& dracoParams, + std::vector& glbData, + const std::vector>& materials); + + /** + * @brief 构建材质(新增) + * + * 将MaterialInfo转换为GLTF材质 + * + * @param matInfo 材质信息 + * @param model GLTF模型 + * @param buffer GLTF缓冲区 + * @return 材质索引,失败返回-1 + */ + int buildMaterial( + const std::shared_ptr& matInfo, + tinygltf::Model& model, + tinygltf::Buffer& buffer); + + /** + * @brief 处理并添加纹理(新增) + * + * 统一的纹理处理流程: + * 1. 处理纹理(可能包括KTX2压缩) + * 2. 添加到GLTF模型 + * 3. 设置到材质构建器 + * 4. 应用纹理变换(如果有) + * + * @param texture OSG纹理对象 + * @param transform 纹理变换 + * @param matBuilder 材质构建器 + * @param model GLTF模型 + * @param buffer GLTF缓冲区 + * @param extMgr 扩展管理器 + * @param type 纹理类型 + */ + void processAndAddTexture( + osg::Texture* texture, + const common::TextureTransformInfo& transform, + gltf::MaterialBuilder& matBuilder, + tinygltf::Model& model, + tinygltf::Buffer& buffer, + gltf::ExtensionManager& extMgr, + TextureType type); + + /** + * @brief 计算材质哈希键(新增) + * + * 用于材质去重,相同材质只创建一次 + */ + std::string computeMaterialKey( + const std::shared_ptr& matInfo); + + /** + * @brief 转换纹理变换格式(新增) + */ + gltf::extensions::TextureTransform convertToGLTFTransform( + const common::TextureTransformInfo& info); + + B3DMGeneratorConfig config_; +}; + +} // namespace b3dm +``` + +## 3. 模块交互关系 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ B3DMGenerator │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ generate() │ │ +│ │ 1. 提取几何体 (extractAndMergeGeometries) │ │ +│ │ 2. 收集材质信息 (getMaterial) │ │ +│ │ 3. 构建GLTF (buildGLTFModel) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ IGeometryExtractor │ +│ (FBXGeometryExtractor实现) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ getMaterial() │ │ +│ │ 1. 获取StateSet │ │ +│ │ 2. 提取PBR参数 (MaterialUtils) │ │ +│ │ 3. 提取纹理 │ │ +│ │ 4. 提取扩展数据 (TextureTransform, SpecularGlossiness) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ MaterialBuilder │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ build() │ │ +│ │ 1. 设置PBR参数 │ │ +│ │ 2. 设置纹理 │ │ +│ │ 3. 应用纹理变换扩展 │ │ +│ │ 4. 应用SpecularGlossiness扩展 │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ TextureUtils │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ processTexture() │ │ +│ │ 1. 检查Alpha通道 │ │ +│ │ 2. KTX2压缩(可选) │ │ +│ │ 3. 编码为PNG/JPEG │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## 4. 数据流向 + +### 4.1 材质提取流程 + +``` +FBX文件 + │ + ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ ufbx_material │──▶│ osg::StateSet │──▶│ MaterialInfo │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ + │ │ │ + ▼ ▼ ▼ + PBR参数 Uniforms 完整材质信息 + (base_color) (roughnessFactor) + 纹理对象 + (diffuse_color) (metallicFactor) + 纹理变换 + (specular_color) (textures) + SpecularGlossiness +``` + +### 4.2 材质构建流程 + +``` +MaterialInfo + │ + ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ processTexture │──▶│ addImageToModel │──▶│ textureIndex │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ MaterialBuilder │──▶│ build() │──▶│ tinygltf::Material │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ + │ ▼ + │ GLTF JSON + 二进制纹理 + │ + ▼ +设置参数: +- baseColor +- roughnessFactor +- metallicFactor +- 纹理索引 +- 纹理变换扩展 +- SpecularGlossiness扩展 +``` + +## 5. 扩展点设计 + +### 5.1 支持新的纹理类型 + +如需支持新的纹理类型(如Clearcoat),需要: + +1. 在`MaterialInfo`中添加纹理对象和变换 +2. 在`MaterialBuilder`中添加设置方法 +3. 在`B3DMGenerator::processAndAddTexture`中添加处理逻辑 + +### 5.2 支持新的材质扩展 + +如需支持新的GLTF材质扩展,需要: + +1. 在`gltf/extensions/`下创建新的扩展头文件 +2. 实现`applyXXX`函数 +3. 在`MaterialBuilder::build`中调用 + +### 5.3 支持新的数据源 + +如需支持新的数据源(如glTF直接输入),需要: + +1. 实现`IGeometryExtractor`接口 +2. 实现`getMaterial`方法返回`MaterialInfo` +3. 其余流程自动复用 + +## 6. 错误处理策略 + +### 6.1 材质提取失败 + +```cpp +std::shared_ptr getMaterial(...) { + if (!stateSet) { + // 返回默认材质,不中断流程 + return std::make_shared(); + } + // ... +} +``` + +### 6.2 纹理处理失败 + +```cpp +void processAndAddTexture(...) { + auto result = osg::utils::TextureUtils::processTexture(...); + if (!result.success) { + LOG_W("Failed to process texture, using material without texture"); + return; // 不使用纹理,继续流程 + } + // ... +} +``` + +### 6.3 材质构建失败 + +```cpp +int buildMaterial(...) { + if (!matInfo) { + return -1; // 返回-1表示使用默认材质 + } + // ... +} +``` diff --git a/docs/fbx_material_migration.md b/docs/fbx_material_migration.md new file mode 100644 index 00000000..f81b5e58 --- /dev/null +++ b/docs/fbx_material_migration.md @@ -0,0 +1,281 @@ +# FBX材质功能移植方案 + +## 概述 + +本文档详细描述如何将`master`分支中的FBX材质处理功能移植到`refactor-master`分支。 + +## 现状分析 + +### master分支支持的材质功能 + +1. **基础PBR材质** + - 基础颜色(BaseColor) + - 金属度(Metallic) + - 粗糙度(Roughness) + - 自发光(Emissive) + +2. **纹理支持** + - 基础颜色纹理 + - 法线贴图(Normal Map) + - 自发光纹理 + - 金属度/粗糙度纹理 + - 遮挡纹理(AO) + +3. **高级特性** + - KTX2/Basis Universal纹理压缩 + - Alpha透明度检测 + - Unlit材质扩展 + - 双面渲染 + +### refactor-master分支现状 + +**已有的基础设施:** +- ✅ `gltf::MaterialBuilder` - GLTF材质构建器 +- ✅ `gltf::GLTFBuilder` - GLTF模型构建器 +- ✅ `osg::utils::MaterialUtils` - OSG材质提取工具 +- ✅ `osg::utils::TextureUtils` - 纹理处理工具 +- ✅ `gltf::extensions::TextureTransform` - 纹理变换扩展 +- ✅ `gltf::extensions::SpecularGlossiness` - 高光-光泽度扩展 + +**缺失的部分:** +- ❌ `B3DMGenerator`没有调用材质构建逻辑 +- ❌ `FBXGeometryExtractor`没有传递材质信息 +- ❌ 材质扩展数据没有传递到GLTF构建器 + +## 移植方案 + +### 阶段1:扩展IGeometryExtractor接口 + +**文件:** `src/common/geometry_extractor.h` + +```cpp +// 新增:纹理变换信息 +struct TextureTransformInfo { + float offset[2] = {0.0f, 0.0f}; + float scale[2] = {1.0f, 1.0f}; + float rotation = 0.0f; + bool hasTransform = false; +}; + +// 新增:材质信息结构 +struct MaterialInfo { + // 基础PBR参数 + std::vector baseColor = {1.0, 1.0, 1.0, 1.0}; + float roughnessFactor = 1.0f; + float metallicFactor = 0.0f; + std::vector emissiveColor = {0.0, 0.0, 0.0}; + + // OSG纹理对象 + osg::ref_ptr baseColorTexture; + osg::ref_ptr normalTexture; + osg::ref_ptr emissiveTexture; + osg::ref_ptr metallicRoughnessTexture; + osg::ref_ptr occlusionTexture; + + // 纹理变换 + TextureTransformInfo baseColorTransform; + TextureTransformInfo normalTransform; + TextureTransformInfo emissiveTransform; + TextureTransformInfo metallicRoughnessTransform; + TextureTransformInfo occlusionTransform; + + // Specular-Glossiness支持 + bool useSpecularGlossiness = false; + std::vector specularFactor = {1.0, 1.0, 1.0}; + double glossinessFactor = 1.0; + osg::ref_ptr specularGlossinessTexture; + + // 其他属性 + bool doubleSided = true; + std::string alphaMode = "OPAQUE"; +}; + +class IGeometryExtractor { +public: + virtual ~IGeometryExtractor() = default; + + // 现有方法... + virtual std::vector> extract( + const spatial::core::SpatialItem* item) = 0; + virtual std::string getId(const spatial::core::SpatialItem* item) = 0; + virtual std::map getAttributes( + const spatial::core::SpatialItem* item) = 0; + + // 新增:获取材质信息 + virtual std::shared_ptr getMaterial( + const spatial::core::SpatialItem* item) = 0; +}; +``` + +### 阶段2:FBXGeometryExtractor实现材质提取 + +**文件:** `src/fbx/fbx_geometry_extractor.cpp` + +实现步骤: + +1. **从StateSet提取基础PBR参数** + ```cpp + osg::utils::PBRParams pbrParams; + osg::utils::MaterialUtils::extractPBRParams(stateSet, pbrParams); + materialInfo->baseColor = pbrParams.baseColor; + materialInfo->roughnessFactor = pbrParams.roughnessFactor; + materialInfo->metallicFactor = pbrParams.metallicFactor; + ``` + +2. **提取纹理对象** + ```cpp + materialInfo->baseColorTexture = + const_cast(osg::utils::MaterialUtils::getBaseColorTexture(stateSet)); + materialInfo->normalTexture = + const_cast(osg::utils::MaterialUtils::getNormalTexture(stateSet)); + materialInfo->emissiveTexture = + const_cast(osg::utils::MaterialUtils::getEmissiveTexture(stateSet)); + ``` + +3. **从FBX扩展数据提取高级特性** + - 需要从`FBXLoader`获取`MaterialExtensionData` + - 复制纹理变换参数 + - 复制Specular-Glossiness参数 + +### 阶段3:重构B3DMGenerator支持材质 + +**文件:** `src/b3dm/b3dm_generator.cpp` + +核心修改点: + +1. **修改`buildGLTFModel`函数** + - 收集所有items的材质信息 + - 为每个唯一材质创建GLTF材质 + - 在创建primitive时关联材质索引 + +2. **新增`buildMaterial`辅助函数** + ```cpp + int B3DMGenerator::buildMaterial( + const std::shared_ptr& matInfo, + tinygltf::Model& model, + tinygltf::Buffer& buffer); + ``` + +3. **纹理处理流程** + ```cpp + // 1. 处理纹理 + auto result = osg::utils::TextureUtils::processTexture( + matInfo->baseColorTexture.get(), config_.enableTextureCompress); + + // 2. 添加到模型 + int texIdx = osg::utils::TextureUtils::addImageToModel( + model, buffer, result.data, result.mimeType, useBasisu); + + // 3. 设置到材质 + matBuilder.setBaseColorTexture(texIdx); + ``` + +### 阶段4:扩展MaterialBuilder + +**文件:** `src/gltf/material_builder.h` 和 `src/gltf/material_builder.cpp` + +新增功能: + +1. **纹理变换支持** + ```cpp + void setBaseColorTextureTransform(const TextureTransformInfo& transform); + void setNormalTextureTransform(const TextureTransformInfo& transform); + void setEmissiveTextureTransform(const TextureTransformInfo& transform); + ``` + +2. **Specular-Glossiness支持** + ```cpp + void setSpecularGlossiness(const SpecularGlossinessInfo& sg); + ``` + +3. **在build()中应用扩展** + ```cpp + // 应用纹理变换 + if (baseColorTransform_.hasTransform) { + gltf::extensions::applyTextureTransform( + material, "baseColorTexture", + convertToGLTFTransform(*baseColorTransform_), extMgr); + } + + // 应用Specular-Glossiness + if (useSpecularGlossiness_) { + gltf::extensions::applySpecularGlossiness( + material, specularGlossiness_, extMgr); + } + ``` + +## 关键代码复用 + +refactor-master已存在的可复用代码: + +| 组件 | 文件路径 | 复用方式 | +|------|----------|----------| +| 纹理处理 | `osg::utils::TextureUtils::processTexture` | 完全复用 | +| 材质构建 | `gltf::MaterialBuilder` | 扩展纹理变换支持 | +| 扩展应用 | `gltf::extensions::applyTextureTransform` | 完全复用 | +| 图像添加 | `osg::utils::TextureUtils::addImageToModel` | 完全复用 | +| PBR参数提取 | `osg::utils::MaterialUtils::extractPBRParams` | 完全复用 | + +## 实施优先级 + +| 优先级 | 任务 | 工作量 | 依赖 | +|--------|------|--------|------| +| P0 | 扩展`IGeometryExtractor`接口 | 小 | 无 | +| P0 | 实现`FBXGeometryExtractor::getMaterial` | 中 | P0 | +| P0 | `B3DMGenerator`材质集成 | 大 | P0, P1 | +| P1 | `MaterialBuilder`纹理变换扩展 | 中 | 无 | +| P1 | `MaterialBuilder`Specular-Glossiness | 中 | 无 | +| P2 | 多材质分组优化 | 大 | P0 | + +## 测试验证点 + +1. **基础材质** + - [ ] 基础颜色正确显示 + - [ ] 金属度/粗糙度参数正确 + - [ ] 自发光效果正确 + +2. **纹理支持** + - [ ] 基础颜色纹理加载 + - [ ] 法线贴图效果 + - [ ] 自发光纹理 + +3. **高级特性** + - [ ] KTX2纹理压缩 + - [ ] Alpha透明度 + - [ ] 纹理变换(偏移、缩放、旋转) + - [ ] Specular-Glossiness材质 + +4. **兼容性** + - [ ] 与现有OSGB流程不冲突 + - [ ] 与Shapefile流程不冲突 + +## 风险与注意事项 + +1. **性能考虑** + - 纹理处理是CPU密集型操作,需要考虑缓存机制 + - 相同纹理应该只处理一次 + +2. **内存管理** + - OSG纹理对象的引用计数需要正确处理 + - 大纹理可能导致内存峰值 + +3. **错误处理** + - 纹理加载失败需要优雅降级 + - 不支持的纹理格式需要处理 + +## 附录:参考代码 + +### master分支材质处理关键代码位置 + +- `src/FBXPipeline.cpp:1144-1780` - 完整材质转换逻辑 +- `src/FBXPipeline.cpp:408` - 材质分组逻辑 +- `src/FBXPipeline.cpp:424` - StateSet到材质映射 + +### refactor-master相关代码位置 + +- `src/gltf/material_builder.h` - 材质构建器接口 +- `src/gltf/material_builder.cpp` - 材质构建实现 +- `src/osg/utils/material_utils.h` - 材质工具类 +- `src/osg/utils/texture_utils.h` - 纹理工具类 +- `src/gltf/extensions/texture_transform.h` - 纹理变换扩展 +- `src/gltf/extensions/specular_glossiness.h` - 高光-光泽度扩展 diff --git a/docs/fbxpipeline_migration.md b/docs/fbxpipeline_migration.md new file mode 100644 index 00000000..e6525c29 --- /dev/null +++ b/docs/fbxpipeline_migration.md @@ -0,0 +1,1327 @@ +# FBXPipeline 迁移方案 + +## 1. 概述 + +### 1.1 目标 +将现有的FBXPipeline从自定义八叉树实现迁移到统一的空间切片抽象框架,实现: +1. 复用 `spatial::strategy::OctreeStrategy` 进行空间索引 +2. 复用 `common::TilesetBuilder` 进行Tileset构建 +3. 复用 `b3dm::B3DMGenerator` 进行B3DM生成 +4. 采用 `tile/{z}/{x}/{y}/` 统一目录结构 +5. 每个阶段都能产生可运行的3D Tiles输出 + +### 1.2 核心原则 + +**每个阶段必须真正替换旧实现,而不是并行保留。** + +- 阶段N完成后,旧实现被移除 +- 新实现通过适配器保持与老接口兼容 +- 每个阶段都是"单行道",验证通过后才能进入下一阶段 +- 每个阶段的成果都必须能产生可运行的3D Tiles数据,可在Cesium中查看验证 + +### 1.3 参考架构 +基于以下已有模块: +- `spatial/strategy/octree_strategy.h` - 八叉树策略 +- `common/tileset_builder.h` - Tileset构建器 +- `b3dm/b3dm_generator.h` - B3DM生成器 +- `common/geometry_extractor.h` - 几何体提取器接口 +- `shapefile/shapefile_processor.h` - Shapefile处理器(参考实现) + +--- + +## 2. 现状分析 + +### 2.1 当前FBXPipeline架构 +``` +FBXPipeline +├── FBXLoader (加载FBX,构建meshPool) +├── 内嵌OctreeNode结构 +├── buildOctree() (自定义八叉树构建) +├── processNode() (递归处理节点) +│ └── createB3DM() (生成B3DM) +└── writeTilesetJson() (写入tileset.json) +``` + +### 2.2 当前数据结构 +```cpp +// FBXPipeline.h +struct InstanceRef { + MeshInstanceInfo* meshInfo; + int transformIndex; +}; + +struct OctreeNode { + osg::BoundingBox bbox; + std::vector content; + std::vector children; + int depth = 0; +}; +``` + +### 2.3 当前输出结构 +``` +output/ +├── tileset.json +└── tile_0/ + ├── content.b3dm + ├── content_lod1.b3dm + └── content_lod2.b3dm +``` + +--- + +## 3. 迁移后架构(基于已有架构复用) + +``` +FBXPipeline (新) +├── FBXLoader (保持不变) +├── FBXSpatialItemAdapter ✅ (阶段1已完成) +├── OctreeStrategy (复用spatial模块) +├── LODPipeline (复用已有lod_pipeline.h) +├── B3DMGenerator (复用b3dm模块) +├── GLTFBuilder (复用gltf/gltf_builder.h) +├── Geometry/Material/TextureUtils ✅ (已完成) +└── TilesetBuilder (复用common模块) +``` + +### 3.1 架构复用说明 + +| 组件 | 状态 | 说明 | +|------|------|------| +| `FBXSpatialItemAdapter` | ✅ 已完成 | 阶段1实现 | +| `GeometryUtils/MaterialUtils/TextureUtils` | ✅ 已完成 | 工具类重构 | +| `LODPipeline` | ✅ 已存在 | `lod_pipeline.h` | +| `GLTFBuilder` | ✅ 已存在 | `gltf/gltf_builder.h` | +| `B3DMGenerator` | ✅ 已存在 | `b3dm/b3dm_generator.h` | +| `OctreeStrategy` | ✅ 已存在 | `spatial/strategy/octree_strategy.h` | +| `TilesetBuilder` | ✅ 已存在 | `common/tileset_builder.h` | + +### 3.2 目录结构迁移 +从 `tile_{treePath}/` 迁移到 `tile/{z}/{x}/{y}/`: + +| 原路径 | 新路径 | 说明 | +|--------|--------|------| +| tile_0/ | tile/0/0/0/ | 根节点 | +| tile_0_3/ | tile/1/3/0/ | 深度1,节点3 | +| tile_0_1_5/ | tile/2/13/0/ | 深度2,x=1*8+5=13 | + +--- + +## 4. 四阶段渐进式迁移(真正的逐步替换) + +``` +阶段0: 原始实现(基准线) + ↓ +阶段1: 数据层替换 + - 新增: FBXSpatialItemAdapter + - 替换: FBX数据访问从InstanceRef改为SpatialItem + - 移除: 无(第一个阶段) + - 适配器: 无(直接替换) + + ↓ [验证通过后才能进入下一阶段] + +阶段2: 空间索引替换 + LOD集成 + - 新增: OctreeStrategy + LODPipeline集成 + - 替换: buildOctree() → buildSpatialIndex() + - 替换: 内嵌generateLODChain → build_lod_levels() + - 移除: 旧OctreeNode结构、旧buildOctree实现 + - 适配器: FBXOctreeAdapter(新OctreeNode → 老processNode接口) + + ↓ [验证通过后才能进入下一阶段] + +阶段3: B3DM生成替换(简化版) + - 使用: 已有B3DMGenerator + GLTFBuilder + - 替换: createB3DM() → B3DMGenerator::generateLODFiles() + - 移除: 旧createB3DM实现、appendGeometryToModel + - 无需自定义适配器,直接使用已有接口 + + ↓ [验证通过后才能进入下一阶段] + +阶段4: Tileset生成替换 + - 新增: TilesetBuilder + - 替换: writeTilesetJson() → TilesetBuilder + - 移除: 旧writeTilesetJson实现、processNode中的旧逻辑 + - 适配器: FBXTilesetAdapter(老接口 → 新TilesetBuilder) + + ↓ [验证通过后才能进入下一阶段] + +阶段5: 清理适配器(可选) + - 移除: 所有适配器(如果不再需要兼容老接口) + - 结果: 纯新实现 +``` + +### 4.1 阶段执行与替换管理 + +| 阶段 | 新增组件 | 替换动作 | 移除组件 | 适配器 | 验证方式 | +|------|----------|----------|----------|--------|----------| +| **阶段1** | FBXSpatialItemAdapter | 数据访问方式 | 无 | 无 | 运行命令,Cesium查看 | +| **阶段2** | OctreeStrategy + LODPipeline | buildOctree() → buildSpatialIndex()
generateLODChain → build_lod_levels() | 旧buildOctree实现 | FBXOctreeAdapter | 运行命令,Cesium查看 | +| **阶段3** | B3DMGenerator
GLTFBuilder | createB3DM() → generateLODFiles() | 旧createB3DM、appendGeometryToModel | 无需适配器 | 运行命令,Cesium查看 | +| **阶段4** | TilesetBuilder | writeTilesetJson() → 新实现 | 旧writeTilesetJson、processNode旧逻辑 | FBXTilesetAdapter | 运行命令,Cesium查看 | +| **阶段5** | 无 | 清理适配器 | 所有适配器(可选) | 无 | 运行命令,Cesium查看 | + +### 4.2 每个阶段的开始步骤 + +#### 阶段1开始前 +- **无需移除任何代码**(这是第一个阶段) +- **新增**: `FBXSpatialItemAdapter` 实现 +- **修改**: `FBXPipeline` 添加 `spatialItems_` 成员 +- **替换**: 修改数据访问代码,从`InstanceRef`改为`SpatialItem` +- **验证**: 确保输出与阶段0一致 + +**状态**: ⏳ 待实现 + +#### 阶段2开始前 +- **确认阶段1验证通过** +- **移除**: 旧`buildOctree()`方法实现 +- **移除**: 内嵌`generateLODChain` Lambda,改用`build_lod_levels()` +- **移除**: 旧的手动八叉树构建代码(填充`rootNode->content`的代码) +- **新增**: `OctreeStrategy` + `FBXOctreeAdapter` +- **修改**: `run()` 方法调用 `buildSpatialIndex()` + `FBXOctreeAdapter::convertToLegacyOctree()` +- **修改**: LOD配置使用`LODPipelineSettings`和`build_lod_levels()` +- **验证**: 确保输出与阶段1一致 + +**状态**: ⏳ 待实现 +- 计划移除约230行旧代码 +- `rootNode` 类型将从 `OctreeNode*` 改为 `fbx::LegacyOctreeNode*` +- `processNode` 签名需更新 +- LOD配置复用已有`lod_pipeline.h` + +#### 阶段3开始前 +- **确认阶段2验证通过** +- **移除**: 旧`createB3DM()`方法实现 +- **移除**: `appendGeometryToModel()`函数 +- **使用**: 已有`B3DMGenerator` + `GLTFBuilder` +- **修改**: `processNode`中调用`B3DMGenerator::generateLODFiles()` +- **验证**: 确保输出与阶段2一致 + +**状态**: ⏳ 待实现 +- 无需创建自定义适配器 +- 直接使用`b3dm::B3DMGenerator`的已有接口 + +#### 阶段4开始前 +- **确认阶段3验证通过** +- **移除**: 旧`writeTilesetJson()`方法实现 +- **移除**: `processNode`中的旧逻辑(或完全移除`processNode`) +- **新增**: `TilesetBuilder` + `FBXTilesetAdapter` +- **修改**: `run()` 方法调用 `tilesetAdapter_->buildAndWriteTileset()` +- **验证**: 确保输出与阶段3一致 + +**状态**: ⏳ 待实现 + +#### 阶段5(最终清理,可选) +- **确认阶段4验证通过并稳定运行** +- **移除**: 适配器代码(`FBXOctreeAdapter`、`FBXTilesetAdapter`) +- **移除**: 所有遗留的旧方法声明 +- **清理**: 代码结构,优化性能 +- **验证**: 最终纯新实现输出与基准一致 + +**注意**: 阶段3无需`FBXB3DMAdapter`,直接使用`B3DMGenerator` + +--- + +## 5. 已有架构复用指南 + +### 5.0 可复用架构清单 + +#### 5.0.1 LOD Pipeline (`lod_pipeline.h`) +```cpp +// 已有功能 +struct LODPipelineSettings { + bool enable_lod = false; + std::vector levels; +}; + +std::vector build_lod_levels( + const std::vector& ratios, + float base_error, + const SimplificationParams& simplify_template, + const DracoCompressionParams& draco_template, + bool draco_for_lod0 = false +); +``` + +**替换FBXPipeline中的**: 内嵌`generateLODChain` Lambda (65-84行) + +#### 5.0.2 GLTFBuilder (`gltf/gltf_builder.h`) +```cpp +// 已有功能 +class GLTFBuilder { +public: + GLTFBuildResult build(const std::vector& instances); + GLTFBuildResult buildWithMaterialGrouping( + const std::vector& instances, + const std::vector& geometries + ); +}; +``` + +**替换FBXPipeline中的**: `appendGeometryToModel` 函数 + +#### 5.0.3 B3DMGenerator (`b3dm/b3dm_generator.h`) +```cpp +// 已有功能 +class B3DMGenerator { +public: + // 生成多LOD级别的B3DM文件 + std::vector generateLODFiles( + const spatial::core::SpatialItemRefList& items, + const std::string& outputDir, + const std::string& baseFilename, + const std::vector& lodLevels + ); +}; +``` + +**替换FBXPipeline中的**: `createB3DM` 方法 + +#### 5.0.4 工具类 (`gltf/utils/`) +```cpp +// 已完成 +GeometryUtils::extractGeometryData() +GeometryUtils::processPrimitiveSet() +MaterialUtils::extractPBRParams() +TextureUtils::processTexture() +``` + +**被GLTFBuilder内部使用** + +--- + +## 6. 详细实施步骤(基于已有架构) + +### 阶段0: 原始实现(基准线) + +**目标**: 建立可验证的基准 + +**当前状态**: `FBXPipeline.cpp` 原始实现 + +**验证命令**: +```bash +./_3dtile -f fbx -i data/test.fbx -o output_baseline/ --lon 116.0 --lat 40.0 +# 在Cesium中查看 output_baseline/tileset.json +``` + +--- + +### 阶段1: 数据层替换 ✅ 已完成 + +**目标**: 创建FBXSpatialItemAdapter,将FBX数据访问从`InstanceRef`改为`SpatialItem` + +**状态**: ✅ 已完成 + +#### 6.1.1 已创建文件 + +**文件**: `src/fbx/fbx_spatial_item_adapter.h` 和 `.cpp` + +- `FBXSpatialItemAdapter` 类继承自 `spatial::core::SpatialItem` +- 实现了 `getBounds()`, `getId()`, `getCenter()` 接口 +- 提供FBX特有接口:`getMeshInfo()`, `getTransformIndex()`, `getTransform()`, `getNodeName()`, `getGeometry()` +- `createSpatialItems()` 辅助函数从FBXLoader创建所有适配器 + +#### 6.1.2 已修改文件 + +**文件**: `src/FBXPipeline.h` +- 添加 `#include "fbx/fbx_spatial_item_adapter.h"` +- 添加 `fbx::FBXSpatialItemList spatialItems_` 成员 + +**文件**: `src/FBXPipeline.cpp` +- 在 `run()` 方法中添加空间对象适配器创建逻辑 +- 日志标记更新为 "Stage 1" + +#### 6.1.3 编译验证 + +✅ 代码编译成功,无错误。 + +```bash +cargo build +# Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s +``` + +--- + +### 阶段2: 空间索引替换 + LOD集成 + +**目标**: 用`OctreeStrategy`替换旧的`buildOctree`,集成`LODPipeline` + +**状态**: ⏳ 待实现 + +#### 6.2.1 复用LODPipeline + +替换FBXPipeline.cpp 65-84行的内嵌Lambda: + +```cpp +// 旧代码(移除) +auto generateLODChain = [&](const PipelineSettings& cfg) -> LODPipelineSettings { + // ... 内嵌实现 +}; + +// 新代码(使用已有架构) +LODPipelineSettings lodSettings; +if (settings.enableLOD) { + SimplificationParams simTemplate; + simTemplate.target_error = 0.0001f; + + DracoCompressionParams dracoTemplate; + dracoTemplate.enable_compression = settings.enableDraco; + + lodSettings.enable_lod = true; + lodSettings.levels = build_lod_levels( + settings.lodRatios, + simTemplate.target_error, + simTemplate, + dracoTemplate, + false // draco_for_lod0 + ); + LOG_I("Generated %zu LOD levels", lodSettings.levels.size()); +} +``` + +#### 6.2.2 复用OctreeStrategy + +```cpp +// 创建八叉树策略 +spatial::strategy::OctreeStrategy octreeStrategy; + +// 添加所有空间对象 +for (const auto& item : spatialItems_) { + octreeStrategy.addItem(item); +} + +// 构建八叉树 +OctreeConfig config; +config.maxDepth = settings.maxDepth; +config.maxItemsPerNode = settings.maxItemsPerTile; +octreeStrategy.build(config); +``` + +#### 6.2.3 创建FBXOctreeAdapter + +**文件**: `src/fbx/fbx_octree_adapter.h` + +适配器将新的`OctreeStrategy`节点转换为老的`OctreeNode`格式,保持`processNode`兼容。 + +#### 6.2.4 修改FBXPipeline + +```cpp +void FBXPipeline::run() { + LOG_I("Starting FBXPipeline (Stage 2)..."); + + // 1. 加载FBX + loader = new FBXLoader(settings.inputPath); + loader->load(); + + // 2. 创建空间对象适配器(阶段1) + spatialItems_ = fbx::createSpatialItems(loader); + + // 3. 构建LOD配置(使用LODPipeline) + LODPipelineSettings lodSettings; + if (settings.enableLOD) { + // ... 使用build_lod_levels() + } + + // 4. 构建新空间索引(使用OctreeStrategy) + LOG_I("Building OctreeStrategy..."); + spatial::strategy::OctreeStrategy octreeStrategy; + for (const auto& item : spatialItems_) { + octreeStrategy.addItem(item); + } + octreeStrategy.build({settings.maxDepth, settings.maxItemsPerTile}); + + // 5. 转换为传统格式(适配器) + rootNode = fbx::FBXOctreeAdapter::convertToLegacyOctree(octreeStrategy, spatialItems_); + + // 6. 处理节点 + tileset::Tile rootTile = processNode(rootNode, settings.outputPath, "0"); + + // ... +} +``` + +#### 6.2.5 阶段2验证 + +**验证内容**: +1. `build_lod_levels()`生成的LOD配置正确 +2. 新八叉树构建正确 +3. 输出与阶段1完全一致 +4. Cesium中正常显示 + +--- + +### 阶段3: B3DM生成替换(简化版) + +**目标**: 用已有的`B3DMGenerator`和`GLTFBuilder`替换旧的`createB3DM` + +**状态**: ⏳ 待实现 + +#### 6.3.1 已有架构复用 + +**已有组件**: +- `GLTFBuilder` (`gltf/gltf_builder.h`) - 替代`appendGeometryToModel` +- `B3DMGenerator` (`b3dm/b3dm_generator.h`) - 生成B3DM文件 +- `Geometry/Material/TextureUtils` - 已完成工具类 + +**架构关系**: +``` +FBXPipeline::processNode() + │ + ▼ +B3DMGenerator::generateLODFiles() + │ + ├──► GLTFBuilder::build() ──► GeometryUtils/MaterialUtils/TextureUtils + │ + └──► B3DMWriter::write() +``` + +#### 5.3.2 appendGeometryToModel 功能拆解(✅ 已完成) + +原 `appendGeometryToModel` 函数(约 600+ 行)包含以下重复逻辑: + +```cpp +// 原函数结构(简化) +void appendGeometryToModel(model, instances, ...) { + // 1. 几何体提取和变换(重复代码:坐标转换、法线转换) + for (每个实例) { + // 顶点变换(Y-up → Z-up) + // 法线变换(逆转置矩阵) + // 纹理坐标提取 + } + + // 2. 索引处理(重复代码:TRIANGLES/TRIANGLE_STRIP/TRIANGLE_FAN) + for (每个PrimitiveSet) { + // 处理不同图元类型 + } + + // 3. 材质处理(重复代码:PBR参数提取) + extractMaterialParams(stateSet, ...); // 被重复调用 + + // 4. 纹理处理(严重重复:基础纹理、法线纹理、自发光纹理各100+行相似代码) + processBaseColorTexture(...); // ~120行 + processNormalTexture(...); // ~120行(几乎相同) + processEmissiveTexture(...); // ~120行(几乎相同) +} +``` + +**✅ 已完成的抽取结构**: + +```cpp +// src/gltf/utils/geometry_utils.h - 几何体处理工具类 +class GeometryUtils { +public: + // 计算法线变换矩阵(逆转置) + static osg::Matrixd computeNormalMatrix(const osg::Matrixd& matrix); + + // 变换顶点(Y-up到Z-up) + static osg::Vec3d transformVertex(const osg::Vec3d& vertex, const osg::Matrixd& matrix); + + // 变换法线(Y-up到Z-up) + static osg::Vec3d transformNormal(const osg::Vec3d& normal, const osg::Matrixd& normalMatrix); + + // 提取变换后的几何体数据 + static size_t extractGeometryData( + const osg::Geometry* geom, + const osg::Matrixd& matrix, + const osg::Matrixd& normalMatrix, + std::vector& outPositions, + std::vector& outNormals, + std::vector& outTexcoords, + size_t baseIndex = 0 + ); + + // 处理索引(支持多种图元类型) + static size_t processPrimitiveSet( + const osg::PrimitiveSet* ps, + uint32_t baseIndex, + std::vector& outIndices + ); + + // 处理DrawArrays + static size_t processDrawArrays( + const osg::DrawArrays* da, + uint32_t baseIndex, + std::vector& outIndices + ); + + // 处理DrawElementsUShort + static size_t processDrawElementsUShort( + const osg::DrawElementsUShort* de, + uint32_t baseIndex, + std::vector& outIndices + ); + + // 处理DrawElementsUInt + static size_t processDrawElementsUInt( + const osg::DrawElementsUInt* de, + uint32_t baseIndex, + std::vector& outIndices + ); +}; + +// src/gltf/utils/material_utils.h - 材质处理工具类 +struct PBRParams { + std::vector baseColor = {1.0, 1.0, 1.0, 1.0}; + double emissiveColor[3] = {0.0, 0.0, 0.0}; + float roughnessFactor = 1.0f; + float metallicFactor = 0.0f; + float aoStrength = 1.0f; +}; + +class MaterialUtils { +public: + // 从StateSet提取PBR参数 + static void extractPBRParams( + const osg::StateSet* stateSet, + PBRParams& outParams + ); + + // 检查是否有材质 + static bool hasMaterial(const osg::StateSet* stateSet); + + // 获取各类纹理 + static const osg::Texture* getBaseColorTexture(const osg::StateSet* stateSet); + static const osg::Texture* getNormalTexture(const osg::StateSet* stateSet); + static const osg::Texture* getEmissiveTexture(const osg::StateSet* stateSet); +}; + +// src/gltf/utils/texture_utils.h - 纹理处理工具类 +struct TextureResult { + std::vector data; + std::string mimeType; + bool hasAlpha = false; + bool success = false; +}; + +class TextureUtils { +public: + // 统一的纹理处理入口,替代3处重复代码 + static TextureResult processTexture( + const osg::Texture* texture, + bool enableKTX2 = false + ); + + // 将图像数据添加到GLTF模型 + static int addImageToModel( + tinygltf::Model& model, + tinygltf::Buffer& buffer, + const std::vector& imageData, + const std::string& mimeType, + bool useBasisu + ); +}; + +// gltf/gltf_builder.h(替代appendGeometryToModel) +class GLTFBuilder { +public: + struct Config { + bool enableDraco = false; + DracoCompressionParams dracoParams; + bool enableKTX2 = false; + bool enableUnlit = false; + }; + + explicit GLTFBuilder(const Config& config); + + // 主构建函数(替代appendGeometryToModel) + bool build( + const std::vector& instances, + std::vector& outGlbData, + osg::BoundingBoxd* outBounds = nullptr + ); + +private: + Config config_; + gltf_writer::ExtensionManager extMgr_; +}; +``` + +#### 5.3.3 gltf_writer 模块完善(可选优化) + +`gltf_writer` 模块可以基于已完成的工具类进一步封装: + +```cpp +// gltf_writer/primitive_builder.h(可选封装) +// 基于 GeometryUtils 封装 Primitive 构建 +class PrimitiveBuilder { +public: + void addGeometryData( + const osg::Geometry* geom, + const osg::Matrixd& matrix + ); + void setMaterial(int materialIndex); + tinygltf::Primitive build(tinygltf::Model& model, tinygltf::Buffer& buffer); + +private: + std::vector positions_; + std::vector normals_; + std::vector texcoords_; + std::vector indices_; + int materialIndex_ = -1; +}; + +// gltf_writer/material_builder.h(可选封装) +// 基于 MaterialUtils 封装 Material 构建 +class MaterialBuilder { +public: + void fromOSGStateSet(const osg::StateSet* stateSet); + void setUnlit(bool unlit); + void setDoubleSided(bool doubleSided); + int build(tinygltf::Model& model); + +private: + PBRParams pbrParams_; + int baseColorTexture_ = -1; + int normalTexture_ = -1; + bool unlit_ = false; + bool doubleSided_ = true; +}; + +// gltf_writer/texture_builder.h(可选封装) +// 基于 TextureUtils 封装 Texture 构建 +class TextureBuilder { +public: + // 从OSG纹理创建GLTF纹理 + int createFromOSGTexture( + tinygltf::Model& model, + tinygltf::Buffer& buffer, + const osg::Texture* texture, + gltf_writer::ExtensionManager& extMgr, + bool enableKTX2 + ); +}; +``` + +**说明**: 这些封装是可选的,因为 `GeometryUtils/MaterialUtils/TextureUtils` 已经提供了底层功能。 +可以直接在 `GLTFBuilder` 中使用工具类,无需额外的封装层。 + +#### 5.3.4 代码复用架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 上层调用者 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ FBXPipeline │ │B3DMGenerator │ │ ShapefileProcessor │ │ +│ │ (老代码) │ │ (新代码) │ │ (参考实现) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │ +│ │ │ │ │ +│ └─────────────────┼──────────────────────┘ │ +│ │ 复用 │ +└───────────────────────────┼─────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ GLTF 构建层(公共) │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ GLTFBuilder │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ +│ │ │GeometryUtils│ │MaterialUtils│ │ TextureUtils │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ gltf_writer 模块 │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ +│ │ │ExtensionMgr │ │ Builders │ │ Extensions │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ OSG/外部工具 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ OSG Geometry│ │process_texture│ │ tinygltf │ │ +│ └──────────────┘ └──────────────┘ └──────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**复用策略**: +1. **GeometryUtils**: 处理所有 OSG 几何体到原始数据的转换 + - 被 `GLTFBuilder` 使用 + - 未来可被其他格式转换器复用 + +2. **MaterialUtils**: 提取 PBR 材质参数 + - 统一的材质参数提取逻辑 + - 支持从 OSG StateSet 读取标准参数 + +3. **TextureUtils**: 纹理加载和压缩 + - 直接调用 `process_texture` 进行 KTX2 压缩 + - 统一的纹理处理流程(文件加载 → 内存编码 → KTX2压缩) + +4. **GLTFBuilder**: 高层构建器 + - 替代 `appendGeometryToModel` + - 被 `B3DMGenerator` 调用生成 glb + - 可被任何需要生成 glTF 的模块复用 + +#### 5.3.5 当前状态(阶段2完成后) + +```cpp +std::pair createB3DM( + const std::vector& instances, ...) { + // 旧实现:直接操作tinygltf::Model + tinygltf::Model model; + appendGeometryToModel(model, instances, ...); // 需要移除! + // ...生成B3DM... +} +``` + +#### 5.3.6 阶段3实现步骤(更新版) + +**步骤1: ✅ 创建公共工具类(gltf/utils/)- 已完成** + +```cpp +// src/gltf/utils/geometry_utils.h & .cpp ✅ +// - 从 appendGeometryToModel 提取几何体处理逻辑 +// - 包括:顶点变换、法线变换、索引处理 +// - 支持泛型数组处理(Vec3/Vec4/Vec3d/Vec4d/double/float) + +// src/gltf/utils/material_utils.h & .cpp ✅ +// - 从 appendGeometryToModel 提取材质参数提取逻辑 +// - PBRParams 结构体封装 +// - 纹理获取辅助函数 + +// src/gltf/utils/texture_utils.h & .cpp ✅ +// - 从 appendGeometryToModel 提取纹理处理逻辑 +// - TextureResult 结构体封装 +// - 直接调用 ::process_texture() 进行 KTX2 压缩 +``` + +**步骤2: 创建 GLTFBuilder(替代 appendGeometryToModel)** + +```cpp +// src/gltf/gltf_builder.h & .cpp(新增) +#pragma once + +#include "utils/geometry_utils.h" +#include "utils/material_utils.h" +#include "utils/texture_utils.h" +#include + +namespace gltf { + +struct GLTFBuilderConfig { + bool enableDraco = false; + bool enableKTX2 = false; + bool enableUnlit = false; + bool doubleSided = true; + int dracoPositionQuantization = 14; + int dracoNormalQuantization = 10; + int dracoTexcoordQuantization = 12; +}; + +class GLTFBuilder { +public: + explicit GLTFBuilder(const GLTFBuilderConfig& config); + + // 主构建函数(替代 appendGeometryToModel) + bool build( + const std::vector& instances, + std::vector& outGlbData, + osg::BoundingBoxd* outBounds = nullptr + ); + +private: + GLTFBuilderConfig config_; + + // 构建单个 Mesh + void buildMesh( + tinygltf::Model& model, + tinygltf::Buffer& buffer, + const osg::Geometry* geom, + const osg::Matrixd& matrix, + int materialIndex + ); + + // 构建材质 + int buildMaterial( + tinygltf::Model& model, + tinygltf::Buffer& buffer, + const osg::StateSet* stateSet + ); + + // 构建纹理 + int buildTexture( + tinygltf::Model& model, + tinygltf::Buffer& buffer, + const osg::Texture* texture + ); + + // 添加 BufferView 和 Accessor + int addBufferViewAndAccessor( + tinygltf::Model& model, + tinygltf::Buffer& buffer, + const std::vector& data, + int componentType, + const std::string& type, + size_t count + ); +}; + +} // namespace gltf +``` + +**步骤3: 修改 B3DMGenerator 使用 GLTFBuilder** + +```cpp +// src/b3dm/b3dm_generator.cpp +#include "../gltf/gltf_builder.h" + +void B3DMGenerator::buildGLTFModel(...) { + // 使用 GLTFBuilder 替代直接调用 appendGeometryToModel + gltf::GLTFBuilderConfig config; + config.enableDraco = settings.enableDraco; + config.enableKTX2 = settings.enableKTX2; + config.enableUnlit = settings.enableUnlit; + + gltf::GLTFBuilder gltfBuilder(config); + std::vector glbData; + osg::BoundingBoxd bounds; + + if (gltfBuilder.build(instances, glbData, &bounds)) { + // 将 glb 包装为 B3DM + // ... + } +} +``` + +**步骤4: 移除旧代码** +- ✅ `appendGeometryToModel` 的功能已拆解到工具类 +- 删除 `appendGeometryToModel` 函数声明和实现 +- 删除 `createB3DM` 旧实现 + +#### 5.3.7 创建FBXGeometryExtractor + +**文件**: `src/fbx/fbx_geometry_extractor.h` + +```cpp +#pragma once + +#include "../common/geometry_extractor.h" +#include "fbx_spatial_item_adapter.h" + +namespace fbx { + +/** + * @brief FBX几何体提取器 + * + * 实现IGeometryExtractor接口,从FBXSpatialItemAdapter提取几何体 + */ +class FBXGeometryExtractor : public common::IGeometryExtractor { +public: + std::vector> extract( + const spatial::core::SpatialItem* item) override; + + std::string getId(const spatial::core::SpatialItem* item) override; + + std::map getAttributes( + const spatial::core::SpatialItem* item) override; +}; + +} // namespace fbx +``` + +#### 5.3.3 创建FBXB3DMContentGenerator + +**文件**: `src/fbx/fbx_b3dm_content_generator.h` + +```cpp +#pragma once + +#include "../b3dm/b3dm_generator.h" +#include "fbx_geometry_extractor.h" +#include "../FBXPipeline.h" +#include + +namespace fbx { + +/** + * @brief FBX B3DM内容生成器 + * + * 封装b3dm::B3DMGenerator,提供FBX特定的B3DM生成功能 + */ +class FBXB3DMContentGenerator { +public: + struct Config { + double centerLongitude = 0.0; + double centerLatitude = 0.0; + double centerHeight = 0.0; + bool enableDraco = false; + bool enableSimplification = false; + SimplificationParams simplifyParams; + }; + + explicit FBXB3DMContentGenerator(const Config& config); + + /** + * @brief 生成B3DM内容 + * @param instances 实例引用列表 + * @param tilePath 瓦片路径 + * @param tileName 瓦片名称 + * @param simParams 简化参数 + * @return B3DM文件名和包围盒 + */ + std::pair generate( + const std::vector& instances, + const std::string& tilePath, + const std::string& tileName, + const SimplificationParams& simParams); + +private: + Config config_; + b3dm::B3DMGeneratorConfig generatorConfig_; +}; + +} // namespace fbx +``` + +#### 5.3.4 创建FBXB3DMAdapter + +**文件**: `src/fbx/fbx_b3dm_adapter.h` + +```cpp +#pragma once + +#include "../FBXPipeline.h" +#include "fbx_b3dm_content_generator.h" +#include +#include + +namespace fbx { + +/** + * @brief B3DM生成适配器 + * + * 阶段3适配器:在老流程中调用新的B3DMGenerator + * 保持createB3DM接口不变,内部使用新实现 + */ +class FBXB3DMAdapter { +public: + struct Config { + double centerLongitude = 0.0; + double centerLatitude = 0.0; + double centerHeight = 0.0; + bool enableDraco = false; + bool enableSimplification = false; + SimplificationParams simplifyParams; + }; + + explicit FBXB3DMAdapter(const Config& config); + + /** + * @brief 生成B3DM(兼容老接口) + * + * 保持与FBXPipeline::createB3DM相同的接口, + * 内部使用新的B3DMGenerator实现 + * + * @param instances 实例引用列表(老格式) + * @param tilePath 瓦片路径 + * @param tileName 瓦片名称 + * @param simParams 简化参数 + * @return B3DM文件名和包围盒 + */ + std::pair createB3DM( + const std::vector& instances, + const std::string& tilePath, + const std::string& tileName, + const SimplificationParams& simParams = SimplificationParams()); + +private: + Config config_; + std::unique_ptr generator_; +}; + +} // namespace fbx +``` + +#### 5.3.5 修改FBXPipeline真正替换createB3DM + +**文件**: `src/FBXPipeline.h` + +```cpp +// 阶段3:添加B3DM生成适配器 +std::unique_ptr b3dmAdapter_; +``` + +**文件**: `src/FBXPipeline.cpp` + +```cpp +void FBXPipeline::run() { + // ...加载FBX、构建空间索引... + + // 阶段3:初始化B3DM生成适配器 + fbx::FBXB3DMAdapter::Config b3dmConfig; + b3dmConfig.centerLongitude = settings.longitude; + b3dmConfig.centerLatitude = settings.latitude; + b3dmConfig.centerHeight = settings.height; + b3dmConfig.enableDraco = settings.enableDraco; + b3dmConfig.enableSimplification = settings.enableSimplify; + b3dmAdapter_ = std::make_unique(b3dmConfig); + + // 使用老流程处理,但B3DM使用新生成器 + LOG_I("Processing Nodes with New B3DM Generator..."); + tileset::Tile rootTile = processNode(rootNode, settings.outputPath, "0"); + + // ...写入Tileset... +} + +// 修改createB3DM调用,使用适配器(旧实现被完全替换) +std::pair FBXPipeline::createB3DM( + const std::vector& instances, + const std::string& tilePath, + const std::string& tileName, + const SimplificationParams& simParams) { + + // 阶段3:使用新的B3DM生成适配器(旧实现已移除) + return b3dmAdapter_->createB3DM(instances, tilePath, tileName, simParams); +} +``` + +**关键**: 旧的`createB3DM`实现和`appendGeometryToModel`被完全移除,取而代之的是`FBXB3DMAdapter`。 + +#### 5.3.8 工具类使用示例 + +以下是使用重构后的工具类的完整示例: + +```cpp +// 示例:使用 GeometryUtils 提取几何体数据 +#include "gltf/utils/geometry_utils.h" + +void processGeometry(const osg::Geometry* geom, const osg::Matrixd& matrix) { + using namespace gltf::utils; + + std::vector positions; + std::vector normals; + std::vector texcoords; + + // 计算法线变换矩阵 + osg::Matrixd normalMatrix = GeometryUtils::computeNormalMatrix(matrix); + + // 提取几何体数据 + size_t vertexCount = GeometryUtils::extractGeometryData( + geom, matrix, normalMatrix, + positions, normals, texcoords + ); + + // 提取索引 + std::vector indices; + for (unsigned int i = 0; i < geom->getNumPrimitiveSets(); ++i) { + GeometryUtils::processPrimitiveSet( + geom->getPrimitiveSet(i), + 0, // baseIndex + indices + ); + } +} +``` + +```cpp +// 示例:使用 MaterialUtils 提取材质参数 +#include "gltf/utils/material_utils.h" + +void processMaterial(const osg::StateSet* stateSet) { + using namespace gltf::utils; + + // 检查是否有材质 + if (!MaterialUtils::hasMaterial(stateSet)) { + return; + } + + // 提取 PBR 参数 + PBRParams params; + MaterialUtils::extractPBRParams(stateSet, params); + + // 获取纹理 + const osg::Texture* baseColorTex = MaterialUtils::getBaseColorTexture(stateSet); + const osg::Texture* normalTex = MaterialUtils::getNormalTexture(stateSet); + const osg::Texture* emissiveTex = MaterialUtils::getEmissiveTexture(stateSet); +} +``` + +```cpp +// 示例:使用 TextureUtils 处理纹理 +#include "gltf/utils/texture_utils.h" + +void processTexture(const osg::Texture* texture) { + using namespace gltf::utils; + + // 处理纹理(自动处理KTX2压缩) + TextureResult result = TextureUtils::processTexture(texture, true); + + if (result.success) { + // result.data - 图像数据 + // result.mimeType - MIME类型 + // result.hasAlpha - 是否包含透明通道 + } +} +``` + +#### 5.3.9 阶段3验证 + +**验证内容**: +1. B3DM文件大小与原始一致(±5%) +2. 几何体数量一致 +3. 顶点数据一致 +4. 在Cesium中正常显示 + +--- + +### 阶段4: Tileset构建替换 + +**目标**: 用`TilesetBuilder`替换旧的`writeTilesetJson`,旧实现被完全移除 + +#### 5.4.1 创建FBXTileMetaConverter + +**文件**: `src/fbx/fbx_tile_meta_converter.h` + +```cpp +#pragma once + +#include "../common/tile_meta.h" +#include "fbx_spatial_item_adapter.h" +#include "../spatial/strategy/octree_strategy.h" +#include +#include + +namespace fbx { + +/** + * @brief FBX瓦片元数据转换器 + * + * 将OctreeStrategy节点转换为TileMeta结构 + */ +class FBXTileMetaConverter { +public: + /** + * @brief 转换八叉树为TileMeta映射表 + * @param strategy 八叉树策略 + * @return 根节点元数据 + 所有节点映射表 + */ + static std::pair convert( + const spatial::strategy::OctreeStrategy& strategy); + +private: + static common::TileMetaPtr convertNodeRecursive( + const spatial::strategy::OctreeNode* node, + common::TileMetaMap& allMetas, + int& nodeIdCounter); +}; + +} // namespace fbx +``` + +#### 5.4.2 创建FBXTilesetAdapter + +**文件**: `src/fbx/fbx_tileset_adapter.h` + +```cpp +#pragma once + +#include "fbx_tile_meta_converter.h" +#include "../common/tileset_builder.h" +#include "../tileset/tileset_types.h" + +namespace fbx { + +/** + * @brief FBX Tileset适配器 + * + * 整合TilesetBuilder生成最终的tileset.json + */ +class FBXTilesetAdapter { +public: + struct Config { + double centerLongitude = 0.0; + double centerLatitude = 0.0; + double centerHeight = 0.0; + double boundingVolumeScale = 1.0; + double geometricErrorScale = 0.5; + bool enableLOD = false; + }; + + explicit FBXTilesetAdapter(const Config& config); + + /** + * @brief 构建Tileset + * @param strategy 八叉树策略 + * @param outputPath 输出路径 + * @return 是否成功 + */ + bool buildAndWriteTileset( + const spatial::strategy::OctreeStrategy& strategy, + const std::string& outputPath); + +private: + Config config_; +}; + +} // namespace fbx +``` + +#### 5.4.3 修改FBXPipeline真正替换writeTilesetJson + +**文件**: `src/FBXPipeline.h` + +```cpp +// 阶段4:添加Tileset适配器 +std::unique_ptr tilesetAdapter_; +``` + +**文件**: `src/FBXPipeline.cpp` + +```cpp +void FBXPipeline::run() { + LOG_I("Starting FBXPipeline (Stage 4)..."); + + // 1. 加载FBX + loader = new FBXLoader(settings.inputPath); + loader->load(); + + // 2. 构建空间索引 + buildSpatialIndex(); + + // 3. 初始化B3DM生成适配器(阶段3) + // ... + + // 4. 初始化Tileset适配器(阶段4新增) + fbx::FBXTilesetAdapter::Config tilesetConfig; + tilesetConfig.centerLongitude = settings.longitude; + tilesetConfig.centerLatitude = settings.latitude; + tilesetConfig.centerHeight = settings.height; + tilesetConfig.geometricErrorScale = settings.geScale; + tilesetConfig.enableLOD = settings.enableLOD; + tilesetAdapter_ = std::make_unique(tilesetConfig); + + // 5. 使用TilesetBuilder统一处理(替换processNode + writeTilesetJson) + LOG_I("Building Tileset with TilesetBuilder..."); + tilesetAdapter_->buildAndWriteTileset(*octreeStrategy_, settings.outputPath); + + LOG_I("FBXPipeline Finished."); +} +``` + +**关键**: 旧的`processNode`和`writeTilesetJson`被完全移除,取而代之的是`FBXTilesetAdapter`。 + +#### 5.4.4 阶段4验证 + +**验证内容**: +1. tileset.json结构正确 +2. 所有B3DM文件被正确引用 +3. 几何误差计算正确 +4. 在Cesium中正常显示 + +--- + +## 6. 总结 + +**正确的迁移节奏**: + +| 阶段 | 动作 | 结果 | +|------|------|------| +| 阶段1 | 替换数据访问方式 | 所有数据通过SpatialItem访问 | +| 阶段2 | 替换空间索引 | OctreeStrategy替代buildOctree | +| 阶段3 | 替换B3DM生成 | B3DMGenerator替代createB3DM | +| 阶段4 | 替换Tileset生成 | TilesetBuilder替代writeTilesetJson | + +**每个阶段都是真正的替换,不是并行保留。** + +**关键原则**: +1. 每个阶段完成后,旧实现被移除 +2. 新实现通过适配器保持与老接口兼容 +3. 验证通过后才能进入下一阶段 +4. 每个阶段都能产生可运行的3D Tiles输出 diff --git a/docs/gltf_writer_refactor.md b/docs/gltf_writer_refactor.md new file mode 100644 index 00000000..515d7bae --- /dev/null +++ b/docs/gltf_writer_refactor.md @@ -0,0 +1,1777 @@ +# glTF Writer 重构方案 + +## 1. 边界定义 + +### 1.1 模块边界 + +| 模块 | 职责 | 不包含 | +|------|------|--------| +| **gltf_writer** | glTF 2.0 模型构建、Extension 管理 | B3DM 封装、Tileset JSON 生成 | +| **b3dm_writer** | B3DM 格式封装(FeatureTable、BatchTable) | glTF 内容生成 | +| **tileset_writer** | Tileset JSON 生成、空间索引结构 | 具体瓦片内容 | + +### 1.2 glTF 坐标系约定 + +glTF 2.0 规范定义: +- **上轴**: Y-Up(Y 轴向上) +- **手性**: 右手坐标系 +- **前向**: -Z 方向 + +**坐标轴转换责任划分**: + +| 数据源 | 源坐标系 | 转换责任 | +|--------|----------|----------| +| FBX | Y-Up(右手) | 无需转换 | +| OSGB | Z-Up(右手) | Pipeline 层负责 Z-Up → Y-Up | +| Shapefile | 投影坐标 | Pipeline 层 + coords 模块负责 | + +**gltf_writer 不负责坐标轴转换**,所有输入数据应已转换为 Y-Up 坐标系。 + +### 1.3 与现有模块的关系 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Pipeline Layer │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ ShapefilePipeline│ │ OsgbPipeline │ │ FBXPipeline │ │ +│ │ - 坐标转换 │ │ - Z-Up→Y-Up │ │ - 材质转换 │ │ +│ │ - 属性处理 │ │ - 纹理处理 │ │ - 纹理变换 │ │ +│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ +│ │ │ │ │ +│ └────────────────────┼────────────────────┘ │ +│ ▼ │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ 核心模块层 │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ coords │ │ gltf_writer │ │ mesh_processor │ │ +│ │ CoordinateSystem│ │ GltfBuilder │ │ simplify_mesh │ │ +│ │ CoordinateTrans-│ │ ExtensionMgr │ │ compress_mesh │ │ +│ │ former │ │ │ │ process_texture │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ b3dm_writer │ │ tileset_writer │ │ +│ │ B3dmWriter │ │ TilesetWriter │ │ +│ │ (独立模块) │ │ (独立模块) │ │ +│ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## 2. 当前问题分析 + +### 2.1 现有代码中的 glTF 底层操作 + +通过分析 `FBXPipeline.cpp`、`osgb23dtile.cpp`、`shp23dtile.cpp`,识别出以下重复的底层操作: + +#### 2.1.1 Buffer 管理 + +```cpp +// 三个 Pipeline 都有类似代码 +tinygltf::Buffer buffer; +buffer.data.resize(offset + size); +memcpy(buffer.data.data() + offset, data, size); +alignment_buffer(buffer.data); // 4 字节对齐 +model.buffers.push_back(std::move(buffer)); +``` + +#### 2.1.2 BufferView 管理 + +```cpp +// 每次创建 BufferView 都需要手动设置属性 +tinygltf::BufferView bv; +bv.buffer = 0; +bv.byteOffset = offset; +bv.byteLength = length; +bv.target = TINYGLTF_TARGET_ARRAY_BUFFER; // 或 ELEMENT_ARRAY_BUFFER +model.bufferViews.push_back(bv); +``` + +#### 2.1.3 Accessor 管理 + +```cpp +// 需要手动计算 min/max +tinygltf::Accessor acc; +acc.bufferView = bvIdx; +acc.byteOffset = 0; +acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; +acc.count = count; +acc.type = TINYGLTF_TYPE_VEC3; + +// 手动计算包围盒 +std::vector box_max = {-1e38, -1e38, -1e38}; +std::vector box_min = {1e38, 1e38, 1e38}; +for (int i = 0; i < count; ++i) { + SET_MAX(box_max[0], positions[i*3]); + SET_MIN(box_min[0], positions[i*3]); + // ... +} +acc.minValues = box_min; +acc.maxValues = box_max; +model.accessors.push_back(acc); +``` + +#### 2.1.4 索引类型选择 + +```cpp +// osgb23dtile.cpp 中的实现 +int pick_index_component_type(uint32_t max_index) { + if (max_index <= std::numeric_limits::max()) { + return TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; + } + if (max_index <= std::numeric_limits::max()) { + return TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT; + } + return TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT; +} +``` + +#### 2.1.5 材质创建 + +```cpp +// shp23dtile.cpp +tinygltf::Material make_color_material(double r, double g, double b) { + tinygltf::Material material; + material.name = "..."; + material.pbrMetallicRoughness.baseColorFactor = {r, g, b, 1}; + material.pbrMetallicRoughness.roughnessFactor = 0.7; + material.pbrMetallicRoughness.metallicFactor = 0.3; + return material; +} + +// osgb23dtile.cpp - 略有不同的默认值 +tinygltf::Material make_color_material_osgb(double r, double g, double b) { + tinygltf::Material material; + material.pbrMetallicRoughness.metallicFactor = 0.0; + material.pbrMetallicRoughness.roughnessFactor = 1.0; + return material; +} +``` + +#### 2.1.6 Extension 管理 + +```cpp +// 每个 Pipeline 独立处理 extension 注册 +if (std::find(model.extensionsUsed.begin(), model.extensionsUsed.end(), + "KHR_draco_mesh_compression") == model.extensionsUsed.end()) { + model.extensionsUsed.push_back("KHR_draco_mesh_compression"); + model.extensionsRequired.push_back("KHR_draco_mesh_compression"); +} + +// 应用 extension 到不同对象 +material.extensions["KHR_materials_unlit"] = tinygltf::Value(...); +primitive.extensions["KHR_draco_mesh_compression"] = tinygltf::Value(...); +texture.extensions["KHR_texture_basisu"] = tinygltf::Value(...); +``` + +#### 2.1.7 纹理/图像管理 + +```cpp +// 图像嵌入 +tinygltf::Image gltfImg; +gltfImg.mimeType = "image/png"; // 或 "image/jpeg", "image/ktx2" +gltfImg.bufferView = bvImgIdx; +model.images.push_back(gltfImg); + +// 纹理创建 +tinygltf::Texture gltfTex; +if (enable_texture_compress) { + tinygltf::Value::Object basisu_ext; + basisu_ext["source"] = tinygltf::Value(imgIdx); + gltfTex.extensions["KHR_texture_basisu"] = tinygltf::Value(basisu_ext); +} else { + gltfTex.source = imgIdx; +} +model.textures.push_back(gltfTex); +``` + +#### 2.1.8 场景结构 + +```cpp +// Node/Scene 组装 +tinygltf::Node node; +node.mesh = meshIdx; +model.nodes.push_back(node); + +tinygltf::Scene scene; +scene.nodes.push_back(nodeIdx); +model.scenes.push_back(scene); +model.defaultScene = 0; +``` + +### 2.2 问题总结 + +| 问题类型 | 具体表现 | 影响 | +|----------|----------|------| +| **代码重复** | Buffer/Accessor 创建逻辑在三个 Pipeline 中重复 | 维护成本高 | +| **默认值不一致** | `make_color_material` 在不同文件中有不同默认值 | 行为不可预测 | +| **手动计算** | 包围盒、索引类型需要手动计算 | 易出错 | +| **Extension 分散** | Extension 注册逻辑分散在各处 | 难以扩展 | +| **类型不安全** | 直接使用 tinygltf 原始类型 | 无编译期检查 | + +### 2.3 已实现的 Extension + +| Extension | 使用位置 | 当前状态 | +|-----------|----------|----------| +| `KHR_draco_mesh_compression` | Primitive | 已实现 | +| `KHR_materials_unlit` | Material | 已实现 | +| `KHR_texture_basisu` | Texture | 已实现 | + +### 2.4 计划新增的 Extension + +| Extension | 使用位置 | 用途 | 优先级 | +|-----------|----------|------|--------| +| `KHR_texture_transform` | TextureInfo | FBX 纹理 UV 变换 | **P0** | +| `KHR_materials_pbrSpecularGlossiness` | Material | FBX PBR 高光光泽度工作流 | **P0** | + +## 3. 设计目标 + +1. **业务层抽象**:封装 Buffer/Accessor/包围盒等底层概念 +2. **Extension 统一管理**:集中的 extension 注册和应用机制 +3. **类型安全**:为每个 extension 定义强类型数据结构 +4. **默认值统一**:提供一致的默认材质、采样器配置 +5. **最小影响范围**:新增功能不影响现有功能,保证可回归测试 +6. **与现有架构兼容**:与 `coords` 命名空间设计风格一致 + +## 4. 底层概念抽象 + +### 4.1 类型定义(types.h) + +```cpp +#pragma once +#include +#include +#include +#include +#include + +namespace gltf_writer { + +enum class ComponentType : int { + Byte = 5120, + UnsignedByte = 5121, + Short = 5122, + UnsignedShort = 5123, + UnsignedInt = 5125, + Float = 5126 +}; + +enum class AccessorType : int { + Scalar = TINYGLTF_TYPE_SCALAR, + Vec2 = TINYGLTF_TYPE_VEC2, + Vec3 = TINYGLTF_TYPE_VEC3, + Vec4 = TINYGLTF_TYPE_VEC4, + Mat2 = TINYGLTF_TYPE_MAT2, + Mat3 = TINYGLTF_TYPE_MAT3, + Mat4 = TINYGLTF_TYPE_MAT4 +}; + +enum class PrimitiveMode : int { + Points = 0, + Lines = 1, + LineLoop = 2, + LineStrip = 3, + Triangles = 4, + TriangleStrip = 5, + TriangleFan = 6 +}; + +enum class BufferViewTarget : int { + None = 0, + ArrayBuffer = 34962, + ElementArrayBuffer = 34963 +}; + +enum class TextureFilter : int { + Nearest = 9728, + Linear = 9729, + NearestMipmapNearest = 9984, + LinearMipmapNearest = 9985, + NearestMipmapLinear = 9986, + LinearMipmapLinear = 9987 +}; + +enum class TextureWrap : int { + ClampToEdge = 33071, + MirroredRepeat = 33648, + Repeat = 10497 +}; + +inline int toTinyGltf(ComponentType t) { return static_cast(t); } +inline int toTinyGltf(AccessorType t) { return static_cast(t); } +inline int toTinyGltf(PrimitiveMode m) { return static_cast(m); } +inline int toTinyGltf(BufferViewTarget t) { return static_cast(t); } + +} +``` + +### 4.2 包围盒(BoundingBox) + +```cpp +#pragma once +#include +#include +#include +#include +#include + +namespace gltf_writer { + +template +struct BoundingBox { + std::array min; + std::array max; + + BoundingBox() { + min.fill(std::numeric_limits::max()); + max.fill(std::numeric_limits::lowest()); + } + + explicit BoundingBox(std::span data, size_t stride = N) { + min.fill(std::numeric_limits::max()); + max.fill(std::numeric_limits::lowest()); + const size_t count = data.size() / stride; + for (size_t i = 0; i < count; ++i) { + for (size_t j = 0; j < N && j < stride; ++j) { + T v = data[i * stride + j]; + min[j] = std::min(min[j], v); + max[j] = std::max(max[j], v); + } + } + } + + void expand(const T* point) { + for (size_t i = 0; i < N; ++i) { + min[i] = std::min(min[i], point[i]); + max[i] = std::max(max[i], point[i]); + } + } + + void expand(const BoundingBox& other) { + for (size_t i = 0; i < N; ++i) { + min[i] = std::min(min[i], other.min[i]); + max[i] = std::max(max[i], other.max[i]); + } + } + + std::array center() const { + std::array c; + for (size_t i = 0; i < N; ++i) { + c[i] = (min[i] + max[i]) / T(2); + } + return c; + } + + std::array size() const { + std::array s; + for (size_t i = 0; i < N; ++i) { + s[i] = max[i] - min[i]; + } + return s; + } + + std::vector minAsDouble() const { + return std::vector(min.begin(), min.end()); + } + + std::vector maxAsDouble() const { + return std::vector(max.begin(), max.end()); + } + + bool isValid() const { + for (size_t i = 0; i < N; ++i) { + if (min[i] > max[i]) return false; + } + return true; + } + + static BoundingBox Invalid() { + return {}; + } + + static BoundingBox FromPoint(const std::array& point) { + BoundingBox box; + box.min = point; + box.max = point; + return box; + } +}; + +using BoundingBox2F = BoundingBox; +using BoundingBox3F = BoundingBox; +using BoundingBox2D = BoundingBox; +using BoundingBox3D = BoundingBox; + +} +``` + +### 4.3 BufferBuilder(Buffer/BufferView 管理) + +```cpp +#pragma once +#include +#include +#include +#include +#include "types.h" + +namespace gltf_writer { + +class BufferBuilder { +public: + BufferBuilder() = default; + + size_t append(std::span data) { + size_t offset = buffer_data_.size(); + buffer_data_.insert(buffer_data_.end(), data.begin(), data.end()); + return offset; + } + + size_t append(const void* data, size_t size) { + return append(std::span( + static_cast(data), size)); + } + + template + size_t append(std::span data) { + return append(std::as_bytes(data)); + } + + void align(size_t alignment = 4) { + size_t padding = (alignment - (buffer_data_.size() % alignment)) % alignment; + if (padding > 0) { + buffer_data_.resize(buffer_data_.size() + padding); + } + } + + int createBufferView(size_t byte_offset, size_t byte_length, + BufferViewTarget target = BufferViewTarget::None) { + tinygltf::BufferView bv; + bv.buffer = 0; + bv.byteOffset = static_cast(byte_offset); + bv.byteLength = static_cast(byte_length); + bv.target = toTinyGltf(target); + int index = static_cast(buffer_views_.size()); + buffer_views_.push_back(bv); + return index; + } + + template + int addBufferView(std::span data, BufferViewTarget target = BufferViewTarget::None) { + align(); + size_t offset = append(data); + return createBufferView(offset, data.size_bytes(), target); + } + + const std::vector& data() const { return buffer_data_; } + size_t size() const { return buffer_data_.size(); } + const std::vector& bufferViews() const { return buffer_views_; } + + tinygltf::Buffer toTinyGltf() const { + tinygltf::Buffer buf; + buf.data.resize(buffer_data_.size()); + std::memcpy(buf.data.data(), buffer_data_.data(), buffer_data_.size()); + return buf; + } + + void clear() { + buffer_data_.clear(); + buffer_views_.clear(); + } + +private: + std::vector buffer_data_; + std::vector buffer_views_; +}; + +} +``` + +### 4.4 AccessorBuilder(Accessor 管理) + +```cpp +#pragma once +#include +#include +#include +#include +#include +#include "types.h" +#include "bounding_box.h" +#include "buffer_builder.h" + +namespace gltf_writer { + +class AccessorBuilder { +public: + AccessorBuilder() = default; + + template + int addVertexAttribute(BufferBuilder& buffer, + std::span data, + AccessorType type, + ComponentType component_type, + bool compute_bounds = true) { + int bv_idx = buffer.addBufferView(data, BufferViewTarget::ArrayBuffer); + + tinygltf::Accessor acc; + acc.bufferView = bv_idx; + acc.byteOffset = 0; + acc.componentType = toTinyGltf(component_type); + acc.count = static_cast(data.size() / componentCount(type)); + acc.type = toTinyGltf(type); + + if (compute_bounds) { + size_t stride = componentCount(type); + if constexpr (std::is_same_v) { + if (type == AccessorType::Vec3) { + BoundingBox3F bbox(data, stride); + acc.minValues = bbox.minAsDouble(); + acc.maxValues = bbox.maxAsDouble(); + } else if (type == AccessorType::Vec2) { + BoundingBox2F bbox(data, stride); + acc.minValues = bbox.minAsDouble(); + acc.maxValues = bbox.maxAsDouble(); + } + } + } + + int idx = static_cast(accessors_.size()); + accessors_.push_back(acc); + return idx; + } + + int addIndices(BufferBuilder& buffer, std::span indices) { + uint32_t max_idx = 0; + for (auto idx : indices) { + max_idx = std::max(max_idx, idx); + } + + ComponentType comp_type = pickIndexComponentType(max_idx); + int bv_idx = -1; + + if (comp_type == ComponentType::UnsignedByte) { + std::vector indices8(indices.begin(), indices.end()); + bv_idx = buffer.addBufferView(std::span(indices8), + BufferViewTarget::ElementArrayBuffer); + } else if (comp_type == ComponentType::UnsignedShort) { + std::vector indices16(indices.begin(), indices.end()); + bv_idx = buffer.addBufferView(std::span(indices16), + BufferViewTarget::ElementArrayBuffer); + } else { + bv_idx = buffer.addBufferView(indices, BufferViewTarget::ElementArrayBuffer); + } + + tinygltf::Accessor acc; + acc.bufferView = bv_idx; + acc.byteOffset = 0; + acc.componentType = toTinyGltf(comp_type); + acc.count = static_cast(indices.size()); + acc.type = toTinyGltf(AccessorType::Scalar); + acc.minValues = {0.0}; + acc.maxValues = {static_cast(max_idx)}; + + int idx = static_cast(accessors_.size()); + accessors_.push_back(acc); + return idx; + } + + int addIndices16(BufferBuilder& buffer, std::span indices) { + uint16_t max_idx = 0; + for (auto idx : indices) { + max_idx = std::max(max_idx, idx); + } + + int bv_idx = buffer.addBufferView(indices, BufferViewTarget::ElementArrayBuffer); + + tinygltf::Accessor acc; + acc.bufferView = bv_idx; + acc.byteOffset = 0; + acc.componentType = toTinyGltf(ComponentType::UnsignedShort); + acc.count = static_cast(indices.size()); + acc.type = toTinyGltf(AccessorType::Scalar); + acc.minValues = {0.0}; + acc.maxValues = {static_cast(max_idx)}; + + int idx = static_cast(accessors_.size()); + accessors_.push_back(acc); + return idx; + } + + template + int addScalarAttribute(BufferBuilder& buffer, + std::span data, + ComponentType component_type) { + int bv_idx = buffer.addBufferView(data, BufferViewTarget::ArrayBuffer); + + T max_val = std::numeric_limits::lowest(); + T min_val = std::numeric_limits::max(); + for (auto v : data) { + max_val = std::max(max_val, v); + min_val = std::min(min_val, v); + } + + tinygltf::Accessor acc; + acc.bufferView = bv_idx; + acc.byteOffset = 0; + acc.componentType = toTinyGltf(component_type); + acc.count = static_cast(data.size()); + acc.type = toTinyGltf(AccessorType::Scalar); + acc.minValues = {static_cast(min_val)}; + acc.maxValues = {static_cast(max_val)}; + + int idx = static_cast(accessors_.size()); + accessors_.push_back(acc); + return idx; + } + + int addPlaceholder(AccessorType type, ComponentType component_type, + size_t count, + std::optional> min = std::nullopt, + std::optional> max = std::nullopt) { + tinygltf::Accessor acc; + acc.bufferView = -1; + acc.byteOffset = 0; + acc.componentType = toTinyGltf(component_type); + acc.count = static_cast(count); + acc.type = toTinyGltf(type); + if (min) acc.minValues = *min; + if (max) acc.maxValues = *max; + + int idx = static_cast(accessors_.size()); + accessors_.push_back(acc); + return idx; + } + + const std::vector& accessors() const { return accessors_; } + std::vector& accessors() { return accessors_; } + + void clear() { accessors_.clear(); } + +private: + std::vector accessors_; + + static size_t componentCount(AccessorType type) { + switch (type) { + case AccessorType::Scalar: return 1; + case AccessorType::Vec2: return 2; + case AccessorType::Vec3: return 3; + case AccessorType::Vec4: return 4; + case AccessorType::Mat2: return 4; + case AccessorType::Mat3: return 9; + case AccessorType::Mat4: return 16; + default: return 1; + } + } + + static ComponentType pickIndexComponentType(uint32_t max_index) { + if (max_index <= std::numeric_limits::max()) { + return ComponentType::UnsignedByte; + } + if (max_index <= std::numeric_limits::max()) { + return ComponentType::UnsignedShort; + } + return ComponentType::UnsignedInt; + } +}; + +} +``` + +### 4.5 MaterialBuilder(材质管理) + +```cpp +#pragma once +#include +#include +#include +#include "types.h" + +namespace gltf_writer { + +struct PbrMetallicRoughness { + std::array base_color_factor = {1.0, 1.0, 1.0, 1.0}; + double metallic_factor = 0.0; + double roughness_factor = 1.0; + int base_color_texture = -1; + int metallic_roughness_texture = -1; + + static PbrMetallicRoughness Default() { return {}; } + + static PbrMetallicRoughness Color(double r, double g, double b, double a = 1.0) { + PbrMetallicRoughness pbr; + pbr.base_color_factor = {r, g, b, a}; + return pbr; + } +}; + +struct MaterialParams { + std::string name; + PbrMetallicRoughness pbr; + std::array emissive_factor = {0.0, 0.0, 0.0}; + int emissive_texture = -1; + int normal_texture = -1; + int occlusion_texture = -1; + double alpha_cutoff = 0.5; + std::string alpha_mode = "OPAQUE"; + bool double_sided = true; + + static MaterialParams Default() { return {}; } + + static MaterialParams Unlit(const std::array& color) { + MaterialParams m; + m.pbr.base_color_factor = color; + return m; + } +}; + +class MaterialBuilder { +public: + MaterialBuilder() = default; + + int addMaterial(const MaterialParams& params) { + tinygltf::Material mat; + mat.name = params.name; + mat.pbrMetallicRoughness.baseColorFactor = params.pbr.base_color_factor; + mat.pbrMetallicRoughness.metallicFactor = params.pbr.metallic_factor; + mat.pbrMetallicRoughness.roughnessFactor = params.pbr.roughness_factor; + + if (params.pbr.base_color_texture >= 0) { + mat.pbrMetallicRoughness.baseColorTexture.index = params.pbr.base_color_texture; + } + if (params.pbr.metallic_roughness_texture >= 0) { + mat.pbrMetallicRoughness.metallicRoughnessTexture.index = params.pbr.metallic_roughness_texture; + } + + mat.emissiveFactor = params.emissive_factor; + if (params.emissive_texture >= 0) { + mat.emissiveTexture.index = params.emissive_texture; + } + if (params.normal_texture >= 0) { + mat.normalTexture.index = params.normal_texture; + } + if (params.occlusion_texture >= 0) { + mat.occlusionTexture.index = params.occlusion_texture; + } + + mat.alphaCutoff = params.alpha_cutoff; + mat.alphaMode = params.alpha_mode; + mat.doubleSided = params.double_sided; + + int idx = static_cast(materials_.size()); + materials_.push_back(mat); + return idx; + } + + int addDefaultMaterial() { + return addMaterial(MaterialParams::Default()); + } + + int addColorMaterial(double r, double g, double b) { + MaterialParams params; + params.pbr = PbrMetallicRoughness::Color(r, g, b); + params.pbr.roughness_factor = 0.7; + params.pbr.metallic_factor = 0.3; + return addMaterial(params); + } + + tinygltf::Material& get(int index) { return materials_.at(index); } + const tinygltf::Material& get(int index) const { return materials_.at(index); } + + const std::vector& materials() const { return materials_; } + std::vector& materials() { return materials_; } + +private: + std::vector materials_; +}; + +} +``` + +### 4.6 TextureBuilder(纹理/图像/采样器管理) + +```cpp +#pragma once +#include +#include +#include +#include "types.h" + +namespace gltf_writer { + +struct SamplerParams { + int mag_filter = static_cast(TextureFilter::Linear); + int min_filter = static_cast(TextureFilter::LinearMipmapLinear); + int wrap_s = static_cast(TextureWrap::Repeat); + int wrap_t = static_cast(TextureWrap::Repeat); + + static SamplerParams Default() { return {}; } + + static SamplerParams Clamp() { + SamplerParams s; + s.wrap_s = static_cast(TextureWrap::ClampToEdge); + s.wrap_t = static_cast(TextureWrap::ClampToEdge); + return s; + } +}; + +struct ImageParams { + std::string mime_type = "image/png"; + std::string name; + std::string uri; + int buffer_view = -1; +}; + +struct TextureParams { + int sampler = -1; + int source = -1; + std::string name; +}; + +class TextureBuilder { +public: + TextureBuilder() = default; + + int addSampler(const SamplerParams& params) { + tinygltf::Sampler sampler; + sampler.magFilter = params.mag_filter; + sampler.minFilter = params.min_filter; + sampler.wrapS = params.wrap_s; + sampler.wrapT = params.wrap_t; + + int idx = static_cast(samplers_.size()); + samplers_.push_back(sampler); + return idx; + } + + int addDefaultSampler() { + return addSampler(SamplerParams::Default()); + } + + int addImage(const ImageParams& params) { + tinygltf::Image img; + img.mimeType = params.mime_type; + img.name = params.name; + img.uri = params.uri; + img.bufferView = params.buffer_view; + + int idx = static_cast(images_.size()); + images_.push_back(img); + return idx; + } + + int addEmbeddedImage(std::span data, + int buffer_view, + std::string_view mime_type = "image/png") { + ImageParams params; + params.mime_type = std::string(mime_type); + params.buffer_view = buffer_view; + return addImage(params); + } + + int addTexture(const TextureParams& params) { + tinygltf::Texture tex; + tex.sampler = params.sampler; + tex.source = params.source; + tex.name = params.name; + + int idx = static_cast(textures_.size()); + textures_.push_back(tex); + return idx; + } + + int addSimpleTexture(int image_index, int sampler_index = -1) { + TextureParams params; + params.source = image_index; + params.sampler = sampler_index; + return addTexture(params); + } + + const std::vector& samplers() const { return samplers_; } + const std::vector& images() const { return images_; } + const std::vector& textures() const { return textures_; } + + std::vector& samplers() { return samplers_; } + std::vector& images() { return images_; } + std::vector& textures() { return textures_; } + +private: + std::vector samplers_; + std::vector images_; + std::vector textures_; +}; + +} +``` + +### 4.7 MeshBuilder(Mesh/Primitive 管理) + +```cpp +#pragma once +#include +#include +#include +#include "types.h" + +namespace gltf_writer { + +struct PrimitiveParams { + std::unordered_map attributes; + int indices = -1; + int material = -1; + PrimitiveMode mode = PrimitiveMode::Triangles; +}; + +class MeshBuilder { +public: + MeshBuilder() = default; + + int addMesh(std::string_view name = "") { + tinygltf::Mesh mesh; + mesh.name = std::string(name); + + int idx = static_cast(meshes_.size()); + meshes_.push_back(mesh); + return idx; + } + + int addPrimitive(int mesh_index, const PrimitiveParams& params) { + tinygltf::Primitive prim; + for (const auto& [name, accessor] : params.attributes) { + prim.attributes[name] = accessor; + } + prim.indices = params.indices; + prim.material = params.material; + prim.mode = toTinyGltf(params.mode); + + meshes_.at(mesh_index).primitives.push_back(prim); + return static_cast(meshes_[mesh_index].primitives.size() - 1); + } + + int addTriangleMesh(std::string_view name, + int position_accessor, + int normal_accessor = -1, + int texcoord_accessor = -1, + int indices_accessor = -1, + int material_index = -1) { + int mesh_idx = addMesh(name); + + PrimitiveParams prim; + prim.attributes["POSITION"] = position_accessor; + if (normal_accessor >= 0) { + prim.attributes["NORMAL"] = normal_accessor; + } + if (texcoord_accessor >= 0) { + prim.attributes["TEXCOORD_0"] = texcoord_accessor; + } + prim.indices = indices_accessor; + prim.material = material_index; + + addPrimitive(mesh_idx, prim); + return mesh_idx; + } + + tinygltf::Mesh& get(int index) { return meshes_.at(index); } + const tinygltf::Mesh& get(int index) const { return meshes_.at(index); } + + const std::vector& meshes() const { return meshes_; } + std::vector& meshes() { return meshes_; } + +private: + std::vector meshes_; +}; + +} +``` + +### 4.8 SceneBuilder(Node/Scene 管理) + +```cpp +#pragma once +#include +#include +#include +#include + +namespace gltf_writer { + +struct NodeParams { + std::string name; + int mesh = -1; + int skin = -1; + std::array matrix = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; + std::vector children; + std::optional> translation; + std::optional> rotation; + std::optional> scale; +}; + +class SceneBuilder { +public: + SceneBuilder() = default; + + int addNode(const NodeParams& params) { + tinygltf::Node node; + node.name = params.name; + node.mesh = params.mesh; + node.skin = params.skin; + node.matrix.assign(params.matrix.begin(), params.matrix.end()); + node.children = params.children; + + if (params.translation) { + node.translation = {(*params.translation)[0], (*params.translation)[1], (*params.translation)[2]}; + } + if (params.rotation) { + node.rotation = {(*params.rotation)[0], (*params.rotation)[1], (*params.rotation)[2], (*params.rotation)[3]}; + } + if (params.scale) { + node.scale = {(*params.scale)[0], (*params.scale)[1], (*params.scale)[2]}; + } + + int idx = static_cast(nodes_.size()); + nodes_.push_back(node); + return idx; + } + + int addMeshNode(int mesh_index, std::string_view name = "") { + NodeParams params; + params.name = std::string(name); + params.mesh = mesh_index; + return addNode(params); + } + + int createScene(std::span node_indices) { + tinygltf::Scene scene; + for (int idx : node_indices) { + scene.nodes.push_back(idx); + } + + int scene_idx = static_cast(scenes_.size()); + scenes_.push_back(scene); + return scene_idx; + } + + void setDefaultScene(int scene_index) { + default_scene_ = scene_index; + } + + int createDefaultScene(std::span node_indices) { + int scene_idx = createScene(node_indices); + setDefaultScene(scene_idx); + return scene_idx; + } + + const std::vector& nodes() const { return nodes_; } + const std::vector& scenes() const { return scenes_; } + std::optional defaultScene() const { return default_scene_; } + + std::vector& nodes() { return nodes_; } + std::vector& scenes() { return scenes_; } + +private: + std::vector nodes_; + std::vector scenes_; + std::optional default_scene_; +}; + +} +``` + +## 5. Extension 管理 + +### 5.1 ExtensionManager + +```cpp +#pragma once +#include +#include +#include + +namespace gltf_writer { + +class ExtensionManager { +public: + ExtensionManager() = default; + + void use(const std::string& name) { used_.insert(name); } + void require(const std::string& name) { required_.insert(name); } + + void useAndRequire(const std::string& name) { + use(name); + require(name); + } + + void apply(tinygltf::Model& model) const { + for (const auto& ext : used_) { + if (std::find(model.extensionsUsed.begin(), + model.extensionsUsed.end(), ext) == model.extensionsUsed.end()) { + model.extensionsUsed.push_back(ext); + } + } + for (const auto& ext : required_) { + if (std::find(model.extensionsRequired.begin(), + model.extensionsRequired.end(), ext) == model.extensionsRequired.end()) { + model.extensionsRequired.push_back(ext); + } + } + } + + bool isUsed(const std::string& name) const { return used_.count(name) > 0; } + bool isRequired(const std::string& name) const { return required_.count(name) > 0; } + + const std::set& used() const { return used_; } + const std::set& required() const { return required_; } + + void clear() { + used_.clear(); + required_.clear(); + } + +private: + std::set used_; + std::set required_; +}; + +} +``` + +### 5.2 Extension 数据结构 + +#### KHR_texture_transform + +```cpp +#pragma once +#include +#include "extension_manager.h" + +namespace gltf_writer::extensions { + +struct TextureTransform { + std::array offset = {0.0f, 0.0f}; + float rotation = 0.0f; + std::array scale = {1.0f, 1.0f}; + int tex_coord = 0; + + static TextureTransform Identity() { return {}; } + + static TextureTransform WithOffset(float u, float v) { + TextureTransform t; + t.offset = {u, v}; + return t; + } + + static TextureTransform WithScale(float u, float v) { + TextureTransform t; + t.scale = {u, v}; + return t; + } + + static TextureTransform WithRotation(float radians) { + TextureTransform t; + t.rotation = radians; + return t; + } +}; + +inline void applyTextureTransform(tinygltf::Material& material, + const std::string& texture_key, + const TextureTransform& transform, + ExtensionManager& ext_mgr) { + tinygltf::Value::Object ext_obj; + ext_obj["offset"] = tinygltf::Value(tinygltf::Value::Array{ + tinygltf::Value(static_cast(transform.offset[0])), + tinygltf::Value(static_cast(transform.offset[1])) + }); + ext_obj["rotation"] = tinygltf::Value(static_cast(transform.rotation)); + ext_obj["scale"] = tinygltf::Value(tinygltf::Value::Array{ + tinygltf::Value(static_cast(transform.scale[0])), + tinygltf::Value(static_cast(transform.scale[1])) + }); + if (transform.tex_coord != 0) { + ext_obj["texCoord"] = tinygltf::Value(transform.tex_coord); + } + + tinygltf::Value::Object texture_info; + texture_info["extensions"] = tinygltf::Value(tinygltf::Value::Object{ + {"KHR_texture_transform", tinygltf::Value(ext_obj)} + }); + + if (texture_key == "baseColorTexture") { + material.pbrMetallicRoughness.baseColorTexture.extensions["KHR_texture_transform"] = + tinygltf::Value(ext_obj); + } else if (texture_key == "normalTexture") { + material.normalTexture.extensions["KHR_texture_transform"] = tinygltf::Value(ext_obj); + } else if (texture_key == "emissiveTexture") { + material.emissiveTexture.extensions["KHR_texture_transform"] = tinygltf::Value(ext_obj); + } + + ext_mgr.use("KHR_texture_transform"); +} + +} +``` + +#### KHR_materials_pbrSpecularGlossiness + +```cpp +#pragma once +#include +#include "extension_manager.h" + +namespace gltf_writer::extensions { + +struct SpecularGlossiness { + std::array diffuse_factor = {1.0, 1.0, 1.0, 1.0}; + std::array specular_factor = {1.0, 1.0, 1.0}; + double glossiness_factor = 1.0; + + int diffuse_texture = -1; + int specular_glossiness_texture = -1; + + static SpecularGlossiness Default() { return {}; } + + static SpecularGlossiness FromDiffuse(const std::array& color) { + SpecularGlossiness sg; + sg.diffuse_factor = color; + return sg; + } + + static SpecularGlossiness FromSpecular(const std::array& specular, + double glossiness) { + SpecularGlossiness sg; + sg.specular_factor = specular; + sg.glossiness_factor = glossiness; + return sg; + } +}; + +inline void applySpecularGlossiness(tinygltf::Material& material, + const SpecularGlossiness& sg, + ExtensionManager& ext_mgr) { + tinygltf::Value::Object ext_obj; + + ext_obj["diffuseFactor"] = tinygltf::Value(tinygltf::Value::Array{ + tinygltf::Value(sg.diffuse_factor[0]), + tinygltf::Value(sg.diffuse_factor[1]), + tinygltf::Value(sg.diffuse_factor[2]), + tinygltf::Value(sg.diffuse_factor[3]) + }); + + ext_obj["specularFactor"] = tinygltf::Value(tinygltf::Value::Array{ + tinygltf::Value(sg.specular_factor[0]), + tinygltf::Value(sg.specular_factor[1]), + tinygltf::Value(sg.specular_factor[2]) + }); + + ext_obj["glossinessFactor"] = tinygltf::Value(sg.glossiness_factor); + + if (sg.diffuse_texture >= 0) { + tinygltf::Value::Object tex_info; + tex_info["index"] = tinygltf::Value(sg.diffuse_texture); + ext_obj["diffuseTexture"] = tinygltf::Value(tex_info); + } + + if (sg.specular_glossiness_texture >= 0) { + tinygltf::Value::Object tex_info; + tex_info["index"] = tinygltf::Value(sg.specular_glossiness_texture); + ext_obj["specularGlossinessTexture"] = tinygltf::Value(tex_info); + } + + material.extensions["KHR_materials_pbrSpecularGlossiness"] = tinygltf::Value(ext_obj); + ext_mgr.use("KHR_materials_pbrSpecularGlossiness"); +} + +} +``` + +#### KHR_materials_unlit(现有扩展封装) + +```cpp +#pragma once +#include "extension_manager.h" + +namespace gltf_writer::extensions { + +inline void applyUnlit(tinygltf::Material& material, ExtensionManager& ext_mgr) { + material.extensions["KHR_materials_unlit"] = tinygltf::Value(tinygltf::Value::Object()); + ext_mgr.use("KHR_materials_unlit"); +} + +} +``` + +#### KHR_draco_mesh_compression(现有扩展封装) + +```cpp +#pragma once +#include +#include "extension_manager.h" + +namespace gltf_writer::extensions { + +struct DracoCompression { + int buffer_view = -1; + std::unordered_map attributes; + + static DracoCompression FromBufferView(int bv, + const std::unordered_map& attrs) { + DracoCompression dc; + dc.buffer_view = bv; + dc.attributes = attrs; + return dc; + } +}; + +inline void applyDracoCompression(tinygltf::Primitive& primitive, + const DracoCompression& draco, + ExtensionManager& ext_mgr) { + tinygltf::Value::Object ext_obj; + ext_obj["bufferView"] = tinygltf::Value(draco.buffer_view); + + tinygltf::Value::Object attrs_obj; + for (const auto& [name, id] : draco.attributes) { + attrs_obj[name] = tinygltf::Value(id); + } + ext_obj["attributes"] = tinygltf::Value(attrs_obj); + + primitive.extensions["KHR_draco_mesh_compression"] = tinygltf::Value(ext_obj); + ext_mgr.useAndRequire("KHR_draco_mesh_compression"); +} + +} +``` + +#### KHR_texture_basisu(现有扩展封装) + +```cpp +#pragma once +#include "extension_manager.h" + +namespace gltf_writer::extensions { + +inline void applyBasisu(tinygltf::Texture& texture, int source_index, ExtensionManager& ext_mgr) { + tinygltf::Value::Object ext_obj; + ext_obj["source"] = tinygltf::Value(source_index); + texture.extensions["KHR_texture_basisu"] = tinygltf::Value(ext_obj); + texture.source = -1; + ext_mgr.useAndRequire("KHR_texture_basisu"); +} + +} +``` + +## 6. GltfBuilder 设计 + +### 6.1 配置结构 + +```cpp +#pragma once +#include +#include +#include "mesh_processor.h" + +namespace gltf_writer { + +struct GltfConfig { + bool enable_draco = false; + bool enable_unlit = false; + bool enable_texture_compress = false; + std::optional draco_params; + + static GltfConfig Default() { return {}; } + + static GltfConfig WithDraco(const DracoCompressionParams& params = {}) { + GltfConfig c; + c.enable_draco = true; + c.draco_params = params; + return c; + } + + static GltfConfig WithUnlit() { + GltfConfig c; + c.enable_unlit = true; + return c; + } + + static GltfConfig WithTextureCompress() { + GltfConfig c; + c.enable_texture_compress = true; + return c; + } +}; + +} +``` + +### 6.2 GltfBuilder 类 + +```cpp +#pragma once +#include +#include +#include "types.h" +#include "bounding_box.h" +#include "buffer_builder.h" +#include "accessor_builder.h" +#include "material_builder.h" +#include "texture_builder.h" +#include "mesh_builder.h" +#include "scene_builder.h" +#include "extension_manager.h" +#include "gltf_config.h" + +namespace gltf_writer { + +class GltfBuilder { +public: + explicit GltfBuilder(const GltfConfig& config = GltfConfig::Default()) + : config_(config) { + model_.asset.version = "2.0"; + model_.asset.generator = "gltf_writer"; + } + + GltfBuilder(const GltfBuilder&) = delete; + GltfBuilder& operator=(const GltfBuilder&) = delete; + GltfBuilder(GltfBuilder&&) noexcept = default; + GltfBuilder& operator=(GltfBuilder&&) noexcept = default; + + BufferBuilder& buffer() { return buffer_; } + AccessorBuilder& accessor() { return accessor_; } + MaterialBuilder& material() { return material_; } + TextureBuilder& texture() { return texture_; } + MeshBuilder& mesh() { return mesh_; } + SceneBuilder& scene() { return scene_; } + ExtensionManager& extensions() { return extensions_; } + + const BufferBuilder& buffer() const { return buffer_; } + const AccessorBuilder& accessor() const { return accessor_; } + const MaterialBuilder& material() const { return material_; } + const TextureBuilder& texture() const { return texture_; } + const MeshBuilder& mesh() const { return mesh_; } + const SceneBuilder& scene() const { return scene_; } + const ExtensionManager& extensions() const { return extensions_; } + + std::string toGLB() const { + tinygltf::Model model = finalizeModel(); + + tinygltf::TinyGLTF gltf; + std::ostringstream ss; + gltf.WriteGltfSceneToStream(&model, ss, false, true); + return ss.str(); + } + + tinygltf::Model toModel() const { + return finalizeModel(); + } + + tinygltf::Model& model() { return model_; } + const tinygltf::Model& model() const { return model_; } + + const GltfConfig& config() const { return config_; } + +private: + tinygltf::Model model_; + BufferBuilder buffer_; + AccessorBuilder accessor_; + MaterialBuilder material_; + TextureBuilder texture_; + MeshBuilder mesh_; + SceneBuilder scene_; + ExtensionManager extensions_; + GltfConfig config_; + + tinygltf::Model finalizeModel() const { + tinygltf::Model model = model_; + + model.buffers.push_back(buffer_.toTinyGltf()); + + const auto& bvs = buffer_.bufferViews(); + model.bufferViews.insert(model.bufferViews.end(), bvs.begin(), bvs.end()); + + const auto& accs = accessor_.accessors(); + model.accessors.insert(model.accessors.end(), accs.begin(), accs.end()); + + const auto& mats = material_.materials(); + model.materials.insert(model.materials.end(), mats.begin(), mats.end()); + + const auto& samplers = texture_.samplers(); + model.samplers.insert(model.samplers.end(), samplers.begin(), samplers.end()); + + const auto& images = texture_.images(); + model.images.insert(model.images.end(), images.begin(), images.end()); + + const auto& textures = texture_.textures(); + model.textures.insert(model.textures.end(), textures.begin(), textures.end()); + + const auto& meshes = mesh_.meshes(); + model.meshes.insert(model.meshes.end(), meshes.begin(), meshes.end()); + + const auto& nodes = scene_.nodes(); + model.nodes.insert(model.nodes.end(), nodes.begin(), nodes.end()); + + const auto& scenes = scene_.scenes(); + model.scenes.insert(model.scenes.end(), scenes.begin(), scenes.end()); + + if (scene_.defaultScene()) { + model.defaultScene = *scene_.defaultScene(); + } + + extensions_.apply(model); + + return model; + } +}; + +} +``` + +## 7. 实现优先级 + +### 7.1 优先级原则 + +1. **最小影响范围**:优先实现新增功能,不修改现有逻辑 +2. **可独立测试**:每个功能模块可独立验证 +3. **渐进式迁移**:先在 FBX Pipeline 试点,验证后再推广 + +### 7.2 Phase 1: FBX 新增 Extension 支持(P0) + +**目标**:为 FBX Pipeline 添加 `KHR_texture_transform` 和 `KHR_materials_pbrSpecularGlossiness` 支持 + +**影响范围**:仅影响 FBX Pipeline,不影响其他 Pipeline + +| 任务 | 风险 | 状态 | +|------|------|------| +| 实现 `ExtensionManager` | 低 | ✅ 已完成 | +| 实现 `KHR_texture_transform` 数据结构 | 低 | ✅ 已完成 | +| 实现 `KHR_materials_pbrSpecularGlossiness` 数据结构 | 低 | ✅ 已完成 | +| 在 `FBXPipeline` 中集成新 Extension | 中 | ✅ 已完成 | +| 单元测试 | 低 | ⏳ 待开始 | + +**验证方式**: +- 使用 FBX 测试文件生成 3D Tiles +- 使用 glTF Validator 验证输出合规性 +- 使用 CesiumJS 验证渲染效果 + +### 7.3 Phase 2: 底层抽象层(P1) + +**目标**:实现 `BufferBuilder`、`AccessorBuilder`、`BoundingBox` 等底层抽象 + +**影响范围**:新增模块,不修改现有代码 + +| 任务 | 风险 | +|------|------| +| 实现 `types.h` 类型定义 | 低 | +| 实现 `BoundingBox` 模板类 | 低 | +| 实现 `BufferBuilder` | 低 | +| 实现 `AccessorBuilder` | 低 | +| 实现 `MaterialBuilder` | 低 | +| 实现 `TextureBuilder` | 低 | +| 实现 `MeshBuilder` | 低 | +| 实现 `SceneBuilder` | 低 | +| 单元测试 | 低 | + +**验证方式**: +- 单元测试验证边界计算正确性 +- 对比新旧实现输出一致性 + +### 7.4 Phase 3: GltfBuilder 集成(P2) + +**目标**:实现 `GltfBuilder`,整合底层抽象和 Extension 管理 + +**影响范围**:新增模块,在 FBX Pipeline 中试点使用 + +| 任务 | 风险 | +|------|------| +| 实现 `GltfBuilder` 基础功能 | 中 | +| 迁移 FBX Pipeline 使用 `GltfBuilder` | 中 | +| 回归测试 | 中 | + +**验证方式**: +- 对比迁移前后输出文件二进制一致性 +- 完整回归测试所有 FBX 测试用例 + +### 7.5 Phase 4: 其他 Pipeline 迁移(P3) + +**目标**:将 `osgb23dtile.cpp` 和 `shp23dtile.cpp` 迁移到新架构 + +**影响范围**:修改现有 Pipeline 代码 + +| 任务 | 风险 | +|------|------| +| 迁移 `osgb23dtile.cpp` | 中 | +| 迁移 `shp23dtile.cpp` | 中 | +| 回归测试 | 中 | + +## 8. 文件组织 + +``` +src/ +├── gltf_writer/ +│ ├── types.h // 枚举、基础类型定义 +│ ├── bounding_box.h // 包围盒模板 +│ ├── buffer_builder.h // Buffer/BufferView 管理 +│ ├── accessor_builder.h // Accessor 管理 +│ ├── material_builder.h // Material 管理 +│ ├── texture_builder.h // Texture/Image/Sampler 管理 +│ ├── mesh_builder.h // Mesh/Primitive 管理 +│ ├── scene_builder.h // Node/Scene 管理 +│ ├── extension_manager.h // Extension 注册管理 +│ ├── gltf_config.h // 配置结构 +│ ├── gltf_builder.h // GltfBuilder 主类 +│ └── extensions/ +│ ├── texture_transform.h // KHR_texture_transform +│ ├── specular_glossiness.h // KHR_materials_pbrSpecularGlossiness +│ ├── unlit.h // KHR_materials_unlit +│ ├── draco.h // KHR_draco_mesh_compression +│ └── basisu.h // KHR_texture_basisu +├── coordinate_system.h // 现有:坐标系定义 +├── coordinate_transformer.h // 现有:坐标转换 +├── mesh_processor.h // 现有:网格处理 +└── ... +``` + +## 9. 使用示例 + +### 9.1 基础使用 + +```cpp +#include "gltf_writer/gltf_builder.h" +#include "gltf_writer/extensions/texture_transform.h" +#include "gltf_writer/extensions/specular_glossiness.h" + +void buildGltfMesh() { + gltf_writer::GltfConfig config = gltf_writer::GltfConfig::Default(); + gltf_writer::GltfBuilder builder(config); + + auto& buffer = builder.buffer(); + auto& accessor = builder.accessor(); + auto& material = builder.material(); + auto& mesh = builder.mesh(); + auto& scene = builder.scene(); + + std::vector positions = { /* ... */ }; + std::vector normals = { /* ... */ }; + std::vector indices = { /* ... */ }; + + int pos_acc = accessor.addVertexAttribute(buffer, + std::span(positions), + gltf_writer::AccessorType::Vec3, + gltf_writer::ComponentType::Float); + + int norm_acc = accessor.addVertexAttribute(buffer, + std::span(normals), + gltf_writer::AccessorType::Vec3, + gltf_writer::ComponentType::Float); + + int idx_acc = accessor.addIndices(buffer, + std::span(indices)); + + int mat_idx = material.addDefaultMaterial(); + + int mesh_idx = mesh.addTriangleMesh("mesh", pos_acc, norm_acc, -1, idx_acc, mat_idx); + + int node_idx = scene.addMeshNode(mesh_idx, "node"); + scene.createDefaultScene({node_idx}); + + std::string glb = builder.toGLB(); +} +``` + +### 9.2 FBX Pipeline 使用新 Extension + +```cpp +#include "gltf_writer/gltf_builder.h" +#include "gltf_writer/extensions/texture_transform.h" +#include "gltf_writer/extensions/specular_glossiness.h" + +void FBXPipeline::processMaterial(const FBXMaterial& fbx_mat) { + gltf_writer::MaterialParams params; + params.name = fbx_mat.name; + params.pbr.base_color_factor = {fbx_mat.diffuse.r, fbx_mat.diffuse.g, + fbx_mat.diffuse.b, fbx_mat.diffuse.a}; + + int mat_idx = builder_.material().addMaterial(params); + auto& mat = builder_.material().get(mat_idx); + + if (fbx_mat.use_specular_glossiness) { + gltf_writer::extensions::SpecularGlossiness sg; + sg.diffuse_factor = {fbx_mat.diffuse.r, fbx_mat.diffuse.g, + fbx_mat.diffuse.b, fbx_mat.diffuse.a}; + sg.specular_factor = {fbx_mat.specular.r, fbx_mat.specular.g, + fbx_mat.specular.b}; + sg.glossiness_factor = fbx_mat.glossiness; + + gltf_writer::extensions::applySpecularGlossiness( + mat, sg, builder_.extensions()); + } + + if (fbx_mat.has_texture_transform) { + gltf_writer::extensions::TextureTransform transform; + transform.offset = {fbx_mat.texture_offset_u, fbx_mat.texture_offset_v}; + transform.scale = {fbx_mat.texture_scale_u, fbx_mat.texture_scale_v}; + transform.rotation = fbx_mat.texture_rotation; + + gltf_writer::extensions::applyTextureTransform( + mat, "baseColorTexture", transform, builder_.extensions()); + } + + if (config_.enable_unlit) { + gltf_writer::extensions::applyUnlit(mat, builder_.extensions()); + } +} +``` + +### 9.3 Draco 压缩集成 + +```cpp +void buildDracoMesh() { + gltf_writer::GltfConfig config = gltf_writer::GltfConfig::WithDraco(); + gltf_writer::GltfBuilder builder(config); + + auto& buffer = builder.buffer(); + auto& accessor = builder.accessor(); + + std::vector positions = { /* ... */ }; + std::vector indices = { /* ... */ }; + + BoundingBox3F bbox(positions, 3); + + int pos_acc = accessor.addPlaceholder( + gltf_writer::AccessorType::Vec3, + gltf_writer::ComponentType::Float, + positions.size() / 3, + bbox.minAsDouble(), + bbox.maxAsDouble()); + + int idx_acc = accessor.addPlaceholder( + gltf_writer::AccessorType::Scalar, + gltf_writer::ComponentType::UnsignedInt, + indices.size()); + + std::vector draco_data; + int draco_pos_att, draco_idx_att; + compress_mesh_geometry(..., draco_data, ..., &draco_pos_att, ..., &draco_idx_att); + + int draco_bv = buffer.addBufferView( + std::span( + reinterpret_cast(draco_data.data()), + draco_data.size()), + gltf_writer::BufferViewTarget::None); + + auto& prim = builder.mesh().get(mesh_idx).primitives[0]; + gltf_writer::extensions::DracoCompression draco; + draco.buffer_view = draco_bv; + draco.attributes = {{"POSITION", draco_pos_att}, {"indices", draco_idx_att}}; + + gltf_writer::extensions::applyDracoCompression(prim, draco, builder.extensions()); +} +``` + +## 10. 参考 + +- glTF 2.0 Specification: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html +- KHR_texture_transform: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_transform +- KHR_materials_pbrSpecularGlossiness: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness +- KHR_draco_mesh_compression: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_draco_mesh_compression +- KHR_materials_unlit: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_unlit +- KHR_texture_basisu: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_basisu diff --git a/docs/output_directory_specification.md b/docs/output_directory_specification.md new file mode 100644 index 00000000..f086d290 --- /dev/null +++ b/docs/output_directory_specification.md @@ -0,0 +1,729 @@ +# 3D Tiles 输出目录规范 + +## 1. 概述 + +本文档定义了四叉树(Quadtree)和八叉树(Octree)切片策略生成B3DM和tileset.json的统一目录规范,采用 **shp23dtile.cpp 的 `tile/{z}/{x}/{y}/` 目录结构** 作为标准,确保两种策略的输出结构一致、可预测、易于管理。 + +## 2. 现状分析 + +### 2.1 shp23dtile 当前目录结构 (采用为标准) +``` +output/ +├── tileset.json # 根tileset +└── tile/ # 瓦片内容目录 + ├── {z}/ # 层级目录 + │ ├── {x}/ # X坐标目录 + │ │ ├── {y}/ # Y坐标目录 + │ │ │ ├── tileset.json # 子tileset + │ │ │ └── content.b3dm # B3DM内容 +``` + +**特点**: +- 使用 `tile/{z}/{x}/{y}/` 层级结构 +- 每个目录包含 `tileset.json` 和 `content.b3dm` +- 基于四叉树坐标 (z/x/y) +- 支持嵌套tileset + +### 2.2 FBXPipeline 当前目录结构 (需迁移) +``` +output/ +├── tileset.json # 根tileset +└── tile_0/ # 根节点内容 + ├── content.b3dm # B3DM内容 + ├── content_lod1.b3dm # LOD1 + └── content_lod2.b3dm # LOD2 +``` + +**特点**: +- 使用 `tile_{treePath}` 扁平结构 +- 使用 `treePath` (如 "0_1_2") 标识节点 +- 支持LOD文件命名 + +### 2.3 问题 +1. **目录结构不一致** - shp23dtile使用z/x/y,FBXPipeline使用treePath +2. **文件命名不统一** - content.b3dm vs content_lod{n}.b3dm +3. **层级组织混乱** - 没有统一的层级深度控制 + +## 3. 统一目录规范 (基于shp23dtile) + +### 3.1 规范设计原则 + +1. **采用shp23dtile结构** - 使用 `tile/{z}/{x}/{y}/` 作为标准 +2. **兼容四叉树和八叉树** - 通过坐标映射统一处理 +3. **层级可预测** - 目录深度与树深度对应 +4. **命名一致性** - 统一的文件命名约定 +5. **扩展性** - 支持LOD、多内容等扩展 + +### 3.2 统一目录结构 + +``` +output/ +├── tileset.json # 根tileset (必需) +└── tile/ # 瓦片内容根目录 + ├── {z}/ # 层级目录 (z=深度) + │ ├── {x}/ # X坐标目录 + │ │ ├── {y}/ # Y坐标目录 + │ │ │ ├── tileset.json # 子tileset + │ │ │ ├── content.b3dm # LOD0 (近距离,最高精度) + │ │ │ ├── content_lod1.b3dm # LOD1 (中距离,中等精度) + │ │ │ └── content_lod2.b3dm # LOD2 (远距离,最低精度) +``` + +**LOD说明**: +- `content.b3dm` = LOD0 (最高精度,近距离显示) +- `content_lod1.b3dm` = LOD1 (中等精度,中距离显示) +- `content_lod2.b3dm` = LOD2 (最低精度,远距离显示) +- LOD级别通过文件名后缀区分,z坐标保持空间层级含义 + +### 3.3 路径编码规则 + +#### 3.3.1 四叉树映射 (直接使用) +- **z** = 四叉树层级 +- **x** = X坐标 +- **y** = Y坐标 + +| 四叉树坐标 | 目录路径 | 说明 | +|-----------|---------|------| +| z=0, x=0, y=0 | tile/0/0/0 | 根节点 | +| z=5, x=16, y=8 | tile/5/16/8 | 第5层节点 | + +#### 3.3.2 八叉树映射 (转换为z/x/y) +- **z** = 八叉树深度 +- **x** = 节点索引 (深度1: 0-7, 深度2+: parentIndex*8 + nodeIndex) +- **y** = 子层级 (深度-2,用于区分不同层级的子节点) + +| 八叉树深度 | 节点路径 | 目录路径 | 说明 | +|-----------|---------|---------|------| +| 0 | - | tile/0/0/0 | 根节点 | +| 1 | 3 | tile/1/3/0 | 深度1,节点3 | +| 2 | 1→5 | tile/2/13/0 | x=1*8+5=13, y=0 | +| 3 | 1→5→2 | tile/3/106/1 | x=13*8+2=106, y=1 | + +**映射公式**: +```cpp +// 八叉树 (depth, nodeIndex, parentIndices) -> (z, x, y) +std::tuple octreeToZXY(int depth, int nodeIndex, + const std::vector& parentIndices) { + int z = depth; + + if (depth == 0) { + return {0, 0, 0}; + } + + if (depth == 1) { + return {1, nodeIndex, 0}; + } + + // 深度>=2 + int parentIndex = parentIndices.empty() ? 0 : parentIndices.back(); + int x = parentIndex * 8 + nodeIndex; + int y = depth - 2; + + return {z, x, y}; +} +``` + +#### 3.3.3 内容文件命名与LOD规范 + +**LOD文件命名规则**: +| 文件名 | LOD级别 | 适用场景 | geometricError | +|--------|---------|----------|----------------| +| `content.b3dm` | LOD0 | 近距离,最高精度 | 最小 | +| `content_lod1.b3dm` | LOD1 | 中距离,中等精度 | 中等 | +| `content_lod2.b3dm` | LOD2 | 远距离,最低精度 | 最大 | + +**注意**: LOD编号与精度一致,编号越小精度越高(与3D Tiles的geometricError递减一致) + +**类型变体**: `content_{type}.b3dm` (type=point, line, polygon等) + +### 3.4 LOD与tileset.json结构 + +#### 3.4.1 嵌套tileset结构(3D Tiles 1.0兼容) +每个节点目录包含独立的tileset.json,通过`children`引用子节点: + +``` +tile/0/0/0/tileset.json (根节点) +├── content.b3dm (LOD0) +├── content_lod1.b3dm (LOD1) +├── content_lod2.b3dm (LOD2) +└── 引用: tile/1/0/0/tileset.json (子节点) +``` + +#### 3.4.2 tileset.json示例 +```json +{ + "asset": { "version": "1.0" }, + "root": { + "boundingVolume": { "region": [...] }, + "geometricError": 10, + "content": { "uri": "content.b3dm" }, + "children": [ + { + "boundingVolume": { "region": [...] }, + "geometricError": 50, + "content": { "uri": "content_lod1.b3dm" } + }, + { + "boundingVolume": { "region": [...] }, + "geometricError": 100, + "content": { "uri": "content_lod2.b3dm" } + }, + { + "boundingVolume": { "region": [...] }, + "geometricError": 200, + "content": { "uri": "../1/0/0/tileset.json" } + } + ] + } +} +``` + +#### 3.4.3 geometricError计算 +- LOD0 (content.b3dm): `geometricError = baseError / 4` (最小) +- LOD1 (content_lod1.b3dm): `geometricError = baseError / 2` (中等) +- LOD2 (content_lod2.b3dm): `geometricError = baseError` (最大) +- 子节点: `geometricError = baseError * 2` (延迟加载) + +## 4. 实现方案 + +### 4.1 目录规范实现位置 + +**建议放在**: `spatial/builder/shp23dtile_path_generator.h` + +原因: +1. 属于builder模块的功能 +2. 与TilesetBuilder紧密配合 +3. 采用shp23dtile标准命名 +4. 可被所有切片策略复用 + +### 4.2 路径生成器接口 + +```cpp +#pragma once + +#include +#include +#include +#include +#include + +namespace spatial::builder { + +/** + * @brief shp23dtile风格路径配置 + */ +struct Shp23dtilePathConfig { + // 根输出目录 + std::string outputRoot; + + // 瓦片子目录名 + std::string tilesDir = "tile"; + + // 内容文件名 + std::string contentFilename = "content.b3dm"; + + // tileset文件名 + std::string tilesetFilename = "tileset.json"; + + // 最小层级 (该层级及以下的tileset放在根目录) + int minZForRoot = 0; +}; + +/** + * @brief shp23dtile风格路径生成器 + * + * 统一生成 tile/{z}/{x}/{y}/ 风格的路径 + * 兼容四叉树 (z/x/y) 和八叉树 (depth/nodeIndex) + */ +class Shp23dtilePathGenerator { +public: + explicit Shp23dtilePathGenerator(const Shp23dtilePathConfig& config); + + // ==================== 四叉树路径生成 ==================== + + /** + * @brief 获取四叉树节点目录 + * @param z 四叉树层级 + * @param x X坐标 + * @param y Y坐标 + * @return 节点目录绝对路径 + */ + std::filesystem::path getQuadtreeNodePath(int z, int x, int y) const; + + /** + * @brief 获取四叉树tileset路径 + */ + std::filesystem::path getQuadtreeTilesetPath(int z, int x, int y) const; + + /** + * @brief 获取四叉树内容路径 + */ + std::filesystem::path getQuadtreeContentPath(int z, int x, int y, + int lodLevel = -1) const; + + // ==================== 八叉树路径生成 ==================== + + /** + * @brief 获取八叉树节点目录 + * @param depth 八叉树深度 + * @param nodeIndex 节点索引 (0-7) + * @param parentIndices 父节点索引路径 (用于深度>1) + */ + std::filesystem::path getOctreeNodePath( + int depth, + int nodeIndex, + const std::vector& parentIndices = {} + ) const; + + /** + * @brief 获取八叉树tileset路径 + */ + std::filesystem::path getOctreeTilesetPath( + int depth, + int nodeIndex, + const std::vector& parentIndices = {} + ) const; + + /** + * @brief 获取八叉树内容路径 + */ + std::filesystem::path getOctreeContentPath( + int depth, + int nodeIndex, + const std::vector& parentIndices = {}, + int lodLevel = -1 + ) const; + + // ==================== 通用方法 ==================== + + /** + * @brief 获取根tileset路径 + */ + std::filesystem::path getRootTilesetPath() const; + + /** + * @brief 获取相对于根tileset的URI + */ + std::string getRelativeUri(const std::filesystem::path& absolutePath) const; + + /** + * @brief 创建目录结构 + */ + bool createDirectory(const std::filesystem::path& path) const; + + /** + * @brief 解析路径为四叉树坐标 + * @return std::optional> (z, x, y) + */ + std::optional> parseQuadtreePath( + const std::filesystem::path& path + ) const; + + /** + * @brief 解析路径为八叉树坐标 + * @return std::optional>> (depth, indices) + */ + std::optional>> parseOctreePath( + const std::filesystem::path& path + ) const; + +private: + Shp23dtilePathConfig config_; + std::filesystem::path tilesRoot_; + + // 将八叉树节点索引转换为x/y坐标 + std::pair octreeIndexToXY(int depth, int nodeIndex, + const std::vector& parentIndices) const; + + // 从x/y坐标解析八叉树节点索引 + std::pair> xyToOctreeIndex(int z, int x, int y) const; +}; + +} // namespace spatial::builder +``` + +### 4.3 实现代码 + +```cpp +// spatial/builder/shp23dtile_path_generator.cpp + +#include "shp23dtile_path_generator.h" +#include +#include + +namespace spatial::builder { + +Shp23dtilePathGenerator::Shp23dtilePathGenerator(const Shp23dtilePathConfig& config) + : config_(config) + , tilesRoot_(std::filesystem::path(config.outputRoot) / config.tilesDir) +{} + +// ==================== 四叉树路径生成 ==================== + +std::filesystem::path Shp23dtilePathGenerator::getQuadtreeNodePath(int z, int x, int y) const { + return tilesRoot_ / std::to_string(z) / std::to_string(x) / std::to_string(y); +} + +std::filesystem::path Shp23dtilePathGenerator::getQuadtreeTilesetPath(int z, int x, int y) const { + if (z <= config_.minZForRoot) { + return std::filesystem::path(config_.outputRoot) / config_.tilesetFilename; + } + return getQuadtreeNodePath(z, x, y) / config_.tilesetFilename; +} + +std::filesystem::path Shp23dtilePathGenerator::getQuadtreeContentPath(int z, int x, int y, + int lodLevel) const { + std::string filename = config_.contentFilename; + if (lodLevel >= 0) { + size_t dotPos = filename.find_last_of('.'); + if (dotPos != std::string::npos) { + filename = filename.substr(0, dotPos) + "_lod" + std::to_string(lodLevel) + + filename.substr(dotPos); + } + } + return getQuadtreeNodePath(z, x, y) / filename; +} + +// ==================== 八叉树路径生成 ==================== + +std::pair Shp23dtilePathGenerator::octreeIndexToXY( + int depth, int nodeIndex, const std::vector& parentIndices +) const { + if (depth == 0) { + return {0, 0}; + } + + if (depth == 1) { + return {nodeIndex, 0}; + } + + // 深度>=2: x = parentIndex * 8 + nodeIndex, y = depth - 2 + int parentIndex = parentIndices.empty() ? 0 : parentIndices.back(); + int x = parentIndex * 8 + nodeIndex; + int y = depth - 2; + + return {x, y}; +} + +std::filesystem::path Shp23dtilePathGenerator::getOctreeNodePath( + int depth, int nodeIndex, const std::vector& parentIndices +) const { + auto [x, y] = octreeIndexToXY(depth, nodeIndex, parentIndices); + return getQuadtreeNodePath(depth, x, y); +} + +std::filesystem::path Shp23dtilePathGenerator::getOctreeTilesetPath( + int depth, int nodeIndex, const std::vector& parentIndices +) const { + auto [x, y] = octreeIndexToXY(depth, nodeIndex, parentIndices); + return getQuadtreeTilesetPath(depth, x, y); +} + +std::filesystem::path Shp23dtilePathGenerator::getOctreeContentPath( + int depth, int nodeIndex, const std::vector& parentIndices, int lodLevel +) const { + auto [x, y] = octreeIndexToXY(depth, nodeIndex, parentIndices); + return getQuadtreeContentPath(depth, x, y, lodLevel); +} + +// ==================== 通用方法 ==================== + +std::filesystem::path Shp23dtilePathGenerator::getRootTilesetPath() const { + return std::filesystem::path(config_.outputRoot) / config_.tilesetFilename; +} + +std::string Shp23dtilePathGenerator::getRelativeUri( + const std::filesystem::path& absolutePath +) const { + std::filesystem::path rootDir = std::filesystem::path(config_.outputRoot); + return std::filesystem::relative(absolutePath, rootDir).generic_string(); +} + +bool Shp23dtilePathGenerator::createDirectory(const std::filesystem::path& path) const { + std::error_code ec; + std::filesystem::create_directories(path, ec); + return !ec; +} + +std::optional> Shp23dtilePathGenerator::parseQuadtreePath( + const std::filesystem::path& path +) const { + std::filesystem::path relPath = std::filesystem::relative(path, tilesRoot_); + + std::vector parts; + for (const auto& part : relPath) { + parts.push_back(part.string()); + } + + if (parts.size() >= 3) { + try { + int z = std::stoi(parts[0]); + int x = std::stoi(parts[1]); + int y = std::stoi(parts[2]); + return std::make_tuple(z, x, y); + } catch (...) { + return std::nullopt; + } + } + + return std::nullopt; +} + +std::optional>> Shp23dtilePathGenerator::parseOctreePath( + const std::filesystem::path& path +) const { + auto quadCoord = parseQuadtreePath(path); + if (!quadCoord) { + return std::nullopt; + } + + auto [z, x, y] = *quadCoord; + return xyToOctreeIndex(z, x, y); +} + +std::pair> Shp23dtilePathGenerator::xyToOctreeIndex( + int z, int x, int y +) const { + std::vector indices; + + if (z == 0) { + return {0, indices}; + } + + if (z == 1) { + indices.push_back(x); + return {1, indices}; + } + + // 深度>=2: 反向解析 + int depth = y + 2; + int remainingX = x; + + for (int d = depth; d > 0; --d) { + int nodeIndex = remainingX % 8; + indices.push_back(nodeIndex); + remainingX /= 8; + } + + std::reverse(indices.begin(), indices.end()); + return {depth, indices}; +} + +} // namespace spatial::builder +``` + +## 5. TilesetBuilder集成 + +### 5.1 更新TilesetBuildConfig + +```cpp +struct TilesetBuildConfig { + // ... 原有配置 ... + + // 输出路径配置 + Shp23dtilePathConfig pathConfig; + + // 或者使用默认配置 + std::optional customPathConfig; +}; +``` + +### 5.2 更新TilesetBuilder构建流程 + +```cpp +template +tileset::Tileset TilesetBuilder::build( + const StrategyType& strategy, + const TilesetBuildConfig& config +) { + // 创建shp23dtile风格路径生成器 + Shp23dtilePathConfig pathConfig = config.customPathConfig.value_or( + Shp23dtilePathConfig{config.outputPath} + ); + Shp23dtilePathGenerator pathGen(pathConfig); + + // 构建根Tile + tileset::Tile rootTile = buildTileRecursive( + strategy, + strategy.getRootNode(), + pathGen, + {}, // 空路径表示根 + true, // isRoot + config + ); + + // 创建Tileset + tileset::Tileset tileset(rootTile); + tileset.setVersion("1.0"); + tileset.setGltfUpAxis("Z"); + tileset.updateGeometricError(); + + return tileset; +} + +template +tileset::Tile TilesetBuilder::buildTileRecursive( + const StrategyType& strategy, + const void* node, + Shp23dtilePathGenerator& pathGen, + const std::vector& nodePath, + bool isRoot, + const TilesetBuildConfig& config +) { + // 获取节点信息 + auto bounds = strategy.getNodeBounds(node); + auto items = strategy.getNodeItems(node); + bool isLeaf = strategy.isLeafNode(node); + int depth = nodePath.size(); + + // 获取节点坐标 + int z, x, y; + std::filesystem::path nodeDir; + + if constexpr (StrategyType::Dimension == 2) { + // 四叉树: 直接获取z/x/y + auto* quadNode = static_cast(node); + z = quadNode->z; + x = quadNode->x; + y = quadNode->y; + nodeDir = pathGen.getQuadtreeNodePath(z, x, y); + } else { + // 八叉树: 使用深度和节点索引 + int nodeIndex = isRoot ? 0 : nodePath.back(); + std::vector parentIndices(nodePath.begin(), + nodePath.begin() + std::max(0, depth - 1)); + nodeDir = pathGen.getOctreeNodePath(depth, nodeIndex, parentIndices); + + auto [zx, xx, yx] = pathGen.octreeIndexToXY(depth, nodeIndex, parentIndices); + z = zx; x = xx; y = yx; + } + + // 创建目录 + pathGen.createDirectory(nodeDir); + + // 创建Tile + tileset::Tile tile; + tile.boundingVolume = bounds.toBoundingVolume(); + tile.geometricError = calculateGeometricError(bounds, depth); + + // 生成内容 + if (shouldGenerateContent(items, isLeaf, config)) { + std::filesystem::path contentPath = pathGen.getQuadtreeContentPath(z, x, y); + std::string contentUri = config.contentGenerator( + node, items, contentPath.string() + ); + + if (!contentUri.empty()) { + std::string relativeUri = pathGen.getRelativeUri(contentPath); + tile.setContent(relativeUri); + } + } + + // 递归构建子节点 + if (!isLeaf) { + auto childNodes = strategy.getChildNodes(node); + for (size_t i = 0; i < childNodes.size(); ++i) { + std::vector childPath = nodePath; + childPath.push_back(static_cast(i)); + + tileset::Tile childTile = buildTileRecursive( + strategy, childNodes[i], pathGen, childPath, false, config + ); + tile.addChild(std::move(childTile)); + } + } + + return tile; +} +``` + +## 6. 向后兼容 + +### 6.1 兼容shp23dtile旧路径 + +shp23dtile的 `tile/{z}/{x}/{y}/` 结构与新的统一规范完全一致,无需特殊处理。 + +### 6.2 兼容FBXPipeline旧路径 + +```cpp +// 提供兼容模式配置 (迁移期间使用) +Shp23dtilePathConfig getLegacyFBXConfig(const std::string& outputRoot) { + Shp23dtilePathConfig config; + config.outputRoot = outputRoot; + config.tilesDir = "tile"; // 使用tile目录 + config.contentFilename = "content.b3dm"; + return config; +} +``` + +## 7. 示例输出 + +### 7.1 四叉树输出 + +``` +/output/shapefile/ +├── tileset.json +└── tile/ + ├── 0/ + │ └── 0/ + │ └── 0/ + │ ├── tileset.json + │ └── content.b3dm + ├── 5/ + │ ├── 16/ + │ │ ├── 8/ + │ │ │ ├── tileset.json + │ │ │ └── content.b3dm + │ │ └── 9/ + │ │ ├── tileset.json + │ │ └── content.b3dm + │ └── 17/ + └── 6/ +``` + +### 7.2 八叉树输出 + +``` +/output/fbx/ +├── tileset.json +└── tile/ + ├── 0/ + │ └── 0/ + │ └── 0/ + │ ├── tileset.json + │ ├── content.b3dm + │ ├── content_lod1.b3dm + │ └── content_lod2.b3dm + ├── 1/ + │ ├── 0/ + │ │ └── 0/ + │ ├── 1/ + │ │ └── 0/ + │ ├── 2/ + │ ├── 3/ + │ ├── 4/ + │ ├── 5/ + │ ├── 6/ + │ └── 7/ + └── 2/ + ├── 0/ + │ ├── 0/ + │ ├── 1/ + │ ├── 2/ + │ └── 3/ + ├── 1/ + │ └── ... + └── ... +``` + +## 8. 总结 + +统一目录规范的关键点: + +1. **采用shp23dtile结构** - 使用 `tile/{z}/{x}/{y}/` 作为标准 +2. **四叉树直接使用** - z/x/y 对应四叉树坐标 +3. **八叉树映射** - 深度→z, 节点索引→x, 子层级→y +4. **文件命名统一** - `content.b3dm`, `content_lod{n}.b3dm` +5. **向后兼容** - 与现有shp23dtile输出完全兼容 + +该规范作为 `spatial/builder` 模块的一部分实现,与 `TilesetBuilder` 紧密集成。 diff --git a/docs/phase2_pipeline_unification_design.md b/docs/phase2_pipeline_unification_design.md new file mode 100644 index 00000000..1c3e610d --- /dev/null +++ b/docs/phase2_pipeline_unification_design.md @@ -0,0 +1,1656 @@ +# 第二阶段:统一管道架构重构方案 + +> **规范**: C++20, Google C++ Style Guide, C++ Core Guidelines +> **核心原则**: RAII, 零开销抽象, const 正确性, 智能指针优先 +> **重构策略**: 渐进式接口抽象(Strangler Fig Pattern) + +## 1. 目标 + +在第一阶段基础上,进一步完善抽象层设计,实现 Shapefile 和 FBX 处理流程的完全一致性。 + +## 2. 当前问题分析 + +### 2.1 现有实现的问题 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 当前架构 (第一阶段) │ +├─────────────────────────────────────────────────────────────┤ +│ ShapefilePipeline FBXPipeline │ +│ │ │ │ +│ ▼ ▼ │ +│ ShapefileProcessor FBXPipeline::run() │ +│ (四叉树内部实现) (八叉树内部实现) │ +│ │ │ │ +│ ▼ ▼ │ +│ B3DM生成 B3DM生成 │ +│ (内部实现不同) (内部实现不同) │ +└─────────────────────────────────────────────────────────────┘ + +问题: +1. 空间索引策略内聚在具体处理器中,无法复用 +2. B3DM 生成逻辑分散,缺乏统一接口 +3. 数据处理流程不一致,维护成本高 +``` + +### 2.2 目标架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 目标架构 (第二阶段) │ +├─────────────────────────────────────────────────────────────┤ +│ UnifiedPipeline │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ LoadData() │→ │BuildSpatial │→ │GenerateB3DM │→ ... │ +│ └─────────────┘ │ Index() │ │ Files() │ │ +│ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ DataSource (抽象) │ +│ ├─ ShapefileDataSource ──→ ShapefileDataPool │ +│ └─ FBXDataSource ────────→ FBXLoader │ +├─────────────────────────────────────────────────────────────┤ +│ SpatialIndex (抽象) │ +│ ├─ QuadtreeIndex ────────→ QuadtreeStrategy │ +│ └─ OctreeIndex ──────────→ OctreeStrategy │ +├─────────────────────────────────────────────────────────────┤ +│ IB3DMGenerator (抽象) │ +│ ├─ ShapefileB3DMGenerator ─→ B3DMContentGenerator │ +│ └─ FBXB3DMGenerator ───────→ B3DMGenerator │ +├─────────────────────────────────────────────────────────────┤ +│ ITilesetBuilder (抽象) │ +│ └─ StandardTilesetBuilder ─→ common::TilesetBuilder │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 3. 核心设计 + +### 3.1 统一处理流程 + +```cpp +// 模板方法模式 - C++20 概念约束 +class ConversionPipeline { + public: + virtual ~ConversionPipeline() = default; + + // 禁止拷贝,允许移动 + ConversionPipeline(const ConversionPipeline&) = delete; + ConversionPipeline& operator=(const ConversionPipeline&) = delete; + ConversionPipeline(ConversionPipeline&&) = default; + ConversionPipeline& operator=(ConversionPipeline&&) = default; + + // 模板方法 - 统一处理流程 + [[nodiscard]] auto Convert(const ConversionParams& params) -> ConversionResult { + // 1. 加载数据 + auto data_source = LoadData(params); + if (!data_source) { + return ConversionResult{.success = false, + .error_message = "Failed to load data"}; + } + + // 2. 构建空间索引 + auto spatial_index = BuildSpatialIndex(data_source.get(), params); + if (!spatial_index) { + return ConversionResult{.success = false, + .error_message = "Failed to build spatial index"}; + } + + // 3. 生成 B3DM 文件 + auto b3dm_files = GenerateB3DMFiles(data_source.get(), + spatial_index.get(), params); + if (b3dm_files.empty()) { + return ConversionResult{.success = false, + .error_message = "No B3DM files generated"}; + } + + // 4. 生成 Tileset + auto tileset_path = GenerateTileset(spatial_index.get(), + b3dm_files, params); + if (tileset_path.empty()) { + return ConversionResult{.success = false, + .error_message = "Failed to generate tileset"}; + } + + return ConversionResult{ + .success = true, + .node_count = static_cast(spatial_index->GetNodeCount()), + .b3dm_count = static_cast(b3dm_files.size()), + .tileset_path = std::move(tileset_path) + }; + } + + protected: + ConversionPipeline() = default; + + // 纯虚函数 - 子类实现 + [[nodiscard]] virtual auto LoadData(const ConversionParams& params) + -> std::unique_ptr = 0; + + [[nodiscard]] virtual auto BuildSpatialIndex(DataSource* source, + const ConversionParams& params) + -> std::unique_ptr = 0; + + [[nodiscard]] virtual auto GenerateB3DMFiles( + DataSource* source, + SpatialIndex* index, + const ConversionParams& params) -> std::vector = 0; + + [[nodiscard]] virtual auto GenerateTileset( + SpatialIndex* index, + const std::vector& files, + const ConversionParams& params) -> std::filesystem::path = 0; +}; +``` + +### 3.2 DataSource 抽象接口 + +```cpp +// pipeline/data_source.h +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace pipeline { + +// 前向声明 +class ISpatialItem; +using SpatialItemPtr = std::shared_ptr; +using SpatialItemList = std::vector; + +// 数据源配置 - 使用聚合初始化 +struct DataSourceConfig { + std::filesystem::path input_path; + std::filesystem::path output_path; + + // 地理参考 + double center_longitude = 0.0; + double center_latitude = 0.0; + double center_height = 0.0; + + // Shapefile 特定 + std::string height_field; + + // 处理选项 + bool enable_simplification = false; + bool enable_draco = false; + bool enable_lod = false; +}; + +// 空间项接口 - 统一表示几何对象 +class ISpatialItem { + public: + virtual ~ISpatialItem() = default; + + // 禁止拷贝,允许移动 + ISpatialItem(const ISpatialItem&) = delete; + ISpatialItem& operator=(const ISpatialItem&) = delete; + ISpatialItem(ISpatialItem&&) = default; + ISpatialItem& operator=(ISpatialItem&&) = default; + + // 获取唯一ID + [[nodiscard]] virtual auto GetId() const -> uint64_t = 0; + + // 获取包围盒 (WGS84 经纬度/高度) + [[nodiscard]] virtual auto GetBounds() const + -> std::tuple = 0; + + // 获取几何数据 (OSG 格式) + [[nodiscard]] virtual auto GetGeometry() const + -> osg::ref_ptr = 0; + + // 获取属性数据 + [[nodiscard]] virtual auto GetProperties() const + -> std::unordered_map = 0; + + protected: + ISpatialItem() = default; +}; + +// 数据源接口 +class DataSource { + public: + virtual ~DataSource() = default; + + // 禁止拷贝,允许移动 + DataSource(const DataSource&) = delete; + DataSource& operator=(const DataSource&) = delete; + DataSource(DataSource&&) = default; + DataSource& operator=(DataSource&&) = default; + + // 加载数据 + [[nodiscard]] virtual auto Load(const DataSourceConfig& config) -> bool = 0; + + // 获取空间项列表 + [[nodiscard]] virtual auto GetSpatialItems() const -> SpatialItemList = 0; + + // 获取世界包围盒 + [[nodiscard]] virtual auto GetWorldBounds() const + -> std::tuple = 0; + + // 获取地理参考 + [[nodiscard]] virtual auto GetGeoReference() const + -> std::tuple = 0; + + // 获取数据项数量 + [[nodiscard]] virtual auto GetItemCount() const noexcept -> std::size_t = 0; + + // 是否已加载 + [[nodiscard]] virtual auto IsLoaded() const noexcept -> bool = 0; + + protected: + DataSource() = default; +}; + +using DataSourcePtr = std::unique_ptr; +using DataSourceCreator = std::function; + +// 数据源工厂 - 单例注册模式 +class DataSourceFactory { + public: + [[nodiscard]] static auto Instance() noexcept -> DataSourceFactory&; + + void Register(std::string_view type, DataSourceCreator creator); + [[nodiscard]] auto Create(std::string_view type) const -> DataSourcePtr; + [[nodiscard]] auto IsRegistered(std::string_view type) const noexcept -> bool; + + private: + DataSourceFactory() = default; + ~DataSourceFactory() = default; + + std::unordered_map creators_; +}; + +// 数据源注册辅助宏 +#define REGISTER_DATA_SOURCE(TYPE, CLASS) \ + namespace { \ + [[maybe_unused]] const bool _##CLASS##_registered = []() -> bool { \ + ::pipeline::DataSourceFactory::Instance().Register( \ + TYPE, []() -> ::pipeline::DataSourcePtr { \ + return std::make_unique(); \ + }); \ + return true; \ + }(); \ + } + +} // namespace pipeline +``` + +### 3.3 SpatialIndexStrategy 抽象 + +```cpp +// pipeline/spatial_index_strategy.h +#pragma once + +#include +#include +#include +#include + +namespace pipeline { + +// 前向声明 +class ISpatialItem; +using SpatialItemPtr = std::shared_ptr; +using SpatialItemList = std::vector; + +// 空间索引节点接口 +class ISpatialIndexNode { + public: + virtual ~ISpatialIndexNode() = default; + + // 禁止拷贝,允许移动 + ISpatialIndexNode(const ISpatialIndexNode&) = delete; + ISpatialIndexNode& operator=(const ISpatialIndexNode&) = delete; + ISpatialIndexNode(ISpatialIndexNode&&) = default; + ISpatialIndexNode& operator=(ISpatialIndexNode&&) = default; + + // 获取节点ID + [[nodiscard]] virtual auto GetId() const -> uint64_t = 0; + + // 获取深度 + [[nodiscard]] virtual auto GetDepth() const -> int = 0; + + // 获取包围盒 + [[nodiscard]] virtual auto GetBounds() const + -> std::tuple = 0; + + // 是否是叶子节点 + [[nodiscard]] virtual auto IsLeaf() const -> bool = 0; + + // 获取子节点 + [[nodiscard]] virtual auto GetChildren() const + -> std::vector = 0; + + // 获取包含的空间项ID + [[nodiscard]] virtual auto GetItemIds() const -> std::vector = 0; + + protected: + ISpatialIndexNode() = default; +}; + +// 空间索引配置 +struct SpatialIndexConfig { + int max_depth = 5; + std::size_t max_items_per_node = 1000; +}; + +// 空间索引接口 +class SpatialIndex { + public: + virtual ~SpatialIndex() = default; + + // 禁止拷贝,允许移动 + SpatialIndex(const SpatialIndex&) = delete; + SpatialIndex& operator=(const SpatialIndex&) = delete; + SpatialIndex(SpatialIndex&&) = default; + SpatialIndex& operator=(SpatialIndex&&) = default; + + // 构建索引 + [[nodiscard]] virtual auto Build(const SpatialItemList& items, + const SpatialIndexConfig& config) -> bool = 0; + + // 获取根节点 + [[nodiscard]] virtual auto GetRoot() const -> const ISpatialIndexNode* = 0; + + // 遍历所有叶子节点 + virtual auto ForEachLeaf( + const std::function& callback) const -> void = 0; + + // 获取节点数量 + [[nodiscard]] virtual auto GetNodeCount() const -> std::size_t = 0; + + // 获取叶子节点数量 + [[nodiscard]] virtual auto GetLeafCount() const -> std::size_t = 0; + + protected: + SpatialIndex() = default; +}; + +using SpatialIndexPtr = std::unique_ptr; +using SpatialIndexCreator = std::function; + +// 空间索引策略类型 +enum class SpatialStrategyType : std::uint8_t { + kQuadtree = 0, + kOctree = 1 +}; + +// 空间索引工厂 +class SpatialIndexFactory { + public: + [[nodiscard]] static auto Instance() noexcept -> SpatialIndexFactory&; + + void Register(SpatialStrategyType type, SpatialIndexCreator creator); + [[nodiscard]] auto Create(SpatialStrategyType type) const -> SpatialIndexPtr; + + private: + SpatialIndexFactory() = default; + ~SpatialIndexFactory() = default; + + std::unordered_map creators_; +}; + +#define REGISTER_SPATIAL_STRATEGY(TYPE, CLASS) \ + namespace { \ + [[maybe_unused]] const bool _##CLASS##_registered = []() -> bool { \ + ::pipeline::SpatialIndexFactory::Instance().Register( \ + TYPE, []() -> ::pipeline::SpatialIndexPtr { \ + return std::make_unique(); \ + }); \ + return true; \ + }(); \ + } + +} // namespace pipeline +``` + +### 3.4 B3DMGenerator 统一接口 + +```cpp +// pipeline/b3dm_generator.h +#pragma once + +#include +#include +#include +#include + +namespace pipeline { + +// 前向声明 +class ISpatialIndexNode; +class ISpatialItem; +using SpatialItemPtr = std::shared_ptr; +using SpatialItemList = std::vector; + +// B3DM 生成配置 - 使用聚合初始化 +struct B3DMGenerationConfig { + std::filesystem::path output_directory; + + // 地理参考 + double center_longitude = 0.0; + double center_latitude = 0.0; + double center_height = 0.0; + + // 压缩选项 + bool enable_draco = false; + bool enable_texture_compress = false; + bool enable_meshopt = false; + + // LOD 选项 + bool enable_lod = false; + std::vector lod_ratios = {1.0f, 0.5f, 0.25f}; +}; + +// B3DM 文件信息 +struct B3DMFile { + std::filesystem::path filepath; + std::filesystem::path relative_path; + uint64_t node_id = 0; + int lod_level = 0; + std::size_t file_size = 0; +}; + +// B3DM 生成器接口 +class IB3DMGenerator { + public: + virtual ~IB3DMGenerator() = default; + + // 禁止拷贝,允许移动 + IB3DMGenerator(const IB3DMGenerator&) = delete; + IB3DMGenerator& operator=(const IB3DMGenerator&) = delete; + IB3DMGenerator(IB3DMGenerator&&) = default; + IB3DMGenerator& operator=(IB3DMGenerator&&) = default; + + // 初始化 + [[nodiscard]] virtual auto Initialize(const B3DMGenerationConfig& config) -> bool = 0; + + // 为单个节点生成 B3DM + [[nodiscard]] virtual auto GenerateForNode( + const ISpatialIndexNode* node, + const SpatialItemList& items, + std::string_view filename) -> B3DMFile = 0; + + // 批量生成(支持 LOD) + [[nodiscard]] virtual auto GenerateWithLOD( + const ISpatialIndexNode* node, + const SpatialItemList& items, + std::string_view base_filename) -> std::vector = 0; + + protected: + IB3DMGenerator() = default; +}; + +using B3DMGeneratorPtr = std::unique_ptr; + +} // namespace pipeline +``` + +### 3.5 TilesetBuilder 统一接口 + +```cpp +// pipeline/tileset_builder.h +#pragma once + +#include "spatial_index_strategy.h" +#include "b3dm_generator.h" +#include + +namespace pipeline { + +// Tileset 节点元数据 +struct TileNodeMetadata { + uint64_t node_id = 0; + uint64_t parent_id = 0; + int depth = 0; + + // 包围盒 + double bbox_min[3] = {0, 0, 0}; + double bbox_max[3] = {0, 0, 0}; + + // 几何误差 + double geometric_error = 0.0; + + // 内容 + std::vector content_uris; + bool has_content = false; + + // 子节点 + std::vector children_ids; + bool is_leaf = false; +}; + +using TileNodeMetadataMap = std::unordered_map; + +// Tileset 配置 +struct TilesetBuilderConfig { + std::filesystem::path output_path; + + // 地理参考 + double center_longitude = 0.0; + double center_latitude = 0.0; + double center_height = 0.0; + + // 几何误差计算 + double geometric_error_scale = 0.5; + double geometric_error_base = 1.0; + + // 包围盒扩展 + double bounding_volume_scale = 1.0; + + // LOD + bool enable_lod = false; + int lod_level_count = 3; +}; + +// Tileset 构建器接口 +class ITilesetBuilder { + public: + virtual ~ITilesetBuilder() = default; + + // 初始化 + virtual bool Initialize(const TilesetBuilderConfig& config) = 0; + + // 添加节点元数据 + virtual void AddNodeMetadata(const TileNodeMetadata& metadata) = 0; + + // 构建并写入 Tileset + virtual bool BuildAndWrite( + const SpatialIndex* index, + const std::vector& b3dm_files + ) = 0; + + // 获取生成的 tileset.json 路径 + virtual std::filesystem::path GetTilesetPath() const = 0; +}; + +using TilesetBuilderPtr = std::unique_ptr; + +} // namespace pipeline +``` + +### 3.6 统一管道实现 + +```cpp +// pipeline/unified_pipeline.h +#pragma once + +#include "conversion_pipeline.h" +#include "spatial_index_strategy.h" + +namespace pipeline { + +// 统一管道配置 +struct UnifiedPipelineConfig { + std::string source_type; // "shapefile" or "fbx" + DataSourceConfig data_source_config; + SpatialIndexConfig spatial_index_config; + B3DMGenerationConfig b3dm_generator_config; + TilesetBuilderConfig tileset_builder_config; +}; + +// 统一管道 - 使用策略模式处理不同数据源 +class UnifiedPipeline : public ConversionPipeline { + public: + explicit UnifiedPipeline(const UnifiedPipelineConfig& config); + ~UnifiedPipeline() override; + + // 禁止拷贝,允许移动 + UnifiedPipeline(const UnifiedPipeline&) = delete; + UnifiedPipeline& operator=(const UnifiedPipeline&) = delete; + UnifiedPipeline(UnifiedPipeline&&) = default; + UnifiedPipeline& operator=(UnifiedPipeline&&) = default; + + // 执行完整转换流程 + ConversionResult Execute(); + + // 分步执行(用于调试) + bool LoadData(); + bool BuildSpatialIndex(); + bool GenerateB3DMFiles(); + bool BuildTileset(); + + protected: + [[nodiscard]] auto LoadData(const ConversionParams& params) + -> std::unique_ptr override; + + [[nodiscard]] auto BuildSpatialIndex(DataSource* source, + const ConversionParams& params) + -> std::unique_ptr override; + + [[nodiscard]] auto GenerateB3DMFiles(DataSource* source, + SpatialIndex* index, + const ConversionParams& params) + -> std::vector override; + + [[nodiscard]] auto GenerateTileset(SpatialIndex* index, + const std::vector& files, + const ConversionParams& params) + -> std::filesystem::path override; + + private: + UnifiedPipelineConfig config_; + + // 组件 + DataSourcePtr dataSource_; + SpatialIndexPtr spatialIndex_; + B3DMGeneratorPtr b3dmGenerator_; + TilesetBuilderPtr tilesetBuilder_; + + // 执行状态 + std::vector spatialItems_; + std::vector b3dmFiles_; + + // 工厂方法 + DataSourcePtr CreateDataSource(); + SpatialIndexPtr CreateSpatialIndex(); + B3DMGeneratorPtr CreateB3DMGenerator(); + TilesetBuilderPtr CreateTilesetBuilder(); +}; + +} // namespace pipeline +``` + +## 4. 渐进式重构步骤 + +采用 **Strangler Fig Pattern(绞杀者模式)**: +1. 每次只抽象一个接口 +2. 新接口包装旧实现(适配器模式) +3. 验证通过后再抽象下一个接口 +4. 最终达到统一架构 + +### 步骤 1:抽象 DataSource 接口 + +#### 4.1.1 Shapefile 数据源实现(包装现有代码) + +```cpp +// pipeline/adapters/shapefile/shapefile_data_source.h +#pragma once + +#include "../../data_source.h" +#include "../../../shapefile/shapefile_data_pool.h" + +namespace pipeline { +namespace adapters { + +// Shapefile 空间项适配器 +class ShapefileSpatialItemAdapter : public ISpatialItem { +public: + explicit ShapefileSpatialItemAdapter(shapefile::ShapefileDataPool::ItemPtr item) + : item_(item) {} + + uint64_t GetId() const override { + return static_cast(item_->featureId); + } + + void GetBounds(double& minX, double& minY, double& minZ, + double& maxX, double& maxY, double& maxZ) const override { + minX = item_->bounds.minx; + minY = item_->bounds.miny; + minZ = item_->bounds.minHeight; + maxX = item_->bounds.maxx; + maxY = item_->bounds.maxy; + maxZ = item_->bounds.maxHeight; + } + + osg::ref_ptr GetGeometry() const override { + if (item_->geometries.empty()) return nullptr; + if (item_->geometries.size() == 1) return item_->geometries[0]; + // 多个几何体合并逻辑... + return osg::ref_ptr(); + } + + std::map GetProperties() const override { + std::map props; + for (const auto& [key, value] : item_->properties) { + props[key] = value.dump(); + } + return props; + } + + const shapefile::ShapefileSpatialItem* GetOriginalItem() const { + return item_.get(); + } + +private: + shapefile::ShapefileDataPool::ItemPtr item_; +}; + +// Shapefile 数据源 +class ShapefileDataSource : public DataSource { +public: + ShapefileDataSource() = default; + + bool Load(const DataSourceConfig& config) override { + dataPool_ = std::make_unique(); + + // 完全复用现有的加载逻辑 + bool result = dataPool_->loadFromShapefileWithGeometry( + config.input_path.string(), + config.height_field, + config.center_longitude, + config.center_latitude + ); + + if (result) { + // 重新计算中心点(复用现有逻辑) + auto worldBounds = dataPool_->computeWorldBounds(); + double centerLon = (worldBounds.minx + worldBounds.maxx) * 0.5; + double centerLat = (worldBounds.miny + worldBounds.maxy) * 0.5; + + if (std::abs(centerLon - config.center_longitude) > 1.0 || + std::abs(centerLat - config.center_latitude) > 1.0) { + dataPool_->clear(); + result = dataPool_->loadFromShapefileWithGeometry( + config.input_path.string(), + config.height_field, + centerLon, + centerLat + ); + } + } + + // 转换为适配器列表 + if (result) { + items_.clear(); + for (const auto& item : dataPool_->getAllItems()) { + items_.push_back(std::make_shared(item)); + } + } + + return result; + } + + SpatialItemList GetSpatialItems() const override { return items_; } + + void GetWorldBounds(double& minX, double& minY, double& minZ, + double& maxX, double& maxY, double& maxZ) const override { + auto bounds = dataPool_->computeWorldBounds(); + minX = bounds.minx; minY = bounds.miny; minZ = bounds.minHeight; + maxX = bounds.maxx; maxY = bounds.maxy; maxZ = bounds.maxHeight; + } + + size_t GetItemCount() const override { return items_.size(); } + bool IsLoaded() const override { return dataPool_ && !items_.empty(); } + + shapefile::ShapefileDataPool* GetOriginalPool() const { + return dataPool_.get(); + } + +private: + std::unique_ptr dataPool_; + SpatialItemList items_; +}; + +} // namespace adapters +} // namespace pipeline +``` + +#### 4.1.2 FBX 数据源实现(包装现有代码) + +```cpp +// pipeline/adapters/fbx/fbx_data_source.h +#pragma once + +#include "../../data_source.h" +#include "../../../fbx.h" +#include "../../../fbx/fbx_spatial_item_adapter.h" + +namespace pipeline { +namespace adapters { + +// FBX 空间项适配器 +class FBXSpatialItemAdapter : public ISpatialItem { +public: + explicit FBXSpatialItemAdapter(fbx::FBXSpatialItemPtr item) + : item_(item) {} + + uint64_t GetId() const override { + return reinterpret_cast(item_->getMeshInfo()); + } + + void GetBounds(double& minX, double& minY, double& minZ, + double& maxX, double& maxY, double& maxZ) const override { + auto bounds = item_->getBounds(); + auto min = bounds.min(); + auto max = bounds.max(); + minX = min[0]; minY = min[1]; minZ = min[2]; + maxX = max[0]; maxY = max[1]; maxZ = max[2]; + } + + osg::ref_ptr GetGeometry() const override { + return osg::ref_ptr( + const_cast(item_->getGeometry()) + ); + } + + std::map GetProperties() const override { + std::map props; + props["nodeName"] = item_->getNodeName(); + return props; + } + + fbx::FBXSpatialItemPtr GetOriginalItem() const { return item_; } + +private: + fbx::FBXSpatialItemPtr item_; +}; + +// FBX 数据源 +class FBXDataSource : public DataSource { +public: + FBXDataSource() = default; + + bool Load(const DataSourceConfig& config) override { + loader_ = std::make_unique(config.input_path.string()); + + if (!loader_->load()) { + return false; + } + + auto fbxItems = fbx::createSpatialItems(loader_.get()); + + items_.clear(); + for (const auto& item : fbxItems) { + items_.push_back(std::make_shared(item)); + } + + return !items_.empty(); + } + + SpatialItemList GetSpatialItems() const override { return items_; } + + void GetWorldBounds(double& minX, double& minY, double& minZ, + double& maxX, double& maxY, double& maxZ) const override { + if (items_.empty()) return; + minX = minY = minZ = std::numeric_limits::max(); + maxX = maxY = maxZ = std::numeric_limits::lowest(); + + for (const auto& item : items_) { + double ixmin, iymin, izmin, ixmax, iymax, izmax; + item->GetBounds(ixmin, iymin, izmin, ixmax, iymax, izmax); + minX = std::min(minX, ixmin); + minY = std::min(minY, iymin); + minZ = std::min(minZ, izmin); + maxX = std::max(maxX, ixmax); + maxY = std::max(maxY, iymax); + maxZ = std::max(maxZ, izmax); + } + } + + size_t GetItemCount() const override { return items_.size(); } + bool IsLoaded() const override { return loader_ && !items_.empty(); } + + FBXLoader* GetOriginalLoader() const { return loader_.get(); } + +private: + std::unique_ptr loader_; + SpatialItemList items_; +}; + +} // namespace adapters +} // namespace pipeline +``` + +#### 4.1.3 修改 ShapefileProcessor 使用新接口 + +```cpp +// shapefile/shapefile_processor.h(步骤1后) +class ShapefileProcessor { +public: + // 新增:使用外部数据源 + void SetDataSource(std::shared_ptr dataSource) { + externalDataSource_ = dataSource; + } + +private: + std::shared_ptr externalDataSource_; + std::unique_ptr internalDataPool_; +}; +``` + +#### 4.1.4 验证步骤 1 + +```cpp +TEST(Step1_DataSource, ShapefileProcessorWithNewDataSource) { + shapefile::ShapefileProcessorConfig config; + config.inputPath = "tests/data/sample.shp"; + config.outputPath = "tests/output/step1_shapefile"; + config.heightField = "height"; + + shapefile::ShapefileProcessor processor(config); + + // 使用新的数据源 + auto dataSource = std::make_unique(); + processor.SetDataSource(dataSource); + + auto result = processor.process(); + EXPECT_TRUE(result.success); + + // 对比基准数据 + CompareWithBenchmark("tests/output/step1_shapefile", "tests/reference/shapefile"); +} +``` + +### 步骤 2:抽象 SpatialIndex 接口 + +#### 4.2.1 四叉树实现(包装现有代码) + +```cpp +// pipeline/adapters/spatial/quadtree_index.h +#pragma once + +#include "../../spatial_index_strategy.h" +#include "../../../spatial/strategy/quadtree_strategy.h" + +namespace pipeline { +namespace adapters { + +class QuadtreeNodeAdapter : public ISpatialIndexNode { +public: + explicit QuadtreeNodeAdapter(const spatial::strategy::QuadtreeNode* node, uint64_t id) + : node_(node), id_(id) {} + + uint64_t GetId() const override { return id_; } + int GetDepth() const override { return static_cast(node_->getDepth()); } + + void GetBounds(double& minX, double& minY, double& minZ, + double& maxX, double& maxY, double& maxZ) const override { + auto bounds = node_->getBounds(); + auto min = bounds.min(); + auto max = bounds.max(); + minX = min[0]; minY = min[1]; minZ = min[2]; + maxX = max[0]; maxY = max[1]; maxZ = max[2]; + } + + bool IsLeaf() const override { return node_->isLeaf(); } + std::vector GetChildren() const override; + std::vector GetItemIds() const override; + size_t GetItemCount() const override { return node_->getItemCount(); } + + const spatial::strategy::QuadtreeNode* GetOriginalNode() const { return node_; } + +private: + const spatial::strategy::QuadtreeNode* node_; + uint64_t id_; +}; + +class QuadtreeIndex : public SpatialIndex { +public: + QuadtreeIndex() = default; + + bool Build(const SpatialItemList& items, const SpatialIndexConfig& config) override { + // 转换为现有接口需要的格式 + spatial::core::SpatialItemList spatialItems; + for (const auto& item : items) { + // 类型转换... + } + + spatial::strategy::QuadtreeStrategy strategy; + spatial::strategy::QuadtreeConfig qtConfig; + qtConfig.maxDepth = config.max_depth; + qtConfig.maxItemsPerNode = config.max_items_per_node; + + index_ = strategy.buildIndex(spatialItems, worldBounds, qtConfig); + BuildNodeMap(); + + return index_ != nullptr; + } + + const ISpatialIndexNode* GetRoot() const override { + if (!index_) return nullptr; + return GetNodeAdapter(index_->getRootNode()); + } + + void ForEachLeaf(const std::function& callback) const override; + size_t GetNodeCount() const override; + size_t GetLeafCount() const override; + +private: + std::unique_ptr index_; + std::unordered_map> nodeAdapters_; + uint64_t nextNodeId_ = 0; + + void BuildNodeMap(); + const QuadtreeNodeAdapter* GetNodeAdapter(const spatial::strategy::QuadtreeNode* node) const; +}; + +} // namespace adapters +} // namespace pipeline +``` + +### 步骤 3:抽象 B3DMGenerator 接口 + +#### 4.3.1 现有实现差异分析 + +| 组件 | Shapefile | FBX | 统一策略 | +|------|-----------|-----|---------| +| 入口类 | `B3DMContentGenerator` | `B3DMGenerator` | 统一为 `IB3DMGenerator` | +| 输入类型 | `vector` | `SpatialItemRefList` | 统一为 `ISpatialItem` 列表 | +| LOD 生成 | `generateLODFiles()` | `generateLODFiles()` | 统一接口 | +| 几何提取 | `GeometryExtractor` | `FBXGeometryExtractor` | 通过 `ISpatialItem::GetGeometry()` | +| 坐标转换 | ENU 转换在数据加载时完成 | ENU 转换在 B3DM 生成时完成 | 统一在 DataSource 层完成 | + +#### 4.3.2 Shapefile B3DM 生成器实现 + +```cpp +// pipeline/adapters/shapefile/shapefile_b3dm_generator.h +#pragma once + +#include "../../b3dm_generator.h" +#include "../../../shapefile/b3dm_content_generator.h" + +namespace pipeline { +namespace adapters { + +class ShapefileB3DMGenerator : public IB3DMGenerator { +public: + ShapefileB3DMGenerator() = default; + + bool Initialize(const B3DMGenerationConfig& config) override { + config_ = config; + generator_ = std::make_unique( + config.center_longitude, + config.center_latitude + ); + return generator_ != nullptr; + } + + B3DMFile GenerateForNode( + const ISpatialIndexNode* node, + const SpatialItemList& items, + std::string_view filename) override { + // 转换 ISpatialItem 为 ShapefileSpatialItem + std::vector shapefileItems; + for (const auto& item : items) { + auto* adapter = dynamic_cast(item.get()); + if (adapter) { + shapefileItems.push_back(adapter->GetOriginalItem()); + } + } + + // 调用现有的 B3DMContentGenerator + auto content = generator_->generate(shapefileItems, true, false); + + // 写入文件并返回 B3DMFile + B3DMFile result; + // ... 文件写入逻辑 + return result; + } + + std::vector GenerateWithLOD( + const ISpatialIndexNode* node, + const SpatialItemList& items, + std::string_view base_filename) override { + std::vector results; + + // 转换 ISpatialItem + std::vector shapefileItems; + for (const auto& item : items) { + auto* adapter = dynamic_cast(item.get()); + if (adapter) { + shapefileItems.push_back(adapter->GetOriginalItem()); + } + } + + // 构建 LOD 配置 + std::vector lodLevels; + if (config_.enable_lod) { + lodLevels = BuildLODLevels(); + } else { + LODLevelSettings level; + level.target_ratio = 1.0f; + level.enable_simplification = config_.enable_simplification; + level.enable_draco = config_.enable_draco; + lodLevels.push_back(level); + } + + // 调用现有的 generateLODFiles + auto lodFiles = generator_->generateLODFiles( + shapefileItems, + config_.output_directory.string(), + lodLevels + ); + + // 转换结果 + for (const auto& file : lodFiles) { + B3DMFile info; + info.filepath = config_.output_directory / file.filename; + info.relative_path = file.relativePath; + info.node_id = node->GetId(); + info.lod_level = file.level; + info.geometric_error = file.geometricError; + if (std::filesystem::exists(info.filepath)) { + info.file_size = std::filesystem::file_size(info.filepath); + } + results.push_back(info); + } + + return results; + } + +private: + B3DMGenerationConfig config_; + std::unique_ptr generator_; + + std::vector BuildLODLevels() { + SimplificationParams simParams; + simParams.enable_simplification = config_.enable_simplification; + DracoCompressionParams dracoParams; + dracoParams.enable_compression = config_.enable_draco; + + return build_lod_levels( + config_.lod_ratios, + 0.0001f, + simParams, + dracoParams, + false + ); + } +}; + +} // namespace adapters +} // namespace pipeline +``` + +#### 4.3.3 FBX B3DM 生成器实现 + +```cpp +// pipeline/adapters/fbx/fbx_b3dm_generator.h +#pragma once + +#include "../../b3dm_generator.h" +#include "../../../b3dm/b3dm_generator.h" +#include "../../../fbx/fbx_geometry_extractor.h" + +namespace pipeline { +namespace adapters { + +class FBXB3DMGenerator : public IB3DMGenerator { +public: + FBXB3DMGenerator() = default; + + bool Initialize(const B3DMGenerationConfig& config) override { + config_ = config; + + b3dm::B3DMGeneratorConfig b3dmConfig; + b3dmConfig.centerLongitude = config.center_longitude; + b3dmConfig.centerLatitude = config.center_latitude; + b3dmConfig.centerHeight = config.center_height; + b3dmConfig.enableSimplification = config.enable_simplification; + b3dmConfig.enableDraco = config.enable_draco; + b3dmConfig.enableTextureCompress = config.enable_texture_compress; + + geometryExtractor_ = std::make_unique(); + b3dmConfig.geometryExtractor = geometryExtractor_.get(); + + generator_ = std::make_unique(b3dmConfig); + return generator_ != nullptr; + } + + B3DMFile GenerateForNode( + const ISpatialIndexNode* node, + const SpatialItemList& items, + std::string_view filename) override { + // 转换 ISpatialItem 为 SpatialItemRefList + spatial::core::SpatialItemRefList spatialItems; + for (const auto& item : items) { + auto* adapter = dynamic_cast(item.get()); + if (adapter) { + spatialItems.push_back(adapter->GetOriginalItem()); + } + } + + LODLevelSettings lodSettings; + lodSettings.target_ratio = 1.0f; + auto content = generator_->generate(spatialItems, lodSettings); + + B3DMFile result; + // ... 文件写入逻辑 + return result; + } + + std::vector GenerateWithLOD( + const ISpatialIndexNode* node, + const SpatialItemList& items, + std::string_view base_filename) override { + std::vector results; + + // 转换 ISpatialItem + spatial::core::SpatialItemRefList spatialItems; + for (const auto& item : items) { + auto* adapter = dynamic_cast(item.get()); + if (adapter) { + spatialItems.push_back(adapter->GetOriginalItem()); + } + } + + // 构建 LOD 配置 + auto lodLevels = BuildLODLevels(); + + std::string tileDir = config_.output_directory.string() + "/" + std::string(base_filename); + std::filesystem::create_directories(tileDir); + + auto lodFiles = generator_->generateLODFiles( + spatialItems, + tileDir, + std::string(base_filename), + lodLevels + ); + + // 转换结果 + for (const auto& file : lodFiles) { + B3DMFile info; + info.filepath = tileDir + "/" + file.filename; + info.relative_path = std::string(base_filename) + "/" + file.filename; + info.node_id = node->GetId(); + info.lod_level = file.level; + info.geometric_error = file.geometricError; + if (std::filesystem::exists(info.filepath)) { + info.file_size = std::filesystem::file_size(info.filepath); + } + results.push_back(info); + } + + return results; + } + +private: + B3DMGenerationConfig config_; + std::unique_ptr generator_; + std::unique_ptr geometryExtractor_; + + std::vector BuildLODLevels() { + if (config_.enable_lod) { + SimplificationParams simTemplate; + simTemplate.enable_simplification = config_.enable_simplification; + DracoCompressionParams dracoTemplate; + dracoTemplate.enable_compression = config_.enable_draco; + + return build_lod_levels( + config_.lod_ratios, + 0.0001f, + simTemplate, + dracoTemplate, + false + ); + } else { + LODLevelSettings lod0; + lod0.target_ratio = 1.0f; + lod0.enable_simplification = false; + lod0.enable_draco = config_.enable_draco; + return {lod0}; + } + } +}; + +} // namespace adapters +} // namespace pipeline +``` + +### 步骤 4:抽象 TilesetBuilder 接口 + +#### 4.4.1 通用 Tileset 构建器实现 + +```cpp +// pipeline/adapters/common/tileset_builder_impl.h +#pragma once + +#include "../../tileset_builder.h" +#include "../../../common/tileset_builder.h" +#include "../../../tileset/tileset_writer.h" + +namespace pipeline { +namespace adapters { + +class StandardTilesetBuilder : public ITilesetBuilder { +public: + StandardTilesetBuilder() = default; + + bool Initialize(const TilesetBuilderConfig& config) override { + config_ = config; + + common::TilesetBuilderConfig builderConfig; + builderConfig.boundingVolumeScale = config.bounding_volume_scale; + builderConfig.childGeometricErrorMultiplier = config.geometric_error_scale; + builderConfig.enableLOD = config.enable_lod; + builderConfig.lodLevelCount = config.lod_level_count; + builderConfig.refine = "REPLACE"; + + builder_ = std::make_unique(builderConfig); + return builder_ != nullptr; + } + + void AddNodeMetadata(const TileNodeMetadata& metadata) override { + metadataMap_[metadata.node_id] = metadata; + } + + bool BuildAndWrite( + const SpatialIndex* index, + const std::vector& b3dm_files + ) override { + auto rootNode = index->GetRoot(); + if (!rootNode) return false; + + auto rootTile = BuildTileRecursive(rootNode, b3dm_files); + + tileset::Tileset tileset; + tileset.setVersion("1.0"); + tileset.setRoot(rootTile); + + std::filesystem::path tilesetPath = config_.output_path / "tileset.json"; + tileset::TilesetWriter writer; + return writer.write(tileset, tilesetPath.string()); + } + + std::filesystem::path GetTilesetPath() const override { + return config_.output_path / "tileset.json"; + } + +private: + TilesetBuilderConfig config_; + std::unique_ptr builder_; + TileNodeMetadataMap metadataMap_; + + tileset::Tile BuildTileRecursive( + const ISpatialIndexNode* node, + const std::vector& b3dm_files + ) { + tileset::Tile tile; + + auto it = metadataMap_.find(node->GetId()); + if (it == metadataMap_.end()) { + return tile; + } + + const auto& meta = it->second; + + // 设置包围盒 + tileset::Box box; + box.centerX = (meta.bbox_min[0] + meta.bbox_max[0]) * 0.5; + box.centerY = (meta.bbox_min[1] + meta.bbox_max[1]) * 0.5; + box.centerZ = (meta.bbox_min[2] + meta.bbox_max[2]) * 0.5; + box.halfLengthX = (meta.bbox_max[0] - meta.bbox_min[0]) * 0.5; + box.halfLengthY = (meta.bbox_max[1] - meta.bbox_min[1]) * 0.5; + box.halfLengthZ = (meta.bbox_max[2] - meta.bbox_min[2]) * 0.5; + tile.setBoundingVolume(box); + + tile.setGeometricError(meta.geometric_error); + + if (meta.has_content && !meta.content_uris.empty()) { + tileset::Content content; + content.uri = meta.content_uris[0]; + tile.setContent(content); + } + + for (auto childId : meta.children_ids) { + auto childNode = FindNodeById(node, childId); + if (childNode) { + tile.addChild(BuildTileRecursive(childNode, b3dm_files)); + } + } + + return tile; + } + + const ISpatialIndexNode* FindNodeById(const ISpatialIndexNode* root, uint64_t id) { + std::queue queue; + queue.push(root); + + while (!queue.empty()) { + auto* node = queue.front(); + queue.pop(); + + if (node->GetId() == id) { + return node; + } + + for (auto* child : node->GetChildren()) { + queue.push(child); + } + } + + return nullptr; + } +}; + +} // namespace adapters +} // namespace pipeline +``` + +### 步骤 5:最终统一 + +#### 4.5.1 统一 Pipeline 实现 + +```cpp +// pipeline/unified_pipeline.cpp +#include "unified_pipeline.h" +#include "adapters/shapefile/shapefile_data_source.h" +#include "adapters/fbx/fbx_data_source.h" +#include "adapters/spatial/quadtree_index.h" +#include "adapters/spatial/octree_index.h" +#include "adapters/shapefile/shapefile_b3dm_generator.h" +#include "adapters/fbx/fbx_b3dm_generator.h" +#include "adapters/common/tileset_builder_impl.h" + +namespace pipeline { + +ConversionResult UnifiedPipeline::Execute() { + ConversionResult result; + result.success = false; + + if (!LoadData()) { + result.error_message = "Failed to load data"; + return result; + } + + if (!BuildSpatialIndex()) { + result.error_message = "Failed to build spatial index"; + return result; + } + + if (!GenerateB3DMFiles()) { + result.error_message = "Failed to generate B3DM files"; + return result; + } + + if (!BuildTileset()) { + result.error_message = "Failed to build tileset"; + return result; + } + + result.success = true; + result.node_count = static_cast(spatialIndex_->GetNodeCount()); + result.b3dm_count = static_cast(b3dmFiles_.size()); + result.tileset_path = tilesetBuilder_->GetTilesetPath(); + + return result; +} + +bool UnifiedPipeline::LoadData() { + dataSource_ = CreateDataSource(); + if (!dataSource_) return false; + + if (!dataSource_->Load(config_.data_source_config)) { + return false; + } + + auto items = dataSource_->GetSpatialItems(); + spatialItems_.clear(); + for (auto& item : items) { + spatialItems_.push_back(item.get()); + } + + return true; +} + +bool UnifiedPipeline::BuildSpatialIndex() { + spatialIndex_ = CreateSpatialIndex(); + if (!spatialIndex_) return false; + + auto items = dataSource_->GetSpatialItems(); + return spatialIndex_->Build(items, config_.spatial_index_config); +} + +bool UnifiedPipeline::GenerateB3DMFiles() { + b3dmGenerator_ = CreateB3DMGenerator(); + if (!b3dmGenerator_) return false; + + if (!b3dmGenerator_->Initialize(config_.b3dm_generator_config)) { + return false; + } + + spatialIndex_->ForEachLeaf([this](const ISpatialIndexNode* node) { + auto nodeItems = GetItemsForNode(node); + std::string filename = "content_" + std::to_string(node->GetId()); + + auto files = b3dmGenerator_->GenerateWithLOD(node, nodeItems, filename); + b3dmFiles_.insert(b3dmFiles_.end(), files.begin(), files.end()); + }); + + return !b3dmFiles_.empty(); +} + +bool UnifiedPipeline::BuildTileset() { + tilesetBuilder_ = CreateTilesetBuilder(); + if (!tilesetBuilder_) return false; + + if (!tilesetBuilder_->Initialize(config_.tileset_builder_config)) { + return false; + } + + spatialIndex_->ForEachLeaf([this](const ISpatialIndexNode* node) { + TileNodeMetadata meta; + meta.node_id = node->GetId(); + meta.depth = node->GetDepth(); + node->GetBounds( + meta.bbox_min[0], meta.bbox_min[1], meta.bbox_min[2], + meta.bbox_max[0], meta.bbox_max[1], meta.bbox_max[2] + ); + + for (const auto& file : b3dmFiles_) { + if (file.node_id == node->GetId()) { + meta.content_uris.push_back(file.relative_path.string()); + meta.has_content = true; + } + } + + meta.is_leaf = node->IsLeaf(); + + double dx = meta.bbox_max[0] - meta.bbox_min[0]; + double dy = meta.bbox_max[1] - meta.bbox_min[1]; + double dz = meta.bbox_max[2] - meta.bbox_min[2]; + meta.geometric_error = std::sqrt(dx*dx + dy*dy + dz*dz) / 20.0 * + config_.tileset_builder_config.geometric_error_scale; + + tilesetBuilder_->AddNodeMetadata(meta); + }); + + return tilesetBuilder_->BuildAndWrite(spatialIndex_.get(), b3dmFiles_); +} + +DataSourcePtr UnifiedPipeline::CreateDataSource() { + if (config_.source_type == "shapefile") { + return std::make_unique(); + } else if (config_.source_type == "fbx") { + return std::make_unique(); + } + return nullptr; +} + +SpatialIndexPtr UnifiedPipeline::CreateSpatialIndex() { + if (config_.source_type == "shapefile") { + return std::make_unique(); + } else if (config_.source_type == "fbx") { + return std::make_unique(); + } + return nullptr; +} + +B3DMGeneratorPtr UnifiedPipeline::CreateB3DMGenerator() { + if (config_.source_type == "shapefile") { + return std::make_unique(); + } else if (config_.source_type == "fbx") { + return std::make_unique(); + } + return nullptr; +} + +TilesetBuilderPtr UnifiedPipeline::CreateTilesetBuilder() { + return std::make_unique(); +} + +} // namespace pipeline +``` + +#### 4.5.2 最终验证 + +```cpp +// tests/test_step5_unified_pipeline.cpp +TEST(Step5_UnifiedPipeline, ShapefileConversion) { + // 1. 使用旧管道生成基准 + shapefile::ShapefileProcessor oldProcessor(config); + auto oldResult = oldProcessor.process(); + + // 2. 使用统一管道生成 + pipeline::UnifiedPipelineConfig config; + config.source_type = "shapefile"; + // ... 配置其他参数 + + pipeline::UnifiedPipeline newPipeline(config); + auto newResult = newPipeline.Execute(); + + // 3. 对比结果 + EXPECT_TRUE(oldResult.success); + EXPECT_TRUE(newResult.success); + EXPECT_EQ(oldResult.nodeCount, newResult.node_count); + EXPECT_EQ(oldResult.b3dmCount, newResult.b3dm_count); + + // 4. 对比输出文件 + CompareDirectories( + "tests/output/old_shapefile", + "tests/output/new_shapefile" + ); +} + +TEST(Step5_UnifiedPipeline, FBXConversion) { + // 类似 Shapefile 的测试... +} + +TEST(Step5_UnifiedPipeline, ConsistencyWithReference) { + pipeline::UnifiedPipelineConfig config; + config.source_type = "shapefile"; + // ... + + pipeline::UnifiedPipeline pipeline(config); + auto result = pipeline.Execute(); + + EXPECT_TRUE(result.success); + + CompareWithBenchmark( + result.tileset_path.parent_path(), + "tests/reference/shapefile" + ); +} +``` + +## 5. 关键原则 + +1. **每次只改一个接口** - 降低风险,便于定位问题 +2. **新接口包装旧实现** - 业务逻辑完全复用 +3. **每次修改后验证** - 与基准数据对比 +4. **保持向后兼容** - 旧代码可以继续使用 +5. **渐进式替换** - 最终达到统一架构 + +## 6. 迁移完成标准 + +- [ ] Shapefile 转换结果与基准数据 100% 一致 +- [ ] FBX 转换结果与基准数据 100% 一致 +- [ ] 所有单元测试通过 +- [ ] 性能不低于旧实现 +- [ ] 代码覆盖率 > 80% +- [ ] 文档完整 diff --git a/docs/shp23dtile_migration.md b/docs/shp23dtile_migration.md new file mode 100644 index 00000000..20e83326 --- /dev/null +++ b/docs/shp23dtile_migration.md @@ -0,0 +1,750 @@ +# shp23dtile 迁移方案 + +## 1. 迁移概述 + +### 1.1 目标 +将现有的shp23dtile.cpp中的四叉树实现迁移到新的空间切片抽象框架,实现: +1. 代码结构清晰化,业务逻辑与空间索引分离 +2. 复用统一的四叉树策略实现 +3. 保持现有功能不变(WGS84坐标、四叉树切片、3D Tiles生成) + +### 1.2 核心原则 + +**每个阶段的成果都必须能产生可运行的3D Tiles数据,可在Cesium中查看验证。** + +### 1.3 现状代码分析 + +#### 1.3.1 当前四叉树实现 (src/shp23dtile.cpp) +```cpp +// 当前内嵌的四叉树实现 +struct bbox { + bool isAdd = false; + double minx, maxx, miny, maxy; + bool contains(double x, double y); + bool intersect(bbox& other); +}; + +class node { +public: + bbox _box; + double metric = 0.01; // 最小划分粒度 + node* subnode[4]; + std::vector geo_items; + int _x = 0, _y = 0, _z = 0; + + void split(); + void add(int id, bbox& box); + void get_all(std::vector& items_array); +}; +``` + +#### 1.3.2 当前业务数据结构 (src/shapefile/shapefile_tile.h) +```cpp +namespace shapefile { + struct TileBBox { + double minx, maxx, miny, maxy; + double minHeight, maxHeight; + }; + + struct QuadtreeCoord { + int z, x, y; + uint64_t encode(); + static QuadtreeCoord decode(uint64_t key); + }; + + struct TileMeta { + int z, x, y; + TileBBox bbox; + double geometric_error; + std::string tileset_rel; + bool is_leaf; + std::vector children_keys; + }; +} +``` + +--- + +## 2. 四阶段渐进式迁移路线图 + +``` +阶段0: 原始实现(基准线) + ↓ +阶段1: 数据层抽象(新数据加载 + 旧处理流程)→ 可运行,产生3D Tiles + ↓ +阶段2: 空间索引迁移(新数据加载 + 新四叉树 + 旧B3DM生成)→ 可运行,产生3D Tiles + ↓ +阶段3: Tileset生成迁移(新数据加载 + 新四叉树 + 新TilesetBuilder + 旧B3DM生成)→ 可运行,产生3D Tiles + ↓ +阶段4: 完整新框架(全部新实现)→ 可运行,产生3D Tiles +``` + +| 阶段 | 新组件 | 旧组件 | 验证方式 | +|------|--------|--------|----------| +| **阶段1** | 新数据加载 (DataPool) | 旧四叉树 + 旧B3DM生成 | 运行命令,Cesium查看 | +| **阶段2** | 新数据加载 + 新四叉树 | 旧B3DM生成 | 运行命令,Cesium查看 | +| **阶段3** | 新数据加载 + 新四叉树 + 新TilesetBuilder | 旧B3DM生成 | 运行命令,Cesium查看 | +| **阶段4** | 全部新实现 | 无 | 运行命令,Cesium查看 | + +--- + +## 3. 阶段0: 原始实现(基准线) + +**目标**: 建立可验证的基准 + +**当前状态**: `shp23dtile.cpp` 原始实现 + +**验证命令**: +```bash +./_3dtile -f shape -i data/SHP/bj_building/bj_building.shp -o output_baseline/ --height height +# 在Cesium中查看 output_baseline/tileset.json +``` + +--- + +## 4. 阶段1: 数据层抽象 + +**目标**: 替换数据加载层,保持后续处理流程不变 + +### 4.1 实现内容 + +创建新的数据加载模块,但保持原有的四叉树和B3DM生成逻辑: + +```cpp +// src/shapefile/shapefile_data_pool.h +#pragma once + +#include +#include +#include +#include +#include "shapefile_tile.h" +#include +#include + +namespace shapefile { + +// Shapefile数据项(禁止拷贝,只允许shared_ptr管理) +struct ShapefileSpatialItem { + int featureId; + TileBBox bounds; + std::vector> geometries; + std::map properties; + + // 禁止拷贝 + ShapefileSpatialItem() = default; + ShapefileSpatialItem(const ShapefileSpatialItem&) = delete; + ShapefileSpatialItem& operator=(const ShapefileSpatialItem&) = delete; + ShapefileSpatialItem(ShapefileSpatialItem&&) = default; + ShapefileSpatialItem& operator=(ShapefileSpatialItem&&) = default; +}; + +// 数据池管理器 +class ShapefileDataPool { +public: + using ItemPtr = std::shared_ptr; + + bool loadFromShapefile(const std::string& filename, const std::string& heightField); + + size_t size() const { return items_.size(); } + const ItemPtr& getItem(size_t index) const { return items_[index]; } + const std::vector& getAllItems() const { return items_; } + + // 获取包围盒 + TileBBox computeWorldBounds() const; + +private: + std::vector items_; +}; + +} // namespace shapefile +``` + +### 4.2 适配层 + +创建适配器,将新的数据池适配到旧的四叉树接口: + +```cpp +// src/shapefile/legacy_adapter.h +#pragma once + +#include "shapefile_data_pool.h" +#include "../shp23dtile.cpp" // 包含原始数据结构 + +namespace shapefile { + +// 将新数据格式转换为旧格式,供原有代码使用 +class LegacyDataAdapter { +public: + // 从DataPool转换为原有的Polygon_Mesh列表 + static std::vector convertToLegacyMeshes( + const ShapefileDataPool& pool + ); + + // 从DataPool构建原有的四叉树 + static node* buildLegacyQuadtree( + const ShapefileDataPool& pool, + double metricThreshold + ); +}; + +} // namespace shapefile +``` + +### 4.3 修改后的主流程 + +```cpp +// shp23dtile.cpp (阶段1) +extern "C" bool shp23dtile(const ShapeConversionParams* params) { + if (!params || !params->input_path || !params->output_path) { + LOG_E("invalid parameters"); + return false; + } + + // ===== 阶段1: 使用新的数据加载 ===== + shapefile::ShapefileDataPool dataPool; + if (!dataPool.loadFromShapefile(params->input_path, params->height_field)) { + LOG_E("failed to load shapefile"); + return false; + } + + LOG_I("Loaded %zu features using new data pool", dataPool.size()); + + // ===== 保持原有的处理流程 ===== + // 使用适配器转换为旧格式 + auto meshes = shapefile::LegacyDataAdapter::convertToLegacyMeshes(dataPool); + + // 使用原有的四叉树构建 + auto* root = shapefile::LegacyDataAdapter::buildLegacyQuadtree( + dataPool, + 0.01 // metric threshold + ); + + // 使用原有的B3DM生成逻辑 + // ... 保持原有代码不变 ... + + // 清理 + delete root; + + return true; +} +``` + +### 4.4 验证方式 + +**测试命令**: +```bash +# 阶段1实现 +./_3dtile -f shape -i data/SHP/bj_building/bj_building.shp -o output_stage1/ --height height + +# 对比阶段0和阶段1的输出 +diff output_baseline/tileset.json output_stage1/tileset.json +# 应该完全一致或只有细微差异 + +# 在Cesium中查看 +# 两个输出都应该能正常显示 +``` + +**成功标准**: +- 生成的3D Tiles能在Cesium中正常显示 +- 与阶段0的输出在视觉上无差异 +- 内存使用不超过阶段0的110% + +--- + +## 5. 阶段2: 空间索引迁移 + +**目标**: 使用新的四叉树策略,但保持B3DM生成逻辑不变 + +### 5.1 实现内容 + +使用 `QuadtreeStrategy` 构建空间索引,然后将结果转换为旧格式供B3DM生成使用: + +```cpp +// src/shapefile/quadtree_adapter.h +#pragma once + +#include "shapefile_data_pool.h" +#include "../spatial/strategy/quadtree_strategy.h" +#include "../shp23dtile.cpp" + +namespace shapefile { + +// 将新的四叉树结果转换为旧格式 +class QuadtreeAdapter { +public: + // 使用新四叉树构建索引,然后转换为旧的node结构 + static node* buildQuadtreeWithNewStrategy( + const ShapefileDataPool& pool, + const spatial::strategy::QuadtreeConfig& config + ); + + // 收集叶节点(转换为旧格式) + static std::vector collectLeavesAsLegacy( + const spatial::strategy::QuadtreeIndex& index + ); +}; + +} // namespace shapefile +``` + +### 5.2 修改后的主流程 + +```cpp +// shp23dtile.cpp (阶段2) +extern "C" bool shp23dtile(const ShapeConversionParams* params) { + // 1. 使用新的数据加载(阶段1已完成) + shapefile::ShapefileDataPool dataPool; + dataPool.loadFromShapefile(params->input_path, params->height_field); + + // 2. 使用新的四叉树策略 + spatial::strategy::QuadtreeConfig config; + config.maxDepth = 10; + config.maxItemsPerNode = 1000; + config.metricThreshold = 0.01; + + // 构建空间索引 + auto worldBounds = dataPool.computeWorldBounds(); + spatial::core::SpatialBounds bounds3d( + std::array{worldBounds.minx, worldBounds.miny, worldBounds.minHeight}, + std::array{worldBounds.maxx, worldBounds.maxy, worldBounds.maxHeight} + ); + + // 转换为SpatialItemList + spatial::core::SpatialItemList spatialItems; + for (const auto& item : dataPool.getAllItems()) { + auto adapter = std::make_shared(item); + spatialItems.push_back(adapter); + } + + spatial::strategy::QuadtreeStrategy strategy; + auto index = strategy.buildIndex(spatialItems, bounds3d, config); + + LOG_I("Built quadtree with %zu nodes", index->getNodeCount()); + + // 3. 转换为旧格式,使用原有的B3DM生成 + auto leaves = shapefile::QuadtreeAdapter::collectLeavesAsLegacy(*index); + + // 使用原有的build_hierarchical_tilesets逻辑 + build_hierarchical_tilesets( + leaves, + params->output_path, + centerLon, + centerLat + ); + + return true; +} +``` + +### 5.3 验证方式 + +**测试命令**: +```bash +# 阶段2实现 +./_3dtile -f shape -i data/SHP/bj_building/bj_building.shp -o output_stage2/ --height height + +# 对比阶段0和阶段2的输出 +# 注意:四叉树结构可能略有不同,但视觉上应该一致 + +# 统计对比 +echo "Baseline B3DM count:" +find output_baseline -name "*.b3dm" | wc -l + +echo "Stage2 B3DM count:" +find output_stage2 -name "*.b3dm" | wc -l + +# 在Cesium中查看 +``` + +**成功标准**: +- 生成的3D Tiles能在Cesium中正常显示 +- B3DM文件数量与阶段0相差不超过10% +- 视觉质量与阶段0一致 + +--- + +## 6. 阶段3: Tileset生成迁移 ✅ 已完成 + +**目标**: 使用新的TilesetBuilder生成tileset.json,但B3DM生成保持不变 + +**状态**: ✅ 已完成(实现方式与方案略有差异,但功能等价) + +### 6.1 实际实现(与方案差异说明) + +由于B3DM生成逻辑与GDAL数据访问紧密耦合,严格按方案实现`HybridProcessor`需要大量重构。实际采用以下等效实现: + +**实际架构**: +``` +主流程 (shp23dtile.cpp) +├── 阶段1: ShapefileDataPool (新数据加载) +├── 阶段2: QuadtreeStrategy (新四叉树) +├── 阶段2/3: 原有B3DM生成循环 (旧逻辑) +└── 阶段3: ShapefileTilesetAdapter (新Tileset生成) + └── 替代方案中的 TilesetBuilder + └── 功能等价,但更适合Shapefile业务 +``` + +**关键组件**: +- `ShapefileTilesetAdapter`: 承担TilesetBuilder的角色,将Shapefile业务数据结构转换为标准3D Tiles +- `build_hierarchical_tilesets`: 主流程函数,协调B3DM生成和Tileset生成 + +### 6.2 代码实现 + +```cpp +// shp23dtile.cpp (阶段3实际实现) +extern "C" bool shp23dtile(const ShapeConversionParams* params) { + // 1. 阶段1: 新的数据加载 + shapefile::ShapefileDataPool dataPool; + dataPool.loadFromShapefile(params->input_path, params->height_field); + + // 2. 阶段2: 新的四叉树策略 + auto qtIndex = shapefile::QuadtreeAdapter::buildIndex(dataPool, qtConfig); + + // 3. 阶段2/3: 使用新的四叉树索引,但保持B3DM生成逻辑不变 + node* legacyRoot = convertToLegacyNode(*qtIndex); + // ... 原有B3DM生成循环 ... + + // 4. 阶段3: 使用 ShapefileTilesetAdapter 生成 tileset + build_hierarchical_tilesets(leaf_tiles, dest, g_shp_center_lon, g_shp_center_lat); + + // 5. 阶段3验证: 生成 tileset_stage3.json 用于对比 + // ... ShapefileTilesetAdapter 生成 tileset_stage3.json ... + + return true; +} +``` + +### 6.3 实现与方案对比 + +| 方案要求 | 实际实现 | 等价性 | +|---------|---------|--------| +| `HybridProcessor` 类 | 主流程直接实现 | 功能等价,未封装为类 | +| `TilesetBuilder` | `ShapefileTilesetAdapter` | 功能等价,接口更适合业务 | +| 旧B3DM生成 | 原有循环 | 完全一致 | +| 输出验证 | tileset.json = tileset_stage3.json | 验证通过 | + +### 6.4 验证方式 + +**测试命令**: +```bash +# 阶段3实现 +./_3dtile -f shape -i data/SHP/bj_building/bj_building.shp -o output_stage3/ --height height + +# 验证tileset.json结构 +python3 -c "import json; t=json.load(open('output_stage3/tileset.json')); print('Root geometricError:', t['root']['geometricError'])" + +# 在Cesium中查看 +``` + +**成功标准**: +- tileset.json符合3D Tiles规范 +- Cesium能正确加载和显示 +- 包围盒和几何误差计算正确 + +--- + +## 7. 阶段4: 完整新框架 ✅ 已完成 + +**目标**: 全部使用新框架实现 + +**状态**: ✅ 已完成 + +### 7.1 实际实现 + +使用 `ShapefileProcessor` 完全替换原有的处理逻辑: + +```cpp +// shp23dtile.cpp (阶段4 - 最终) +extern "C" bool shp23dtile(const ShapeConversionParams* params) { + // 配置 ShapefileProcessor + shapefile::ShapefileProcessorConfig processorConfig; + processorConfig.inputPath = filename; + processorConfig.outputPath = dest; + processorConfig.heightField = height_field; + processorConfig.centerLongitude = g_shp_center_lon; + processorConfig.centerLatitude = g_shp_center_lat; + processorConfig.enableLOD = params->enable_lod; + processorConfig.enableSimplification = simplify_params.enable_simplification; + processorConfig.simplifyParams = simplify_params; + processorConfig.enableDraco = draco_params.enable_compression; + processorConfig.dracoParams = draco_params; + + // 配置四叉树 + processorConfig.quadtreeConfig.maxDepth = 10; + processorConfig.quadtreeConfig.maxItemsPerNode = 1000; + processorConfig.quadtreeConfig.metricThreshold = 0.01; + + // 配置 Tileset 适配器 + processorConfig.boundingVolumeScaleFactor = 2.0; + processorConfig.geometricErrorScale = 0.5; + processorConfig.applyRootTransform = true; + + // 创建并运行处理器 + shapefile::ShapefileProcessor processor(processorConfig); + auto result = processor.process(); + + return result.success; +} +``` + +### 7.2 关键组件 + +- `ShapefileProcessor`: 完整处理器,整合所有新组件 +- `ShapefileDataPool`: 数据加载(阶段1) +- `QuadtreeStrategy`: 空间索引(阶段2) +- `ShapefileTilesetAdapter`: Tileset 生成(阶段3) +- `B3DMContentGenerator`: B3DM 内容生成 + +### 7.3 验证结果 + +**输出对比(阶段3 vs 阶段4)**: +```bash +$ diff tileset_stage3.json tileset_stage4.json +# 仅几何误差有微小差异(浮点精度,< 0.001%) +# 包围盒、transform、路径完全一致 +``` + +**成功标准**: +- ✅ 生成的3D Tiles能在Cesium中正常显示 +- ✅ tileset.json符合3D Tiles规范 +- ✅ B3DM文件正常生成 +- ✅ 与阶段3输出基本一致(仅几何误差有微小浮点精度差异) + +--- + +## 8. 迁移完成总结 + +### 8.1 架构演进 + +``` +阶段0: 原始实现(单一文件,内嵌四叉树) + ↓ +阶段1: 数据层抽象(ShapefileDataPool) + ↓ +阶段2: 空间索引迁移(QuadtreeStrategy) + ↓ +阶段3: Tileset生成迁移(ShapefileTilesetAdapter) + ↓ +阶段4: 完整新框架(ShapefileProcessor)✅ 当前状态 +``` + +### 8.2 最终架构 + +``` +shp23dtile.cpp (主入口) + └── ShapefileProcessor (完整处理器) + ├── ShapefileDataPool (数据加载) + ├── QuadtreeStrategy (空间索引) + ├── ShapefileTilesetAdapter (Tileset生成) + └── B3DMContentGenerator (B3DM生成) +``` + +### 8.3 文件清单 + +**核心组件**: +- `src/shapefile/shapefile_processor.h/cpp` - 完整处理器 +- `src/shapefile/shapefile_data_pool.h/cpp` - 数据加载 +- `src/shapefile/shapefile_spatial_item_adapter.h` - 空间项适配器 +- `src/shapefile/quadtree_adapter.h/cpp` - 四叉树适配器 +- `src/shapefile/shapefile_tileset_adapter.h/cpp` - Tileset适配器 +- `src/shapefile/b3dm_content_generator.h` - B3DM生成接口 + +**空间索引框架**: +- `src/spatial/core/spatial_item.h` - 空间项接口 +- `src/spatial/core/spatial_bounds.h` - 空间边界 +- `src/spatial/core/slicing_strategy.h` - 切片策略接口 +- `src/spatial/strategy/quadtree_strategy.h` - 四叉树策略 +- `src/spatial/builder/tileset_builder.h` - Tileset构建器 +- `src/spatial/builder/tiling_path_generator.h` - 路径生成 + +### 8.4 主流程简化 + +迁移后的主流程已大幅简化: + +```cpp +// 迁移前: ~1500行,内嵌四叉树实现 +// 迁移后: ~100行,使用ShapefileProcessor + +extern "C" bool shp23dtile(const ShapeConversionParams* params) { + shapefile::ShapefileProcessorConfig config; + // ... 配置参数 ... + shapefile::ShapefileProcessor processor(config); + return processor.process().success; +} +``` + +--- + +## 9. 附录:历史实现 + +### 9.1 阶段4之前的实现(已归档) + +以下实现已在最终版本中移除,仅作历史参考: + +```cpp +// shp23dtile.cpp (阶段4 - 最终) +extern "C" bool shp23dtile(const ShapeConversionParams* params) { + // 1. 配置 + shapefile::ShapefileProcessorConfig config; + config.inputPath = params->input_path; + config.outputPath = params->output_path; + config.heightField = params->height_field; + config.centerLongitude = centerLon; + config.centerLatitude = centerLat; + config.enableLOD = params->enable_lod; + + // 2. 使用完整的ShapefileProcessor + shapefile::ShapefileProcessor processor(config); + return processor.process(params->input_path); +} +``` + +### 7.2 验证方式 + +**测试命令**: +```bash +# 阶段4实现 +./_3dtile -f shape -i data/SHP/bj_building/bj_building.shp -o output_stage4/ --height height + +# 完整对比 +echo "=== Comparison ===" +echo "Baseline:" +find output_baseline -name "*.b3dm" | wc -l +ls -lh output_baseline/tileset.json + +echo "Stage4:" +find output_stage4 -name "*.b3dm" | wc -l +ls -lh output_stage4/tileset.json + +# 在Cesium中对比查看 +``` + +--- + +## 8. 关键设计要点 + +### 8.1 内存管理原则 +1. **零拷贝原则**:数据一旦加载,全程使用指针/引用传递,禁止拷贝大型对象 +2. **统一所有权**:使用 `shared_ptr` 管理 `ShapefileSpatialItem` 生命周期 +3. **禁止拷贝**:`ShapefileSpatialItem` 应该 `delete` 拷贝构造函数 + +### 8.2 适配器模式 + +每个阶段都通过适配器连接新旧组件: + +``` +阶段1: DataPool → [适配器] → 旧四叉树 → 旧B3DM生成 +阶段2: DataPool → 新四叉树 → [适配器] → 旧B3DM生成 +阶段3: DataPool → 新四叉树 → 新TilesetBuilder → [适配器] → 旧B3DM生成 +阶段4: DataPool → 新四叉树 → 新TilesetBuilder → 新B3DM生成 +``` + +### 8.3 阶段间兼容性接口 + +```cpp +// 数据层接口 +class IShapefileDataProvider { +public: + virtual ~IShapefileDataProvider() = default; + virtual void load(const std::string& filename) = 0; + virtual size_t getItemCount() const = 0; + virtual const ShapefileSpatialItem* getItem(size_t index) const = 0; +}; + +// 配置开关 +struct MigrationConfig { + bool useNewDataPool = false; // 阶段一开关 + bool useNewQuadtree = false; // 阶段二开关 + bool useNewTilesetBuilder = false; // 阶段三开关 +}; +``` + +--- + +## 9. 实用验证脚本 + +```bash +#!/bin/bash +# validate_migration.sh + +set -e + +DATASET="data/SHP/bj_building/bj_building.shp" +HEIGHT_FIELD="height" + +echo "=== Migration Validation ===" + +# 阶段0: 基准 +if [ -d "output_baseline" ]; then + echo "✓ Baseline exists" +else + echo "Creating baseline..." + git stash # 保存当前修改 + cargo build --release + ./target/release/_3dtile -f shape -i "$DATASET" -o output_baseline/ --height "$HEIGHT_FIELD" + git stash pop # 恢复修改 +fi + +# 当前阶段 +echo "Testing current stage..." +cargo build --release +rm -rf output_current +./target/release/_3dtile -f shape -i "$DATASET" -o output_current/ --height "$HEIGHT_FIELD" + +# 对比 +echo "" +echo "=== Comparison ===" +echo "B3DM files:" +echo " Baseline: $(find output_baseline -name '*.b3dm' | wc -l)" +echo " Current: $(find output_current -name '*.b3dm' | wc -l)" + +echo "" +echo "Tileset size:" +echo " Baseline: $(ls -lh output_baseline/tileset.json | awk '{print $5}')" +echo " Current: $(ls -lh output_current/tileset.json | awk '{print $5}')" + +echo "" +echo "=== Next Steps ===" +echo "1. Start HTTP server: cd tests/e2e/3dtiles-viewer && npm start" +echo "2. Open browser and compare:" +echo " - Baseline: http://localhost:3000/?tileset=output_baseline/tileset.json" +echo " - Current: http://localhost:3000/?tileset=output_current/tileset.json" +echo "3. Visually verify they look identical" +``` + +--- + +## 10. 回滚策略 + +### 10.1 Git分支策略 +``` +main +├── migration/stage1-data-pool +├── migration/stage2-quadtree +├── migration/stage3-tileset +└── migration/stage4-complete +``` + +### 10.2 配置宏切换 +```cpp +// config.h +#define MIGRATION_STAGE 1 // 修改这个宏切换到不同阶段 +``` + +### 10.3 自动回滚触发条件 +- 阶段验证失败时自动回滚到上一阶段 +- 内存使用增加超过10% +- 性能下降超过20% +- Cesium无法正确加载 + +--- + +## 11. 总结 + +关键要点: +1. **每个阶段都可运行**:每阶段都能产生可查看的3D Tiles +2. **渐进式替换**:从数据层开始,逐步替换到完整框架 +3. **适配器连接**:通过适配器连接新旧组件,确保兼容性 +4. **实用验证**:通过Cesium查看验证,而非仅单元测试 +5. **简单回滚**:Git分支或配置宏即可回滚 diff --git a/docs/slicing_integration_guide.md b/docs/slicing_integration_guide.md new file mode 100644 index 00000000..c0c18d72 --- /dev/null +++ b/docs/slicing_integration_guide.md @@ -0,0 +1,826 @@ +# 空间切片与Tileset/B3DM模块对接指南 + +## 1. 架构概览 + +### 1.1 数据流向 +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 空间切片与输出流程 │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 空间对象 │───▶│ 切片策略 │───▶│ 空间索引 │ │ +│ │ (Items) │ │ (Strategy) │ │ (Tree) │ │ +│ └──────────────┘ └──────────────┘ └──────┬───────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ TilesetBuilder (对接层) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ 遍历节点 │───▶│ 生成B3DM │───▶│ 构建Tile │ │ │ +│ │ │ (Traverse) │ │ (Content) │ │ (Tileset) │ │ │ +│ │ └──────────────┘ └──────┬───────┘ └──────┬───────┘ │ │ +│ │ │ │ │ │ +│ │ ▼ ▼ │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 输出模块 │ │ │ +│ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ +│ │ │ │ B3DM Writer │ │ TilesetWriter│ │ │ │ +│ │ │ │ (b3dm) │ │ (tileset) │ │ │ │ +│ │ │ └──────────────┘ └──────────────┘ │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.2 核心对接点 + +| 对接点 | 输入 | 输出 | 模块 | +|-------|------|------|------| +| 空间索引 → TilesetBuilder | 树节点 + 空间对象 | Tile层次结构 | spatial/builder | +| TilesetBuilder → ContentGenerator | 节点对象 | B3DM文件路径 | 业务层实现 | +| ContentGenerator → B3DM Writer | GLB数据 + Batch数据 | B3DM文件 | b3dm | +| TilesetBuilder → TilesetWriter | Tileset对象 | tileset.json | tileset | + +--- + +## 2. 与Tileset模块的对接 + +### 2.1 对接流程 + +```cpp +// 1. 从空间索引构建Tileset +template +tileset::Tileset TilesetBuilder::build( + const StrategyType& strategy, + const TilesetBuildConfig& config +) { + // 2. 递归构建Tile层次结构 + tileset::Tile rootTile = buildTileRecursive( + strategy, + strategy.getRootNode(), + config.outputPath, + "0", + config + ); + + // 3. 创建Tileset + tileset::Tileset tileset(rootTile); + tileset.setVersion("1.0"); + tileset.setGltfUpAxis("Z"); + + // 4. 计算根几何误差 + tileset.updateGeometricError(); + + return tileset; +} +``` + +### 2.2 Tile构建递归函数 + +```cpp +template +tileset::Tile TilesetBuilder::buildTileRecursive( + const StrategyType& strategy, + const void* node, + const std::string& parentPath, + const std::string& treePath, + const TilesetBuildConfig& config +) { + // 1. 获取节点信息 + auto bounds = strategy.getNodeBounds(node); + auto items = strategy.getNodeItems(node); + bool isLeaf = strategy.isLeafNode(node); + + // 2. 创建Tile的包围体 (ENU坐标) + tileset::Box boundingVolume = createBoundingVolume(bounds, config); + + // 3. 计算几何误差 + double geometricError = computeGeometricError(bounds, config.geometricErrorScale); + + // 4. 创建Tile + tileset::Tile tile(boundingVolume, geometricError); + + // 5. 生成内容 (如果是叶子节点或达到输出条件) + if (shouldGenerateContent(node, items, config)) { + std::string contentPath = generateContentPath(parentPath, treePath); + + // 调用内容生成器 (生成B3DM) + std::string contentUri = config.contentGenerator( + node, items, contentPath + ); + + if (!contentUri.empty()) { + tile.setContent(contentUri); + } + } + + // 6. 递归构建子节点 + if (!isLeaf) { + auto childNodes = strategy.getChildNodes(node); + int childIndex = 0; + for (const void* childNode : childNodes) { + std::string childTreePath = treePath + "_" + std::to_string(childIndex); + tileset::Tile childTile = buildTileRecursive( + strategy, childNode, parentPath, childTreePath, config + ); + tile.addChild(std::move(childTile)); + childIndex++; + } + } + + return tile; +} +``` + +### 2.3 包围体创建 + +```cpp +// 四叉树 (2D) → Box (ENU 3D) +tileset::Box createBoundingVolume2D( + const spatial::core::SpatialBounds& bounds2D, + const TilesetBuildConfig& config +) { + // 2D bounds (WGS84度) → ENU米 + double centerLon = (bounds2D.min[0] + bounds2D.max[0]) * 0.5; + double centerLat = (bounds2D.min[1] + bounds2D.max[1]) * 0.5; + + // 转换为ENU坐标 (相对于全局中心) + double offsetX = longti_to_meter( + degree2rad(centerLon - config.centerLongitude), + degree2rad(config.centerLatitude) + ); + double offsetY = lati_to_meter( + degree2rad(centerLat - config.centerLatitude) + ); + + // 计算半轴长度 + double halfWidth = longti_to_meter( + degree2rad(bounds2D.max[0] - centerLon), + degree2rad(centerLat) + ) * config.boundingVolumeScaleFactor; + + double halfHeight = lati_to_meter( + degree2rad(bounds2D.max[1] - centerLat) + ) * config.boundingVolumeScaleFactor; + + // 高度范围 (shp23dtile特有) + double halfZ = (config.maxHeight - config.minHeight) * 0.5 * + config.boundingVolumeScaleFactor; + + return tileset::Box::fromCenterAndHalfLengths( + offsetX, offsetY, halfZ, // 中心点 + halfWidth, halfHeight, halfZ // 半轴 + ); +} + +// 八叉树 (3D) → Box (ENU 3D) +tileset::Box createBoundingVolume3D( + const spatial::core::SpatialBounds& bounds3D, + const TilesetBuildConfig& config +) { + // 已经是ENU坐标,直接转换 + double cx = (bounds3D.min[0] + bounds3D.max[0]) * 0.5; + double cy = (bounds3D.min[1] + bounds3D.max[1]) * 0.5; + double cz = (bounds3D.min[2] + bounds3D.max[2]) * 0.5; + + double hx = (bounds3D.max[0] - bounds3D.min[0]) * 0.5 * + config.boundingVolumeScaleFactor; + double hy = (bounds3D.max[1] - bounds3D.min[1]) * 0.5 * + config.boundingVolumeScaleFactor; + double hz = (bounds3D.max[2] - bounds3D.min[2]) * 0.5 * + config.boundingVolumeScaleFactor; + + return tileset::Box::fromCenterAndHalfLengths(cx, cy, cz, hx, hy, hz); +} +``` + +### 2.4 根节点Transform (ENU → ECEF) + +```cpp +// 创建根节点Transform矩阵 (仅根节点需要) +std::optional createRootTransform( + const TilesetBuildConfig& config +) { + if (!config.applyRootTransform) { + return std::nullopt; + } + + // 使用CoordinateTransformer计算ENU→ECEF矩阵 + glm::dmat4 enuToEcef = coords::CoordinateTransformer::CalcEnuToEcefMatrix( + config.centerLongitude, + config.centerLatitude, + config.centerHeight + ); + + // 转换为tileset::TransformMatrix + tileset::TransformMatrix matrix; + for (int c = 0; c < 4; ++c) { + for (int r = 0; r < 4; ++r) { + matrix.values[c * 4 + r] = enuToEcef[c][r]; + } + } + + return matrix; +} + +// 在构建根Tile时应用 +if (isRootNode) { + auto transform = createRootTransform(config); + if (transform) { + tile.setTransform(*transform); + } +} +``` + +--- + +## 3. 与B3DM模块的对接 + +### 3.1 内容生成器接口 + +```cpp +/** + * @brief 内容生成器配置 + */ +struct ContentGeneratorConfig { + // 输出路径 + std::string outputRoot; + std::string contentPrefix = "tile"; + + // 地理坐标 (用于坐标转换) + double centerLongitude = 0.0; + double centerLatitude = 0.0; + double centerHeight = 0.0; + + // 优化选项 + bool enableDraco = false; + bool enableLOD = false; + + // BatchTable配置 + bool includeBatchTable = true; + std::vector batchAttributes; // 要包含的属性名 +}; + +/** + * @brief 内容生成器接口 + * + * 业务层需要实现此接口来生成B3DM内容 + */ +class IContentGenerator { +public: + virtual ~IContentGenerator() = default; + + /** + * @brief 生成节点内容 + * + * @param node 空间索引节点 (void* 避免模板暴露) + * @param items 节点内的空间对象 + * @param outputPath 输出路径 + * @return 生成的内容URI (相对于tileset.json的路径) + */ + virtual std::string generate( + const void* node, + const std::vector>& items, + const std::string& outputPath + ) = 0; +}; +``` + +### 3.2 B3DM生成流程 + +```cpp +/** + * @brief Shapefile B3DM生成器实现 + */ +class ShapefileContentGenerator : public IContentGenerator { +public: + ShapefileContentGenerator(const ContentGeneratorConfig& config) + : config_(config) {} + + std::string generate( + const void* node, + const std::vector>& items, + const std::string& outputPath + ) override { + if (items.empty()) { + return ""; + } + + // 1. 转换空间对象为几何数据 + std::vector> geometries; + std::vector> properties; + + for (auto& itemRef : items) { + auto* item = static_cast(itemRef.get()); + geometries.insert(geometries.end(), + item->geometries.begin(), + item->geometries.end()); + properties.push_back(item->properties); + } + + // 2. 合并几何体 + osg::ref_ptr mergedGeom = mergeGeometries(geometries); + + // 3. 创建GLTF模型 + tinygltf::Model gltfModel = createGLTFModel(mergedGeom, properties); + + // 4. 序列化为GLB + std::string glbBuffer = serializeToGLB(gltfModel); + + // 5. 创建Batch数据 + b3dm::BatchData batchData; + for (size_t i = 0; i < items.size(); ++i) { + batchData.batchIds.push_back(static_cast(i)); + // 添加其他属性... + } + + // 6. 包装为B3DM + b3dm::Options b3dmOptions; + std::string b3dmBuffer = b3dm::wrapGlbToB3dm(glbBuffer, batchData, b3dmOptions); + + // 7. 写入文件 + std::string fileName = "content.b3dm"; + std::string fullPath = outputPath + "/" + fileName; + b3dm::writeB3dmToFile(fullPath, b3dmBuffer); + + // 8. 返回相对URI + return fileName; + } + +private: + ContentGeneratorConfig config_; +}; +``` + +### 3.3 FBX B3DM生成器 (带LOD) + +```cpp +/** + * @brief FBX B3DM生成器实现 (支持LOD) + */ +class FBXContentGenerator : public IContentGenerator { +public: + FBXContentGenerator(const ContentGeneratorConfig& config, FBXLoader* loader) + : config_(config), loader_(loader) {} + + std::string generate( + const void* node, + const std::vector>& items, + const std::string& outputPath + ) override { + if (items.empty()) { + return ""; + } + + // 1. 转换空间对象 + std::vector fbxItems; + for (auto& itemRef : items) { + fbxItems.push_back(static_cast(itemRef.get())); + } + + // 2. 生成LOD链 (如果启用) + if (config_.enableLOD) { + return generateWithLOD(fbxItems, outputPath); + } else { + return generateSingle(fbxItems, outputPath); + } + } + +private: + std::string generateSingle( + const std::vector& items, + const std::string& outputPath + ) { + // 1. 合并几何体 + auto mergedGeom = mergeFBXGeometries(items); + + // 2. 应用简化 (如果启用) + if (config_.enableSimplify) { + simplifyGeometry(mergedGeom); + } + + // 3. 创建GLTF + tinygltf::Model gltfModel = createGLTFModel(mergedGeom, items); + + // 4. 应用Draco压缩 (如果启用) + if (config_.enableDraco) { + applyDracoCompression(gltfModel); + } + + // 5. 序列化并包装为B3DM + std::string glbBuffer = serializeToGLB(gltfModel); + b3dm::BatchData batchData = createBatchData(items); + std::string b3dmBuffer = b3dm::wrapGlbToB3dm(glbBuffer, batchData, {}); + + // 6. 写入文件 + std::string filePath = outputPath + "/content.b3dm"; + b3dm::writeB3dmToFile(filePath, b3dmBuffer); + + return "content.b3dm"; + } + + std::string generateWithLOD( + const std::vector& items, + const std::string& outputPath + ) { + // LOD比例: 1.0, 0.5, 0.25 + std::vector lodRatios = {1.0f, 0.5f, 0.25f}; + std::vector contentUris; + + for (size_t lodLevel = 0; lodLevel < lodRatios.size(); ++lodLevel) { + float ratio = lodRatios[lodLevel]; + + // 1. 合并几何体 + auto mergedGeom = mergeFBXGeometries(items); + + // 2. 应用简化 + SimplificationParams simParams; + simParams.target_ratio = ratio; + simplifyGeometry(mergedGeom, simParams); + + // 3. 创建GLTF + tinygltf::Model gltfModel = createGLTFModel(mergedGeom, items); + + // 4. 应用Draco (LOD0可选) + if (config_.enableDraco && (lodLevel > 0 || config_.dracoForLOD0)) { + applyDracoCompression(gltfModel); + } + + // 5. 生成B3DM + std::string glbBuffer = serializeToGLB(gltfModel); + b3dm::BatchData batchData = createBatchData(items); + std::string b3dmBuffer = b3dm::wrapGlbToB3dm(glbBuffer, batchData, {}); + + // 6. 写入文件 (content_lod0.b3dm, content_lod1.b3dm, ...) + std::string fileName = "content_lod" + std::to_string(lodLevel) + ".b3dm"; + std::string filePath = outputPath + "/" + fileName; + b3dm::writeB3dmToFile(filePath, b3dmBuffer); + + contentUris.push_back(fileName); + } + + // 返回多个URI (使用逗号分隔或返回主URI) + return contentUris[0]; // 主内容 + } + + ContentGeneratorConfig config_; + FBXLoader* loader_; +}; +``` + +--- + +## 4. 完整对接示例 + +### 4.1 shp23dtile完整流程 + +```cpp +// shp23dtile.cpp (迁移后) +void processShapefile(const std::string& inputPath, + const std::string& outputPath, + double centerLon, double centerLat) { + + // 1. 读取Shapefile并创建空间对象 + std::vector items = readShapefile(inputPath); + + // 2. 计算世界包围盒 + auto worldBounds = computeWorldBounds(items); + + // 3. 配置并构建四叉树 + spatial::strategy::QuadtreeConfig treeConfig; + treeConfig.maxDepth = 10; + treeConfig.maxItemsPerNode = 1000; + + spatial::strategy::QuadtreeStrategy strategy(treeConfig); + strategy.build(items, worldBounds); + + // 4. 配置内容生成器 + ContentGeneratorConfig contentConfig; + contentConfig.outputRoot = outputPath; + contentConfig.centerLongitude = centerLon; + contentConfig.centerLatitude = centerLat; + contentConfig.includeBatchTable = true; + + auto contentGenerator = std::make_shared(contentConfig); + + // 5. 配置Tileset构建器 + spatial::builder::TilesetBuildConfig tilesetConfig; + tilesetConfig.outputPath = outputPath; + tilesetConfig.centerLongitude = centerLon; + tilesetConfig.centerLatitude = centerLat; + tilesetConfig.applyRootTransform = true; + tilesetConfig.contentGenerator = contentGenerator; + + // 6. 构建Tileset + tileset::Tileset tileset = spatial::builder::TilesetBuilder::build( + strategy, tilesetConfig + ); + + // 7. 写入tileset.json + tileset::TilesetWriter writer; + writer.writeToFile(tileset, outputPath + "/tileset.json"); +} +``` + +### 4.2 FBXPipeline完整流程 + +```cpp +// FBXPipeline.cpp (迁移后) +void FBXPipeline::run() { + // 1. 加载FBX + FBXLoader loader(settings.inputPath); + loader.load(); + + // 2. 创建空间对象 + std::vector items = createSpatialItems(loader); + + // 3. 计算世界包围盒 + auto worldBounds = computeWorldBounds(items); + + // 4. 配置并构建八叉树 + spatial::strategy::OctreeConfig treeConfig; + treeConfig.maxDepth = settings.maxDepth; + treeConfig.maxItemsPerNode = settings.maxItemsPerTile; + + spatial::strategy::OctreeStrategy strategy(treeConfig); + strategy.build(items, worldBounds); + + // 5. 配置内容生成器 (支持LOD) + ContentGeneratorConfig contentConfig; + contentConfig.outputRoot = settings.outputPath; + contentConfig.centerLongitude = settings.longitude; + contentConfig.centerLatitude = settings.latitude; + contentConfig.centerHeight = settings.height; + contentConfig.enableDraco = settings.enableDraco; + contentConfig.enableLOD = settings.enableLOD; + + auto contentGenerator = std::make_shared( + contentConfig, &loader + ); + + // 6. 配置Tileset构建器 + spatial::builder::TilesetBuildConfig tilesetConfig; + tilesetConfig.outputPath = settings.outputPath; + tilesetConfig.centerLongitude = settings.longitude; + tilesetConfig.centerLatitude = settings.latitude; + tilesetConfig.centerHeight = settings.height; + tilesetConfig.applyRootTransform = true; + tilesetConfig.contentGenerator = contentGenerator; + + // 7. 构建Tileset + tileset::Tileset tileset = spatial::builder::TilesetBuilder::build( + strategy, tilesetConfig + ); + + // 8. 写入tileset.json + tileset::TilesetWriter writer; + writer.writeToFile(tileset, settings.outputPath + "/tileset.json"); +} +``` + +--- + +## 5. 关键对接代码实现 + +### 5.1 TilesetBuilder完整实现 + +```cpp +// spatial/builder/tileset_builder.h +#pragma once + +#include "../core/slicing_strategy.h" +#include "../../tileset/tileset_types.h" +#include "../../tileset/tileset_writer.h" +#include "../../tileset/bounding_volume.h" +#include +#include + +namespace spatial::builder { + +/** + * @brief 内容生成器回调类型 + */ +using ContentGenerator = std::function>& items, // 空间对象 + const std::string& outputPath // 输出路径 +)>; + +/** + * @brief Tileset构建配置 + */ +struct TilesetBuildConfig { + // 输出配置 + std::string outputPath; + std::string contentPrefix = "tile"; + + // 地理坐标配置 + double centerLongitude = 0.0; + double centerLatitude = 0.0; + double centerHeight = 0.0; + + // 包围体配置 + double boundingVolumeScaleFactor = 2.0; + double geometricErrorScale = 0.5; + double minHeight = 0.0; // shp23dtile特有 + double maxHeight = 100.0; // shp23dtile特有 + + // Transform配置 + bool applyRootTransform = true; + + // 内容生成器 + ContentGenerator contentGenerator; + + // 内容生成条件 + size_t minItemsForContent = 1; // 最小对象数才生成内容 + size_t maxDepthForContent = 100; // 最大深度才生成内容 +}; + +/** + * @brief Tileset构建器 + */ +class TilesetBuilder { +public: + /** + * @brief 从空间策略构建Tileset + */ + template + static tileset::Tileset build( + const StrategyType& strategy, + const TilesetBuildConfig& config + ) { + // 构建根Tile + tileset::Tile rootTile = buildTileRecursive( + strategy, + strategy.getRootNode(), + config.outputPath, + "0", + true, // isRoot + config + ); + + // 创建Tileset + tileset::Tileset tileset(rootTile); + tileset.setVersion("1.0"); + tileset.setGltfUpAxis("Z"); + tileset.updateGeometricError(); + + return tileset; + } + +private: + template + static tileset::Tile buildTileRecursive( + const StrategyType& strategy, + const void* node, + const std::string& parentPath, + const std::string& treePath, + bool isRoot, + const TilesetBuildConfig& config + ) { + using ItemType = typename StrategyType::ItemType; + + // 获取节点信息 + auto bounds = strategy.getNodeBounds(node); + auto items = strategy.getNodeItems(node); + bool isLeaf = strategy.isLeafNode(node); + + // 创建包围体 + tileset::BoundingVolume boundingVolume = createBoundingVolume( + bounds, config, StrategyType::Dimension + ); + + // 计算几何误差 + double geometricError = computeGeometricError(bounds, config.geometricErrorScale); + + // 创建Tile + tileset::Tile tile(boundingVolume, geometricError); + + // 应用根Transform + if (isRoot && config.applyRootTransform) { + auto transform = createRootTransform(config); + if (transform) { + tile.setTransform(*transform); + } + } + + // 生成内容 + if (shouldGenerateContent(items, isLeaf, config)) { + std::string nodePath = parentPath + "/" + config.contentPrefix + "_" + treePath; + std::filesystem::create_directories(nodePath); + + // 转换items为void* + std::vector> voidItems; + for (auto& item : items) { + voidItems.push_back(std::ref(static_cast(item.get()))); + } + + std::string contentUri = config.contentGenerator(node, voidItems, nodePath); + if (!contentUri.empty()) { + tile.setContent(contentUri); + } + } + + // 递归构建子节点 + if (!isLeaf) { + auto childNodes = strategy.getChildNodes(node); + int childIndex = 0; + for (const void* childNode : childNodes) { + std::string childTreePath = treePath + "_" + std::to_string(childIndex); + tileset::Tile childTile = buildTileRecursive( + strategy, childNode, parentPath, childTreePath, + false, config + ); + tile.addChild(std::move(childTile)); + childIndex++; + } + } + + return tile; + } + + // 辅助函数... + static tileset::BoundingVolume createBoundingVolume( + const auto& bounds, + const TilesetBuildConfig& config, + size_t dimension + ); + + static double computeGeometricError(const auto& bounds, double scale); + + static std::optional createRootTransform( + const TilesetBuildConfig& config + ); + + static bool shouldGenerateContent( + const auto& items, + bool isLeaf, + const TilesetBuildConfig& config + ) { + if (items.empty()) return false; + if (isLeaf) return true; + return items.size() >= config.minItemsForContent; + } +}; + +} // namespace spatial::builder +``` + +--- + +## 6. 输出文件结构 + +### 6.1 shp23dtile输出结构 +``` +output/ +├── tileset.json # 根tileset +└── tile/ + ├── 5/ + │ ├── 16/ + │ │ ├── 8/ + │ │ │ └── content.b3dm + │ │ └── 9/ + │ │ └── content.b3dm + │ └── 17/ + │ └── ... + └── 6/ + └── ... +``` + +### 6.2 FBXPipeline输出结构 (带LOD) +``` +output/ +├── tileset.json # 根tileset +└── tile/ + ├── 0_0/ + │ ├── content_lod0.b3dm # 精细 + │ ├── content_lod1.b3dm # 中等 + │ └── content_lod2.b3dm # 粗糙 + ├── 0_1/ + │ └── ... + └── ... +``` + +--- + +## 7. 注意事项 + +### 7.1 坐标系统 +- **shp23dtile**: WGS84(度) → ENU(米) → ECEF (通过Transform) +- **FBXPipeline**: ENU(米) → ECEF (通过Transform) + +### 7.2 内存管理 +- 空间对象通过`reference_wrapper`传递,避免拷贝 +- B3DM生成时合并几何体,减少DrawCall + +### 7.3 错误处理 +- 空节点跳过内容生成 +- 文件写入失败记录日志但不中断流程 +- 几何误差为0时设置默认值 + +### 7.4 性能优化 +- 使用线程池并行生成B3DM +- 预分配GLTF buffer大小 +- 批量写入文件 diff --git a/docs/spatial_indexing_abstraction.md b/docs/spatial_indexing_abstraction.md new file mode 100644 index 00000000..297f1c4b --- /dev/null +++ b/docs/spatial_indexing_abstraction.md @@ -0,0 +1,724 @@ +# 空间切片索引抽象技术方案 + +## 1. 概述 + +### 1.1 目标 +抽象四叉树(Quadtree)和八叉树(Octree)的核心数据结构,提供统一的空间切片接口,使shp23dtile和FBXPipeline能够使用一致的策略进行空间划分和3D Tiles生成。 + +### 1.2 现状分析 + +#### 1.2.1 shp23dtile - 四叉树实现 +- **文件**: `src/shp23dtile.cpp` +- **特点**: + - 2D空间划分(XY平面) + - 基于WGS84经纬度坐标 + - 每个节点包含4个子节点 (subnode[4]) + - 使用z/x/y编码标识瓦片位置 + - 支持metric阈值控制最小划分粒度 + - 业务数据结构:`shapefile::TileMeta`, `shapefile::TileBBox`, `shapefile::QuadtreeCoord` + +#### 1.2.2 FBXPipeline - 八叉树实现 +- **文件**: `src/FBXPipeline.h`, `src/FBXPipeline.cpp` +- **特点**: + - 3D空间划分(XYZ空间) + - 基于ENU局部坐标系(米) + - 每个节点包含8个子节点 + - 使用深度优先递归构建 + - 支持maxDepth和maxItemsPerTile控制 + - 内嵌结构体:`OctreeNode` + +### 1.3 设计原则 +1. **策略模式**: 四叉树和八叉树作为两种切片策略实现统一接口 +2. **类型安全**: 使用模板支持不同类型的空间对象 +3. **可扩展性**: 易于添加新的切片策略(如KD树、R树等) +4. **零开销抽象**: 编译期多态,无运行时开销 + +--- + +## 2. 核心接口设计 + +### 2.1 命名空间规划 +```cpp +namespace spatial { + // 核心抽象接口 + namespace core { + struct BoundingBox; // 通用包围盒 + struct SpatialIndex; // 空间索引基类 + struct SlicingStrategy; // 切片策略接口 + } + + // 策略实现 + namespace strategy { + struct QuadtreeStrategy; // 四叉树策略 + struct OctreeStrategy; // 八叉树策略 + } + + // 构建器 + namespace builder { + struct TilesetBuilder; // Tileset构建器 + } +} +``` + +### 2.2 核心数据结构 + +#### 2.2.1 通用包围盒 (SpatialBounds) +```cpp +namespace spatial::core { + +/** + * @brief 通用空间包围盒 + * + * 支持2D和3D空间,使用模板参数区分 + */ +template +struct SpatialBounds { + static_assert(Dim == 2 || Dim == 3, "Only 2D or 3D supported"); + + std::array min; + std::array max; + + // 2D构造器 + SpatialBounds(T minX, T minY, T maxX, T maxY) + requires (Dim == 2); + + // 3D构造器 + SpatialBounds(T minX, T minY, T minZ, T maxX, T maxY, T maxZ) + requires (Dim == 3); + + // 核心操作 + bool contains(const SpatialBounds& other) const; + bool intersects(const SpatialBounds& other) const; + SpatialBounds merge(const SpatialBounds& other) const; + + std::array center() const; + std::array size() const; + T volume() const; // 2D返回面积,3D返回体积 + + // 分割为子包围盒 + std::vector split(size_t childCount) const; +}; + +// 类型别名 +using Bounds2D = SpatialBounds; +using Bounds3D = SpatialBounds; + +} // namespace spatial::core +``` + +#### 2.2.2 空间对象接口 (SpatialItem) +```cpp +namespace spatial::core { + +/** + * @brief 空间对象概念 (C++20 Concept) + * + * 任何可以被放入空间索引的对象必须满足此概念 + */ +template +concept SpatialItem = requires(T item) { + { item.getBounds() } -> std::convertible_to>; + { item.getCenter() } -> std::convertible_to>; +}; + +/** + * @brief 空间对象包装器 + * + * 用于将现有数据结构适配到空间索引系统 + */ +template +struct SpatialItemWrapper { + T* item; + std::function(const T&)> boundsGetter; + + SpatialBounds getBounds() const { + return boundsGetter(*item); + } + + std::array getCenter() const { + auto bounds = getBounds(); + return bounds.center(); + } +}; + +} // namespace spatial::core +``` + +### 2.3 切片策略接口 + +#### 2.3.1 切片配置 (SlicingConfig) +```cpp +namespace spatial::core { + +/** + * @brief 切片策略配置基类 + */ +struct SlicingConfig { + virtual ~SlicingConfig() = default; + + // 通用配置 + size_t maxDepth = 10; // 最大深度 + size_t maxItemsPerNode = 1000; // 每个节点最大对象数 + double minSize = 0.01; // 最小划分尺寸(防止无限细分) +}; + +/** + * @brief 四叉树专用配置 + */ +struct QuadtreeConfig : SlicingConfig { + // 四叉树特有配置 + bool useZOrderCurve = true; // 是否使用Z序曲线编码 + bool strictContainment = false; // 是否严格包含(对象跨边界时的处理) +}; + +/** + * @brief 八叉树专用配置 + */ +struct OctreeConfig : SlicingConfig { + // 八叉树特有配置 + bool looseOctree = false; // 是否使用松散八叉树 + float loosenessFactor = 1.0f; // 松散系数 +}; + +} // namespace spatial::core +``` + +#### 2.3.2 切片策略接口 (ISlicingStrategy) +```cpp +namespace spatial::core { + +/** + * @brief 切片策略接口 + * + * 定义空间切片的通用操作 + */ +template +class ISlicingStrategy { +public: + using BoundsType = SpatialBounds; + using ItemRef = std::reference_wrapper; + + virtual ~ISlicingStrategy() = default; + + // 构建空间索引 + virtual void build(std::vector& items, const BoundsType& worldBounds) = 0; + + // 查询指定包围盒内的对象 + virtual std::vector query(const BoundsType& bounds) const = 0; + + // 查询指定点的最近对象 + virtual std::vector queryNearest(const std::array& point, + size_t maxResults) const = 0; + + // 获取所有叶子节点 + virtual std::vector getLeafNodes() const = 0; + + // 获取节点深度 + virtual size_t getMaxDepth() const = 0; + + // 获取节点包围盒 + virtual BoundsType getNodeBounds(const void* node) const = 0; + + // 获取节点内对象 + virtual std::vector getNodeItems(const void* node) const = 0; + + // 获取子节点 + virtual std::vector getChildNodes(const void* node) const = 0; + + // 判断是否为叶子节点 + virtual bool isLeafNode(const void* node) const = 0; + + // 获取根节点 + virtual const void* getRootNode() const = 0; +}; + +} // namespace spatial::core +``` + +--- + +## 3. 四叉树实现方案 + +### 3.1 四叉树节点结构 +```cpp +namespace spatial::strategy { + +/** + * @brief 四叉树节点 + */ +template +struct QuadtreeNode { + using BoundsType = core::SpatialBounds; + using ItemRef = std::reference_wrapper; + + // 空间信息 + BoundsType bounds; + size_t depth = 0; + + // 四叉树坐标 (z/x/y) + uint32_t z = 0; + uint32_t x = 0; + uint32_t y = 0; + + // 内容 + std::vector items; + + // 子节点 (4个或0个) + std::array, 4> children; + + // 状态 + bool isLeaf() const { return !children[0]; } + + // 编码为64位整数 + uint64_t encode() const { + return (static_cast(z) << 42) | + (static_cast(x) << 21) | + static_cast(y); + } + + // 从编码解码 + static std::tuple decode(uint64_t key) { + return { + static_cast((key >> 42) & 0x1FFFFF), + static_cast((key >> 21) & 0x1FFFFF), + static_cast(key & 0x1FFFFF) + }; + } +}; + +} // namespace spatial::strategy +``` + +### 3.2 四叉树策略实现 +```cpp +namespace spatial::strategy { + +/** + * @brief 四叉树切片策略 + */ +template +class QuadtreeStrategy : public core::ISlicingStrategy { +public: + using BaseType = core::ISlicingStrategy; + using BoundsType = typename BaseType::BoundsType; + using ItemRef = typename BaseType::ItemRef; + using NodeType = QuadtreeNode; + using ConfigType = core::QuadtreeConfig; + + explicit QuadtreeStrategy(const ConfigType& config = {}); + + // ISlicingStrategy 实现 + void build(std::vector& items, const BoundsType& worldBounds) override; + std::vector query(const BoundsType& bounds) const override; + std::vector queryNearest(const std::array& point, + size_t maxResults) const override; + std::vector getLeafNodes() const override; + size_t getMaxDepth() const override { return maxDepth_; } + BoundsType getNodeBounds(const void* node) const override; + std::vector getNodeItems(const void* node) const override; + std::vector getChildNodes(const void* node) const override; + bool isLeafNode(const void* node) const override; + const void* getRootNode() const override { return root_.get(); } + + // 四叉树特有接口 + const NodeType* getNodeByCoord(uint32_t z, uint32_t x, uint32_t y) const; + std::vector getNodesAtLevel(uint32_t level) const; + +private: + void splitNode(NodeType* node); + void distributeItems(NodeType* node, std::vector& items); + void collectLeafNodes(const NodeType* node, std::vector& leaves) const; + void queryRecursive(const NodeType* node, const BoundsType& bounds, + std::vector& results) const; + + std::unique_ptr root_; + ConfigType config_; + size_t maxDepth_ = 0; +}; + +} // namespace spatial::strategy +``` + +### 3.3 四叉树构建算法 +```cpp +template +void QuadtreeStrategy::build(std::vector& items, + const BoundsType& worldBounds) { + // 1. 创建根节点 + root_ = std::make_unique(); + root_->bounds = worldBounds; + root_->depth = 0; + root_->z = 0; + root_->x = 0; + root_->y = 0; + + // 2. 将所有对象加入根节点 + std::vector allItems; + allItems.reserve(items.size()); + for (auto& item : items) { + allItems.push_back(std::ref(item)); + } + + // 3. 递归构建 + buildRecursive(root_.get(), allItems); +} + +template +void QuadtreeStrategy::buildRecursive(NodeType* node, + std::vector& items) { + // 1. 过滤不在节点范围内的对象 + std::vector containedItems; + for (auto& item : items) { + if (node->bounds.intersects(item.get().getBounds())) { + containedItems.push_back(item); + } + } + + // 2. 检查终止条件 + if (node->depth >= config_.maxDepth || + containedItems.size() <= config_.maxItemsPerNode || + node->bounds.size()[0] < config_.minSize) { + node->items = std::move(containedItems); + maxDepth_ = std::max(maxDepth_, node->depth); + return; + } + + // 3. 分割节点 + splitNode(node); + + // 4. 分发对象到子节点 + distributeItems(node, containedItems); + + // 5. 递归构建子节点 + for (auto& child : node->children) { + if (!child->items.empty()) { + std::vector childItems = std::move(child->items); + buildRecursive(child.get(), childItems); + } + } +} + +template +void QuadtreeStrategy::splitNode(NodeType* node) { + double cx = (node->bounds.min[0] + node->bounds.max[0]) * 0.5; + double cy = (node->bounds.min[1] + node->bounds.max[1]) * 0.5; + + double minX = node->bounds.min[0]; + double minY = node->bounds.min[1]; + double maxX = node->bounds.max[0]; + double maxY = node->bounds.max[1]; + + // 创建4个子节点 (Z序: SW, SE, NW, NE) + // 0: SW (minX, minY) -> (cx, cy) + node->children[0] = std::make_unique(); + node->children[0]->bounds = BoundsType(minX, minY, cx, cy); + node->children[0]->depth = node->depth + 1; + node->children[0]->z = node->z + 1; + node->children[0]->x = node->x * 2; + node->children[0]->y = node->y * 2; + + // 1: SE (cx, minY) -> (maxX, cy) + node->children[1] = std::make_unique(); + node->children[1]->bounds = BoundsType(cx, minY, maxX, cy); + node->children[1]->depth = node->depth + 1; + node->children[1]->z = node->z + 1; + node->children[1]->x = node->x * 2 + 1; + node->children[1]->y = node->y * 2; + + // 2: NW (minX, cy) -> (cx, maxY) + node->children[2] = std::make_unique(); + node->children[2]->bounds = BoundsType(minX, cy, cx, maxY); + node->children[2]->depth = node->depth + 1; + node->children[2]->z = node->z + 1; + node->children[2]->x = node->x * 2; + node->children[2]->y = node->y * 2 + 1; + + // 3: NE (cx, cy) -> (maxX, maxY) + node->children[3] = std::make_unique(); + node->children[3]->bounds = BoundsType(cx, cy, maxX, maxY); + node->children[3]->depth = node->depth + 1; + node->children[3]->z = node->z + 1; + node->children[3]->x = node->x * 2 + 1; + node->children[3]->y = node->y * 2 + 1; +} +``` + +--- + +## 4. 八叉树实现方案 + +### 4.1 八叉树节点结构 +```cpp +namespace spatial::strategy { + +/** + * @brief 八叉树节点 + */ +template +struct OctreeNode { + using BoundsType = core::SpatialBounds; + using ItemRef = std::reference_wrapper; + + // 空间信息 + BoundsType bounds; + size_t depth = 0; + + // 内容 + std::vector items; + + // 子节点 (8个或0个) + // 索引: 0=左下后, 1=右下后, 2=左上前, 3=右上前, + // 4=左下前, 5=右下前, 6=左上后, 7=右上后 + std::array, 8> children; + + // 状态 + bool isLeaf() const { return !children[0]; } +}; + +} // namespace spatial::strategy +``` + +### 4.2 八叉树策略实现 +```cpp +namespace spatial::strategy { + +/** + * @brief 八叉树切片策略 + */ +template +class OctreeStrategy : public core::ISlicingStrategy { +public: + using BaseType = core::ISlicingStrategy; + using BoundsType = typename BaseType::BoundsType; + using ItemRef = typename BaseType::ItemRef; + using NodeType = OctreeNode; + using ConfigType = core::OctreeConfig; + + explicit OctreeStrategy(const ConfigType& config = {}); + + // ISlicingStrategy 实现 + void build(std::vector& items, const BoundsType& worldBounds) override; + std::vector query(const BoundsType& bounds) const override; + std::vector queryNearest(const std::array& point, + size_t maxResults) const override; + std::vector getLeafNodes() const override; + size_t getMaxDepth() const override { return maxDepth_; } + BoundsType getNodeBounds(const void* node) const override; + std::vector getNodeItems(const void* node) const override; + std::vector getChildNodes(const void* node) const override; + bool isLeafNode(const void* node) const override; + const void* getRootNode() const override { return root_.get(); } + +private: + void buildRecursive(NodeType* node, std::vector& items); + void splitNode(NodeType* node); + void distributeItems(NodeType* node, std::vector& items); + void collectLeafNodes(const NodeType* node, std::vector& leaves) const; + void queryRecursive(const NodeType* node, const BoundsType& bounds, + std::vector& results) const; + + std::unique_ptr root_; + ConfigType config_; + size_t maxDepth_ = 0; +}; + +} // namespace spatial::strategy +``` + +### 4.3 八叉树构建算法 +```cpp +template +void OctreeStrategy::splitNode(NodeType* node) { + double cx = (node->bounds.min[0] + node->bounds.max[0]) * 0.5; + double cy = (node->bounds.min[1] + node->bounds.max[1]) * 0.5; + double cz = (node->bounds.min[2] + node->bounds.max[2]) * 0.5; + + double minX = node->bounds.min[0]; + double minY = node->bounds.min[1]; + double minZ = node->bounds.min[2]; + double maxX = node->bounds.max[0]; + double maxY = node->bounds.max[1]; + double maxZ = node->bounds.max[2]; + + // 创建8个子节点 + // 0: 左下后 (minX, minY, minZ) -> (cx, cy, cz) + node->children[0] = std::make_unique(); + node->children[0]->bounds = BoundsType(minX, minY, minZ, cx, cy, cz); + node->children[0]->depth = node->depth + 1; + + // 1: 右下后 (cx, minY, minZ) -> (maxX, cy, cz) + node->children[1] = std::make_unique(); + node->children[1]->bounds = BoundsType(cx, minY, minZ, maxX, cy, cz); + node->children[1]->depth = node->depth + 1; + + // 2: 左上前 (minX, cy, minZ) -> (cx, maxY, cz) + node->children[2] = std::make_unique(); + node->children[2]->bounds = BoundsType(minX, cy, minZ, cx, maxY, cz); + node->children[2]->depth = node->depth + 1; + + // 3: 右上前 (cx, cy, minZ) -> (maxX, maxY, cz) + node->children[3] = std::make_unique(); + node->children[3]->bounds = BoundsType(cx, cy, minZ, maxX, maxY, cz); + node->children[3]->depth = node->depth + 1; + + // 4: 左下前 (minX, minY, cz) -> (cx, cy, maxZ) + node->children[4] = std::make_unique(); + node->children[4]->bounds = BoundsType(minX, minY, cz, cx, cy, maxZ); + node->children[4]->depth = node->depth + 1; + + // 5: 右下前 (cx, minY, cz) -> (maxX, cy, maxZ) + node->children[5] = std::make_unique(); + node->children[5]->bounds = BoundsType(cx, minY, cz, maxX, cy, maxZ); + node->children[5]->depth = node->depth + 1; + + // 6: 左上后 (minX, cy, cz) -> (cx, maxY, maxZ) + node->children[6] = std::make_unique(); + node->children[6]->bounds = BoundsType(minX, cy, cz, cx, maxY, maxZ); + node->children[6]->depth = node->depth + 1; + + // 7: 右上后 (cx, cy, cz) -> (maxX, maxY, maxZ) + node->children[7] = std::make_unique(); + node->children[7]->bounds = BoundsType(cx, cy, cz, maxX, maxY, maxZ); + node->children[7]->depth = node->depth + 1; +} +``` + +--- + +## 5. Tileset构建器 + +### 5.1 构建器接口 +```cpp +namespace spatial::builder { + +/** + * @brief Tileset构建配置 + */ +struct TilesetBuildConfig { + // 输出配置 + std::string outputPath; + std::string contentPrefix = "tile"; + + // 几何误差配置 + double geometricErrorScale = 0.5; + double baseGeometricError = 1000.0; + + // 坐标系统配置 + double centerLongitude = 0.0; + double centerLatitude = 0.0; + double centerHeight = 0.0; + + // 内容生成回调 + using ContentGenerator = std::function>& items, + const std::string& path + )>; + ContentGenerator contentGenerator; +}; + +/** + * @brief Tileset构建器 + * + * 将空间索引转换为3D Tiles Tileset + */ +class TilesetBuilder { +public: + /** + * @brief 从四叉树构建Tileset + */ + template + static tileset::Tileset buildFromQuadtree( + const strategy::QuadtreeStrategy& strategy, + const TilesetBuildConfig& config + ); + + /** + * @brief 从八叉树构建Tileset + */ + template + static tileset::Tileset buildFromOctree( + const strategy::OctreeStrategy& strategy, + const TilesetBuildConfig& config + ); + +private: + template + static tileset::Tileset buildInternal( + const StrategyType& strategy, + const TilesetBuildConfig& config + ); + + template + static tileset::Tile buildTileRecursive( + const StrategyType& strategy, + const void* node, + const std::string& path, + const TilesetBuildConfig& config + ); +}; + +} // namespace spatial::builder +``` + +--- + +## 6. 文件结构规划 + +``` +src/spatial/ +├── core/ +│ ├── spatial_bounds.h # 通用包围盒 +│ ├── spatial_bounds.cpp +│ ├── spatial_item.h # 空间对象接口 +│ ├── slicing_strategy.h # 切片策略接口 +│ └── slicing_config.h # 切片配置 +├── strategy/ +│ ├── quadtree_node.h # 四叉树节点 +│ ├── quadtree_strategy.h # 四叉树策略 +│ ├── quadtree_strategy.cpp +│ ├── octree_node.h # 八叉树节点 +│ ├── octree_strategy.h # 八叉树策略 +│ └── octree_strategy.cpp +├── builder/ +│ ├── tileset_builder.h # Tileset构建器 +│ └── tileset_builder.cpp +└── utils/ + ├── coordinate_utils.h # 坐标转换工具 + └── encoding_utils.h # 编码工具 (Z序等) +``` + +--- + +## 7. 关键技术点 + +### 7.1 对象分发策略 +1. **中心点法**: 使用对象中心点决定归属(简单,无重复) +2. **完全包含法**: 对象必须完全包含在子节点内(严格,可能保留在父节点) +3. **交集法**: 对象与多个子节点相交时复制到所有相交节点(查询精确,内存开销大) + +### 7.2 几何误差计算 +```cpp +double computeGeometricError(const SpatialBounds& bounds) { + auto size = bounds.size(); + double diagonal = std::sqrt( + size[0] * size[0] + + size[1] * size[1] + + (size.size() > 2 ? size[2] * size[2] : 0) + ); + return diagonal * config.geometricErrorScale; +} +``` + +### 7.3 坐标系统处理 +- **四叉树**: WGS84 (度) -> ENU (米) -> ECEF +- **八叉树**: ENU (米) -> ECEF + +--- + +## 8. 性能考虑 + +1. **内存布局**: 使用SOA (Structure of Arrays) 优化缓存友好性 +2. **并行构建**: 使用OpenMP或TBB并行化节点分割 +3. **延迟加载**: 支持按需加载深层节点 +4. **空间压缩**: 使用Z序曲线优化空间局部性 diff --git a/docs/unified_conversion_pipeline_design.md b/docs/unified_conversion_pipeline_design.md new file mode 100644 index 00000000..75e43225 --- /dev/null +++ b/docs/unified_conversion_pipeline_design.md @@ -0,0 +1,2644 @@ +# 统一数据转换管道设计方案 + +## 关键约束(必须遵守) + +### 1. Rust 调用 C++ 的接口保持不变 + +**Shapefile 转换接口:** +```rust +// Rust 侧(src/shape.rs) +#[repr(C)] +struct ShapeConversionParams { + input_path: *const c_char, + output_path: *const c_char, + height_field: *const c_char, + layer_id: i32, + enable_lod: bool, + draco_compression_params: DracoCompressionParams, + simplify_params: SimplificationParams, +} + +extern "C" { + fn shp23dtile(params: *const ShapeConversionParams) -> bool; +} +``` + +```cpp +// C++ 侧(src/shp23dtile.cpp) +extern "C" bool shp23dtile(const ShapeConversionParams* params); +``` + +**FBX 转换接口:** +```rust +// Rust 侧(src/fbx.rs) +extern "C" { + fn fbx23dtile( + in_path: *const u8, + out_path: *const u8, + box_ptr: *mut f64, + len: *mut i32, + max_lvl: i32, + enable_texture_compress: bool, + enable_meshopt: bool, + enable_draco: bool, + enable_unlit: bool, + longitude: f64, + latitude: f64, + height: f64, + enable_lod: bool, + ) -> *mut libc::c_void; +} +``` + +```cpp +// C++ 侧(src/fbx.cpp 或 src/FBXPipeline.cpp) +extern "C" void* fbx23dtile( + const char* in_path, + const char* out_path, + double* box_ptr, + int* len, + int max_lvl, + bool enable_texture_compress, + bool enable_meshopt, + bool enable_draco, + bool enable_unlit, + double longitude, + double latitude, + double height, + bool enable_lod +); +``` + +### 2. 使用命令行选项切换新旧流水线 + +不修改 CMakeLists.txt,通过命令行参数 `--use-new-pipeline` 控制: + +```bash +# 使用旧流水线(默认) +./target/debug/_3dtile -f shape -i input.shp -o output + +# 使用新流水线 +./target/debug/_3dtile -f shape -i input.shp -o output --use-new-pipeline +``` + +**实现机制:** +- Rust `main.rs` 解析 `--use-new-pipeline` 参数 +- 通过 FFI 设置 C++ 全局标志 `g_use_new_pipeline` +- C++ 侧 `shp23dtile()` 和 `fbx23dtile()` 根据标志决定走旧逻辑还是新管道 +- 新管道代码放在 `src/pipeline/` 目录,与旧代码共存 +- CMakeLists.txt 使用 `file(GLOB_RECURSE)` 自动收集所有文件,无需修改 + +### 3. 禁止重写业务逻辑,优先复用现有代码 + +**核心原则:** +- **禁止重新实现**任何业务逻辑(坐标变换、包围盒计算、geometricError 计算、LOD 生成等) +- **优先复用**现有代码,通过提取、包装、适配等方式使用 +- **必须保持**与重构前完全一致的计算结果(顶点坐标、法线、包围盒、geometricError 等) + +**复用方式:** +1. **直接类复用** - 直接使用现有类(如 `ShapefileDataPool`、`FBXLoader`) +2. **函数提取** - 将静态逻辑提取为公共工具函数 +3. **适配器包装** - 用适配器模式包装现有实现,统一接口 + +**验证要求:** +- 每个重构阶段必须能与基准数据对比验证 +- 顶点坐标误差必须 < 1e-6 +- 包围盒、geometricError 必须完全一致 +- B3DM 文件内容必须一致(或差异可解释) + +### 4. 业务逻辑确保和重构前完全一致 + +**一致性检查清单:** + +| 检查项 | 验证方法 | 通过标准 | +|--------|----------|----------| +| 顶点坐标 | 对比 B3DM 顶点数据 | 浮点误差 < 1e-6 | +| 法线向量 | 对比 B3DM 法线数据 | 向量值完全一致 | +| 包围盒 | 对比 tileset.json box | 数值完全一致 | +| geometricError | 对比 tileset.json | 数值完全一致 | +| 节点结构 | 对比 tileset.json 层级 | 节点数量和关系一致 | +| B3DM 文件 | 对比文件哈希 | 内容一致 | +| 纹理坐标 | 对比 B3DM UV 数据 | 数值完全一致 | +| 属性数据 | 对比 attributes.db | 数据完全一致 | + +**禁止修改的代码:** +- 坐标变换矩阵(Y-up 到 Z-up 转换) +- 包围盒计算逻辑 +- geometricError 计算公式 +- LOD 简化算法 +- Draco 压缩参数 +- 八叉树/四叉树分割逻辑 + +--- + +## 1. 设计概述 + +### 1.1 目标 + +- 统一 Shapefile 和 FBX 的转换流程,提高代码可扩展性 +- 支持未来轻松添加新数据类型(GeoJSON、CityGML、OSGB 等) +- 遵循 C++20 标准、Google C++ 规范和 C++ Core Guidelines +- 保持 Rust 与 C++ 的 FFI 接口清晰简洁 +- 将 Rust main.rs 中的业务逻辑迁移到专门的模块 + +### 1.2 核心设计原则 + +1. **单一职责原则 (SRP)**:每个类只负责一个功能 +2. **开闭原则 (OCP)**:对扩展开放,对修改关闭 +3. **依赖倒置原则 (DIP)**:依赖抽象接口,而非具体实现 +4. **组合优于继承**:通过组合实现功能复用 +5. **显式优于隐式**:地理参考配置必须显式指定 + +--- + +## 2. 架构设计 + +### 2.1 整体架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Rust 应用层 │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ main.rs (简洁入口) │ │ +│ │ └── 仅负责参数解析和模块调用 │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ converter/ 模块 (Rust业务逻辑) │ │ +│ │ ├── shapefile_converter.rs │ │ +│ │ ├── fbx_converter.rs │ │ +│ │ └── mod.rs │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ FFI │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ C++ 统一管道层 (Unified Pipeline) │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ConversionPipeline (模板方法模式) │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ Load │→ │ Index │→ │ Generate │→ │ Output │ │ │ +│ │ │ Data │ │ Space │ │ B3DM │ │ Tileset │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ┌─────────┴─────────┐ + ▼ ▼ +┌────────────────────────┐ ┌────────────────────────┐ +│ DataSource │ │ SpatialIndexStrategy │ +│ (抽象接口) │ │ (抽象接口) │ +├────────────────────────┤ ├────────────────────────┤ +│ + Load() │ │ + BuildIndex() │ +│ + GetSpatialItems() │ │ + Query() │ +│ + GetWorldBounds() │ │ + GetRootNode() │ +│ + CreateGeometryExtractor()│ │ +└────────────────────────┘ └────────────────────────┘ + ▲ ▲ ▲ + │ │ │ +┌──────┘ └──────┐ ┌───────┴───────┐ +│ │ │ │ +┌────────────────┐ ┌────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ ShapefileSource│ │ FBXSource │ │ShapefilePipeline │ │ FbxPipeline │ +│ (无地理参考) │ │ (需地理参考) │ │ (使用四叉树) │ │ (使用八叉树) │ +└────────────────┘ └────────────────┘ └──────────────────┘ └──────────────────┘ +``` + +### 2.2 关键区别:地理参考处理 + +| 数据源 | 地理参考 | 处理方式 | +|--------|----------|----------| +| **Shapefile** | 从文件解析 | 自动从 .shp 文件读取坐标系信息,转换为 WGS84 | +| **FBX** | 必须显式提供 | 通过命令行参数 `--lon`, `--lat`, `--alt` 指定 FBX 原点在地理空间中的位置 | + +--- + +## 3. 核心接口定义 (C++20) + +### 3.1 数据源接口 + +```cpp +// pipeline/data_source.h +#pragma once + +#include +#include +#include +#include +#include + +#include "common/geometry_extractor.h" +#include "spatial/core/spatial_bounds.h" +#include "spatial/core/spatial_item.h" + +namespace pipeline { + +// 数据源类型枚举 +enum class DataSourceType : std::uint8_t { + kShapefile = 0, + kFbx = 1, + kOsgb = 2, + kGeoJson = 3, + kCityGml = 4, + kUnknown = 255 +}; + +// 地理参考信息 +struct GeoReference { + // 对于 FBX:显式指定的原点坐标 + // 对于 Shapefile:从文件解析的中心点 + double center_longitude = 0.0; // 度 + double center_latitude = 0.0; // 度 + double center_height = 0.0; // 米(椭球高) + + // 坐标系 EPSG 代码 + int epsg_code = 4326; // 默认 WGS84 + + [[nodiscard]] bool IsValid() const noexcept { + return center_longitude >= -180.0 && center_longitude <= 180.0 && + center_latitude >= -90.0 && center_latitude <= 90.0; + } +}; + +// 数据源配置基类 +struct DataSourceConfig { + virtual ~DataSourceConfig() = default; + + std::filesystem::path input_path; + std::filesystem::path output_path; + + // 地理参考(FBX 必须显式提供,Shapefile 自动解析) + GeoReference geo_reference; + + // 处理选项 + bool enable_simplification = false; + SimplificationParams simplification_params; + + bool enable_draco = false; + DracoCompressionParams draco_params; + + bool enable_lod = false; + std::vector lod_levels; + + // 验证配置 + [[nodiscard]] virtual bool Validate() const { return true; } +}; + +// 数据源接口 - 纯抽象类 +class DataSource { + public: + virtual ~DataSource() = default; + + // 禁止拷贝,允许移动 + DataSource(const DataSource&) = delete; + DataSource& operator=(const DataSource&) = delete; + DataSource(DataSource&&) = default; + DataSource& operator=(DataSource&&) = default; + + [[nodiscard]] virtual DataSourceType GetType() const noexcept = 0; + + // 加载数据 - 纯虚函数 + [[nodiscard]] virtual bool Load(const DataSourceConfig& config) = 0; + + // 获取地理参考信息(加载后可用) + [[nodiscard]] virtual const GeoReference& GetGeoReference() const noexcept = 0; + + // 获取空间对象列表 + [[nodiscard]] virtual spatial::core::SpatialItemList GetSpatialItems() const = 0; + + // 获取世界包围盒 + [[nodiscard]] virtual spatial::core::SpatialBounds GetWorldBounds() const = 0; + + // 创建几何提取器 + [[nodiscard]] virtual std::unique_ptr + CreateGeometryExtractor() const = 0; + + [[nodiscard]] virtual std::size_t GetItemCount() const noexcept = 0; + + // 检查是否已加载 + [[nodiscard]] virtual bool IsLoaded() const noexcept = 0; + + protected: + DataSource() = default; +}; + +using DataSourcePtr = std::unique_ptr; + +// 数据源创建函数类型 +using DataSourceCreator = std::function; + +// 数据源工厂 - 单例模式 +class DataSourceFactory { + public: + [[nodiscard]] static DataSourceFactory& Instance() noexcept; + + void Register(DataSourceType type, DataSourceCreator creator); + [[nodiscard]] DataSourcePtr Create(DataSourceType type) const; + [[nodiscard]] bool IsRegistered(DataSourceType type) const noexcept; + + private: + DataSourceFactory() = default; + ~DataSourceFactory() = default; + + std::unordered_map creators_; +}; + +// 数据源注册辅助宏 +#define REGISTER_DATA_SOURCE(TYPE, CLASS) \ + namespace { \ + [[maybe_unused]] const bool _##CLASS##_registered = []() { \ + ::pipeline::DataSourceFactory::Instance().Register( \ + TYPE, []() -> ::pipeline::DataSourcePtr { \ + return std::make_unique(); \ + }); \ + return true; \ + }(); \ + } + +} // namespace pipeline +``` + +### 3.2 统一管道接口 + +```cpp +// pipeline/conversion_pipeline.h +#pragma once + +#include +#include +#include + +#include "b3dm/b3dm_generator.h" +#include "common/tile_meta.h" +#include "common/tileset_builder.h" +#include "data_source.h" +#include "spatial/core/slicing_strategy.h" + +namespace pipeline { + +// 转换结果 +struct ConversionResult { + bool success = false; + std::filesystem::path tileset_path; + std::size_t item_count = 0; + std::size_t node_count = 0; + std::size_t b3dm_count = 0; + std::string error_message; + + // C++20 显式 bool 转换 + [[nodiscard]] explicit operator bool() const noexcept { return success; } +}; + +// 管道配置 +struct PipelineConfig { + // 数据源配置(必须由调用方提供所有权) + std::unique_ptr source_config; + DataSourceType source_type; + + // 空间索引配置 + std::unique_ptr slicing_config; + + // B3DM 生成配置 + std::unique_ptr b3dm_config; + + // Tileset 构建配置 + std::unique_ptr tileset_config; + + // 验证配置完整性 + [[nodiscard]] bool Validate() const noexcept; +}; + +// 统一转换管道 - 模板方法模式 +class ConversionPipeline { + public: + explicit ConversionPipeline(PipelineConfig config); + virtual ~ConversionPipeline() = default; + + // 禁止拷贝,允许移动 + ConversionPipeline(const ConversionPipeline&) = delete; + ConversionPipeline& operator=(const ConversionPipeline&) = delete; + ConversionPipeline(ConversionPipeline&&) = default; + ConversionPipeline& operator=(ConversionPipeline&&) = default; + + // 执行完整的转换流程 - 模板方法 + [[nodiscard]] ConversionResult Execute(); + + protected: + // 模板方法步骤 - 可被子类覆盖 + [[nodiscard]] virtual bool LoadData(); + [[nodiscard]] virtual bool BuildSpatialIndex(); + [[nodiscard]] virtual bool GenerateB3DMFiles(); + [[nodiscard]] virtual bool GenerateTileset(); + + // 钩子方法 - 必须由子类实现 + [[nodiscard]] virtual std::unique_ptr + CreateSlicingStrategy() = 0; + + [[nodiscard]] virtual std::unique_ptr + CreateTileMetaFromNode(const spatial::core::SpatialIndexNode& node) = 0; + + [[nodiscard]] virtual common::BoundingBoxConverter + GetBoundingBoxConverter() const = 0; + + [[nodiscard]] virtual common::GeometricErrorCalculator + GetGeometricErrorCalculator() const = 0; + + // 访问器 + [[nodiscard]] const PipelineConfig& GetConfig() const noexcept { return config_; } + [[nodiscard]] DataSource& GetDataSource() noexcept { return *data_source_; } + + private: + PipelineConfig config_; + + // 执行状态 + DataSourcePtr data_source_; + std::unique_ptr spatial_index_; + std::unique_ptr b3dm_generator_; + common::TileMetaMap tile_meta_map_; + common::TileMetaPtr root_meta_; + + // 节点 ID 计数器 + int node_id_counter_ = 0; + + // 递归处理节点 + void ProcessNodeRecursive(const spatial::core::SpatialIndexNode& node, + std::optional parent_id); +}; + +// Shapefile 专用管道 - 使用四叉树策略 +class ShapefilePipeline : public ConversionPipeline { + public: + using ConversionPipeline::ConversionPipeline; + + protected: + [[nodiscard]] std::unique_ptr + CreateSlicingStrategy() override; +}; + +// FBX 专用管道 - 使用八叉树策略 +class FbxPipeline : public ConversionPipeline { + public: + using ConversionPipeline::ConversionPipeline; + + protected: + [[nodiscard]] std::unique_ptr + CreateSlicingStrategy() override; +}; + +// 工厂函数 +[[nodiscard]] std::unique_ptr CreatePipeline( + DataSourceType type, PipelineConfig config); + +} // namespace pipeline +``` + +### 3.3 Shapefile 专用配置和数据源 + +```cpp +// pipeline/shapefile_source.h +#pragma once + +#include "data_source.h" + +namespace pipeline { + +// Shapefile 专用配置 +struct ShapefileSourceConfig : public DataSourceConfig { + std::string height_field; // 高度字段名 + int layer_id = 0; // 图层 ID + + [[nodiscard]] bool Validate() const override { + if (!DataSourceConfig::Validate()) return false; + // Shapefile 不需要显式地理参考,从文件自动解析 + return true; + } +}; + +// Shapefile 数据源实现 +class ShapefileSource : public DataSource { + public: + ShapefileSource() = default; + ~ShapefileSource() override = default; + + [[nodiscard]] DataSourceType GetType() const noexcept override { + return DataSourceType::kShapefile; + } + + [[nodiscard]] bool Load(const DataSourceConfig& config) override; + [[nodiscard]] const GeoReference& GetGeoReference() const noexcept override; + [[nodiscard]] spatial::core::SpatialItemList GetSpatialItems() const override; + [[nodiscard]] spatial::core::SpatialBounds GetWorldBounds() const override; + [[nodiscard]] std::unique_ptr + CreateGeometryExtractor() const override; + [[nodiscard]] std::size_t GetItemCount() const noexcept override; + [[nodiscard]] bool IsLoaded() const noexcept override; + + private: + class Impl; // PIMPL 模式隐藏实现细节 + std::unique_ptr impl_; +}; + +REGISTER_DATA_SOURCE(DataSourceType::kShapefile, ShapefileSource) + +} // namespace pipeline +``` + +### 3.4 FBX 专用配置和数据源 + +```cpp +// pipeline/fbx_source.h +#pragma once + +#include "data_source.h" + +namespace pipeline { + +// FBX 专用配置 +struct FbxSourceConfig : public DataSourceConfig { + bool load_textures = true; + bool convert_to_y_up = true; + + [[nodiscard]] bool Validate() const override { + if (!DataSourceConfig::Validate()) return false; + // FBX 必须提供有效的地理参考 + if (!geo_reference.IsValid()) { + return false; + } + return true; + } +}; + +// FBX 数据源实现 +class FbxSource : public DataSource { + public: + FbxSource() = default; + ~FbxSource() override = default; + + [[nodiscard]] DataSourceType GetType() const noexcept override { + return DataSourceType::kFbx; + } + + [[nodiscard]] bool Load(const DataSourceConfig& config) override; + [[nodiscard]] const GeoReference& GetGeoReference() const noexcept override; + [[nodiscard]] spatial::core::SpatialItemList GetSpatialItems() const override; + [[nodiscard]] spatial::core::SpatialBounds GetWorldBounds() const override; + [[nodiscard]] std::unique_ptr + CreateGeometryExtractor() const override; + [[nodiscard]] std::size_t GetItemCount() const noexcept override; + [[nodiscard]] bool IsLoaded() const noexcept override; + + private: + class Impl; + std::unique_ptr impl_; +}; + +REGISTER_DATA_SOURCE(DataSourceType::kFbx, FbxSource) + +} // namespace pipeline +``` + +--- + +## 4. Rust FFI 接口设计 + +### 4.1 Rust 侧模块结构 + +``` +src/ +├── main.rs # 简洁入口,仅参数解析 +├── lib.rs # 库入口(如果需要) +├── common.rs # 通用工具函数 +├── error.rs # 错误处理 +├── fun_c.rs # C 函数导出(供 C++ 调用) +├── ffi/ # FFI 接口模块 +│ ├── mod.rs # FFI 模块入口 +│ ├── c_api.rs # C API 定义 +│ └── types.rs # FFI 类型定义 +└── converter/ # 转换器业务逻辑 + ├── mod.rs # 模块入口 + ├── shapefile.rs # Shapefile 转换逻辑 + ├── fbx.rs # FBX 转换逻辑 + └── osgb.rs # OSGB 转换逻辑 +``` + +### 4.2 FFI 类型定义 + +```rust +// src/ffi/types.rs +use libc::{c_char, c_double, c_int, c_void}; + +/// 数据源类型 +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CDataSourceType { + Shapefile = 0, + Fbx = 1, + Osgb = 2, +} + +/// 地理参考信息 +#[repr(C)] +pub struct CGeoReference { + pub center_longitude: c_double, + pub center_latitude: c_double, + pub center_height: c_double, + pub epsg_code: c_int, +} + +impl Default for CGeoReference { + fn default() -> Self { + Self { + center_longitude: 0.0, + center_latitude: 0.0, + center_height: 0.0, + epsg_code: 4326, + } + } +} + +/// Draco 压缩参数 +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CDracoCompressionParams { + pub position_quantization_bits: c_int, + pub normal_quantization_bits: c_int, + pub tex_coord_quantization_bits: c_int, + pub generic_quantization_bits: c_int, + pub enable_compression: bool, +} + +impl Default for CDracoCompressionParams { + fn default() -> Self { + Self { + position_quantization_bits: 11, + normal_quantization_bits: 10, + tex_coord_quantization_bits: 12, + generic_quantization_bits: 8, + enable_compression: false, + } + } +} + +/// 简化参数 +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CSimplificationParams { + pub target_error: c_float, + pub target_ratio: c_float, + pub enable_simplification: bool, + pub preserve_texture_coords: bool, + pub preserve_normals: bool, +} + +impl Default for CSimplificationParams { + fn default() -> Self { + Self { + target_error: 0.01, + target_ratio: 0.5, + enable_simplification: false, + preserve_texture_coords: true, + preserve_normals: true, + } + } +} + +/// LOD 级别设置 +#[repr(C)] +pub struct CLodLevelSettings { + pub level: c_int, + pub geometric_error: c_double, + pub simplification_ratio: c_float, +} + +/// 转换参数 +#[repr(C)] +pub struct CConversionParams { + pub input_path: *const c_char, + pub output_path: *const c_char, + pub source_type: CDataSourceType, + pub geo_reference: CGeoReference, + pub enable_lod: bool, + pub enable_simplification: bool, + pub enable_draco: bool, + pub enable_texture_compress: bool, + pub enable_unlit: bool, + pub draco_params: CDracoCompressionParams, + pub simplify_params: CSimplificationParams, + pub max_depth: c_int, + pub max_items_per_node: c_int, + pub height_field: *const c_char, // Shapefile 专用 + pub layer_id: c_int, // Shapefile 专用 +} + +/// 转换结果 +#[repr(C)] +pub struct CConversionResult { + pub success: bool, + pub item_count: usize, + pub node_count: usize, + pub b3dm_count: usize, + pub error_message: *mut c_char, +} +``` + +### 4.3 Rust 转换器模块 + +```rust +// src/converter/mod.rs +use std::path::Path; + +pub mod shapefile; +pub mod fbx; +pub mod osgb; + +#[derive(Debug, Clone)] +pub enum DataSourceType { + Shapefile, + Fbx, + Osgb, +} + +#[derive(Debug, Clone)] +pub struct GeoReference { + pub center_longitude: f64, + pub center_latitude: f64, + pub center_height: f64, + pub epsg_code: i32, +} + +impl Default for GeoReference { + fn default() -> Self { + Self { + center_longitude: 0.0, + center_latitude: 0.0, + center_height: 0.0, + epsg_code: 4326, + } + } +} + +#[derive(Debug, Clone)] +pub struct ConversionParams { + pub input_path: String, + pub output_path: String, + pub source_type: DataSourceType, + pub geo_reference: GeoReference, + pub enable_lod: bool, + pub enable_simplification: bool, + pub enable_draco: bool, + pub enable_texture_compress: bool, + pub enable_unlit: bool, + pub draco_params: crate::ffi::types::CDracoCompressionParams, + pub simplify_params: crate::ffi::types::CSimplificationParams, + pub max_depth: i32, + pub max_items_per_node: i32, + pub height_field: Option, + pub layer_id: i32, +} + +impl ConversionParams { + pub fn to_c_params(&self) -> crate::ffi::types::CConversionParams { + use std::ffi::CString; + use crate::ffi::types::*; + + CConversionParams { + input_path: CString::new(self.input_path.clone()).unwrap().into_raw(), + output_path: CString::new(self.output_path.clone()).unwrap().into_raw(), + source_type: match self.source_type { + DataSourceType::Shapefile => CDataSourceType::Shapefile, + DataSourceType::Fbx => CDataSourceType::Fbx, + DataSourceType::Osgb => CDataSourceType::Osgb, + }, + geo_reference: CGeoReference { + center_longitude: self.geo_reference.center_longitude, + center_latitude: self.geo_reference.center_latitude, + center_height: self.geo_reference.center_height, + epsg_code: self.geo_reference.epsg_code, + }, + enable_lod: self.enable_lod, + enable_simplification: self.enable_simplification, + enable_draco: self.enable_draco, + enable_texture_compress: self.enable_texture_compress, + enable_unlit: self.enable_unlit, + draco_params: self.draco_params, + simplify_params: self.simplify_params, + max_depth: self.max_depth, + max_items_per_node: self.max_items_per_node, + height_field: self.height_field.as_ref() + .map(|s| CString::new(s.clone()).unwrap().into_raw()) + .unwrap_or(std::ptr::null()), + layer_id: self.layer_id, + } + } +} + +pub trait Converter { + fn convert(input: &Path, output: &Path, params: &ConversionParams) -> Result<(), Box>; +} +``` + +### 4.4 Shapefile 转换器 + +```rust +// src/converter/shapefile.rs +use std::path::Path; +use crate::converter::{ConversionParams, Converter}; +use crate::ffi::types::*; + +pub struct ShapefileConverter; + +impl Converter for ShapefileConverter { + fn convert( + input: &Path, + output: &Path, + params: &ConversionParams, + ) -> Result<(), Box> { + let mut c_params = params.to_c_params(); + + // Shapefile 不需要显式地理参考,从文件自动解析 + // 但如果有提供,也可以使用 + + let result = unsafe { convert_with_pipeline(&c_params) }; + + // 释放分配的字符串内存 + unsafe { + if !c_params.input_path.is_null() { + let _ = std::ffi::CString::from_raw(c_params.input_path as *mut _); + } + if !c_params.output_path.is_null() { + let _ = std::ffi::CString::from_raw(c_params.output_path as *mut _); + } + if !c_params.height_field.is_null() { + let _ = std::ffi::CString::from_raw(c_params.height_field as *mut _); + } + } + + if result.success { + log::info!("Shapefile conversion successful: {} items, {} B3DM files", + result.item_count, result.b3dm_count); + Ok(()) + } else { + let error = unsafe { + if !result.error_message.is_null() { + let msg = std::ffi::CStr::from_ptr(result.error_message) + .to_string_lossy() + .into_owned(); + libc::free(result.error_message as *mut _); + msg + } else { + "Unknown error".to_string() + } + }; + Err(error.into()) + } + } +} + +// 对外暴露的便捷函数 +pub fn convert( + input: &str, + output: &str, + params: &ConversionParams, +) -> Result<(), Box> { + ShapefileConverter::convert( + Path::new(input), + Path::new(output), + params, + ) +} + +extern "C" { + fn convert_with_pipeline(params: *const CConversionParams) -> CConversionResult; +} +``` + +### 4.5 FBX 转换器 + +```rust +// src/converter/fbx.rs +use std::path::Path; +use crate::converter::{ConversionParams, Converter, DataSourceType, GeoReference}; +use crate::ffi::types::*; + +pub struct FbxConverter; + +impl Converter for FbxConverter { + fn convert( + input: &Path, + output: &Path, + params: &ConversionParams, + ) -> Result<(), Box> { + // FBX 必须提供有效的地理参考 + if !params.geo_reference.is_valid() { + return Err("FBX conversion requires valid geo-reference (lon, lat, alt)".into()); + } + + let mut c_params = params.to_c_params(); + + let result = unsafe { convert_with_pipeline(&c_params) }; + + // 释放内存 + unsafe { + if !c_params.input_path.is_null() { + let _ = std::ffi::CString::from_raw(c_params.input_path as *mut _); + } + if !c_params.output_path.is_null() { + let _ = std::ffi::CString::from_raw(c_params.output_path as *mut _); + } + } + + if result.success { + log::info!("FBX conversion successful: {} items, {} B3DM files", + result.item_count, result.b3dm_count); + Ok(()) + } else { + let error = unsafe { + if !result.error_message.is_null() { + let msg = std::ffi::CStr::from_ptr(result.error_message) + .to_string_lossy() + .into_owned(); + libc::free(result.error_message as *mut _); + msg + } else { + "Unknown error".to_string() + } + }; + Err(error.into()) + } + } +} + +impl GeoReference { + fn is_valid(&self) -> bool { + self.center_longitude >= -180.0 && self.center_longitude <= 180.0 && + self.center_latitude >= -90.0 && self.center_latitude <= 90.0 + } +} + +pub fn convert( + input: &str, + output: &str, + params: &ConversionParams, +) -> Result<(), Box> { + FbxConverter::convert( + Path::new(input), + Path::new(output), + params, + ) +} + +extern "C" { + fn convert_with_pipeline(params: *const CConversionParams) -> CConversionResult; +} +``` + +### 4.6 简化后的 main.rs + +```rust +// src/main.rs +use clap::{Arg, ArgAction, Command}; +use log::LevelFilter; +use std::io::Write; +use chrono::prelude::*; + +// 迁移后的模块 +mod common; +mod converter; +mod error; +mod ffi; +mod fun_c; +mod utils; + +use converter::{ConversionParams, DataSourceType, GeoReference}; + +fn main() { + setup_environment(); + init_logger(); + + let matches = build_cli().get_matches(); + + let input = matches.get_one::("input").expect("input is required"); + let output = matches.get_one::("output").expect("output is required"); + let format = matches.get_one::("format").expect("format is required"); + + // 构建转换参数 + let params = build_conversion_params(&matches); + + // 执行转换 + match format.as_str() { + "shape" => converter::shapefile::convert(input, output, params), + "fbx" => converter::fbx::convert(input, output, params), + "osgb" => converter::osgb::convert(input, output, params), + _ => { + log::error!("Unsupported format: {}", format); + std::process::exit(1); + } + } +} + +fn build_cli() -> Command { + Command::new("3dtiles") + .version("1.0") + .about("Unified 3D Tiles conversion tool") + .arg(Arg::new("input") + .short('i') + .long("input") + .required(true) + .help("Input file path")) + .arg(Arg::new("output") + .short('o') + .long("output") + .required(true) + .help("Output directory path")) + .arg(Arg::new("format") + .short('f') + .long("format") + .required(true) + .value_parser(["shape", "fbx", "osgb"]) + .help("Input format")) + // FBX 地理参考参数(FBX 必需) + .arg(Arg::new("longitude") + .long("lon") + .help("Longitude for FBX origin (required for FBX)") + .num_args(1)) + .arg(Arg::new("latitude") + .long("lat") + .help("Latitude for FBX origin (required for FBX)") + .num_args(1)) + .arg(Arg::new("altitude") + .long("alt") + .help("Altitude for FBX origin") + .num_args(1)) + // Shapefile 专用参数 + .arg(Arg::new("height-field") + .long("height") + .help("Height field name for Shapefile") + .num_args(1)) + // 通用功能开关 + .arg(Arg::new("enable-lod") + .long("enable-lod") + .action(ArgAction::SetTrue) + .help("Enable LOD generation")) + .arg(Arg::new("enable-draco") + .long("enable-draco") + .action(ArgAction::SetTrue) + .help("Enable Draco compression")) + .arg(Arg::new("enable-simplify") + .long("enable-simplify") + .action(ArgAction::SetTrue) + .help("Enable mesh simplification")) + .arg(Arg::new("enable-texture-compress") + .long("enable-texture-compress") + .action(ArgAction::SetTrue) + .help("Enable texture compression (KTX2)")) + .arg(Arg::new("use-new-pipeline") + .long("use-new-pipeline") + .action(ArgAction::SetTrue) + .help("Use new unified pipeline (experimental)")) +} + +fn build_conversion_params(matches: &clap::ArgMatches) -> ConversionParams { + ConversionParams { + input_path: matches.get_one::("input").unwrap().clone(), + output_path: matches.get_one::("output").unwrap().clone(), + source_type: parse_source_type(matches.get_one::("format").unwrap()), + geo_reference: GeoReference { + center_longitude: matches.get_one::("longitude") + .and_then(|s| s.parse().ok()) + .unwrap_or(0.0), + center_latitude: matches.get_one::("latitude") + .and_then(|s| s.parse().ok()) + .unwrap_or(0.0), + center_height: matches.get_one::("altitude") + .and_then(|s| s.parse().ok()) + .unwrap_or(0.0), + epsg_code: 4326, + }, + enable_lod: matches.get_flag("enable-lod"), + enable_simplification: matches.get_flag("enable-simplify"), + enable_draco: matches.get_flag("enable-draco"), + enable_texture_compress: matches.get_flag("enable-texture-compress"), + enable_unlit: false, + draco_params: Default::default(), + simplify_params: Default::default(), + max_depth: 10, + max_items_per_node: 1000, + height_field: matches.get_one::("height-field").cloned(), + layer_id: 0, + } +} + +fn parse_source_type(format: &str) -> DataSourceType { + match format { + "shape" => DataSourceType::Shapefile, + "fbx" => DataSourceType::Fbx, + "osgb" => DataSourceType::Osgb, + _ => panic!("Unsupported format: {}", format), + } +} + +fn setup_environment() { + // 环境设置代码... +} + +fn init_logger() { + env_logger::Builder::from_default_env() + .format(|buf, record| { + let dt = Local::now(); + writeln!( + buf, + "{}: {} - {}", + record.level(), + dt.format("%Y-%m-%d %H:%M:%S"), + record.args() + ) + }) + .filter(None, LevelFilter::Info) + .init(); +} +``` + +--- + +## 5. C++ FFI 实现 + +### 5.1 保持旧接口不变,内部转发到新管道 + +```cpp +// src/shp23dtile.cpp - 修改现有文件,添加新管道转发 +#include "pipeline/conversion_pipeline.h" + +// 全局标志,由 Rust 通过 FFI 设置 +extern "C" bool g_use_new_pipeline = false; + +// 设置全局标志(供 Rust 调用) +extern "C" void set_use_new_pipeline(bool use_new) { + g_use_new_pipeline = use_new; +} + +// 原有接口保持不变 +extern "C" bool shp23dtile(const ShapeConversionParams* params) { + if (g_use_new_pipeline) { + // 转发到新管道 + return shp23dtile_new_pipeline(params); + } + // 原有旧实现 + return shp23dtile_legacy(params); +} + +// 旧实现(原有代码移动到这里) +bool shp23dtile_legacy(const ShapeConversionParams* params) { + // ... 原有实现不变 +} + +// 新管道包装器 +bool shp23dtile_new_pipeline(const ShapeConversionParams* params) { + // 将旧参数转换为新管道参数 + CConversionParams c_params{}; + c_params.input_path = params->input_path; + c_params.output_path = params->output_path; + c_params.source_type = kShapefile; + c_params.height_field = params->height_field; + c_params.layer_id = params->layer_id; + c_params.enable_lod = params->enable_lod; + // ... 其他参数转换 + + auto result = convert_with_pipeline(&c_params); + return result.success; +} +``` + +```cpp +// src/fbx.cpp - 修改现有文件,添加新管道转发 +#include "pipeline/conversion_pipeline.h" + +// 原有接口保持不变 +extern "C" void* fbx23dtile( + const char* in_path, + const char* out_path, + double* box_ptr, + int* len, + int max_lvl, + bool enable_texture_compress, + bool enable_meshopt, + bool enable_draco, + bool enable_unlit, + double longitude, + double latitude, + double height, + bool enable_lod +) { + if (g_use_new_pipeline) { + // 转发到新管道 + return fbx23dtile_new_pipeline(...); + } + // 原有旧实现 + return fbx23dtile_legacy(...); +} +``` + +### 5.2 新管道内部实现 + +```cpp +// src/pipeline/ffi_bridge.cpp +#include "ffi_bridge.h" + +#include +#include + +#include "conversion_pipeline.h" +#include "fbx_source.h" +#include "shapefile_source.h" + +extern "C" { + +// C 结构体定义(新管道内部使用) +enum CDataSourceType : uint8_t { + kShapefile = 0, + kFbx = 1, + kOsgb = 2, +}; + +struct CGeoReference { + double center_longitude; + double center_latitude; + double center_height; + int epsg_code; +}; + +struct CDracoCompressionParams { + int position_quantization_bits; + int normal_quantization_bits; + int tex_coord_quantization_bits; + int generic_quantization_bits; + bool enable_compression; +}; + +struct CSimplificationParams { + float target_error; + float target_ratio; + bool enable_simplification; + bool preserve_texture_coords; + bool preserve_normals; +}; + +struct CConversionParams { + const char* input_path; + const char* output_path; + CDataSourceType source_type; + CGeoReference geo_reference; + bool enable_lod; + bool enable_simplification; + bool enable_draco; + bool enable_texture_compress; + bool enable_unlit; + CDracoCompressionParams draco_params; + CSimplificationParams simplify_params; + int max_depth; + int max_items_per_node; + const char* height_field; + int layer_id; +}; + +struct CConversionResult { + bool success; + size_t item_count; + size_t node_count; + size_t b3dm_count; + char* error_message; +}; + +// 新管道统一转换入口(内部使用) +CConversionResult convert_with_pipeline(const CConversionParams* c_params) { + CConversionResult result{}; + + if (c_params == nullptr) { + result.success = false; + const char* msg = "Null params pointer"; + result.error_message = new char[strlen(msg) + 1]; + strcpy(result.error_message, msg); + return result; + } + + try { + // 构建管道配置 + auto config = BuildPipelineConfig(*c_params); + + // 创建并执行管道 + auto pipeline = pipeline::CreatePipeline( + static_cast(c_params->source_type), + std::move(config)); + + if (!pipeline) { + result.success = false; + const char* msg = "Failed to create pipeline"; + result.error_message = new char[strlen(msg) + 1]; + strcpy(result.error_message, msg); + return result; + } + + auto conversion_result = pipeline->Execute(); + + result.success = conversion_result.success; + result.item_count = conversion_result.item_count; + result.node_count = conversion_result.node_count; + result.b3dm_count = conversion_result.b3dm_count; + + if (!conversion_result.success) { + result.error_message = new char[conversion_result.error_message.size() + 1]; + strcpy(result.error_message, conversion_result.error_message.c_str()); + } + + } catch (const std::exception& e) { + result.success = false; + result.error_message = new char[strlen(e.what()) + 1]; + strcpy(result.error_message, e.what()); + } + + return result; +} + +// 释放结果内存 +void free_conversion_result(CConversionResult* result) { + if (result && result->error_message) { + delete[] result->error_message; + result->error_message = nullptr; + } +} + +} // extern "C" + +namespace pipeline { + +namespace { + +PipelineConfig BuildPipelineConfig(const CConversionParams& c_params) { + PipelineConfig config; + + // 根据类型构建数据源配置 + switch (c_params.source_type) { + case kShapefile: { + auto source_config = std::make_unique(); + source_config->input_path = c_params.input_path; + source_config->output_path = c_params.output_path; + if (c_params.height_field) { + source_config->height_field = c_params.height_field; + } + source_config->layer_id = c_params.layer_id; + source_config->enable_lod = c_params.enable_lod; + source_config->enable_simplification = c_params.enable_simplification; + source_config->enable_draco = c_params.enable_draco; + // ... 其他参数 + config.source_config = std::move(source_config); + config.source_type = DataSourceType::kShapefile; + break; + } + case kFbx: { + auto source_config = std::make_unique(); + source_config->input_path = c_params.input_path; + source_config->output_path = c_params.output_path; + source_config->geo_reference.center_longitude = c_params.geo_reference.center_longitude; + source_config->geo_reference.center_latitude = c_params.geo_reference.center_latitude; + source_config->geo_reference.center_height = c_params.geo_reference.center_height; + source_config->geo_reference.epsg_code = c_params.geo_reference.epsg_code; + source_config->enable_lod = c_params.enable_lod; + source_config->enable_simplification = c_params.enable_simplification; + source_config->enable_draco = c_params.enable_draco; + // ... 其他参数 + config.source_config = std::move(source_config); + config.source_type = DataSourceType::kFbx; + break; + } + default: + throw std::invalid_argument("Unknown source type"); + } + + // 空间索引配置 + auto slicing_config = std::make_unique(); + slicing_config->max_depth = c_params.max_depth; + slicing_config->max_items_per_node = c_params.max_items_per_node; + config.slicing_config = std::move(slicing_config); + + // B3DM 配置 + auto b3dm_config = std::make_unique(); + b3dm_config->enable_simplification = c_params.enable_simplification; + b3dm_config->enable_draco = c_params.enable_draco; + // ... 其他参数 + config.b3dm_config = std::move(b3dm_config); + + // Tileset 配置 + config.tileset_config = std::make_unique(); + + return config; +} + +} // namespace +} // namespace pipeline +``` + +--- + +## 6. 代码复用与一致性保证策略 + +### 6.1 核心原则:**复用现有代码,而非重写** + +为确保新接口实现与旧逻辑完全一致,所有关键算法必须**直接复用**现有代码实现,而非重新编写。 + +### 6.2 必须复用的关键代码模块 + +| 功能模块 | 现有代码位置 | 复用方式 | 一致性风险 | +|----------|--------------|----------|------------| +| **Shapefile 中心点计算** | `shapefile_processor.cpp:85-95` | 提取为公共函数 | ⚠️ 高 | +| **FBX Y-up 到 Z-up 变换** | `fbx_geometry_extractor.cpp:50-70` | 直接复用矩阵定义 | ⚠️ 高 | +| **geometricError 计算** | `FBXPipeline.cpp:225-228` | 提取为公共函数 | ⚠️ 高 | +| **boundingVolume 转换** | `FBXPipeline.cpp:220-230` | 提取为公共函数 | ⚠️ 高 | +| **Shapefile 数据加载** | `ShapefileDataPool::loadFromShapefileWithGeometry()` | 直接调用 | ⚠️ 中 | +| **FBX 数据加载** | `FBXLoader` 类 | 包装为适配器 | ⚠️ 中 | +| **B3DM 生成** | `B3DMContentGenerator` 类 | 直接调用 | ⚠️ 中 | +| **四叉树分割** | `QuadtreeStrategy` 类 | 直接复用 | ✅ 低 | +| **八叉树分割** | `OctreeStrategy` 类 | 直接复用 | ✅ 低 | + +### 6.3 代码复用模式 + +#### 模式 1:直接调用现有类(推荐) + +```cpp +// adapters/shapefile/shapefile_source.cpp +// 直接复用 ShapefileDataPool,不重新实现加载逻辑 +bool ShapefileSource::Load(const DataSourceConfig& config) { + auto& shpConfig = static_cast(config); + + // 复用现有实现 + dataPool_ = std::make_unique(); + bool result = dataPool_->loadFromShapefileWithGeometry( + shpConfig.input_path.string(), + shpConfig.height_field, + shpConfig.geo_reference.center_longitude, + shpConfig.geo_reference.center_latitude + ); + + // 复用中心点重新计算逻辑(shapefile_processor.cpp:85-95) + if (result) { + auto worldBounds = dataPool_->computeWorldBounds(); + double centerLon = (worldBounds.minx + worldBounds.maxx) * 0.5; + double centerLat = (worldBounds.miny + worldBounds.maxy) * 0.5; + + if (std::abs(centerLon - shpConfig.geo_reference.center_longitude) > 1.0 || + std::abs(centerLat - shpConfig.geo_reference.center_latitude) > 1.0) { + // 使用正确的中心点重新加载数据 + dataPool_->clear(); + result = dataPool_->loadFromShapefileWithGeometry( + shpConfig.input_path.string(), + shpConfig.height_field, + centerLon, centerLat + ); + } + } + + return result; +} +``` + +#### 模式 2:提取公共函数 + +```cpp +// common/geometric_error.h +#pragma once +#include + +namespace common { + +// 提取自 FBXPipeline.cpp:225-228 +// 必须与旧实现保持完全一致 +inline double CalculateGeometricError( + const osg::BoundingBoxd& bbox, + double scale = 1.0) { + double dx = bbox.xMax() - bbox.xMin(); + double dy = bbox.yMax() - bbox.yMin(); + double dz = bbox.zMax() - bbox.zMin(); + return std::sqrt(dx*dx + dy*dy + dz*dz) / 20.0 * scale; +} + +// 提取自 fbx_geometry_extractor.cpp:50-70 +// Y-up 到 Z-up 的转换矩阵 +inline osg::Matrixd GetYupToZupMatrix() { + // 注意:OSG 使用列主序矩阵 + return osg::Matrixd( + 1, 0, 0, 0, // 第一列 + 0, 0, 1, 0, // 第二列 + 0, -1, 0, 0, // 第三列 + 0, 0, 0, 1 // 第四列 + ); +} + +} // namespace common +``` + +#### 模式 3:适配器包装 + +```cpp +// adapters/fbx/fbx_geometry_extractor.cpp +// 复用 FBXLoader 的实现,包装为 IGeometryExtractor 接口 +std::vector> +FbxGeometryExtractor::extract(const spatial::core::SpatialItem* item) { + const auto* fbxItem = dynamic_cast(item); + if (!fbxItem) return {}; + + // 获取几何体(复用现有 FBXLoader 的数据) + const osg::Geometry* geom = fbxItem->getGeometry(); + if (!geom) return {}; + + // 克隆几何体 + osg::ref_ptr clonedGeom = + static_cast(geom->clone(osg::CopyOp::DEEP_COPY_ALL)); + + // 应用世界变换 + osg::Matrixd transform = fbxItem->getTransform(); + + // 复用 Y-up 到 Z-up 的转换矩阵(必须与旧代码完全一致) + osg::Matrixd finalTransform = transform * common::GetYupToZupMatrix(); + + // 变换顶点(复用现有逻辑) + if (auto* vertices = dynamic_cast(clonedGeom->getVertexArray())) { + for (auto& vertex : *vertices) { + vertex = vertex * finalTransform; // OSG 是行向量右乘矩阵 + } + vertices->dirty(); + } + + // 变换法线(复用现有逻辑) + osg::Matrixd normalMatrix = osg::Matrixd::inverse(finalTransform); + normalMatrix.transpose3x3(normalMatrix); + + if (auto* normals = dynamic_cast(clonedGeom->getNormalArray())) { + for (auto& normal : *normals) { + normal = osg::Matrixd::transform3x3(normal, normalMatrix); + normal.normalize(); + } + normals->dirty(); + } + + return {clonedGeom}; +} +``` + +### 6.4 一致性验证检查点 + +每个实施步骤必须验证以下检查点: + +| 检查点 | 验证方法 | 通过标准 | +|--------|----------|----------| +| **顶点坐标** | 对比 B3DM 中的顶点数据 | 坐标值完全一致(浮点误差 < 1e-6) | +| **法线向量** | 对比 B3DM 中的法线数据 | 向量值完全一致 | +| **包围盒** | 对比 tileset.json 中的 box | 数值完全一致 | +| **geometricError** | 对比 tileset.json | 数值完全一致 | +| **节点结构** | 对比 tileset.json 层级 | 节点数量和关系一致 | +| **B3DM 文件** | 对比文件哈希 | 文件内容一致(或差异可解释) | + +### 6.5 各阶段对比验证流程 + +``` +阶段 1: 保存基准数据 + │ + ├── 使用旧管道生成 Shapefile 基准输出 → tests/reference/shapefile/ + └── 使用旧管道生成 FBX 基准输出 → tests/reference/fbx/ + +阶段 2: Shapefile 新管道 + │ + ├── 实现 Shapefile 新管道 + ├── 编译(新旧代码共存) + ├── 使用 --use-new-pipeline 生成输出 + └── 与 tests/reference/shapefile/ 对比验证 + +阶段 3: FBX 新管道 + │ + ├── 实现 FBX 新管道 + ├── 编译(新旧代码共存) + ├── 使用 --use-new-pipeline 生成 FBX 输出 + ├── 与 tests/reference/fbx/ 对比验证 + ├── 验证 Shapefile 新管道仍可用 + └── 验证旧管道仍可用(不添加 --use-new-pipeline) + +阶段 4: 默认切换到新管道 + │ + ├── 修改 main.rs 默认启用新管道 + ├── 验证新管道为默认 + └── 保留 --use-legacy-pipeline 回退选项 + +阶段 5: 清理旧代码 + │ + └── 删除旧逻辑,只保留新管道转发 +``` + +--- + +## 7. 详细实施方案 + +### 实施原则 + +每个实施步骤完成后,必须能够: +1. **编译通过** - 项目可正常构建 +2. **Shapefile 转换** - 执行 `3dtiles -i test.shp -o out -f shape` 成功 +3. **FBX 转换** - 执行 `3dtiles -i test.fbx -o out -f fbx --lon 116 --lat 39` 成功 +4. **输出一致** - 生成的 tileset.json 和 B3DM 文件与重构前一致 + +**关键要求**: +- 所有算法逻辑必须复用现有代码,禁止重新实现关键计算逻辑 +- 直接在 `src/` 目录中修改,不创建 `src_new/` +- **不修改 CMakeLists.txt**,通过运行时命令行参数切换新旧实现 + +每个阶段完成后由人工确认,再继续下一阶段。 + +--- + +### 阶段 1:建立双轨运行框架(保持旧代码可用) + +#### 7.1.1 目标 +建立新架构框架,同时保持旧代码完全可用,确保随时可以回退。 + +#### 7.1.2 实施步骤 + +**步骤 1.1: 保存旧代码生成的参考输出(基准数据)** +```bash +# 使用旧代码(默认)编译 +cargo build -vv + +# 生成 Shapefile 参考输出 +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f shape -i data/SHP/bj_building/bj_building.shp \ + -o tests/e2e/3dtiles-viewer/public/output \ + --height height --enable-lod --enable-simplify + +# 保存参考数据 +mkdir -p tests/reference/shapefile +cp -r tests/e2e/3dtiles-viewer/public/output/* tests/reference/shapefile/ + +# 生成 FBX 参考输出 +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f fbx -i data/FBX/TZCS_0829.FBX \ + -o tests/e2e/3dtiles-viewer/public/output \ + --lon 118 --lat 32 --alt 20 \ + --enable-draco --enable-texture-compress --enable-lod + +# 保存参考数据 +mkdir -p tests/reference/fbx +cp -r tests/e2e/3dtiles-viewer/public/output/* tests/reference/fbx/ +``` + +**步骤 1.2: 创建新架构目录(与旧代码共存)** +```bash +# 在 src/ 目录中创建新架构子目录 +mkdir -p src/core/spatial +mkdir -p src/core/geometry +mkdir -p src/core/output/b3dm +mkdir -p src/core/output/gltf +mkdir -p src/core/output/tileset +mkdir -p src/adapters/shapefile +mkdir -p src/adapters/fbx +mkdir -p src/pipeline +mkdir -p src/ffi + +# 创建测试目录 +mkdir -p tests/fixtures/shapefile +mkdir -p tests/fixtures/fbx +mkdir -p tests/reference +``` + +**步骤 1.3: 实现核心接口(空实现)** +- `src/pipeline/data_source.h` - DataSource 接口定义 +- `src/pipeline/conversion_pipeline.h` - ConversionPipeline 基类 +- `src/ffi/ffi_bridge.h` - C API 定义 + +所有方法先做空实现或返回 `false`,确保能编译通过。 + +**步骤 1.4: 修改现有入口文件支持运行时切换** + +修改 `src/shp23dtile.cpp` 和 `src/fbx.cpp`,添加运行时切换逻辑: + +```cpp +// src/shp23dtile.cpp - 修改现有文件 +#include "pipeline/conversion_pipeline.h" + +// 全局标志,由 Rust 通过 FFI 设置 +extern "C" bool g_use_new_pipeline = false; + +// 设置全局标志(供 Rust 调用) +extern "C" void set_use_new_pipeline(bool use_new) { + g_use_new_pipeline = use_new; +} + +// 原有接口保持不变 +extern "C" bool shp23dtile(const ShapeConversionParams* params) { + if (g_use_new_pipeline) { + // 转发到新管道 + return shp23dtile_new_pipeline(params); + } + // 原有旧实现 + return shp23dtile_legacy(params); +} + +// 旧实现(原有代码移动到这里或保持原位) +bool shp23dtile_legacy(const ShapeConversionParams* params) { + // ... 原有实现不变 +} + +// 新管道包装器 +bool shp23dtile_new_pipeline(const ShapeConversionParams* params) { + // 阶段 1: 空实现,返回 false + // 阶段 2: 实现 Shapefile 新管道逻辑 + return false; +} +``` + +**步骤 1.4: 创建新架构目录(与旧代码共存)** +```bash +# 在 src/ 目录中创建新架构子目录 +mkdir -p src/core/spatial +mkdir -p src/core/geometry +mkdir -p src/core/output/b3dm +mkdir -p src/core/output/gltf +mkdir -p src/core/output/tileset +mkdir -p src/adapters/shapefile +mkdir -p src/adapters/fbx +mkdir -p src/pipeline +mkdir -p src/ffi + +# 创建测试目录 +mkdir -p tests/fixtures/shapefile +mkdir -p tests/fixtures/fbx +mkdir -p tests/reference +``` + +**步骤 1.5: 实现核心接口(空实现)** +- `src/pipeline/data_source.h` - DataSource 接口定义 +- `src/pipeline/conversion_pipeline.h` - ConversionPipeline 基类 +- `src/ffi/ffi_bridge.h` - C API 定义 + +所有方法先做空实现或返回 `false`,确保能编译通过。 + +#### 7.1.3 阶段 1 完成标准 +- [ ] 旧代码完全未改动,可正常编译运行 +- [ ] **基准数据已保存到 `tests/reference/`**(Shapefile 和 FBX 各一份) +- [ ] 新代码框架目录结构已创建 +- [ ] 新代码框架可编译(空实现,返回失败) + +**阶段 1 交付物:** +``` +tests/reference/ +├── shapefile/ # Shapefile 基准输出 +│ ├── tileset.json +│ ├── data/ +│ │ ├── 0_0_0.b3dm +│ │ └── ... +│ └── ... +└── fbx/ # FBX 基准输出 + ├── tileset.json + ├── data/ + │ ├── 0_0_0.b3dm + │ └── ... + └── ... +``` + +**验证步骤:** +```bash +# 1. 编译项目 +cargo build -vv + +# 2. 生成 Shapefile 基准数据 +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f shape -i data/SHP/bj_building/bj_building.shp \ + -o tests/e2e/3dtiles-viewer/public/output \ + --height height --enable-lod --enable-simplify +mkdir -p tests/reference/shapefile +cp -r tests/e2e/3dtiles-viewer/public/output/* tests/reference/shapefile/ + +# 3. 生成 FBX 基准数据 +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f fbx -i data/FBX/TZCS_0829.FBX \ + -o tests/e2e/3dtiles-viewer/public/output \ + --lon 118 --lat 32 --alt 20 \ + --enable-draco --enable-texture-compress --enable-lod +mkdir -p tests/reference/fbx +cp -r tests/e2e/3dtiles-viewer/public/output/* tests/reference/fbx/ + +# 4. 验证基准数据完整 +ls -la tests/reference/shapefile/tileset.json +ls -la tests/reference/fbx/tileset.json +``` + +**说明:** 阶段 1 只保存基准数据,新管道为空实现,无法执行完整转换。阶段 2 开始实现 Shapefile 新管道,阶段 3 实现 FBX 新管道。 + +--- + +### 阶段 2:实现 Shapefile 新管道(保持旧代码可用) + +#### 7.2.1 目标 +在 `src/` 中实现 Shapefile 的新管道,复用现有代码逻辑,确保输出一致。 + +#### 7.2.2 实施步骤 + +**步骤 2.1: 创建公共工具函数(提取自旧代码)** +- 文件: `src/common/shapefile_utils.h/cpp` +- **复用** `shapefile/shapefile_processor.cpp:85-95` 的中心点重新计算逻辑 +- **复用** `shapefile/ShapefileDataPool::loadFromShapefileWithGeometry()` 数据加载逻辑 +- **禁止重新实现**,只允许提取和包装 + +```cpp +// common/shapefile_utils.h +#pragma once +#include "shapefile/shapefile_data_pool.h" + +namespace common { + +// 提取自 shapefile_processor.cpp:85-95 +// 必须与旧实现保持完全一致 +inline bool ReloadDataWithCorrectCenter( + ShapefileDataPool* dataPool, + const std::string& inputPath, + const std::string& heightField, + double expectedCenterLon, + double expectedCenterLat) { + + auto worldBounds = dataPool->computeWorldBounds(); + double centerLon = (worldBounds.minx + worldBounds.maxx) * 0.5; + double centerLat = (worldBounds.miny + worldBounds.maxy) * 0.5; + + if (std::abs(centerLon - expectedCenterLon) > 1.0 || + std::abs(centerLat - expectedCenterLat) > 1.0) { + dataPool->clear(); + return dataPool->loadFromShapefileWithGeometry( + inputPath, heightField, centerLon, centerLat); + } + return true; +} + +} // namespace common +``` + +**步骤 2.2: 实现 ShapefileSource(复用现有类)** +- 文件: `src/adapters/shapefile/shapefile_source.h/cpp` +- **复用** `ShapefileDataPool` 类进行数据加载 +- **复用** `ShapefileSpatialItemAdapter` 进行空间项适配 +- `Load()` 方法调用 `ShapefileDataPool::loadFromShapefileWithGeometry()` +- 使用步骤 2.1 的公共函数处理中心点重新计算 + +```cpp +// adapters/shapefile/shapefile_source.cpp +bool ShapefileSource::Load(const DataSourceConfig& config) { + auto& shpConfig = static_cast(config); + + // 复用现有 ShapefileDataPool + dataPool_ = std::make_unique(); + bool result = dataPool_->loadFromShapefileWithGeometry( + shpConfig.input_path.string(), + shpConfig.height_field, + shpConfig.geo_reference.center_longitude, + shpConfig.geo_reference.center_latitude + ); + + if (result) { + // 复用中心点重新计算逻辑 + result = common::ReloadDataWithCorrectCenter( + dataPool_.get(), + shpConfig.input_path.string(), + shpConfig.height_field, + shpConfig.geo_reference.center_longitude, + shpConfig.geo_reference.center_latitude + ); + } + + return result; +} +``` + +**步骤 2.3: 实现几何提取器(复用现有实现)** +- 文件: `src/adapters/shapefile/shapefile_geometry_extractor.cpp` +- **复用** `GeometryExtractor` 或 `B3DMContentGenerator` 的几何处理逻辑 +- **禁止重新实现** OGRGeometry 到 OSG 几何体的转换 + +**步骤 2.4: 复用四叉树空间索引** +- 文件: 无需新文件 +- **直接复用** `src/spatial/strategy/quadtree_strategy.h/cpp` +- 确保分割参数与旧代码一致 + +**步骤 2.5: 实现 ShapefilePipeline(复用 B3DM 生成器)** +- 文件: `src/pipeline/shapefile_pipeline.cpp` +- **复用** `B3DMContentGenerator` 生成 B3DM 文件 +- **复用** `ShapefileTilesetAdapter` 生成 tileset +- 实现 `CreateSlicingStrategy()` 返回现有的 `QuadtreeStrategy` + +```cpp +// pipeline/shapefile_pipeline.cpp +std::unique_ptr +ShapefilePipeline::CreateSlicingStrategy() { + // 复用现有的 QuadtreeStrategy + return std::make_unique(); +} +``` + +**步骤 2.6: 编译并验证新管道(与基准数据对比)** + +```bash +# 编译项目(新旧代码共存) +cargo build -vv + +# 生成 Shapefile 输出(新管道) +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f shape -i data/SHP/bj_building/bj_building.shp \ + -o tests/e2e/3dtiles-viewer/public/output \ + --height height --enable-lod --enable-simplify --use-new-pipeline + +# 对比验证检查点: +# 1. 顶点坐标一致性(误差 < 1e-6) +# 2. 包围盒数值一致性 +# 3. geometricError 一致性 +# 4. 节点结构一致性 +# 5. B3DM 文件哈希一致性 +``` + +**自动化对比脚本(建议添加到 tests/compare.sh):** +```bash +#!/bin/bash +# tests/compare.sh - 对比新输出与基准数据 + +set -e + +REFERENCE_DIR="tests/reference/shapefile" +OUTPUT_DIR="tests/e2e/3dtiles-viewer/public/output" + +echo "=== 对比 tileset.json ===" +# 对比关键字段:geometricError、boundingVolume +diff <(jq '.root.geometricError' $REFERENCE_DIR/tileset.json) \ + <(jq '.root.geometricError' $OUTPUT_DIR/tileset.json) \ + || { echo "❌ geometricError 不匹配"; exit 1; } + +echo "✅ geometricError 匹配" + +# 对比 B3DM 文件数量 +ref_count=$(find $REFERENCE_DIR -name "*.b3dm" | wc -l) +out_count=$(find $OUTPUT_DIR -name "*.b3dm" | wc -l) +if [ "$ref_count" -eq "$out_count" ]; then + echo "✅ B3DM 文件数量匹配: $ref_count" +else + echo "❌ B3DM 文件数量不匹配: 基准=$ref_count, 输出=$out_count" + exit 1 +fi + +# 对比每个 B3DM 文件大小(允许 1% 误差) +for ref_file in $REFERENCE_DIR/data/*.b3dm; do + filename=$(basename $ref_file) + out_file="$OUTPUT_DIR/data/$filename" + if [ ! -f "$out_file" ]; then + echo "❌ 缺少文件: $filename" + exit 1 + fi + + ref_size=$(stat -f%z "$ref_file" 2>/dev/null || stat -c%s "$ref_file") + out_size=$(stat -f%z "$out_file" 2>/dev/null || stat -c%s "$out_file") + + diff_percent=$(echo "scale=2; abs($ref_size - $out_size) * 100 / $ref_size" | bc) + if (( $(echo "$diff_percent > 1" | bc -l) )); then + echo "❌ 文件大小差异过大: $filename (差异 ${diff_percent}%)" + exit 1 + fi +done +echo "✅ 所有 B3DM 文件大小匹配" + +echo "" +echo "=== 阶段 2 验证通过 ===" +``` + +#### 7.2.3 阶段 2 完成标准 +- [ ] Shapefile 新管道可执行**完整转换流程** +- [ ] **输出可与基准数据对比验证** +- [ ] `cargo build -vv` 编译成功(新旧代码共存) +- [ ] **顶点坐标与基准数据一致**(误差 < 1e-6) +- [ ] **包围盒数值与基准数据一致** +- [ ] **geometricError 与基准数据一致** +- [ ] **B3DM 文件内容与基准数据一致** +- [ ] 旧管道仍可用(不添加 `--use-new-pipeline` 参数) + +**阶段 2 交付物:** +- 可工作的 Shapefile 新管道实现 +- 通过对比验证的输出结果 +- 阶段 2 验证报告(包含对比数据) + +**验证命令:** +```bash +# 1. 编译 +cargo build -vv + +# 2. 使用新管道生成输出 +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f shape -i data/SHP/bj_building/bj_building.shp \ + -o tests/e2e/3dtiles-viewer/public/output \ + --height height --enable-lod --enable-simplify --use-new-pipeline + +# 3. 与基准数据对比 +./tests/compare.sh + +# 4. 验证旧管道仍可用 +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f shape -i data/SHP/bj_building/bj_building.shp \ + -o tests/e2e/3dtiles-viewer/public/output \ + --height height --enable-lod --enable-simplify +# 应使用旧管道成功执行 +``` + +--- + +### 阶段 3:实现 FBX 新管道(保持旧代码可用) + +#### 7.3.1 目标 +在 `src/` 中实现 FBX 的新管道,复用现有代码逻辑,确保输出一致。 + +#### 7.3.2 实施步骤 + +**步骤 3.1: 创建公共工具函数(提取自旧代码)** +- 文件: `src/common/fbx_utils.h/cpp` +- **复用** `fbx/fbx_geometry_extractor.cpp:50-70` 的 Y-up 到 Z-up 变换矩阵 +- **复用** `FBXPipeline.cpp:225-228` 的 geometricError 计算逻辑 +- **复用** `FBXPipeline.cpp:220-230` 的 boundingVolume 转换逻辑 +- **禁止重新实现**,只允许提取和包装 + +```cpp +// common/fbx_utils.h +#pragma once +#include +#include + +namespace common { + +// 提取自 fbx_geometry_extractor.cpp:50-70 +// Y-up 到 Z-up 的转换矩阵 +// 注意:OSG 使用列主序矩阵,必须与旧代码完全一致 +inline osg::Matrixd GetYupToZupMatrix() { + return osg::Matrixd( + 1, 0, 0, 0, // 第一列: x' = 1*x + 0*y + 0*z + 0, 0, 1, 0, // 第二列: y' = 0*x + 0*y + 1*z = z + 0, -1, 0, 0, // 第三列: z' = 0*x - 1*y + 0*z = -y + 0, 0, 0, 1 // 第四列 + ); +} + +// 提取自 FBXPipeline.cpp:225-228 +// geometricError 计算公式 +inline double CalculateGeometricError( + const osg::BoundingBoxd& bbox, + double scale = 1.0) { + double dx = bbox.xMax() - bbox.xMin(); + double dy = bbox.yMax() - bbox.yMin(); + double dz = bbox.zMax() - bbox.zMin(); + return std::sqrt(dx*dx + dy*dy + dz*dz) / 20.0 * scale; +} + +// 提取自 FBXPipeline.cpp:220-230 +// boundingVolume 转换(Y-up 到 Z-up) +inline osg::BoundingBoxd ConvertBoundsYupToZup( + const std::array& min, + const std::array& max) { + // 转换: (x, y, z) -> (x, -z, y) + osg::BoundingBoxd bbox; + bbox.expandBy(osg::Vec3d(min[0], -max[2], min[1])); + bbox.expandBy(osg::Vec3d(max[0], -min[2], max[1])); + return bbox; +} + +} // namespace common +``` + +**步骤 3.2: 实现 FbxSource(复用 FBXLoader)** +- 文件: `src/adapters/fbx/fbx_source.h/cpp` +- **复用** `FBXLoader` 类进行数据加载 +- **复用** `FbxSpatialItemAdapter` 进行空间项适配 +- `Load()` 方法调用 `FBXLoader::load()` + +```cpp +// adapters/fbx/fbx_source.cpp +bool FbxSource::Load(const DataSourceConfig& config) { + auto& fbxConfig = static_cast(config); + + // 复用现有 FBXLoader + loader_ = std::make_unique(fbxConfig.input_path.string()); + if (!loader_->load()) { + return false; + } + + // 复用 FbxSpatialItemAdapter 创建空间项 + spatialItems_ = fbx::createSpatialItems(loader_.get()); + + return !spatialItems_.empty(); +} +``` + +**步骤 3.3: 实现 FBX 几何提取器(复用变换逻辑)** +- 文件: `src/adapters/fbx/fbx_geometry_extractor.cpp` +- **复用** `common::GetYupToZupMatrix()` 进行坐标变换 +- **复用** `common::CalculateGeometricError()` 计算误差 +- **复用** `common::ConvertBoundsYupToZup()` 转换包围盒 +- **禁止重新实现** 坐标变换逻辑 + +```cpp +// adapters/fbx/fbx_geometry_extractor.cpp +std::vector> +FbxGeometryExtractor::extract(const spatial::core::SpatialItem* item) { + const auto* fbxItem = dynamic_cast(item); + if (!fbxItem) return {}; + + const osg::Geometry* geom = fbxItem->getGeometry(); + if (!geom) return {}; + + // 克隆几何体 + osg::ref_ptr clonedGeom = + static_cast(geom->clone(osg::CopyOp::DEEP_COPY_ALL)); + + // 复用 Y-up 到 Z-up 的转换矩阵(必须与旧代码完全一致) + osg::Matrixd transform = fbxItem->getTransform(); + osg::Matrixd finalTransform = transform * common::GetYupToZupMatrix(); + + // 变换顶点 + if (auto* vertices = dynamic_cast(clonedGeom->getVertexArray())) { + for (auto& vertex : *vertices) { + vertex = vertex * finalTransform; // OSG 是行向量右乘矩阵 + } + vertices->dirty(); + } + + // 变换法线 + osg::Matrixd normalMatrix = osg::Matrixd::inverse(finalTransform); + normalMatrix.transpose3x3(normalMatrix); + + if (auto* normals = dynamic_cast(clonedGeom->getNormalArray())) { + for (auto& normal : *normals) { + normal = osg::Matrixd::transform3x3(normal, normalMatrix); + normal.normalize(); + } + normals->dirty(); + } + + return {clonedGeom}; +} +``` + +**步骤 3.4: 复用八叉树空间索引** +- 文件: 无需新文件 +- **直接复用** `src/spatial/strategy/octree_strategy.h/cpp` +- 确保分割参数与旧代码一致 + +**步骤 3.5: 实现 FbxPipeline(复用 B3DM 生成器)** +- 文件: `src/pipeline/fbx_pipeline.cpp` +- **复用** `B3DMGenerator` 生成 B3DM 文件 +- **复用** `FbxTilesetAdapter` 生成 tileset +- **复用** `common::CalculateGeometricError()` 计算 geometricError +- **复用** `common::ConvertBoundsYupToZup()` 转换包围盒 +- 实现 `CreateSlicingStrategy()` 返回现有的 `OctreeStrategy` + +```cpp +// pipeline/fbx_pipeline.cpp +std::unique_ptr +FbxPipeline::CreateSlicingStrategy() { + // 复用现有的 OctreeStrategy + return std::make_unique(); +} + +common::GeometricErrorCalculator +FbxPipeline::GetGeometricErrorCalculator() const { + // 复用提取的公共函数 + return [](const auto& bbox) { + return common::CalculateGeometricError(bbox); + }; +} +``` + +**步骤 3.6: 编译并验证新管道(与基准数据对比)** + +```bash +# 编译项目(新旧代码共存) +cargo build -vv + +# 生成 FBX 输出(新管道) +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f fbx -i data/FBX/TZCS_0829.FBX \ + -o tests/e2e/3dtiles-viewer/public/output \ + --lon 118 --lat 32 --alt 20 \ + --enable-draco --enable-texture-compress --enable-lod --use-new-pipeline + +# 对比验证检查点: +# 1. 顶点坐标一致性(变换后,误差 < 1e-6) +# 2. 法线向量一致性 +# 3. 包围盒数值一致性 +# 4. geometricError 一致性 +# 5. 节点结构一致性 +# 6. B3DM 文件哈希一致性 +``` + +**FBX 对比脚本(建议添加到 tests/compare_fbx.sh):** +```bash +#!/bin/bash +# tests/compare_fbx.sh - 对比 FBX 新输出与基准数据 + +set -e + +REFERENCE_DIR="tests/reference/fbx" +OUTPUT_DIR="tests/e2e/3dtiles-viewer/public/output" + +echo "=== 对比 FBX 转换结果 ===" + +# 对比 tileset.json 关键字段 +diff <(jq '.root.geometricError' $REFERENCE_DIR/tileset.json) \ + <(jq '.root.geometricError' $OUTPUT_DIR/tileset.json) \ + || { echo "❌ geometricError 不匹配"; exit 1; } +echo "✅ geometricError 匹配" + +# 对比 B3DM 文件数量 +ref_count=$(find $REFERENCE_DIR -name "*.b3dm" | wc -l) +out_count=$(find $OUTPUT_DIR -name "*.b3dm" | wc -l) +if [ "$ref_count" -eq "$out_count" ]; then + echo "✅ B3DM 文件数量匹配: $ref_count" +else + echo "❌ B3DM 文件数量不匹配: 基准=$ref_count, 输出=$out_count" + exit 1 +fi + +# FBX 特有的对比:检查法线向量(如果工具可用) +# 这里可以添加 b3dm 解析工具来对比顶点/法线数据 + +echo "" +echo "=== 阶段 3 验证通过 ===" +``` + +#### 7.3.3 阶段 3 完成标准 +- [ ] FBX 新管道可执行**完整转换流程** +- [ ] **输出可与基准数据对比验证** +- [ ] `cargo build -vv` 编译成功(新旧代码共存) +- [ ] **顶点坐标与基准数据一致**(误差 < 1e-6) +- [ ] **法线向量与基准数据一致** +- [ ] **包围盒数值与基准数据一致** +- [ ] **geometricError 与基准数据一致** +- [ ] **B3DM 文件内容与基准数据一致** +- [ ] 地理参考参数正确处理 +- [ ] 旧管道仍可用(不添加 `--use-new-pipeline` 参数) + +**阶段 3 交付物:** +- 可工作的 FBX 新管道实现 +- 通过对比验证的输出结果 +- 阶段 3 验证报告(包含对比数据) +- Shapefile 和 FBX 双管道都可用 + +**验证命令:** +```bash +# 1. 编译 +cargo build -vv + +# 2. 使用新管道生成 FBX 输出 +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f fbx -i data/FBX/TZCS_0829.FBX \ + -o tests/e2e/3dtiles-viewer/public/output \ + --lon 118 --lat 32 --alt 20 \ + --enable-draco --enable-texture-compress --enable-lod --use-new-pipeline + +# 3. 与基准数据对比 +./tests/compare_fbx.sh + +# 4. 验证 Shapefile 新管道仍可用 +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f shape -i data/SHP/bj_building/bj_building.shp \ + -o tests/e2e/3dtiles-viewer/public/output \ + --height height --enable-lod --enable-simplify --use-new-pipeline +./tests/compare.sh + +# 5. 验证旧管道仍可用(Shapefile) +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f shape -i data/SHP/bj_building/bj_building.shp \ + -o tests/e2e/3dtiles-viewer/public/output \ + --height height --enable-lod --enable-simplify + +# 6. 验证旧管道仍可用(FBX) +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f fbx -i data/FBX/TZCS_0829.FBX \ + -o tests/e2e/3dtiles-viewer/public/output \ + --lon 118 --lat 32 --alt 20 \ + --enable-draco --enable-texture-compress --enable-lod +``` + +--- + +### 阶段 4:全面验证和切换 + +#### 7.4.1 目标 +全面验证新管道,将默认实现切换到新管道。 + +#### 7.4.2 实施步骤 + +**步骤 4.1: 扩展数据集验证** +- 使用多个不同规模的数据集测试 +- 验证各种参数组合 + +**步骤 4.2: 性能对比** +- 对比新旧管道的执行时间 +- 确保新管道性能不低于旧管道的 90% + +**步骤 4.3: 切换默认实现** +- 修改 `main.rs`:默认设置 `use_new_pipeline = true` +- 旧代码保留在 `src/` 中,与新代码共存 +- 通过 `--use-new-pipeline` 参数控制(默认启用) + +```rust +// main.rs 修改 +let use_new_pipeline = !matches.get_flag("use-legacy-pipeline"); // 默认启用新管道 + +// 设置 C++ 全局标志 +unsafe { + set_use_new_pipeline(use_new_pipeline); +} +``` + +**命令行使用:** +```bash +# 默认使用新管道 +./target/debug/_3dtile -f shape -i input.shp -o output + +# 显式使用旧管道 +./target/debug/_3dtile -f shape -i input.shp -o output --use-legacy-pipeline +``` + +**步骤 4.4: 添加命令行参数回退机制** +- 保留旧代码作为回退选项 +- 通过 `--use-legacy-pipeline` 命令行参数切换回旧实现 + +```bash +# 默认使用新管道 +./target/debug/_3dtile -f shape -i input.shp -o output + +# 显式使用旧管道 +./target/debug/_3dtile -f shape -i input.shp -o output --use-legacy-pipeline +``` + +#### 7.4.3 阶段 4 完成标准 +- [ ] 多个数据集验证通过(人工确认) +- [ ] 性能达到要求 +- [ ] 默认实现已切换到新管道(`main.rs` 默认启用) +- [ ] `--use-legacy-pipeline` 回退机制工作正常 +- [ ] 旧代码保留在 `src/` 中与新代码共存 + +**验证命令:** +```bash +# 编译 +cargo build -vv + +# 默认使用新管道 +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f shape -i data/SHP/bj_building/bj_building.shp \ + -o tests/e2e/3dtiles-viewer/public/output \ + --height height --enable-lod --enable-simplify +# 人工确认输出正常 + +# 回退到旧管道验证 +rm -rf tests/e2e/3dtiles-viewer/public/output +./target/debug/_3dtile -f shape -i data/SHP/bj_building/bj_building.shp \ + -o tests/e2e/3dtiles-viewer/public/output \ + --height height --enable-lod --enable-simplify --use-legacy-pipeline +# 确认旧实现仍然可用 +``` + +--- + +### 阶段 5:清理和优化 + +#### 7.5.1 目标 +删除旧代码,优化新实现,完善文档。 + +#### 7.5.2 实施步骤 + +**步骤 5.1: 删除旧代码** +- 删除旧实现文件中的旧逻辑: + - `src/shp23dtile.cpp` 中的 `shp23dtile_legacy()` 函数 + - `src/fbx.cpp` 中的 `fbx23dtile_legacy()` 函数 + - 保留文件,但只保留新管道转发逻辑 +- 清理 `main.rs` 中的 `--use-legacy-pipeline` 参数(不再支持旧管道) +- CMakeLists.txt 保持不变(始终使用 GLOB 收集所有文件) + +```cpp +// src/shp23dtile.cpp 最终状态 - 只保留新管道 +extern "C" bool shp23dtile(const ShapeConversionParams* params) { + // 直接转发到新管道(不再支持旧管道) + return shp23dtile_new_pipeline(params); +} +``` + +```rust +// main.rs 最终状态 - 移除 --use-legacy-pipeline 参数 +// 不再添加 use-legacy-pipeline 参数,始终使用新管道 +``` + +**步骤 5.2: 代码优化** +- 性能分析(Profiling) +- 内存优化 +- 并发优化(如适用) + +**步骤 5.3: 更新文档** +- 更新 README.md +- 更新架构文档 + +#### 7.5.3 阶段 5 完成标准 +- [ ] 旧代码已完全删除 +- [ ] 项目可正常编译运行 +- [ ] 所有转换功能正常工作(人工确认) +- [ ] 性能达到或超过旧实现 +- [ ] 文档已更新 + +--- + +## 8. 目录结构重构方案 + +### 8.1 新目录结构 + +``` +src/ +├── core/ # 核心框架层(稳定、通用) +│ ├── spatial/ # 空间索引核心 +│ │ ├── spatial_item.h +│ │ ├── spatial_bounds.h +│ │ └── slicing_strategy.h +│ ├── geometry/ # 几何处理核心 +│ │ ├── geometry_extractor.h +│ │ └── mesh_processor.h +│ └── output/ # 输出格式核心 +│ ├── b3dm/ +│ ├── gltf/ +│ └── tileset/ +│ +├── adapters/ # 数据源适配器层 +│ ├── shapefile/ # Shapefile 适配器 +│ │ ├── shapefile_source.h/cpp # 实现 DataSource 接口 +│ │ ├── shapefile_geometry_extractor.h/cpp +│ │ ├── shapefile_spatial_item_adapter.h/cpp +│ │ └── shapefile_tile_meta.h +│ │ +│ ├── fbx/ # FBX 适配器 +│ │ ├── fbx_source.h/cpp # 实现 DataSource 接口 +│ │ ├── fbx_geometry_extractor.h/cpp +│ │ ├── fbx_spatial_item_adapter.h/cpp +│ │ └── fbx_tile_meta.h +│ │ +│ └── osgb/ # OSGB 适配器(未来) +│ +├── pipeline/ # 统一管道层(新架构核心) +│ ├── data_source.h # DataSource 接口 +│ ├── data_source_factory.h/cpp +│ ├── conversion_pipeline.h/cpp # 模板方法模式 +│ ├── shapefile_pipeline.h/cpp # Shapefile 专用管道 +│ ├── fbx_pipeline.h/cpp # FBX 专用管道 +│ └── ffi_bridge.h/cpp # Rust FFI 桥接 +│ +├── shapefile/ # 现有 Shapefile 实现(复用) +│ ├── shapefile_processor.h/cpp +│ ├── shapefile_data_pool.h/cpp +│ ├── b3dm_content_generator.h/cpp +│ └── ... +│ +├── fbx/ # 现有 FBX 实现(复用) +│ ├── fbx_loader.h/cpp +│ ├── fbx_tileset_adapter.h/cpp +│ ├── fbx_geometry_extractor.h/cpp +│ └── ... +│ +├── spatial/ # 现有空间索引实现(复用) +│ └── strategy/ +│ ├── quadtree_strategy.h/cpp +│ └── octree_strategy.h/cpp +│ +├── b3dm/ # 现有 B3DM 实现(复用) +│ ├── b3dm_generator.h/cpp +│ └── b3dm_writer.h/cpp +│ +├── common/ # 现有公共代码(复用) +│ └── ... +│ +└── utils/ # 工具类 + ├── extern.h/cpp + ├── attribute_storage.h/cpp + ├── GeoidHeight.h/cpp + └── dxt_img.h/cpp + +# Rust 代码(项目根目录) +# src/ +# ├── main.rs # 简洁入口 +# ├── lib.rs +# ├── common.rs +# ├── error.rs +# ├── fun_c.rs +# ├── utils.rs +# ├── shape.rs # Shapefile 转换 +# ├── fbx.rs # FBX 转换 +# └── osgb.rs # OSGB 转换 + +# 旧实现文件(迁移期间保留,与新代码共存) +# 这些文件在 src/ 根目录,迁移完成后只保留转发逻辑: +# - shp23dtile.cpp(Shapefile 入口,内部转发到新管道) +# - fbx.cpp(FBX 入口,内部转发到新管道) +# - FBXPipeline.cpp(FBX 管道实现) +``` + +### 8.2 分层职责 + +| 目录 | 职责 | 依赖方向 | +|------|------|----------| +| `core/` | 通用核心,不依赖任何数据源 | 无 | +| `adapters/` | 数据源适配,实现 `DataSource` 接口 | core → adapters | +| `pipeline/` | 流程编排,组合 core 和 adapters | core/adapters → pipeline | +| `shapefile/` | 现有 Shapefile 实现(复用) | adapters → shapefile | +| `fbx/` | 现有 FBX 实现(复用) | adapters → fbx | +| `spatial/` | 现有空间索引实现(复用) | core → spatial | +| `b3dm/` | 现有 B3DM 实现(复用) | core/output → b3dm | +| `common/` | 现有公共代码(复用) | 无 | + +### 8.3 代码迁移映射 + +| 旧文件 | 处理方式 | 说明 | +|--------|----------|------| +| `src/shp23dtile.cpp` | **修改转发** | 保留文件,内部添加新旧管道切换逻辑,最终只保留新管道转发 | +| `src/fbx.cpp` | **修改转发** | 保留文件,内部添加新旧管道切换逻辑,最终只保留新管道转发 | +| `src/FBXPipeline.cpp` | **保留复用** | FBX 管道实现,被新管道适配器调用 | +| `src/shapefile/*` | **直接复用** | 现有实现保持不变,新适配器直接调用 | +| `src/fbx/*` | **直接复用** | 现有实现保持不变,新适配器直接调用 | +| `src/spatial/*` | **直接复用** | 现有实现保持不变 | +| `src/b3dm/*` | **直接复用** | 现有实现保持不变 | +| `src/common/*` | **直接复用** | 现有实现保持不变,提取关键函数到此目录 | +| `src/gltf/`, `src/tileset/` | **直接复用** | 现有实现保持不变 | + +### 8.4 依赖关系 + +``` +src/main.rs (Rust) + │ + ▼ FFI 调用 +src/shp23dtile.cpp 或 src/fbx.cpp (C++) + │ + ▼ 内部转发 +pipeline/conversion_pipeline.h + │ + ├──► adapters/shapefile/shapefile_source.h ──► core/spatial/ + │ + └──► adapters/fbx/fbx_source.h ──────────────► core/spatial/ + │ + ▼ + core/output/b3dm/ + core/output/gltf/ + core/output/tileset/ +``` + +--- + +## 9. 关键设计决策说明 + +### 9.1 管道命名策略 + +采用**数据源导向**的命名方式,而非切片策略导向: + +| 管道类 | 使用的空间索引 | 说明 | +|--------|---------------|------| +| `ShapefilePipeline` | QuadtreeStrategy | Shapefile 目前使用四叉树,但未来可能支持其他策略 | +| `FbxPipeline` | OctreeStrategy | FBX 目前使用八叉树,但未来可能支持其他策略 | + +这种命名方式的优势: +1. **语义清晰** - 管道为特定数据源服务,而非特定算法 +2. **易于扩展** - 如果 Shapefile 未来需要支持八叉树,只需修改 `ShapefilePipeline` 内部实现 +3. **隐藏实现细节** - 调用方不需要知道内部使用什么空间索引策略 + +### 9.2 地理参考处理 + +- **Shapefile**:从 .shp 文件自动解析坐标系,计算中心点,转换为 WGS84 +- **FBX**:必须通过 `--lon`, `--lat`, `--alt` 参数显式指定,无默认值 + +### 9.3 错误处理 + +- C++ 侧使用异常处理内部错误,FFI 边界转换为错误码/消息 +- Rust 侧使用 `Result` 类型传递错误 + +### 9.4 内存管理 + +- C++ 侧使用智能指针管理资源 +- FFI 边界明确所有权:Rust 分配的参数,C++ 执行,Rust 释放结果 + +### 9.5 向后兼容 + +- 不保留旧 API 兼容层,直接替换 +- 新的统一 API 通过 `convert_with_pipeline` 暴露 +- 旧代码移动到 `legacy/` 目录,迁移完成后直接删除 + +--- + +## 10. 代码规范检查清单 + +- [ ] 使用 C++20 特性(概念、约束、范围等) +- [ ] 遵循 Google C++ 命名规范 +- [ ] 遵循 C++ Core Guidelines +- [ ] 所有公共接口使用 `[[nodiscard]]` +- [ ] 使用智能指针管理资源 +- [ ] 使用 `explicit` 防止隐式转换 +- [ ] 使用 `noexcept` 标记不抛出异常的函数 +- [ ] 使用 PIMPL 模式隐藏实现细节 +- [ ] Rust 代码使用 `unsafe` 块并标注安全前提 diff --git a/src/FBXPipeline.cpp b/src/FBXPipeline.cpp deleted file mode 100644 index 9f632234..00000000 --- a/src/FBXPipeline.cpp +++ /dev/null @@ -1,2513 +0,0 @@ -#include "FBXPipeline.h" -#include "extern.h" -#include "coordinate_transformer.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Use existing tinygltf if possible, or include it -#include -#include -#include -#include -#include "mesh_processor.h" -#include -#include -#include "lod_pipeline.h" -#include -#include -#include - -using json = nlohmann::json; -namespace fs = std::filesystem; - -// Constants -const uint32_t B3DM_MAGIC = 0x6D643362; -const uint32_t I3DM_MAGIC = 0x6D643369; - -struct B3dmHeader { - uint32_t magic; - uint32_t version; - uint32_t byteLength; - uint32_t featureTableJSONByteLength; - uint32_t featureTableBinaryByteLength; - uint32_t batchTableJSONByteLength; - uint32_t batchTableBinaryByteLength; -}; - -struct I3dmHeader { - uint32_t magic; - uint32_t version; - uint32_t byteLength; - uint32_t featureTableJSONByteLength; - uint32_t featureTableBinaryByteLength; - uint32_t batchTableJSONByteLength; - uint32_t batchTableBinaryByteLength; - uint32_t gltfFormat; // 0: uri, 1: embedded -}; - -// Helper to check point in box -bool isPointInBox(const osg::Vec3d& p, const osg::BoundingBox& b) { - return p.x() >= b.xMin() && p.x() <= b.xMax() && - p.y() >= b.yMin() && p.y() <= b.yMax() && - p.z() >= b.zMin() && p.z() <= b.zMax(); -} - -FBXPipeline::FBXPipeline(const PipelineSettings& s) : settings(s) { -} - -FBXPipeline::~FBXPipeline() { - if (loader) delete loader; - if (rootNode) delete rootNode; -} - -void FBXPipeline::run() { - LOG_I("Starting FBXPipeline..."); - - loader = new FBXLoader(settings.inputPath); - loader->load(); - LOG_I("FBX Loaded. Mesh Pool Size: %zu", loader->meshPool.size()); - { - auto stats = loader->getStats(); - LOG_I("Material dedup: created=%d reused_by_hash=%d pointer_hits=%d unique_statesets=%zu", - stats.material_created, stats.material_hash_reused, stats.material_ptr_reused, stats.unique_statesets); - LOG_I("Mesh dedup: geometries_created=%d reused_by_hash=%d mesh_cache_hit_count=%d unique_geometries=%zu", - stats.geometry_created, stats.geometry_hash_reused, stats.mesh_cache_hit_count, stats.unique_geometries); - } - - // Lambda to generate LOD settings chain - auto generateLODChain = [&](const PipelineSettings& cfg) -> LODPipelineSettings { - LODPipelineSettings lodOps; - lodOps.enable_lod = cfg.enableLOD; - - SimplificationParams simTemplate; - simTemplate.enable_simplification = true; - simTemplate.target_error = 0.0001f; // Base error - - DracoCompressionParams dracoTemplate; - dracoTemplate.enable_compression = cfg.enableDraco; - - // Use build_lod_levels from lod_pipeline.h - // Ratios are expected to be e.g. [1.0, 0.5, 0.25] - lodOps.levels = build_lod_levels( - cfg.lodRatios, - simTemplate.target_error, - simTemplate, - dracoTemplate, - false // draco_for_lod0 - ); - - return lodOps; - }; - - // Simplification Step (Only if LOD is NOT enabled, otherwise we do it per-level or later) - if (settings.enableSimplify && !settings.enableLOD) { - LOG_I("Simplifying meshes (Global)..."); - SimplificationParams simParams; - simParams.enable_simplification = true; - simParams.target_ratio = 0.5f; // Default ratio - simParams.target_error = 0.0001f; // Base error - - for (auto& pair : loader->meshPool) { - if (pair.second.geometry) { - simplify_mesh_geometry(pair.second.geometry.get(), simParams); - } - } - } else if (settings.enableLOD) { - // If LOD is enabled, we prepare the settings - LODPipelineSettings lodSettings = generateLODChain(settings); - LOG_I("LOD Enabled. Generated %zu LOD levels configuration.", lodSettings.levels.size()); - // Actual HLOD generation implementation would go here or in buildOctree/processNode - } - - rootNode = new OctreeNode(); - - // --- 1. Pre-pass: Detect Outliers --- - osg::Vec3d centroid(0,0,0); - size_t totalInstanceCount = 0; - - // Track Extrema for Debugging - struct Extrema { double val; std::string name; osg::Vec3d pos; }; - Extrema minX = {DBL_MAX, "", osg::Vec3d()}; - Extrema maxX = {-DBL_MAX, "", osg::Vec3d()}; - Extrema minY = {DBL_MAX, "", osg::Vec3d()}; - Extrema maxY = {-DBL_MAX, "", osg::Vec3d()}; - Extrema minZ = {DBL_MAX, "", osg::Vec3d()}; - Extrema maxZ = {-DBL_MAX, "", osg::Vec3d()}; - - struct VolumeInfo { - std::string name; - double volume; - double dx, dy, dz; - osg::Vec3d center; - osg::Vec3d minPt, maxPt; - }; - std::vector volumeStats; - - LOG_I("--- Analyzing All Processed Nodes (Sorted by Volume) ---"); - { - osg::Vec3d sumPos(0,0,0); - for (auto& pair : loader->meshPool) { - MeshInstanceInfo& info = pair.second; - if (!info.geometry) continue; - osg::BoundingBox geomBox = info.geometry->getBoundingBox(); - for (size_t i = 0; i < info.transforms.size(); ++i) { - osg::Vec3d center = geomBox.center() * info.transforms[i]; - sumPos += center; - totalInstanceCount++; - - // Track Extrema - if (center.x() < minX.val) minX = {center.x(), info.nodeNames[i], center}; - if (center.x() > maxX.val) maxX = {center.x(), info.nodeNames[i], center}; - if (center.y() < minY.val) minY = {center.y(), info.nodeNames[i], center}; - if (center.y() > maxY.val) maxY = {center.y(), info.nodeNames[i], center}; - if (center.z() < minZ.val) minZ = {center.z(), info.nodeNames[i], center}; - if (center.z() > maxZ.val) maxZ = {center.z(), info.nodeNames[i], center}; - - // Calculate World AABB - osg::BoundingBox worldBox; - for(int k=0; k<8; ++k) { - worldBox.expandBy(geomBox.corner(k) * info.transforms[i]); - } - double dx = worldBox.xMax() - worldBox.xMin(); - double dy = worldBox.yMax() - worldBox.yMin(); - double dz = worldBox.zMax() - worldBox.zMin(); - double vol = dx * dy * dz; - - volumeStats.push_back({ - (i < info.nodeNames.size()) ? info.nodeNames[i] : "unknown", - vol, dx, dy, dz, center, - osg::Vec3d(worldBox.xMin(), worldBox.yMin(), worldBox.zMin()), - osg::Vec3d(worldBox.xMax(), worldBox.yMax(), worldBox.zMax()) - }); - } - } - - // Sort by Volume Descending - std::sort(volumeStats.begin(), volumeStats.end(), [](const VolumeInfo& a, const VolumeInfo& b){ - return a.volume > b.volume; - }); - - // Log - for(const auto& v : volumeStats) { - LOG_I("Nodu: '%s' Vol=%.3f Dim=(%.2f, %.2f, %.2f) Center=(%.2f, %.2f, %.2f) Min=(%.2f, %.2f, %.2f) Max=(%.2f, %.2f, %.2f)", - v.name.c_str(), v.volume, v.dx, v.dy, v.dz, - v.center.x(), v.center.y(), v.center.z(), - v.minPt.x(), v.minPt.y(), v.minPt.z(), - v.maxPt.x(), v.maxPt.y(), v.maxPt.z()); - } - - if (totalInstanceCount > 0) { - centroid = sumPos / (double)totalInstanceCount; - } - } - - LOG_I("--- Scene Extrema Analysis ---"); - LOG_I("Min X: '%s' at %.3f", minX.name.c_str(), minX.val); - LOG_I("Max X: '%s' at %.3f", maxX.name.c_str(), maxX.val); - LOG_I("Min Y: '%s' at %.3f", minY.name.c_str(), minY.val); - LOG_I("Max Y: '%s' at %.3f", maxY.name.c_str(), maxY.val); - LOG_I("Min Z: '%s' at %.3f", minZ.name.c_str(), minZ.val); - LOG_I("Max Z: '%s' at %.3f", maxZ.name.c_str(), maxZ.val); - LOG_I("Total Extent: X[%.3f, %.3f] Y[%.3f, %.3f] Z[%.3f, %.3f]", - minX.val, maxX.val, minY.val, maxY.val, minZ.val, maxZ.val); - - // Calculate distance statistics - double maxDist = 0.0; - double sumDist = 0.0; - if (totalInstanceCount > 0) { - for (auto& pair : loader->meshPool) { - MeshInstanceInfo& info = pair.second; - if (!info.geometry) continue; - osg::BoundingBox geomBox = info.geometry->getBoundingBox(); - for (size_t i = 0; i < info.transforms.size(); ++i) { - double d = (geomBox.center() * info.transforms[i] - centroid).length(); - if (d > maxDist) maxDist = d; - sumDist += d; - } - } - } - double avgDist = (totalInstanceCount > 0) ? (sumDist / totalInstanceCount) : 0.0; - - // Threshold: Only filter if we have very far objects (> 10km) AND they are far from average - // Adjust this logic as needed. - double outlierThreshold = std::max(10000.0, avgDist * 20.0); - bool hasOutliers = (maxDist > outlierThreshold); - - LOG_I("Scene Analysis: Count=%zu Centroid=(%.2f, %.2f, %.2f)", totalInstanceCount, centroid.x(), centroid.y(), centroid.z()); - LOG_I("Distance Stats: Avg=%.2f Max=%.2f Threshold=%.2f", avgDist, maxDist, outlierThreshold); - - // --- 2. Main Pass: Build Root Node & Filter --- - osg::BoundingBox globalBounds; - size_t skippedCount = 0; - - for (auto& pair : loader->meshPool) { - MeshInstanceInfo& info = pair.second; - if (!info.geometry) continue; - - osg::BoundingBox geomBox = info.geometry->getBoundingBox(); - for (size_t i = 0; i < info.transforms.size(); ++i) { - const auto& mat = info.transforms[i]; - - // Outlier Check - if (hasOutliers) { - osg::Vec3d instCenter = geomBox.center() * mat; - double d = (instCenter - centroid).length(); - if (d > outlierThreshold) { - std::string name = (i < info.nodeNames.size()) ? info.nodeNames[i] : "unknown"; - LOG_W("Filtering Outlier: '%s' Dist=%.2f Pos=(%.2f, %.2f, %.2f)", - name.c_str(), d, instCenter.x(), instCenter.y(), instCenter.z()); - skippedCount++; - continue; // SKIP this instance - } - } - - // Expand global bounds - osg::BoundingBox instBox; - for (int k = 0; k < 8; ++k) instBox.expandBy(geomBox.corner(k) * mat); - globalBounds.expandBy(instBox); - - // Add to root node content initially - InstanceRef ref; - ref.meshInfo = &info; - ref.transformIndex = (int)i; - rootNode->content.push_back(ref); - } - } - - if (skippedCount > 0) { - LOG_I("Filtered %zu outlier instances.", skippedCount); - } - rootNode->bbox = globalBounds; - - // --- End of Filtering --- - - json rootJson; - if (settings.splitAverageByCount) { - LOG_I("Using average count split tiling..."); - rootJson = buildAverageTiles(globalBounds, settings.outputPath); - } else { - LOG_I("Building Octree..."); - buildOctree(rootNode); - LOG_I("Processing Nodes and Generating Tiles..."); - rootJson = processNode(rootNode, settings.outputPath, -1, -1, "0"); - } - - LOG_I("--- Generated Tile Bounding Boxes (Sorted by Volume) ---"); - std::sort(tileStats.begin(), tileStats.end(), [](const TileInfo& a, const TileInfo& b){ - return a.volume > b.volume; - }); - - for(const auto& t : tileStats) { - LOG_I("Tile: '%s' Depth=%d Vol=%.3f Dim=(%.2f, %.2f, %.2f) Center=(%.2f, %.2f, %.2f) Min=(%.2f, %.2f, %.2f) Max=(%.2f, %.2f, %.2f)", - t.name.c_str(), t.depth, t.volume, t.dx, t.dy, t.dz, - t.center.x(), t.center.y(), t.center.z(), - t.minPt.x(), t.minPt.y(), t.minPt.z(), - t.maxPt.x(), t.maxPt.y(), t.maxPt.z()); - } - - LOG_I("Writing tileset.json..."); - writeTilesetJson(settings.outputPath, globalBounds, rootJson); - - LOG_I("FBXPipeline Finished."); - logLevelStats(); - { - auto stats = loader->getStats(); - LOG_I("Material dedup: created=%d reused_by_hash=%d pointer_hits=%d unique_statesets=%zu", - stats.material_created, stats.material_hash_reused, stats.material_ptr_reused, stats.unique_statesets); - LOG_I("Mesh dedup: geometries_created=%d reused_by_hash=%d mesh_cache_hit_count=%d unique_geometries=%zu", - stats.geometry_created, stats.geometry_hash_reused, stats.mesh_cache_hit_count, stats.unique_geometries); - } -} - -void FBXPipeline::buildOctree(OctreeNode* node) { - if (node->depth >= settings.maxDepth || node->content.size() <= settings.maxItemsPerTile) { - return; - } - - // Split bbox into 8 children - osg::Vec3d center = node->bbox.center(); - osg::Vec3d min = node->bbox._min; - osg::Vec3d max = node->bbox._max; - - // 8 quadrants - osg::BoundingBox boxes[8]; - // Bottom-Left-Back (0) to Top-Right-Front (7) - // x: left/right, y: back/front, z: bottom/top - - // min, center, max combinations - // 0: min.x, min.y, min.z -> center.x, center.y, center.z - boxes[0] = osg::BoundingBox(min.x(), min.y(), min.z(), center.x(), center.y(), center.z()); - boxes[1] = osg::BoundingBox(center.x(), min.y(), min.z(), max.x(), center.y(), center.z()); - boxes[2] = osg::BoundingBox(min.x(), center.y(), min.z(), center.x(), max.y(), center.z()); - boxes[3] = osg::BoundingBox(center.x(), center.y(), min.z(), max.x(), max.y(), center.z()); - boxes[4] = osg::BoundingBox(min.x(), min.y(), center.z(), center.x(), center.y(), max.z()); - boxes[5] = osg::BoundingBox(center.x(), min.y(), center.z(), max.x(), center.y(), max.z()); - boxes[6] = osg::BoundingBox(min.x(), center.y(), center.z(), center.x(), max.y(), max.z()); - boxes[7] = osg::BoundingBox(center.x(), center.y(), center.z(), max.x(), max.y(), max.z()); - - for (int i = 0; i < 8; ++i) { - OctreeNode* child = new OctreeNode(); - child->bbox = boxes[i]; - child->depth = node->depth + 1; - node->children.push_back(child); - } - - // Distribute content - // For simplicity, if an object center is in box, it goes there. - // Or strictly by bounding box intersection. - // Here we use center point for distribution to avoid duplication across nodes (unless we want loose octree) - std::vector remaining; - for (const auto& ref : node->content) { - osg::Matrixd mat = ref.meshInfo->transforms[ref.transformIndex]; - osg::BoundingBox meshBox = ref.meshInfo->geometry->getBoundingBox(); - osg::Vec3d meshCenter = meshBox.center() * mat; - - bool placed = false; - for (auto child : node->children) { - if (isPointInBox(meshCenter, child->bbox)) { - child->content.push_back(ref); - placed = true; - break; - } - } - if (!placed) { - // Should not happen if box covers all, but maybe precision issues - // Keep in current node? Or force into one? - // Let's keep in current node if not fitting children (e.g. exactly on boundary?) - // Or just put in first matching - node->children[0]->content.push_back(ref); - } - } - node->content.clear(); // Moved to children - - // Recurse - for (auto child : node->children) { - if (!child->content.empty()) { - buildOctree(child); - } - } - - // Prune empty children - auto it = std::remove_if(node->children.begin(), node->children.end(), [](OctreeNode* c){ - if (c->content.empty() && c->children.empty()) { - delete c; - return true; - } - return false; - }); - node->children.erase(it, node->children.end()); -} - -struct TileStats { size_t node_count = 0; size_t vertex_count = 0; size_t triangle_count = 0; size_t material_count = 0; }; -void appendGeometryToModel(tinygltf::Model& model, const std::vector& instances, const PipelineSettings& settings, json* batchTableJson, int* batchIdCounter, const SimplificationParams& simParams, osg::BoundingBoxd* outBox = nullptr, TileStats* stats = nullptr, const char* dbgTileName = nullptr) { - if (instances.empty()) return; - - // Ensure model has at least one buffer - if (model.buffers.empty()) { - model.buffers.push_back(tinygltf::Buffer()); - } - tinygltf::Buffer& buffer = model.buffers[0]; - - // Group instances by material - struct GeomInst { - osg::Geometry* geom; - osg::Matrixd matrix; - int originalBatchId; - }; - std::map> materialGroups; - - for (const auto& ref : instances) { - osg::Geometry* geom = ref.meshInfo->geometry.get(); - if (!geom) continue; - osg::StateSet* ss = geom->getStateSet(); - materialGroups[ss].push_back({geom, ref.meshInfo->transforms[ref.transformIndex], *batchIdCounter}); - (*batchIdCounter)++; - } - if (stats) { - stats->node_count = instances.size(); - stats->material_count = materialGroups.size(); - } - - // Process each material group - for (auto& pair : materialGroups) { - // Prepare merged data - std::vector positions; - std::vector normals; - std::vector texcoords; - std::vector indices; // Use uint32 for safety - std::vector batchIds; // Use float for BATCHID attribute (vec1) - - double minPos[3] = {1e30, 1e30, 1e30}; - double maxPos[3] = {-1e30, -1e30, -1e30}; - int triSets = 0, stripSets = 0, fanSets = 0, otherSets = 0, drawArraysSets = 0; - int missingVertexInstances = 0; - - for (const auto& inst : pair.second) { - osg::ref_ptr processedGeom = inst.geom; - if (simParams.enable_simplification) { - processedGeom = (osg::Geometry*)inst.geom->clone(osg::CopyOp::DEEP_COPY_ALL); - simplify_mesh_geometry(processedGeom.get(), simParams); - } - - osg::Matrixd normalXform; - normalXform.transpose(osg::Matrix::inverse(inst.matrix)); - - uint32_t baseIndex = (uint32_t)(positions.size() / 3); - osg::Array* va = processedGeom->getVertexArray(); - osg::Vec3Array* v = dynamic_cast(va); - osg::Vec3dArray* v3d = dynamic_cast(va); - osg::Vec4Array* v4 = dynamic_cast(va); - osg::Vec4dArray* v4d = dynamic_cast(va); - osg::Array* na = processedGeom->getNormalArray(); - osg::Vec3Array* n = dynamic_cast(na); - osg::Vec3dArray* n3d = dynamic_cast(na); - osg::Array* ta = processedGeom->getTexCoordArray(0); - osg::Vec2Array* t = dynamic_cast(ta); - osg::Vec2dArray* t2d = dynamic_cast(ta); - - bool handledVertices = false; - if ((!v || v->empty()) && (!v3d || v3d->empty()) && (!v4 || v4->empty()) && (!v4d || v4d->empty())) { - if (va && va->getNumElements() > 0) { - GLenum dt = va->getDataType(); - unsigned int cnt = va->getNumElements(); - unsigned int totalBytes = va->getTotalDataSize(); - if (dt == GL_FLOAT || dt == GL_DOUBLE) { - unsigned int comps = dt == GL_FLOAT ? totalBytes / (cnt * (unsigned int)sizeof(float)) : totalBytes / (cnt * (unsigned int)sizeof(double)); - if (comps >= 3) { - if (dt == GL_FLOAT) { - const float* ptr = static_cast(va->getDataPointer()); - for (unsigned int i = 0; i < cnt; ++i) { - osg::Vec3d p((double)ptr[i*comps+0], (double)ptr[i*comps+1], (double)ptr[i*comps+2]); - p = p * inst.matrix; - float px = (float)p.x(); - float py = (float)-p.z(); - float pz = (float)p.y(); - positions.push_back(px); positions.push_back(py); positions.push_back(pz); - if (px < minPos[0]) minPos[0] = px; - if (py < minPos[1]) minPos[1] = py; - if (pz < minPos[2]) minPos[2] = pz; - if (px > maxPos[0]) maxPos[0] = px; - if (py > maxPos[1]) maxPos[1] = py; - if (pz > maxPos[2]) maxPos[2] = pz; - if (outBox) outBox->expandBy(osg::Vec3d(px, py, pz)); - if (n && i < n->size()) { - osg::Vec3 nm = (*n)[i]; - osg::Vec3d nmd(nm.x(), nm.y(), nm.z()); - nmd = osg::Matrix::transform3x3(normalXform, nmd); nmd.normalize(); - normals.push_back((float)nmd.x()); normals.push_back((float)-nmd.z()); normals.push_back((float)nmd.y()); - } else if (n3d && i < n3d->size()) { - osg::Vec3d nmd = (*n3d)[i]; - nmd = osg::Matrix::transform3x3(normalXform, nmd); nmd.normalize(); - normals.push_back((float)nmd.x()); normals.push_back((float)-nmd.z()); normals.push_back((float)nmd.y()); - } else if (na && na->getNumElements() > i) { - GLenum ndt = na->getDataType(); - unsigned int ncnt = na->getNumElements(); - unsigned int nbytes = na->getTotalDataSize(); - unsigned int ncomps = ndt == GL_FLOAT ? nbytes / (ncnt * (unsigned int)sizeof(float)) : ndt == GL_DOUBLE ? nbytes / (ncnt * (unsigned int)sizeof(double)) : 0; - if (ncomps >= 3) { - if (ndt == GL_FLOAT) { - const float* nptr = static_cast(na->getDataPointer()); - osg::Vec3d nm2((double)nptr[i*ncomps+0], (double)nptr[i*ncomps+1], (double)nptr[i*ncomps+2]); - nm2 = osg::Matrix::transform3x3(normalXform, nm2); nm2.normalize(); - normals.push_back((float)nm2.x()); normals.push_back((float)-nm2.z()); normals.push_back((float)nm2.y()); - } else if (ndt == GL_DOUBLE) { - const double* nptr = static_cast(na->getDataPointer()); - osg::Vec3d nm2(nptr[i*ncomps+0], nptr[i*ncomps+1], nptr[i*ncomps+2]); - nm2 = osg::Matrix::transform3x3(normalXform, nm2); nm2.normalize(); - normals.push_back((float)nm2.x()); normals.push_back((float)-nm2.z()); normals.push_back((float)nm2.y()); - } else { - normals.push_back(0.0f); normals.push_back(0.0f); normals.push_back(1.0f); - } - } else { - normals.push_back(0.0f); normals.push_back(0.0f); normals.push_back(1.0f); - } - } else { - normals.push_back(0.0f); normals.push_back(0.0f); normals.push_back(1.0f); - } - if (t && i < t->size()) { - texcoords.push_back((float)(*t)[i].x()); - texcoords.push_back((float)(*t)[i].y()); - } else if (t2d && i < t2d->size()) { - texcoords.push_back((float)(*t2d)[i].x()); - texcoords.push_back((float)(*t2d)[i].y()); - } else if (ta && ta->getNumElements() > i) { - GLenum tdt = ta->getDataType(); - unsigned int tcnt = ta->getNumElements(); - unsigned int tbytes = ta->getTotalDataSize(); - unsigned int tcomps = tdt == GL_FLOAT ? tbytes / (tcnt * (unsigned int)sizeof(float)) : tdt == GL_DOUBLE ? tbytes / (tcnt * (unsigned int)sizeof(double)) : 0; - if (tcomps >= 2) { - if (tdt == GL_FLOAT) { - const float* tptr = static_cast(ta->getDataPointer()); - texcoords.push_back((float)tptr[i*tcomps+0]); - texcoords.push_back((float)tptr[i*tcomps+1]); - } else if (tdt == GL_DOUBLE) { - const double* tptr = static_cast(ta->getDataPointer()); - texcoords.push_back((float)tptr[i*tcomps+0]); - texcoords.push_back((float)tptr[i*tcomps+1]); - } else { - texcoords.push_back(0.0f); texcoords.push_back(0.0f); - } - } else { - texcoords.push_back(0.0f); texcoords.push_back(0.0f); - } - } else { - texcoords.push_back(0.0f); texcoords.push_back(0.0f); - } - batchIds.push_back((float)inst.originalBatchId); - } - } else { - const double* ptr = static_cast(va->getDataPointer()); - for (unsigned int i = 0; i < cnt; ++i) { - osg::Vec3d p(ptr[i*comps+0], ptr[i*comps+1], ptr[i*comps+2]); - p = p * inst.matrix; - float px = (float)p.x(); - float py = (float)-p.z(); - float pz = (float)p.y(); - positions.push_back(px); positions.push_back(py); positions.push_back(pz); - if (px < minPos[0]) minPos[0] = px; - if (py < minPos[1]) minPos[1] = py; - if (pz < minPos[2]) minPos[2] = pz; - if (px > maxPos[0]) maxPos[0] = px; - if (py > maxPos[1]) maxPos[1] = py; - if (pz > maxPos[2]) maxPos[2] = pz; - if (outBox) outBox->expandBy(osg::Vec3d(px, py, pz)); - if (n && i < n->size()) { - osg::Vec3 nm = (*n)[i]; - osg::Vec3d nmd(nm.x(), nm.y(), nm.z()); - nmd = osg::Matrix::transform3x3(normalXform, nmd); nmd.normalize(); - normals.push_back((float)nmd.x()); normals.push_back((float)-nmd.z()); normals.push_back((float)nmd.y()); - } else if (n3d && i < n3d->size()) { - osg::Vec3d nmd = (*n3d)[i]; - nmd = osg::Matrix::transform3x3(normalXform, nmd); nmd.normalize(); - normals.push_back((float)nmd.x()); normals.push_back((float)-nmd.z()); normals.push_back((float)nmd.y()); - } else if (na && na->getNumElements() > i) { - GLenum ndt = na->getDataType(); - unsigned int ncnt = na->getNumElements(); - unsigned int nbytes = na->getTotalDataSize(); - unsigned int ncomps = ndt == GL_FLOAT ? nbytes / (ncnt * (unsigned int)sizeof(float)) : ndt == GL_DOUBLE ? nbytes / (ncnt * (unsigned int)sizeof(double)) : 0; - if (ncomps >= 3) { - if (ndt == GL_FLOAT) { - const float* nptr = static_cast(na->getDataPointer()); - osg::Vec3d nm2((double)nptr[i*ncomps+0], (double)nptr[i*ncomps+1], (double)nptr[i*ncomps+2]); - nm2 = osg::Matrix::transform3x3(normalXform, nm2); nm2.normalize(); - normals.push_back((float)nm2.x()); normals.push_back((float)-nm2.z()); normals.push_back((float)nm2.y()); - } else if (ndt == GL_DOUBLE) { - const double* nptr = static_cast(na->getDataPointer()); - osg::Vec3d nm2(nptr[i*ncomps+0], nptr[i*ncomps+1], nptr[i*ncomps+2]); - nm2 = osg::Matrix::transform3x3(normalXform, nm2); nm2.normalize(); - normals.push_back((float)nm2.x()); normals.push_back((float)-nm2.z()); normals.push_back((float)nm2.y()); - } else { - normals.push_back(0.0f); normals.push_back(0.0f); normals.push_back(1.0f); - } - } else { - normals.push_back(0.0f); normals.push_back(0.0f); normals.push_back(1.0f); - } - } else { - normals.push_back(0.0f); normals.push_back(0.0f); normals.push_back(1.0f); - } - if (t && i < t->size()) { - texcoords.push_back((float)(*t)[i].x()); - texcoords.push_back((float)(*t)[i].y()); - } else if (t2d && i < t2d->size()) { - texcoords.push_back((float)(*t2d)[i].x()); - texcoords.push_back((float)(*t2d)[i].y()); - } else if (ta && ta->getNumElements() > i) { - GLenum tdt = ta->getDataType(); - unsigned int tcnt = ta->getNumElements(); - unsigned int tbytes = ta->getTotalDataSize(); - unsigned int tcomps = tdt == GL_FLOAT ? tbytes / (tcnt * (unsigned int)sizeof(float)) : tdt == GL_DOUBLE ? tbytes / (tcnt * (unsigned int)sizeof(double)) : 0; - if (tcomps >= 2) { - if (tdt == GL_FLOAT) { - const float* tptr = static_cast(ta->getDataPointer()); - texcoords.push_back((float)tptr[i*tcomps+0]); - texcoords.push_back((float)tptr[i*tcomps+1]); - } else if (tdt == GL_DOUBLE) { - const double* tptr = static_cast(ta->getDataPointer()); - texcoords.push_back((float)tptr[i*tcomps+0]); - texcoords.push_back((float)tptr[i*tcomps+1]); - } else { - texcoords.push_back(0.0f); texcoords.push_back(0.0f); - } - } else { - texcoords.push_back(0.0f); texcoords.push_back(0.0f); - } - } else { - texcoords.push_back(0.0f); texcoords.push_back(0.0f); - } - batchIds.push_back((float)inst.originalBatchId); - } - } - handledVertices = true; - } - } - } - if (!handledVertices) { - missingVertexInstances++; - if (dbgTileName) { - if (!va) { - LOG_I("Tile %s: missing vertex array (null)", dbgTileName); - } else if (va->getNumElements() == 0) { - LOG_I("Tile %s: empty vertex array (0 elements), type: %s", dbgTileName, typeid(*va).name()); - } else { - LOG_I("Tile %s: unsupported vertex array type: %s", dbgTileName, typeid(*va).name()); - } - } - continue; - } - } - - if (v && !v->empty()) { - for (unsigned int i = 0; i < v->size(); ++i) { - osg::Vec3 vf = (*v)[i]; - osg::Vec3d p(vf.x(), vf.y(), vf.z()); - p = p * inst.matrix; - float px = (float)p.x(); - float py = (float)-p.z(); - float pz = (float)p.y(); - positions.push_back(px); positions.push_back(py); positions.push_back(pz); - if (px < minPos[0]) minPos[0] = px; - if (py < minPos[1]) minPos[1] = py; - if (pz < minPos[2]) minPos[2] = pz; - if (px > maxPos[0]) maxPos[0] = px; - if (py > maxPos[1]) maxPos[1] = py; - if (pz > maxPos[2]) maxPos[2] = pz; - if (outBox) outBox->expandBy(osg::Vec3d(px, py, pz)); - if (n && i < n->size()) { - osg::Vec3 nmf = (*n)[i]; - osg::Vec3d nm(nmf.x(), nmf.y(), nmf.z()); - nm = osg::Matrix::transform3x3(normalXform, nm); nm.normalize(); - normals.push_back((float)nm.x()); normals.push_back((float)-nm.z()); normals.push_back((float)nm.y()); - } else if (n3d && i < n3d->size()) { - osg::Vec3d nm = (*n3d)[i]; - nm = osg::Matrix::transform3x3(normalXform, nm); nm.normalize(); - normals.push_back((float)nm.x()); normals.push_back((float)-nm.z()); normals.push_back((float)nm.y()); - } else { - normals.push_back(0.0f); normals.push_back(0.0f); normals.push_back(1.0f); - } - if (t && i < t->size()) { - texcoords.push_back((float)(*t)[i].x()); - texcoords.push_back((float)(*t)[i].y()); - } else if (t2d && i < t2d->size()) { - texcoords.push_back((float)(*t2d)[i].x()); - texcoords.push_back((float)(*t2d)[i].y()); - } else { - texcoords.push_back(0.0f); texcoords.push_back(0.0f); - } - batchIds.push_back((float)inst.originalBatchId); - } - } else if (v3d && !v3d->empty()) { - for (unsigned int i = 0; i < v3d->size(); ++i) { - osg::Vec3d p = (*v3d)[i]; - p = p * inst.matrix; - float px = (float)p.x(); - float py = (float)-p.z(); - float pz = (float)p.y(); - positions.push_back(px); positions.push_back(py); positions.push_back(pz); - if (px < minPos[0]) minPos[0] = px; - if (py < minPos[1]) minPos[1] = py; - if (pz < minPos[2]) minPos[2] = pz; - if (px > maxPos[0]) maxPos[0] = px; - if (py > maxPos[1]) maxPos[1] = py; - if (pz > maxPos[2]) maxPos[2] = pz; - if (outBox) outBox->expandBy(osg::Vec3d(px, py, pz)); - if (n3d && i < n3d->size()) { - osg::Vec3d nm = (*n3d)[i]; - nm = osg::Matrix::transform3x3(normalXform, nm); nm.normalize(); - normals.push_back((float)nm.x()); normals.push_back((float)-nm.z()); normals.push_back((float)nm.y()); - } else if (n && i < n->size()) { - osg::Vec3 nmf = (*n)[i]; - osg::Vec3d nm(nmf.x(), nmf.y(), nmf.z()); - nm = osg::Matrix::transform3x3(normalXform, nm); nm.normalize(); - normals.push_back((float)nm.x()); normals.push_back((float)-nm.z()); normals.push_back((float)nm.y()); - } else { - normals.push_back(0.0f); normals.push_back(0.0f); normals.push_back(1.0f); - } - if (t2d && i < t2d->size()) { - texcoords.push_back((float)(*t2d)[i].x()); - texcoords.push_back((float)(*t2d)[i].y()); - } else if (t && i < t->size()) { - texcoords.push_back((float)(*t)[i].x()); - texcoords.push_back((float)(*t)[i].y()); - } else { - texcoords.push_back(0.0f); texcoords.push_back(0.0f); - } - batchIds.push_back((float)inst.originalBatchId); - } - } else if (v4 && !v4->empty()) { - for (unsigned int i = 0; i < v4->size(); ++i) { - osg::Vec4 vf = (*v4)[i]; - osg::Vec3d p(vf.x(), vf.y(), vf.z()); - p = p * inst.matrix; - double gx = p.x(); - double gy = -p.z(); - double gz = p.y(); - float px = (float)gx; - float py = (float)gy; - float pz = (float)gz; - positions.push_back(px); positions.push_back(py); positions.push_back(pz); - if (px < minPos[0]) minPos[0] = px; - if (py < minPos[1]) minPos[1] = py; - if (pz < minPos[2]) minPos[2] = pz; - if (px > maxPos[0]) maxPos[0] = px; - if (py > maxPos[1]) maxPos[1] = py; - if (pz > maxPos[2]) maxPos[2] = pz; - if (outBox) outBox->expandBy(osg::Vec3d(gx, gy, gz)); - if (n && i < n->size()) { - osg::Vec3 nmf = (*n)[i]; - osg::Vec3d nm(nmf.x(), nmf.y(), nmf.z()); - nm = osg::Matrix::transform3x3(normalXform, nm); nm.normalize(); - normals.push_back((float)nm.x()); normals.push_back((float)-nm.z()); normals.push_back((float)nm.y()); - } else if (n3d && i < n3d->size()) { - osg::Vec3d nm = (*n3d)[i]; - nm = osg::Matrix::transform3x3(normalXform, nm); nm.normalize(); - normals.push_back((float)nm.x()); normals.push_back((float)-nm.z()); normals.push_back((float)nm.y()); - } else { - normals.push_back(0.0f); normals.push_back(0.0f); normals.push_back(1.0f); - } - if (t && i < t->size()) { - texcoords.push_back((float)(*t)[i].x()); - texcoords.push_back((float)(*t)[i].y()); - } else if (t2d && i < t2d->size()) { - texcoords.push_back((float)(*t2d)[i].x()); - texcoords.push_back((float)(*t2d)[i].y()); - } else { - texcoords.push_back(0.0f); texcoords.push_back(0.0f); - } - batchIds.push_back((float)inst.originalBatchId); - } - } else if (v4d && !v4d->empty()) { - for (unsigned int i = 0; i < v4d->size(); ++i) { - osg::Vec4d vd = (*v4d)[i]; - osg::Vec3d p(vd.x(), vd.y(), vd.z()); - p = p * inst.matrix; - double gx = p.x(); - double gy = -p.z(); - double gz = p.y(); - float px = (float)gx; - float py = (float)gy; - float pz = (float)gz; - positions.push_back(px); positions.push_back(py); positions.push_back(pz); - if (px < minPos[0]) minPos[0] = px; - if (py < minPos[1]) minPos[1] = py; - if (pz < minPos[2]) minPos[2] = pz; - if (px > maxPos[0]) maxPos[0] = px; - if (py > maxPos[1]) maxPos[1] = py; - if (pz > maxPos[2]) maxPos[2] = pz; - if (outBox) outBox->expandBy(osg::Vec3d(gx, gy, gz)); - if (n3d && i < n3d->size()) { - osg::Vec3d nm = (*n3d)[i]; - nm = osg::Matrix::transform3x3(normalXform, nm); nm.normalize(); - normals.push_back((float)nm.x()); normals.push_back((float)-nm.z()); normals.push_back((float)nm.y()); - } else if (n && i < n->size()) { - osg::Vec3 nmf = (*n)[i]; - osg::Vec3d nm(nmf.x(), nmf.y(), nmf.z()); - nm = osg::Matrix::transform3x3(normalXform, nm); nm.normalize(); - normals.push_back((float)nm.x()); normals.push_back((float)-nm.z()); normals.push_back((float)nm.y()); - } else { - normals.push_back(0.0f); normals.push_back(0.0f); normals.push_back(1.0f); - } - if (t2d && i < t2d->size()) { - texcoords.push_back((float)(*t2d)[i].x()); - texcoords.push_back((float)(*t2d)[i].y()); - } else if (t && i < t->size()) { - texcoords.push_back((float)(*t)[i].x()); - texcoords.push_back((float)(*t)[i].y()); - } else { - texcoords.push_back(0.0f); texcoords.push_back(0.0f); - } - batchIds.push_back((float)inst.originalBatchId); - } - } - - // Indices - for (unsigned int k = 0; k < processedGeom->getNumPrimitiveSets(); ++k) { - osg::PrimitiveSet* ps = processedGeom->getPrimitiveSet(k); - osg::PrimitiveSet::Mode mode = (osg::PrimitiveSet::Mode)ps->getMode(); - if (mode == osg::PrimitiveSet::TRIANGLES) { triSets++; } - else if (mode == osg::PrimitiveSet::TRIANGLE_STRIP) { stripSets++; } - else if (mode == osg::PrimitiveSet::TRIANGLE_FAN) { fanSets++; } - else { otherSets++; continue; } - - const osg::DrawElementsUShort* deus = dynamic_cast(ps); - const osg::DrawElementsUInt* deui = dynamic_cast(ps); - const osg::DrawArrays* da = dynamic_cast(ps); - if (da) { - drawArraysSets++; - if (mode == osg::PrimitiveSet::TRIANGLES) { - unsigned int first = da->getFirst(); - unsigned int count = da->getCount(); - for (unsigned int idx = 0; idx + 2 < count; idx += 3) { - indices.push_back(baseIndex + first + idx); - indices.push_back(baseIndex + first + idx + 1); - indices.push_back(baseIndex + first + idx + 2); - } - } else if (mode == osg::PrimitiveSet::TRIANGLE_STRIP) { - unsigned int first = da->getFirst(); - unsigned int count = da->getCount(); - for (unsigned int i = 0; i + 2 < count; ++i) { - unsigned int a = baseIndex + first + i; - unsigned int b = baseIndex + first + i + 1; - unsigned int c = baseIndex + first + i + 2; - if ((i & 1) == 0) { indices.push_back(a); indices.push_back(b); indices.push_back(c); } - else { indices.push_back(b); indices.push_back(a); indices.push_back(c); } - } - } else if (mode == osg::PrimitiveSet::TRIANGLE_FAN) { - unsigned int first = da->getFirst(); - unsigned int count = da->getCount(); - unsigned int center = baseIndex + first; - for (unsigned int i = 1; i + 1 < count; ++i) { - indices.push_back(center); - indices.push_back(baseIndex + first + i); - indices.push_back(baseIndex + first + i + 1); - } - } - } - - if (deus) { - if (mode == osg::PrimitiveSet::TRIANGLES) { - for (unsigned int idx = 0; idx < deus->size(); ++idx) indices.push_back(baseIndex + (*deus)[idx]); - } else if (mode == osg::PrimitiveSet::TRIANGLE_STRIP) { - if (deus->size() >= 3) { - for (unsigned int i = 0; i + 2 < deus->size(); ++i) { - unsigned int a = baseIndex + (*deus)[i]; - unsigned int b = baseIndex + (*deus)[i + 1]; - unsigned int c = baseIndex + (*deus)[i + 2]; - if ((i & 1) == 0) { indices.push_back(a); indices.push_back(b); indices.push_back(c); } - else { indices.push_back(b); indices.push_back(a); indices.push_back(c); } - } - } - } else if (mode == osg::PrimitiveSet::TRIANGLE_FAN) { - if (deus->size() >= 3) { - unsigned int center = baseIndex + (*deus)[0]; - for (unsigned int i = 1; i + 1 < deus->size(); ++i) { - indices.push_back(center); - indices.push_back(baseIndex + (*deus)[i]); - indices.push_back(baseIndex + (*deus)[i + 1]); - } - } - } - } else if (deui) { - if (mode == osg::PrimitiveSet::TRIANGLES) { - for (unsigned int idx = 0; idx < deui->size(); ++idx) indices.push_back(baseIndex + (*deui)[idx]); - } else if (mode == osg::PrimitiveSet::TRIANGLE_STRIP) { - if (deui->size() >= 3) { - for (unsigned int i = 0; i + 2 < deui->size(); ++i) { - unsigned int a = baseIndex + (*deui)[i]; - unsigned int b = baseIndex + (*deui)[i + 1]; - unsigned int c = baseIndex + (*deui)[i + 2]; - if ((i & 1) == 0) { indices.push_back(a); indices.push_back(b); indices.push_back(c); } - else { indices.push_back(b); indices.push_back(a); indices.push_back(c); } - } - } - } else if (mode == osg::PrimitiveSet::TRIANGLE_FAN) { - if (deui->size() >= 3) { - unsigned int center = baseIndex + (*deui)[0]; - for (unsigned int i = 1; i + 1 < deui->size(); ++i) { - indices.push_back(center); - indices.push_back(baseIndex + (*deui)[i]); - indices.push_back(baseIndex + (*deui)[i + 1]); - } - } - } - } - } - } - - if (positions.empty() || indices.empty()) { - if (dbgTileName) { - LOG_I("Tile %s: group produced no triangles: v=%zu i=%zu tri=%d strip=%d fan=%d other=%d missVtxInst=%d drawArrays=%d", - dbgTileName, - positions.size() / 3, - indices.size() / 3, - triSets, stripSets, fanSets, otherSets, - missingVertexInstances, drawArraysSets); - } - continue; - } - if (stats) { - stats->vertex_count += positions.size() / 3; - stats->triangle_count += indices.size() / 3; - } - - // Prepare Draco compression if enabled - bool dracoCompressed = false; - int dracoBufferViewIdx = -1; - int dracoPosId = -1, dracoNormId = -1, dracoTexId = -1, dracoBatchId = -1; - - if (settings.enableDraco) { - osg::ref_ptr tempGeom = new osg::Geometry; - osg::ref_ptr va = new osg::Vec3Array; - for(size_t i=0; ipush_back(osg::Vec3(positions[i], positions[i+1], positions[i+2])); - tempGeom->setVertexArray(va); - - if(!normals.empty()) { - osg::ref_ptr na = new osg::Vec3Array; - for(size_t i=0; ipush_back(osg::Vec3(normals[i], normals[i+1], normals[i+2])); - tempGeom->setNormalArray(na); - tempGeom->setNormalBinding(osg::Geometry::BIND_PER_VERTEX); - } - - if(!texcoords.empty()) { - osg::ref_ptr ta = new osg::Vec2Array; - for(size_t i=0; ipush_back(osg::Vec2(texcoords[i], texcoords[i+1])); - tempGeom->setTexCoordArray(0, ta); - } - - osg::ref_ptr de = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES); - for(unsigned int idx : indices) de->push_back(idx); - tempGeom->addPrimitiveSet(de); - - DracoCompressionParams dracoParams; - dracoParams.enable_compression = true; - std::vector compressedData; - size_t compressedSize = 0; - - if (compress_mesh_geometry(tempGeom.get(), dracoParams, compressedData, compressedSize, &dracoPosId, &dracoNormId, &dracoTexId, &dracoBatchId, &batchIds)) { - size_t bufOffset = buffer.data.size(); - size_t padding = (4 - (bufOffset % 4)) % 4; - if (padding > 0) { - buffer.data.resize(bufOffset + padding); - memset(buffer.data.data() + bufOffset, 0, padding); - bufOffset += padding; - } - - buffer.data.resize(bufOffset + compressedSize); - memcpy(buffer.data.data() + bufOffset, compressedData.data(), compressedSize); - - tinygltf::BufferView bv; - bv.buffer = 0; - bv.byteOffset = bufOffset; - bv.byteLength = compressedSize; - dracoBufferViewIdx = (int)model.bufferViews.size(); - model.bufferViews.push_back(bv); - - dracoCompressed = true; - - // Register extension - if (std::find(model.extensionsUsed.begin(), model.extensionsUsed.end(), "KHR_draco_mesh_compression") == model.extensionsUsed.end()) { - model.extensionsUsed.push_back("KHR_draco_mesh_compression"); - model.extensionsRequired.push_back("KHR_draco_mesh_compression"); - } - } - } - - int bvPosIdx = -1, bvNormIdx = -1, bvTexIdx = -1, bvIndIdx = -1, bvBatchIdx = -1; - - if (!dracoCompressed) { - auto alignTo4 = [](size_t currentSize) -> size_t { - size_t padding = (4 - (currentSize % 4)) % 4; - return padding; - }; - - size_t posPadding = alignTo4(buffer.data.size()); - for (size_t i = 0; i < posPadding; ++i) { - buffer.data.push_back(0); - } - - size_t posOffset = buffer.data.size(); - size_t posLen = positions.size() * sizeof(float); - buffer.data.resize(posOffset + posLen); - memcpy(buffer.data.data() + posOffset, positions.data(), posLen); - - size_t normPadding = alignTo4(buffer.data.size()); - for (size_t i = 0; i < normPadding; ++i) { - buffer.data.push_back(0); - } - - size_t normOffset = buffer.data.size(); - size_t normLen = normals.size() * sizeof(float); - buffer.data.resize(normOffset + normLen); - memcpy(buffer.data.data() + normOffset, normals.data(), normLen); - - size_t texPadding = alignTo4(buffer.data.size()); - for (size_t i = 0; i < texPadding; ++i) { - buffer.data.push_back(0); - } - - size_t texOffset = buffer.data.size(); - size_t texLen = texcoords.size() * sizeof(float); - buffer.data.resize(texOffset + texLen); - memcpy(buffer.data.data() + texOffset, texcoords.data(), texLen); - - size_t indPadding = alignTo4(buffer.data.size()); - for (size_t i = 0; i < indPadding; ++i) { - buffer.data.push_back(0); - } - - size_t indOffset = buffer.data.size(); - size_t indLen = indices.size() * sizeof(unsigned int); - buffer.data.resize(indOffset + indLen); - memcpy(buffer.data.data() + indOffset, indices.data(), indLen); - - size_t batchPadding = alignTo4(buffer.data.size()); - for (size_t i = 0; i < batchPadding; ++i) { - buffer.data.push_back(0); - } - - size_t batchOffset = buffer.data.size(); - size_t batchLen = batchIds.size() * sizeof(float); - buffer.data.resize(batchOffset + batchLen); - memcpy(buffer.data.data() + batchOffset, batchIds.data(), batchLen); - - // BufferViews - tinygltf::BufferView bvPos; - bvPos.buffer = 0; - bvPos.byteOffset = posOffset; - bvPos.byteLength = posLen; - bvPos.target = TINYGLTF_TARGET_ARRAY_BUFFER; - bvPosIdx = (int)model.bufferViews.size(); - model.bufferViews.push_back(bvPos); - - tinygltf::BufferView bvNorm; - bvNorm.buffer = 0; - bvNorm.byteOffset = normOffset; - bvNorm.byteLength = normLen; - bvNorm.target = TINYGLTF_TARGET_ARRAY_BUFFER; - bvNormIdx = (int)model.bufferViews.size(); - model.bufferViews.push_back(bvNorm); - - tinygltf::BufferView bvTex; - bvTex.buffer = 0; - bvTex.byteOffset = texOffset; - bvTex.byteLength = texLen; - bvTex.target = TINYGLTF_TARGET_ARRAY_BUFFER; - bvTexIdx = (int)model.bufferViews.size(); - model.bufferViews.push_back(bvTex); - - tinygltf::BufferView bvInd; - bvInd.buffer = 0; - bvInd.byteOffset = indOffset; - bvInd.byteLength = indLen; - bvInd.target = TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER; - bvIndIdx = (int)model.bufferViews.size(); - model.bufferViews.push_back(bvInd); - - tinygltf::BufferView bvBatch; - bvBatch.buffer = 0; - bvBatch.byteOffset = batchOffset; - bvBatch.byteLength = batchLen; - bvBatch.target = TINYGLTF_TARGET_ARRAY_BUFFER; - bvBatchIdx = (int)model.bufferViews.size(); - model.bufferViews.push_back(bvBatch); - } - - // Accessors - tinygltf::Accessor accPos; - accPos.bufferView = dracoCompressed ? -1 : bvPosIdx; - accPos.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; - accPos.count = positions.size() / 3; - accPos.type = TINYGLTF_TYPE_VEC3; - accPos.minValues = {minPos[0], minPos[1], minPos[2]}; - accPos.maxValues = {maxPos[0], maxPos[1], maxPos[2]}; - int accPosIdx = (int)model.accessors.size(); - model.accessors.push_back(accPos); - - tinygltf::Accessor accNorm; - accNorm.bufferView = dracoCompressed ? -1 : bvNormIdx; - accNorm.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; - accNorm.count = normals.size() / 3; - accNorm.type = TINYGLTF_TYPE_VEC3; - int accNormIdx = (int)model.accessors.size(); - model.accessors.push_back(accNorm); - - tinygltf::Accessor accTex; - accTex.bufferView = dracoCompressed ? -1 : bvTexIdx; - accTex.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; - accTex.count = texcoords.size() / 2; - accTex.type = TINYGLTF_TYPE_VEC2; - int accTexIdx = (int)model.accessors.size(); - model.accessors.push_back(accTex); - - tinygltf::Accessor accInd; - accInd.bufferView = dracoCompressed ? -1 : bvIndIdx; - accInd.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT; - accInd.count = indices.size(); - accInd.type = TINYGLTF_TYPE_SCALAR; - int accIndIdx = (int)model.accessors.size(); - model.accessors.push_back(accInd); - - tinygltf::Accessor accBatch; - accBatch.bufferView = dracoCompressed ? -1 : bvBatchIdx; - accBatch.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; - accBatch.count = batchIds.size(); - accBatch.type = TINYGLTF_TYPE_SCALAR; - int accBatchIdx = (int)model.accessors.size(); - model.accessors.push_back(accBatch); - - // Material - tinygltf::Material mat; - mat.name = "Default"; - mat.doubleSided = true; // Fix for potential backface culling issues - - if (settings.enableUnlit) { - mat.extensions["KHR_materials_unlit"] = tinygltf::Value(tinygltf::Value::Object()); - if (std::find(model.extensionsUsed.begin(), model.extensionsUsed.end(), "KHR_materials_unlit") == model.extensionsUsed.end()) { - model.extensionsUsed.push_back("KHR_materials_unlit"); - } - } - - const osg::StateSet* stateSet = pair.first; - std::vector baseColor = {1.0, 1.0, 1.0, 1.0}; - double emissiveColor[3] = {0.0, 0.0, 0.0}; - float roughnessFactor = 1.0f; - float metallicFactor = 0.0f; - float aoStrength = 1.0f; - - // Check for texture in StateSet - if (stateSet) { - // Get Material Color - const osg::Material* osgMat = dynamic_cast(stateSet->getAttribute(osg::StateAttribute::MATERIAL)); - if (osgMat) { - osg::Vec4 diffuse = osgMat->getDiffuse(osg::Material::FRONT); - baseColor = {diffuse.r(), diffuse.g(), diffuse.b(), diffuse.a()}; - osg::Vec4 em = osgMat->getEmission(osg::Material::FRONT); - emissiveColor[0] = em.r(); - emissiveColor[1] = em.g(); - emissiveColor[2] = em.b(); - } - const osg::Uniform* uR = stateSet->getUniform("roughnessFactor"); - if (uR) uR->get(roughnessFactor); - const osg::Uniform* uM = stateSet->getUniform("metallicFactor"); - if (uM) uM->get(metallicFactor); - const osg::Uniform* uAO = stateSet->getUniform("aoStrength"); - if (uAO) uAO->get(aoStrength); - - const osg::Texture* tex = dynamic_cast(stateSet->getTextureAttribute(0, osg::StateAttribute::TEXTURE)); - if (tex && tex->getNumImages() > 0) { - const osg::Image* img = tex->getImage(0); - if (img) { - std::string imgPath = img->getFileName(); - std::vector imgData; - std::string mimeType = "image/png"; // default - bool hasData = false; - - GLenum pf = img->getPixelFormat(); - GLenum dt = img->getDataType(); - int w = img->s(); - int h = img->t(); - LOG_I("Texture: %s, pixelFormat=0x%X, dataType=0x%X, width=%d, height=%d, hasData=%d", - imgPath.empty() ? "(embedded)" : imgPath.c_str(), pf, dt, w, h, img->data() ? 1 : 0); - - // Try KTX2 compression if enabled - if (settings.enableTextureCompress) { - std::vector compressedData; - std::string compressedMime; - if (process_texture(const_cast(tex), compressedData, compressedMime, true)) { - if (compressedMime == "image/ktx2") { - imgData = compressedData; - mimeType = compressedMime; - hasData = true; - - // Register extension - if (std::find(model.extensionsUsed.begin(), model.extensionsUsed.end(), "KHR_texture_basisu") == model.extensionsUsed.end()) { - model.extensionsUsed.push_back("KHR_texture_basisu"); - model.extensionsRequired.push_back("KHR_texture_basisu"); - } - } - } - } - - bool hasAlphaTransparency = false; - { - int channels = 0; - if (pf == GL_LUMINANCE) channels = 1; - else if (pf == GL_LUMINANCE_ALPHA) channels = 2; - else if (pf == GL_RGB) channels = 3; - else if (pf == GL_RGBA) channels = 4; - if ((channels == 2 || channels == 4) && dt == GL_UNSIGNED_BYTE && img->data() && w > 0 && h > 0) { - const unsigned char* p = img->data(); - int alphaIndex = (channels == 2) ? 1 : 3; - int total = w * h; - for (int i = 0; i < total; ++i) { - if (p[i * channels + alphaIndex] < 255) { - hasAlphaTransparency = true; - break; - } - } - } - } - if (!hasData && !imgPath.empty() && fs::exists(imgPath)) { - std::ifstream file(imgPath, std::ios::binary | std::ios::ate); - if (file) { - size_t size = file.tellg(); - imgData.resize(size); - file.seekg(0); - file.read(reinterpret_cast(imgData.data()), size); - hasData = true; - - std::string ext = fs::path(imgPath).extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); - if (ext == ".jpg" || ext == ".jpeg") mimeType = "image/jpeg"; - } - } - - // Fallback: If file not found but image data exists (e.g. embedded or generated) - if (!hasData && img->data() != nullptr) { - LOG_I("Fallback: Writing image from memory, imgPath=%s, pixelFormat=0x%X, width=%d, height=%d", - imgPath.empty() ? "(empty)" : imgPath.c_str(), pf, w, h); - - // glTF only supports PNG and JPEG, so always use PNG for other formats - std::string ext = "png"; - if (!imgPath.empty()) { - std::string e = fs::path(imgPath).extension().string(); - if (!e.empty() && e.size() > 1) { - e = e.substr(1); // remove dot - std::transform(e.begin(), e.end(), e.begin(), ::tolower); - // Only use jpg/jpeg extension, otherwise force PNG - if (e == "jpg" || e == "jpeg") { - ext = e; - } - } - } - - // Try to write to memory - std::stringstream ss; - osgDB::ReaderWriter* rw = osgDB::Registry::instance()->getReaderWriterForExtension(ext); - if (rw) { - osgDB::ReaderWriter::WriteResult wr = rw->writeImage(*img, ss); - if (wr.success()) { - std::string s = ss.str(); - imgData.assign(s.begin(), s.end()); - hasData = true; - if (ext == "jpg" || ext == "jpeg") mimeType = "image/jpeg"; - else mimeType = "image/png"; - } - } - - // Retry with PNG if failed - if (!hasData && ext != "png") { - rw = osgDB::Registry::instance()->getReaderWriterForExtension("png"); - if (rw) { - std::stringstream ss2; - osgDB::ReaderWriter::WriteResult wr = rw->writeImage(*img, ss2); - if (wr.success()) { - std::string s = ss2.str(); - imgData.assign(s.begin(), s.end()); - hasData = true; - mimeType = "image/png"; - } - } - } - } - - if (hasData) { - LOG_I("Writing image: mimeType=%s, size=%zu", mimeType.c_str(), imgData.size()); - // Add Image - tinygltf::Image gltfImg; - gltfImg.mimeType = mimeType; - - // Create BufferView for image data (Embedded) - // Ensure 4-byte alignment before writing image - size_t currentSize = buffer.data.size(); - size_t padding = (4 - (currentSize % 4)) % 4; - if (padding > 0) { - buffer.data.resize(currentSize + padding); - // Fill padding with 0 - memset(buffer.data.data() + currentSize, 0, padding); - } - - size_t imgOffset = buffer.data.size(); - size_t imgLen = imgData.size(); - buffer.data.resize(imgOffset + imgLen); - memcpy(buffer.data.data() + imgOffset, imgData.data(), imgLen); - - tinygltf::BufferView bvImg; - bvImg.buffer = 0; - bvImg.byteOffset = imgOffset; - bvImg.byteLength = imgLen; - int bvImgIdx = (int)model.bufferViews.size(); - model.bufferViews.push_back(bvImg); - - gltfImg.bufferView = bvImgIdx; - int imgIdx = (int)model.images.size(); - model.images.push_back(gltfImg); - - // Add Texture - tinygltf::Texture gltfTex; - if (mimeType == "image/ktx2") { - tinygltf::Value::Object ktxExt; - ktxExt["source"] = tinygltf::Value(imgIdx); - gltfTex.extensions["KHR_texture_basisu"] = tinygltf::Value(ktxExt); - } else { - gltfTex.source = imgIdx; - } - int texIdx = (int)model.textures.size(); - model.textures.push_back(gltfTex); - - // Link to Material - mat.pbrMetallicRoughness.baseColorTexture.index = texIdx; - - // Ensure 4-byte alignment AFTER writing image, so next bufferView starts aligned - size_t endSize = buffer.data.size(); - size_t endPadding = (4 - (endSize % 4)) % 4; - if (endPadding > 0) { - buffer.data.resize(endSize + endPadding); - memset(buffer.data.data() + endSize, 0, endPadding); - } - if (hasAlphaTransparency) { - mat.alphaMode = "BLEND"; - } - } - } - } - const osg::Texture* ntex = dynamic_cast(stateSet->getTextureAttribute(1, osg::StateAttribute::TEXTURE)); - if (ntex && ntex->getNumImages() > 0) { - const osg::Image* img = ntex->getImage(0); - if (img) { - std::string imgPath = img->getFileName(); - std::vector imgData; - std::string mimeType = "image/png"; - bool hasData = false; - - // Try KTX2 compression if enabled - if (settings.enableTextureCompress) { - std::vector compressedData; - std::string compressedMime; - if (process_texture(const_cast(ntex), compressedData, compressedMime, true)) { - if (compressedMime == "image/ktx2") { - imgData = compressedData; - mimeType = compressedMime; - hasData = true; - - // Register extension - if (std::find(model.extensionsUsed.begin(), model.extensionsUsed.end(), "KHR_texture_basisu") == model.extensionsUsed.end()) { - model.extensionsUsed.push_back("KHR_texture_basisu"); - model.extensionsRequired.push_back("KHR_texture_basisu"); - } - } - } - } - - if (!hasData && !imgPath.empty() && fs::exists(imgPath)) { - std::ifstream file(imgPath, std::ios::binary | std::ios::ate); - if (file) { - size_t size = file.tellg(); - imgData.resize(size); - file.seekg(0); - file.read(reinterpret_cast(imgData.data()), size); - hasData = true; - std::string ext = fs::path(imgPath).extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); - if (ext == ".jpg" || ext == ".jpeg") mimeType = "image/jpeg"; - } - } - if (!hasData && img->data() != nullptr) { - std::string ext = "png"; - if (!imgPath.empty()) { - std::string e = fs::path(imgPath).extension().string(); - if (!e.empty() && e.size() > 1) { - ext = e.substr(1); - std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); - } - } - osgDB::ReaderWriter* rw = osgDB::Registry::instance()->getReaderWriterForExtension(ext); - if (rw) { - std::stringstream ss; - osgDB::ReaderWriter::WriteResult wr = rw->writeImage(*img, ss); - if (wr.success()) { - std::string s = ss.str(); - imgData.assign(s.begin(), s.end()); - hasData = true; - if (ext == "jpg" || ext == "jpeg") mimeType = "image/jpeg"; - else mimeType = "image/png"; - } - } - if (!hasData && ext != "png") { - rw = osgDB::Registry::instance()->getReaderWriterForExtension("png"); - if (rw) { - std::stringstream ss2; - osgDB::ReaderWriter::WriteResult wr = rw->writeImage(*img, ss2); - if (wr.success()) { - std::string s = ss2.str(); - imgData.assign(s.begin(), s.end()); - hasData = true; - mimeType = "image/png"; - } - } - } - } - if (hasData) { - tinygltf::Image gltfImg; - gltfImg.mimeType = mimeType; - size_t currentSize = buffer.data.size(); - size_t padding = (4 - (currentSize % 4)) % 4; - if (padding > 0) { - buffer.data.resize(currentSize + padding); - memset(buffer.data.data() + currentSize, 0, padding); - } - size_t imgOffset = buffer.data.size(); - size_t imgLen = imgData.size(); - buffer.data.resize(imgOffset + imgLen); - memcpy(buffer.data.data() + imgOffset, imgData.data(), imgLen); - tinygltf::BufferView bvImg; - bvImg.buffer = 0; - bvImg.byteOffset = imgOffset; - bvImg.byteLength = imgLen; - int bvImgIdx = (int)model.bufferViews.size(); - model.bufferViews.push_back(bvImg); - gltfImg.bufferView = bvImgIdx; - int imgIdx = (int)model.images.size(); - model.images.push_back(gltfImg); - tinygltf::Texture gltfTex; - if (mimeType == "image/ktx2") { - tinygltf::Value::Object ktxExt; - ktxExt["source"] = tinygltf::Value(imgIdx); - gltfTex.extensions["KHR_texture_basisu"] = tinygltf::Value(ktxExt); - } else { - gltfTex.source = imgIdx; - } - int texIdx = (int)model.textures.size(); - model.textures.push_back(gltfTex); - mat.normalTexture.index = texIdx; - size_t endSize = buffer.data.size(); - size_t endPadding = (4 - (endSize % 4)) % 4; - if (endPadding > 0) { - buffer.data.resize(endSize + endPadding); - memset(buffer.data.data() + endSize, 0, endPadding); - } - } - } - } - const osg::Texture* etex = dynamic_cast(stateSet->getTextureAttribute(4, osg::StateAttribute::TEXTURE)); - if (etex && etex->getNumImages() > 0) { - const osg::Image* img = etex->getImage(0); - if (img) { - std::string imgPath = img->getFileName(); - std::vector imgData; - std::string mimeType = "image/png"; - bool hasData = false; - - // Try KTX2 compression if enabled - if (settings.enableTextureCompress) { - std::vector compressedData; - std::string compressedMime; - if (process_texture(const_cast(etex), compressedData, compressedMime, true)) { - if (compressedMime == "image/ktx2") { - imgData = compressedData; - mimeType = compressedMime; - hasData = true; - - // Register extension - if (std::find(model.extensionsUsed.begin(), model.extensionsUsed.end(), "KHR_texture_basisu") == model.extensionsUsed.end()) { - model.extensionsUsed.push_back("KHR_texture_basisu"); - model.extensionsRequired.push_back("KHR_texture_basisu"); - } - } - } - } - - if (!hasData && !imgPath.empty() && fs::exists(imgPath)) { - std::ifstream file(imgPath, std::ios::binary | std::ios::ate); - if (file) { - size_t size = file.tellg(); - imgData.resize(size); - file.seekg(0); - file.read(reinterpret_cast(imgData.data()), size); - hasData = true; - std::string ext = fs::path(imgPath).extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); - if (ext == ".jpg" || ext == ".jpeg") mimeType = "image/jpeg"; - } - } - if (!hasData && img->data() != nullptr) { - std::string ext = "png"; - if (!imgPath.empty()) { - std::string e = fs::path(imgPath).extension().string(); - if (!e.empty() && e.size() > 1) { - ext = e.substr(1); - std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); - } - } - osgDB::ReaderWriter* rw = osgDB::Registry::instance()->getReaderWriterForExtension(ext); - if (rw) { - std::stringstream ss; - osgDB::ReaderWriter::WriteResult wr = rw->writeImage(*img, ss); - if (wr.success()) { - std::string s = ss.str(); - imgData.assign(s.begin(), s.end()); - hasData = true; - if (ext == "jpg" || ext == "jpeg") mimeType = "image/jpeg"; - else mimeType = "image/png"; - } - } - if (!hasData && ext != "png") { - rw = osgDB::Registry::instance()->getReaderWriterForExtension("png"); - if (rw) { - std::stringstream ss2; - osgDB::ReaderWriter::WriteResult wr = rw->writeImage(*img, ss2); - if (wr.success()) { - std::string s = ss2.str(); - imgData.assign(s.begin(), s.end()); - hasData = true; - mimeType = "image/png"; - } - } - } - } - if (hasData) { - tinygltf::Image gltfImg; - gltfImg.mimeType = mimeType; - size_t currentSize = buffer.data.size(); - size_t padding = (4 - (currentSize % 4)) % 4; - if (padding > 0) { - buffer.data.resize(currentSize + padding); - memset(buffer.data.data() + currentSize, 0, padding); - } - size_t imgOffset = buffer.data.size(); - size_t imgLen = imgData.size(); - buffer.data.resize(imgOffset + imgLen); - memcpy(buffer.data.data() + imgOffset, imgData.data(), imgLen); - tinygltf::BufferView bvImg; - bvImg.buffer = 0; - bvImg.byteOffset = imgOffset; - bvImg.byteLength = imgLen; - int bvImgIdx = (int)model.bufferViews.size(); - model.bufferViews.push_back(bvImg); - gltfImg.bufferView = bvImgIdx; - int imgIdx = (int)model.images.size(); - model.images.push_back(gltfImg); - tinygltf::Texture gltfTex; - if (mimeType == "image/ktx2") { - tinygltf::Value::Object ktxExt; - ktxExt["source"] = tinygltf::Value(imgIdx); - gltfTex.extensions["KHR_texture_basisu"] = tinygltf::Value(ktxExt); - } else { - gltfTex.source = imgIdx; - } - int texIdx = (int)model.textures.size(); - model.textures.push_back(gltfTex); - mat.emissiveTexture.index = texIdx; - size_t endSize = buffer.data.size(); - size_t endPadding = (4 - (endSize % 4)) % 4; - if (endPadding > 0) { - buffer.data.resize(endSize + endPadding); - memset(buffer.data.data() + endSize, 0, endPadding); - } - } - } - } - const osg::Texture* rtex = dynamic_cast(stateSet->getTextureAttribute(2, osg::StateAttribute::TEXTURE)); - const osg::Texture* mtex = dynamic_cast(stateSet->getTextureAttribute(3, osg::StateAttribute::TEXTURE)); - const osg::Texture* atex = dynamic_cast(stateSet->getTextureAttribute(5, osg::StateAttribute::TEXTURE)); - if ((rtex && rtex->getNumImages() > 0) || (mtex && mtex->getNumImages() > 0) || (atex && atex->getNumImages() > 0)) { - const osg::Image* rimg = rtex ? rtex->getImage(0) : nullptr; - const osg::Image* mimg = mtex ? mtex->getImage(0) : nullptr; - const osg::Image* aimg = atex ? atex->getImage(0) : nullptr; - int rw = rimg ? rimg->s() : 0, rh = rimg ? rimg->t() : 0; - int mw = mimg ? mimg->s() : 0, mh = mimg ? mimg->t() : 0; - int aw = aimg ? aimg->s() : 0, ah = aimg ? aimg->t() : 0; - int tw = std::max({rw, mw, aw}); - int th = std::max({rh, mh, ah}); - if (tw == 0 || th == 0) { - // Fallback: use 1x1 texture from factors - tw = 1; th = 1; - } - auto extract_channel = [](const osg::Image* img, int& w, int& h) -> std::vector { - std::vector out; - if (!img || !img->data()) { w = 0; h = 0; return out; } - w = img->s(); h = img->t(); - out.assign(w * h, 0); - int channels = 4; - GLenum pf = img->getPixelFormat(); - if (pf == GL_LUMINANCE) channels = 1; - else if (pf == GL_LUMINANCE_ALPHA) channels = 2; - else if (pf == GL_RGB) channels = 3; - else if (pf == GL_RGBA) channels = 4; - const unsigned char* p = img->data(); - for (int i = 0; i < w * h; ++i) out[i] = p[i * channels]; - return out; - }; - auto bilinear = [](const std::vector& src, int sw, int sh, int tw, int th) -> std::vector { - if (sw == tw && sh == th) return src; - std::vector dst(tw * th, 0); - const float sx = sw > 1 ? (float)(sw - 1) / (float)(tw - 1) : 0.0f; - const float sy = sh > 1 ? (float)(sh - 1) / (float)(th - 1) : 0.0f; - for (int y = 0; y < th; ++y) { - float fy = y * sy; - int y0 = (int)floorf(fy); - int y1 = std::min(y0 + 1, sh - 1); - float ty = fy - y0; - for (int x = 0; x < tw; ++x) { - float fx = x * sx; - int x0 = (int)floorf(fx); - int x1 = std::min(x0 + 1, sw - 1); - float tx = fx - x0; - int i00 = y0 * sw + x0; - int i10 = y0 * sw + x1; - int i01 = y1 * sw + x0; - int i11 = y1 * sw + x1; - float v0 = src.empty() ? 0.0f : (float)src[i00] * (1.0f - tx) + (float)src[i10] * tx; - float v1 = src.empty() ? 0.0f : (float)src[i01] * (1.0f - tx) + (float)src[i11] * tx; - float v = v0 * (1.0f - ty) + v1 * ty; - dst[y * tw + x] = (unsigned char)std::round(std::min(std::max(v, 0.0f), 255.0f)); - } - } - return dst; - }; - int rw0 = 0, rh0 = 0, mw0 = 0, mh0 = 0, aw0 = 0, ah0 = 0; - std::vector rch = extract_channel(rimg, rw0, rh0); - std::vector mch = extract_channel(mimg, mw0, mh0); - std::vector aoch = extract_channel(aimg, aw0, ah0); - if (!rch.empty()) rch = bilinear(rch, rw0, rh0, tw, th); - if (!mch.empty()) mch = bilinear(mch, mw0, mh0, tw, th); - if (!aoch.empty()) aoch = bilinear(aoch, aw0, ah0, tw, th); - std::vector mr(tw * th * 3, 0xff); - for (int i = 0; i < tw * th; ++i) { - mr[i * 3 + 0] = atex && !aoch.empty() ? aoch[i] : 0xff; - mr[i * 3 + 1] = rtex && !rch.empty() ? rch[i] : (unsigned char)std::round(roughnessFactor * 255.0f); - mr[i * 3 + 2] = mtex && !mch.empty() ? mch[i] : (unsigned char)std::round(metallicFactor * 255.0f); - } - std::string finalMimeType = "image/png"; - std::vector finalData; - - if (settings.enableTextureCompress) { - std::vector mr_rgba(tw * th * 4); - for (int i = 0; i < tw * th; ++i) { - mr_rgba[i * 4 + 0] = mr[i * 3 + 0]; - mr_rgba[i * 4 + 1] = mr[i * 3 + 1]; - mr_rgba[i * 4 + 2] = mr[i * 3 + 2]; - mr_rgba[i * 4 + 3] = 255; - } - if (compress_to_ktx2(mr_rgba, tw, th, finalData)) { - finalMimeType = "image/ktx2"; - - // Register extension if not already - if (std::find(model.extensionsUsed.begin(), model.extensionsUsed.end(), "KHR_texture_basisu") == model.extensionsUsed.end()) { - model.extensionsUsed.push_back("KHR_texture_basisu"); - model.extensionsRequired.push_back("KHR_texture_basisu"); - } - } - } - - if (finalData.empty()) { - osg::ref_ptr outImg = new osg::Image(); - outImg->allocateImage(tw, th, 1, GL_RGB, GL_UNSIGNED_BYTE); - memcpy(outImg->data(), mr.data(), mr.size()); - osgDB::ReaderWriter* writer = osgDB::Registry::instance()->getReaderWriterForExtension("png"); - if (writer) { - std::stringstream ss; - osgDB::ReaderWriter::WriteResult wr = writer->writeImage(*outImg, ss); - if (wr.success()) { - std::string s = ss.str(); - finalData.assign(s.begin(), s.end()); - } - } - } - - if (!finalData.empty()) { - size_t currentSize = buffer.data.size(); - size_t padding = (4 - (currentSize % 4)) % 4; - if (padding > 0) { - buffer.data.resize(currentSize + padding); - memset(buffer.data.data() + currentSize, 0, padding); - } - size_t imgOffset = buffer.data.size(); - size_t imgLen = finalData.size(); - buffer.data.resize(imgOffset + imgLen); - memcpy(buffer.data.data() + imgOffset, finalData.data(), imgLen); - tinygltf::BufferView bvImg; - bvImg.buffer = 0; - bvImg.byteOffset = imgOffset; - bvImg.byteLength = imgLen; - int bvImgIdx = (int)model.bufferViews.size(); - model.bufferViews.push_back(bvImg); - tinygltf::Image gltfImg; - gltfImg.mimeType = finalMimeType; - gltfImg.bufferView = bvImgIdx; - int imgIdx = (int)model.images.size(); - model.images.push_back(gltfImg); - tinygltf::Texture gltfTex; - - if (finalMimeType == "image/ktx2") { - tinygltf::Value::Object ktxExt; - ktxExt["source"] = tinygltf::Value(imgIdx); - gltfTex.extensions["KHR_texture_basisu"] = tinygltf::Value(ktxExt); - } else { - gltfTex.source = imgIdx; - } - - int texIdx = (int)model.textures.size(); - model.textures.push_back(gltfTex); - mat.pbrMetallicRoughness.metallicRoughnessTexture.index = texIdx; - mat.occlusionTexture.index = texIdx; - float aoStrengthOut = (atex && aimg && !aoch.empty()) ? aoStrength : 1.0f; - mat.occlusionTexture.strength = aoStrengthOut; - size_t endSize = buffer.data.size(); - size_t endPadding = (4 - (endSize % 4)) % 4; - if (endPadding > 0) { - buffer.data.resize(endSize + endPadding); - memset(buffer.data.data() + endSize, 0, endPadding); - } - } - } - } - - mat.pbrMetallicRoughness.baseColorFactor = baseColor; - if (mat.alphaMode.empty()) { - if (baseColor[3] < 0.99) mat.alphaMode = "BLEND"; - else mat.alphaMode = "OPAQUE"; - } - - mat.pbrMetallicRoughness.metallicFactor = metallicFactor; - mat.pbrMetallicRoughness.roughnessFactor = roughnessFactor; - mat.emissiveFactor = {emissiveColor[0], emissiveColor[1], emissiveColor[2]}; - int matIdx = (int)model.materials.size(); - model.materials.push_back(mat); - - // Mesh - tinygltf::Mesh mesh; - tinygltf::Primitive prim; - prim.mode = TINYGLTF_MODE_TRIANGLES; - prim.indices = accIndIdx; - prim.attributes["POSITION"] = accPosIdx; - prim.attributes["NORMAL"] = accNormIdx; - prim.attributes["TEXCOORD_0"] = accTexIdx; - prim.attributes["_BATCHID"] = accBatchIdx; - prim.material = matIdx; - - if (dracoCompressed) { - tinygltf::Value::Object dracoExt; - dracoExt["bufferView"] = tinygltf::Value(dracoBufferViewIdx); - - tinygltf::Value::Object dracoAttribs; - if (dracoPosId != -1) dracoAttribs["POSITION"] = tinygltf::Value(dracoPosId); - if (dracoNormId != -1) dracoAttribs["NORMAL"] = tinygltf::Value(dracoNormId); - if (dracoTexId != -1) dracoAttribs["TEXCOORD_0"] = tinygltf::Value(dracoTexId); - if (dracoBatchId != -1) dracoAttribs["_BATCHID"] = tinygltf::Value(dracoBatchId); - - dracoExt["attributes"] = tinygltf::Value(dracoAttribs); - - prim.extensions["KHR_draco_mesh_compression"] = tinygltf::Value(dracoExt); - } - - mesh.primitives.push_back(prim); - int meshIdx = (int)model.meshes.size(); - model.meshes.push_back(mesh); - - // Node - tinygltf::Node node; - node.mesh = meshIdx; - int nodeIdx = (int)model.nodes.size(); - model.nodes.push_back(node); - } - - // Scene - tinygltf::Scene scene; - for (size_t i = 0; i < model.nodes.size(); ++i) { - scene.nodes.push_back((int)i); - } - model.scenes.push_back(scene); - model.defaultScene = 0; -} - -json FBXPipeline::processNode(OctreeNode* node, const std::string& parentPath, int parentDepth, int childIndexAtParent, const std::string& treePath) { - json nodeJson; - nodeJson["refine"] = "REPLACE"; - - osg::BoundingBoxd tightBox; - bool hasTightBox = false; - - // 2. Content - if (!node->content.empty()) { - // Naming convention: tile_{treePath} - std::string tileName = "tile_" + treePath; - - // Create content - // For B3DM - SimplificationParams simParams; - simParams.enable_simplification = settings.enableSimplify; - simParams.target_ratio = 0.5f; - simParams.target_error = 0.0001f; // Base error - auto result = createB3DM(node->content, parentPath, tileName, simParams); - std::string contentUrl = result.first; - osg::BoundingBoxd cBox = result.second; - - if (!contentUrl.empty()) { - nodeJson["content"] = {{"uri", contentUrl}}; - if (cBox.valid()) { - tightBox.expandBy(cBox); - hasTightBox = true; - } - } - } - - // 3. Children - if (!node->children.empty()) { - nodeJson["children"] = json::array(); - for (size_t i = 0; i < node->children.size(); ++i) { - auto child = node->children[i]; - json childJson = processNode(child, parentPath, node->depth, (int)i, treePath + "_" + std::to_string(i)); - bool isEmptyChild = (!childJson.contains("content")) && (!childJson.contains("children") || childJson["children"].empty()); - if (!isEmptyChild) { - nodeJson["children"].push_back(childJson); - try { - auto& cBoxJson = childJson["boundingVolume"]["box"]; - if (cBoxJson.is_array() && cBoxJson.size() == 12) { - double cx = cBoxJson[0]; - double cy = cBoxJson[1]; - double cz = cBoxJson[2]; - double dx = cBoxJson[3]; - double dy = cBoxJson[7]; - double dz = cBoxJson[11]; - tightBox.expandBy(osg::Vec3d(cx - dx, cy - dy, cz - dz)); - tightBox.expandBy(osg::Vec3d(cx + dx, cy + dy, cz + dz)); - hasTightBox = true; - } - } catch (...) {} - } else { - LOG_I("Filtered empty tile: parentDepth=%d childIndex=%d nodes=%zu", node->depth, (int)i, node->children[i]->content.size()); - } - } - } - - // 1. Calculate Geometric Error and Bounding Volume - double diagonal = 0.0; - if (hasTightBox) { - double dx = tightBox.xMax() - tightBox.xMin(); - double dy = tightBox.yMax() - tightBox.yMin(); - double dz = tightBox.zMax() - tightBox.zMin(); - double diagonalOriginal = std::sqrt(dx*dx + dy*dy + dz*dz); - - double cx = tightBox.center().x(); - double cy = tightBox.center().y(); - double cz = tightBox.center().z(); - double hx = (tightBox.xMax() - tightBox.xMin()) / 2.0; - double hy = (tightBox.yMax() - tightBox.yMin()) / 2.0; - double hz = (tightBox.zMax() - tightBox.zMin()) / 2.0; - - // Add small padding to avoid near-plane culling or precision issues - hx = std::max(hx * 1.25, 1e-6); - hy = std::max(hy * 1.25, 1e-6); - hz = std::max(hz * 1.25, 1e-6); - diagonal = 2.0 * std::sqrt(hx*hx + hy*hy + hz*hz); - LOG_I("Node depth=%d tightBox center=(%.3f,%.3f,%.3f) halfAxes=(%.3f,%.3f,%.3f) diagOriginal=%.3f diagInflated=%.3f inflate=1.25", node->depth, cx, cy, cz, hx, hy, hz, diagonalOriginal, diagonal); - - nodeJson["boundingVolume"] = { - {"box", { - cx, cy, cz, - hx, 0, 0, - 0, hy, 0, - 0, 0, hz - }} - }; - } else { - // Fallback: Transform node->bbox from Y-up to Z-up - double cx = node->bbox.center().x(); - double cy = node->bbox.center().y(); - double cz = node->bbox.center().z(); - - double extentX = (node->bbox.xMax() - node->bbox.xMin()) / 2.0; - double extentY = (node->bbox.yMax() - node->bbox.yMin()) / 2.0; - double extentZ = (node->bbox.zMax() - node->bbox.zMin()) / 2.0; - - extentX = std::max(extentX, 1e-6); - extentY = std::max(extentY, 1e-6); - extentZ = std::max(extentZ, 1e-6); - diagonal = 2.0 * std::sqrt(extentX*extentX + extentY*extentY + extentZ*extentZ); - LOG_I("Node depth=%d fallbackBox center=(%.3f,%.3f,%.3f) halfAxes=(%.3f,%.3f,%.3f) diag=%.3f", node->depth, cx, -cz, cy, extentX, extentZ, extentY, diagonal); - - diagonal = std::sqrt(extentX*extentX*4 + extentY*extentY*4 + extentZ*extentZ*4); - - // Collect Tile Stats - double dimX = extentX * 2.0; - double dimY = extentY * 2.0; - double dimZ = extentZ * 2.0; - double vol = dimX * dimY * dimZ; - std::string tName = "Node_d" + std::to_string(node->depth) + "_i" + std::to_string(childIndexAtParent); - if (!node->content.empty()) tName += "_Content"; - - tileStats.push_back({ - tName, node->depth, vol, dimX, dimY, dimZ, - osg::Vec3d(cx, cy, cz), - osg::Vec3d(cx - extentX, cy - extentY, cz - extentZ), - osg::Vec3d(cx + extentX, cy + extentY, cz + extentZ) - }); - - nodeJson["boundingVolume"] = { - {"box", { - cx, -cz, cy, // Center transformed - extentX, 0, 0, // X axis - 0, extentZ, 0, // Y axis (was Z) - 0, 0, extentY // Z axis (was Y) - }} - }; - } - - // Geometric error = scale * diagonal (no clamp). Ensure > 0 by epsilon if degenerate. - double geOut = std::max(1e-3, settings.geScale * diagonal); - nodeJson["geometricError"] = geOut; - std::string refineMode = "REPLACE"; - nodeJson["refine"] = refineMode; - LOG_I("Node depth=%d isLeaf=%d content=%zu children=%zu geScale=%.3f geOut=%.3f refine=%s", node->depth, (int)node->isLeaf(), node->content.size(), node->children.size(), settings.geScale, geOut, refineMode.c_str()); - { - auto& acc = levelStats[node->depth]; - acc.count += 1; - acc.sumDiag += diagonal; - acc.sumGe += geOut; - if (hasTightBox) acc.tightCount += 1; else acc.fallbackCount += 1; - if (refineMode == "ADD") acc.refineAdd += 1; else acc.refineReplace += 1; - - // Log contained nodes - if (!node->content.empty()) { - std::string tName = "Node_d" + std::to_string(node->depth) + "_i" + std::to_string(childIndexAtParent); - if (!node->content.empty()) tName += "_Content"; - - for (const auto& ref : node->content) { - std::string nName = (ref.transformIndex < ref.meshInfo->nodeNames.size()) ? ref.meshInfo->nodeNames[ref.transformIndex] : "unknown"; - LOG_I("Tile: %s contains Node: %s", tName.c_str(), nName.c_str()); - } - } - } - - return nodeJson; -} - -std::pair FBXPipeline::createB3DM(const std::vector& instances, const std::string& tilePath, const std::string& tileName, const SimplificationParams& simParams) { - // 1. Create GLB (TinyGLTF) - tinygltf::Model model; - tinygltf::Asset asset; - asset.version = "2.0"; - asset.generator = "FBX23DTiles"; - model.asset = asset; - - json batchTableJson; - int batchIdCounter = 0; - osg::BoundingBoxd contentBox; - - TileStats tileStats; - appendGeometryToModel(model, instances, settings, &batchTableJson, &batchIdCounter, simParams, &contentBox, &tileStats, tileName.c_str()); - LOG_I("Tile %s: nodes=%zu triangles=%zu vertices=%zu materials=%zu", tileName.c_str(), tileStats.node_count, tileStats.triangle_count, tileStats.vertex_count, tileStats.material_count); - - // Populate Batch Table with node names and attributes - std::vector batchNames; - std::vector> allAttrs; - std::set attrKeys; - - for (const auto& ref : instances) { - if (!ref.meshInfo || !ref.meshInfo->geometry) continue; - std::string nName = "unknown"; - std::unordered_map attrs; - - if (ref.transformIndex < ref.meshInfo->nodeNames.size()) { - nName = ref.meshInfo->nodeNames[ref.transformIndex]; - } - if (ref.transformIndex < ref.meshInfo->nodeAttrs.size()) { - attrs = ref.meshInfo->nodeAttrs[ref.transformIndex]; - for (const auto& kv : attrs) attrKeys.insert(kv.first); - } - batchNames.push_back(nName); - allAttrs.push_back(attrs); - } - - if (!batchNames.empty()) { - batchTableJson["name"] = batchNames; - } - - // Add other attributes - for (const std::string& key : attrKeys) { - // Skip "name" as it is already handled - if (key == "name") continue; - - std::vector values; - for (const auto& attrs : allAttrs) { - auto it = attrs.find(key); - if (it != attrs.end()) { - values.push_back(it->second); - } else { - values.push_back(""); // Default empty string - } - } - batchTableJson[key] = values; - } - - // Skip writing B3DM if no mesh content was generated - if (tileStats.triangle_count == 0 || model.meshes.empty()) { - LOG_I("Tile %s: no content generated, skip B3DM", tileName.c_str()); - return {"", contentBox}; - } - - // 2. Create B3DM wrapping GLB - std::string filename = tileName + ".b3dm"; - std::string fullPath = (fs::path(tilePath) / filename).string(); - - std::ofstream outfile(fullPath, std::ios::binary); - if (!outfile) { - LOG_E("Failed to create B3DM file: %s", fullPath.c_str()); - return {"", contentBox}; - } - - // Serialize GLB to memory - tinygltf::TinyGLTF gltf; - std::stringstream ss; - gltf.WriteGltfSceneToStream(&model, ss, false, true); // pretty=false, binary=true - std::string glbData = ss.str(); - - // Create Feature Table JSON - json featureTable; - - // For single instance (or merged mesh), if we have only 1 batch ID (0), - // we can simplify by setting BATCH_LENGTH to 0, which implies no batching. - // This avoids issues with _BATCHID attribute requirement if BATCH_LENGTH > 0. - // However, if we do have multiple batches, we set it. - if (batchIdCounter == 0) { - featureTable["BATCH_LENGTH"] = 0; - } else { - featureTable["BATCH_LENGTH"] = batchIdCounter; - } - - std::string featureTableString = featureTable.dump(); - size_t featureTableJsonByteLength = featureTableString.size(); - // Per 3D Tiles spec: JSON must be padded with spaces to 8-byte boundary - // The padding ensures the NEXT section starts at an 8-byte aligned offset - // Padding calculation: (8 - (current_offset + json_length) % 8) % 8 - // where current_offset is the byte offset where JSON starts (28 for B3DM) - size_t featureTableStartOffset = sizeof(B3dmHeader); // 28 bytes - size_t featureTablePadding = (8 - ((featureTableStartOffset + featureTableJsonByteLength) % 8)) % 8; - featureTableString.append(featureTablePadding, ' '); - featureTableJsonByteLength = featureTableString.size(); // Update to include padding - - std::string batchTableString = ""; - size_t batchTableJsonByteLength = 0; - size_t batchTablePadding = 0; - if (!batchTableJson.empty()) { - batchTableString = batchTableJson.dump(); - batchTableJsonByteLength = batchTableString.size(); - // Batch Table JSON starts after Feature Table JSON - // Feature Table Binary length is 0, so Batch Table JSON starts at featureTableStartOffset + featureTableJsonByteLength - size_t batchTableStartOffset = featureTableStartOffset + featureTableJsonByteLength; - batchTablePadding = (8 - ((batchTableStartOffset + batchTableJsonByteLength) % 8)) % 8; - batchTableString.append(batchTablePadding, ' '); - batchTableJsonByteLength = batchTableString.size(); // Update to include padding - } - - // Note: Per glTF 2.0 spec, GLB file does not require padding at the end - // The file should end immediately after the last chunk - // GLB chunks must be aligned to 4-byte (not 8-byte) boundaries - - // Update GLB header length - // GLB header: magic(4) + version(4) + length(4) = 12 bytes - // Per glTF 2.0 spec: length is the total length of the GLB file in bytes, - // including the header and all chunks - size_t glbTotalSize = glbData.size(); - if (glbData.size() >= 12) { - uint32_t* glbLengthPtr = reinterpret_cast(&glbData[8]); - *glbLengthPtr = static_cast(glbTotalSize); - } - - // Per 3D Tiles spec 1.0: Header is 28 bytes, no padding after header - // Feature Table JSON starts immediately after header at byte 28 - // Each JSON section is padded to 8-byte boundary within its length field - - // Calculate positions for alignment verification - // Note: featureTableJsonByteLength and batchTableJsonByteLength INCLUDE padding - size_t featureTableJsonStart = sizeof(B3dmHeader); // 28 bytes - size_t featureTableBinaryStart = featureTableJsonStart + featureTableJsonByteLength; // Aligned to 8 bytes - size_t batchTableJsonStart = featureTableBinaryStart; // Since featureTableBinaryByteLength = 0 - size_t batchTableBinaryStart = batchTableJsonStart + batchTableJsonByteLength; // Aligned to 8 bytes - size_t glbStart = batchTableBinaryStart; // Since batchTableBinaryByteLength = 0 - - // Calculate total byte length - // Per 3D Tiles spec 1.0: The total byte length must be aligned to 8 bytes - size_t totalByteLength = glbStart + glbData.size(); - size_t filePadding = (8 - (totalByteLength % 8)) % 8; - totalByteLength += filePadding; - - // Write header - // Per 3D Tiles spec 1.0 and validator implementation: - // - JSON byte length INCLUDES padding to 8-byte boundary - // - Binary byte length INCLUDES padding to 8-byte boundary - // The validator calculates offsets by summing these lengths - B3dmHeader header; - header.magic = B3DM_MAGIC; - header.version = 1; - // featureTableJsonByteLength and batchTableJsonByteLength already include padding - header.featureTableJSONByteLength = (uint32_t)featureTableJsonByteLength; - header.featureTableBinaryByteLength = 0; // No binary data - header.batchTableJSONByteLength = (uint32_t)batchTableJsonByteLength; - header.batchTableBinaryByteLength = 0; // No binary data - header.byteLength = (uint32_t)totalByteLength; - - outfile.write(reinterpret_cast(&header), sizeof(B3dmHeader)); - - // Per 3D Tiles spec 1.0: Header is 28 bytes, Feature Table JSON starts immediately after - // No padding between header and Feature Table JSON - // Feature Table JSON must be padded to 8-byte boundary so that next section is aligned - - // Write feature table JSON (padding is already included in the string) - outfile.write(featureTableString.c_str(), featureTableJsonByteLength); - - // Write batch table JSON (padding is already included in the string) - if (!batchTableString.empty()) { - outfile.write(batchTableString.c_str(), batchTableJsonByteLength); - } - - outfile.write(glbData.data(), glbData.size()); - - // Write file padding to ensure total byte length is aligned to 8 bytes - // Per 3D Tiles spec: padding must be 0x00 (null bytes) - if (filePadding > 0) { - std::vector paddingBytes(filePadding, '\0'); - outfile.write(paddingBytes.data(), filePadding); - } - - outfile.close(); - - return {filename, contentBox}; -} - -std::string FBXPipeline::createI3DM(MeshInstanceInfo* meshInfo, const std::vector& transformIndices, const std::string& tilePath, const std::string& tileName, const SimplificationParams& simParams) { - return ""; -} - -void FBXPipeline::writeTilesetJson(const std::string& basePath, const osg::BoundingBox& globalBounds, const nlohmann::json& rootContent) { - json tileset; - tileset["asset"] = { - {"version", "1.0"}, - {"gltfUpAxis", "Z"} // OSG/FBX usually Z-up or we converted - }; - - // Use geometric error from root content if available, otherwise fallback to global bounds - double geometricError = 0.0; - if (rootContent.contains("geometricError")) { - geometricError = rootContent["geometricError"]; - } else { - double dx = globalBounds.xMax() - globalBounds.xMin(); - double dy = globalBounds.yMax() - globalBounds.yMin(); - double dz = globalBounds.zMax() - globalBounds.zMin(); - double diag = std::sqrt(dx*dx + dy*dy + dz*dz); - geometricError = std::max(1e-3, settings.geScale * diag); - } - tileset["geometricError"] = geometricError; - LOG_I("Tileset top-level geometricError=%.3f", geometricError); - - // Root - tileset["root"] = rootContent; - - // Force geometric error for root node if it's 0.0 (which happens if it's a leaf) - // A root node with 0.0 geometric error might cause visibility issues in some viewers - // if the screen space error calculation behaves unexpectedly. - // We set it to the calculated diagonal size to ensure it passes the SSE check initially. - if (tileset["root"]["geometricError"] == 0.0) { - double diag = 0.0; - if (rootContent["boundingVolume"].contains("box")) { - auto& box = rootContent["boundingVolume"]["box"]; - if (box.is_array() && box.size() == 12) { - // box is center(3) + x_axis(3) + y_axis(3) + z_axis(3) - // Half axes vectors (assuming AABB structure as generated in processNode) - double hx = box[3]; // box[3], box[4], box[5] - double hy = box[7]; // box[6], box[7], box[8] - double hz = box[11]; // box[9], box[10], box[11] - - // If it's not strictly AABB (rotated), we should compute length of vectors - // But our processNode generates diagonal matrices for axes. - double xlen = std::sqrt(double(box[3])*double(box[3]) + double(box[4])*double(box[4]) + double(box[5])*double(box[5])); - double ylen = std::sqrt(double(box[6])*double(box[6]) + double(box[7])*double(box[7]) + double(box[8])*double(box[8])); - double zlen = std::sqrt(double(box[9])*double(box[9]) + double(box[10])*double(box[10]) + double(box[11])*double(box[11])); - - // Diagonal of the box (2 * half_diagonal) - diag = 2.0 * std::sqrt(xlen*xlen + ylen*ylen + zlen*zlen); - LOG_I("Root boundingVolume lengths x=%.3f y=%.3f z=%.3f diag=%.3f", xlen, ylen, zlen, diag); - } - } - - if (diag > 0.0) { - tileset["root"]["geometricError"] = diag; - tileset["geometricError"] = tileset["root"]["geometricError"]; - LOG_I("Forcing root geometric error to %f (calculated from root box)", diag); - LOG_I("Tileset geometricError updated to root=%.3f", double(tileset["root"]["geometricError"])); - } else { - // Fallback to globalBounds if box missing (unlikely) - // globalBounds is Y-up from FBX, but size is same - double dx = globalBounds.xMax() - globalBounds.xMin(); - double dy = globalBounds.yMax() - globalBounds.yMin(); - double dz = globalBounds.zMax() - globalBounds.zMin(); - double fallbackDiag = std::sqrt(dx*dx + dy*dy + dz*dz); - tileset["root"]["geometricError"] = fallbackDiag; - tileset["geometricError"] = tileset["root"]["geometricError"]; - LOG_I("Forcing root geometric error to %f (calculated from global bounds)", fallbackDiag); - LOG_I("Tileset geometricError updated to fallback=%.3f", double(tileset["root"]["geometricError"])); - } - } - - // Always add Transform to anchor local ENU coordinates to ECEF - if (settings.longitude != 0.0 || settings.latitude != 0.0 || settings.height != 0.0) { - glm::dmat4 enuToEcef = coords::CoordinateTransformer::CalcEnuToEcefMatrix(settings.longitude, settings.latitude, settings.height); - - // Calculate center of the model (in original local coordinates - Y-up from FBX) - double cx = (globalBounds.xMin() + globalBounds.xMax()) * 0.5; - double cy = (globalBounds.yMin() + globalBounds.yMax()) * 0.5; - double cz = (globalBounds.zMin() + globalBounds.zMax()) * 0.5; - - // The geometry is in Z-up coordinates (x, -z, y) in B3DM. - // Model center in Z-up: (cx, -cz, cy) - // - // ENU_to_ECEF maps ENU origin (0,0,0) to target lon/lat/height. - // To place model center at target position, we need ENU origin to be at model center. - // - // In ENU coordinates (Z-up), model center is at (cx, -cz, cy). - // So we need to shift ENU origin by (cx, -cz, cy) in the ENU frame. - // - // This is equivalent to: transform = ENU_to_ECEF * translate(cx, -cz, cy) - // Which shifts the ENU origin so that model center maps to target position. - - // Apply translation to ENU origin (in ENU frame, then rotated to ECEF) - // glm is column-major - // Translation in ENU frame: (cx, -cz, cy) - // After ENU_to_ECEF rotation, this becomes a translation in ECEF - double tx = cx; - double ty = -cz; // Z-up: y is north, FBX z becomes -y in Z-up - double tz = cy; // FBX y becomes z in Z-up - - // Add translation to the transform (ENU_to_ECEF * translation) - enuToEcef[3][0] += tx * enuToEcef[0][0] + ty * enuToEcef[1][0] + tz * enuToEcef[2][0]; - enuToEcef[3][1] += tx * enuToEcef[0][1] + ty * enuToEcef[1][1] + tz * enuToEcef[2][1]; - enuToEcef[3][2] += tx * enuToEcef[0][2] + ty * enuToEcef[1][2] + tz * enuToEcef[2][2]; - - LOG_I("Model center Y-up: (%.2f, %.2f, %.2f), Z-up: (%.2f, %.2f, %.2f)", cx, cy, cz, tx, ty, tz); - - const double* m = (const double*)&enuToEcef; - tileset["root"]["transform"] = { - m[0], m[1], m[2], m[3], - m[4], m[5], m[6], m[7], - m[8], m[9], m[10], m[11], - m[12], m[13], m[14], m[15] - }; - LOG_I("Applied root transform ENU->ECEF at lon=%.6f lat=%.6f h=%.3f", settings.longitude, settings.latitude, settings.height); - } else { - LOG_W("No geolocation provided; root.transform not set. Tiles remain in local ENU space."); - } - - std::string s = tileset.dump(4); - std::ofstream out(fs::path(basePath) / "tileset.json"); - out << s; - out.close(); -} - -void FBXPipeline::logLevelStats() { - std::vector levels; - levels.reserve(levelStats.size()); - for (const auto& kv : levelStats) levels.push_back(kv.first); - std::sort(levels.begin(), levels.end()); - LOG_I("LevelStats summary begin"); - for (int d : levels) { - const auto& acc = levelStats[d]; - double avgDiag = acc.count ? acc.sumDiag / acc.count : 0.0; - double avgGe = acc.count ? acc.sumGe / acc.count : 0.0; - double tightPct = acc.count ? (double)acc.tightCount * 100.0 / (double)acc.count : 0.0; - double fallbackPct = acc.count ? (double)acc.fallbackCount * 100.0 / (double)acc.count : 0.0; - double addPct = acc.count ? (double)acc.refineAdd * 100.0 / (double)acc.count : 0.0; - double replacePct = acc.count ? (double)acc.refineReplace * 100.0 / (double)acc.count : 0.0; - LOG_I("LevelStats depth=%d tiles=%zu avgDiag=%.3f avgGe=%.3f inflate=1.25 tight=%.1f%% fallback=%.1f%% refineAdd=%.1f%% refineReplace=%.1f%%", d, acc.count, avgDiag, avgGe, tightPct, fallbackPct, addPct, replacePct); - } - LOG_I("LevelStats summary end"); -} - -nlohmann::json FBXPipeline::buildAverageTiles(const osg::BoundingBox& globalBounds, const std::string& parentPath) { - nlohmann::json rootJson; - rootJson["children"] = nlohmann::json::array(); - rootJson["refine"] = "REPLACE"; - - // Gather all instances - std::vector all; - for (auto& pair : loader->meshPool) { - MeshInstanceInfo& info = pair.second; - if (!info.geometry) continue; - for (size_t i = 0; i < info.transforms.size(); ++i) { - InstanceRef ref; - ref.meshInfo = &info; - ref.transformIndex = (int)i; - all.push_back(ref); - } - } - - // Split by average count and generate children; simultaneously accumulate ENU global bounds - osg::BoundingBox enuGlobal; - size_t total = all.size(); - size_t step = std::max(1, (size_t)settings.maxItemsPerTile); - size_t tiles = (total + step - 1) / step; - for (size_t t = 0; t < tiles; ++t) { - size_t start = t * step; - size_t end = std::min(total, start + step); - if (start >= end) break; - std::vector chunk(all.begin() + start, all.begin() + end); - std::string tileName = "tile_" + std::to_string(t); - SimplificationParams simParams; - auto b3dm = createB3DM(chunk, parentPath, tileName, simParams); - if (b3dm.first.empty()) { - LOG_I("AvgSplit tile=%s produced no content, skipped", tileName.c_str()); - continue; - } - osg::BoundingBox cb = b3dm.second; // Already ENU due to appendGeometryToModel - enuGlobal.expandBy(cb); - - double cx = cb.center().x(); - double cy = cb.center().y(); - double cz = cb.center().z(); - double hx = std::max((cb.xMax() - cb.xMin()) / 2.0, 1e-6); - double hy = std::max((cb.yMax() - cb.yMin()) / 2.0, 1e-6); - double hz = std::max((cb.zMax() - cb.zMin()) / 2.0, 1e-6); - - // Collect Tile Stats - double dimX = hx * 2.0; - double dimY = hy * 2.0; - double dimZ = hz * 2.0; - double vol = dimX * dimY * dimZ; - - tileStats.push_back({ - tileName, 1, vol, dimX, dimY, dimZ, - osg::Vec3d(cx, cy, cz), - osg::Vec3d(cb.xMin(), cb.yMin(), cb.zMin()), - osg::Vec3d(cb.xMax(), cb.yMax(), cb.zMax()) - }); - - double diag = 2.0 * std::sqrt(hx*hx + hy*hy + hz*hz); - double geOut = std::max(1e-3, settings.geScale * diag); - - nlohmann::json child; - child["boundingVolume"]["box"] = { cx, cy, cz, hx, 0, 0, 0, hy, 0, 0, 0, hz }; - child["geometricError"] = geOut; - child["refine"] = "REPLACE"; - child["content"]["uri"] = b3dm.first; - rootJson["children"].push_back(child); - - auto& acc = levelStats[1]; - acc.count += 1; - acc.sumDiag += diag; - acc.sumGe += geOut; - acc.tightCount += 1; - acc.refineReplace += 1; - LOG_I("AvgSplit tile=%s count=%zu diag=%.3f ge=%.3f", tileName.c_str(), chunk.size(), diag, geOut); - - // Log contained nodes - for (const auto& ref : chunk) { - std::string nName = (ref.transformIndex < ref.meshInfo->nodeNames.size()) ? ref.meshInfo->nodeNames[ref.transformIndex] : "unknown"; - LOG_I("Tile: %s contains Node: %s", tileName.c_str(), nName.c_str()); - } - } - - // Compute root bounding volume from union of children (ENU space, consistent with root.transform) - if (enuGlobal.valid()) { - double gcx = enuGlobal.center().x(); - double gcy = enuGlobal.center().y(); - double gcz = enuGlobal.center().z(); - double halfX = std::max((enuGlobal.xMax() - enuGlobal.xMin()) / 2.0 * 1.25, 1e-6); - double halfY = std::max((enuGlobal.yMax() - enuGlobal.yMin()) / 2.0 * 1.25, 1e-6); - double halfZ = std::max((enuGlobal.zMax() - enuGlobal.zMin()) / 2.0 * 1.25, 1e-6); - double gdiag = 2.0 * std::sqrt(halfX*halfX + halfY*halfY + halfZ*halfZ); - double gge = std::max(1e-3, settings.geScale * gdiag); - rootJson["boundingVolume"]["box"] = { gcx, gcy, gcz, halfX, 0, 0, 0, halfY, 0, 0, 0, halfZ }; - rootJson["geometricError"] = gge; - auto& acc = levelStats[0]; - acc.count += 1; - acc.sumDiag += gdiag; - acc.sumGe += gge; - acc.tightCount += 1; - acc.refineReplace += 1; - LOG_I("AvgSplit root diag=%.3f ge=%.3f center=(%.3f,%.3f,%.3f) halfAxes=(%.3f,%.3f,%.3f)", gdiag, gge, gcx, gcy, gcz, halfX, halfY, halfZ); - } else { - // Fallback to transformed original global bounds if no children created - double halfX = std::max((globalBounds.xMax() - globalBounds.xMin()) / 2.0 * 1.25, 1e-6); - double halfY = std::max((globalBounds.yMax() - globalBounds.yMin()) / 2.0 * 1.25, 1e-6); - double halfZ = std::max((globalBounds.zMax() - globalBounds.zMin()) / 2.0 * 1.25, 1e-6); - double gdiag = 2.0 * std::sqrt(halfX*halfX + halfY*halfY + halfZ*halfZ); - double gge = settings.geScale * gdiag; - double gcx = globalBounds.center().x(); - double gcy = -globalBounds.center().z(); - double gcz = globalBounds.center().y(); - rootJson["boundingVolume"]["box"] = { gcx, gcy, gcz, halfX, 0, 0, 0, halfZ, 0, 0, 0, halfY }; - rootJson["geometricError"] = gge; - auto& acc = levelStats[0]; - acc.count += 1; - acc.sumDiag += gdiag; - acc.sumGe += gge; - acc.fallbackCount += 1; - acc.refineReplace += 1; - LOG_I("AvgSplit root (fallback) diag=%.3f ge=%.3f center=(%.3f,%.3f,%.3f) halfAxes=(%.3f,%.3f,%.3f)", gdiag, gge, gcx, gcy, gcz, halfX, halfZ, halfY); - } - - return rootJson; -} - -// C-API Implementation -extern "C" void* fbx23dtile( - const char* in_path, - const char* out_path, - double* box_ptr, - int* len, - int max_lvl, - bool enable_texture_compress, - bool enable_meshopt, - bool enable_draco, - bool enable_unlit, - double longitude, - double latitude, - double height -) { - std::string input(in_path); - std::string output(out_path); - - PipelineSettings settings; - settings.inputPath = input; - settings.outputPath = output; - settings.maxDepth = max_lvl > 0 ? max_lvl : 5; - settings.enableTextureCompress = enable_texture_compress; - settings.enableDraco = enable_draco; - settings.enableSimplify = enable_meshopt; - settings.enableLOD = false; // HLOD not yet implemented - settings.enableUnlit = enable_unlit; - settings.longitude = longitude; - settings.latitude = latitude; - settings.height = height; - - FBXPipeline pipeline(settings); - pipeline.run(); - - fs::path tilesetPath = fs::path(output) / "tileset.json"; - if (!fs::exists(tilesetPath)) { - LOG_E("Failed to generate tileset.json at %s", tilesetPath.string().c_str()); - return nullptr; - } - - std::ifstream t(tilesetPath); - std::stringstream buffer; - buffer << t.rdbuf(); - std::string jsonStr = buffer.str(); - - // Parse json to get bounding box - try { - json root = json::parse(jsonStr); - auto& box = root["root"]["boundingVolume"]["box"]; - if (box.is_array() && box.size() == 12) { - double cx = box[0]; - double cy = box[1]; - double cz = box[2]; - double hx = box[3]; - double hy = box[7]; - double hz = box[11]; - - double max[3] = {cx + hx, cy + hy, cz + hz}; - double min[3] = {cx - hx, cy - hy, cz - hz}; - - memcpy(box_ptr, max, 3 * sizeof(double)); - memcpy(box_ptr + 3, min, 3 * sizeof(double)); - } - } catch (const std::exception& e) { - LOG_E("Failed to parse tileset.json: %s", e.what()); - } - - void* str = malloc(jsonStr.length() + 1); - if (str) { - memcpy(str, jsonStr.c_str(), jsonStr.length()); - ((char*)str)[jsonStr.length()] = '\0'; - *len = (int)jsonStr.length(); - } - - return str; -} diff --git a/src/FBXPipeline.h b/src/FBXPipeline.h deleted file mode 100644 index bb849568..00000000 --- a/src/FBXPipeline.h +++ /dev/null @@ -1,103 +0,0 @@ -#pragma once - -#include "fbx.h" -#include -#include -#include -#include -#include -#include -#include "mesh_processor.h" -#include - -// Forward declarations -namespace tinygltf { - class Model; -} - -struct PipelineSettings { - std::string inputPath; - std::string outputPath; - int maxDepth = 5; - int maxItemsPerTile = 1000; - - // Optimization flags - bool enableSimplify = false; - bool enableDraco = false; - bool enableTextureCompress = false; // KTX2 - bool enableLOD = false; // Enable Hierarchical LOD generation - bool enableUnlit = false; // Enable KHR_materials_unlit - std::vector lodRatios = {1.0f, 0.5f, 0.25f}; // Default LOD ratios (Fine to Coarse) - - // Geolocation (Origin) - double longitude = 0.0; - double latitude = 0.0; - double height = 0.0; - - // Geometric error scale (multiplier applied to boundingVolume diagonal) - double geScale = 0.5; // Adjusted for better LOD switching with SSE=16 - - // Split strategy: when true, split by average count using maxItemsPerTile; when false, use octree - bool splitAverageByCount = false; -} ; - -struct InstanceRef { - MeshInstanceInfo* meshInfo; - int transformIndex; -}; - -class FBXPipeline { -public: - FBXPipeline(const PipelineSettings& settings); - ~FBXPipeline(); - - void run(); - -private: - PipelineSettings settings; - FBXLoader* loader = nullptr; - struct LevelAccum { size_t count = 0; double sumDiag = 0.0; double sumGe = 0.0; size_t tightCount = 0; size_t fallbackCount = 0; size_t refineAdd = 0; size_t refineReplace = 0; }; - std::unordered_map levelStats; - - struct TileInfo { - std::string name; - int depth; - double volume; - double dx, dy, dz; - osg::Vec3d center; - osg::Vec3d minPt, maxPt; - }; - std::vector tileStats; - - void logLevelStats(); - nlohmann::json buildAverageTiles(const osg::BoundingBox& globalBounds, const std::string& parentPath); - - // Octree Node Definition - struct OctreeNode { - osg::BoundingBox bbox; - std::vector content; - std::vector children; - int depth = 0; - - bool isLeaf() const { return children.empty(); } - ~OctreeNode() { for (auto c : children) delete c; } - }; - - OctreeNode* rootNode = nullptr; - - // Build Octree - void buildOctree(OctreeNode* node); - - // Process Octree to generate Tiles - // Returns the JSON object representing this node and its children (if any) - // treePath: A string representing the path in the tree (e.g., "0_1_4") for naming - nlohmann::json processNode(OctreeNode* node, const std::string& parentPath, int parentDepth, int childIndexAtParent, const std::string& treePath); - - // Converters - // Returns filename created and the tight bounding box of the content (in ENU) - std::pair createB3DM(const std::vector& instances, const std::string& tilePath, const std::string& tileName, const SimplificationParams& simParams = SimplificationParams()); - std::string createI3DM(MeshInstanceInfo* meshInfo, const std::vector& transformIndices, const std::string& tilePath, const std::string& tileName, const SimplificationParams& simParams = SimplificationParams()); - - // Helpers - void writeTilesetJson(const std::string& basePath, const osg::BoundingBox& globalBounds, const nlohmann::json& rootContent); -}; diff --git a/src/attribute_storage.cpp b/src/attribute_storage.cpp index 7da99ed2..badc8547 100644 --- a/src/attribute_storage.cpp +++ b/src/attribute_storage.cpp @@ -1,5 +1,5 @@ #include "attribute_storage.h" -#include "extern.h" +#include "utils/log.h" #include #include diff --git a/src/b3dm/b3dm_generator.cpp b/src/b3dm/b3dm_generator.cpp new file mode 100644 index 00000000..94df6b68 --- /dev/null +++ b/src/b3dm/b3dm_generator.cpp @@ -0,0 +1,1286 @@ +#include "b3dm_generator.h" +#include "../utils/log.h" +#include "../gltf/extension_manager.h" +#include "../gltf/material_builder.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +// For image encoding +#include "stb_image_write.h" + +namespace b3dm { + +// 辅助宏 +#define SET_MIN(x,v) do{ if (x > v) x = v; }while (0); +#define SET_MAX(x,v) do{ if (x < v) x = v; }while (0); + +template +void put_val(std::vector& buf, T val) { + buf.insert(buf.end(), (unsigned char*)&val, (unsigned char*)&val + sizeof(T)); +} + +void alignment_buffer(std::vector& buf) { + while (buf.size() % 4 != 0) { + buf.push_back(0x00); + } +} + +tinygltf::BufferView create_buffer_view(int target, int byteOffset, int byteLength) { + tinygltf::BufferView bfv; + bfv.buffer = 0; + bfv.target = target; + bfv.byteOffset = byteOffset; + bfv.byteLength = byteLength; + return bfv; +} + +B3DMGenerator::B3DMGenerator(const B3DMGeneratorConfig& config) + : config_(config) {} + +osg::ref_ptr B3DMGenerator::extractAndMergeGeometries( + const spatial::core::SpatialItemRefList& items) { + + if (!config_.geometryExtractor) { + LOG_E("Geometry extractor not set"); + return nullptr; + } + + // 提取所有几何体 + std::vector> allGeoms; + for (const auto& itemRef : items) { + auto geoms = config_.geometryExtractor->extract(itemRef.get()); + allGeoms.insert(allGeoms.end(), geoms.begin(), geoms.end()); + } + + if (allGeoms.empty()) { + LOG_W("No geometries extracted from items"); + return nullptr; + } + + // 合并几何体 + osg::ref_ptr mergedGeom = new osg::Geometry; + osg::ref_ptr mergedVertices = new osg::Vec3Array(); + osg::ref_ptr mergedNormals = new osg::Vec3Array(); + osg::ref_ptr mergedTexCoords = new osg::Vec2Array(); + osg::ref_ptr mergedIndices = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES); + + for (size_t i = 0; i < allGeoms.size(); ++i) { + if (!allGeoms[i].valid()) { + continue; + } + + // 尝试获取顶点数组 (支持 Vec3Array 和 Vec3dArray) + osg::Vec3Array* vArr = dynamic_cast(allGeoms[i]->getVertexArray()); + osg::Vec3dArray* vArrd = dynamic_cast(allGeoms[i]->getVertexArray()); + + size_t vertexCount = 0; + if (vArr && !vArr->empty()) { + vertexCount = vArr->size(); + } else if (vArrd && !vArrd->empty()) { + vertexCount = vArrd->size(); + } else { + continue; + } + + osg::Vec3Array* nArr = dynamic_cast(allGeoms[i]->getNormalArray()); + osg::Vec2Array* tArr = dynamic_cast(allGeoms[i]->getTexCoordArray(0)); + + const size_t base = mergedVertices->size(); + + // 添加顶点 (转换 Vec3dArray 到 Vec3Array) + if (vArr) { + mergedVertices->insert(mergedVertices->end(), vArr->begin(), vArr->end()); + } else if (vArrd) { + for (const auto& v : *vArrd) { + mergedVertices->push_back(osg::Vec3(v.x(), v.y(), v.z())); + } + } + + if (nArr && nArr->size() == vertexCount) { + mergedNormals->insert(mergedNormals->end(), nArr->begin(), nArr->end()); + } else { + mergedNormals->insert(mergedNormals->end(), vertexCount, osg::Vec3(0.0f, 0.0f, 1.0f)); + } + + if (tArr && tArr->size() == vertexCount) { + mergedTexCoords->insert(mergedTexCoords->end(), tArr->begin(), tArr->end()); + } else { + mergedTexCoords->insert(mergedTexCoords->end(), vertexCount, osg::Vec2(0.0f, 0.0f)); + } + + if (allGeoms[i]->getNumPrimitiveSets() > 0) { + osg::PrimitiveSet* ps = allGeoms[i]->getPrimitiveSet(0); + const auto idxCnt = ps->getNumIndices(); + for (unsigned int k = 0; k < idxCnt; ++k) { + mergedIndices->push_back(static_cast(base + ps->index(k))); + } + } + } + + if (mergedVertices->empty() || mergedIndices->empty()) { + return nullptr; + } + + mergedGeom->setVertexArray(mergedVertices.get()); + mergedGeom->setNormalArray(mergedNormals.get()); + mergedGeom->setTexCoordArray(0, mergedTexCoords.get()); + mergedGeom->addPrimitiveSet(mergedIndices.get()); + + // 复制第一个有效几何体的StateSet(材质信息) + for (const auto& geom : allGeoms) { + if (geom.valid() && geom->getStateSet()) { + mergedGeom->setStateSet(const_cast(geom->getStateSet())); + break; + } + } + + return mergedGeom; +} + +std::vector B3DMGenerator::extractAndMergeGeometriesByMaterial( + const spatial::core::SpatialItemRefList& items) { + + std::vector result; + + if (!config_.geometryExtractor) { + LOG_E("Geometry extractor not set"); + return result; + } + + // 按材质分组收集几何体 + std::map>> materialGroups; + std::map> materialInfoMap; + + for (const auto& itemRef : items) { + auto geoms = config_.geometryExtractor->extract(itemRef.get()); + auto matInfo = config_.geometryExtractor->getMaterial(itemRef.get()); + + // 计算材质键值用于分组 + std::string matKey = computeMaterialKey(matInfo); + + if (materialInfoMap.find(matKey) == materialInfoMap.end()) { + materialInfoMap[matKey] = matInfo ? matInfo : std::make_shared(); + } + + for (auto& geom : geoms) { + if (geom.valid()) { + materialGroups[matKey].push_back(geom); + } + } + } + + // 对每个材质组合并几何体 + for (auto& pair : materialGroups) { + const std::string& matKey = pair.first; + auto& geoms = pair.second; + + if (geoms.empty()) { + continue; + } + + // 合并几何体 + osg::ref_ptr mergedGeom = new osg::Geometry; + osg::ref_ptr mergedVertices = new osg::Vec3Array(); + osg::ref_ptr mergedNormals = new osg::Vec3Array(); + osg::ref_ptr mergedTexCoords = new osg::Vec2Array(); + osg::ref_ptr mergedIndices = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES); + + for (size_t i = 0; i < geoms.size(); ++i) { + if (!geoms[i].valid()) { + continue; + } + + // 尝试获取顶点数组 (支持 Vec3Array 和 Vec3dArray) + osg::Vec3Array* vArr = dynamic_cast(geoms[i]->getVertexArray()); + osg::Vec3dArray* vArrd = dynamic_cast(geoms[i]->getVertexArray()); + + size_t vertexCount = 0; + if (vArr && !vArr->empty()) { + vertexCount = vArr->size(); + } else if (vArrd && !vArrd->empty()) { + vertexCount = vArrd->size(); + } else { + continue; + } + + osg::Vec3Array* nArr = dynamic_cast(geoms[i]->getNormalArray()); + osg::Vec2Array* tArr = dynamic_cast(geoms[i]->getTexCoordArray(0)); + + const size_t base = mergedVertices->size(); + + // 添加顶点 (转换 Vec3dArray 到 Vec3Array) + if (vArr) { + mergedVertices->insert(mergedVertices->end(), vArr->begin(), vArr->end()); + } else if (vArrd) { + for (const auto& v : *vArrd) { + mergedVertices->push_back(osg::Vec3(v.x(), v.y(), v.z())); + } + } + + if (nArr && nArr->size() == vertexCount) { + mergedNormals->insert(mergedNormals->end(), nArr->begin(), nArr->end()); + } else { + mergedNormals->insert(mergedNormals->end(), vertexCount, osg::Vec3(0.0f, 0.0f, 1.0f)); + } + + if (tArr && tArr->size() == vertexCount) { + mergedTexCoords->insert(mergedTexCoords->end(), tArr->begin(), tArr->end()); + } else { + mergedTexCoords->insert(mergedTexCoords->end(), vertexCount, osg::Vec2(0.0f, 0.0f)); + } + + if (geoms[i]->getNumPrimitiveSets() > 0) { + osg::PrimitiveSet* ps = geoms[i]->getPrimitiveSet(0); + const auto idxCnt = ps->getNumIndices(); + for (unsigned int k = 0; k < idxCnt; ++k) { + mergedIndices->push_back(static_cast(base + ps->index(k))); + } + } + } + + if (mergedVertices->empty() || mergedIndices->empty()) { + continue; + } + + mergedGeom->setVertexArray(mergedVertices.get()); + mergedGeom->setNormalArray(mergedNormals.get()); + mergedGeom->setTexCoordArray(0, mergedTexCoords.get()); + mergedGeom->addPrimitiveSet(mergedIndices.get()); + + // 复制第一个有效几何体的StateSet(材质信息) + for (const auto& geom : geoms) { + if (geom.valid() && geom->getStateSet()) { + mergedGeom->setStateSet(const_cast(geom->getStateSet())); + break; + } + } + + // 添加到结果 + MaterialGroup group; + group.materialInfo = materialInfoMap[matKey]; + group.geometry = mergedGeom; + result.push_back(group); + } + + return result; +} + +void B3DMGenerator::applySimplification( + osg::Geometry* geometry, + const SimplificationParams& params) { + + if (!geometry || !params.enable_simplification) { + return; + } + + simplify_mesh_geometry(geometry, params); +} + +BatchData B3DMGenerator::buildBatchData( + const spatial::core::SpatialItemRefList& items) { + + BatchData batchData; + + if (!config_.geometryExtractor) { + return batchData; + } + + // 收集所有属性键 + std::set attributeKeys; + for (const auto& itemRef : items) { + auto attrs = config_.geometryExtractor->getAttributes(itemRef.get()); + for (const auto& kv : attrs) { + attributeKeys.insert(kv.first); + } + } + + // 构建每个属性的数组 + for (const auto& key : attributeKeys) { + std::vector values; + values.reserve(items.size()); + + for (const auto& itemRef : items) { + auto attrs = config_.geometryExtractor->getAttributes(itemRef.get()); + auto it = attrs.find(key); + if (it != attrs.end()) { + values.push_back(it->second); + } else { + values.push_back(nullptr); + } + } + batchData.attributes[key] = std::move(values); + } + + // 设置batchIds和names + for (size_t i = 0; i < items.size(); ++i) { + batchData.batchIds.push_back(static_cast(i)); + batchData.names.push_back( + config_.geometryExtractor->getId(items[i].get()) + ); + } + + return batchData; +} + +void B3DMGenerator::buildGLTFModel( + osg::Geometry* mergedGeom, + const spatial::core::SpatialItemRefList& items, + bool enableDraco, + const DracoCompressionParams& dracoParams, + std::vector& glbData, + const std::vector>& materials) { + + tinygltf::Model model; + tinygltf::Buffer buffer; + tinygltf::Scene scene; + + // 提取几何体数据 + osg::Vec3Array* vertices = dynamic_cast(mergedGeom->getVertexArray()); + osg::Vec3Array* normals = dynamic_cast(mergedGeom->getNormalArray()); + osg::Vec2Array* texcoords = dynamic_cast(mergedGeom->getTexCoordArray(0)); + osg::DrawElementsUInt* indices = dynamic_cast( + mergedGeom->getPrimitiveSet(0) + ); + + if (!vertices || !indices) { + return; + } + + // Draco压缩 + const bool dracoRequested = enableDraco && dracoParams.enable_compression; + std::vector dracoData; + size_t dracoSize = 0; + int dracoPosAtt = -1, dracoNormAtt = -1, dracoTexAtt = -1; + + if (dracoRequested) { + DracoCompressionParams params = dracoParams; + params.enable_compression = true; + + bool compressSuccess = compress_mesh_geometry( + mergedGeom, params, dracoData, dracoSize, + &dracoPosAtt, &dracoNormAtt, &dracoTexAtt, + nullptr, nullptr + ); + + if (!compressSuccess) { + LOG_E("Draco compression failed"); + } + } + + // 构建Accessors和BufferViews + int indexAccessorIndex = -1; + int vertexAccessorIndex = -1; + int normalAccessorIndex = -1; + int texcoordAccessorIndex = -1; + + // 索引accessor + { + indexAccessorIndex = static_cast(model.accessors.size()); + tinygltf::Accessor acc; + acc.byteOffset = 0; + acc.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT; + acc.count = static_cast(indices->size()); + acc.type = TINYGLTF_TYPE_SCALAR; + acc.maxValues = {static_cast(vertices->size() - 1)}; + acc.minValues = {0.0}; + + if (!dracoRequested) { + int byteOffset = static_cast(buffer.data.size()); + for (unsigned int idx : *indices) { + put_val(buffer.data, idx); + } + acc.bufferView = static_cast(model.bufferViews.size()); + alignment_buffer(buffer.data); + model.bufferViews.push_back(create_buffer_view( + TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER, byteOffset, + static_cast(buffer.data.size()) - byteOffset)); + } else { + acc.bufferView = -1; + } + model.accessors.push_back(acc); + } + + // 顶点accessor + { + vertexAccessorIndex = static_cast(model.accessors.size()); + std::vector boxMax = {-1e38, -1e38, -1e38}; + std::vector boxMin = {1e38, 1e38, 1e38}; + + for (const auto& v : *vertices) { + SET_MAX(boxMax[0], v.x()); + SET_MAX(boxMax[1], v.y()); + SET_MAX(boxMax[2], v.z()); + SET_MIN(boxMin[0], v.x()); + SET_MIN(boxMin[1], v.y()); + SET_MIN(boxMin[2], v.z()); + } + + tinygltf::Accessor acc; + acc.byteOffset = 0; + acc.count = static_cast(vertices->size()); + acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + acc.type = TINYGLTF_TYPE_VEC3; + acc.maxValues = boxMax; + acc.minValues = boxMin; + + if (!dracoRequested) { + int byteOffset = static_cast(buffer.data.size()); + for (const auto& v : *vertices) { + put_val(buffer.data, v.x()); + put_val(buffer.data, v.y()); + put_val(buffer.data, v.z()); + } + acc.bufferView = static_cast(model.bufferViews.size()); + alignment_buffer(buffer.data); + model.bufferViews.push_back(create_buffer_view( + TINYGLTF_TARGET_ARRAY_BUFFER, byteOffset, + static_cast(buffer.data.size()) - byteOffset)); + } else { + acc.bufferView = -1; + } + model.accessors.push_back(acc); + } + + // 法线accessor + if (normals && !normals->empty()) { + normalAccessorIndex = static_cast(model.accessors.size()); + tinygltf::Accessor acc; + acc.byteOffset = 0; + acc.count = static_cast(normals->size()); + acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + acc.type = TINYGLTF_TYPE_VEC3; + + if (!dracoRequested) { + int byteOffset = static_cast(buffer.data.size()); + for (const auto& n : *normals) { + put_val(buffer.data, n.x()); + put_val(buffer.data, n.y()); + put_val(buffer.data, n.z()); + } + acc.bufferView = static_cast(model.bufferViews.size()); + alignment_buffer(buffer.data); + model.bufferViews.push_back(create_buffer_view( + TINYGLTF_TARGET_ARRAY_BUFFER, byteOffset, + static_cast(buffer.data.size()) - byteOffset)); + } else { + acc.bufferView = -1; + } + model.accessors.push_back(acc); + } + + // 纹理坐标accessor + if (texcoords && !texcoords->empty()) { + texcoordAccessorIndex = static_cast(model.accessors.size()); + tinygltf::Accessor acc; + acc.byteOffset = 0; + acc.count = static_cast(texcoords->size()); + acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + acc.type = TINYGLTF_TYPE_VEC2; + + if (!dracoRequested) { + int byteOffset = static_cast(buffer.data.size()); + for (const auto& t : *texcoords) { + put_val(buffer.data, t.x()); + put_val(buffer.data, t.y()); + } + acc.bufferView = static_cast(model.bufferViews.size()); + alignment_buffer(buffer.data); + model.bufferViews.push_back(create_buffer_view( + TINYGLTF_TARGET_ARRAY_BUFFER, byteOffset, + static_cast(buffer.data.size()) - byteOffset)); + } else { + acc.bufferView = -1; + } + model.accessors.push_back(acc); + } + + // ===== 构建材质 ===== + int materialIndex = -1; + if (!materials.empty() && materials[0]) { + // 使用第一个对象的材质(简化处理,实际应该根据合并后的几何体材质情况处理) + // 只要有材质信息就创建,不局限于有纹理的情况 + materialIndex = buildMaterial(materials[0], model, buffer); + } + + // 处理Draco数据 + if (dracoRequested && !dracoData.empty()) { + int dracoBufferView = static_cast(model.bufferViews.size()); + int byteOffset = static_cast(buffer.data.size()); + buffer.data.insert(buffer.data.end(), dracoData.begin(), dracoData.end()); + + tinygltf::BufferView bfv; + bfv.buffer = 0; + bfv.byteOffset = byteOffset; + bfv.byteLength = static_cast(dracoSize); + model.bufferViews.push_back(bfv); + + // 添加Draco扩展 + tinygltf::ExtensionMap dracoExt; + dracoExt["bufferView"] = tinygltf::Value(dracoBufferView); + dracoExt["attributes"] = tinygltf::Value(tinygltf::Value::Object{ + {"POSITION", tinygltf::Value(dracoPosAtt)}, + {"NORMAL", tinygltf::Value(dracoNormAtt)}, + {"TEXCOORD_0", tinygltf::Value(dracoTexAtt)} + }); + + tinygltf::ExtensionMap extMap; + extMap["KHR_draco_mesh_compression"] = tinygltf::Value(dracoExt); + + // 创建mesh和primitive + tinygltf::Mesh mesh; + tinygltf::Primitive primitive; + primitive.indices = indexAccessorIndex; + primitive.attributes["POSITION"] = vertexAccessorIndex; + if (normalAccessorIndex >= 0) { + primitive.attributes["NORMAL"] = normalAccessorIndex; + } + if (texcoordAccessorIndex >= 0) { + primitive.attributes["TEXCOORD_0"] = texcoordAccessorIndex; + } + if (materialIndex >= 0) { + primitive.material = materialIndex; + } + primitive.extensions = extMap; + primitive.mode = TINYGLTF_MODE_TRIANGLES; + mesh.primitives.push_back(primitive); + model.meshes.push_back(mesh); + + model.extensionsRequired.push_back("KHR_draco_mesh_compression"); + model.extensionsUsed.push_back("KHR_draco_mesh_compression"); + } else { + // 创建普通mesh + tinygltf::Mesh mesh; + tinygltf::Primitive primitive; + primitive.indices = indexAccessorIndex; + primitive.attributes["POSITION"] = vertexAccessorIndex; + if (normalAccessorIndex >= 0) { + primitive.attributes["NORMAL"] = normalAccessorIndex; + } + if (texcoordAccessorIndex >= 0) { + primitive.attributes["TEXCOORD_0"] = texcoordAccessorIndex; + } + if (materialIndex >= 0) { + primitive.material = materialIndex; + } + primitive.mode = TINYGLTF_MODE_TRIANGLES; + mesh.primitives.push_back(primitive); + model.meshes.push_back(mesh); + } + + // 创建node + tinygltf::Node node; + node.mesh = 0; + model.nodes.push_back(node); + scene.nodes.push_back(0); + + // 设置场景和buffer + model.scenes.push_back(scene); + model.defaultScene = 0; + model.buffers.push_back(buffer); + + // 序列化为GLB + tinygltf::TinyGLTF gltf; + std::ostringstream ss; + gltf.WriteGltfSceneToStream(&model, ss, false, true); + std::string glbStr = ss.str(); + glbData.assign(glbStr.begin(), glbStr.end()); +} + +void B3DMGenerator::buildGLTFModelMultiMaterial( + const std::vector& materialGroups, + const spatial::core::SpatialItemRefList& items, + bool enableDraco, + const DracoCompressionParams& dracoParams, + std::vector& glbData) { + + tinygltf::Model model; + tinygltf::Buffer buffer; + tinygltf::Scene scene; + + // 创建mesh,包含多个primitive(每个材质一个) + tinygltf::Mesh mesh; + + // 处理每个材质组 + for (const auto& group : materialGroups) { + if (!group.geometry.valid()) { + continue; + } + + osg::Geometry* geom = group.geometry.get(); + osg::Vec3Array* vertices = dynamic_cast(geom->getVertexArray()); + osg::Vec3Array* normals = dynamic_cast(geom->getNormalArray()); + osg::Vec2Array* texcoords = dynamic_cast(geom->getTexCoordArray(0)); + osg::DrawElementsUInt* indices = dynamic_cast( + geom->getPrimitiveSet(0) + ); + + if (!vertices || !indices) { + continue; + } + + // 记录当前的accessor索引 + int indexAccessorIndex = static_cast(model.accessors.size()); + int vertexAccessorIndex = static_cast(model.accessors.size() + 1); + int normalAccessorIndex = normals && !normals->empty() ? static_cast(model.accessors.size() + 2) : -1; + int texcoordAccessorIndex = texcoords && !texcoords->empty() ? static_cast(model.accessors.size() + 3) : -1; + + // 索引accessor + { + tinygltf::Accessor acc; + acc.byteOffset = 0; + acc.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT; + acc.count = static_cast(indices->size()); + acc.type = TINYGLTF_TYPE_SCALAR; + acc.maxValues = {static_cast(vertices->size() - 1)}; + acc.minValues = {0.0}; + + int byteOffset = static_cast(buffer.data.size()); + for (unsigned int idx : *indices) { + put_val(buffer.data, idx); + } + acc.bufferView = static_cast(model.bufferViews.size()); + alignment_buffer(buffer.data); + model.bufferViews.push_back(create_buffer_view( + TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER, byteOffset, + static_cast(buffer.data.size()) - byteOffset)); + model.accessors.push_back(acc); + } + + // 顶点accessor + { + std::vector boxMax = {-1e38, -1e38, -1e38}; + std::vector boxMin = {1e38, 1e38, 1e38}; + + for (const auto& v : *vertices) { + SET_MAX(boxMax[0], v.x()); + SET_MAX(boxMax[1], v.y()); + SET_MAX(boxMax[2], v.z()); + SET_MIN(boxMin[0], v.x()); + SET_MIN(boxMin[1], v.y()); + SET_MIN(boxMin[2], v.z()); + } + + tinygltf::Accessor acc; + acc.byteOffset = 0; + acc.count = static_cast(vertices->size()); + acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + acc.type = TINYGLTF_TYPE_VEC3; + acc.maxValues = boxMax; + acc.minValues = boxMin; + + int byteOffset = static_cast(buffer.data.size()); + for (const auto& v : *vertices) { + put_val(buffer.data, v.x()); + put_val(buffer.data, v.y()); + put_val(buffer.data, v.z()); + } + acc.bufferView = static_cast(model.bufferViews.size()); + alignment_buffer(buffer.data); + model.bufferViews.push_back(create_buffer_view( + TINYGLTF_TARGET_ARRAY_BUFFER, byteOffset, + static_cast(buffer.data.size()) - byteOffset)); + model.accessors.push_back(acc); + } + + // 法线accessor + if (normals && !normals->empty()) { + tinygltf::Accessor acc; + acc.byteOffset = 0; + acc.count = static_cast(normals->size()); + acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + acc.type = TINYGLTF_TYPE_VEC3; + + int byteOffset = static_cast(buffer.data.size()); + for (const auto& n : *normals) { + put_val(buffer.data, n.x()); + put_val(buffer.data, n.y()); + put_val(buffer.data, n.z()); + } + acc.bufferView = static_cast(model.bufferViews.size()); + alignment_buffer(buffer.data); + model.bufferViews.push_back(create_buffer_view( + TINYGLTF_TARGET_ARRAY_BUFFER, byteOffset, + static_cast(buffer.data.size()) - byteOffset)); + model.accessors.push_back(acc); + } + + // 纹理坐标accessor + if (texcoords && !texcoords->empty()) { + tinygltf::Accessor acc; + acc.byteOffset = 0; + acc.count = static_cast(texcoords->size()); + acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + acc.type = TINYGLTF_TYPE_VEC2; + + int byteOffset = static_cast(buffer.data.size()); + for (const auto& t : *texcoords) { + put_val(buffer.data, t.x()); + put_val(buffer.data, t.y()); + } + acc.bufferView = static_cast(model.bufferViews.size()); + alignment_buffer(buffer.data); + model.bufferViews.push_back(create_buffer_view( + TINYGLTF_TARGET_ARRAY_BUFFER, byteOffset, + static_cast(buffer.data.size()) - byteOffset)); + model.accessors.push_back(acc); + } + + // 构建材质 + int materialIndex = -1; + if (group.materialInfo) { + materialIndex = buildMaterial(group.materialInfo, model, buffer); + } + + // 创建primitive + tinygltf::Primitive primitive; + primitive.indices = indexAccessorIndex; + primitive.attributes["POSITION"] = vertexAccessorIndex; + if (normalAccessorIndex >= 0) { + primitive.attributes["NORMAL"] = normalAccessorIndex; + } + if (texcoordAccessorIndex >= 0) { + primitive.attributes["TEXCOORD_0"] = texcoordAccessorIndex; + } + if (materialIndex >= 0) { + primitive.material = materialIndex; + } + primitive.mode = TINYGLTF_MODE_TRIANGLES; + mesh.primitives.push_back(primitive); + } + + if (mesh.primitives.empty()) { + LOG_E("No valid primitives created"); + return; + } + + model.meshes.push_back(mesh); + + // 创建node + tinygltf::Node node; + node.mesh = 0; + model.nodes.push_back(node); + scene.nodes.push_back(0); + + // 设置场景和buffer + model.scenes.push_back(scene); + model.defaultScene = 0; + model.buffers.push_back(buffer); + + // 序列化为GLB + tinygltf::TinyGLTF gltf; + std::ostringstream ss; + gltf.WriteGltfSceneToStream(&model, ss, false, true); + std::string glbStr = ss.str(); + glbData.assign(glbStr.begin(), glbStr.end()); +} + +std::string B3DMGenerator::generateFilename(int lodLevel) const { + return "content_lod" + std::to_string(lodLevel) + ".b3dm"; +} + +std::string B3DMGenerator::generate( + const spatial::core::SpatialItemRefList& items, + const LODLevelSettings& lodSettings) { + + if (items.empty()) { + return std::string(); + } + + // 清空材质缓存(每个B3DM独立) + materialCache_.clear(); + + // 按材质分组合并几何体 + std::vector materialGroups = extractAndMergeGeometriesByMaterial(items); + if (materialGroups.empty()) { + LOG_E("Failed to extract and merge geometries"); + return std::string(); + } + + // 应用简化到每个材质组的几何体 + if (lodSettings.enable_simplification) { + for (auto& group : materialGroups) { + if (group.geometry.valid()) { + applySimplification(group.geometry.get(), lodSettings.simplify); + } + } + } + + // 构建GLTF模型(支持多材质) + std::vector glbData; + buildGLTFModelMultiMaterial( + materialGroups, + items, + lodSettings.enable_draco, + lodSettings.draco, + glbData + ); + + if (glbData.empty()) { + LOG_E("Failed to build GLTF model"); + return std::string(); + } + + // 构建BatchData并包装为B3DM + BatchData batchData = buildBatchData(items); + Options opts; + opts.alignTo8Bytes = true; + + std::string glbStr(glbData.begin(), glbData.end()); + return wrapGlbToB3dm(glbStr, batchData, opts); +} + +std::vector B3DMGenerator::generateLODFiles( + const spatial::core::SpatialItemRefList& items, + const std::string& outputDir, + const std::string& baseFilename, + const std::vector& lodLevels) { + + std::vector result; + + if (items.empty() || lodLevels.empty()) { + return result; + } + + // 创建输出目录 + std::filesystem::create_directories(outputDir); + + // 生成每个LOD级别 + for (size_t i = 0; i < lodLevels.size(); ++i) { + const auto& level = lodLevels[i]; + + // 生成B3DM数据 + std::string b3dmData = generate(items, level); + if (b3dmData.empty()) { + LOG_W("Failed to generate B3DM for LOD %zu", i); + continue; + } + + // 构建文件名 + std::string filename = generateFilename(static_cast(i)); + std::string filepath = outputDir + "/" + filename; + + // 写入文件 + std::ofstream file(filepath, std::ios::binary); + if (!file) { + LOG_E("Failed to open file for writing: %s", filepath.c_str()); + continue; + } + file.write(b3dmData.data(), b3dmData.size()); + file.close(); + + // 计算几何误差 + double geometricError = config_.simplifyParams.target_error; + if (i > 0 && !lodLevels.empty()) { + geometricError = lodLevels[0].target_error * (1.0 + i * 0.5); + } + + LODFileInfo info; + info.level = static_cast(i); + info.filename = filename; + info.relativePath = filename; + info.geometricError = geometricError; + result.push_back(info); + } + + return result; +} + +std::vector B3DMGenerator::generateLODFilesWithPath( + const spatial::core::SpatialItemRefList& items, + const std::string& outputRoot, + const std::string& tilePath, + const std::vector& lodLevels) { + + std::string fullPath = outputRoot + "/" + tilePath; + std::string baseName = std::filesystem::path(tilePath).filename().string(); + + auto files = generateLODFiles(items, fullPath, baseName, lodLevels); + + // 更新相对路径 + for (auto& file : files) { + file.relativePath = tilePath + "/" + file.filename; + } + + return files; +} + +// ===== 材质相关辅助函数实现 ===== + +std::string B3DMGenerator::computeMaterialKey( + const std::shared_ptr& matInfo) { + if (!matInfo) { + return "default"; + } + + std::ostringstream oss; + // 基础颜色 + for (const auto& c : matInfo->baseColor) { + oss << c << "_"; + } + // 粗糙度和金属度 + oss << matInfo->roughnessFactor << "_" << matInfo->metallicFactor << "_"; + // 纹理指针(作为唯一标识) + oss << matInfo->baseColorTexture.get() << "_" + << matInfo->normalTexture.get() << "_" + << matInfo->metallicRoughnessTexture.get() << "_" + << matInfo->emissiveTexture.get(); + + return oss.str(); +} + +// Helper function to write buffer data for stb_image_write +static void write_buffer(void* context, void* data, int len) { + std::vector* buf = static_cast*>(context); + buf->insert(buf->end(), static_cast(data), static_cast(data) + len); +} + +void B3DMGenerator::processAndAddTexture( + osg::Texture* texture, + const common::TextureTransformInfo& transform, + TextureType type, + tinygltf::Model& model, + tinygltf::Buffer& buffer, + int& textureIndexOut, + bool& hasAlphaOut) { + + textureIndexOut = -1; + hasAlphaOut = false; + + if (!texture) { + return; + } + + // 获取纹理图像 + osg::Image* image = texture->getImage(0); + if (!image) { + LOG_W("Texture has no image"); + return; + } + + int width = image->s(); + int height = image->t(); + GLenum pixelFormat = image->getPixelFormat(); + const unsigned char* sourceData = image->data(); + unsigned int rowStep = image->getRowStepInBytes(); + unsigned int rowSize = image->getRowSizeInBytes(); + bool hasRowPadding = (rowStep != rowSize); + + // 检查是否有Alpha通道 + hasAlphaOut = (pixelFormat == GL_RGBA || pixelFormat == GL_BGRA); + + std::vector encodedData; + std::string mimeType; + bool encodeSuccess = false; + + // 根据是否有Alpha通道选择编码格式 + if (hasAlphaOut) { + // 有Alpha通道,使用PNG编码 + std::vector rgbaData; + rgbaData.resize(width * height * 4); + + if (pixelFormat == GL_RGBA) { + if (hasRowPadding) { + for (int row = 0; row < height; row++) { + memcpy(&rgbaData[row * width * 4], + &sourceData[row * rowStep], + width * 4); + } + } else { + memcpy(rgbaData.data(), sourceData, width * height * 4); + } + } else if (pixelFormat == GL_BGRA) { + // Convert BGRA to RGBA + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + int srcIdx = row * rowStep + col * 4; + int dstIdx = (row * width + col) * 4; + rgbaData[dstIdx + 0] = sourceData[srcIdx + 2]; // R + rgbaData[dstIdx + 1] = sourceData[srcIdx + 1]; // G + rgbaData[dstIdx + 2] = sourceData[srcIdx + 0]; // B + rgbaData[dstIdx + 3] = sourceData[srcIdx + 3]; // A + } + } + } + + // 使用stb_image_write编码为PNG + encodeSuccess = stbi_write_png_to_func(write_buffer, &encodedData, + width, height, 4, rgbaData.data(), width * 4) != 0; + mimeType = "image/png"; + } else { + // 无Alpha通道,使用JPEG编码(更小的文件大小) + std::vector rgbData; + rgbData.resize(width * height * 3); + + if (pixelFormat == GL_RGB) { + if (hasRowPadding) { + for (int row = 0; row < height; row++) { + memcpy(&rgbData[row * width * 3], + &sourceData[row * rowStep], + width * 3); + } + } else { + memcpy(rgbData.data(), sourceData, width * height * 3); + } + } else if (pixelFormat == GL_RGBA || pixelFormat == GL_BGRA) { + // Extract RGB from RGBA/BGRA + int rOffset = (pixelFormat == GL_BGRA) ? 2 : 0; + int bOffset = (pixelFormat == GL_BGRA) ? 0 : 2; + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + int srcIdx = row * rowStep + col * 4; + int dstIdx = (row * width + col) * 3; + rgbData[dstIdx + 0] = sourceData[srcIdx + rOffset]; + rgbData[dstIdx + 1] = sourceData[srcIdx + 1]; + rgbData[dstIdx + 2] = sourceData[srcIdx + bOffset]; + } + } + } else if (pixelFormat == GL_BGR) { + // Convert BGR to RGB + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + int srcIdx = row * rowStep + col * 3; + int dstIdx = (row * width + col) * 3; + rgbData[dstIdx + 0] = sourceData[srcIdx + 2]; + rgbData[dstIdx + 1] = sourceData[srcIdx + 1]; + rgbData[dstIdx + 2] = sourceData[srcIdx + 0]; + } + } + } + + // 使用stb_image_write编码为JPEG,质量90 + encodeSuccess = stbi_write_jpg_to_func(write_buffer, &encodedData, + width, height, 3, rgbData.data(), 90) != 0; + mimeType = "image/jpeg"; + } + + if (!encodeSuccess || encodedData.empty()) { + LOG_W("Failed to encode texture image"); + return; + } + + // 创建buffer view + int bufferViewIndex = static_cast(model.bufferViews.size()); + int byteOffset = static_cast(buffer.data.size()); + + buffer.data.insert(buffer.data.end(), encodedData.begin(), encodedData.end()); + alignment_buffer(buffer.data); + + tinygltf::BufferView bfv; + bfv.buffer = 0; + bfv.byteOffset = byteOffset; + bfv.byteLength = static_cast(encodedData.size()); + model.bufferViews.push_back(bfv); + + // 创建image + int imageIndex = static_cast(model.images.size()); + tinygltf::Image img; + img.bufferView = bufferViewIndex; + img.mimeType = mimeType; + model.images.push_back(img); + + // 创建sampler + int samplerIndex = static_cast(model.samplers.size()); + tinygltf::Sampler sampler; + sampler.wrapS = TINYGLTF_TEXTURE_WRAP_REPEAT; + sampler.wrapT = TINYGLTF_TEXTURE_WRAP_REPEAT; + sampler.minFilter = TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR; + sampler.magFilter = TINYGLTF_TEXTURE_FILTER_LINEAR; + model.samplers.push_back(sampler); + + // 创建texture + textureIndexOut = static_cast(model.textures.size()); + tinygltf::Texture tex; + tex.sampler = samplerIndex; + tex.source = imageIndex; + model.textures.push_back(tex); +} + +int B3DMGenerator::buildMaterial( + const std::shared_ptr& matInfo, + tinygltf::Model& model, + tinygltf::Buffer& buffer) { + + if (!matInfo) { + return -1; + } + + // 计算材质键值,用于去重 + std::string materialKey = computeMaterialKey(matInfo); + + // 检查是否已有相同材质 + auto it = materialCache_.find(materialKey); + if (it != materialCache_.end()) { + return it->second; + } + + // 使用MaterialBuilder构建材质 + gltf::MaterialBuilder builder; + gltf::ExtensionManager extMgr; + + // 处理基础颜色纹理 + int baseColorTexIdx = -1; + bool hasAlpha = false; + if (matInfo->baseColorTexture) { + processAndAddTexture( + matInfo->baseColorTexture.get(), + matInfo->baseColorTransform, + TextureType::BASE_COLOR, + model, buffer, baseColorTexIdx, hasAlpha + ); + } + + // 设置基础PBR参数 + // 如果有纹理,baseColor应该设为白色(纹理提供颜色,baseColorFactor作为tint) + if (baseColorTexIdx >= 0) { + builder.setBaseColor({1.0, 1.0, 1.0, 1.0}); // 白色,不染色 + } else { + builder.setBaseColor(matInfo->baseColor); // 使用材质颜色 + } + builder.setPBRParams(matInfo->roughnessFactor, matInfo->metallicFactor); + builder.setDoubleSided(matInfo->doubleSided); + builder.setAlphaMode(matInfo->alphaMode); + builder.setAlphaCutoff(matInfo->alphaCutoff); + + // 设置基础颜色纹理 + if (baseColorTexIdx >= 0) { + builder.setBaseColorTexture(baseColorTexIdx); + // 应用纹理变换 + if (matInfo->baseColorTransform.hasTransform) { + gltf::extensions::TextureTransform transform; + transform.offset = {matInfo->baseColorTransform.offset[0], matInfo->baseColorTransform.offset[1]}; + transform.scale = {matInfo->baseColorTransform.scale[0], matInfo->baseColorTransform.scale[1]}; + transform.rotation = matInfo->baseColorTransform.rotation; + transform.tex_coord = matInfo->baseColorTransform.texCoord; + builder.setBaseColorTextureTransform(transform); + } + // 如果有Alpha通道,设置Alpha模式为BLEND + if (hasAlpha) { + builder.setAlphaMode("BLEND"); + } + } + + // 处理金属度粗糙度纹理 + int metallicRoughnessTexIdx = -1; + bool mrHasAlpha = false; + if (matInfo->metallicRoughnessTexture) { + processAndAddTexture( + matInfo->metallicRoughnessTexture.get(), + matInfo->metallicRoughnessTransform, + TextureType::METALLIC_ROUGHNESS, + model, buffer, metallicRoughnessTexIdx, mrHasAlpha + ); + if (metallicRoughnessTexIdx >= 0) { + builder.setMetallicRoughnessTexture(metallicRoughnessTexIdx); + if (matInfo->metallicRoughnessTransform.hasTransform) { + gltf::extensions::TextureTransform transform; + transform.offset = {matInfo->metallicRoughnessTransform.offset[0], matInfo->metallicRoughnessTransform.offset[1]}; + transform.scale = {matInfo->metallicRoughnessTransform.scale[0], matInfo->metallicRoughnessTransform.scale[1]}; + transform.rotation = matInfo->metallicRoughnessTransform.rotation; + transform.tex_coord = matInfo->metallicRoughnessTransform.texCoord; + builder.setMetallicRoughnessTextureTransform(transform); + } + } + } + + // 处理法线纹理 + int normalTexIdx = -1; + bool normalHasAlpha = false; + if (matInfo->normalTexture) { + processAndAddTexture( + matInfo->normalTexture.get(), + matInfo->normalTransform, + TextureType::NORMAL, + model, buffer, normalTexIdx, normalHasAlpha + ); + if (normalTexIdx >= 0) { + builder.setNormalTexture(normalTexIdx); + if (matInfo->normalTransform.hasTransform) { + gltf::extensions::TextureTransform transform; + transform.offset = {matInfo->normalTransform.offset[0], matInfo->normalTransform.offset[1]}; + transform.scale = {matInfo->normalTransform.scale[0], matInfo->normalTransform.scale[1]}; + transform.rotation = matInfo->normalTransform.rotation; + transform.tex_coord = matInfo->normalTransform.texCoord; + builder.setNormalTextureTransform(transform); + } + } + } + + // 处理环境光遮蔽纹理 + int occlusionTexIdx = -1; + bool occlusionHasAlpha = false; + if (matInfo->occlusionTexture) { + processAndAddTexture( + matInfo->occlusionTexture.get(), + matInfo->occlusionTransform, + TextureType::OCCLUSION, + model, buffer, occlusionTexIdx, occlusionHasAlpha + ); + if (occlusionTexIdx >= 0) { + builder.setOcclusionTexture(occlusionTexIdx); + builder.setOcclusionStrength(matInfo->aoStrength); + if (matInfo->occlusionTransform.hasTransform) { + gltf::extensions::TextureTransform transform; + transform.offset = {matInfo->occlusionTransform.offset[0], matInfo->occlusionTransform.offset[1]}; + transform.scale = {matInfo->occlusionTransform.scale[0], matInfo->occlusionTransform.scale[1]}; + transform.rotation = matInfo->occlusionTransform.rotation; + transform.tex_coord = matInfo->occlusionTransform.texCoord; + builder.setOcclusionTextureTransform(transform); + } + } + } + + // 处理自发光纹理和颜色 + int emissiveTexIdx = -1; + bool emissiveHasAlpha = false; + if (matInfo->emissiveTexture) { + processAndAddTexture( + matInfo->emissiveTexture.get(), + matInfo->emissiveTransform, + TextureType::EMISSIVE, + model, buffer, emissiveTexIdx, emissiveHasAlpha + ); + if (emissiveTexIdx >= 0) { + builder.setEmissiveTexture(emissiveTexIdx); + if (matInfo->emissiveTransform.hasTransform) { + gltf::extensions::TextureTransform transform; + transform.offset = {matInfo->emissiveTransform.offset[0], matInfo->emissiveTransform.offset[1]}; + transform.scale = {matInfo->emissiveTransform.scale[0], matInfo->emissiveTransform.scale[1]}; + transform.rotation = matInfo->emissiveTransform.rotation; + transform.tex_coord = matInfo->emissiveTransform.texCoord; + builder.setEmissiveTextureTransform(transform); + } + } + } + if (!matInfo->emissiveColor.empty()) { + builder.setEmissiveColor(matInfo->emissiveColor); + } + + // 处理Specular-Glossiness + if (matInfo->useSpecularGlossiness) { + gltf::extensions::SpecularGlossiness sg; + sg.diffuse_factor = {matInfo->diffuseFactor[0], matInfo->diffuseFactor[1], + matInfo->diffuseFactor[2], matInfo->diffuseFactor[3]}; + sg.specular_factor = {matInfo->specularFactor[0], matInfo->specularFactor[1], + matInfo->specularFactor[2]}; + sg.glossiness_factor = matInfo->glossinessFactor; + builder.setSpecularGlossiness(sg); + } + + // 构建材质 + int materialIndex = builder.build(model, extMgr); + + // 缓存材质索引 + materialCache_[materialKey] = materialIndex; + + return materialIndex; +} + +} // namespace b3dm diff --git a/src/b3dm/b3dm_generator.h b/src/b3dm/b3dm_generator.h new file mode 100644 index 00000000..9d79556b --- /dev/null +++ b/src/b3dm/b3dm_generator.h @@ -0,0 +1,209 @@ +#pragma once + +/** + * @file b3dm/b3dm_generator.h + * @brief 通用B3DM内容生成器 + * + * 该模块提供统一的B3DM生成功能,支持: + * - 几何体合并与转换 + * - LOD级别生成 + * - Draco压缩 + * - 纹理压缩(KTX2) + * - 批量属性处理 + * + * 被FBXPipeline和ShapefileProcessor复用 + */ + +#include "../common/geometry_extractor.h" +#include "b3dm_writer.h" +#include "../common/mesh_processor.h" +#include "../lod_pipeline.h" +#include +#include +#include +#include +#include +#include + +// Forward declaration for tinygltf +namespace tinygltf { + class Model; + class Buffer; +} + +namespace b3dm { + +/** + * @brief B3DM生成配置 + */ +struct B3DMGeneratorConfig { + // 坐标转换参数 + double centerLongitude = 0.0; + double centerLatitude = 0.0; + double centerHeight = 0.0; + + // 几何简化 + bool enableSimplification = false; + SimplificationParams simplifyParams; + + // Draco压缩 + bool enableDraco = false; + DracoCompressionParams dracoParams; + + // 纹理压缩 + bool enableTextureCompress = false; + + // 几何体提取器(由调用方提供) + common::IGeometryExtractor* geometryExtractor = nullptr; +}; + +/** + * @brief LOD文件信息 + */ +struct LODFileInfo { + int level; // LOD级别 + std::string filename; // 文件名 + std::string relativePath; // 相对路径 + double geometricError; // 几何误差 +}; + +/** + * @brief 通用B3DM内容生成器 + */ +class B3DMGenerator { +public: + explicit B3DMGenerator(const B3DMGeneratorConfig& config); + + /** + * @brief 生成单LOD级别的B3DM + * + * @param items 空间对象列表 + * @param lodSettings LOD级别设置 + * @return B3DM二进制数据,失败返回空字符串 + */ + std::string generate( + const spatial::core::SpatialItemRefList& items, + const LODLevelSettings& lodSettings + ); + + /** + * @brief 生成多LOD级别的B3DM文件 + * + * @param items 空间对象列表 + * @param outputDir 输出目录 + * @param baseFilename 基础文件名(不含扩展名) + * @param lodLevels LOD级别配置列表 + * @return 生成的文件信息列表 + */ + std::vector generateLODFiles( + const spatial::core::SpatialItemRefList& items, + const std::string& outputDir, + const std::string& baseFilename, + const std::vector& lodLevels + ); + + /** + * @brief 生成多LOD级别的B3DM文件(带坐标路径) + * + * @param items 空间对象列表 + * @param outputRoot 输出根目录 + * @param tilePath 瓦片路径(如 "tile/5/3/2") + * @param lodLevels LOD级别配置列表 + * @return 生成的文件信息列表 + */ + std::vector generateLODFilesWithPath( + const spatial::core::SpatialItemRefList& items, + const std::string& outputRoot, + const std::string& tilePath, + const std::vector& lodLevels + ); + +private: + B3DMGeneratorConfig config_; + + // 纹理类型枚举(用于材质处理) + enum class TextureType { + BASE_COLOR, + NORMAL, + EMISSIVE, + METALLIC_ROUGHNESS, + OCCLUSION + }; + + // 材质缓存(用于去重) + std::unordered_map materialCache_; + + // 按材质分组的几何体信息 + struct MaterialGroup { + std::shared_ptr materialInfo; + osg::ref_ptr geometry; + }; + + // 从空间对象提取并按材质分组合并几何体 + std::vector extractAndMergeGeometriesByMaterial( + const spatial::core::SpatialItemRefList& items + ); + + // 从空间对象提取并合并几何体(已废弃,使用 extractAndMergeGeometriesByMaterial) + osg::ref_ptr extractAndMergeGeometries( + const spatial::core::SpatialItemRefList& items + ); + + // 应用几何简化 + void applySimplification( + osg::Geometry* geometry, + const SimplificationParams& params + ); + + // 构建GLTF模型(支持材质) + void buildGLTFModel( + osg::Geometry* mergedGeom, + const spatial::core::SpatialItemRefList& items, + bool enableDraco, + const DracoCompressionParams& dracoParams, + std::vector& glbData, + const std::vector>& materials + ); + + // 构建GLTF模型(支持多材质) + void buildGLTFModelMultiMaterial( + const std::vector& materialGroups, + const spatial::core::SpatialItemRefList& items, + bool enableDraco, + const DracoCompressionParams& dracoParams, + std::vector& glbData + ); + + // 构建材质(新增) + int buildMaterial( + const std::shared_ptr& matInfo, + tinygltf::Model& model, + tinygltf::Buffer& buffer + ); + + // 处理并添加纹理(新增) + void processAndAddTexture( + osg::Texture* texture, + const common::TextureTransformInfo& transform, + TextureType type, + tinygltf::Model& model, + tinygltf::Buffer& buffer, + int& textureIndexOut, + bool& hasAlphaOut + ); + + // 计算材质哈希键(用于去重) + std::string computeMaterialKey( + const std::shared_ptr& matInfo + ); + + // 构建BatchData + BatchData buildBatchData( + const spatial::core::SpatialItemRefList& items + ); + + // 生成文件名 + std::string generateFilename(int lodLevel) const; +}; + +} // namespace b3dm diff --git a/src/b3dm/b3dm_writer.cpp b/src/b3dm/b3dm_writer.cpp new file mode 100644 index 00000000..7dc3855c --- /dev/null +++ b/src/b3dm/b3dm_writer.cpp @@ -0,0 +1,200 @@ +#include "b3dm_writer.h" + +#include +#include +#include + +namespace b3dm { + +void padString(std::string& str, size_t alignment) { + if (alignment == 0) return; + size_t remainder = str.size() % alignment; + if (remainder != 0) { + str.append(alignment - remainder, ' '); + } +} + +size_t calculateB3dmSize( + size_t glbSize, + size_t featureTableSize, + size_t batchTableSize, + bool alignTo8Bytes +) { + size_t alignment = alignTo8Bytes ? 8 : 4; + + // 计算对齐后的尺寸 + size_t alignedFeatureTableSize = featureTableSize; + size_t remainder = alignedFeatureTableSize % alignment; + if (remainder != 0) { + alignedFeatureTableSize += alignment - remainder; + } + + size_t alignedBatchTableSize = batchTableSize; + remainder = alignedBatchTableSize % alignment; + if (remainder != 0) { + alignedBatchTableSize += alignment - remainder; + } + + return B3DM_HEADER_SIZE + alignedFeatureTableSize + alignedBatchTableSize + glbSize; +} + +bool validateHeader(const Header& header) { + if (header.magic != B3DM_MAGIC) { + return false; + } + if (header.version != B3DM_VERSION) { + return false; + } + if (header.byteLength < B3DM_HEADER_SIZE) { + return false; + } + return true; +} + +std::string wrapGlbToB3dm( + const std::string& glbBuffer, + const BatchData& batchData, + const Options& options +) { + using nlohmann::json; + + // 1. 构造 Feature Table + json featureTable; + size_t batchLength = batchData.empty() ? 0 : batchData.size(); + + // 如果 batch 为空且不允许空 batch,则返回空字符串表示错误 + if (batchLength == 0 && !options.allowEmptyBatch) { + return ""; + } + + featureTable["BATCH_LENGTH"] = batchLength; + + // 合并额外的 Feature Table 字段 + if (!options.extraFeatureTable.empty()) { + for (auto& [key, value] : options.extraFeatureTable.items()) { + featureTable[key] = value; + } + } + + std::string featureTableStr = featureTable.dump(); + + // 2. 构造 Batch Table + json batchTable; + if (batchLength > 0) { + batchTable["batchId"] = batchData.batchIds; + + if (!batchData.names.empty()) { + batchTable["name"] = batchData.names; + } + + // 添加动态属性 + for (const auto& [key, values] : batchData.attributes) { + batchTable[key] = values; + } + } + + // 合并额外的 Batch Table 字段 + if (!options.extraBatchTable.empty()) { + for (auto& [key, value] : options.extraBatchTable.items()) { + batchTable[key] = value; + } + } + + std::string batchTableStr = batchTable.empty() ? "" : batchTable.dump(); + + // 3. 对齐处理 + size_t alignment = options.alignTo8Bytes ? 8 : 4; + padString(featureTableStr, alignment); + if (!batchTableStr.empty()) { + padString(batchTableStr, alignment); + } + + // 4. 构造 Header + Header header; + header.magic = B3DM_MAGIC; + header.version = B3DM_VERSION; + header.featureTableJSONByteLength = static_cast(featureTableStr.size()); + header.featureTableBinaryByteLength = 0; + header.batchTableJSONByteLength = static_cast(batchTableStr.size()); + header.batchTableBinaryByteLength = 0; + header.byteLength = static_cast(B3DM_HEADER_SIZE + + featureTableStr.size() + + batchTableStr.size() + + glbBuffer.size()); + + // 5. 组装 Buffer + std::string result; + result.reserve(header.byteLength); + + // 写入 Header + result.append(reinterpret_cast(&header), sizeof(Header)); + + // 写入 Feature Table JSON + result.append(featureTableStr); + + // 写入 Batch Table JSON(如果有) + if (!batchTableStr.empty()) { + result.append(batchTableStr); + } + + // 写入 GLB 数据 + result.append(glbBuffer); + + return result; +} + +std::string wrapGlbToB3dmSimple( + const std::string& glbBuffer, + size_t batchLength, + const Options& options +) { + BatchData batchData; + + if (batchLength > 0) { + batchData.batchIds.reserve(batchLength); + batchData.names.reserve(batchLength); + + for (size_t i = 0; i < batchLength; ++i) { + batchData.batchIds.push_back(static_cast(i)); + batchData.names.push_back("mesh_" + std::to_string(i)); + } + } + + return wrapGlbToB3dm(glbBuffer, batchData, options); +} + +bool writeB3dmToFile( + const std::string& filePath, + const std::string& b3dmData +) { + if (b3dmData.empty()) { + return false; + } + + std::ofstream outfile(filePath, std::ios::binary); + if (!outfile) { + return false; + } + + outfile.write(b3dmData.data(), static_cast(b3dmData.size())); + bool success = outfile.good(); + outfile.close(); + + return success; +} + +bool writeGlbAsB3dm( + const std::string& filePath, + const std::string& glbBuffer, + const BatchData& batchData, + const Options& options +) { + std::string b3dmData = wrapGlbToB3dm(glbBuffer, batchData, options); + if (b3dmData.empty()) { + return false; + } + + return writeB3dmToFile(filePath, b3dmData); +} + +} // namespace b3dm diff --git a/src/b3dm/b3dm_writer.h b/src/b3dm/b3dm_writer.h new file mode 100644 index 00000000..e87e3d7d --- /dev/null +++ b/src/b3dm/b3dm_writer.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace b3dm { + +// B3DM 文件魔数和版本 +constexpr uint32_t B3DM_MAGIC = 0x6D643362; // 'b3dm' +constexpr uint32_t B3DM_VERSION = 1; +constexpr size_t B3DM_HEADER_SIZE = 28; + +// B3DM Header 结构(与 3D Tiles 规范一致) +#pragma pack(push, 1) +struct Header { + uint32_t magic; // 'b3dm' = 0x6D643362 + uint32_t version; // 1 + uint32_t byteLength; // 整个文件的字节长度 + uint32_t featureTableJSONByteLength; // Feature Table JSON 长度 + uint32_t featureTableBinaryByteLength; // Feature Table Binary 长度(通常为 0) + uint32_t batchTableJSONByteLength; // Batch Table JSON 长度 + uint32_t batchTableBinaryByteLength; // Batch Table Binary 长度(通常为 0) +}; +#pragma pack(pop) + +// Batch 数据定义 +struct BatchData { + std::vector batchIds; // 必需:batch ID 数组 + std::vector names; // 可选:名称数组 + std::map> attributes; // 动态属性 + + // 便捷方法:检查是否为空 + bool empty() const { return batchIds.empty(); } + + // 便捷方法:获取 batch 数量 + size_t size() const { return batchIds.size(); } +}; + +// B3DM 构建选项 +struct Options { + bool alignTo8Bytes = true; // 是否使用 8 字节对齐(推荐,符合规范) + bool allowEmptyBatch = false; // 是否允许 BATCH_LENGTH=0 + nlohmann::json extraFeatureTable; // 额外的 Feature Table 字段(如 RTC_CENTER) + nlohmann::json extraBatchTable; // 额外的 Batch Table 字段 +}; + +// ============================================ +// 核心函数:GLB Buffer → B3DM Buffer +// ============================================ + +/** + * @brief 将 GLB buffer 包装为 B3DM 格式 + * + * @param glbBuffer 输入的 GLB 二进制数据 + * @param batchData Batch 数据(batchIds, names, attributes) + * @param options 构建选项 + * @return std::string B3DM 格式的二进制数据 + */ +std::string wrapGlbToB3dm( + const std::string& glbBuffer, + const BatchData& batchData, + const Options& options = {} +); + +/** + * @brief 将 GLB buffer 包装为 B3DM 格式(简化版,自动构造 BatchData) + * + * @param glbBuffer 输入的 GLB 二进制数据 + * @param batchLength BATCH_LENGTH 值 + * @param options 构建选项 + * @return std::string B3DM 格式的二进制数据 + */ +std::string wrapGlbToB3dmSimple( + const std::string& glbBuffer, + size_t batchLength, + const Options& options = {} +); + +// ============================================ +// 文件操作函数 +// ============================================ + +/** + * @brief 将 B3DM 数据写入文件 + * + * @param filePath 输出文件路径 + * @param b3dmData B3DM 二进制数据 + * @return true 写入成功 + * @return false 写入失败 + */ +bool writeB3dmToFile( + const std::string& filePath, + const std::string& b3dmData +); + +/** + * @brief 直接将 GLB 转换为 B3DM 并写入文件 + * + * @param filePath 输出文件路径 + * @param glbBuffer 输入的 GLB 二进制数据 + * @param batchData Batch 数据 + * @param options 构建选项 + * @return true 写入成功 + * @return false 写入失败 + */ +bool writeGlbAsB3dm( + const std::string& filePath, + const std::string& glbBuffer, + const BatchData& batchData, + const Options& options = {} +); + +// ============================================ +// 工具函数 +// ============================================ + +/** + * @brief 计算 B3DM 总大小(用于预分配内存) + * + * @param glbSize GLB 数据大小 + * @param featureTableSize Feature Table JSON 大小 + * @param batchTableSize Batch Table JSON 大小 + * @param alignTo8Bytes 是否使用 8 字节对齐 + * @return size_t B3DM 总大小 + */ +size_t calculateB3dmSize( + size_t glbSize, + size_t featureTableSize, + size_t batchTableSize, + bool alignTo8Bytes = true +); + +/** + * @brief 填充字符串到指定对齐边界 + * + * @param str 要填充的字符串(会被修改) + * @param alignment 对齐字节数(通常为 4 或 8) + */ +void padString(std::string& str, size_t alignment); + +/** + * @brief 验证 B3DM Header 的有效性 + * + * @param header Header 结构体 + * @return true 有效 + * @return false 无效 + */ +bool validateHeader(const Header& header); + +} // namespace b3dm diff --git a/src/common/geometry_extractor.h b/src/common/geometry_extractor.h new file mode 100644 index 00000000..ff4f912c --- /dev/null +++ b/src/common/geometry_extractor.h @@ -0,0 +1,199 @@ +#pragma once + +/** + * @file common/geometry_extractor.h + * @brief 几何体提取器接口 + * + * 该接口抽象不同数据源(FBX/Shapefile)的几何体和材质提取逻辑, + * 供B3DM生成器统一使用。 + */ + +#include "../spatial/core/spatial_item.h" +#include +#include +#include +#include +#include +#include +#include + +namespace common { + +/** + * @brief 纹理变换信息 + * + * 对应GLTF KHR_texture_transform扩展的参数 + * 用于描述纹理坐标的变换(偏移、缩放、旋转) + */ +struct TextureTransformInfo { + float offset[2] = {0.0f, 0.0f}; // UV偏移 [u, v] + float scale[2] = {1.0f, 1.0f}; // UV缩放 [u, v] + float rotation = 0.0f; // 旋转角度(弧度) + int texCoord = 0; // 纹理坐标集索引 + bool hasTransform = false; // 标记是否有变换 + + /** + * @brief 创建默认变换(无变换) + */ + static TextureTransformInfo Identity() { + return {}; + } + + /** + * @brief 创建带偏移的变换 + */ + static TextureTransformInfo WithOffset(float u, float v) { + TextureTransformInfo t; + t.offset[0] = u; + t.offset[1] = v; + t.hasTransform = true; + return t; + } + + /** + * @brief 创建带缩放的变换 + */ + static TextureTransformInfo WithScale(float u, float v) { + TextureTransformInfo t; + t.scale[0] = u; + t.scale[1] = v; + t.hasTransform = true; + return t; + } +}; + +/** + * @brief 完整的材质信息 + * + * 包含PBR材质的所有参数,支持: + * - Metallic-Roughness工作流 + * - Specular-Glossiness工作流(传统FBX材质) + * - 纹理变换 + * - 各种GLTF材质扩展 + */ +struct MaterialInfo { + // ==================== 基础PBR参数 ==================== + + /** + * @brief 基础颜色 + * 线性颜色空间,RGBA格式,默认: [1.0, 1.0, 1.0, 1.0] + */ + std::vector baseColor = {1.0, 1.0, 1.0, 1.0}; + + /** + * @brief 粗糙度 [0.0, 1.0],默认: 1.0 + */ + float roughnessFactor = 1.0f; + + /** + * @brief 金属度 [0.0, 1.0],默认: 0.0 + */ + float metallicFactor = 0.0f; + + /** + * @brief 自发光颜色 [r,g,b],默认: [0.0, 0.0, 0.0] + */ + std::vector emissiveColor = {0.0, 0.0, 0.0}; + + /** + * @brief 遮挡强度 [0.0, 1.0],默认: 1.0 + */ + float aoStrength = 1.0f; + + // ==================== 纹理对象 ==================== + + osg::ref_ptr baseColorTexture; // 基础颜色纹理(纹理单元0) + osg::ref_ptr normalTexture; // 法线纹理(纹理单元1) + osg::ref_ptr metallicRoughnessTexture; // 金属度/粗糙度纹理(纹理单元2) + osg::ref_ptr occlusionTexture; // 遮挡纹理(纹理单元3) + osg::ref_ptr emissiveTexture; // 自发光纹理(纹理单元4) + + // ==================== 纹理变换 ==================== + + TextureTransformInfo baseColorTransform; + TextureTransformInfo normalTransform; + TextureTransformInfo metallicRoughnessTransform; + TextureTransformInfo occlusionTransform; + TextureTransformInfo emissiveTransform; + + // ==================== Specular-Glossiness ==================== + + bool useSpecularGlossiness = false; // 是否使用Specular-Glossiness + std::vector diffuseFactor = {1.0, 1.0, 1.0, 1.0}; // 漫反射因子 + std::vector specularFactor = {1.0, 1.0, 1.0}; // 高光因子 + double glossinessFactor = 1.0; // 光泽度 + osg::ref_ptr specularGlossinessTexture; // Specular-Glossiness纹理 + osg::ref_ptr diffuseTexture; // 漫反射纹理 + + // ==================== 其他属性 ==================== + + bool doubleSided = true; // 双面渲染 + std::string alphaMode = "OPAQUE"; // Alpha模式 + float alphaCutoff = 0.5f; // Alpha裁剪值 + + // ==================== 辅助方法 ==================== + + /** + * @brief 检查是否有任何纹理 + */ + bool hasAnyTexture() const { + return baseColorTexture || normalTexture || metallicRoughnessTexture || + occlusionTexture || emissiveTexture || specularGlossinessTexture || + diffuseTexture; + } + + /** + * @brief 检查是否有纹理变换 + */ + bool hasAnyTextureTransform() const { + return baseColorTransform.hasTransform || normalTransform.hasTransform || + metallicRoughnessTransform.hasTransform || occlusionTransform.hasTransform || + emissiveTransform.hasTransform; + } +}; + +/** + * @brief 几何体提取器接口 + * + * 不同数据源(FBX/Shapefile)实现此接口以提供几何体和材质 + */ +class IGeometryExtractor { +public: + virtual ~IGeometryExtractor() = default; + + /** + * @brief 从空间对象提取几何体 + * @param item 空间对象 + * @return 几何体列表 + */ + virtual std::vector> extract( + const spatial::core::SpatialItem* item) = 0; + + /** + * @brief 获取对象的唯一标识(用于BatchID) + */ + virtual std::string getId(const spatial::core::SpatialItem* item) = 0; + + /** + * @brief 获取对象的属性(用于BatchTable) + */ + virtual std::map getAttributes( + const spatial::core::SpatialItem* item) = 0; + + /** + * @brief 获取对象的材质信息 + * + * 提取空间对象的完整材质信息,包括: + * - PBR参数 + * - 纹理对象 + * - 纹理变换 + * - 扩展数据 + * + * @param item 空间对象 + * @return 材质信息,如果没有材质返回nullptr或默认材质 + */ + virtual std::shared_ptr getMaterial( + const spatial::core::SpatialItem* item) = 0; +}; + +} // namespace common diff --git a/src/mesh_processor.cpp b/src/common/mesh_processor.cpp similarity index 100% rename from src/mesh_processor.cpp rename to src/common/mesh_processor.cpp diff --git a/src/mesh_processor.h b/src/common/mesh_processor.h similarity index 100% rename from src/mesh_processor.h rename to src/common/mesh_processor.h diff --git a/src/common/tile_meta.h b/src/common/tile_meta.h new file mode 100644 index 00000000..2cb1dd29 --- /dev/null +++ b/src/common/tile_meta.h @@ -0,0 +1,117 @@ +#pragma once + +/** + * @file common/tile_meta.h + * @brief 通用瓦片元数据基类 + * + * 为不同数据源(Shapefile/FBX)提供统一的瓦片元数据接口 + */ + +#include "tile_path_utils.h" +#include +#include +#include +#include +#include + +namespace common { + +/** + * @brief 通用包围盒结构 + * + * 使用双精度浮点数表示3D空间中的包围盒 + */ +struct BoundingBox { + double minX = 0.0; + double maxX = 0.0; + double minY = 0.0; + double maxY = 0.0; + double minZ = 0.0; + double maxZ = 0.0; + + BoundingBox() = default; + BoundingBox(double minx, double maxx, double miny, double maxy, + double minz, double maxz) + : minX(minx), maxX(maxx), minY(miny), maxY(maxy), + minZ(minz), maxZ(maxz) {} + + bool isValid() const { + return minX < maxX && minY < maxY && minZ <= maxZ; + } + + double centerX() const { return (minX + maxX) * 0.5; } + double centerY() const { return (minY + maxY) * 0.5; } + double centerZ() const { return (minZ + maxZ) * 0.5; } + double width() const { return maxX - minX; } + double height() const { return maxY - minY; } + double depth() const { return maxZ - minZ; } +}; + +/** + * @brief 瓦片内容信息 + */ +struct TileContent { + std::string uri; // 内容文件URI(相对路径) + std::string b3dmPath; // B3DM文件路径 + bool hasContent = false; // 是否有内容 + + TileContent() = default; + explicit TileContent(const std::string& u) : uri(u), hasContent(true) {} +}; + +/** + * @brief 通用瓦片元数据基类 + * + * 所有数据源的瓦片元数据都应继承此类 + */ +class TileMeta { +public: + TileCoord coord; // 瓦片坐标 + BoundingBox bbox; // 包围盒 + double geometricError = 0.0; // 几何误差 + TileContent content; // 内容信息 + bool isLeaf = false; // 是否为叶子节点 + std::vector childrenKeys; // 子节点键值 + + TileMeta() = default; + explicit TileMeta(const TileCoord& c) : coord(c) {} + + virtual ~TileMeta() = default; + + // 获取唯一键值 + uint64_t key() const { return coord.encode(); } + + // 获取父节点键值 + uint64_t parentKey() const { + if (coord.z <= 0) return 0; + return TileCoord(coord.z - 1, coord.x / 2, coord.y / 2).encode(); + } + + // 判断是否为根节点 + bool isRoot() const { return coord.z == 0; } + + // 获取层级 + int getLevel() const { return coord.z; } + + // 获取瓦片路径(用于tileset.json中的uri) + virtual std::string getTilesetPath() const { + return TilePathUtils::getTilesetPath(coord); + } + + // 获取内容路径 + virtual std::string getContentPath(int lodLevel = 0) const { + return TilePathUtils::getContentPath(coord, lodLevel); + } +}; + +/** + * @brief 瓦片元数据指针类型 + */ +using TileMetaPtr = std::shared_ptr; + +/** + * @brief 瓦片元数据映射表 + */ +using TileMetaMap = std::unordered_map; + +} // namespace common diff --git a/src/common/tile_path_utils.cpp b/src/common/tile_path_utils.cpp new file mode 100644 index 00000000..a4cea85e --- /dev/null +++ b/src/common/tile_path_utils.cpp @@ -0,0 +1,72 @@ +#include "tile_path_utils.h" +#include + +namespace common { + +TileCoord OctreeCoord::toTileCoord() const { + // 八叉树映射到瓦片坐标 + // x = 路径编码, y = 深度偏移 + int x = 0; + for (int node : path) { + x = x * 8 + node; + } + x = x * 8 + index; + int y = depth > 1 ? depth - 2 : 0; + return TileCoord(depth, x, y); +} + +std::string TilePathUtils::getTilesetPath(const TileCoord& coord) { + if (coord.z == 0) { + return "tileset.json"; + } + std::filesystem::path p = "tile"; + p /= std::to_string(coord.z); + p /= std::to_string(coord.x); + p /= std::to_string(coord.y); + p /= "tileset.json"; + return p.generic_string(); +} + +std::string TilePathUtils::getContentPath(const TileCoord& coord, int lodLevel) { + std::filesystem::path p = "tile"; + p /= std::to_string(coord.z); + p /= std::to_string(coord.x); + p /= std::to_string(coord.y); + + if (lodLevel == 0) { + p /= "content.b3dm"; + } else { + p /= "content_lod" + std::to_string(lodLevel) + ".b3dm"; + } + return p.generic_string(); +} + +std::string TilePathUtils::getTileDirectory(const TileCoord& coord) { + std::filesystem::path p = "tile"; + p /= std::to_string(coord.z); + p /= std::to_string(coord.x); + p /= std::to_string(coord.y); + return p.generic_string(); +} + +bool TilePathUtils::createTileDirectory(const std::string& outputRoot, const TileCoord& coord) { + std::filesystem::path p = outputRoot; + p /= getTileDirectory(coord); + + try { + std::filesystem::create_directories(p); + return true; + } catch (const std::exception& e) { + return false; + } +} + +std::string TilePathUtils::getTilesetPath(const OctreeCoord& coord) { + return getTilesetPath(coord.toTileCoord()); +} + +std::string TilePathUtils::getContentPath(const OctreeCoord& coord, int lodLevel) { + return getContentPath(coord.toTileCoord(), lodLevel); +} + +} // namespace pipeline diff --git a/src/common/tile_path_utils.h b/src/common/tile_path_utils.h new file mode 100644 index 00000000..b6a641fc --- /dev/null +++ b/src/common/tile_path_utils.h @@ -0,0 +1,120 @@ +#pragma once + +/** + * @file common/tile_path_utils.h + * @brief 瓦片路径工具 + * + * 统一输出目录结构 tile/{z}/{x}/{y}/ + */ + +#include +#include +#include + +namespace common { + +/** + * @brief 瓦片坐标结构 + */ +struct TileCoord { + int z = 0; // 层级 + int x = 0; // X坐标 + int y = 0; // Y坐标 + + TileCoord() = default; + TileCoord(int level, int x_coord, int y_coord) + : z(level), x(x_coord), y(y_coord) {} + + /** + * @brief 编码为64位整数(用于哈希表键值) + */ + uint64_t encode() const { + return (static_cast(z) << 42) | + (static_cast(x) << 21) | + static_cast(y); + } + + /** + * @brief 从编码解码 + */ + static TileCoord decode(uint64_t key) { + TileCoord coord; + coord.z = static_cast((key >> 42) & 0x1FFFFF); + coord.x = static_cast((key >> 21) & 0x1FFFFF); + coord.y = static_cast(key & 0x1FFFFF); + return coord; + } + + bool operator==(const TileCoord& other) const { + return z == other.z && x == other.x && y == other.y; + } + + bool operator!=(const TileCoord& other) const { + return !(*this == other); + } +}; + +/** + * @brief 八叉树坐标(FBX使用) + */ +struct OctreeCoord { + int depth = 0; // 深度 + int index = 0; // 当前节点索引 (0-7) + std::vector path; // 从根到当前节点的路径 + + OctreeCoord() = default; + OctreeCoord(int d, int idx, std::vector p) + : depth(d), index(idx), path(std::move(p)) {} + + /** + * @brief 编码为64位整数(用于哈希表键值) + */ + uint64_t encode() const { + uint64_t key = 0; + for (int p : path) { + key = (key << 3) | (p & 0x7); + } + key = (key << 3) | (index & 0x7); + key = (key << 8) | (depth & 0xFF); + return key; + } + + /** + * @brief 转换为标准瓦片坐标 + */ + TileCoord toTileCoord() const; +}; + +/** + * @brief 路径生成工具 + */ +class TilePathUtils { +public: + /** + * @brief 获取tileset.json路径 + */ + static std::string getTilesetPath(const TileCoord& coord); + + /** + * @brief 获取B3DM内容路径 + */ + static std::string getContentPath(const TileCoord& coord, int lodLevel = 0); + + /** + * @brief 获取瓦片目录路径 + */ + static std::string getTileDirectory(const TileCoord& coord); + + /** + * @brief 创建瓦片目录 + */ + static bool createTileDirectory(const std::string& outputRoot, const TileCoord& coord); + + /** + * @brief 从八叉树坐标获取路径 + */ + static std::string getTilesetPath(const OctreeCoord& coord); + static std::string getContentPath(const OctreeCoord& coord, int lodLevel = 0); +}; + +} // namespace common diff --git a/src/common/tileset_builder.cpp b/src/common/tileset_builder.cpp new file mode 100644 index 00000000..ea41b393 --- /dev/null +++ b/src/common/tileset_builder.cpp @@ -0,0 +1,271 @@ +/** + * @file common/tileset_builder.cpp + * @brief 通用Tileset构建器实现 + */ + +#include "tileset_builder.h" +#include "../coords/geo_math.h" +#include +#include + +namespace common { + +// 使用 coords 命名空间的数学函数 +using coords::degree2rad; +using coords::lati_to_meter; +using coords::longti_to_meter; + +// ============================================================================ +// TilesetBuilder 基类实现 +// ============================================================================ + +TilesetBuilder::TilesetBuilder(const TilesetBuilderConfig& config) + : config_(config) {} + +tileset::Tileset TilesetBuilder::buildTileset( + const TileMetaPtr& rootMeta, + const TileMetaMap& allMetas, + const BoundingBoxConverter& bboxConverter, + const GeometricErrorCalculator& geCalculator) const { + + tileset::Tileset tileset; + tileset.asset.version = config_.version; + tileset.asset.gltfUpAxis = config_.gltfUpAxis; + + // 构建根节点 + tileset.root = buildTile(rootMeta, allMetas, bboxConverter, geCalculator); + + // 设置根几何误差 + tileset.geometricError = tileset.root.geometricError * config_.rootGeometricErrorMultiplier; + + return tileset; +} + +tileset::Tile TilesetBuilder::buildTile( + const TileMetaPtr& meta, + const TileMetaMap& allMetas, + const BoundingBoxConverter& bboxConverter, + const GeometricErrorCalculator& geCalculator) const { + + tileset::Tile tile; + + // 设置包围体 + tile.boundingVolume = bboxConverter(meta->bbox); + + // 设置几何误差 + tile.geometricError = geCalculator(meta->bbox); + if (meta->geometricError > 0.0) { + tile.geometricError = meta->geometricError; + } + + // 设置细化策略 + tile.refine = config_.refine; + + // 设置内容 + if (meta->content.hasContent) { + tile.content = createContent(meta); + } + + // 递归构建子节点 + if (!meta->isLeaf && !meta->childrenKeys.empty()) { + buildChildren(tile, meta, allMetas, bboxConverter, geCalculator); + } + + return tile; +} + +void TilesetBuilder::buildChildren( + tileset::Tile& parentTile, + const TileMetaPtr& parentMeta, + const TileMetaMap& allMetas, + const BoundingBoxConverter& bboxConverter, + const GeometricErrorCalculator& geCalculator) const { + + for (uint64_t childKey : parentMeta->childrenKeys) { + auto it = allMetas.find(childKey); + if (it != allMetas.end()) { + tileset::Tile childTile = buildTile( + it->second, allMetas, bboxConverter, geCalculator); + parentTile.addChild(std::move(childTile)); + } + } +} + +tileset::Content TilesetBuilder::createContent(const TileMetaPtr& meta) const { + tileset::Content content; + content.uri = meta->content.uri; + return content; +} + +tileset::TransformMatrix TilesetBuilder::createRootTransform( + double centerLon, double centerLat, double centerHeight) { + + // ENU到ECEF的变换矩阵 + // 基于WGS84坐标系 + double lon = degree2rad(centerLon); + double lat = degree2rad(centerLat); + + double cosLon = std::cos(lon); + double sinLon = std::sin(lon); + double cosLat = std::cos(lat); + double sinLat = std::sin(lat); + + // 构建旋转矩阵(ENU到ECEF) + tileset::TransformMatrix matrix; + // 第一列: -sin(lon), cos(lon), 0 + matrix[0] = -sinLon; + matrix[1] = cosLon; + matrix[2] = 0.0; + matrix[3] = 0.0; + + // 第二列: -sin(lat)*cos(lon), -sin(lat)*sin(lon), cos(lat) + matrix[4] = -sinLat * cosLon; + matrix[5] = -sinLat * sinLon; + matrix[6] = cosLat; + matrix[7] = 0.0; + + // 第三列: cos(lat)*cos(lon), cos(lat)*sin(lon), sin(lat) + matrix[8] = cosLat * cosLon; + matrix[9] = cosLat * sinLon; + matrix[10] = sinLat; + matrix[11] = 0.0; + + // 第四列: 平移(ECEF坐标) + // 简化为ENU原点在ECEF中的位置 + matrix[12] = 0.0; + matrix[13] = 0.0; + matrix[14] = centerHeight; + matrix[15] = 1.0; + + return matrix; +} + +// ============================================================================ +// QuadtreeTilesetBuilder 四叉树构建器实现 +// ============================================================================ + +QuadtreeTilesetBuilder::QuadtreeTilesetBuilder(double globalCenterLon, + double globalCenterLat, + const TilesetBuilderConfig& config) + : TilesetBuilder(config) + , globalCenterLon_(globalCenterLon) + , globalCenterLat_(globalCenterLat) {} + +tileset::Tileset QuadtreeTilesetBuilder::buildTileset( + const TileMetaPtr& rootMeta, + const TileMetaMap& allMetas) const { + + // 创建转换函数 + auto bboxConverter = [this](const BoundingBox& bbox) { + return this->convertBoundingBox(bbox); + }; + + auto geCalculator = [this](const BoundingBox& bbox) { + return this->computeGeometricError(bbox); + }; + + return TilesetBuilder::buildTileset(rootMeta, allMetas, bboxConverter, geCalculator); +} + +tileset::Box QuadtreeTilesetBuilder::convertBoundingBox(const BoundingBox& bbox) const { + // 假设输入bbox是WGS84经纬度(度) + // 需要转换为ENU坐标系(米) + + // 计算中心点经纬度 + double centerLon = bbox.centerX(); + double centerLat = bbox.centerY(); + + // 计算经纬度跨度(弧度) + double lonRadSpan = degree2rad(bbox.width()); + double latRadSpan = degree2rad(bbox.height()); + + // 转换为米(ENU坐标系) + double halfW = longti_to_meter(lonRadSpan * 0.5, degree2rad(centerLat)) * + config_.boundingVolumeScale; + double halfH = lati_to_meter(latRadSpan * 0.5) * + config_.boundingVolumeScale; + double halfZ = bbox.depth() * 0.5 * config_.boundingVolumeScale; + + // 计算相对于全局中心的ENU偏移 + double offsetX, offsetY; + computeEnuOffset(centerLon, centerLat, offsetX, offsetY); + + // 计算Z轴中心点 + double centerZ = bbox.centerZ(); + + // 创建Box(中心点 + 半轴长度) + return tileset::Box::fromCenterAndHalfLengths( + offsetX, offsetY, centerZ, halfW, halfH, halfZ); +} + +double QuadtreeTilesetBuilder::computeGeometricError(const BoundingBox& bbox) const { + // 基于包围盒对角线计算几何误差 + // 将经纬度跨度转换为米(近似) + double centerLat = bbox.centerY(); + double meterX = longti_to_meter(degree2rad(bbox.width()), degree2rad(centerLat)); + double meterY = lati_to_meter(degree2rad(bbox.height())); + double meterZ = bbox.depth(); + + double maxSpan = std::max({meterX, meterY, meterZ}); + if (maxSpan <= 0.0) { + return 1.0; // 最小几何误差 + } + + return maxSpan * config_.childGeometricErrorMultiplier; +} + +void QuadtreeTilesetBuilder::computeEnuOffset(double lon, double lat, + double& offsetX, double& offsetY) const { + // 计算相对于全局中心的ENU偏移(米) + offsetX = longti_to_meter(degree2rad(lon - globalCenterLon_), degree2rad(globalCenterLat_)); + offsetY = lati_to_meter(degree2rad(lat - globalCenterLat_)); +} + +// ============================================================================ +// OctreeTilesetBuilder 八叉树构建器实现 +// ============================================================================ + +OctreeTilesetBuilder::OctreeTilesetBuilder(const TilesetBuilderConfig& config) + : TilesetBuilder(config) {} + +tileset::Tileset OctreeTilesetBuilder::buildTileset( + const TileMetaPtr& rootMeta, + const TileMetaMap& allMetas) const { + + // 创建转换函数 + auto bboxConverter = [this](const BoundingBox& bbox) { + return this->convertBoundingBox(bbox); + }; + + auto geCalculator = [this](const BoundingBox& bbox) { + return this->computeGeometricError(bbox); + }; + + return TilesetBuilder::buildTileset(rootMeta, allMetas, bboxConverter, geCalculator); +} + +tileset::Box OctreeTilesetBuilder::convertBoundingBox(const BoundingBox& bbox) const { + // FBX使用本地坐标系,直接转换为Box + double centerX = bbox.centerX(); + double centerY = bbox.centerY(); + double centerZ = bbox.centerZ(); + + double halfW = bbox.width() * 0.5 * config_.boundingVolumeScale; + double halfH = bbox.height() * 0.5 * config_.boundingVolumeScale; + double halfD = bbox.depth() * 0.5 * config_.boundingVolumeScale; + + return tileset::Box::fromCenterAndHalfLengths( + centerX, centerY, centerZ, halfW, halfH, halfD); +} + +double OctreeTilesetBuilder::computeGeometricError(const BoundingBox& bbox) const { + // 基于包围盒最大边长计算几何误差 + double maxSpan = std::max({bbox.width(), bbox.height(), bbox.depth()}); + if (maxSpan <= 0.0) { + return 1.0; // 最小几何误差 + } + + return maxSpan * config_.childGeometricErrorMultiplier; +} + +} // namespace common diff --git a/src/common/tileset_builder.h b/src/common/tileset_builder.h new file mode 100644 index 00000000..9aeb1f23 --- /dev/null +++ b/src/common/tileset_builder.h @@ -0,0 +1,215 @@ +#pragma once + +/** + * @file common/tileset_builder.h + * @brief 通用Tileset构建器 + * + * 提供统一的Tileset构建功能,支持四叉树(Shapefile)和八叉树(FBX)结构 + */ + +#include "tile_meta.h" +#include "../tileset/tileset_types.h" +#include "../tileset/bounding_volume.h" +#include "../tileset/transform.h" +#include +#include + +namespace common { + +/** + * @brief Tileset构建器配置 + */ +struct TilesetBuilderConfig { + // 版本信息 + std::string version = "1.0"; + std::string gltfUpAxis = "Z"; + + // 几何误差配置 + double rootGeometricErrorMultiplier = 2.0; + double childGeometricErrorMultiplier = 0.5; + + // 包围盒扩展系数 + double boundingVolumeScale = 1.0; + + // 是否启用LOD + bool enableLOD = false; + + // LOD级别数量 + int lodLevelCount = 1; + + // 细化策略 ("ADD" 或 "REPLACE") + std::string refine = "REPLACE"; +}; + +/** + * @brief 包围盒转换函数类型 + */ +using BoundingBoxConverter = std::function; + +/** + * @brief 几何误差计算函数类型 + */ +using GeometricErrorCalculator = std::function; + +/** + * @brief 通用Tileset构建器 + * + * 将通用的TileMeta结构转换为标准的tileset::Tileset + */ +class TilesetBuilder { +public: + /** + * @brief 构造函数 + * @param config 构建器配置 + */ + explicit TilesetBuilder(const TilesetBuilderConfig& config = {}); + + /** + * @brief 构建完整的Tileset + * + * @param rootMeta 根节点元数据 + * @param allMetas 所有节点的元数据映射表 + * @param bboxConverter 包围盒转换函数 + * @param geCalculator 几何误差计算函数 + * @return 完整的Tileset对象 + */ + tileset::Tileset buildTileset( + const TileMetaPtr& rootMeta, + const TileMetaMap& allMetas, + const BoundingBoxConverter& bboxConverter, + const GeometricErrorCalculator& geCalculator) const; + + /** + * @brief 构建单个Tile + * + * @param meta 瓦片元数据 + * @param allMetas 所有节点的元数据映射表 + * @param bboxConverter 包围盒转换函数 + * @param geCalculator 几何误差计算函数 + * @return Tile对象 + */ + tileset::Tile buildTile( + const TileMetaPtr& meta, + const TileMetaMap& allMetas, + const BoundingBoxConverter& bboxConverter, + const GeometricErrorCalculator& geCalculator) const; + + /** + * @brief 创建根节点变换矩阵(ENU到ECEF) + * + * @param centerLon 中心经度(度) + * @param centerLat 中心纬度(度) + * @param centerHeight 中心高度(米) + * @return 变换矩阵 + */ + static tileset::TransformMatrix createRootTransform( + double centerLon, double centerLat, double centerHeight = 0.0); + +protected: + TilesetBuilderConfig config_; + +private: + // 递归构建子节点 + void buildChildren( + tileset::Tile& parentTile, + const TileMetaPtr& parentMeta, + const TileMetaMap& allMetas, + const BoundingBoxConverter& bboxConverter, + const GeometricErrorCalculator& geCalculator) const; + + // 创建Content对象 + tileset::Content createContent(const TileMetaPtr& meta) const; +}; + +/** + * @brief 四叉树Tileset构建器(Shapefile专用) + */ +class QuadtreeTilesetBuilder : public TilesetBuilder { +public: + /** + * @brief 构造函数 + * @param globalCenterLon 全局中心经度 + * @param globalCenterLat 全局中心纬度 + * @param config 构建器配置 + */ + QuadtreeTilesetBuilder(double globalCenterLon, + double globalCenterLat, + const TilesetBuilderConfig& config = {}); + + /** + * @brief 构建Tileset(使用默认的包围盒转换和几何误差计算) + * + * @param rootMeta 根节点元数据 + * @param allMetas 所有节点的元数据映射表 + * @return 完整的Tileset对象 + */ + tileset::Tileset buildTileset( + const TileMetaPtr& rootMeta, + const TileMetaMap& allMetas) const; + + /** + * @brief 将经纬度包围盒转换为ENU坐标系的Box + * + * @param bbox 包围盒(WGS84度) + * @return tileset::Box(ENU米) + */ + tileset::Box convertBoundingBox(const BoundingBox& bbox) const; + + /** + * @brief 计算几何误差(基于包围盒对角线) + * + * @param bbox 包围盒 + * @return 几何误差值 + */ + double computeGeometricError(const BoundingBox& bbox) const; + +protected: + double globalCenterLon_; + double globalCenterLat_; + +private: + // 计算ENU坐标偏移 + void computeEnuOffset(double lon, double lat, + double& offsetX, double& offsetY) const; +}; + +/** + * @brief 八叉树Tileset构建器(FBX专用) + */ +class OctreeTilesetBuilder : public TilesetBuilder { +public: + /** + * @brief 构造函数 + * @param config 构建器配置 + */ + explicit OctreeTilesetBuilder(const TilesetBuilderConfig& config = {}); + + /** + * @brief 构建Tileset(使用默认的包围盒转换和几何误差计算) + * + * @param rootMeta 根节点元数据 + * @param allMetas 所有节点的元数据映射表 + * @return 完整的Tileset对象 + */ + tileset::Tileset buildTileset( + const TileMetaPtr& rootMeta, + const TileMetaMap& allMetas) const; + + /** + * @brief 将本地坐标包围盒转换为Box + * + * @param bbox 包围盒(本地坐标) + * @return tileset::Box + */ + tileset::Box convertBoundingBox(const BoundingBox& bbox) const; + + /** + * @brief 计算几何误差 + * + * @param bbox 包围盒 + * @return 几何误差值 + */ + double computeGeometricError(const BoundingBox& bbox) const; +}; + +} // namespace common diff --git a/src/coordinate_system.cpp b/src/coords/coordinate_system.cpp similarity index 100% rename from src/coordinate_system.cpp rename to src/coords/coordinate_system.cpp diff --git a/src/coordinate_system.h b/src/coords/coordinate_system.h similarity index 100% rename from src/coordinate_system.h rename to src/coords/coordinate_system.h diff --git a/src/coordinate_transformer.cpp b/src/coords/coordinate_transformer.cpp similarity index 99% rename from src/coordinate_transformer.cpp rename to src/coords/coordinate_transformer.cpp index 69e97196..0c22c1d3 100644 --- a/src/coordinate_transformer.cpp +++ b/src/coords/coordinate_transformer.cpp @@ -197,9 +197,6 @@ double CoordinateTransformer::ApplyGeoidCorrection(double lat, double lon, doubl double corrected = GeoidHeight::GetGlobalGeoidCalculator() .ConvertOrthometricToEllipsoidal(lat, lon, height); - fprintf(stderr, "[CoordinateTransformer] Geoid correction: orthometric=%.3f -> ellipsoidal=%.3f\n", - height, corrected); - return corrected; } diff --git a/src/coordinate_transformer.h b/src/coords/coordinate_transformer.h similarity index 100% rename from src/coordinate_transformer.h rename to src/coords/coordinate_transformer.h diff --git a/src/coords/geo_math.h b/src/coords/geo_math.h new file mode 100644 index 00000000..f42402b3 --- /dev/null +++ b/src/coords/geo_math.h @@ -0,0 +1,98 @@ +#pragma once + +/** + * @file geo_math.h + * @brief 地理数学工具函数 + * + * 提供常用的地理坐标转换数学函数 + * 替代 extern.h 中的全局函数 + */ + +#include + +namespace coords { + +// 数学常量 +constexpr double PI = 3.14159265358979323846; +constexpr double DEG_TO_RAD = PI / 180.0; +constexpr double RAD_TO_DEG = 180.0 / PI; + +// WGS84 椭球参数 +constexpr double WGS84_A = 6378137.0; // 长半轴 (米) +constexpr double WGS84_F = 1.0 / 298.257223563; // 扁率 +constexpr double WGS84_E2 = 2 * WGS84_F - WGS84_F * WGS84_F; // 第一偏心率平方 + +/** + * @brief 角度转弧度 + */ +inline double degree2rad(double degrees) { + return degrees * DEG_TO_RAD; +} + +/** + * @brief 弧度转角度 + */ +inline double rad2degree(double radians) { + return radians * RAD_TO_DEG; +} + +/** + * @brief 纬度差转米(近似) + * @param diff 纬度差(弧度) + * @return 对应的距离(米) + */ +inline double lati_to_meter(double diff) { + return diff * WGS84_A; +} + +/** + * @brief 米转纬度差(近似) + * @param meters 距离(米) + * @return 对应的纬度差(弧度) + */ +inline double meter_to_lati(double meters) { + return meters / WGS84_A; +} + +/** + * @brief 经度差转米(近似,与纬度相关) + * @param diff 经度差(弧度) + * @param lati 纬度(弧度) + * @return 对应的距离(米) + */ +inline double longti_to_meter(double diff, double lati) { + return diff * WGS84_A * std::cos(lati); +} + +/** + * @brief 米转经度差(近似,与纬度相关) + * @param meters 距离(米) + * @param lati 纬度(弧度) + * @return 对应的经度差(弧度) + */ +inline double meter_to_longti(double meters, double lati) { + return meters / (WGS84_A * std::cos(lati)); +} + +/** + * @brief 计算 WGS84 椭球上的子午线曲率半径 + * @param lat 纬度(弧度) + * @return 曲率半径(米) + */ +inline double meridian_radius(double lat) { + double sin_lat = std::sin(lat); + return WGS84_A * (1 - WGS84_E2) / + std::pow(1 - WGS84_E2 * sin_lat * sin_lat, 1.5); +} + +/** + * @brief 计算 WGS84 椭球上的卯酉圈曲率半径 + * @param lat 纬度(弧度) + * @return 曲率半径(米) + */ +inline double prime_vertical_radius(double lat) { + double sin_lat = std::sin(lat); + return WGS84_A / std::sqrt(1 - WGS84_E2 * sin_lat * sin_lat); +} + +} // namespace coords diff --git a/src/core/processing_params.h b/src/core/processing_params.h new file mode 100644 index 00000000..dfc05657 --- /dev/null +++ b/src/core/processing_params.h @@ -0,0 +1,308 @@ +#pragma once + +/** + * @file core/processing_params.h + * @brief 统一处理参数定义 - 阶段 1 重构 + * + * 集中定义所有处理参数,消除重复定义 + * 替代以下文件中的重复定义: + * - lod_pipeline.h + * - common/mesh_processor.h + * - gltf/gltf_builder.h + */ + +#include +#include +#include +#include + +namespace core { + +// ============================================ +// 网格简化参数 +// ============================================ + +/** + * @brief 网格简化参数 + * + * 用于 meshoptimizer 等简化库的配置 + */ +struct SimplificationParams { + float target_error = 0.01f; ///< 目标误差 (0.01 = 1%) + float target_ratio = 0.5f; ///< 目标三角形保留比例 (0.5 = 50%) + bool enable_simplification = false; ///< 是否启用简化 + bool preserve_texture_coords = true; ///< 是否保留纹理坐标 + bool preserve_normals = true; ///< 是否保留法线 + bool preserve_bounds = false; ///< 是否保留边界(防止边界收缩) + + /** + * @brief 验证参数有效性 + */ + [[nodiscard]] bool IsValid() const { + return target_error >= 0.0f && target_error <= 1.0f && + target_ratio > 0.0f && target_ratio <= 1.0f; + } + + /** + * @brief 创建高质量预设 + */ + static SimplificationParams HighQuality() { + return {0.005f, 0.8f, true, true, true, true}; + } + + /** + * @brief 创建平衡预设 + */ + static SimplificationParams Balanced() { + return {0.01f, 0.5f, true, true, true, false}; + } + + /** + * @brief 创建高性能预设(激进简化) + */ + static SimplificationParams Aggressive() { + return {0.05f, 0.2f, false, false, false, false}; + } +}; + +// ============================================ +// Draco 压缩参数 +// ============================================ + +/** + * @brief Draco 压缩参数 + * + * 用于 Draco 几何压缩库的配置 + */ +struct DracoCompressionParams { + int position_quantization_bits = 11; ///< 位置量化位数 (10-16) + int normal_quantization_bits = 10; ///< 法线量化位数 (8-16) + int tex_coord_quantization_bits = 12; ///< 纹理坐标量化位数 (8-16) + int generic_quantization_bits = 8; ///< 其他属性量化位数 (8-16) + int compression_level = 7; ///< 压缩级别 (0-10, 10=最大压缩) + bool enable_compression = false; ///< 是否启用压缩 + + /** + * @brief 验证参数有效性 + */ + [[nodiscard]] bool IsValid() const { + return position_quantization_bits >= 1 && position_quantization_bits <= 30 && + normal_quantization_bits >= 1 && normal_quantization_bits <= 30 && + tex_coord_quantization_bits >= 1 && tex_coord_quantization_bits <= 30 && + generic_quantization_bits >= 1 && generic_quantization_bits <= 30 && + compression_level >= 0 && compression_level <= 10; + } + + /** + * @brief 创建高质量预设(高精度) + */ + static DracoCompressionParams HighQuality() { + return {14, 12, 14, 10, 7, true}; + } + + /** + * @brief 创建平衡预设 + */ + static DracoCompressionParams Balanced() { + return {11, 10, 12, 8, 7, true}; + } + + /** + * @brief 创建低带宽预设(高压缩) + */ + static DracoCompressionParams LowBandwidth() { + return {10, 8, 10, 8, 10, true}; + } + + /** + * @brief 创建最小预设(用于调试) + */ + static DracoCompressionParams Minimal() { + return {8, 8, 8, 8, 0, true}; + } +}; + +// ============================================ +// LOD 级别设置 +// ============================================ + +/** + * @brief 单个 LOD 级别的设置 + */ +struct LODLevelSettings { + float target_ratio = 1.0f; ///< 目标三角形比例 + float target_error = 0.01f; ///< 目标误差 + bool enable_simplification = false; ///< 是否启用简化 + bool enable_draco = false; ///< 是否启用 Draco 压缩 + + SimplificationParams simplify; ///< 简化参数 + DracoCompressionParams draco; ///< Draco 参数 + + /** + * @brief 验证设置有效性 + */ + [[nodiscard]] bool IsValid() const { + return target_ratio > 0.0f && target_ratio <= 1.0f && + target_error >= 0.0f && + simplify.IsValid() && + draco.IsValid(); + } +}; + +// ============================================ +// LOD 管道设置 +// ============================================ + +/** + * @brief LOD 管道完整设置 + */ +struct LODPipelineSettings { + bool enable_lod = false; ///< 主开关 + std::vector levels; ///< LOD 级别列表(从精细到粗糙) + + /** + * @brief 验证设置有效性 + */ + [[nodiscard]] bool IsValid() const { + if (!enable_lod) { + return true; + } + for (const auto& level : levels) { + if (!level.IsValid()) { + return false; + } + } + return true; + } + + /** + * @brief 获取 LOD 级别数量 + */ + [[nodiscard]] size_t GetLevelCount() const { + return enable_lod ? levels.size() : 1; + } + + /** + * @brief 从预设比例生成级别 + * @param ratios 目标比例列表(如 {1.0, 0.5, 0.25}) + * @param base_error 基础误差 + * @param simplify_template 简化参数模板 + * @param draco_template Draco 参数模板 + * @param draco_for_lod0 LOD0 是否启用 Draco + * @return 生成的 LOD 设置 + */ + static LODPipelineSettings FromRatios( + const std::vector& ratios, + float base_error, + const SimplificationParams& simplify_template, + const DracoCompressionParams& draco_template, + bool draco_for_lod0 = false); + + /** + * @brief 创建默认 LOD 设置(3 级) + */ + static LODPipelineSettings DefaultThreeLevel( + const SimplificationParams& simplify_template = {}, + const DracoCompressionParams& draco_template = {}); +}; + +// ============================================ +// 纹理处理参数 +// ============================================ + +/** + * @brief 纹理处理参数 + */ +struct TextureProcessingParams { + enum class Format { + Original, ///< 保持原始格式 + KTX2, ///< KTX2 + Basis Universal + WebP, ///< WebP 压缩 + PNG, ///< PNG(无损) + JPEG ///< JPEG(有损) + }; + + enum class Quality { + Low, ///< 低质量(高压缩) + Medium, ///< 中等质量 + High, ///< 高质量 + Lossless ///< 无损 + }; + + Format format = Format::KTX2; ///< 输出格式 + Quality quality = Quality::Medium; ///< 质量级别 + int quality_value = 85; ///< 具体质量值 (0-100) + int max_size = 2048; ///< 最大纹理尺寸 + bool generate_mipmaps = true; ///< 是否生成 mipmap + bool flip_y = false; ///< 是否 Y 轴翻转 + bool premultiply_alpha = false; ///< 是否预乘 alpha + + /** + * @brief 验证参数有效性 + */ + [[nodiscard]] bool IsValid() const { + return quality_value >= 0 && quality_value <= 100 && + max_size > 0; + } + + /** + * @brief 获取实际质量值 + */ + [[nodiscard]] int GetQualityValue() const { + if (quality_value >= 0) { + return quality_value; + } + switch (quality) { + case Quality::Low: return 50; + case Quality::Medium: return 85; + case Quality::High: return 95; + case Quality::Lossless: return 100; + } + return 85; + } +}; + +// ============================================ +// B3DM 生成参数 +// ============================================ + +/** + * @brief B3DM 生成参数 + */ +struct B3DMGenerationParams { + bool embed_textures = true; ///< 是否嵌入纹理 + bool embed_gltf = true; ///< 是否嵌入 glTF(而非外部引用) + bool include_batch_table = true; ///< 是否包含批次表 + bool include_feature_table = true; ///< 是否包含要素表 + std::string batch_table_json; ///< 自定义批次表 JSON + + /** + * @brief 验证参数有效性 + */ + [[nodiscard]] bool IsValid() const { + return true; // 当前所有组合都有效 + } +}; + +// ============================================ +// 顶点数据格式 +// ============================================ + +/** + * @brief 顶点数据结构 + * + * 用于网格处理的标准顶点格式 + */ +struct VertexData { + float x = 0.0f, y = 0.0f, z = 0.0f; ///< 位置 + float nx = 0.0f, ny = 0.0f, nz = 0.0f; ///< 法线 + float u = 0.0f, v = 0.0f; ///< 纹理坐标 + float r = 1.0f, g = 1.0f, b = 1.0f, a = 1.0f; ///< 颜色 + + VertexData() = default; + VertexData(float px, float py, float pz) : x(px), y(py), z(pz) {} + VertexData(float px, float py, float pz, float nx_, float ny_, float nz_) + : x(px), y(py), z(pz), nx(nx_), ny(ny_), nz(nz_) {} +}; + +} // namespace core diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..859db28b --- /dev/null +++ b/src/error.rs @@ -0,0 +1,130 @@ +//! 统一错误类型定义 +//! +//! 提供整个项目使用的错误类型和Result别名 + +#![allow(dead_code)] + +use std::path::PathBuf; +use thiserror::Error; + +/// 项目统一错误类型 +#[derive(Error, Debug)] +pub enum TileError { + /// IO错误 + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + /// 无效的路径 + #[error("Invalid path: {path}")] + InvalidPath { path: String }, + + /// 路径包含无效的UTF-8 + #[error("Path contains invalid UTF-8: {}", .path.display())] + InvalidUtf8 { path: PathBuf }, + + /// FFI调用失败 + #[error("FFI call failed: {func} - {reason}")] + FfiError { func: &'static str, reason: String }, + + /// 格式转换失败 + #[error("Conversion failed for format: {format}")] + ConversionFailed { format: String }, + + /// 缺少必需的参数 + #[error("Required argument missing: {arg}")] + MissingArgument { arg: &'static str }, + + /// 无效的坐标值 + #[error("Invalid coordinate value: {value}")] + InvalidCoordinate { value: String }, + + /// 大地水准面初始化失败 + #[error("Geoid initialization failed: {model}")] + GeoidInitFailed { model: String }, + + /// JSON解析错误 + #[error("JSON parse error: {0}")] + JsonParse(#[from] serde_json::Error), + + /// 无效的数字格式 + #[error("Invalid number format: {value}")] + InvalidNumber { value: String }, + + /// 文件不存在 + #[error("File not found: {}", .path.display())] + FileNotFound { path: PathBuf }, + + /// 目录不存在 + #[error("Directory not found: {}", .path.display())] + DirectoryNotFound { path: PathBuf }, +} + +/// 项目统一Result类型 +pub type TileResult = Result; + +/// 将Option转换为TileResult的辅助函数 +pub trait OptionExt { + fn ok_or_missing(self, arg: &'static str) -> TileResult; + fn ok_or_invalid_path(self, path: impl Into) -> TileResult; + fn ok_or_file_not_found(self, path: PathBuf) -> TileResult; +} + +impl OptionExt for Option { + fn ok_or_missing(self, arg: &'static str) -> TileResult { + self.ok_or(TileError::MissingArgument { arg }) + } + + fn ok_or_invalid_path(self, path: impl Into) -> TileResult { + self.ok_or(TileError::InvalidPath { path: path.into() }) + } + + fn ok_or_file_not_found(self, path: PathBuf) -> TileResult { + self.ok_or(TileError::FileNotFound { path }) + } +} + +/// 解析f64的辅助函数,返回TileResult +pub fn parse_f64(s: &str) -> TileResult { + s.parse::() + .map_err(|_| TileError::InvalidNumber { value: s.to_string() }) +} + +/// 解析i32的辅助函数,返回TileResult +pub fn parse_i32(s: &str) -> TileResult { + s.parse::() + .map_err(|_| TileError::InvalidNumber { value: s.to_string() }) +} + +/// 将Path转换为字符串的辅助函数 +pub fn path_to_string(path: &std::path::Path) -> TileResult { + path.to_str() + .map(|s| s.to_string()) + .ok_or_else(|| TileError::InvalidUtf8 { + path: path.to_path_buf(), + }) +} + +/// 获取可执行文件所在目录 +pub fn get_exe_dir() -> TileResult { + std::env::current_exe() + .map_err(TileError::Io)? + .parent() + .map(|p| p.to_path_buf()) + .ok_or(TileError::InvalidPath { + path: "Could not get executable directory".to_string(), + }) +} + +/// 获取GDAL数据路径 +pub fn get_gdal_data_path() -> TileResult { + let exe_dir = get_exe_dir()?; + let gdal_path = exe_dir.join("gdal"); + path_to_string(&gdal_path) +} + +/// 获取PROJ数据路径 +pub fn get_proj_data_path() -> TileResult { + let exe_dir = get_exe_dir()?; + let proj_path = exe_dir.join("proj"); + path_to_string(&proj_path) +} diff --git a/src/fbx.rs b/src/fbx.rs index 647e531c..96943775 100644 --- a/src/fbx.rs +++ b/src/fbx.rs @@ -3,7 +3,6 @@ use std::{error::Error, fs}; use crate::common::str_to_vec_c; extern "C" { - fn fbx23dtile( in_path: *const u8, out_path: *const u8, @@ -17,9 +16,11 @@ extern "C" { longitude: f64, latitude: f64, height: f64, + enable_lod: bool, ) -> *mut libc::c_void; } +#[allow(clippy::too_many_arguments)] pub fn convert_fbx( in_file: &str, out_dir: &str, @@ -31,6 +32,7 @@ pub fn convert_fbx( longitude: f64, latitude: f64, height: f64, + enable_lod: bool, ) -> Result<(), Box> { let in_path = str_to_vec_c(in_file); let out_path = str_to_vec_c(out_dir); @@ -56,6 +58,7 @@ pub fn convert_fbx( longitude, latitude, height, + enable_lod, ); if out_ptr.is_null() { diff --git a/src/fbx.cpp b/src/fbx/core/fbx.cpp similarity index 93% rename from src/fbx.cpp rename to src/fbx/core/fbx.cpp index 8421587c..d60166f4 100644 --- a/src/fbx.cpp +++ b/src/fbx/core/fbx.cpp @@ -1,7 +1,8 @@ -#include "fbx.h" +#include "fbx/core/fbx.h" #include "extern.h" #include +#include #include #include #include @@ -253,13 +254,6 @@ osg::StateSet* FBXLoader::getOrCreateStateSet(const ufbx_material* mat) { if (tex) { osg::ref_ptr image; - LOG_I("Texture '%s': type=%d, has_content=%d, content_size=%zu, filename='%s'", - tex->name.data ? tex->name.data : "(unnamed)", - tex->type, - tex->content.data ? 1 : 0, - tex->content.size, - tex->filename.data ? tex->filename.data : "(none)"); - // 1. Try embedded content if (tex->content.data && tex->content.size > 0) { std::string filename = ufbx_string_to_std(tex->filename); @@ -497,6 +491,90 @@ osg::StateSet* FBXLoader::getOrCreateStateSet(const ufbx_material* mat) { stateSet->addUniform(new osg::Uniform("roughnessFactor", roughness)); stateSet->addUniform(new osg::Uniform("metallicFactor", metallic)); + MaterialExtensionData extData; + + auto extractTextureTransform = [](const ufbx_texture* texture) -> TextureTransformData { + TextureTransformData result; + if (!texture || !texture->has_uv_transform) { + return result; + } + + const ufbx_transform& t = texture->uv_transform; + result.has_transform = true; + result.offset[0] = static_cast(t.translation.x); + result.offset[1] = static_cast(t.translation.y); + result.scale[0] = static_cast(t.scale.x); + result.scale[1] = static_cast(t.scale.y); + + ufbx_vec3 euler = ufbx_quat_to_euler(t.rotation, UFBX_ROTATION_ORDER_XYZ); + result.rotation = static_cast(euler.z * std::numbers::pi / 180.0); + + return result; + }; + + if (tex) { + extData.base_color_transform = extractTextureTransform(tex); + if (extData.base_color_transform.has_transform) { + extData.has_any_extension = true; + } + } + if (ntex) { + extData.normal_transform = extractTextureTransform(ntex); + if (extData.normal_transform.has_transform) { + extData.has_any_extension = true; + } + } + if (etex) { + extData.emissive_transform = extractTextureTransform(etex); + if (extData.emissive_transform.has_transform) { + extData.has_any_extension = true; + } + } + if (rtex) { + extData.metallic_roughness_transform = extractTextureTransform(rtex); + if (extData.metallic_roughness_transform.has_transform) { + extData.has_any_extension = true; + } + } + if (aotex) { + extData.occlusion_transform = extractTextureTransform(aotex); + if (extData.occlusion_transform.has_transform) { + extData.has_any_extension = true; + } + } + + bool hasSpecular = mat->fbx.specular_color.has_value && + (mat->fbx.specular_color.value_vec3.x > 0.5 || + mat->fbx.specular_color.value_vec3.y > 0.5 || + mat->fbx.specular_color.value_vec3.z > 0.5) && + !mat->pbr.base_color.has_value; + if (hasSpecular) { + extData.specular_glossiness.use_specular_glossiness = true; + extData.specular_glossiness.diffuse_factor = { + static_cast(diffuse.r()), + static_cast(diffuse.g()), + static_cast(diffuse.b()), + static_cast(diffuse.a()) + }; + extData.specular_glossiness.specular_factor = { + static_cast(specular.r()), + static_cast(specular.g()), + static_cast(specular.b()) + }; + if (mat->fbx.specular_exponent.has_value) { + float shininess = static_cast(mat->fbx.specular_exponent.value_real); + if (shininess < 0.0f) shininess = 0.0f; + if (shininess > 128.0f) shininess = 128.0f; + extData.specular_glossiness.glossiness_factor = shininess / 128.0; + } + extData.has_any_extension = true; + } + + if (extData.has_any_extension) { + materialExtensionCache[mat] = extData; + stateSetExtensionCache[stateSet] = extData; + } + materialCache[mat] = stateSet; materialHashCache[matHash] = stateSet; material_created_count++; diff --git a/src/fbx.h b/src/fbx/core/fbx.h similarity index 70% rename from src/fbx.h rename to src/fbx/core/fbx.h index 10a613ed..c8a8fc87 100644 --- a/src/fbx.h +++ b/src/fbx/core/fbx.h @@ -7,8 +7,39 @@ #include #include #include +#include + +struct TextureTransformData { + std::array offset = {0.0f, 0.0f}; + float rotation = 0.0f; + std::array scale = {1.0f, 1.0f}; + int tex_coord = 0; + bool has_transform = false; + + static TextureTransformData Identity() { return {}; } +}; + +struct SpecularGlossinessData { + std::array diffuse_factor = {1.0, 1.0, 1.0, 1.0}; + std::array specular_factor = {1.0, 1.0, 1.0}; + double glossiness_factor = 1.0; + bool use_specular_glossiness = false; + int diffuse_texture_index = -1; + int specular_glossiness_texture_index = -1; + + static SpecularGlossinessData Default() { return {}; } +}; + +struct MaterialExtensionData { + TextureTransformData base_color_transform; + TextureTransformData normal_transform; + TextureTransformData emissive_transform; + TextureTransformData metallic_roughness_transform; + TextureTransformData occlusion_transform; + SpecularGlossinessData specular_glossiness; + bool has_any_extension = false; +}; -// Mesh合并与属性挂载辅助结构 struct MeshKey { std::string geomHash; // mesh内容hash std::string matHash; // 材质hash @@ -32,6 +63,12 @@ struct MeshInstanceInfo { int featureId = -1; // 合并后featureId }; +// Instance reference for pipeline processing +struct InstanceRef { + MeshInstanceInfo* meshInfo; + int transformIndex; +}; + class FBXLoader { public: FBXLoader(const std::string &filename); @@ -64,6 +101,10 @@ class FBXLoader { std::unordered_map> materialHashCache; // 基于几何内容哈希的去重缓存 (hash -> osg::Geometry*) std::unordered_map> geometryHashCache; + // 材质扩展数据缓存 (ufbx_material* -> MaterialExtensionData) + std::unordered_map materialExtensionCache; + // StateSet 到扩展数据的映射 (用于 Pipeline 访问) + std::unordered_map stateSetExtensionCache; // 处理 Mesh 并返回 Geode (如果需要挂载到场景) osg::ref_ptr processMesh(ufbx_node *node, ufbx_mesh *mesh, const osg::Matrixd &globalXform); diff --git a/src/fbx/fbx_geometry_extractor.cpp b/src/fbx/fbx_geometry_extractor.cpp new file mode 100644 index 00000000..1ba1f319 --- /dev/null +++ b/src/fbx/fbx_geometry_extractor.cpp @@ -0,0 +1,228 @@ +#include "fbx_geometry_extractor.h" +#include "fbx_spatial_item_adapter.h" +#include "../extern.h" +#include "../osg/utils/material_utils.h" + +namespace fbx { + +namespace { + /** + * @brief 复制纹理变换数据 + */ + void copyTextureTransform(const ::TextureTransformData& src, + common::TextureTransformInfo& dst) { + if (src.has_transform) { + dst.hasTransform = true; + dst.offset[0] = src.offset[0]; + dst.offset[1] = src.offset[1]; + dst.scale[0] = src.scale[0]; + dst.scale[1] = src.scale[1]; + dst.rotation = src.rotation; + dst.texCoord = src.tex_coord; + } + } +} + +std::vector> FBXGeometryExtractor::extract( + const spatial::core::SpatialItem* item) { + + std::vector> result; + + // 尝试转换为FBXSpatialItemAdapter + const auto* fbxItem = dynamic_cast(item); + if (!fbxItem) { + LOG_W("FBXGeometryExtractor: item is not FBXSpatialItemAdapter"); + return result; + } + + // 获取几何体 + const osg::Geometry* geom = fbxItem->getGeometry(); + if (!geom) { + LOG_W("FBXGeometryExtractor: no geometry found for %s", fbxItem->getNodeName().c_str()); + return result; + } + + // 检查原始几何体的顶点数组 + const osg::Array* vertexArray = geom->getVertexArray(); + if (!vertexArray || vertexArray->getNumElements() == 0) { + return result; + } + + // 克隆几何体 + osg::ref_ptr clonedGeom = static_cast( + geom->clone(osg::CopyOp::DEEP_COPY_ALL) + ); + + // 应用世界变换到顶点 + osg::Matrixd transform = fbxItem->getTransform(); + + // 创建Y-up到Z-up的坐标转换矩阵 + // FBX是Y-up,3D Tiles是Z-up + // 转换: (x, y, z) -> (x, -z, y) + // 注意: OSG使用行主序矩阵,但构造时是列主序 + osg::Matrixd yupToZup( + 1, 0, 0, 0, // 第一列: x' = 1*x + 0*y + 0*z + 0, 0, 1, 0, // 第二列: y' = 0*x + 0*y + 1*z = z + 0, -1, 0, 0, // 第三列: z' = 0*x - 1*y + 0*z = -y + 0, 0, 0, 1 + ); + + // 组合变换: 先应用世界变换,再Y-up到Z-up + // OSG是右乘: v' = v * transform * yupToZup + osg::Matrixd finalTransform = transform * yupToZup; + + // 处理不同类型的顶点数组 (Vec3Array 或 Vec3dArray) + osg::Vec3Array* vertices = dynamic_cast(clonedGeom->getVertexArray()); + osg::Vec3dArray* verticesd = dynamic_cast(clonedGeom->getVertexArray()); + + if (vertices) { + // 单精度顶点数组 + for (auto& vertex : *vertices) { + vertex = vertex * finalTransform; + } + vertices->dirty(); + } else if (verticesd) { + // 双精度顶点数组 + for (auto& vertex : *verticesd) { + vertex = vertex * finalTransform; + } + verticesd->dirty(); + } else { + return result; + } + + // 变换法线 (使用finalTransform) + osg::Matrixd normalMatrix = osg::Matrixd::inverse(finalTransform); + normalMatrix.transpose3x3(normalMatrix); + + osg::Vec3Array* normals = dynamic_cast(clonedGeom->getNormalArray()); + if (normals) { + for (auto& normal : *normals) { + normal = osg::Matrixd::transform3x3(normal, normalMatrix); + normal.normalize(); + } + normals->dirty(); + } + + result.push_back(clonedGeom); + + return result; +} + +std::string FBXGeometryExtractor::getId(const spatial::core::SpatialItem* item) { + const auto* fbxItem = dynamic_cast(item); + if (!fbxItem) { + return ""; + } + + return fbxItem->getNodeName(); +} + +std::map FBXGeometryExtractor::getAttributes( + const spatial::core::SpatialItem* item) { + + std::map attrs; + + const auto* fbxItem = dynamic_cast(item); + if (!fbxItem) { + return attrs; + } + + // 添加节点名称 + attrs["name"] = fbxItem->getNodeName(); + + // 可以添加更多属性... + + return attrs; +} + +std::shared_ptr FBXGeometryExtractor::getMaterial( + const spatial::core::SpatialItem* item) { + + const auto* fbxItem = dynamic_cast(item); + if (!fbxItem) { + LOG_W("FBXGeometryExtractor::getMaterial: item is not FBXSpatialItemAdapter"); + return std::make_shared(); + } + + // 获取几何体的StateSet + const osg::Geometry* geom = fbxItem->getGeometry(); + if (!geom) { + return std::make_shared(); + } + + const osg::StateSet* stateSet = geom->getStateSet(); + if (!stateSet) { + // 没有StateSet表示使用默认材质 + return std::make_shared(); + } + + auto materialInfo = std::make_shared(); + + // ===== 提取基础PBR参数 ===== + osg::utils::PBRParams pbrParams; + osg::utils::MaterialUtils::extractPBRParams(stateSet, pbrParams); + + materialInfo->baseColor = pbrParams.baseColor; + materialInfo->roughnessFactor = pbrParams.roughnessFactor; + materialInfo->metallicFactor = pbrParams.metallicFactor; + materialInfo->emissiveColor = { + pbrParams.emissiveColor[0], + pbrParams.emissiveColor[1], + pbrParams.emissiveColor[2] + }; + materialInfo->aoStrength = pbrParams.aoStrength; + + // ===== 提取纹理对象 ===== + materialInfo->baseColorTexture = + const_cast(osg::utils::MaterialUtils::getBaseColorTexture(stateSet)); + materialInfo->normalTexture = + const_cast(osg::utils::MaterialUtils::getNormalTexture(stateSet)); + materialInfo->emissiveTexture = + const_cast(osg::utils::MaterialUtils::getEmissiveTexture(stateSet)); + + // 金属度/粗糙度和遮挡纹理需要从其他纹理单元获取 + // 根据FBXLoader的实现,它们可能在特定的纹理单元 + if (stateSet->getTextureAttributeList().size() > 2) { + materialInfo->metallicRoughnessTexture = + const_cast(dynamic_cast( + stateSet->getTextureAttribute(2, osg::StateAttribute::TEXTURE))); + } + if (stateSet->getTextureAttributeList().size() > 3) { + materialInfo->occlusionTexture = + const_cast(dynamic_cast( + stateSet->getTextureAttribute(3, osg::StateAttribute::TEXTURE))); + } + + // ===== 提取纹理变换和Specular-Glossiness数据 ===== + const MaterialExtensionData* extData = fbxItem->getMaterialExtensionData(); + if (extData) { + // 复制纹理变换 + copyTextureTransform(extData->base_color_transform, materialInfo->baseColorTransform); + copyTextureTransform(extData->normal_transform, materialInfo->normalTransform); + copyTextureTransform(extData->metallic_roughness_transform, materialInfo->metallicRoughnessTransform); + copyTextureTransform(extData->occlusion_transform, materialInfo->occlusionTransform); + copyTextureTransform(extData->emissive_transform, materialInfo->emissiveTransform); + + // 复制Specular-Glossiness数据 + if (extData->specular_glossiness.use_specular_glossiness) { + materialInfo->useSpecularGlossiness = true; + materialInfo->diffuseFactor = { + extData->specular_glossiness.diffuse_factor[0], + extData->specular_glossiness.diffuse_factor[1], + extData->specular_glossiness.diffuse_factor[2], + extData->specular_glossiness.diffuse_factor[3] + }; + materialInfo->specularFactor = { + extData->specular_glossiness.specular_factor[0], + extData->specular_glossiness.specular_factor[1], + extData->specular_glossiness.specular_factor[2] + }; + materialInfo->glossinessFactor = extData->specular_glossiness.glossiness_factor; + } + } + + return materialInfo; +} + +} // namespace fbx diff --git a/src/fbx/fbx_geometry_extractor.h b/src/fbx/fbx_geometry_extractor.h new file mode 100644 index 00000000..03da88af --- /dev/null +++ b/src/fbx/fbx_geometry_extractor.h @@ -0,0 +1,61 @@ +#pragma once + +/** + * @file fbx/fbx_geometry_extractor.h + * @brief FBX几何体提取器 + * + * 实现IGeometryExtractor接口,供B3DMGenerator使用 + */ + +#include "../common/geometry_extractor.h" +#include "fbx_spatial_item_adapter.h" +#include + +namespace fbx { + +/** + * @brief FBX几何体提取器 + * + * 从FBXSpatialItemAdapter提取几何体信息 + */ +class FBXGeometryExtractor : public common::IGeometryExtractor { +public: + FBXGeometryExtractor() = default; + ~FBXGeometryExtractor() override = default; + + /** + * @brief 从空间对象提取几何体 + * @param item 空间对象(必须是FBXSpatialItemAdapter) + * @return 几何体列表 + */ + std::vector> extract( + const spatial::core::SpatialItem* item) override; + + /** + * @brief 获取对象的唯一标识(用于BatchID) + */ + std::string getId(const spatial::core::SpatialItem* item) override; + + /** + * @brief 获取对象的属性(用于BatchTable) + */ + std::map getAttributes( + const spatial::core::SpatialItem* item) override; + + /** + * @brief 获取对象的材质信息 + * + * 从FBX空间对象提取完整的材质信息,包括: + * - PBR参数(baseColor, roughness, metallic等) + * - 纹理对象(从StateSet提取) + * - 纹理变换(从MaterialExtensionData提取) + * - Specular-Glossiness参数(传统FBX材质) + * + * @param item FBX空间对象 + * @return 材质信息,如果没有材质返回默认材质 + */ + std::shared_ptr getMaterial( + const spatial::core::SpatialItem* item) override; +}; + +} // namespace fbx diff --git a/src/fbx/fbx_octree_adapter.cpp b/src/fbx/fbx_octree_adapter.cpp new file mode 100644 index 00000000..2c87e1d5 --- /dev/null +++ b/src/fbx/fbx_octree_adapter.cpp @@ -0,0 +1,136 @@ +#include "fbx_octree_adapter.h" +#include "fbx_spatial_item_adapter.h" +#include "fbx/core/fbx.h" +#include + +namespace fbx { + +LegacyOctreeNode* FBXOctreeAdapter::convertFromIndex( + const spatial::strategy::OctreeIndex& index, + const FBXSpatialItemList& spatialItems) { + + const auto* root = index.getRoot(); + if (!root) { + return nullptr; + } + + return convertNode(static_cast(root), spatialItems); +} + +LegacyOctreeNode* FBXOctreeAdapter::convertNode( + const spatial::strategy::OctreeNode* node, + const FBXSpatialItemList& spatialItems) { + + if (!node) { + return nullptr; + } + + auto* legacyNode = new LegacyOctreeNode(); + + // 转换包围盒 + auto bounds = node->getBounds3D(); + auto min = bounds.min(); + auto max = bounds.max(); + legacyNode->bbox = osg::BoundingBox( + min[0], min[1], min[2], + max[0], max[1], max[2] + ); + + // 转换深度 + legacyNode->depth = static_cast(node->getDepth()); + + // 转换内容 + auto items = node->getItems(); + for (const auto& itemRef : items) { + auto ref = findInstanceRef(itemRef.get(), spatialItems); + if (ref.meshInfo) { + legacyNode->content.push_back(ref); + } + } + + // 递归转换子节点 + auto children = node->getChildren(); + for (const auto* child : children) { + auto* legacyChild = convertNode( + static_cast(child), + spatialItems + ); + if (legacyChild) { + legacyNode->children.push_back(legacyChild); + } + } + + return legacyNode; +} + +InstanceRef FBXOctreeAdapter::findInstanceRef( + const spatial::core::SpatialItem* item, + const FBXSpatialItemList& spatialItems) { + + InstanceRef ref; + ref.meshInfo = nullptr; + ref.transformIndex = -1; + + // 查找匹配的spatial item + for (const auto& spatialItem : spatialItems) { + if (spatialItem.get() == item) { + ref.meshInfo = spatialItem->getMeshInfo(); + ref.transformIndex = spatialItem->getTransformIndex(); + return ref; + } + } + + return ref; +} + +bool FBXOctreeAdapter::verifyConversion( + const LegacyOctreeNode* legacyRoot, + const spatial::strategy::OctreeIndex& index) { + + if (!legacyRoot) { + return index.getRoot() == nullptr; + } + + const auto* root = index.getRoot(); + if (!root) { + return false; + } + + // 验证根节点 + auto newBounds = root->getBounds(); + auto legacyMin = legacyRoot->bbox._min; + auto legacyMax = legacyRoot->bbox._max; + + // 简单的包围盒验证 + bool boundsMatch = + std::abs(newBounds.min()[0] - legacyMin.x()) < 0.001 && + std::abs(newBounds.min()[1] - legacyMin.y()) < 0.001 && + std::abs(newBounds.min()[2] - legacyMin.z()) < 0.001 && + std::abs(newBounds.max()[0] - legacyMax.x()) < 0.001 && + std::abs(newBounds.max()[1] - legacyMax.y()) < 0.001 && + std::abs(newBounds.max()[2] - legacyMax.z()) < 0.001; + + if (!boundsMatch) { + return false; + } + + // 验证项目数量 + size_t newItemCount = index.getItemCount(); + + // 计算legacy树中的项目数量 + std::function countItems = + [&countItems](const LegacyOctreeNode* node) -> size_t { + if (!node) return 0; + size_t count = node->content.size(); + for (const auto* child : node->children) { + count += countItems(child); + } + return count; + }; + + size_t legacyItemCount = countItems(legacyRoot); + + return newItemCount == legacyItemCount; +} + +} // namespace fbx diff --git a/src/fbx/fbx_octree_adapter.h b/src/fbx/fbx_octree_adapter.h new file mode 100644 index 00000000..812be656 --- /dev/null +++ b/src/fbx/fbx_octree_adapter.h @@ -0,0 +1,67 @@ +#pragma once + +#include "../spatial/strategy/octree_strategy.h" +#include "fbx_spatial_item_adapter.h" +#include "fbx/core/fbx.h" +#include +#include +#include + +namespace fbx { + +/** + * @brief 传统八叉树节点结构(与FBXPipeline::OctreeNode兼容) + * + * 注意:这个结构体定义在FBXPipeline中是私有的,这里重新定义用于适配器 + */ +struct LegacyOctreeNode { + osg::BoundingBox bbox; + std::vector content; + std::vector children; + int depth = 0; + + bool isLeaf() const { return children.empty(); } + ~LegacyOctreeNode() { + for (auto c : children) delete c; + } +}; + +/** + * @brief FBX八叉树适配器 + * + * 阶段2适配器:将新的OctreeStrategy节点转换为LegacyOctreeNode格式 + * 使后续的processNode可以继续使用老的数据结构 + */ +class FBXOctreeAdapter { +public: + /** + * @brief 将OctreeIndex转换为传统OctreeNode + * @param index 八叉树索引 + * @param spatialItems 空间对象列表(用于查找原始InstanceRef) + * @return 传统OctreeNode树根节点 + */ + static LegacyOctreeNode* convertFromIndex( + const spatial::strategy::OctreeIndex& index, + const FBXSpatialItemList& spatialItems); + + /** + * @brief 验证转换结果 + * @param legacyRoot 传统八叉树根节点 + * @param index 八叉树索引 + * @return 验证是否通过 + */ + static bool verifyConversion( + const LegacyOctreeNode* legacyRoot, + const spatial::strategy::OctreeIndex& index); + +private: + static LegacyOctreeNode* convertNode( + const spatial::strategy::OctreeNode* node, + const FBXSpatialItemList& spatialItems); + + static InstanceRef findInstanceRef( + const spatial::core::SpatialItem* item, + const FBXSpatialItemList& spatialItems); +}; + +} // namespace fbx diff --git a/src/fbx/fbx_processor.cpp b/src/fbx/fbx_processor.cpp new file mode 100644 index 00000000..98a79669 --- /dev/null +++ b/src/fbx/fbx_processor.cpp @@ -0,0 +1,545 @@ +#include "fbx/fbx_processor.h" +#include "utils/log.h" +#include "utils/file_utils.h" +#include "./coords/coordinate_transformer.h" +#include "pipeline/conversion_pipeline.h" +#include "pipeline/fbx_pipeline.h" +#include "pipeline/adapters/fbx/fbx_data_source.h" +#include +#include +#include +#include +#include +#include +#include + +// Use existing tinygltf if possible, or include it +#include +#include +#include +#include +#include "common/mesh_processor.h" +#include +#include +#include "lod_pipeline.h" +#include +#include + +// Stage 2: New architecture integration +#include "spatial/strategy/octree_strategy.h" + +// Stage 3: B3DMGenerator integration +#include "fbx/fbx_geometry_extractor.h" + +// Stage 4: TilesetBuilder integration +#include "fbx/fbx_tileset_adapter.h" + +using json = nlohmann::json; +namespace fs = std::filesystem; + +// Helper to check point in box +bool isPointInBox(const osg::Vec3d& p, const osg::BoundingBox& b) { + return p.x() >= b.xMin() && p.x() <= b.xMax() && + p.y() >= b.yMin() && p.y() <= b.yMax() && + p.z() >= b.zMin() && p.z() <= b.zMax(); +} + +FBXPipeline::FBXPipeline(const PipelineSettings& s) : settings(s) { +} + +void FBXPipeline::SetDataSource(pipeline::DataSource* dataSource) { + externalDataSource_ = dataSource; +} + +void FBXPipeline::SetSpatialIndex(pipeline::ISpatialIndex* spatialIndex) { + externalSpatialIndex_ = spatialIndex; +} + +void FBXPipeline::SetTilesetBuilder(pipeline::ITilesetBuilder* tilesetBuilder) { + externalTilesetBuilder_ = tilesetBuilder; +} + +pipeline::ITilesetBuilder* FBXPipeline::GetCurrentTilesetBuilder() { + if (externalTilesetBuilder_) { + return externalTilesetBuilder_; + } + // 如果内部 TilesetBuilder 未创建,则创建它 + if (!tilesetBuilder_) { + // 使用工厂创建 FBX TilesetBuilder + tilesetBuilder_ = pipeline::TilesetBuilderFactory::Instance().Create("fbx"); + if (tilesetBuilder_) { + pipeline::TilesetBuilderConfig tbConfig; + tbConfig.center_longitude = settings.longitude; + tbConfig.center_latitude = settings.latitude; + tbConfig.center_height = settings.height; + tbConfig.bounding_volume_scale = 1.0; + tbConfig.child_geometric_error_multiplier = settings.geScale; + tbConfig.enable_lod = settings.enableLOD; + tilesetBuilder_->Initialize(tbConfig); + } + } + return tilesetBuilder_.get(); +} + +pipeline::DataSource* FBXPipeline::GetCurrentDataSource() { + if (externalDataSource_) { + return externalDataSource_; + } + // 如果内部数据源未创建,则使用工厂创建 + if (!dataSource_) { + dataSource_ = pipeline::DataSourceFactory::Instance().Create("fbx"); + if (dataSource_) { + LOG_I("FBXPipeline: Created FBXDataSource using factory"); + } + } + return dataSource_.get(); +} + +pipeline::ISpatialIndex* FBXPipeline::GetCurrentSpatialIndex() { + if (externalSpatialIndex_) { + return externalSpatialIndex_; + } + // 如果内部空间索引未创建,则使用工厂创建 + if (!spatialIndex_) { + spatialIndex_ = pipeline::SpatialIndexFactory::Instance().Create("octree"); + if (spatialIndex_) { + LOG_I("FBXPipeline: Created OctreeIndex using factory"); + } + } + return spatialIndex_.get(); +} + +const spatial::strategy::OctreeNode* FBXPipeline::GetCurrentRootNode() const { + if (externalSpatialIndex_) { + // 从外部空间索引获取根节点 + auto* rawNode = externalSpatialIndex_->GetRootNode(); + if (rawNode) { + // 需要适配器转换 + // 暂时返回 nullptr,实际使用时需要正确处理 + return nullptr; + } + } + if (spatialIndex_) { + // 从内部空间索引获取根节点 + auto* rawNode = spatialIndex_->GetRootNode(); + if (rawNode) { + // 需要适配器转换 + return nullptr; + } + } + // 原有逻辑返回内部构建的根节点 + return nullptr; +} + +bool FBXPipeline::loadData() { + // 步骤1修改:如果提供了外部数据源,直接使用其空间项 + if (externalDataSource_) { + LOG_I("FBXPipeline: Using external data source"); + + // 检查外部数据源是否已加载 + if (!externalDataSource_->IsLoaded()) { + pipeline::DataSourceConfig dsConfig; + dsConfig.input_path = settings.inputPath; + dsConfig.output_path = settings.outputPath; + dsConfig.center_longitude = settings.longitude; + dsConfig.center_latitude = settings.latitude; + dsConfig.center_height = settings.height; + + if (!externalDataSource_->Load(dsConfig)) { + LOG_E("FBXPipeline: Failed to load external data source"); + return false; + } + } + + // 从外部数据源获取空间项 + auto* fbxSource = dynamic_cast(externalDataSource_); + if (fbxSource) { + spatialItems_ = fbxSource->GetFBXSpatialItems(); + // 注意:不要获取外部数据源的 loader,避免双重释放 + // 我们自己加载 loader + } + + LOG_I("FBXPipeline: External data source loaded with %zu items", + spatialItems_.size()); + // 继续执行下面的 loader 加载逻辑 + } + + // 原有逻辑:内部加载数据(无论是否使用外部数据源,都需要加载 loader) + loader_ = std::make_unique(settings.inputPath); + loader_->load(); + LOG_I("FBX Loaded. Mesh Pool Size: %zu", loader_->meshPool.size()); + + // 阶段1:创建空间对象适配器 + LOG_I("Creating Spatial Items..."); + spatialItems_ = fbx::createSpatialItems(loader_.get()); + LOG_I("Created %zu spatial items.", spatialItems_.size()); + + return !spatialItems_.empty(); +} + +void FBXPipeline::run() { + LOG_I("Starting FBXPipeline (Stage 1)..."); + + // 步骤1修改:使用 loadData 方法加载数据 + if (!loadData()) { + LOG_E("FBXPipeline: Failed to load data"); + return; + } + + // Lambda to generate LOD settings chain + auto generateLODChain = [&](const PipelineSettings& cfg) -> LODPipelineSettings { + LODPipelineSettings lodOps; + lodOps.enable_lod = cfg.enableLOD; + + SimplificationParams simTemplate; + simTemplate.enable_simplification = true; + simTemplate.target_error = 0.0001f; // Base error + + DracoCompressionParams dracoTemplate; + dracoTemplate.enable_compression = cfg.enableDraco; + + // Use build_lod_levels from lod_pipeline.h + // Ratios are expected to be e.g. [1.0, 0.5, 0.25] + lodOps.levels = build_lod_levels( + cfg.lodRatios, + simTemplate.target_error, + simTemplate, + dracoTemplate, + false // draco_for_lod0 + ); + + return lodOps; + }; + + // Simplification Step (Only if LOD is NOT enabled, otherwise we do it per-level or later) + if (settings.enableSimplify && !settings.enableLOD) { + LOG_I("Simplifying meshes (Global)..."); + SimplificationParams simParams; + simParams.enable_simplification = true; + simParams.target_ratio = 0.5f; // Default ratio + simParams.target_error = 0.0001f; // Base error + + for (auto& pair : loader_->meshPool) { + if (pair.second.geometry) { + simplify_mesh_geometry(pair.second.geometry.get(), simParams); + } + } + } else if (settings.enableLOD) { + // If LOD is enabled, we prepare the settings + LODPipelineSettings lodSettings = generateLODChain(settings); + LOG_I("LOD Enabled. Generated %zu LOD levels configuration.", lodSettings.levels.size()); + } + + LOG_I("Building Octree (Stage 2 - Using OctreeStrategy)..."); + + // Stage 2: Use OctreeStrategy to build spatial index + spatial::strategy::OctreeStrategy octreeStrategy; + spatial::strategy::OctreeConfig octreeConfig; + octreeConfig.maxDepth = settings.maxDepth; + octreeConfig.maxItemsPerNode = settings.maxItemsPerTile; + + // Convert spatialItems_ to SpatialItemList for the strategy + spatial::core::SpatialItemList spatialItemList; + for (const auto& item : spatialItems_) { + spatialItemList.push_back(item); + } + + // Calculate world bounds from spatial items + spatial::core::SpatialBounds worldBounds; + if (!spatialItems_.empty()) { + auto firstBounds = spatialItems_[0]->getBounds(); + std::array min = firstBounds.min(); + std::array max = firstBounds.max(); + + for (const auto& item : spatialItems_) { + auto bounds = item->getBounds(); + for (int i = 0; i < 3; ++i) { + min[i] = std::min(min[i], bounds.min()[i]); + max[i] = std::max(max[i], bounds.max()[i]); + } + } + worldBounds = spatial::core::SpatialBounds(min, max); + } + + // Build the octree index + auto spatialIndex = octreeStrategy.buildIndex(spatialItemList, worldBounds, octreeConfig); + auto* octreeIndex = static_cast(spatialIndex.get()); + + LOG_I("Octree built with %zu nodes and %zu items", + octreeIndex->getNodeCount(), octreeIndex->getItemCount()); + + // Stage 3: Setup B3DMGenerator + LOG_I("Setting up B3DMGenerator (Stage 3)..."); + b3dm::B3DMGeneratorConfig b3dmConfig; + b3dmConfig.centerLongitude = settings.longitude; + b3dmConfig.centerLatitude = settings.latitude; + b3dmConfig.centerHeight = settings.height; + b3dmConfig.enableSimplification = settings.enableSimplify; + b3dmConfig.enableDraco = settings.enableDraco; + b3dmConfig.enableTextureCompress = settings.enableTextureCompress; + + // Create geometry extractor + auto geometryExtractor = std::make_unique(); + b3dmConfig.geometryExtractor = geometryExtractor.get(); + + b3dm::B3DMGenerator b3dmGenerator(b3dmConfig); + + // Build LOD levels configuration + std::vector lodLevels; + if (settings.enableLOD) { + SimplificationParams simTemplate; + simTemplate.enable_simplification = true; + simTemplate.target_error = 0.0001f; + + DracoCompressionParams dracoTemplate; + dracoTemplate.enable_compression = settings.enableDraco; + + lodLevels = build_lod_levels( + settings.lodRatios, + simTemplate.target_error, + simTemplate, + dracoTemplate, + false + ); + LOG_I("Stage 3: Generated %zu LOD levels", lodLevels.size()); + } else { + // Single LOD level (LOD0) + LODLevelSettings lod0; + lod0.target_ratio = 1.0f; + lod0.enable_simplification = false; + lod0.enable_draco = settings.enableDraco; + lodLevels.push_back(lod0); + } + + // Stage 4: Process nodes and build tileset using FBXTilesetAdapter + LOG_I("Processing Nodes and Building Tileset (Stage 4)..."); + + // Collect all node metadata directly from OctreeIndex + fbx::FBXTileMetaMap allMetas; + int nodeIdCounter = 0; + auto* octreeRoot = static_cast(octreeIndex->getRootNode()); + auto rootMeta = processOctreeNode(octreeRoot, settings.outputPath, "0", b3dmGenerator, lodLevels, allMetas, nodeIdCounter); + + if (!rootMeta || allMetas.empty()) { + LOG_E("Stage 4: Failed to process nodes"); + return; + } + + // Create adapter and build tileset + fbx::FBXTilesetAdapterConfig adapterConfig; + adapterConfig.centerLongitude = settings.longitude; + adapterConfig.centerLatitude = settings.latitude; + adapterConfig.centerHeight = settings.height; + adapterConfig.boundingVolumeScale = 1.0; + adapterConfig.geometricErrorScale = settings.geScale; + adapterConfig.enableLOD = settings.enableLOD; + adapterConfig.lodLevelCount = static_cast(lodLevels.size()); + + fbx::FBXTilesetAdapter tilesetAdapter(adapterConfig); + + // Build and write tileset (completely replaces writeTilesetJson) + if (!tilesetAdapter.buildAndWriteTileset(allMetas, settings.outputPath)) { + LOG_E("Stage 4: Failed to build and write tileset"); + return; + } + + LOG_I("FBXPipeline Finished."); +} + +// Process OctreeNode directly to generate tile metadata +fbx::FBXTileMetaPtr FBXPipeline::processOctreeNode( + const spatial::strategy::OctreeNode* node, + const std::string& parentPath, + const std::string& treePath, + b3dm::B3DMGenerator& generator, + const std::vector& lodLevels, + fbx::FBXTileMetaMap& allMetas, + int& nodeIdCounter) { + + // Create TileCoord (using depth and position) + int x = 0, y = 0, z = 0; + if (!treePath.empty()) { + std::stringstream ss(treePath); + std::string token; + int depth = 0; + while (std::getline(ss, token, '_')) { + int index = std::stoi(token); + x = index % 2; + y = (index / 2) % 2; + z = index / 4; + depth++; + } + } + + common::TileCoord coord(static_cast(node->getDepth()), x, y); + auto meta = std::make_shared(coord); + meta->isLeaf = node->isLeaf(); + + // Calculate bounding box (Y-up to Z-up conversion) + auto bounds = node->getBounds3D(); + osg::BoundingBoxd bbox; + bbox.expandBy(osg::Vec3d(bounds.min()[0], -bounds.max()[2], bounds.min()[1])); + bbox.expandBy(osg::Vec3d(bounds.max()[0], -bounds.min()[2], bounds.max()[1])); + meta->setBoundingBox(bbox); + + // Calculate geometric error + double dx = bbox.xMax() - bbox.xMin(); + double dy = bbox.yMax() - bbox.yMin(); + double dz = bbox.zMax() - bbox.zMin(); + meta->geometricError = std::sqrt(dx*dx + dy*dy + dz*dz) / 20.0 * settings.geScale; + + // Generate B3DM if node has content + auto items = node->getItems(); + if (!items.empty()) { + meta->hasGeometry = true; + + // Create tile directory + std::string tileDir = settings.outputPath + "/" + meta->getTileDirectory(); + std::filesystem::create_directories(tileDir); + + // Convert SpatialItemRefList to the format expected by B3DMGenerator + spatial::core::SpatialItemRefList spatialItems; + for (const auto& item : items) { + spatialItems.push_back(item); + } + + if (!spatialItems.empty() && !lodLevels.empty()) { + // Generate B3DM files + std::string tileName = "tile_" + treePath; + auto lodFiles = generator.generateLODFiles(spatialItems, tileDir, tileName, lodLevels); + + // Save LOD file paths + for (const auto& file : lodFiles) { + meta->lodFiles.push_back(meta->getTileDirectory() + "/" + file.filename); + } + + // Set content URI + if (settings.enableLOD && !meta->lodFiles.empty()) { + meta->content.uri = meta->getTilesetPath(); + meta->content.hasContent = true; + } else if (!meta->lodFiles.empty()) { + meta->content.uri = meta->lodFiles[0]; + meta->content.hasContent = true; + } + } + } + + // Save to map + allMetas[meta->key()] = meta; + + // Recursively process children + auto children = node->getChildren(); + for (size_t i = 0; i < children.size(); ++i) { + std::string childTreePath = treePath + "_" + std::to_string(i); + auto* childNode = static_cast(children[i]); + auto childMeta = processOctreeNode( + childNode, + parentPath, + childTreePath, + generator, + lodLevels, + allMetas, + nodeIdCounter + ); + if (childMeta) { + meta->childrenKeys.push_back(childMeta->key()); + } + } + + return meta; +} + +// C-API Implementation +extern "C" void* fbx23dtile( + const char* in_path, + const char* out_path, + double* box_ptr, + int* len, + int max_lvl, + bool enable_texture_compress, + bool enable_meshopt, + bool enable_draco, + bool enable_unlit, + double longitude, + double latitude, + double height, + bool enable_lod +) { + std::cout << "[fbx23dtile] Using unified pipeline" << std::endl; + + // 构建 ConversionParams + pipeline::ConversionParams params; + params.input_path = in_path; + params.output_path = out_path; + params.source_type = "fbx"; + + // 创建 FBX 特定参数 + auto fbxSpecific = std::make_unique(); + fbxSpecific->longitude = longitude; + fbxSpecific->latitude = latitude; + fbxSpecific->height = height; + params.specific = std::move(fbxSpecific); + + // 设置通用选项 + params.options.enable_lod = enable_lod; + params.options.enable_simplify = enable_meshopt; + params.options.enable_draco = enable_draco; + params.options.enable_texture_compress = enable_texture_compress; + params.options.enable_unlit = enable_unlit; + + // 创建管道并执行转换 + auto pipeline = pipeline::PipelineFactory::Instance().Create("fbx"); + if (!pipeline) { + std::cerr << "[fbx23dtile] Failed to create pipeline" << std::endl; + return nullptr; + } + + auto result = pipeline->Convert(params); + if (!result.success) { + std::cerr << "[fbx23dtile] Conversion failed" << std::endl; + return nullptr; + } + + // 读取 tileset.json 返回给 Rust + fs::path tilesetPath = fs::path(out_path) / "tileset.json"; + if (!fs::exists(tilesetPath)) { + std::cerr << "[fbx23dtile] tileset.json not found" << std::endl; + return nullptr; + } + + std::ifstream t(tilesetPath); + std::stringstream buffer; + buffer << t.rdbuf(); + std::string jsonStr = buffer.str(); + + // Parse json to get bounding box + try { + json root = json::parse(jsonStr); + auto& box = root["root"]["boundingVolume"]["box"]; + if (box.is_array() && box.size() == 12) { + double cx = box[0]; + double cy = box[1]; + double cz = box[2]; + double hx = box[3]; + double hy = box[7]; + double hz = box[11]; + + double max[3] = {cx + hx, cy + hy, cz + hz}; + double min[3] = {cx - hx, cy - hy, cz - hz}; + + memcpy(box_ptr, max, 3 * sizeof(double)); + memcpy(box_ptr + 3, min, 3 * sizeof(double)); + } + } catch (const std::exception& e) { + std::cerr << "[fbx23dtile] Failed to parse tileset.json: " << e.what() << std::endl; + } + + void* str = malloc(jsonStr.length() + 1); + if (str) { + memcpy(str, jsonStr.c_str(), jsonStr.length()); + ((char*)str)[jsonStr.length()] = '\0'; + *len = (int)jsonStr.length(); + } + + return str; +} diff --git a/src/fbx/fbx_processor.h b/src/fbx/fbx_processor.h new file mode 100644 index 00000000..4b29edad --- /dev/null +++ b/src/fbx/fbx_processor.h @@ -0,0 +1,143 @@ +#pragma once + +/** + * @file FBXPipeline.h + * @brief FBX Pipeline - 步骤3修改:支持外部 TilesetBuilder + * + * 修改内容: + * - 步骤1:添加 SetDataSource 方法,允许外部注入数据源 + * - 步骤2:添加 SetSpatialIndex 方法,允许外部注入空间索引 + * - 步骤3:添加 SetTilesetBuilder 方法,允许外部注入 TilesetBuilder + * - 保持原有接口不变,确保向后兼容 + */ + +#include "fbx/core/fbx.h" +#include "fbx/fbx_spatial_item_adapter.h" +#include "fbx/fbx_tile_meta.h" +#include "spatial/strategy/octree_strategy.h" +#include "b3dm/b3dm_generator.h" +#include "pipeline/data_source.h" +#include "pipeline/spatial_index.h" +#include "pipeline/tileset_builder.h" +#include +#include +#include +#include +#include +#include + +// Forward declarations +namespace tinygltf { + class Model; +} + +struct PipelineSettings { + std::string inputPath; + std::string outputPath; + int maxDepth = 5; + int maxItemsPerTile = 1000; + + // Optimization flags + bool enableSimplify = false; + bool enableDraco = false; + bool enableTextureCompress = false; // KTX2 + bool enableLOD = false; // Enable Hierarchical LOD generation + bool enableUnlit = false; // Enable KHR_materials_unlit + std::vector lodRatios = {1.0f, 0.5f, 0.25f}; // Default LOD ratios (Fine to Coarse) + + // Geolocation (Origin) + double longitude = 0.0; + double latitude = 0.0; + double height = 0.0; + + // Geometric error scale (multiplier applied to boundingVolume diagonal) + double geScale = 0.5; // Adjusted for better LOD switching with SSE=16 +}; + +class FBXPipeline { +public: + explicit FBXPipeline(const PipelineSettings& settings); + ~FBXPipeline() = default; + + /** + * @brief 设置外部数据源(步骤1新增) + * @param dataSource 外部数据源,如果为 nullptr 则内部加载 + * + * 注意:外部数据源的生命周期必须超过 FBXPipeline 的生命周期 + */ + void SetDataSource(pipeline::DataSource* dataSource); + + /** + * @brief 设置外部空间索引(步骤2新增) + * @param spatialIndex 外部空间索引,如果为 nullptr 则内部构建 + * + * 注意:外部空间索引的生命周期必须超过 FBXPipeline 的生命周期 + */ + void SetSpatialIndex(pipeline::ISpatialIndex* spatialIndex); + + /** + * @brief 设置外部 TilesetBuilder(步骤3新增) + * @param tilesetBuilder 外部 TilesetBuilder,如果为 nullptr 则内部创建 + * + * 注意:外部 TilesetBuilder 的生命周期必须超过 FBXPipeline 的生命周期 + */ + void SetTilesetBuilder(pipeline::ITilesetBuilder* tilesetBuilder); + + void run(); + +private: + PipelineSettings settings; + std::unique_ptr loader_; + + // 数据源(内部创建时使用) + std::unique_ptr dataSource_; + + // 外部数据源(步骤1新增) + pipeline::DataSource* externalDataSource_ = nullptr; + + // 空间索引(内部创建时使用) + std::unique_ptr spatialIndex_; + + // 外部空间索引(步骤2新增) + pipeline::ISpatialIndex* externalSpatialIndex_ = nullptr; + + // TilesetBuilder(内部创建时使用) + std::unique_ptr tilesetBuilder_; + + // 外部 TilesetBuilder(步骤3新增) + pipeline::ITilesetBuilder* externalTilesetBuilder_ = nullptr; + + // 阶段1:空间对象适配器列表 + fbx::FBXSpatialItemList spatialItems_; + + // Process OctreeNode directly to generate tile metadata + fbx::FBXTileMetaPtr processOctreeNode( + const spatial::strategy::OctreeNode* node, + const std::string& parentPath, + const std::string& treePath, + b3dm::B3DMGenerator& generator, + const std::vector& lodLevels, + fbx::FBXTileMetaMap& allMetas, + int& nodeIdCounter + ); + + // 获取当前使用的空间项列表(步骤1新增) + [[nodiscard]] const fbx::FBXSpatialItemList& GetCurrentSpatialItems() const { + return spatialItems_; + } + + // 加载数据(步骤1新增:支持外部数据源) + bool loadData(); + + // 获取当前使用的数据源(新接口,步骤1补充) + [[nodiscard]] pipeline::DataSource* GetCurrentDataSource(); + + // 获取当前使用的空间索引根节点(步骤2新增) + [[nodiscard]] const spatial::strategy::OctreeNode* GetCurrentRootNode() const; + + // 获取当前使用的空间索引(新接口,步骤2补充) + [[nodiscard]] pipeline::ISpatialIndex* GetCurrentSpatialIndex(); + + // 获取当前使用的 TilesetBuilder(步骤3新增) + [[nodiscard]] pipeline::ITilesetBuilder* GetCurrentTilesetBuilder(); +}; diff --git a/src/fbx/fbx_spatial_item_adapter.cpp b/src/fbx/fbx_spatial_item_adapter.cpp new file mode 100644 index 00000000..705aa8df --- /dev/null +++ b/src/fbx/fbx_spatial_item_adapter.cpp @@ -0,0 +1,135 @@ +#include "fbx_spatial_item_adapter.h" +#include "../extern.h" +#include +#include + +namespace fbx { + +FBXSpatialItemAdapter::FBXSpatialItemAdapter(MeshInstanceInfo* meshInfo, int transformIndex) + : meshInfo_(meshInfo) + , transformIndex_(transformIndex) { +} + +spatial::core::SpatialBounds FBXSpatialItemAdapter::getBounds() const { + if (!worldBoundsCache_.has_value()) { + computeWorldBounds(); + } + + const osg::BoundingBox& bbox = worldBoundsCache_.value(); + return spatial::core::SpatialBounds( + std::array{bbox.xMin(), bbox.yMin(), bbox.zMin()}, + std::array{bbox.xMax(), bbox.yMax(), bbox.zMax()} + ); +} + +size_t FBXSpatialItemAdapter::getId() const { + // 使用 meshInfo 指针和 transformIndex 组合生成唯一 ID + size_t meshId = reinterpret_cast(meshInfo_); + return (meshId << 16) | static_cast(transformIndex_); +} + +std::array FBXSpatialItemAdapter::getCenter() const { + if (!worldBoundsCache_.has_value()) { + computeWorldBounds(); + } + + const osg::BoundingBox& bbox = worldBoundsCache_.value(); + return { + (bbox.xMin() + bbox.xMax()) * 0.5, + (bbox.yMin() + bbox.yMax()) * 0.5, + (bbox.zMin() + bbox.zMax()) * 0.5 + }; +} + +osg::Matrixd FBXSpatialItemAdapter::getTransform() const { + if (meshInfo_ && transformIndex_ >= 0 && transformIndex_ < static_cast(meshInfo_->transforms.size())) { + return meshInfo_->transforms[transformIndex_]; + } + return osg::Matrixd::identity(); +} + +std::string FBXSpatialItemAdapter::getNodeName() const { + if (meshInfo_ && transformIndex_ >= 0 && transformIndex_ < static_cast(meshInfo_->nodeNames.size())) { + return meshInfo_->nodeNames[transformIndex_]; + } + return ""; +} + +const osg::Geometry* FBXSpatialItemAdapter::getGeometry() const { + if (meshInfo_) { + return meshInfo_->geometry.get(); + } + return nullptr; +} + +void FBXSpatialItemAdapter::computeWorldBounds() const { + worldBoundsCache_ = osg::BoundingBox(); + + if (!meshInfo_ || !meshInfo_->geometry) { + return; + } + + // 获取局部包围盒 + osg::BoundingBox localBounds; + const osg::Geometry* geom = meshInfo_->geometry.get(); + + if (geom->getVertexArray()) { + osg::ComputeBoundsVisitor cbv; + const_cast(geom)->accept(cbv); + localBounds = cbv.getBoundingBox(); + } + + // 应用世界变换 + osg::Matrixd transform = getTransform(); + + // 变换包围盒的8个角点 + osg::BoundingBox worldBounds; + for (int i = 0; i < 8; ++i) { + osg::Vec3d corner( + (i & 1) ? localBounds.xMax() : localBounds.xMin(), + (i & 2) ? localBounds.yMax() : localBounds.yMin(), + (i & 4) ? localBounds.zMax() : localBounds.zMin() + ); + worldBounds.expandBy(corner * transform); + } + + worldBoundsCache_ = worldBounds; +} + +FBXSpatialItemList createSpatialItems(FBXLoader* loader) { + FBXSpatialItemList items; + + if (!loader) { + return items; + } + + // 遍历 meshPool,为每个 transform 创建适配器 + for (auto& pair : loader->meshPool) { + MeshInstanceInfo& meshInfo = pair.second; + + // 获取该mesh的材质扩展数据(如果有) + const MaterialExtensionData* matExtData = nullptr; + if (meshInfo.geometry && meshInfo.geometry->getStateSet()) { + const osg::StateSet* stateSet = meshInfo.geometry->getStateSet(); + auto it = loader->stateSetExtensionCache.find(stateSet); + if (it != loader->stateSetExtensionCache.end()) { + matExtData = &(it->second); + } + } + + // 为每个变换实例创建适配器 + for (int i = 0; i < static_cast(meshInfo.transforms.size()); ++i) { + auto adapter = std::make_shared(&meshInfo, i); + // 设置材质扩展数据(所有实例共享相同的材质) + if (matExtData) { + adapter->setMaterialExtensionData(matExtData); + } + items.push_back(adapter); + } + } + + LOG_I("Created %zu spatial items from FBX loader", items.size()); + return items; +} + +} // namespace fbx diff --git a/src/fbx/fbx_spatial_item_adapter.h b/src/fbx/fbx_spatial_item_adapter.h new file mode 100644 index 00000000..e231e769 --- /dev/null +++ b/src/fbx/fbx_spatial_item_adapter.h @@ -0,0 +1,77 @@ +#pragma once + +/** + * @file fbx/fbx_spatial_item_adapter.h + * @brief FBX 空间项适配器 + * + * 阶段1迁移组件:将 FBX 的 MeshInstanceInfo 适配为空间索引接口 + */ + +#include "../spatial/core/spatial_item.h" +#include "../spatial/core/spatial_bounds.h" +#include "fbx/core/fbx.h" +#include +#include +#include +#include + +namespace fbx { + +/** + * @brief FBX 空间项适配器 + * + * 将 FBX 的 MeshInstanceInfo 包装为空间索引可用的 SpatialItem 接口 + */ +class FBXSpatialItemAdapter : public spatial::core::SpatialItem { +public: + /** + * @brief 构造函数 + * @param meshInfo Mesh 实例信息 + * @param transformIndex 变换矩阵索引 + */ + FBXSpatialItemAdapter(MeshInstanceInfo* meshInfo, int transformIndex); + + // SpatialItem 接口实现 + spatial::core::SpatialBounds getBounds() const override; + size_t getId() const override; + std::array getCenter() const override; + + // FBX 特有接口 + MeshInstanceInfo* getMeshInfo() const { return meshInfo_; } + int getTransformIndex() const { return transformIndex_; } + osg::Matrixd getTransform() const; + std::string getNodeName() const; + const osg::Geometry* getGeometry() const; + + /** + * @brief 获取材质扩展数据 + * @return 材质扩展数据指针,如果没有返回nullptr + */ + const MaterialExtensionData* getMaterialExtensionData() const { return materialExtData_; } + + /** + * @brief 设置材质扩展数据 + * @param data 材质扩展数据指针(由外部管理生命周期) + */ + void setMaterialExtensionData(const MaterialExtensionData* data) { materialExtData_ = data; } + +private: + MeshInstanceInfo* meshInfo_; + int transformIndex_; + mutable std::optional worldBoundsCache_; + const MaterialExtensionData* materialExtData_ = nullptr; // 材质扩展数据(不拥有所有权) + + void computeWorldBounds() const; +}; + +using FBXSpatialItemPtr = std::shared_ptr; +using FBXSpatialItemList = std::vector; + +/** + * @brief 从 FBXLoader 创建所有空间对象适配器 + * @param loader FBX 加载器 + * @return 空间对象适配器列表 + */ +FBXSpatialItemList createSpatialItems(FBXLoader* loader); + +} // namespace fbx diff --git a/src/fbx/fbx_tile_meta.h b/src/fbx/fbx_tile_meta.h new file mode 100644 index 00000000..a52d4ca8 --- /dev/null +++ b/src/fbx/fbx_tile_meta.h @@ -0,0 +1,81 @@ +#pragma once + +/** + * @file fbx/fbx_tile_meta.h + * @brief FBX瓦片元数据 + * + * 继承common::TileMeta,添加FBX特有的属性 + */ + +#include "../common/tile_meta.h" +#include +#include + +namespace fbx { + +/** + * @brief FBX瓦片元数据 + * + * 扩展通用TileMeta,添加FBX特有的包围盒和路径信息 + */ +class FBXTileMeta : public common::TileMeta { +public: + // FBX特有的包围盒(ENU坐标系,米) + osg::BoundingBoxd bbox; + + // B3DM文件路径(相对于输出根目录) + std::string b3dmPath; + + // 是否有实际几何内容 + bool hasGeometry = false; + + // LOD文件列表(如果启用了LOD) + std::vector lodFiles; + + FBXTileMeta() = default; + explicit FBXTileMeta(const common::TileCoord& c) : common::TileMeta(c) {} + + /** + * @brief 从osg::BoundingBoxd设置包围盒 + */ + void setBoundingBox(const osg::BoundingBoxd& box) { + bbox = box; + // 同时更新基类的BoundingBox (common::BoundingBox使用minX/maxX等命名) + this->common::TileMeta::bbox.minX = box.xMin(); + this->common::TileMeta::bbox.maxX = box.xMax(); + this->common::TileMeta::bbox.minY = box.yMin(); + this->common::TileMeta::bbox.maxY = box.yMax(); + this->common::TileMeta::bbox.minZ = box.zMin(); + this->common::TileMeta::bbox.maxZ = box.zMax(); + } + + /** + * @brief 获取瓦片目录路径(用于创建目录) + */ + std::string getTileDirectory() const { + return "tile/" + std::to_string(coord.z) + "/" + + std::to_string(coord.x) + "/" + std::to_string(coord.y); + } + + /** + * @brief 获取子tileset的相对路径 + */ + std::string getTilesetPath() const override { + return getTileDirectory() + "/tileset.json"; + } + + /** + * @brief 获取B3DM文件的相对路径 + */ + std::string getB3DMPath(int lodLevel = 0) const { + if (lodLevel >= 0 && lodLevel < static_cast(lodFiles.size())) { + return lodFiles[lodLevel]; + } + return getTileDirectory() + "/content_lod" + std::to_string(lodLevel) + ".b3dm"; + } +}; + +using FBXTileMetaPtr = std::shared_ptr; +using FBXTileMetaMap = std::unordered_map; + +} // namespace fbx diff --git a/src/fbx/fbx_tile_meta_converter.cpp b/src/fbx/fbx_tile_meta_converter.cpp new file mode 100644 index 00000000..a549d4bb --- /dev/null +++ b/src/fbx/fbx_tile_meta_converter.cpp @@ -0,0 +1,169 @@ +/** + * @file fbx/fbx_tile_meta_converter.cpp + * @brief FBX瓦片元数据转换器实现 + */ + +#include "fbx_tile_meta_converter.h" +#include "../extern.h" +#include + +namespace fbx { + +// 辅助函数:将SpatialBounds转换为osg::BoundingBoxd +static osg::BoundingBoxd spatialBoundsToOSG(const spatial::core::SpatialBounds& bounds) { + auto min = bounds.min(); + auto max = bounds.max(); + return osg::BoundingBoxd(min[0], min[1], min[2], max[0], max[1], max[2]); +} + +// 辅助函数:计算节点在层级中的位置索引 +static void computeNodePosition(const spatial::strategy::OctreeNode* node, int& x, int& y) { + // 对于八叉树,我们使用简化的位置编码 + // 基于节点在父节点中的索引计算位置 + x = 0; + y = 0; + auto* parent = node->getParent(); + if (parent) { + for (int i = 0; i < 8; ++i) { + if (parent->getChild(i) == node) { + x = i % 2; + y = (i / 2) % 2; + break; + } + } + } +} + +std::pair FBXTileMetaConverter::convert( + const spatial::strategy::OctreeStrategy& strategy, + const Config& config) { + + FBXTileMetaMap allMetas; + int nodeIdCounter = 0; + + // OctreeStrategy没有getRootNode方法,需要通过其他方式获取根节点 + // 这里我们需要修改策略来暴露根节点,或者使用不同的方法 + // 暂时返回空,需要在FBXPipeline中直接处理 + + return {nullptr, allMetas}; +} + +FBXTileMetaPtr FBXTileMetaConverter::convertNodeRecursive( + const spatial::strategy::OctreeNode* node, + FBXTileMetaMap& allMetas, + const Config& config, + int& nodeIdCounter) { + + if (!node) { + return nullptr; + } + + // 计算节点位置 + int x, y; + computeNodePosition(node, x, y); + + // 创建TileCoord + common::TileCoord coord(static_cast(node->getDepth()), x, y); + auto meta = std::make_shared(coord); + + // 设置包围盒 + auto bounds = spatialBoundsToOSG(node->getBounds3D()); + meta->setBoundingBox(bounds); + + // 计算几何误差 + meta->geometricError = computeGeometricError(bounds); + + // 判断是否为叶子节点 + meta->isLeaf = node->isLeaf(); + + // 如果有内容,生成B3DM + auto items = node->getItems(); + if (!items.empty()) { + meta->hasGeometry = true; + if (config.generator) { + generateB3DMForNode(meta, node, config); + } + } + + // 递归处理子节点 + auto children = node->getChildren(); + for (const auto* child : children) { + if (child) { + auto childMeta = convertNodeRecursive( + static_cast(child), + allMetas, config, nodeIdCounter); + if (childMeta) { + meta->childrenKeys.push_back(childMeta->key()); + } + } + } + + // 保存到映射表 + allMetas[meta->key()] = meta; + + return meta; +} + +void FBXTileMetaConverter::generateB3DMForNode( + FBXTileMetaPtr& meta, + const spatial::strategy::OctreeNode* node, + const Config& config) { + + auto items = node->getItems(); + if (!config.generator || items.empty()) { + return; + } + + // 创建瓦片目录 + std::string tileDir = config.outputPath + "/" + meta->getTileDirectory(); + std::filesystem::create_directories(tileDir); + + // 转换空间项引用 + spatial::core::SpatialItemRefList spatialItems; + for (const auto& item : items) { + spatialItems.push_back(item); + } + + if (spatialItems.empty()) { + return; + } + + // 生成LOD文件 + std::string tileName = "tile_" + std::to_string(node->getDepth()) + "_" + + std::to_string(meta->coord.x) + "_" + std::to_string(meta->coord.y); + + auto lodFiles = config.generator->generateLODFiles( + spatialItems, + tileDir, + tileName, + config.lodLevels + ); + + // 保存LOD文件路径 + for (const auto& file : lodFiles) { + meta->lodFiles.push_back(meta->getTileDirectory() + "/" + file.filename); + } + + // 设置内容URI(第一个LOD文件或子tileset) + if (config.enableLOD && !meta->lodFiles.empty()) { + // 启用LOD时,内容指向子tileset + meta->content.uri = meta->getTilesetPath(); + meta->content.hasContent = true; + } else if (!meta->lodFiles.empty()) { + // 不启用LOD时,直接指向B3DM文件 + meta->content.uri = meta->lodFiles[0]; + meta->content.hasContent = true; + } +} + +double FBXTileMetaConverter::computeGeometricError(const osg::BoundingBoxd& bbox) { + // 基于包围盒对角线计算几何误差 + double dx = bbox.xMax() - bbox.xMin(); + double dy = bbox.yMax() - bbox.yMin(); + double dz = bbox.zMax() - bbox.zMin(); + + double diagonal = std::sqrt(dx * dx + dy * dy + dz * dz); + return diagonal / 20.0; // 与shapefile使用相同的比例 +} + +} // namespace fbx diff --git a/src/fbx/fbx_tile_meta_converter.h b/src/fbx/fbx_tile_meta_converter.h new file mode 100644 index 00000000..27f17a19 --- /dev/null +++ b/src/fbx/fbx_tile_meta_converter.h @@ -0,0 +1,72 @@ +#pragma once + +/** + * @file fbx/fbx_tile_meta_converter.h + * @brief FBX瓦片元数据转换器 + * + * 将OctreeStrategy节点转换为TileMeta结构,复用common::TilesetBuilder + */ + +#include "fbx_tile_meta.h" +#include "fbx_spatial_item_adapter.h" +#include "../spatial/strategy/octree_strategy.h" +#include "../b3dm/b3dm_generator.h" +#include +#include + +namespace fbx { + +/** + * @brief FBX瓦片元数据转换器 + * + * 将OctreeStrategy节点转换为TileMeta结构,支持LOD生成 + */ +class FBXTileMetaConverter { +public: + /** + * @brief 转换配置 + */ + struct Config { + std::string outputPath; // 输出根目录 + bool enableLOD = false; // 是否启用LOD + std::vector lodLevels; // LOD配置 + b3dm::B3DMGenerator* generator = nullptr; // B3DM生成器(非拥有指针,由调用方管理生命周期) + }; + + /** + * @brief 转换八叉树为TileMeta映射表 + * @param strategy 八叉树策略 + * @param config 转换配置 + * @return 根节点元数据 + 所有节点映射表 + */ + static std::pair convert( + const spatial::strategy::OctreeStrategy& strategy, + const Config& config); + +private: + static FBXTileMetaPtr convertNodeRecursive( + const spatial::strategy::OctreeNode* node, + FBXTileMetaMap& allMetas, + const Config& config, + int& nodeIdCounter); + + /** + * @brief 为节点生成B3DM文件和LOD + * @param meta 瓦片元数据 + * @param node 八叉树节点 + * @param config 转换配置 + */ + static void generateB3DMForNode( + FBXTileMetaPtr& meta, + const spatial::strategy::OctreeNode* node, + const Config& config); + + /** + * @brief 计算几何误差 + * @param bbox 包围盒 + * @return 几何误差值 + */ + static double computeGeometricError(const osg::BoundingBoxd& bbox); +}; + +} // namespace fbx diff --git a/src/fbx/fbx_tileset_adapter.cpp b/src/fbx/fbx_tileset_adapter.cpp new file mode 100644 index 00000000..aeb21d07 --- /dev/null +++ b/src/fbx/fbx_tileset_adapter.cpp @@ -0,0 +1,233 @@ +/** + * @file fbx/fbx_tileset_adapter.cpp + * @brief FBX Tileset适配器实现 + */ + +#include "fbx_tileset_adapter.h" +#include "../extern.h" +#include "../tileset/tileset_writer.h" +#include + +namespace fbx { + +FBXTilesetAdapter::FBXTilesetAdapter(const FBXTilesetAdapterConfig& config) + : config_(config) {} + +bool FBXTilesetAdapter::buildAndWriteTileset( + const FBXTileMetaMap& allMetas, + const std::string& outputPath) { + + if (allMetas.empty()) { + LOG_E("FBXTilesetAdapter: No tile metadata available"); + return false; + } + + // 查找根节点 + auto rootMeta = findRootNode(allMetas); + if (!rootMeta) { + LOG_E("FBXTilesetAdapter: No root node found"); + return false; + } + + // 为所有叶子节点生成子tileset(如果启用LOD) + if (config_.enableLOD) { + for (const auto& [key, meta] : allMetas) { + if (meta->isLeaf && meta->hasGeometry) { + if (!generateLeafTileset(meta, outputPath)) { + LOG_E("FBXTilesetAdapter: Failed to generate leaf tileset for node %lu", key); + return false; + } + } + } + } + + // 构建根tileset + tileset::Tileset tileset; + tileset.setVersion("1.0"); + tileset.setGltfUpAxis("Z"); + + // 构建根节点 + tileset::Tile rootTile = buildTileRecursive(rootMeta, allMetas); + + // 添加根节点变换矩阵(ENU到ECEF) + tileset::TransformMatrix transform = createRootTransform(); + rootTile.setTransform(transform); + + // 设置根几何误差 + double rootGe = rootMeta->geometricError > 0.0 + ? rootMeta->geometricError + : computeGeometricError(rootMeta->bbox) * 2.0; + tileset.geometricError = rootGe; + tileset.root = std::move(rootTile); + + // 写入文件 + std::filesystem::path tilesetPath = std::filesystem::path(outputPath) / "tileset.json"; + tileset::TilesetWriter writer; + + if (!writer.writeToFile(tileset, tilesetPath.string())) { + LOG_E("FBXTilesetAdapter: Failed to write tileset to %s", tilesetPath.string().c_str()); + return false; + } + + return true; +} + +bool FBXTilesetAdapter::generateLeafTileset( + const FBXTileMetaPtr& meta, + const std::string& outputPath) { + + if (!meta || meta->lodFiles.empty()) { + return true; // 没有LOD文件,不需要生成子tileset + } + + // 创建叶子节点的tileset + tileset::Tileset leafTileset; + leafTileset.setVersion("1.0"); + leafTileset.setGltfUpAxis("Z"); + + // 计算几何误差 + double geometricError = meta->geometricError > 0.0 + ? meta->geometricError + : computeGeometricError(meta->bbox); + + // 创建根tile(对应LOD0) + tileset::Tile rootTile; + rootTile.geometricError = geometricError; + rootTile.refine = "REPLACE"; + + // 设置包围盒 + rootTile.boundingVolume = convertBoundingBox(meta->bbox); + + // 创建LOD层级结构:LOD0 -> LOD1 -> LOD2 + // 在子tileset中,content URI应该是相对于子tileset所在目录的路径 + std::vector> lodLevels; + for (size_t i = 0; i < meta->lodFiles.size(); ++i) { + double ge = geometricError * (1.0 - i * 0.25); // LOD误差递减 + // 提取文件名(去掉目录路径) + std::string filename = std::filesystem::path(meta->lodFiles[i]).filename().string(); + lodLevels.emplace_back(filename, ge); + } + + // 构建层级结构 + tileset::Tile* currentParent = &rootTile; + for (size_t i = 0; i < lodLevels.size(); ++i) { + const auto& [content, geError] = lodLevels[i]; + + tileset::Tile lodTile; + lodTile.boundingVolume = rootTile.boundingVolume; + lodTile.geometricError = geError; + lodTile.refine = "REPLACE"; + lodTile.setContent(content); + + // 如果不是最后一个LOD,需要继续添加子节点 + if (i < lodLevels.size() - 1) { + currentParent->addChild(std::move(lodTile)); + currentParent = ¤tParent->children.back(); + } else { + // 最后一个LOD,直接添加为叶子节点 + currentParent->addChild(std::move(lodTile)); + } + } + + leafTileset.root = std::move(rootTile); + leafTileset.updateGeometricError(); + + // 写入文件 + std::filesystem::path tilesetDir = std::filesystem::path(outputPath) / meta->getTileDirectory(); + std::filesystem::create_directories(tilesetDir); + std::filesystem::path tilesetPath = tilesetDir / "tileset.json"; + + tileset::TilesetWriter writer; + return writer.writeToFile(leafTileset, tilesetPath.string()); +} + +tileset::Box FBXTilesetAdapter::convertBoundingBox(const osg::BoundingBoxd& bbox) const { + // 计算中心点 + double centerX = (bbox.xMin() + bbox.xMax()) * 0.5; + double centerY = (bbox.yMin() + bbox.yMax()) * 0.5; + double centerZ = (bbox.zMin() + bbox.zMax()) * 0.5; + + // 计算半轴长度 + double halfX = (bbox.xMax() - bbox.xMin()) * 0.5 * config_.boundingVolumeScale; + double halfY = (bbox.yMax() - bbox.yMin()) * 0.5 * config_.boundingVolumeScale; + double halfZ = (bbox.zMax() - bbox.zMin()) * 0.5 * config_.boundingVolumeScale; + + // 创建Box(中心点 + 半轴长度) + return tileset::Box::fromCenterAndHalfLengths(centerX, centerY, centerZ, halfX, halfY, halfZ); +} + +double FBXTilesetAdapter::computeGeometricError(const osg::BoundingBoxd& bbox) const { + // 基于包围盒对角线计算几何误差 + double dx = bbox.xMax() - bbox.xMin(); + double dy = bbox.yMax() - bbox.yMin(); + double dz = bbox.zMax() - bbox.zMin(); + + double diagonal = std::sqrt(dx * dx + dy * dy + dz * dz); + return diagonal / 20.0 * config_.geometricErrorScale; +} + +tileset::TransformMatrix FBXTilesetAdapter::createRootTransform() const { + // 使用CoordinateTransformer计算ENU->ECEF变换矩阵 + glm::dmat4 enuToEcef = coords::CoordinateTransformer::CalcEnuToEcefMatrix( + config_.centerLongitude, config_.centerLatitude, config_.centerHeight); + + // 转换为tileset::TransformMatrix (std::array) + tileset::TransformMatrix matrix; + for (int c = 0; c < 4; ++c) { + for (int r = 0; r < 4; ++r) { + matrix[c * 4 + r] = enuToEcef[c][r]; + } + } + return matrix; +} + +FBXTileMetaPtr FBXTilesetAdapter::findRootNode(const FBXTileMetaMap& allMetas) const { + for (const auto& [key, meta] : allMetas) { + if (meta->isRoot()) { + return meta; + } + } + // 如果没有找到根节点,返回第一个节点 + if (!allMetas.empty()) { + return allMetas.begin()->second; + } + return nullptr; +} + +tileset::Tile FBXTilesetAdapter::buildTileRecursive( + const FBXTileMetaPtr& meta, + const FBXTileMetaMap& allMetas) const { + + tileset::Tile tile; + + // 设置包围体 + tile.boundingVolume = convertBoundingBox(meta->bbox); + + // 设置几何误差 + tile.geometricError = meta->geometricError > 0.0 + ? meta->geometricError + : computeGeometricError(meta->bbox); + + // 设置细化策略 + tile.refine = "REPLACE"; + + // 设置内容 + if (meta->content.hasContent) { + tile.setContent(meta->content.uri); + } + + // 递归构建子节点 + if (!meta->isLeaf && !meta->childrenKeys.empty()) { + for (uint64_t childKey : meta->childrenKeys) { + auto it = allMetas.find(childKey); + if (it != allMetas.end()) { + tileset::Tile childTile = buildTileRecursive(it->second, allMetas); + tile.addChild(std::move(childTile)); + } + } + } + + return tile; +} + +} // namespace fbx diff --git a/src/fbx/fbx_tileset_adapter.h b/src/fbx/fbx_tileset_adapter.h new file mode 100644 index 00000000..37a51d17 --- /dev/null +++ b/src/fbx/fbx_tileset_adapter.h @@ -0,0 +1,112 @@ +#pragma once + +/** + * @file fbx/fbx_tileset_adapter.h + * @brief FBX Tileset适配器 + * + * 整合TilesetBuilder生成最终的tileset.json + * 复用common::TilesetBuilder,与shapefile保持一致 + */ + +#include "fbx_tile_meta.h" +#include "fbx_tile_meta_converter.h" +#include "../common/tileset_builder.h" +#include "../tileset/tileset_types.h" +#include "../tileset/bounding_volume.h" +#include "../coords/coordinate_transformer.h" +#include +#include +#include +#include + +namespace fbx { + +/** + * @brief FBX Tileset适配器配置 + */ +struct FBXTilesetAdapterConfig { + double centerLongitude = 0.0; // 中心经度(度) + double centerLatitude = 0.0; // 中心纬度(度) + double centerHeight = 0.0; // 中心高度(米) + double boundingVolumeScale = 1.0; // 包围盒扩展系数 + double geometricErrorScale = 0.5; // 几何误差缩放系数 + bool enableLOD = false; // 是否启用LOD + int lodLevelCount = 3; // LOD级别数量 + + /** + * @brief 转换为TilesetBuilderConfig + */ + common::TilesetBuilderConfig toBuilderConfig() const { + common::TilesetBuilderConfig config; + config.boundingVolumeScale = boundingVolumeScale; + config.childGeometricErrorMultiplier = geometricErrorScale; + config.enableLOD = enableLOD; + config.lodLevelCount = lodLevelCount; + config.refine = "REPLACE"; + return config; + } +}; + +/** + * @brief FBX Tileset适配器 + * + * 整合TilesetBuilder生成最终的tileset.json + */ +class FBXTilesetAdapter { +public: + explicit FBXTilesetAdapter(const FBXTilesetAdapterConfig& config); + + /** + * @brief 构建并写入Tileset + * + * @param allMetas 所有节点的元数据映射表 + * @param outputPath 输出路径 + * @return 是否成功 + */ + bool buildAndWriteTileset( + const FBXTileMetaMap& allMetas, + const std::string& outputPath); + + /** + * @brief 为叶子节点生成子tileset(包含LOD层级) + * + * @param meta 叶子节点元数据 + * @param outputPath 输出根目录 + * @return 是否成功 + */ + bool generateLeafTileset( + const FBXTileMetaPtr& meta, + const std::string& outputPath); + +private: + FBXTilesetAdapterConfig config_; + + /** + * @brief 将FBX包围盒转换为tileset::Box + */ + tileset::Box convertBoundingBox(const osg::BoundingBoxd& bbox) const; + + /** + * @brief 计算几何误差 + */ + double computeGeometricError(const osg::BoundingBoxd& bbox) const; + + /** + * @brief 创建根节点变换矩阵(ENU到ECEF) + */ + tileset::TransformMatrix createRootTransform() const; + + /** + * @brief 查找根节点 + */ + FBXTileMetaPtr findRootNode(const FBXTileMetaMap& allMetas) const; + + /** + * @brief 递归构建Tile层次结构 + */ + tileset::Tile buildTileRecursive( + const FBXTileMetaPtr& meta, + const FBXTileMetaMap& allMetas) const; +}; + +} // namespace fbx diff --git a/src/fun_c.rs b/src/fun_c.rs index 5f2d5e8a..eb112e86 100644 --- a/src/fun_c.rs +++ b/src/fun_c.rs @@ -1,12 +1,17 @@ +/// # Safety +/// This function is unsafe because it dereferences raw pointers. +/// The caller must ensure that: +/// - `file_name` is a valid, null-terminated C string +/// - `buf` is a valid pointer to at least `buf_len` bytes #[no_mangle] -pub extern "C" fn write_file(file_name: *const libc::c_char, buf: *const u8, buf_len: u32) -> bool { +pub unsafe extern "C" fn write_file(file_name: *const libc::c_char, buf: *const u8, buf_len: u32) -> bool { use std::ffi; use std::fs; use std::fs::File; use std::io::prelude::*; use std::slice; - unsafe { + { if let Ok(file_name) = ffi::CStr::from_ptr(file_name).to_str() { use std::path::Path; let path = Path::new(file_name); @@ -37,11 +42,14 @@ pub extern "C" fn write_file(file_name: *const libc::c_char, buf: *const u8, buf } } +/// # Safety +/// This function is unsafe because it dereferences a raw pointer. +/// The caller must ensure that `path` is a valid, null-terminated C string. #[no_mangle] -pub extern "C" fn mkdirs(path: *const libc::c_char) -> bool { +pub unsafe extern "C" fn mkdirs(path: *const libc::c_char) -> bool { use std::ffi; use std::fs; - unsafe { + { match ffi::CStr::from_ptr(path).to_str() { Ok(buf) => match fs::create_dir_all(buf) { Ok(_) => true, diff --git a/src/gltf/extension_manager.h b/src/gltf/extension_manager.h new file mode 100644 index 00000000..f56646a6 --- /dev/null +++ b/src/gltf/extension_manager.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace gltf { + +class ExtensionManager { +public: + ExtensionManager() = default; + + void use(const std::string& name) { used_.insert(name); } + void require(const std::string& name) { required_.insert(name); } + + void useAndRequire(const std::string& name) { + use(name); + require(name); + } + + void apply(tinygltf::Model& model) const { + for (const auto& ext : used_) { + if (std::find(model.extensionsUsed.begin(), + model.extensionsUsed.end(), ext) == model.extensionsUsed.end()) { + model.extensionsUsed.push_back(ext); + } + } + for (const auto& ext : required_) { + if (std::find(model.extensionsRequired.begin(), + model.extensionsRequired.end(), ext) == model.extensionsRequired.end()) { + model.extensionsRequired.push_back(ext); + } + } + } + + bool isUsed(const std::string& name) const { return used_.count(name) > 0; } + bool isRequired(const std::string& name) const { return required_.count(name) > 0; } + + const std::set& used() const { return used_; } + const std::set& required() const { return required_; } + + void clear() { + used_.clear(); + required_.clear(); + } + +private: + std::set used_; + std::set required_; +}; + +} // namespace gltf diff --git a/src/gltf/extensions/basisu.h b/src/gltf/extensions/basisu.h new file mode 100644 index 00000000..8840dd2a --- /dev/null +++ b/src/gltf/extensions/basisu.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include "../extension_manager.h" + +namespace gltf::extensions { + +inline void applyBasisu(tinygltf::Texture& texture, int source_index, ExtensionManager& ext_mgr) { + tinygltf::Value::Object ext_obj; + ext_obj["source"] = tinygltf::Value(source_index); + texture.extensions["KHR_texture_basisu"] = tinygltf::Value(ext_obj); + texture.source = -1; + ext_mgr.useAndRequire("KHR_texture_basisu"); +} + +inline bool hasBasisu(const tinygltf::Texture& texture) { + return texture.extensions.count("KHR_texture_basisu") > 0; +} + +inline int getBasisuSource(const tinygltf::Texture& texture) { + auto it = texture.extensions.find("KHR_texture_basisu"); + if (it != texture.extensions.end()) { + const auto& ext = it->second; + if (ext.IsObject()) { + auto src_it = ext.Get().find("source"); + if (src_it != ext.Get().end() && src_it->second.IsInt()) { + return src_it->second.Get(); + } + } + } + return -1; +} + +} // namespace gltf::extensions diff --git a/src/gltf/extensions/draco.h b/src/gltf/extensions/draco.h new file mode 100644 index 00000000..9ca0c865 --- /dev/null +++ b/src/gltf/extensions/draco.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include "../extension_manager.h" + +namespace gltf::extensions { + +struct DracoCompression { + int buffer_view = -1; + std::unordered_map attributes; + + static DracoCompression FromBufferView(int bv, + const std::unordered_map& attrs) { + DracoCompression dc; + dc.buffer_view = bv; + dc.attributes = attrs; + return dc; + } + + DracoCompression& addAttribute(const std::string& name, int id) { + attributes[name] = id; + return *this; + } +}; + +inline void applyDracoCompression(tinygltf::Primitive& primitive, + const DracoCompression& draco, + ExtensionManager& ext_mgr) { + tinygltf::Value::Object ext_obj; + ext_obj["bufferView"] = tinygltf::Value(draco.buffer_view); + + tinygltf::Value::Object attrs_obj; + for (const auto& [name, id] : draco.attributes) { + attrs_obj[name] = tinygltf::Value(id); + } + ext_obj["attributes"] = tinygltf::Value(attrs_obj); + + primitive.extensions["KHR_draco_mesh_compression"] = tinygltf::Value(ext_obj); + ext_mgr.useAndRequire("KHR_draco_mesh_compression"); +} + +inline bool hasDracoCompression(const tinygltf::Primitive& primitive) { + return primitive.extensions.count("KHR_draco_mesh_compression") > 0; +} + +} // namespace gltf::extensions diff --git a/src/gltf/extensions/specular_glossiness.h b/src/gltf/extensions/specular_glossiness.h new file mode 100644 index 00000000..3e965c19 --- /dev/null +++ b/src/gltf/extensions/specular_glossiness.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include "../extension_manager.h" + +namespace gltf::extensions { + +struct SpecularGlossiness { + std::array diffuse_factor = {1.0, 1.0, 1.0, 1.0}; + std::array specular_factor = {1.0, 1.0, 1.0}; + double glossiness_factor = 1.0; + + int diffuse_texture = -1; + int specular_glossiness_texture = -1; + + static SpecularGlossiness Default() { return {}; } + + static SpecularGlossiness FromDiffuse(const std::array& color) { + SpecularGlossiness sg; + sg.diffuse_factor = color; + return sg; + } + + static SpecularGlossiness FromDiffuse(double r, double g, double b, double a = 1.0) { + SpecularGlossiness sg; + sg.diffuse_factor = {r, g, b, a}; + return sg; + } + + static SpecularGlossiness FromSpecular(const std::array& specular, + double glossiness) { + SpecularGlossiness sg; + sg.specular_factor = specular; + sg.glossiness_factor = glossiness; + return sg; + } + + static SpecularGlossiness FromSpecular(double sr, double sg, double sb, + double glossiness) { + SpecularGlossiness spec; + spec.specular_factor = {sr, sg, sb}; + spec.glossiness_factor = glossiness; + return spec; + } + + static SpecularGlossiness Full(double dr, double dg, double db, double da, + double sr, double sg, double sb, + double glossiness) { + SpecularGlossiness spec; + spec.diffuse_factor = {dr, dg, db, da}; + spec.specular_factor = {sr, sg, sb}; + spec.glossiness_factor = glossiness; + return spec; + } +}; + +inline void applySpecularGlossiness(tinygltf::Material& material, + const SpecularGlossiness& sg, + ExtensionManager& ext_mgr) { + tinygltf::Value::Object ext_obj; + + ext_obj["diffuseFactor"] = tinygltf::Value(tinygltf::Value::Array{ + tinygltf::Value(sg.diffuse_factor[0]), + tinygltf::Value(sg.diffuse_factor[1]), + tinygltf::Value(sg.diffuse_factor[2]), + tinygltf::Value(sg.diffuse_factor[3]) + }); + + ext_obj["specularFactor"] = tinygltf::Value(tinygltf::Value::Array{ + tinygltf::Value(sg.specular_factor[0]), + tinygltf::Value(sg.specular_factor[1]), + tinygltf::Value(sg.specular_factor[2]) + }); + + ext_obj["glossinessFactor"] = tinygltf::Value(sg.glossiness_factor); + + if (sg.diffuse_texture >= 0) { + tinygltf::Value::Object tex_info; + tex_info["index"] = tinygltf::Value(sg.diffuse_texture); + ext_obj["diffuseTexture"] = tinygltf::Value(tex_info); + } + + if (sg.specular_glossiness_texture >= 0) { + tinygltf::Value::Object tex_info; + tex_info["index"] = tinygltf::Value(sg.specular_glossiness_texture); + ext_obj["specularGlossinessTexture"] = tinygltf::Value(tex_info); + } + + material.extensions["KHR_materials_pbrSpecularGlossiness"] = tinygltf::Value(ext_obj); + ext_mgr.use("KHR_materials_pbrSpecularGlossiness"); +} + +} // namespace gltf::extensions diff --git a/src/gltf/extensions/texture_transform.h b/src/gltf/extensions/texture_transform.h new file mode 100644 index 00000000..6e8217d7 --- /dev/null +++ b/src/gltf/extensions/texture_transform.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include "../extension_manager.h" + +namespace gltf::extensions { + +struct TextureTransform { + std::array offset = {0.0f, 0.0f}; + float rotation = 0.0f; + std::array scale = {1.0f, 1.0f}; + int tex_coord = 0; + + static TextureTransform Identity() { return {}; } + + static TextureTransform WithOffset(float u, float v) { + TextureTransform t; + t.offset = {u, v}; + return t; + } + + static TextureTransform WithScale(float u, float v) { + TextureTransform t; + t.scale = {u, v}; + return t; + } + + static TextureTransform WithRotation(float radians) { + TextureTransform t; + t.rotation = radians; + return t; + } + + static TextureTransform WithOffsetAndScale(float offset_u, float offset_v, + float scale_u, float scale_v) { + TextureTransform t; + t.offset = {offset_u, offset_v}; + t.scale = {scale_u, scale_v}; + return t; + } +}; + +inline void applyTextureTransform(tinygltf::Material& material, + const std::string& texture_key, + const TextureTransform& transform, + ExtensionManager& ext_mgr) { + tinygltf::Value::Object ext_obj; + ext_obj["offset"] = tinygltf::Value(tinygltf::Value::Array{ + tinygltf::Value(static_cast(transform.offset[0])), + tinygltf::Value(static_cast(transform.offset[1])) + }); + ext_obj["rotation"] = tinygltf::Value(static_cast(transform.rotation)); + ext_obj["scale"] = tinygltf::Value(tinygltf::Value::Array{ + tinygltf::Value(static_cast(transform.scale[0])), + tinygltf::Value(static_cast(transform.scale[1])) + }); + if (transform.tex_coord != 0) { + ext_obj["texCoord"] = tinygltf::Value(transform.tex_coord); + } + + if (texture_key == "baseColorTexture") { + material.pbrMetallicRoughness.baseColorTexture.extensions["KHR_texture_transform"] = + tinygltf::Value(ext_obj); + } else if (texture_key == "normalTexture") { + material.normalTexture.extensions["KHR_texture_transform"] = tinygltf::Value(ext_obj); + } else if (texture_key == "emissiveTexture") { + material.emissiveTexture.extensions["KHR_texture_transform"] = tinygltf::Value(ext_obj); + } else if (texture_key == "occlusionTexture") { + material.occlusionTexture.extensions["KHR_texture_transform"] = tinygltf::Value(ext_obj); + } else if (texture_key == "metallicRoughnessTexture") { + material.pbrMetallicRoughness.metallicRoughnessTexture.extensions["KHR_texture_transform"] = + tinygltf::Value(ext_obj); + } + + ext_mgr.use("KHR_texture_transform"); +} + +} // namespace gltf::extensions diff --git a/src/gltf/extensions/unlit.h b/src/gltf/extensions/unlit.h new file mode 100644 index 00000000..26504c54 --- /dev/null +++ b/src/gltf/extensions/unlit.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include "../extension_manager.h" + +namespace gltf::extensions { + +inline void applyUnlit(tinygltf::Material& material, ExtensionManager& ext_mgr) { + material.extensions["KHR_materials_unlit"] = tinygltf::Value(tinygltf::Value::Object()); + ext_mgr.use("KHR_materials_unlit"); +} + +inline bool hasUnlit(const tinygltf::Material& material) { + return material.extensions.count("KHR_materials_unlit") > 0; +} + +} // namespace gltf::extensions diff --git a/src/gltf/gltf_builder.cpp b/src/gltf/gltf_builder.cpp new file mode 100644 index 00000000..fd4f1bbf --- /dev/null +++ b/src/gltf/gltf_builder.cpp @@ -0,0 +1,289 @@ +#include "gltf_builder.h" +#include "../utils/log.h" + +#include +#include +#include + +namespace gltf { + +GLTFBuilder::GLTFBuilder(const GLTFBuilderConfig& config) + : config_(config) { +} + +GLTFBuildResult GLTFBuilder::build(const std::vector& instances) { + GLTFBuildResult result; + + if (instances.empty()) { + return result; + } + + // 创建模型 + tinygltf::Model model; + tinygltf::Buffer buffer; + buffer.data.reserve(1024 * 1024); // 预分配1MB + + // 提取几何体数据 + std::vector positions; + std::vector normals; + std::vector texcoords; + std::vector indices; + std::vector batchIds; + osg::BoundingBoxd bounds; + + // 这里简化处理,实际需要从instances提取geometry + // 暂时返回失败,需要外部提供geometry + LOG_W("GLTFBuilder::build requires geometry data from instances"); + + result.success = false; + return result; +} + +GLTFBuildResult GLTFBuilder::buildWithMaterialGrouping( + const std::vector& instances, + const std::vector& geometries) { + + GLTFBuildResult result; + + if (instances.empty() || geometries.empty()) { + return result; + } + + // 创建模型和缓冲区 + tinygltf::Model model; + tinygltf::Buffer buffer; + buffer.data.reserve(1024 * 1024); + + // 构建几何体数据 + std::vector positions; + std::vector normals; + std::vector texcoords; + std::vector indices; + std::vector batchIds; + osg::BoundingBoxd bounds; + + if (!buildGeometries(instances, geometries, positions, normals, texcoords, indices, batchIds, bounds)) { + LOG_E("Failed to build geometries"); + return result; + } + + // 构建材质 + std::vector materialIndices; + if (!buildMaterials(geometries, model, buffer, materialIndices)) { + LOG_W("Failed to build materials, using default"); + } + + // 创建Primitive + PrimitiveBuilder primBuilder; + primBuilder.addVertices(positions); + primBuilder.addNormals(normals); + primBuilder.addTexcoords(texcoords); + primBuilder.addIndices(indices); + + if (!materialIndices.empty() && materialIndices[0] >= 0) { + primBuilder.setMaterial(materialIndices[0]); + } + + tinygltf::Primitive primitive = primBuilder.build(model, buffer); + + // 创建Mesh + tinygltf::Mesh mesh; + mesh.primitives.push_back(primitive); + model.meshes.push_back(mesh); + + // 创建Node + tinygltf::Node node; + node.mesh = 0; + model.nodes.push_back(node); + + // 创建Scene + tinygltf::Scene scene; + scene.nodes.push_back(0); + model.scenes.push_back(scene); + model.defaultScene = 0; + + // 添加缓冲区 + model.buffers.push_back(buffer); + + // 应用扩展 + applyExtensions(model); + + // 序列化为GLB + if (!serializeToGLB(model, result.glbData)) { + LOG_E("Failed to serialize GLTF to GLB"); + return result; + } + + result.success = true; + result.bounds = bounds; + result.vertexCount = positions.size() / 3; + result.triangleCount = indices.size() / 3; + + return result; +} + +bool GLTFBuilder::buildGeometries( + const std::vector& instances, + const std::vector& geometries, + std::vector& positions, + std::vector& normals, + std::vector& texcoords, + std::vector& indices, + std::vector& batchIds, + osg::BoundingBoxd& bounds) { + + // 预分配空间 + size_t estimatedVertices = 0; + size_t estimatedIndices = 0; + for (size_t i = 0; i < geometries.size() && i < instances.size(); ++i) { + if (geometries[i]) { + estimatedVertices += geometries[i]->getVertexArray() ? geometries[i]->getVertexArray()->getNumElements() : 0; + for (unsigned int j = 0; j < geometries[i]->getNumPrimitiveSets(); ++j) { + estimatedIndices += geometries[i]->getPrimitiveSet(j)->getNumIndices(); + } + } + } + + positions.reserve(estimatedVertices * 3); + normals.reserve(estimatedVertices * 3); + texcoords.reserve(estimatedVertices * 2); + indices.reserve(estimatedIndices); + batchIds.reserve(estimatedVertices); + + // 处理每个实例 + uint32_t baseIndex = 0; + for (size_t i = 0; i < instances.size() && i < geometries.size(); ++i) { + const auto& inst = instances[i]; + osg::Geometry* geom = geometries[i]; + + if (!geom) continue; + + // 计算法线变换矩阵 + osg::Matrixd normalMatrix = osg::utils::GeometryUtils::computeNormalMatrix(inst.matrix); + + // 提取几何体数据 + size_t vertexCount = osg::utils::GeometryUtils::extractGeometryData( + geom, + inst.matrix, + normalMatrix, + positions, + normals, + texcoords, + baseIndex + ); + + // 处理索引 + for (unsigned int j = 0; j < geom->getNumPrimitiveSets(); ++j) { + osg::PrimitiveSet* ps = geom->getPrimitiveSet(j); + osg::utils::GeometryUtils::processPrimitiveSet(ps, baseIndex, indices); + } + + // 添加batch ID(用于B3DM) + for (size_t v = 0; v < vertexCount; ++v) { + batchIds.push_back(static_cast(inst.originalBatchId)); + } + + // 更新包围盒 + if (geom->getBound().valid()) { + bounds.expandBy(geom->getBound()); + } + + baseIndex += static_cast(vertexCount); + } + + return !positions.empty() && !indices.empty(); +} + +bool GLTFBuilder::buildMaterials( + const std::vector& geometries, + tinygltf::Model& model, + tinygltf::Buffer& buffer, + std::vector& materialIndices) { + + materialIndices.clear(); + materialIndices.reserve(geometries.size()); + + for (osg::Geometry* geom : geometries) { + int matIdx = buildMaterialFromGeometry(geom, model, buffer); + materialIndices.push_back(matIdx); + } + + return true; +} + +int GLTFBuilder::buildMaterialFromGeometry( + osg::Geometry* geom, + tinygltf::Model& model, + tinygltf::Buffer& buffer) { + + const osg::StateSet* stateSet = geom ? geom->getStateSet() : nullptr; + + // 提取PBR参数 + osg::utils::PBRParams pbrParams; + osg::utils::MaterialUtils::extractPBRParams(stateSet, pbrParams); + + // 创建材质构建器 + MaterialBuilder matBuilder; + matBuilder.setBaseColor(pbrParams.baseColor); + matBuilder.setPBRParams(pbrParams.roughnessFactor, pbrParams.metallicFactor); + matBuilder.setEmissiveColor({pbrParams.emissiveColor[0], pbrParams.emissiveColor[1], pbrParams.emissiveColor[2]}); + matBuilder.setDoubleSided(config_.doubleSided); + matBuilder.setUnlit(config_.enableUnlit); + + // 处理纹理 + const osg::Texture* baseColorTex = osg::utils::MaterialUtils::getBaseColorTexture(stateSet); + if (baseColorTex) { + auto texResult = osg::utils::TextureUtils::processTexture(baseColorTex, config_.enableKTX2); + if (texResult.success) { + bool useBasisu = (texResult.mimeType == "image/ktx2"); + if (useBasisu) { + extMgr_.useAndRequire("KHR_texture_basisu"); + } + int texIdx = osg::utils::TextureUtils::addImageToModel( + model, buffer, texResult.data, texResult.mimeType, useBasisu); + matBuilder.setBaseColorTexture(texIdx); + + if (texResult.hasAlpha) { + matBuilder.setAlphaMode("BLEND"); + } + } + } + + const osg::Texture* normalTex = osg::utils::MaterialUtils::getNormalTexture(stateSet); + if (normalTex) { + auto texResult = osg::utils::TextureUtils::processTexture(normalTex, config_.enableKTX2); + if (texResult.success) { + bool useBasisu = (texResult.mimeType == "image/ktx2"); + if (useBasisu) { + extMgr_.useAndRequire("KHR_texture_basisu"); + } + int texIdx = osg::utils::TextureUtils::addImageToModel( + model, buffer, texResult.data, texResult.mimeType, useBasisu); + matBuilder.setNormalTexture(texIdx); + } + } + + // 构建材质 + return matBuilder.build(model, extMgr_); +} + +bool GLTFBuilder::serializeToGLB( + tinygltf::Model& model, + std::vector& outGlbData) { + + tinygltf::TinyGLTF gltf; + std::ostringstream ss; + if (!gltf.WriteGltfSceneToStream(&model, ss, false, true)) { + return false; + } + + std::string glbStr = ss.str(); + outGlbData.assign(glbStr.begin(), glbStr.end()); + return !outGlbData.empty(); +} + +void GLTFBuilder::applyExtensions(tinygltf::Model& model) { + extMgr_.apply(model); +} + +} // namespace gltf diff --git a/src/gltf/gltf_builder.h b/src/gltf/gltf_builder.h new file mode 100644 index 00000000..45a67bf3 --- /dev/null +++ b/src/gltf/gltf_builder.h @@ -0,0 +1,145 @@ +#pragma once + +/** + * @file gltf/gltf_builder.h + * @brief GLTF构建器 + * + * 替代appendGeometryToModel的核心类 + * 协调GeometryUtils、MaterialUtils、TextureUtils和gltf_writer模块 + */ + +#include "../osg/utils/geometry_utils.h" +#include "../osg/utils/material_utils.h" +#include "../osg/utils/texture_utils.h" +#include "primitive_builder.h" +#include "material_builder.h" +#include "extension_manager.h" +#include "../common/mesh_processor.h" +#include +#include +#include +#include +#include + +namespace gltf { + +/** + * @brief GLTF构建器配置 + */ +struct GLTFBuilderConfig { + // Draco压缩 + bool enableDraco = false; + struct DracoCompressionParams { + int positionQuantizationBits = 14; + int normalQuantizationBits = 10; + int texcoordQuantizationBits = 12; + int compressionLevel = 7; + } dracoParams; + + // KTX2纹理压缩 + bool enableKTX2 = false; + + // Unlit材质 + bool enableUnlit = false; + + // 双面渲染 + bool doubleSided = true; +}; + +/** + * @brief 实例引用(与FBXPipeline::InstanceRef兼容) + */ +struct InstanceRef { + void* meshInfo; // MeshInstanceInfo指针 + int transformIndex; // 变换索引 + osg::Matrixd matrix; // 世界变换矩阵 + int originalBatchId; // 原始批次ID +}; + +/** + * @brief GLTF构建结果 + */ +struct GLTFBuildResult { + bool success = false; + std::vector glbData; + osg::BoundingBoxd bounds; + size_t vertexCount = 0; + size_t triangleCount = 0; +}; + +/** + * @brief GLTF构建器 + * + * 替代appendGeometryToModel,提供清晰的职责分离: + * - GeometryUtils: 几何体处理 + * - MaterialUtils: 材质参数提取 + * - TextureUtils: 纹理处理 + * - gltf_writer: GLTF格式构建 + */ +class GLTFBuilder { +public: + explicit GLTFBuilder(const GLTFBuilderConfig& config); + + /** + * @brief 构建GLTF模型 + * + * 主构建函数,替代appendGeometryToModel + * + * @param instances 实例引用列表 + * @return 构建结果 + */ + GLTFBuildResult build(const std::vector& instances); + + /** + * @brief 构建GLTF模型(带材质分组) + * + * 根据StateSet分组处理,保持材质一致性 + * + * @param instances 实例引用列表 + * @param geometries 对应的几何体列表 + * @return 构建结果 + */ + GLTFBuildResult buildWithMaterialGrouping( + const std::vector& instances, + const std::vector& geometries + ); + +private: + GLTFBuilderConfig config_; + ExtensionManager extMgr_; + + // 构建步骤 + bool buildGeometries( + const std::vector& instances, + const std::vector& geometries, + std::vector& positions, + std::vector& normals, + std::vector& texcoords, + std::vector& indices, + std::vector& batchIds, + osg::BoundingBoxd& bounds + ); + + bool buildMaterials( + const std::vector& geometries, + tinygltf::Model& model, + tinygltf::Buffer& buffer, + std::vector& materialIndices + ); + + int buildMaterialFromGeometry( + osg::Geometry* geom, + tinygltf::Model& model, + tinygltf::Buffer& buffer + ); + + bool serializeToGLB( + tinygltf::Model& model, + std::vector& outGlbData + ); + + // 应用扩展 + void applyExtensions(tinygltf::Model& model); +}; + +} // namespace gltf diff --git a/src/gltf/gltf_writer.h b/src/gltf/gltf_writer.h new file mode 100644 index 00000000..ad8ddd06 --- /dev/null +++ b/src/gltf/gltf_writer.h @@ -0,0 +1,22 @@ +#pragma once + +/** + * @file gltf/gltf_writer.h + * @brief GLTF Writer 统一头文件 + * + * 包含所有GLTF相关组件: + * - 扩展管理器 + * - 扩展(Draco, Basisu, Unlit, TextureTransform, SpecularGlossiness) + * - 构建器(PrimitiveBuilder, MaterialBuilder) + * - GLTFBuilder(高级API) + */ + +#include "extension_manager.h" +#include "extensions/texture_transform.h" +#include "extensions/specular_glossiness.h" +#include "extensions/unlit.h" +#include "extensions/draco.h" +#include "extensions/basisu.h" +#include "primitive_builder.h" +#include "material_builder.h" +#include "gltf_builder.h" diff --git a/src/gltf/material_builder.cpp b/src/gltf/material_builder.cpp new file mode 100644 index 00000000..4d52b6ba --- /dev/null +++ b/src/gltf/material_builder.cpp @@ -0,0 +1,197 @@ +#include "material_builder.h" + +namespace gltf { + +MaterialBuilder::MaterialBuilder() + : baseColor_({1.0, 1.0, 1.0, 1.0}) + , roughnessFactor_(1.0f) + , metallicFactor_(0.0f) + , baseColorTexture_(-1) + , normalTexture_(-1) + , emissiveTexture_(-1) + , emissiveColor_({0.0, 0.0, 0.0}) + , unlit_(false) + , doubleSided_(true) + , alphaMode_("OPAQUE") { +} + +void MaterialBuilder::setBaseColor(const std::vector& color) { + baseColor_ = color; + if (baseColor_.size() < 4) { + baseColor_.resize(4, 1.0); + } +} + +void MaterialBuilder::setBaseColorTexture(int textureIndex) { + baseColorTexture_ = textureIndex; +} + +void MaterialBuilder::setNormalTexture(int textureIndex) { + normalTexture_ = textureIndex; +} + +void MaterialBuilder::setEmissiveTexture(int textureIndex) { + emissiveTexture_ = textureIndex; +} + +void MaterialBuilder::setMetallicRoughnessTexture(int textureIndex) { + metallicRoughnessTexture_ = textureIndex; +} + +void MaterialBuilder::setOcclusionTexture(int textureIndex) { + occlusionTexture_ = textureIndex; +} + +void MaterialBuilder::setPBRParams(float roughness, float metallic) { + roughnessFactor_ = roughness; + metallicFactor_ = metallic; +} + +void MaterialBuilder::setEmissiveColor(const std::vector& color) { + emissiveColor_ = color; + if (emissiveColor_.size() < 3) { + emissiveColor_.resize(3, 0.0); + } +} + +void MaterialBuilder::setUnlit(bool unlit) { + unlit_ = unlit; +} + +void MaterialBuilder::setDoubleSided(bool doubleSided) { + doubleSided_ = doubleSided; +} + +void MaterialBuilder::setAlphaMode(const std::string& alphaMode) { + alphaMode_ = alphaMode; +} + +void MaterialBuilder::setAlphaCutoff(float alphaCutoff) { + alphaCutoff_ = alphaCutoff; +} + +void MaterialBuilder::setBaseColorTextureTransform(const extensions::TextureTransform& transform) { + baseColorTransform_ = transform; +} + +void MaterialBuilder::setNormalTextureTransform(const extensions::TextureTransform& transform) { + normalTransform_ = transform; +} + +void MaterialBuilder::setEmissiveTextureTransform(const extensions::TextureTransform& transform) { + emissiveTransform_ = transform; +} + +void MaterialBuilder::setMetallicRoughnessTextureTransform(const extensions::TextureTransform& transform) { + metallicRoughnessTransform_ = transform; +} + +void MaterialBuilder::setOcclusionTextureTransform(const extensions::TextureTransform& transform) { + occlusionTransform_ = transform; +} + +void MaterialBuilder::setOcclusionStrength(float strength) { + occlusionStrength_ = strength; +} + +void MaterialBuilder::setSpecularGlossiness(const extensions::SpecularGlossiness& sg) { + specularGlossiness_ = sg; +} + +void MaterialBuilder::clear() { + baseColor_ = {1.0, 1.0, 1.0, 1.0}; + roughnessFactor_ = 1.0f; + metallicFactor_ = 0.0f; + baseColorTexture_ = -1; + normalTexture_ = -1; + emissiveTexture_ = -1; + metallicRoughnessTexture_ = -1; + occlusionTexture_ = -1; + baseColorTransform_.reset(); + normalTransform_.reset(); + emissiveTransform_.reset(); + metallicRoughnessTransform_.reset(); + occlusionTransform_.reset(); + emissiveColor_ = {0.0, 0.0, 0.0}; + unlit_ = false; + doubleSided_ = true; + alphaMode_ = "OPAQUE"; + alphaCutoff_ = 0.5f; + occlusionStrength_ = 1.0f; + specularGlossiness_.reset(); +} + +int MaterialBuilder::build(tinygltf::Model& model, ExtensionManager& extMgr) { + tinygltf::Material material; + material.name = "Material"; + material.doubleSided = doubleSided_; + material.alphaMode = alphaMode_; + material.alphaCutoff = alphaCutoff_; + + // 设置PBR参数 + material.pbrMetallicRoughness.baseColorFactor = baseColor_; + material.pbrMetallicRoughness.roughnessFactor = roughnessFactor_; + material.pbrMetallicRoughness.metallicFactor = metallicFactor_; + + // 设置基础颜色纹理 + if (baseColorTexture_ >= 0) { + material.pbrMetallicRoughness.baseColorTexture.index = baseColorTexture_; + // 应用纹理变换 + if (baseColorTransform_) { + extensions::applyTextureTransform(material, "baseColorTexture", *baseColorTransform_, extMgr); + } + } + + // 设置金属度粗糙度纹理 + if (metallicRoughnessTexture_ >= 0) { + material.pbrMetallicRoughness.metallicRoughnessTexture.index = metallicRoughnessTexture_; + if (metallicRoughnessTransform_) { + extensions::applyTextureTransform(material, "metallicRoughnessTexture", *metallicRoughnessTransform_, extMgr); + } + } + + // 设置法线纹理 + if (normalTexture_ >= 0) { + material.normalTexture.index = normalTexture_; + if (normalTransform_) { + extensions::applyTextureTransform(material, "normalTexture", *normalTransform_, extMgr); + } + } + + // 设置环境光遮蔽纹理 + if (occlusionTexture_ >= 0) { + material.occlusionTexture.index = occlusionTexture_; + material.occlusionTexture.strength = occlusionStrength_; + if (occlusionTransform_) { + extensions::applyTextureTransform(material, "occlusionTexture", *occlusionTransform_, extMgr); + } + } + + // 设置自发光 + if (emissiveTexture_ >= 0) { + material.emissiveTexture.index = emissiveTexture_; + if (emissiveTransform_) { + extensions::applyTextureTransform(material, "emissiveTexture", *emissiveTransform_, extMgr); + } + } + if (emissiveColor_[0] > 0.0 || emissiveColor_[1] > 0.0 || emissiveColor_[2] > 0.0) { + material.emissiveFactor = emissiveColor_; + } + + // 设置Unlit扩展 + if (unlit_) { + material.extensions["KHR_materials_unlit"] = tinygltf::Value(tinygltf::Value::Object()); + extMgr.use("KHR_materials_unlit"); + } + + // 应用Specular-Glossiness扩展 + if (specularGlossiness_) { + extensions::applySpecularGlossiness(material, *specularGlossiness_, extMgr); + } + + int index = static_cast(model.materials.size()); + model.materials.push_back(material); + return index; +} + +} // namespace gltf diff --git a/src/gltf/material_builder.h b/src/gltf/material_builder.h new file mode 100644 index 00000000..3dcef846 --- /dev/null +++ b/src/gltf/material_builder.h @@ -0,0 +1,191 @@ +#pragma once + +/** + * @file gltf/material_builder.h + * @brief GLTF Material构建器 + * + * 封装Material构建逻辑,简化GLTF材质创建 + */ + +#include "types.h" +#include "extension_manager.h" +#include "extensions/texture_transform.h" +#include "extensions/specular_glossiness.h" +#include +#include +#include + +namespace gltf { + +/** + * @brief 材质构建器 + * + * 简化GLTF Material的创建过程 + */ +class MaterialBuilder { +public: + MaterialBuilder(); + + /** + * @brief 设置基础颜色 + * @param color 基础颜色 [r,g,b,a] + */ + void setBaseColor(const std::vector& color); + + /** + * @brief 设置基础颜色纹理 + * @param textureIndex 纹理索引 + */ + void setBaseColorTexture(int textureIndex); + + /** + * @brief 设置法线纹理 + * @param textureIndex 纹理索引 + */ + void setNormalTexture(int textureIndex); + + /** + * @brief 设置自发光纹理 + * @param textureIndex 纹理索引 + */ + void setEmissiveTexture(int textureIndex); + + /** + * @brief 设置金属度粗糙度纹理 + * @param textureIndex 纹理索引 + */ + void setMetallicRoughnessTexture(int textureIndex); + + /** + * @brief 设置环境光遮蔽纹理 + * @param textureIndex 纹理索引 + */ + void setOcclusionTexture(int textureIndex); + + /** + * @brief 设置PBR参数 + * @param roughness 粗糙度 [0,1] + * @param metallic 金属度 [0,1] + */ + void setPBRParams(float roughness, float metallic); + + /** + * @brief 设置自发光颜色 + * @param color 自发光颜色 [r,g,b] + */ + void setEmissiveColor(const std::vector& color); + + /** + * @brief 设置Unlit扩展 + * @param unlit 是否启用Unlit + */ + void setUnlit(bool unlit); + + /** + * @brief 设置双面渲染 + * @param doubleSided 是否双面渲染 + */ + void setDoubleSided(bool doubleSided); + + /** + * @brief 设置Alpha模式 + * @param alphaMode Alpha模式 (OPAQUE, MASK, BLEND) + */ + void setAlphaMode(const std::string& alphaMode); + + /** + * @brief 设置Alpha裁剪值 + * @param alphaCutoff Alpha裁剪值 + */ + void setAlphaCutoff(float alphaCutoff); + + /** + * @brief 设置基础颜色纹理变换 + * @param transform 纹理变换 + */ + void setBaseColorTextureTransform(const extensions::TextureTransform& transform); + + /** + * @brief 设置法线纹理变换 + * @param transform 纹理变换 + */ + void setNormalTextureTransform(const extensions::TextureTransform& transform); + + /** + * @brief 设置自发光纹理变换 + * @param transform 纹理变换 + */ + void setEmissiveTextureTransform(const extensions::TextureTransform& transform); + + /** + * @brief 设置金属度粗糙度纹理变换 + * @param transform 纹理变换 + */ + void setMetallicRoughnessTextureTransform(const extensions::TextureTransform& transform); + + /** + * @brief 设置环境光遮蔽纹理变换 + * @param transform 纹理变换 + */ + void setOcclusionTextureTransform(const extensions::TextureTransform& transform); + + /** + * @brief 设置环境光遮蔽强度 + * @param strength 强度值 [0,1] + */ + void setOcclusionStrength(float strength); + + /** + * @brief 设置Specular-Glossiness参数 + * @param sg Specular-Glossiness参数 + */ + void setSpecularGlossiness(const extensions::SpecularGlossiness& sg); + + /** + * @brief 构建Material + * @param model GLTF模型 + * @param extMgr 扩展管理器(用于记录使用的扩展) + * @return 材质索引 + */ + int build(tinygltf::Model& model, ExtensionManager& extMgr); + + /** + * @brief 清空数据 + */ + void clear(); + +private: + // PBR参数 + std::vector baseColor_ = {1.0, 1.0, 1.0, 1.0}; + float roughnessFactor_ = 1.0f; + float metallicFactor_ = 0.0f; + + // 纹理索引 + int baseColorTexture_ = -1; + int normalTexture_ = -1; + int emissiveTexture_ = -1; + int metallicRoughnessTexture_ = -1; + int occlusionTexture_ = -1; + + // 纹理变换 + std::optional baseColorTransform_; + std::optional normalTransform_; + std::optional emissiveTransform_; + std::optional metallicRoughnessTransform_; + std::optional occlusionTransform_; + + // 自发光颜色 + std::vector emissiveColor_ = {0.0, 0.0, 0.0}; + + // 配置 + bool unlit_ = false; + bool doubleSided_ = true; + std::string alphaMode_ = "OPAQUE"; + float alphaCutoff_ = 0.5f; + float occlusionStrength_ = 1.0f; + + // Specular-Glossiness + std::optional specularGlossiness_; +}; + +} // namespace gltf diff --git a/src/gltf/primitive_builder.cpp b/src/gltf/primitive_builder.cpp new file mode 100644 index 00000000..934a26b0 --- /dev/null +++ b/src/gltf/primitive_builder.cpp @@ -0,0 +1,199 @@ +#include "primitive_builder.h" +#include +#include + +namespace gltf { + +PrimitiveBuilder::PrimitiveBuilder() + : materialIndex_(-1) + , mode_(PrimitiveMode::Triangles) + , vertexCount_(0) { +} + +void PrimitiveBuilder::addVertices(const std::vector& positions) { + positions_.insert(positions_.end(), positions.begin(), positions.end()); + // 更新顶点数量(每个顶点3个float) + vertexCount_ = positions_.size() / 3; +} + +void PrimitiveBuilder::addNormals(const std::vector& normals) { + normals_.insert(normals_.end(), normals.begin(), normals.end()); +} + +void PrimitiveBuilder::addTexcoords(const std::vector& texcoords) { + texcoords_.insert(texcoords_.end(), texcoords.begin(), texcoords.end()); +} + +void PrimitiveBuilder::addIndices(const std::vector& indices) { + indices_.insert(indices_.end(), indices.begin(), indices.end()); +} + +void PrimitiveBuilder::setMaterial(int materialIndex) { + materialIndex_ = materialIndex; +} + +void PrimitiveBuilder::setMode(PrimitiveMode mode) { + mode_ = mode; +} + +void PrimitiveBuilder::clear() { + positions_.clear(); + normals_.clear(); + texcoords_.clear(); + indices_.clear(); + materialIndex_ = -1; + mode_ = PrimitiveMode::Triangles; + vertexCount_ = 0; +} + +tinygltf::Primitive PrimitiveBuilder::build(tinygltf::Model& model, tinygltf::Buffer& buffer) { + tinygltf::Primitive primitive; + primitive.mode = toTinyGltf(mode_); + + if (materialIndex_ >= 0) { + primitive.material = materialIndex_; + } + + // 计算数据大小 + size_t positionsSize = positions_.size() * sizeof(float); + size_t normalsSize = normals_.size() * sizeof(float); + size_t texcoordsSize = texcoords_.size() * sizeof(float); + size_t indicesSize = indices_.size() * sizeof(uint32_t); + + // 对齐当前缓冲区 + alignBuffer(buffer.data); + size_t bufferStart = buffer.data.size(); + + // 写入位置数据 + size_t positionsOffset = buffer.data.size(); + buffer.data.resize(positionsOffset + positionsSize); + memcpy(buffer.data.data() + positionsOffset, positions_.data(), positionsSize); + + // 写入法线数据 + size_t normalsOffset = buffer.data.size(); + if (normalsSize > 0) { + buffer.data.resize(normalsOffset + normalsSize); + memcpy(buffer.data.data() + normalsOffset, normals_.data(), normalsSize); + } + + // 写入纹理坐标数据 + size_t texcoordsOffset = buffer.data.size(); + if (texcoordsSize > 0) { + buffer.data.resize(texcoordsOffset + texcoordsSize); + memcpy(buffer.data.data() + texcoordsOffset, texcoords_.data(), texcoordsSize); + } + + // 写入索引数据 + size_t indicesOffset = buffer.data.size(); + if (indicesSize > 0) { + buffer.data.resize(indicesOffset + indicesSize); + memcpy(buffer.data.data() + indicesOffset, indices_.data(), indicesSize); + } + + // 创建BufferViews和Accessors + // 位置 + int posBvIndex = createBufferView(model, positionsOffset, positionsSize, BufferViewTarget::ArrayBuffer); + + // 计算min/max + std::vector minPos(3, std::numeric_limits::max()); + std::vector maxPos(3, std::numeric_limits::lowest()); + for (size_t i = 0; i < positions_.size(); i += 3) { + for (int j = 0; j < 3; ++j) { + minPos[j] = std::min(minPos[j], static_cast(positions_[i + j])); + maxPos[j] = std::max(maxPos[j], static_cast(positions_[i + j])); + } + } + + int posAccIndex = createAccessor( + model, posBvIndex, 0, ComponentType::Float, vertexCount_, AccessorType::Vec3, minPos, maxPos); + primitive.attributes["POSITION"] = posAccIndex; + + // 法线 + if (normalsSize > 0) { + int normBvIndex = createBufferView(model, normalsOffset, normalsSize, BufferViewTarget::ArrayBuffer); + int normAccIndex = createAccessor( + model, normBvIndex, 0, ComponentType::Float, vertexCount_, AccessorType::Vec3); + primitive.attributes["NORMAL"] = normAccIndex; + } + + // 纹理坐标 + if (texcoordsSize > 0) { + int texBvIndex = createBufferView(model, texcoordsOffset, texcoordsSize, BufferViewTarget::ArrayBuffer); + int texAccIndex = createAccessor( + model, texBvIndex, 0, ComponentType::Float, vertexCount_, AccessorType::Vec2); + primitive.attributes["TEXCOORD_0"] = texAccIndex; + } + + // 索引 + if (indicesSize > 0) { + int idxBvIndex = createBufferView(model, indicesOffset, indicesSize, BufferViewTarget::ElementArrayBuffer); + int idxAccIndex = createAccessor( + model, idxBvIndex, 0, ComponentType::UnsignedInt, indices_.size(), AccessorType::Scalar); + primitive.indices = idxAccIndex; + } + + return primitive; +} + +void PrimitiveBuilder::alignBuffer(std::vector& buffer) { + while (buffer.size() % 4 != 0) { + buffer.push_back(0); + } +} + +int PrimitiveBuilder::createBufferView( + tinygltf::Model& model, + size_t byteOffset, + size_t byteLength, + BufferViewTarget target) { + + tinygltf::BufferView bv; + bv.buffer = 0; + bv.byteOffset = static_cast(byteOffset); + bv.byteLength = static_cast(byteLength); + bv.target = toTinyGltf(target); + + int index = static_cast(model.bufferViews.size()); + model.bufferViews.push_back(bv); + return index; +} + +int PrimitiveBuilder::createAccessor( + tinygltf::Model& model, + int bufferViewIndex, + size_t byteOffset, + ComponentType componentType, + size_t count, + AccessorType type, + const std::vector& minValues, + const std::vector& maxValues) { + + tinygltf::Accessor acc; + acc.bufferView = bufferViewIndex; + acc.byteOffset = static_cast(byteOffset); + acc.componentType = toTinyGltf(componentType); + acc.count = static_cast(count); + switch (type) { + case AccessorType::Scalar: acc.type = TINYGLTF_TYPE_SCALAR; break; + case AccessorType::Vec2: acc.type = TINYGLTF_TYPE_VEC2; break; + case AccessorType::Vec3: acc.type = TINYGLTF_TYPE_VEC3; break; + case AccessorType::Vec4: acc.type = TINYGLTF_TYPE_VEC4; break; + case AccessorType::Mat2: acc.type = TINYGLTF_TYPE_MAT2; break; + case AccessorType::Mat3: acc.type = TINYGLTF_TYPE_MAT3; break; + case AccessorType::Mat4: acc.type = TINYGLTF_TYPE_MAT4; break; + default: acc.type = TINYGLTF_TYPE_SCALAR; break; + } + + if (!minValues.empty()) { + acc.minValues = minValues; + } + if (!maxValues.empty()) { + acc.maxValues = maxValues; + } + + int index = static_cast(model.accessors.size()); + model.accessors.push_back(acc); + return index; +} + +} // namespace gltf diff --git a/src/gltf/primitive_builder.h b/src/gltf/primitive_builder.h new file mode 100644 index 00000000..a8f6659a --- /dev/null +++ b/src/gltf/primitive_builder.h @@ -0,0 +1,126 @@ +#pragma once + +/** + * @file gltf/primitive_builder.h + * @brief GLTF Primitive构建器 + * + * 封装Primitive构建逻辑,简化GLTF网格创建 + */ + +#include "types.h" +#include +#include +#include + +namespace gltf { + +/** + * @brief Primitive构建器 + * + * 简化GLTF Primitive的创建过程 + */ +class PrimitiveBuilder { +public: + PrimitiveBuilder(); + + /** + * @brief 添加顶点位置 + * @param positions 顶点位置数组 [x,y,z, x,y,z, ...] + */ + void addVertices(const std::vector& positions); + + /** + * @brief 添加法线 + * @param normals 法线数组 [x,y,z, x,y,z, ...] + */ + void addNormals(const std::vector& normals); + + /** + * @brief 添加纹理坐标 + * @param texcoords 纹理坐标数组 [u,v, u,v, ...] + */ + void addTexcoords(const std::vector& texcoords); + + /** + * @brief 添加索引 + * @param indices 索引数组 + */ + void addIndices(const std::vector& indices); + + /** + * @brief 设置材质索引 + * @param materialIndex 材质索引 + */ + void setMaterial(int materialIndex); + + /** + * @brief 设置图元模式 + * @param mode 图元模式(默认TRIANGLES) + */ + void setMode(PrimitiveMode mode); + + /** + * @brief 构建Primitive + * + * 将数据写入模型和缓冲区,返回Primitive + * + * @param model GLTF模型 + * @param buffer GLTF缓冲区 + * @return 构建的Primitive + */ + tinygltf::Primitive build(tinygltf::Model& model, tinygltf::Buffer& buffer); + + /** + * @brief 获取顶点数量 + * @return 顶点数量 + */ + size_t getVertexCount() const { return vertexCount_; } + + /** + * @brief 获取索引数量 + * @return 索引数量 + */ + size_t getIndexCount() const { return indices_.size(); } + + /** + * @brief 清空数据 + */ + void clear(); + +private: + // 输入数据 + std::vector positions_; + std::vector normals_; + std::vector texcoords_; + std::vector indices_; + + // 配置 + int materialIndex_ = -1; + PrimitiveMode mode_ = PrimitiveMode::Triangles; + size_t vertexCount_ = 0; + + // 辅助函数:对齐缓冲区到4字节 + void alignBuffer(std::vector& buffer); + + // 创建BufferView + int createBufferView( + tinygltf::Model& model, + size_t byteOffset, + size_t byteLength, + BufferViewTarget target + ); + + // 创建Accessor + int createAccessor( + tinygltf::Model& model, + int bufferViewIndex, + size_t byteOffset, + ComponentType componentType, + size_t count, + AccessorType type, + const std::vector& minValues = {}, + const std::vector& maxValues = {} + ); +}; + +} // namespace gltf diff --git a/src/gltf/types.h b/src/gltf/types.h new file mode 100644 index 00000000..0aaf3086 --- /dev/null +++ b/src/gltf/types.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +namespace gltf { + +enum class ComponentType : int { + Byte = 5120, + UnsignedByte = 5121, + Short = 5122, + UnsignedShort = 5123, + UnsignedInt = 5125, + Float = 5126 +}; + +enum class AccessorType : int { + Scalar = 1, + Vec2 = 2, + Vec3 = 3, + Vec4 = 4, + Mat2 = 16 | 2, + Mat3 = 36 | 3, + Mat4 = 64 | 4 +}; + +enum class PrimitiveMode : int { + Points = 0, + Lines = 1, + LineLoop = 2, + LineStrip = 3, + Triangles = 4, + TriangleStrip = 5, + TriangleFan = 6 +}; + +enum class BufferViewTarget : int { + None = 0, + ArrayBuffer = 34962, + ElementArrayBuffer = 34963 +}; + +enum class TextureFilter : int { + Nearest = 9728, + Linear = 9729, + NearestMipmapNearest = 9984, + LinearMipmapNearest = 9985, + NearestMipmapLinear = 9986, + LinearMipmapLinear = 9987 +}; + +enum class TextureWrap : int { + ClampToEdge = 33071, + MirroredRepeat = 33648, + Repeat = 10497 +}; + +inline int toTinyGltf(ComponentType t) { return static_cast(t); } +inline int toTinyGltf(AccessorType t) { return static_cast(t); } +inline int toTinyGltf(PrimitiveMode m) { return static_cast(m); } +inline int toTinyGltf(BufferViewTarget t) { return static_cast(t); } + +} // namespace gltf diff --git a/src/lod_pipeline.h b/src/lod_pipeline.h index 2620a4ac..81a1383a 100644 --- a/src/lod_pipeline.h +++ b/src/lod_pipeline.h @@ -3,7 +3,7 @@ #include #include -#include "mesh_processor.h" +#include "common/mesh_processor.h" // Settings for a single LOD output (one b3dm per level) struct LODLevelSettings { diff --git a/src/main.rs b/src/main.rs index 1f394989..04162736 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,10 +11,12 @@ extern crate env_logger; extern crate libc; mod common; +mod error; mod fbx; pub mod fun_c; mod osgb; mod shape; +mod utils; use chrono::prelude::*; use clap::{Arg, ArgAction, Command}; @@ -75,7 +77,7 @@ fn main() { // Setup OSG plugin path for runtime plugin loading setup_osg_environment(); - if let Err(_) = env::var("RUST_LOG") { + if env::var("RUST_LOG").is_err() { unsafe { env::set_var("RUST_LOG", "info") }; } unsafe { env::set_var("RUST_BACKTRACE", "1") }; @@ -87,7 +89,7 @@ fn main() { buf, "{}: {} - {}", record.level(), - dt.format("%Y-%m-%d %H:%M:%S").to_string(), + dt.format("%Y-%m-%d %H:%M:%S"), record.args() ) }) @@ -216,6 +218,12 @@ fn main() { .help("Set the path to geoid data files (egm96-5.pgm, etc.). Default: GEOGRAPHICLIB_GEOID_PATH env or /usr/local/share/GeographicLib/geoids") .num_args(1), ) + .arg( + Arg::new("use-new-pipeline") + .long("use-new-pipeline") + .help("Use new unified pipeline (experimental)") + .action(ArgAction::SetTrue), + ) .get_matches(); let input = matches @@ -263,6 +271,7 @@ fn main() { let enable_texture_compress = matches.get_flag("enable-texture-compress"); let enable_lod = matches.get_flag("enable-lod"); let enable_unlit = matches.get_flag("enable-unlit"); + let use_new_pipeline = matches.get_flag("use-new-pipeline"); if matches.get_flag("verbose") { info!("set program versose on"); @@ -300,6 +309,10 @@ fn main() { let abs_input_buf = in_path.canonicalize().unwrap_or(in_path.to_path_buf()); let input = abs_input_buf.to_str().unwrap(); + if use_new_pipeline { + info!("Using new unified pipeline (experimental)"); + } + match format { "osgb" => { // osgb默认开启material_unlit @@ -342,6 +355,7 @@ fn main() { } } +#[allow(clippy::too_many_arguments)] fn convert_fbx_cmd( input: &str, output: &str, @@ -393,9 +407,6 @@ fn convert_fbx_cmd( info!("Starting FBX conversion: {} -> {}", input, output); info!("Origin: lon={}, lat={}, height={}", longitude, latitude, height_f); - if enable_lod { - warn!("LOD is not supported for FBX; flag will be ignored"); - } if let Err(e) = fbx::convert_fbx( input, @@ -408,6 +419,7 @@ fn convert_fbx_cmd( longitude, latitude, height_f, + enable_lod, ) { error!("FBX conversion failed: {}", e); } else { @@ -514,7 +526,7 @@ fn convert_osgb(src: &str, dest: &str, config: &str, enable_simplify: bool, enab // read and parse if let Ok(mut f) = File::open(&metadata_file) { let mut buffer = String::new(); - if let Ok(_) = f.read_to_string(&mut buffer) { + if f.read_to_string(&mut buffer).is_ok() { info!("metadata.xml content: {}", buffer); // match serde_xml_rs::from_str::(buffer.as_str()) { @@ -526,11 +538,9 @@ fn convert_osgb(src: &str, dest: &str, config: &str, enable_simplify: bool, enab if v[0] == "ENU" { let v1: Vec<&str> = v[1].split(",").collect(); if v1.len() > 1 { - let v1_num = (*v1[0]).parse::(); - let v2_num = v1[1].parse::(); - if v1_num.is_ok() && v2_num.is_ok() { - center_y = v1_num.unwrap(); - center_x = v2_num.unwrap(); + if let (Ok(y), Ok(x)) = (v1[0].parse::(), v1[1].parse::()) { + center_y = y; + center_x = x; // Parse and apply SRSOrigin offset let origin_parts: Vec<&str> = @@ -548,35 +558,31 @@ fn convert_osgb(src: &str, dest: &str, config: &str, enable_simplify: bool, enab }; // Call enu_init to set up GeoTransform for geometry correction - let gdal_data: String = { - use std::path::Path; - let exe_dir = ::std::env::current_exe().unwrap(); - Path::new(&exe_dir) - .parent() - .unwrap() - .join("gdal") - .to_str() - .unwrap() - .into() - }; - let proj_lib: String = { - use std::path::Path; - let exe_dir = ::std::env::current_exe().unwrap(); - Path::new(&exe_dir) - .parent() - .unwrap() - .join("proj") - .to_str() - .unwrap() - .into() + let (gdal_data, proj_lib) = match (utils::get_gdal_data_path(), utils::get_proj_data_path()) { + (Ok(gdal), Ok(proj)) => (gdal, proj), + (Err(e), _) | (_, Err(e)) => { + error!("Failed to get GDAL/PROJ path: {}", e); + return; + } }; unsafe { - use std::ffi::CString; let mut origin_enu = vec![offset_x, offset_y, offset_z]; - let gdal_c_str = CString::new(gdal_data).unwrap(); + let gdal_c_str = match utils::string_to_cstring(gdal_data) { + Ok(s) => s, + Err(e) => { + error!("Invalid GDAL path: {}", e); + return; + } + }; let gdal_ptr = gdal_c_str.as_ptr(); - let proj_c_str = CString::new(proj_lib).unwrap(); + let proj_c_str = match utils::string_to_cstring(proj_lib) { + Ok(s) => s, + Err(e) => { + error!("Invalid PROJ path: {}", e); + return; + } + }; let proj_ptr = proj_c_str.as_ptr(); if !osgb::enu_init(center_x, center_y, origin_enu.as_mut_ptr(), gdal_ptr, proj_ptr) { error!("enu_init failed!"); @@ -608,54 +614,56 @@ fn convert_osgb(src: &str, dest: &str, config: &str, enable_simplify: bool, enab } else if v[0] == "EPSG" { // call gdal to convert if let Ok(srs) = v[1].parse::() { - let mut pt: Vec = metadata + let pt_result: Result, _> = metadata .SRSOrigin .split(",") - .map(|v| v.parse().unwrap()) + .map(|v| v.parse()) .collect(); - if pt.len() >= 2 { - let gdal_data: String = { - use std::path::Path; - let exe_dir = ::std::env::current_exe().unwrap(); - Path::new(&exe_dir) - .parent() - .unwrap() - .join("gdal") - .to_str() - .unwrap() - .into() - }; - let proj_lib: String = { - use std::path::Path; - let exe_dir = ::std::env::current_exe().unwrap(); - Path::new(&exe_dir) - .parent() - .unwrap() - .join("proj") - .to_str() - .unwrap() - .into() - }; - unsafe { - use std::ffi::CString; - let gdal_c_str = CString::new(gdal_data).unwrap(); - let gdal_ptr = gdal_c_str.as_ptr(); - let proj_c_str = CString::new(proj_lib).unwrap(); - let proj_ptr = proj_c_str.as_ptr(); - if osgb::epsg_convert(srs, pt.as_mut_ptr(), gdal_ptr, proj_ptr) { - center_x = pt[0]; - center_y = pt[1]; - // Use the geoid-corrected height from GeoTransform (if geoid is initialized) - // This handles the conversion from orthometric height (China 1985) to ellipsoidal height (WGS84) - let geo_origin_height = osgb::get_geo_origin_height(); - origin_height = Some(geo_origin_height); - info!("epsg: x->{}, y->{}, h={} (geoid-corrected from original h={})", pt[0], pt[1], geo_origin_height, pt[2]); - } else { - error!("epsg convert failed!"); + match pt_result { + Ok(mut pt) if pt.len() >= 2 => { + let (gdal_data, proj_lib) = match (utils::get_gdal_data_path(), utils::get_proj_data_path()) { + (Ok(gdal), Ok(proj)) => (gdal, proj), + (Err(e), _) | (_, Err(e)) => { + error!("Failed to get GDAL/PROJ path: {}", e); + return; + } + }; + unsafe { + let gdal_c_str = match utils::string_to_cstring(gdal_data) { + Ok(s) => s, + Err(e) => { + error!("Invalid GDAL path: {}", e); + return; + } + }; + let gdal_ptr = gdal_c_str.as_ptr(); + let proj_c_str = match utils::string_to_cstring(proj_lib) { + Ok(s) => s, + Err(e) => { + error!("Invalid PROJ path: {}", e); + return; + } + }; + let proj_ptr = proj_c_str.as_ptr(); + if osgb::epsg_convert(srs, pt.as_mut_ptr(), gdal_ptr, proj_ptr) { + center_x = pt[0]; + center_y = pt[1]; + // Use the geoid-corrected height from GeoTransform (if geoid is initialized) + // This handles the conversion from orthometric height (China 1985) to ellipsoidal height (WGS84) + let geo_origin_height = osgb::get_geo_origin_height(); + origin_height = Some(geo_origin_height); + info!("epsg: x->{}, y->{}, h={} (geoid-corrected from original h={})", pt[0], pt[1], geo_origin_height, pt[2]); + } else { + error!("epsg convert failed!"); + } } } - } else { - error!("epsg point is not enough"); + Ok(_) => { + error!("epsg point is not enough"); + } + Err(e) => { + error!("Failed to parse EPSG points: {}", e); + } } } else { error!("parse EPSG failed"); @@ -667,39 +675,62 @@ fn convert_osgb(src: &str, dest: &str, config: &str, enable_simplify: bool, enab } else { // error!("SRS content error"); // treat as wkt - let mut pt: Vec = metadata + let pt_result: Result, _> = metadata .SRSOrigin .split(",") - .map(|v| v.parse().unwrap()) + .map(|v| v.parse()) .collect(); - if pt.len() >= 2 { - let gdal_data: String = { - use std::path::Path; - let exe_dir = ::std::env::current_exe().unwrap(); - Path::new(&exe_dir) - .parent() - .unwrap() - .join("gdal_data") - .to_str() - .unwrap() - .into() - }; - unsafe { - use std::ffi::CString; - let wkt: String = metadata.SRS; - // println!("{:?}", wkt); - let c_str = CString::new(gdal_data).unwrap(); - let ptr = c_str.as_ptr(); - let wkt_cstr = CString::new(wkt).unwrap(); - let wkt_ptr = wkt_cstr.as_ptr(); - if osgb::wkt_convert(wkt_ptr, pt.as_mut_ptr(), ptr) { - center_x = pt[0]; - center_y = pt[1]; - info!("wkt: x->{}, y->{}", pt[0], pt[1]); - } else { - error!("wkt convert failed!"); + match pt_result { + Ok(mut pt) if pt.len() >= 2 => { + let exe_dir = match utils::get_exe_dir() { + Ok(dir) => dir, + Err(e) => { + error!("Failed to get exe dir: {}", e); + return; + } + }; + let gdal_data_path = exe_dir.join("gdal_data"); + let gdal_data = match utils::path_to_string(&gdal_data_path) { + Ok(s) => s, + Err(e) => { + error!("Invalid GDAL data path: {}", e); + return; + } + }; + unsafe { + let wkt: String = metadata.SRS; + // println!("{:?}", wkt); + let c_str = match utils::string_to_cstring(gdal_data) { + Ok(s) => s, + Err(e) => { + error!("Invalid GDAL path: {}", e); + return; + } + }; + let ptr = c_str.as_ptr(); + let wkt_cstr = match utils::string_to_cstring(wkt) { + Ok(s) => s, + Err(e) => { + error!("Invalid WKT: {}", e); + return; + } + }; + let wkt_ptr = wkt_cstr.as_ptr(); + if osgb::wkt_convert(wkt_ptr, pt.as_mut_ptr(), ptr) { + center_x = pt[0]; + center_y = pt[1]; + info!("wkt: x->{}, y->{}", pt[0], pt[1]); + } else { + error!("wkt convert failed!"); + } } } + Ok(_) => { + error!("WKT point is not enough"); + } + Err(e) => { + error!("Failed to parse WKT points: {}", e); + } } } } @@ -729,12 +760,12 @@ fn convert_osgb(src: &str, dest: &str, config: &str, enable_simplify: bool, enab if let Some(lvl) = v["max_lvl"].as_i64() { max_lvl = Some(lvl as i32); } - } else if config.len() > 0 { + } else if !config.is_empty() { error!("config error --> {}", config); } let tick = time::SystemTime::now(); if let Err(e) = osgb::osgb_batch_convert( - &dir, &dir_dest, max_lvl, + dir, dir_dest, max_lvl, center_x, center_y, trans_region, enu_offset, origin_height, enable_texture_compress, enable_simplify, enable_draco, enable_unlit) { diff --git a/src/osg/utils/geometry_utils.cpp b/src/osg/utils/geometry_utils.cpp new file mode 100644 index 00000000..0b5d8461 --- /dev/null +++ b/src/osg/utils/geometry_utils.cpp @@ -0,0 +1,394 @@ +#include "geometry_utils.h" +#include "../../utils/log.h" + +#include +#include +#include +#include +#include + +namespace osg { +namespace utils { + +osg::Matrixd GeometryUtils::computeNormalMatrix(const osg::Matrixd& matrix) { + osg::Matrixd normalMatrix = matrix; + normalMatrix.setTrans(0.0, 0.0, 0.0); + normalMatrix.invert(normalMatrix); + normalMatrix.transpose(normalMatrix); + return normalMatrix; +} + +osg::Vec3d GeometryUtils::transformVertex(const osg::Vec3d& vertex, const osg::Matrixd& matrix) { + osg::Vec3d p = vertex * matrix; + // Y-up to Z-up: x' = x, y' = -z, z' = y + return osg::Vec3d(p.x(), -p.z(), p.y()); +} + +osg::Vec3d GeometryUtils::transformNormal(const osg::Vec3d& normal, const osg::Matrixd& normalMatrix) { + osg::Vec3d nm = osg::Matrix::transform3x3(normal, normalMatrix); + nm.normalize(); + // Y-up to Z-up: x' = x, y' = -z, z' = y + return osg::Vec3d(nm.x(), -nm.z(), nm.y()); +} + +size_t GeometryUtils::extractGeometryData( + const osg::Geometry* geom, + const osg::Matrixd& matrix, + const osg::Matrixd& normalMatrix, + std::vector& outPositions, + std::vector& outNormals, + std::vector& outTexcoords, + size_t baseIndex) { + + if (!geom) return 0; + + const osg::Array* va = geom->getVertexArray(); + if (!va || va->getNumElements() == 0) return 0; + + const osg::Array* na = geom->getNormalArray(); + const osg::Array* ta = geom->getTexCoordArray(0); + + size_t vertexCount = va->getNumElements(); + size_t startIdx = outPositions.size() / 3; + + // 预分配空间 + outPositions.reserve(outPositions.size() + vertexCount * 3); + outNormals.reserve(outNormals.size() + vertexCount * 3); + outTexcoords.reserve(outTexcoords.size() + vertexCount * 2); + + // 根据数组类型提取数据 + const osg::Vec3Array* v3 = dynamic_cast(va); + const osg::Vec4Array* v4 = dynamic_cast(va); + const osg::Vec3dArray* v3d = dynamic_cast(va); + const osg::Vec4dArray* v4d = dynamic_cast(va); + + const osg::Vec3Array* n = dynamic_cast(na); + const osg::Vec3dArray* n3d = dynamic_cast(na); + + const osg::Vec2Array* t = dynamic_cast(ta); + const osg::Vec2dArray* t2d = dynamic_cast(ta); + + // 检查是否需要使用泛型数组处理 + bool useGenericVertexArray = (!v3 || v3->empty()) && (!v4 || v4->empty()) && + (!v3d || v3d->empty()) && (!v4d || v4d->empty()); + + for (size_t i = 0; i < vertexCount; ++i) { + // 提取顶点 + osg::Vec3d p; + if (v3 && i < v3->size()) { + p = (*v3)[i]; + } else if (v4 && i < v4->size()) { + osg::Vec4 vf = (*v4)[i]; + p = osg::Vec3d(vf.x(), vf.y(), vf.z()); + } else if (v3d && i < v3d->size()) { + p = (*v3d)[i]; + } else if (v4d && i < v4d->size()) { + osg::Vec4d vd = (*v4d)[i]; + p = osg::Vec3d(vd.x(), vd.y(), vd.z()); + } else if (useGenericVertexArray) { + // 泛型数组处理 + GLenum dt = va->getDataType(); + unsigned int cnt = va->getNumElements(); + unsigned int totalBytes = va->getTotalDataSize(); + if (dt == GL_FLOAT || dt == GL_DOUBLE) { + unsigned int comps = (dt == GL_FLOAT) ? + totalBytes / (cnt * sizeof(float)) : + totalBytes / (cnt * sizeof(double)); + if (comps >= 3) { + if (dt == GL_FLOAT) { + const float* ptr = static_cast(va->getDataPointer()); + p = osg::Vec3d((double)ptr[i*comps+0], (double)ptr[i*comps+1], (double)ptr[i*comps+2]); + } else { + const double* ptr = static_cast(va->getDataPointer()); + p = osg::Vec3d(ptr[i*comps+0], ptr[i*comps+1], ptr[i*comps+2]); + } + } else { + continue; + } + } else { + continue; + } + } else { + continue; + } + + // 变换顶点 + osg::Vec3d tp = transformVertex(p, matrix); + outPositions.push_back(static_cast(tp.x())); + outPositions.push_back(static_cast(tp.y())); + outPositions.push_back(static_cast(tp.z())); + + // 提取并变换法线 + osg::Vec3d nm(0.0, 0.0, 1.0); + if (n && i < n->size()) { + nm = (*n)[i]; + } else if (n3d && i < n3d->size()) { + nm = (*n3d)[i]; + } else if (na && i < na->getNumElements()) { + // 泛型法线数组处理 + GLenum ndt = na->getDataType(); + unsigned int ncnt = na->getNumElements(); + unsigned int nbytes = na->getTotalDataSize(); + unsigned int ncomps = (ndt == GL_FLOAT) ? + nbytes / (ncnt * sizeof(float)) : + (ndt == GL_DOUBLE) ? nbytes / (ncnt * sizeof(double)) : 0; + if (ncomps >= 3) { + if (ndt == GL_FLOAT) { + const float* nptr = static_cast(na->getDataPointer()); + nm = osg::Vec3d((double)nptr[i*ncomps+0], (double)nptr[i*ncomps+1], (double)nptr[i*ncomps+2]); + } else if (ndt == GL_DOUBLE) { + const double* nptr = static_cast(na->getDataPointer()); + nm = osg::Vec3d(nptr[i*ncomps+0], nptr[i*ncomps+1], nptr[i*ncomps+2]); + } + } + } + osg::Vec3d tnm = transformNormal(nm, normalMatrix); + outNormals.push_back(static_cast(tnm.x())); + outNormals.push_back(static_cast(tnm.y())); + outNormals.push_back(static_cast(tnm.z())); + + // 提取纹理坐标 + float u = 0.0f, v = 0.0f; + if (t && i < t->size()) { + u = (*t)[i].x(); + v = (*t)[i].y(); + } else if (t2d && i < t2d->size()) { + u = static_cast((*t2d)[i].x()); + v = static_cast((*t2d)[i].y()); + } else if (ta && i < ta->getNumElements()) { + // 处理泛型数组 + GLenum tdt = ta->getDataType(); + unsigned int tcnt = ta->getNumElements(); + unsigned int tbytes = ta->getTotalDataSize(); + unsigned int tcomps = (tdt == GL_FLOAT) ? + tbytes / (tcnt * sizeof(float)) : + (tdt == GL_DOUBLE) ? tbytes / (tcnt * sizeof(double)) : 0; + + if (tcomps >= 2) { + if (tdt == GL_FLOAT) { + const float* tptr = static_cast(ta->getDataPointer()); + u = tptr[i * tcomps + 0]; + v = tptr[i * tcomps + 1]; + } else if (tdt == GL_DOUBLE) { + const double* tptr = static_cast(ta->getDataPointer()); + u = static_cast(tptr[i * tcomps + 0]); + v = static_cast(tptr[i * tcomps + 1]); + } + } + } + outTexcoords.push_back(u); + outTexcoords.push_back(v); + } + + return outPositions.size() / 3 - startIdx; +} + +// 前向声明内部辅助函数 +static size_t processDrawArrays( + const osg::DrawArrays* da, + uint32_t baseIndex, + std::vector& outIndices); + +static size_t processDrawElementsUShort( + const osg::DrawElementsUShort* deus, + uint32_t baseIndex, + std::vector& outIndices); + +static size_t processDrawElementsUInt( + const osg::DrawElementsUInt* deui, + uint32_t baseIndex, + std::vector& outIndices); + +size_t GeometryUtils::processPrimitiveSet( + const osg::PrimitiveSet* ps, + uint32_t baseIndex, + std::vector& outIndices) { + + if (!ps) return 0; + + osg::PrimitiveSet::Mode mode = static_cast(ps->getMode()); + + // 只处理三角形相关图元 + if (mode != osg::PrimitiveSet::TRIANGLES && + mode != osg::PrimitiveSet::TRIANGLE_STRIP && + mode != osg::PrimitiveSet::TRIANGLE_FAN) { + return 0; + } + + const osg::DrawArrays* da = dynamic_cast(ps); + const osg::DrawElementsUShort* deus = dynamic_cast(ps); + const osg::DrawElementsUInt* deui = dynamic_cast(ps); + + if (da) { + return processDrawArrays(da, baseIndex, outIndices); + } else if (deus) { + return processDrawElementsUShort(deus, baseIndex, outIndices); + } else if (deui) { + return processDrawElementsUInt(deui, baseIndex, outIndices); + } + + return 0; +} + +osg::BoundingBoxd GeometryUtils::computeWorldBounds( + const osg::Geometry* geom, + const osg::Matrixd& matrix) { + + osg::BoundingBoxd worldBounds; + if (!geom) return worldBounds; + + const osg::Array* va = geom->getVertexArray(); + if (!va) return worldBounds; + + const osg::Vec3Array* v3 = dynamic_cast(va); + const osg::Vec3dArray* v3d = dynamic_cast(va); + + if (v3) { + for (const auto& v : *v3) { + worldBounds.expandBy(transformVertex(osg::Vec3d(v), matrix)); + } + } else if (v3d) { + for (const auto& v : *v3d) { + worldBounds.expandBy(transformVertex(v, matrix)); + } + } + + return worldBounds; +} + +// 内部辅助函数实现 + +static size_t processDrawArrays( + const osg::DrawArrays* da, + uint32_t baseIndex, + std::vector& outIndices) { + + unsigned int first = da->getFirst(); + unsigned int count = da->getCount(); + osg::PrimitiveSet::Mode mode = static_cast(da->getMode()); + size_t triangleCount = 0; + + if (mode == osg::PrimitiveSet::TRIANGLES) { + for (unsigned int idx = 0; idx + 2 < count; idx += 3) { + outIndices.push_back(baseIndex + first + idx); + outIndices.push_back(baseIndex + first + idx + 1); + outIndices.push_back(baseIndex + first + idx + 2); + triangleCount++; + } + } else if (mode == osg::PrimitiveSet::TRIANGLE_STRIP) { + for (unsigned int i = 0; i + 2 < count; ++i) { + unsigned int a = baseIndex + first + i; + unsigned int b = baseIndex + first + i + 1; + unsigned int c = baseIndex + first + i + 2; + if ((i & 1) == 0) { + outIndices.push_back(a); + outIndices.push_back(b); + outIndices.push_back(c); + } else { + outIndices.push_back(b); + outIndices.push_back(a); + outIndices.push_back(c); + } + triangleCount++; + } + } else if (mode == osg::PrimitiveSet::TRIANGLE_FAN) { + unsigned int center = baseIndex + first; + for (unsigned int i = 1; i + 1 < count; ++i) { + outIndices.push_back(center); + outIndices.push_back(baseIndex + first + i); + outIndices.push_back(baseIndex + first + i + 1); + triangleCount++; + } + } + + return triangleCount; +} + +static size_t processDrawElementsUShort( + const osg::DrawElementsUShort* deus, + uint32_t baseIndex, + std::vector& outIndices) { + + size_t count = deus->size(); + osg::PrimitiveSet::Mode mode = static_cast(deus->getMode()); + size_t triangleCount = 0; + + if (mode == osg::PrimitiveSet::TRIANGLES) { + for (size_t idx = 0; idx < count; ++idx) { + outIndices.push_back(baseIndex + (*deus)[idx]); + } + triangleCount = count / 3; + } else if (mode == osg::PrimitiveSet::TRIANGLE_STRIP && count >= 3) { + for (size_t i = 0; i + 2 < count; ++i) { + unsigned int a = baseIndex + (*deus)[i]; + unsigned int b = baseIndex + (*deus)[i + 1]; + unsigned int c = baseIndex + (*deus)[i + 2]; + if ((i & 1) == 0) { + outIndices.push_back(a); + outIndices.push_back(b); + outIndices.push_back(c); + } else { + outIndices.push_back(b); + outIndices.push_back(a); + outIndices.push_back(c); + } + triangleCount++; + } + } else if (mode == osg::PrimitiveSet::TRIANGLE_FAN && count >= 3) { + unsigned int center = baseIndex + (*deus)[0]; + for (size_t i = 1; i + 1 < count; ++i) { + outIndices.push_back(center); + outIndices.push_back(baseIndex + (*deus)[i]); + outIndices.push_back(baseIndex + (*deus)[i + 1]); + triangleCount++; + } + } + + return triangleCount; +} + +static size_t processDrawElementsUInt( + const osg::DrawElementsUInt* deui, + uint32_t baseIndex, + std::vector& outIndices) { + + size_t count = deui->size(); + osg::PrimitiveSet::Mode mode = static_cast(deui->getMode()); + size_t triangleCount = 0; + + if (mode == osg::PrimitiveSet::TRIANGLES) { + for (size_t idx = 0; idx < count; ++idx) { + outIndices.push_back(baseIndex + (*deui)[idx]); + } + triangleCount = count / 3; + } else if (mode == osg::PrimitiveSet::TRIANGLE_STRIP && count >= 3) { + for (size_t i = 0; i + 2 < count; ++i) { + unsigned int a = baseIndex + (*deui)[i]; + unsigned int b = baseIndex + (*deui)[i + 1]; + unsigned int c = baseIndex + (*deui)[i + 2]; + if ((i & 1) == 0) { + outIndices.push_back(a); + outIndices.push_back(b); + outIndices.push_back(c); + } else { + outIndices.push_back(b); + outIndices.push_back(a); + outIndices.push_back(c); + } + triangleCount++; + } + } else if (mode == osg::PrimitiveSet::TRIANGLE_FAN && count >= 3) { + unsigned int center = baseIndex + (*deui)[0]; + for (size_t i = 1; i + 1 < count; ++i) { + outIndices.push_back(center); + outIndices.push_back(baseIndex + (*deui)[i]); + outIndices.push_back(baseIndex + (*deui)[i + 1]); + triangleCount++; + } + } + + return triangleCount; +} + +} // namespace utils +} // namespace osg diff --git a/src/osg/utils/geometry_utils.h b/src/osg/utils/geometry_utils.h new file mode 100644 index 00000000..6e4b8096 --- /dev/null +++ b/src/osg/utils/geometry_utils.h @@ -0,0 +1,118 @@ +#pragma once + +/** + * @file osg/utils/geometry_utils.h + * @brief OSG几何体工具类 + * + * 从appendGeometryToModel提取的几何体处理逻辑 + * 包括:顶点变换、法线变换、索引处理 + */ + +#include +#include +#include +#include +#include +#include + +namespace osg { +namespace utils { + +/** + * @brief 几何体工具类 + * + * 处理OSG几何体到GLTF原始数据的转换 + */ +class GeometryUtils { +public: + /** + * @brief 提取变换后的几何体数据 + * + * 从OSG几何体提取顶点、法线、纹理坐标,并应用世界变换 + * 同时进行Y-up到Z-up的坐标转换 + * + * @param geom OSG几何体 + * @param matrix 世界变换矩阵 + * @param normalMatrix 法线变换矩阵(逆转置) + * @param outPositions 输出顶点位置(已变换) + * @param outNormals 输出法线(已变换) + * @param outTexcoords 输出纹理坐标 + * @param baseIndex 基础索引偏移 + * @return 提取的顶点数量 + */ + static size_t extractGeometryData( + const osg::Geometry* geom, + const osg::Matrixd& matrix, + const osg::Matrixd& normalMatrix, + std::vector& outPositions, + std::vector& outNormals, + std::vector& outTexcoords, + size_t baseIndex = 0 + ); + + /** + * @brief 处理索引数据 + * + * 支持TRIANGLES、TRIANGLE_STRIP、TRIANGLE_FAN等多种图元类型 + * + * @param ps OSG图元集 + * @param baseIndex 基础索引偏移 + * @param outIndices 输出索引 + * @return 处理的三角形数量 + */ + static size_t processPrimitiveSet( + const osg::PrimitiveSet* ps, + uint32_t baseIndex, + std::vector& outIndices + ); + + /** + * @brief 计算法线变换矩阵(逆转置) + * + * 从世界矩阵计算法线变换矩阵,保持法线正交性 + * + * @param matrix 世界变换矩阵 + * @return 法线变换矩阵 + */ + static osg::Matrixd computeNormalMatrix(const osg::Matrixd& matrix); + + /** + * @brief 变换顶点(Y-up到Z-up) + * + * 应用世界变换并进行坐标系转换: + * x' = x + * y' = -z + * z' = y + * + * @param vertex 原始顶点 + * @param matrix 世界变换矩阵 + * @return 变换后的顶点 + */ + static osg::Vec3d transformVertex(const osg::Vec3d& vertex, const osg::Matrixd& matrix); + + /** + * @brief 变换法线(Y-up到Z-up) + * + * 应用法线变换矩阵并进行坐标系转换 + * + * @param normal 原始法线 + * @param normalMatrix 法线变换矩阵 + * @return 变换后的法线 + */ + static osg::Vec3d transformNormal(const osg::Vec3d& normal, const osg::Matrixd& normalMatrix); + + /** + * @brief 计算几何体包围盒(世界空间) + * + * @param geom OSG几何体 + * @param matrix 世界变换矩阵 + * @return 世界空间的包围盒 + */ + static osg::BoundingBoxd computeWorldBounds( + const osg::Geometry* geom, + const osg::Matrixd& matrix + ); +}; + +} // namespace utils +} // namespace osg diff --git a/src/osg/utils/material_utils.cpp b/src/osg/utils/material_utils.cpp new file mode 100644 index 00000000..74228a42 --- /dev/null +++ b/src/osg/utils/material_utils.cpp @@ -0,0 +1,142 @@ +#include "material_utils.h" +#include + +namespace osg { +namespace utils { + +void MaterialUtils::extractPBRParams( + const osg::StateSet* stateSet, + PBRParams& outParams) { + + // 重置为默认值 + outParams.baseColor = {1.0, 1.0, 1.0, 1.0}; + outParams.emissiveColor[0] = 0.0; + outParams.emissiveColor[1] = 0.0; + outParams.emissiveColor[2] = 0.0; + outParams.roughnessFactor = 1.0f; + outParams.metallicFactor = 0.0f; + outParams.aoStrength = 1.0f; + + if (!stateSet) { + return; + } + + // 从Material提取颜色 + const osg::Material* material = dynamic_cast( + stateSet->getAttribute(osg::StateAttribute::MATERIAL)); + if (material) { + extractColorsFromMaterial(material, outParams); + } + + // 从Uniform提取PBR参数 + extractUniformsFromStateSet(stateSet, outParams); +} + +bool MaterialUtils::hasMaterial(const osg::StateSet* stateSet) { + if (!stateSet) { + return false; + } + + // 检查是否有Material属性 + const osg::Material* material = dynamic_cast( + stateSet->getAttribute(osg::StateAttribute::MATERIAL)); + if (material) { + return true; + } + + // 检查是否有纹理 + for (unsigned int i = 0; i < 8; ++i) { + const osg::Texture* texture = dynamic_cast( + stateSet->getTextureAttribute(i, osg::StateAttribute::TEXTURE)); + if (texture) { + return true; + } + } + + return false; +} + +const osg::Texture* MaterialUtils::getBaseColorTexture(const osg::StateSet* stateSet) { + if (!stateSet) { + return nullptr; + } + + // 纹理单元0:基础颜色纹理 + return dynamic_cast( + stateSet->getTextureAttribute(0, osg::StateAttribute::TEXTURE)); +} + +const osg::Texture* MaterialUtils::getNormalTexture(const osg::StateSet* stateSet) { + if (!stateSet) { + return nullptr; + } + + // 纹理单元1:法线纹理 + return dynamic_cast( + stateSet->getTextureAttribute(1, osg::StateAttribute::TEXTURE)); +} + +const osg::Texture* MaterialUtils::getEmissiveTexture(const osg::StateSet* stateSet) { + if (!stateSet) { + return nullptr; + } + + // 纹理单元4:自发光纹理 + return dynamic_cast( + stateSet->getTextureAttribute(4, osg::StateAttribute::TEXTURE)); +} + +void MaterialUtils::extractColorsFromMaterial( + const osg::Material* material, + PBRParams& params) { + + if (!material) { + return; + } + + // 提取diffuse颜色作为基础颜色 + // FBXLoader中设置材质时使用的是FRONT + osg::Vec4 diffuse = material->getDiffuse(osg::Material::FRONT); + params.baseColor = { + static_cast(diffuse.r()), + static_cast(diffuse.g()), + static_cast(diffuse.b()), + static_cast(diffuse.a()) + }; + + // 提取emission颜色作为自发光颜色 + osg::Vec4 emission = material->getEmission(osg::Material::FRONT); + params.emissiveColor[0] = emission.r(); + params.emissiveColor[1] = emission.g(); + params.emissiveColor[2] = emission.b(); +} + +void MaterialUtils::extractUniformsFromStateSet( + const osg::StateSet* stateSet, + PBRParams& params) { + + if (!stateSet) { + return; + } + + // 提取粗糙度 + const osg::Uniform* roughnessUniform = stateSet->getUniform("roughnessFactor"); + if (roughnessUniform) { + roughnessUniform->get(params.roughnessFactor); + } + + // 提取金属度 + const osg::Uniform* metallicUniform = stateSet->getUniform("metallicFactor"); + if (metallicUniform) { + metallicUniform->get(params.metallicFactor); + } + + // 提取AO强度 + const osg::Uniform* aoUniform = stateSet->getUniform("aoStrength"); + if (aoUniform) { + aoUniform->get(params.aoStrength); + } +} + +} // namespace utils +} // namespace osg diff --git a/src/osg/utils/material_utils.h b/src/osg/utils/material_utils.h new file mode 100644 index 00000000..089dea18 --- /dev/null +++ b/src/osg/utils/material_utils.h @@ -0,0 +1,112 @@ +#pragma once + +/** + * @file osg/utils/material_utils.h + * @brief OSG材质工具类 + * + * 从appendGeometryToModel提取的材质处理逻辑 + * 包括:PBR参数提取、材质创建 + */ + +#include +#include +#include +#include + +namespace osg { +namespace utils { + +/** + * @brief 材质配置 + */ +struct MaterialConfig { + bool enableUnlit = false; // 启用KHR_materials_unlit + bool doubleSided = true; // 双面渲染 +}; + +/** + * @brief PBR材质参数 + */ +struct PBRParams { + std::vector baseColor = {1.0, 1.0, 1.0, 1.0}; // 基础颜色 [r,g,b,a] + double emissiveColor[3] = {0.0, 0.0, 0.0}; // 自发光颜色 [r,g,b] + float roughnessFactor = 1.0f; // 粗糙度 + float metallicFactor = 0.0f; // 金属度 + float aoStrength = 1.0f; // AO强度 +}; + +/** + * @brief 材质工具类 + * + * 处理OSG材质到GLTF材质的转换 + */ +class MaterialUtils { +public: + /** + * @brief 从StateSet提取PBR参数 + * + * 从OSG StateSet中提取PBR材质参数: + * - 基础颜色(从osg::Material的diffuse) + * - 自发光颜色(从osg::Material的emission) + * - 粗糙度、金属度(从Uniform) + * + * @param stateSet OSG状态集 + * @param outParams 输出的PBR参数 + */ + static void extractPBRParams( + const osg::StateSet* stateSet, + PBRParams& outParams + ); + + /** + * @brief 检查是否有材质 + * + * @param stateSet OSG状态集 + * @return true 如果有Material或纹理 + */ + static bool hasMaterial(const osg::StateSet* stateSet); + + /** + * @brief 获取基础颜色纹理 + * + * @param stateSet OSG状态集 + * @return 纹理对象,如果没有则返回nullptr + */ + static const osg::Texture* getBaseColorTexture(const osg::StateSet* stateSet); + + /** + * @brief 获取法线纹理 + * + * @param stateSet OSG状态集 + * @return 纹理对象,如果没有则返回nullptr + */ + static const osg::Texture* getNormalTexture(const osg::StateSet* stateSet); + + /** + * @brief 获取自发光纹理 + * + * @param stateSet OSG状态集 + * @return 纹理对象,如果没有则返回nullptr + */ + static const osg::Texture* getEmissiveTexture(const osg::StateSet* stateSet); + +private: + /** + * @brief 从Material提取颜色 + */ + static void extractColorsFromMaterial( + const osg::Material* material, + PBRParams& params + ); + + /** + * @brief 从StateSet提取Uniform参数 + */ + static void extractUniformsFromStateSet( + const osg::StateSet* stateSet, + PBRParams& params + ); +}; + +} // namespace utils +} // namespace osg diff --git a/src/osg/utils/texture_utils.cpp b/src/osg/utils/texture_utils.cpp new file mode 100644 index 00000000..a123bf69 --- /dev/null +++ b/src/osg/utils/texture_utils.cpp @@ -0,0 +1,264 @@ +#include "texture_utils.h" +#include "../../utils/log.h" +#include "../../common/mesh_processor.h" + +#include +#include +#include +#include +#include +#include + +namespace osg { +namespace utils { + +TextureResult TextureUtils::processTexture( + const osg::Texture* texture, + bool enableKTX2) { + + TextureResult result; + + if (!texture || texture->getNumImages() == 0) { + return result; + } + + const osg::Image* image = texture->getImage(0); + if (!image) { + return result; + } + + // 检查透明通道 + result.hasAlpha = hasAlphaTransparency(image); + + std::string imgPath = image->getFileName(); + + // 1. 尝试KTX2压缩 + if (enableKTX2) { + if (tryKTX2Compression(texture, result.data, result.mimeType)) { + result.success = true; + return result; + } + } + + // 2. 从文件加载 + if (!imgPath.empty() && std::filesystem::exists(imgPath)) { + if (loadFromFile(imgPath, result.data, result.mimeType)) { + result.success = true; + return result; + } + } + + // 3. 从内存编码 + if (image->data() != nullptr) { + if (encodeFromMemory(image, result.data, result.mimeType)) { + result.success = true; + return result; + } + } + + return result; +} + +int TextureUtils::addImageToModel( + tinygltf::Model& model, + tinygltf::Buffer& buffer, + const std::vector& imageData, + const std::string& mimeType, + bool useBasisu) { + + // 确保4字节对齐 + size_t currentSize = buffer.data.size(); + size_t padding = (4 - (currentSize % 4)) % 4; + if (padding > 0) { + buffer.data.resize(currentSize + padding, 0); + } + + size_t imgOffset = buffer.data.size(); + size_t imgLen = imageData.size(); + buffer.data.resize(imgOffset + imgLen); + memcpy(buffer.data.data() + imgOffset, imageData.data(), imgLen); + + // 创建BufferView + tinygltf::BufferView bvImg; + bvImg.buffer = 0; + bvImg.byteOffset = static_cast(imgOffset); + bvImg.byteLength = static_cast(imgLen); + int bvImgIdx = static_cast(model.bufferViews.size()); + model.bufferViews.push_back(bvImg); + + // 创建Image + tinygltf::Image gltfImg; + gltfImg.mimeType = mimeType; + gltfImg.bufferView = bvImgIdx; + int imgIdx = static_cast(model.images.size()); + model.images.push_back(gltfImg); + + // 创建Texture + tinygltf::Texture gltfTex; + if (useBasisu) { + tinygltf::Value::Object ktxExt; + ktxExt["source"] = tinygltf::Value(imgIdx); + gltfTex.extensions["KHR_texture_basisu"] = tinygltf::Value(ktxExt); + } else { + gltfTex.source = imgIdx; + } + int texIdx = static_cast(model.textures.size()); + model.textures.push_back(gltfTex); + + // 确保结束对齐 + size_t endSize = buffer.data.size(); + size_t endPadding = (4 - (endSize % 4)) % 4; + if (endPadding > 0) { + buffer.data.resize(endSize + endPadding, 0); + } + + return texIdx; +} + +bool TextureUtils::hasAlphaTransparency(const osg::Image* image) { + if (!image || !image->data()) { + return false; + } + + GLenum pf = image->getPixelFormat(); + GLenum dt = image->getDataType(); + int w = image->s(); + int h = image->t(); + + // 确定通道数 + int channels = 0; + if (pf == GL_LUMINANCE) channels = 1; + else if (pf == GL_LUMINANCE_ALPHA) channels = 2; + else if (pf == GL_RGB) channels = 3; + else if (pf == GL_RGBA) channels = 4; + + // 检查透明像素 + if ((channels == 2 || channels == 4) && + dt == GL_UNSIGNED_BYTE && + w > 0 && h > 0) { + + const unsigned char* p = image->data(); + int alphaIndex = (channels == 2) ? 1 : 3; + int total = w * h; + + for (int i = 0; i < total; ++i) { + if (p[i * channels + alphaIndex] < 255) { + return true; + } + } + } + + return false; +} + +bool TextureUtils::tryKTX2Compression( + const osg::Texture* texture, + std::vector& outData, + std::string& outMimeType) { + + std::vector compressedData; + std::string compressedMime; + + // 调用全局的process_texture函数 + if (process_texture(const_cast(texture), + compressedData, compressedMime, true)) { + if (compressedMime == "image/ktx2") { + outData = compressedData; + outMimeType = compressedMime; + return true; + } + } + + return false; +} + +bool TextureUtils::loadFromFile( + const std::string& filePath, + std::vector& outData, + std::string& outMimeType) { + + std::ifstream file(filePath, std::ios::binary | std::ios::ate); + if (!file) { + return false; + } + + size_t size = file.tellg(); + outData.resize(size); + file.seekg(0); + file.read(reinterpret_cast(outData.data()), size); + + // 根据扩展名确定MIME类型 + std::string ext = std::filesystem::path(filePath).extension().string(); + outMimeType = getMimeTypeFromExtension(ext); + + return true; +} + +bool TextureUtils::encodeFromMemory( + const osg::Image* image, + std::vector& outData, + std::string& outMimeType) { + + std::string imgPath = image->getFileName(); + std::string ext = "png"; + + // 尝试从文件名获取扩展名 + if (!imgPath.empty()) { + std::string e = std::filesystem::path(imgPath).extension().string(); + if (!e.empty() && e.size() > 1) { + e = e.substr(1); + std::transform(e.begin(), e.end(), e.begin(), ::tolower); + if (e == "jpg" || e == "jpeg") { + ext = e; + } + } + } + + // 尝试使用对应格式的Writer + osgDB::ReaderWriter* rw = osgDB::Registry::instance()->getReaderWriterForExtension(ext); + if (rw) { + std::stringstream ss; + osgDB::ReaderWriter::WriteResult wr = rw->writeImage(*image, ss); + if (wr.success()) { + std::string s = ss.str(); + outData.assign(s.begin(), s.end()); + outMimeType = getMimeTypeFromExtension(ext); + return true; + } + } + + // 如果失败,尝试PNG + if (ext != "png") { + rw = osgDB::Registry::instance()->getReaderWriterForExtension("png"); + if (rw) { + std::stringstream ss; + osgDB::ReaderWriter::WriteResult wr = rw->writeImage(*image, ss); + if (wr.success()) { + std::string s = ss.str(); + outData.assign(s.begin(), s.end()); + outMimeType = "image/png"; + return true; + } + } + } + + return false; +} + +std::string TextureUtils::getMimeTypeFromExtension(const std::string& ext) { + std::string lowerExt = ext; + std::transform(lowerExt.begin(), lowerExt.end(), lowerExt.begin(), ::tolower); + + if (lowerExt == ".jpg" || lowerExt == ".jpeg") { + return "image/jpeg"; + } else if (lowerExt == ".png") { + return "image/png"; + } else if (lowerExt == ".ktx2") { + return "image/ktx2"; + } + + return "image/png"; // 默认 +} + +} // namespace utils +} // namespace osg diff --git a/src/osg/utils/texture_utils.h b/src/osg/utils/texture_utils.h new file mode 100644 index 00000000..99a613c5 --- /dev/null +++ b/src/osg/utils/texture_utils.h @@ -0,0 +1,110 @@ +#pragma once + +/** + * @file osg/utils/texture_utils.h + * @brief OSG纹理工具类 + * + * 从appendGeometryToModel提取的纹理处理逻辑 + * 包括:纹理加载、KTX2压缩、添加到GLTF模型 + */ + +#include +#include +#include +#include +#include + +namespace osg { +namespace utils { + +/** + * @brief 纹理处理结果 + */ +struct TextureResult { + std::vector data; // 图像数据 + std::string mimeType; // MIME类型 + bool hasAlpha = false; // 是否包含透明通道 + bool success = false; // 是否成功 +}; + +/** + * @brief 纹理工具类 + * + * 处理OSG纹理到GLTF纹理的转换 + * 统一处理基础纹理、法线纹理、自发光纹理 + */ +class TextureUtils { +public: + /** + * @brief 处理纹理 + * + * 统一的纹理处理入口,替代appendGeometryToModel中的3处重复代码 + * 处理流程: + * 1. 检查透明通道 + * 2. 尝试KTX2压缩(如果启用) + * 3. 从文件加载 + * 4. 从内存编码 + * + * @param texture OSG纹理对象 + * @param enableKTX2 是否启用KTX2压缩 + * @return 处理结果 + */ + static TextureResult processTexture( + const osg::Texture* texture, + bool enableKTX2 = false + ); + + /** + * @brief 将图像数据添加到GLTF模型 + * + * @param model GLTF模型 + * @param buffer GLTF缓冲区 + * @param imageData 图像数据 + * @param mimeType MIME类型 + * @param useBasisu 是否使用Basisu扩展(KTX2) + * @return 纹理索引 + */ + static int addImageToModel( + tinygltf::Model& model, + tinygltf::Buffer& buffer, + const std::vector& imageData, + const std::string& mimeType, + bool useBasisu + ); + + /** + * @brief 检查图像是否有透明通道 + * + * @param image OSG图像 + * @return 是否包含透明像素 + */ + static bool hasAlphaTransparency(const osg::Image* image); + +private: + // 尝试KTX2压缩 + static bool tryKTX2Compression( + const osg::Texture* texture, + std::vector& outData, + std::string& outMimeType + ); + + // 从文件加载图像 + static bool loadFromFile( + const std::string& filePath, + std::vector& outData, + std::string& outMimeType + ); + + // 从内存编码图像 + static bool encodeFromMemory( + const osg::Image* image, + std::vector& outData, + std::string& outMimeType + ); + + // 根据扩展名获取MIME类型 + static std::string getMimeTypeFromExtension(const std::string& ext); +}; + +} // namespace utils +} // namespace osg diff --git a/src/osgb.rs b/src/osgb.rs index c99cc924..9233043c 100644 --- a/src/osgb.rs +++ b/src/osgb.rs @@ -67,6 +67,7 @@ struct OsgbInfo { sender: ::std::sync::mpsc::Sender, } +#[allow(clippy::too_many_arguments)] pub fn osgb_batch_convert( dir: &Path, dir_dest: &Path, @@ -155,7 +156,7 @@ pub fn osgb_batch_convert( libc::free(out_ptr); } let t = TileResult { - path: info.out_dir.into(), + path: info.out_dir, json: String::from_utf8(json_buf).unwrap(), box_v: root_box, }; @@ -175,14 +176,14 @@ pub fn osgb_batch_convert( let mut root_box = vec![-1.0E+38f64, -1.0E+38, -1.0E+38, 1.0E+38, 1.0E+38, 1.0E+38]; let mut root_geometric_error = 0.0; for x in tile_array.iter() { - for i in 0..3 { - if x.box_v[i] > root_box[i] { - root_box[i] = x.box_v[i] + for (i, root_val) in root_box.iter_mut().enumerate().take(3) { + if x.box_v[i] > *root_val { + *root_val = x.box_v[i] } } - for i in 3..6 { - if x.box_v[i] < root_box[i] { - root_box[i] = x.box_v[i] + for (i, root_val) in root_box.iter_mut().enumerate().take(6).skip(3) { + if x.box_v[i] < *root_val { + *root_val = x.box_v[i] } } let json_val: serde_json::Value = serde_json::from_str(&x.json).unwrap(); @@ -227,7 +228,6 @@ pub fn osgb_batch_convert( "box": box_to_tileset_box(&root_box) }, "geometricError": root_geometric_error * 2.0, - "refine": "REPLACE", "children": [] } } @@ -282,24 +282,20 @@ fn get_geometric_error(center_y: f64, lvl: i32) -> f64 { 4.0 * round / (256 * pow) as f64 } -fn box_to_tileset_box(box_v: &Vec) -> Vec { - let mut box_new = vec![]; - box_new.push((box_v[0] + box_v[3]) / 2.0); - box_new.push((box_v[1] + box_v[4]) / 2.0); - box_new.push((box_v[2] + box_v[5]) / 2.0); - - box_new.push((box_v[3] - box_v[0]).abs() / 2.0); - box_new.push(0.0); - box_new.push(0.0); - - box_new.push(0.0); - box_new.push((box_v[4] - box_v[1]).abs() / 2.0); - box_new.push(0.0); - - box_new.push(0.0); - box_new.push(0.0); - box_new.push((box_v[5] - box_v[2]).abs() / 2.0); - - box_new +fn box_to_tileset_box(box_v: &[f64]) -> Vec { + vec![ + (box_v[0] + box_v[3]) / 2.0, + (box_v[1] + box_v[4]) / 2.0, + (box_v[2] + box_v[5]) / 2.0, + (box_v[3] - box_v[0]).abs() / 2.0, + 0.0, + 0.0, + 0.0, + (box_v[4] - box_v[1]).abs() / 2.0, + 0.0, + 0.0, + 0.0, + (box_v[5] - box_v[2]).abs() / 2.0, + ] } diff --git a/src/osgb23dtile.cpp b/src/osgb23dtile.cpp index e97ba9e4..90887775 100644 --- a/src/osgb23dtile.cpp +++ b/src/osgb23dtile.cpp @@ -21,15 +21,21 @@ #include // Add Draco compression includes -#include "mesh_processor.h" +#include "common/mesh_processor.h" #define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION #define TINYGLTF_IMPLEMENTATION #include #include -#include "extern.h" -#include "coordinate_transformer.h" +#include "utils/log.h" +#include "utils/file_utils.h" +#include "extern.h" // for GetGlobalTransformer +#include "coords/coordinate_transformer.h" +#include "b3dm/b3dm_writer.h" +#include "tileset/bounding_volume.h" +#include "tileset/tileset_writer.h" +#include "tileset/geometric_error.h" using namespace std; @@ -110,28 +116,11 @@ void write_buf(void* context, void* data, int len) { buf->insert(buf->end(), (char*)data, (char*)data + len); } -struct TileBox -{ - std::vector max; - std::vector min; - - void extend(double ratio) { - ratio /= 2; - double x = max[0] - min[0]; - double y = max[1] - min[1]; - double z = max[2] - min[2]; - max[0] += x * ratio; - max[1] += y * ratio; - max[2] += z * ratio; - - min[0] -= x * ratio; - min[1] -= y * ratio; - min[2] -= z * ratio; - } -}; +// Alias for backward compatibility - using tileset::Box for bounding volumes +using TileBox = tileset::Box; struct osg_tree { - TileBox bbox; + tileset::Box bbox; double geometricError; std::string file_name; std::vector sub_nodes; @@ -282,19 +271,10 @@ class InfoVisitor : public osg::NodeVisitor std::set other_texture_array; }; -double get_geometric_error(TileBox& bbox){ - if (bbox.max.empty() || bbox.min.empty()) - { - //LOG_E("bbox is empty!"); - return 0; - } - - double max_err = std::max((bbox.max[0] - bbox.min[0]),(bbox.max[1] - bbox.min[1])); - max_err = std::max(max_err, (bbox.max[2] - bbox.min[2])); - return max_err / 2.0; -// const double pi = std::acos(-1); -// double round = 2 * pi * 6378137.0 / 128.0; -// return round / std::pow(2.0, lvl ); +// Wrapper function that delegates to tileset::computeGeometricError +// Kept for backward compatibility with existing code +double get_geometric_error(const tileset::Box& bbox){ + return tileset::computeGeometricError(bbox, 0.05); } std::string get_file_name(std::string path) { @@ -458,13 +438,6 @@ struct MeshInfo template void alignment_buffer(std::vector& buf) { - while (buf.size() % 8 != 0) { - buf.push_back(0x00); - } -} - -template -void alignment_buffer_4(std::vector& buf) { while (buf.size() % 4 != 0) { buf.push_back(0x00); } @@ -690,7 +663,7 @@ bool triangulate_quad_like(const std::vector& indices, GLenum mode, } void -write_vec3_array(osg::Vec3Array* v3f, OsgBuildState* osgState, osg::Vec3f& point_max, osg::Vec3f& point_min, bool isNormal = false) +write_vec3_array(osg::Vec3Array* v3f, OsgBuildState* osgState, osg::Vec3f& point_max, osg::Vec3f& point_min) { int vec_start = 0; int vec_end = v3f->size(); @@ -703,17 +676,6 @@ write_vec3_array(osg::Vec3Array* v3f, OsgBuildState* osgState, osg::Vec3f& point for (int vidx = vec_start; vidx < vec_end; vidx++) { osg::Vec3f point = v3f->at(vidx); - // Per glTF spec: Normal vectors must be unit length - // Fix zero-length normals by replacing with (0, 0, 1) - if (isNormal) { - float len = point.length(); - if (len < 0.0001f) { - point.set(0.0f, 0.0f, 1.0f); - } else if (std::abs(len - 1.0f) > 0.0001f) { - // Normalize if not already unit length - point.normalize(); - } - } put_val(osgState->buffer->data, point.x()); put_val(osgState->buffer->data, point.y()); put_val(osgState->buffer->data, point.z()); @@ -944,7 +906,7 @@ void write_element_array_primitive(osg::Geometry* g, osg::PrimitiveSet* ps, if (pmtState->normalAccessor == -1 && osgState->draw_array_first == -1) { pmtState->normalAccessor = osgState->model->accessors.size(); } - write_vec3_array(normalArr, osgState, point_max, point_min, true); + write_vec3_array(normalArr, osgState, point_max, point_min); } } } @@ -1267,112 +1229,25 @@ bool osgb2glb_buf(std::string path, std::string& glb_buff, MeshInfo& mesh_info, return true; } -bool osgb2b3dm_buf(std::string path, std::string& b3dm_buf, TileBox& tile_box, int node_type, bool enable_texture_compress = false, bool enable_meshopt = false, bool enable_draco = false, bool enable_unlit = true) +bool osgb2b3dm_buf(std::string path, std::string& b3dm_buf, tileset::Box& tile_box, int node_type, bool enable_texture_compress = false, bool enable_meshopt = false, bool enable_draco = false, bool enable_unlit = true) { - using nlohmann::json; - std::string glb_buf; MeshInfo minfo; bool ret = osgb2glb_buf(path, glb_buf, minfo, node_type, enable_texture_compress, enable_meshopt, enable_draco, enable_unlit); if (!ret) return false; - tile_box.max = minfo.max; - tile_box.min = minfo.min; - - int mesh_count = 1; - std::string feature_json_string; - feature_json_string += "{\"BATCH_LENGTH\":"; - feature_json_string += std::to_string(mesh_count); - feature_json_string += "}"; - while ((feature_json_string.size()+28) % 8 != 0 ) { - feature_json_string.push_back(' '); - } - json batch_json; - std::vector ids; - for (int i = 0; i < mesh_count; ++i) { - ids.push_back(i); - } - std::vector names; - for (int i = 0; i < mesh_count; ++i) { - std::string mesh_name = "mesh_"; - mesh_name += std::to_string(i); - names.push_back(mesh_name); - } - batch_json["batchId"] = ids; - batch_json["name"] = names; - std::string batch_json_string = batch_json.dump(); - while (batch_json_string.size() % 8 != 0 ) { - batch_json_string.push_back(' '); - } - - - // how length total ? - //test - //feature_json_string.clear(); - //batch_json_string.clear(); - //end-test - - int feature_json_len = feature_json_string.size(); - int feature_bin_len = 0; - int batch_json_len = batch_json_string.size(); - int batch_bin_len = 0; - - int header_len = 28; - - // First, build the B3DM buffer without GLB to calculate the correct total length - b3dm_buf += "b3dm"; - int version = 1; - put_val(b3dm_buf, version); - // Placeholder for total_len, will update later - int total_len_offset = b3dm_buf.size(); - put_val(b3dm_buf, 0); // placeholder - put_val(b3dm_buf, feature_json_len); - put_val(b3dm_buf, feature_bin_len); - put_val(b3dm_buf, batch_json_len); - put_val(b3dm_buf, batch_bin_len); - b3dm_buf.append(feature_json_string.begin(),feature_json_string.end()); - b3dm_buf.append(batch_json_string.begin(),batch_json_string.end()); - - // Ensure Feature Table JSON + Batch Table JSON end at 8-byte boundary - // so that GLB starts at 8-byte aligned offset - while (b3dm_buf.size() % 8 != 0) { - b3dm_buf.push_back(0x00); - } - - // Append GLB data - b3dm_buf.append(glb_buf); - - // Per 3D Tiles spec: B3DM total length must be 8-byte aligned - // Ensure the final B3DM buffer is 8-byte aligned - while (b3dm_buf.size() % 8 != 0) { - b3dm_buf.push_back(0x00); - } - - // Update total_len in the header - int total_len = b3dm_buf.size(); - *reinterpret_cast(&b3dm_buf[total_len_offset]) = total_len; + // Convert MeshInfo min/max to tileset::Box + tile_box = tileset::createBoxFromMinMax( + minfo.min[0], minfo.min[1], minfo.min[2], + minfo.max[0], minfo.max[1], minfo.max[2] + ); - return true; -} + // 使用统一的 B3DM 写入接口 + constexpr int mesh_count = 1; + b3dm_buf = b3dm::wrapGlbToB3dmSimple(glb_buf, mesh_count); -std::vector convert_bbox(TileBox tile) { - double center_mx = (tile.max[0] + tile.min[0]) / 2; - double center_my = (tile.max[1] + tile.min[1]) / 2; - double center_mz = (tile.max[2] + tile.min[2]) / 2; - double x_meter = (tile.max[0] - tile.min[0]) * 1; - double y_meter = (tile.max[1] - tile.min[1]) * 1; - double z_meter = (tile.max[2] - tile.min[2]) * 1; - if (x_meter < 0.01) { x_meter = 0.01; } - if (y_meter < 0.01) { y_meter = 0.01; } - if (z_meter < 0.01) { z_meter = 0.01; } - std::vector v = { - center_mx,center_my,center_mz, - x_meter/2, 0, 0, - 0, y_meter/2, 0, - 0, 0, z_meter/2 - }; - return v; + return !b3dm_buf.empty(); } void do_tile_job(osg_tree& tree, std::string out_path, int max_lvl, bool enable_texture_compress = false, bool enable_meshopt = false, bool enable_draco = false, bool enable_unlit = true) { @@ -1387,7 +1262,7 @@ void do_tile_job(osg_tree& tree, std::string out_path, int max_lvl, bool enable_ out_file += "/"; out_file += replace(get_file_name(tree.file_name), ".osgb", tree.type != 2 ? ".b3dm" : "o.b3dm"); if (!b3dm_buf.empty()) { - write_file(out_file.c_str(), b3dm_buf.data(), b3dm_buf.size()); + utils::write_file(out_file.c_str(), b3dm_buf.data(), b3dm_buf.size()); } // test // std::string glb_buf; @@ -1402,69 +1277,73 @@ void do_tile_job(osg_tree& tree, std::string out_path, int max_lvl, bool enable_ } } -void expend_box(TileBox& box, TileBox& box_new) { - if (box_new.max.empty() || box_new.min.empty()) { +// Helper function to extract min/max from tileset::Box +static void box_to_min_max(const tileset::Box& box, double& min_x, double& min_y, double& min_z, double& max_x, double& max_y, double& max_z) { + auto center = box.center(); + auto x_axis = box.xAxis(); + auto y_axis = box.yAxis(); + auto z_axis = box.zAxis(); + + // Calculate half-lengths + double hx = std::sqrt(x_axis[0]*x_axis[0] + x_axis[1]*x_axis[1] + x_axis[2]*x_axis[2]); + double hy = std::sqrt(y_axis[0]*y_axis[0] + y_axis[1]*y_axis[1] + y_axis[2]*y_axis[2]); + double hz = std::sqrt(z_axis[0]*z_axis[0] + z_axis[1]*z_axis[1] + z_axis[2]*z_axis[2]); + + min_x = center[0] - hx; + max_x = center[0] + hx; + min_y = center[1] - hy; + max_y = center[1] + hy; + min_z = center[2] - hz; + max_z = center[2] + hz; +} + +// Check if box is empty (all half-lengths are zero) +static bool is_box_empty(const tileset::Box& box) { + double hx = std::sqrt(box.values[3]*box.values[3] + box.values[4]*box.values[4] + box.values[5]*box.values[5]); + double hy = std::sqrt(box.values[6]*box.values[6] + box.values[7]*box.values[7] + box.values[8]*box.values[8]); + double hz = std::sqrt(box.values[9]*box.values[9] + box.values[10]*box.values[10] + box.values[11]*box.values[11]); + return hx == 0.0 && hy == 0.0 && hz == 0.0; +} + +// Merge two boxes by expanding the first to contain both +void expend_box(tileset::Box& box, const tileset::Box& box_new) { + if (is_box_empty(box_new)) { return; } - if (box.max.empty()) { - box.max = box_new.max; - } - if (box.min.empty()) { - box.min = box_new.min; - } - for (int i = 0; i < 3; i++) { - if (box.min[i] > box_new.min[i]) - box.min[i] = box_new.min[i]; - if (box.max[i] < box_new.max[i]) - box.max[i] = box_new.max[i]; + + double min_x1, min_y1, min_z1, max_x1, max_y1, max_z1; + double min_x2, min_y2, min_z2, max_x2, max_y2, max_z2; + + box_to_min_max(box, min_x1, min_y1, min_z1, max_x1, max_y1, max_z1); + box_to_min_max(box_new, min_x2, min_y2, min_z2, max_x2, max_y2, max_z2); + + if (is_box_empty(box)) { + box = box_new; + return; } + + // Compute union + double new_min_x = std::min(min_x1, min_x2); + double new_min_y = std::min(min_y1, min_y2); + double new_min_z = std::min(min_z1, min_z2); + double new_max_x = std::max(max_x1, max_x2); + double new_max_y = std::max(max_y1, max_y2); + double new_max_z = std::max(max_z1, max_z2); + + box = tileset::createBoxFromMinMax(new_min_x, new_min_y, new_min_z, new_max_x, new_max_y, new_max_z); } -TileBox extend_tile_box(osg_tree& tree) { - TileBox box = tree.bbox; +tileset::Box extend_tile_box(osg_tree& tree) { + tileset::Box box = tree.bbox; for (auto& i : tree.sub_nodes) { - TileBox sub_tile = extend_tile_box(i); + tileset::Box sub_tile = extend_tile_box(i); expend_box(box, sub_tile); } tree.bbox = box; return box; } -std::string get_boundingBox(TileBox bbox) { - std::string box_str = "\"boundingVolume\":{"; - box_str += "\"box\":["; - std::vector v_box = convert_bbox(bbox); - for (auto v: v_box) { - box_str += std::to_string(v); - box_str += ","; - } - box_str.pop_back(); - box_str += "]}"; - return box_str; -} - -std::string get_boundingRegion(TileBox bbox, double x, double y) { - std::string box_str = "\"boundingVolume\":{"; - box_str += "\"region\":["; - std::vector v_box(6); - v_box[0] = meter_to_longti(bbox.min[0],y) + x; - v_box[1] = meter_to_lati(bbox.min[1]) + y; - v_box[2] = meter_to_longti(bbox.max[0], y) + x; - v_box[3] = meter_to_lati(bbox.max[1]) + y; - v_box[4] = bbox.min[2]; - v_box[5] = bbox.max[2]; - - for (auto v : v_box) { - box_str += std::to_string(v); - box_str += ","; - } - box_str.pop_back(); - box_str += "]}"; - return box_str; -} - void calc_geometric_error(osg_tree& tree) { - const double EPS = 1e-12; // depth first for (auto& i : tree.sub_nodes) { calc_geometric_error(i); @@ -1473,69 +1352,68 @@ void calc_geometric_error(osg_tree& tree) { tree.geometricError = get_geometric_error(tree.bbox); } else { - double max_sub_geometric_error = 0.0; + // Collect child errors and use tileset::computeParentGeometricError + std::vector childErrors; + childErrors.reserve(tree.sub_nodes.size()); for (auto &sub_node : tree.sub_nodes) { - max_sub_geometric_error = std::max(max_sub_geometric_error, sub_node.geometricError); + childErrors.push_back(sub_node.geometricError); } - - tree.geometricError = max_sub_geometric_error * 2.0; + tree.geometricError = tileset::computeParentGeometricError(childErrors, 2.0); } } -std::string -encode_tile_json(osg_tree& tree, double x, double y) -{ - if (tree.bbox.max.empty() || tree.bbox.min.empty()) - return ""; +// Convert osg_tree to tileset::Tile for use with TilesetWriter +tileset::Tile convert_osg_tree_to_tile(osg_tree& tree) { + // Create tile with bounding volume and geometric error + tileset::Tile tile(tree.bbox, tree.geometricError); - std::string file_name = get_file_name(tree.file_name); - std::string parent_str = get_parent(tree.file_name); - std::string file_path = get_file_name(parent_str); - - char buf[512]; - sprintf(buf, "{ \"geometricError\":%.2f,", tree.geometricError); - std::string tile = buf; - // Per 3D Tiles spec: refine property must be set in root tiles - tile += " \"refine\":\"REPLACE\","; - TileBox cBox = tree.bbox; - //cBox.extend(0.1); - std::string content_box = get_boundingBox(cBox); - TileBox bbox = tree.bbox; - //bbox.extend(0.1); - std::string tile_box = get_boundingBox(bbox); - - tile += tile_box; - if (tree.type > 0) { - tile += ", \"content\":{ \"uri\":"; - // Data/Tile_0/Tile_0.b3dm - std::string uri_path = "./"; - uri_path += file_name; + // Set content if this is a leaf node (type > 0) + if (tree.type > 0 && !tree.file_name.empty()) { + std::string file_name = get_file_name(tree.file_name); + std::string uri_path = "./" + file_name; std::string uri = replace(uri_path, ".osgb", tree.type != 2 ? ".b3dm" : "o.b3dm"); - tile += "\""; - tile += uri; - tile += "\","; - tile += content_box; - tile += "}"; - } - // Only include children array if there are sub-nodes - // Per 3D Tiles spec: Empty children arrays cause validation warnings - if (!tree.sub_nodes.empty()) { - tile += ",\"children\":["; - for ( auto& i : tree.sub_nodes ){ - std::string node_json = encode_tile_json(i,x,y); - if (!node_json.empty()) { - tile += node_json; - tile += ","; - } - } - if (tile.back() == ',') - tile.pop_back(); - tile += "]"; + tile.setContent(uri); + } + + // Recursively convert children + for (auto& child_tree : tree.sub_nodes) { + tile.addChild(convert_osg_tree_to_tile(child_tree)); } - tile += "}"; + return tile; } +// Convert osg_tree to JSON for a single tile (not full tileset) +// Uses TilesetWriter internally but extracts only the root tile JSON +std::string convert_osg_tree_to_tile_json(osg_tree& tree) { + if (is_box_empty(tree.bbox)) + return ""; + + // Convert osg_tree to tileset::Tile + tileset::Tile root_tile = convert_osg_tree_to_tile(tree); + + // Create a minimal tileset with just this tile + tileset::Tileset tileset(root_tile, tree.geometricError); + + // Use TilesetWriter to generate full tileset JSON + tileset::TilesetWriter writer; + std::string full_tileset_json = writer.write(tileset); + + // Parse and extract only the "root" tile object + nlohmann::json full_json = nlohmann::json::parse(full_tileset_json); + nlohmann::json root_tile_json = full_json["root"]; + + return root_tile_json.dump(); +} + +// Legacy function - kept for backward compatibility +// Returns a single tile JSON (not full tileset) to match Rust expectations +std::string +encode_tile_json(osg_tree& tree, double x, double y) +{ + return convert_osg_tree_to_tile_json(tree); +} + /***/ extern "C" void* osgb23dtile_path(const char* in_path, const char* out_path, @@ -1552,7 +1430,7 @@ osgb23dtile_path(const char* in_path, const char* out_path, } do_tile_job(root, out_path, max_lvl, enable_texture_compress, enable_meshopt, enable_draco, enable_unlit); extend_tile_box(root); - if (root.bbox.max.empty() || root.bbox.min.empty()) + if (is_box_empty(root.bbox)) { LOG_E( "[%s] bbox is empty!", in_path); return NULL; @@ -1560,9 +1438,15 @@ osgb23dtile_path(const char* in_path, const char* out_path, // prevent for root node disappear calc_geometric_error(root); std::string json = encode_tile_json(root, x, y); - root.bbox.extend(0.2); - memcpy(box, root.bbox.max.data(), 3 * sizeof(double)); - memcpy(box + 3, root.bbox.min.data(), 3 * sizeof(double)); + root.bbox = root.bbox.extended(0.2); + + // Extract min/max from tileset::Box for output + double min_x, min_y, min_z, max_x, max_y, max_z; + box_to_min_max(root.bbox, min_x, min_y, min_z, max_x, max_y, max_z); + double max_vals[3] = {max_x, max_y, max_z}; + double min_vals[3] = {min_x, min_y, min_z}; + memcpy(box, max_vals, 3 * sizeof(double)); + memcpy(box + 3, min_vals, 3 * sizeof(double)); void* str = malloc(json.length()); memcpy(str, json.c_str(), json.length()); *len = json.length(); @@ -1582,7 +1466,7 @@ osgb2glb(const char* in, const char* out) return false; } - ret = write_file(out, glb_buf.data(), (unsigned long)glb_buf.size()); + ret = utils::write_file(out, glb_buf.data(), (unsigned long)glb_buf.size()); if (!ret) { LOG_E("write glb file failed"); diff --git a/src/pipeline/adapters/fbx/fbx_data_source.h b/src/pipeline/adapters/fbx/fbx_data_source.h new file mode 100644 index 00000000..ad8e835f --- /dev/null +++ b/src/pipeline/adapters/fbx/fbx_data_source.h @@ -0,0 +1,188 @@ +#pragma once + +/** + * @file fbx_data_source.h + * @brief FBX 数据源适配器 + * + * 步骤1:将 FBXLoader 适配为 pipeline::DataSource 接口 + */ + +#include "pipeline/data_source.h" +#include "fbx/core/fbx.h" +#include "fbx/fbx_spatial_item_adapter.h" +#include +#include + +namespace pipeline::adapters::fbx { + +// FBX 空间项适配器 - 实现 ISpatialItem 接口 +class FBXSpatialItemImpl : public ISpatialItem { +public: + explicit FBXSpatialItemImpl(::fbx::FBXSpatialItemPtr item) + : item_(std::move(item)) {} + + [[nodiscard]] auto GetId() const -> uint64_t override { + return static_cast(item_->getId()); + } + + [[nodiscard]] auto GetBounds() const + -> std::tuple override { + auto bounds = item_->getBounds(); + auto min = bounds.min(); + auto max = bounds.max(); + return { + min[0], min[1], min[2], + max[0], max[1], max[2] + }; + } + + [[nodiscard]] auto GetGeometry() const + -> osg::ref_ptr override { + const auto* geom = item_->getGeometry(); + return const_cast(geom); + } + + [[nodiscard]] auto GetProperties() const + -> std::unordered_map override { + std::unordered_map props; + props["node_name"] = item_->getNodeName(); + props["transform_index"] = std::to_string(item_->getTransformIndex()); + return props; + } + + // 获取原始适配器 + [[nodiscard]] auto GetOriginalAdapter() const + -> const ::fbx::FBXSpatialItemAdapter* { + return item_.get(); + } + + [[nodiscard]] auto GetOriginalAdapter() -> ::fbx::FBXSpatialItemAdapter* { + return item_.get(); + } + +private: + ::fbx::FBXSpatialItemPtr item_; +}; + +// FBX 数据源适配器 +class FBXDataSource : public DataSource { +public: + FBXDataSource() = default; + ~FBXDataSource() override = default; + + // 禁止拷贝,允许移动 + FBXDataSource(const FBXDataSource&) = delete; + FBXDataSource& operator=(const FBXDataSource&) = delete; + FBXDataSource(FBXDataSource&&) = default; + FBXDataSource& operator=(FBXDataSource&&) = default; + + // 加载数据 + [[nodiscard]] auto Load(const DataSourceConfig& config) -> bool override { + config_ = config; + + loader_ = std::make_unique<::FBXLoader>(config.input_path.string()); + loader_->load(); + + if (!loader_->getRoot()) { + return false; + } + + // 创建空间项适配器 + spatialItems_ = ::fbx::createSpatialItems(loader_.get()); + + // 转换为统一接口 + pipelineItems_.clear(); + pipelineItems_.reserve(spatialItems_.size()); + for (const auto& item : spatialItems_) { + pipelineItems_.push_back(std::make_shared(item)); + } + + // 计算世界包围盒 + CalculateWorldBounds(); + + return true; + } + + // 获取空间项列表 + [[nodiscard]] auto GetSpatialItems() const -> SpatialItemList override { + return pipelineItems_; + } + + // 获取世界包围盒 + [[nodiscard]] auto GetWorldBounds() const + -> std::tuple override { + return { + worldMinX_, worldMinY_, worldMinZ_, + worldMaxX_, worldMaxY_, worldMaxZ_ + }; + } + + // 获取地理参考 + [[nodiscard]] auto GetGeoReference() const + -> std::tuple override { + return { + config_.center_longitude, + config_.center_latitude, + config_.center_height + }; + } + + // 获取数据项数量 + [[nodiscard]] auto GetItemCount() const noexcept -> std::size_t override { + return pipelineItems_.size(); + } + + // 是否已加载 + [[nodiscard]] auto IsLoaded() const noexcept -> bool override { + return loader_ != nullptr && loader_->getRoot() != nullptr; + } + + // 获取原始加载器(供其他组件使用) + [[nodiscard]] auto GetLoader() const -> ::FBXLoader* { + return loader_.get(); + } + + // 获取原始空间项列表 + [[nodiscard]] auto GetFBXSpatialItems() const + -> const ::fbx::FBXSpatialItemList& { + return spatialItems_; + } + +private: + void CalculateWorldBounds() { + if (pipelineItems_.empty()) { + worldMinX_ = worldMinY_ = worldMinZ_ = 0.0; + worldMaxX_ = worldMaxY_ = worldMaxZ_ = 0.0; + return; + } + + worldMinX_ = std::numeric_limits::max(); + worldMinY_ = std::numeric_limits::max(); + worldMinZ_ = std::numeric_limits::max(); + worldMaxX_ = std::numeric_limits::lowest(); + worldMaxY_ = std::numeric_limits::lowest(); + worldMaxZ_ = std::numeric_limits::lowest(); + + for (const auto& item : pipelineItems_) { + auto [minx, miny, minz, maxx, maxy, maxz] = item->GetBounds(); + worldMinX_ = std::min(worldMinX_, minx); + worldMinY_ = std::min(worldMinY_, miny); + worldMinZ_ = std::min(worldMinZ_, minz); + worldMaxX_ = std::max(worldMaxX_, maxx); + worldMaxY_ = std::max(worldMaxY_, maxy); + worldMaxZ_ = std::max(worldMaxZ_, maxz); + } + } + + DataSourceConfig config_; + std::unique_ptr<::FBXLoader> loader_; + ::fbx::FBXSpatialItemList spatialItems_; + SpatialItemList pipelineItems_; + double worldMinX_ = 0.0, worldMinY_ = 0.0, worldMinZ_ = 0.0; + double worldMaxX_ = 0.0, worldMaxY_ = 0.0, worldMaxZ_ = 0.0; +}; + +// 注册 FBX 数据源 +REGISTER_DATA_SOURCE("fbx", FBXDataSource); + +} // namespace pipeline::adapters::fbx diff --git a/src/pipeline/adapters/shapefile/shapefile_data_source.h b/src/pipeline/adapters/shapefile/shapefile_data_source.h new file mode 100644 index 00000000..efcbc819 --- /dev/null +++ b/src/pipeline/adapters/shapefile/shapefile_data_source.h @@ -0,0 +1,185 @@ +#pragma once + +/** + * @file shapefile_data_source.h + * @brief Shapefile 数据源适配器 + * + * 步骤1:将 ShapefileDataPool 适配为 pipeline::DataSource 接口 + */ + +#include "pipeline/data_source.h" +#include "shapefile/shapefile_data_pool.h" +#include "shapefile/shapefile_spatial_item_adapter.h" +#include +#include + +namespace pipeline::adapters::shapefile { + +// Shapefile 空间项适配器 - 实现 ISpatialItem 接口 +class ShapefileSpatialItemImpl : public ISpatialItem { +public: + explicit ShapefileSpatialItemImpl(::shapefile::ShapefileDataPool::ItemPtr item) + : item_(std::move(item)) {} + + [[nodiscard]] auto GetId() const -> uint64_t override { + return static_cast(item_->featureId); + } + + [[nodiscard]] auto GetBounds() const + -> std::tuple override { + return { + item_->bounds.minx, + item_->bounds.miny, + item_->bounds.minHeight, + item_->bounds.maxx, + item_->bounds.maxy, + item_->bounds.maxHeight + }; + } + + [[nodiscard]] auto GetGeometry() const + -> osg::ref_ptr override { + if (!item_->geometries.empty()) { + return item_->geometries[0]; + } + return nullptr; + } + + [[nodiscard]] auto GetProperties() const + -> std::unordered_map override { + std::unordered_map props; + for (const auto& [key, value] : item_->properties) { + props[key] = value.dump(); + } + return props; + } + + // 获取原始数据项 + [[nodiscard]] auto GetOriginalItem() const + -> const ::shapefile::ShapefileSpatialItem* { + return item_.get(); + } + +private: + ::shapefile::ShapefileDataPool::ItemPtr item_; +}; + +// Shapefile 数据源适配器 +class ShapefileDataSource : public DataSource { +public: + ShapefileDataSource() = default; + ~ShapefileDataSource() override = default; + + // 禁止拷贝,允许移动 + ShapefileDataSource(const ShapefileDataSource&) = delete; + ShapefileDataSource& operator=(const ShapefileDataSource&) = delete; + ShapefileDataSource(ShapefileDataSource&&) = default; + ShapefileDataSource& operator=(ShapefileDataSource&&) = default; + + // 加载数据 + [[nodiscard]] auto Load(const DataSourceConfig& config) -> bool override { + config_ = config; + + dataPool_ = std::make_unique<::shapefile::ShapefileDataPool>(); + + if (!dataPool_->loadFromShapefileWithGeometry( + config.input_path.string(), + config.height_field, + config.center_longitude, + config.center_latitude)) { + return false; + } + + // 构建空间项列表 + spatialItems_.clear(); + const auto& items = dataPool_->getAllItems(); + spatialItems_.reserve(items.size()); + + for (const auto& item : items) { + spatialItems_.push_back( + std::make_shared(item)); + } + + // 计算世界包围盒 + CalculateWorldBounds(); + + return true; + } + + // 获取空间项列表 + [[nodiscard]] auto GetSpatialItems() const -> SpatialItemList override { + return spatialItems_; + } + + // 获取世界包围盒 + [[nodiscard]] auto GetWorldBounds() const + -> std::tuple override { + return { + worldMinX_, worldMinY_, worldMinZ_, + worldMaxX_, worldMaxY_, worldMaxZ_ + }; + } + + // 获取地理参考 + [[nodiscard]] auto GetGeoReference() const + -> std::tuple override { + return { + config_.center_longitude, + config_.center_latitude, + config_.center_height + }; + } + + // 获取数据项数量 + [[nodiscard]] auto GetItemCount() const noexcept -> std::size_t override { + return spatialItems_.size(); + } + + // 是否已加载 + [[nodiscard]] auto IsLoaded() const noexcept -> bool override { + return dataPool_ != nullptr && !spatialItems_.empty(); + } + + // 获取原始数据池(供其他组件使用) + [[nodiscard]] auto GetDataPool() const + -> const ::shapefile::ShapefileDataPool* { + return dataPool_.get(); + } + +private: + void CalculateWorldBounds() { + if (spatialItems_.empty()) { + worldMinX_ = worldMinY_ = worldMinZ_ = 0.0; + worldMaxX_ = worldMaxY_ = worldMaxZ_ = 0.0; + return; + } + + worldMinX_ = std::numeric_limits::max(); + worldMinY_ = std::numeric_limits::max(); + worldMinZ_ = std::numeric_limits::max(); + worldMaxX_ = std::numeric_limits::lowest(); + worldMaxY_ = std::numeric_limits::lowest(); + worldMaxZ_ = std::numeric_limits::lowest(); + + for (const auto& item : spatialItems_) { + auto [minx, miny, minz, maxx, maxy, maxz] = item->GetBounds(); + worldMinX_ = std::min(worldMinX_, minx); + worldMinY_ = std::min(worldMinY_, miny); + worldMinZ_ = std::min(worldMinZ_, minz); + worldMaxX_ = std::max(worldMaxX_, maxx); + worldMaxY_ = std::max(worldMaxY_, maxy); + worldMaxZ_ = std::max(worldMaxZ_, maxz); + } + } + + DataSourceConfig config_; + std::unique_ptr<::shapefile::ShapefileDataPool> dataPool_; + SpatialItemList spatialItems_; + double worldMinX_ = 0.0, worldMinY_ = 0.0, worldMinZ_ = 0.0; + double worldMaxX_ = 0.0, worldMaxY_ = 0.0, worldMaxZ_ = 0.0; +}; + +// 注册 Shapefile 数据源 +REGISTER_DATA_SOURCE("shapefile", ShapefileDataSource); + +} // namespace pipeline::adapters::shapefile diff --git a/src/pipeline/adapters/spatial/octree_index.h b/src/pipeline/adapters/spatial/octree_index.h new file mode 100644 index 00000000..f0cf86b1 --- /dev/null +++ b/src/pipeline/adapters/spatial/octree_index.h @@ -0,0 +1,211 @@ +#pragma once + +/** + * @file octree_index.h + * @brief Octree 空间索引适配器 + * + * 步骤2:将 spatial::strategy::OctreeStrategy 适配为 pipeline::ISpatialIndex 接口 + */ + +#include "pipeline/spatial_index.h" +#include "spatial/strategy/octree_strategy.h" +#include "spatial/core/slicing_strategy.h" +#include +#include + +namespace pipeline::adapters::spatial { + +// Octree 节点适配器 +class OctreeNodeAdapter : public ISpatialIndexNode { +public: + explicit OctreeNodeAdapter(const ::spatial::strategy::OctreeNode* node) + : node_(node) {} + + [[nodiscard]] auto GetId() const -> uint64_t override { + if (!node_) return 0; + // 使用深度和位置计算ID + // 简化:使用深度和包围盒中心 + auto bounds = node_->getBounds3D(); + auto center = bounds.center(); + return (static_cast(node_->getDepth()) << 48) | + (static_cast(center[0] * 1000) << 24) | + static_cast(center[1] * 1000); + } + + [[nodiscard]] auto GetDepth() const -> int override { + if (!node_) return 0; + return static_cast(node_->getDepth()); + } + + [[nodiscard]] auto GetBounds() const + -> std::tuple override { + if (!node_) return {0, 0, 0, 0, 0, 0}; + + auto bounds = node_->getBounds3D(); + auto min = bounds.min(); + auto max = bounds.max(); + return {min[0], min[1], min[2], max[0], max[1], max[2]}; + } + + [[nodiscard]] auto GetItems() const -> SpatialItemList override { + if (!node_) return {}; + + SpatialItemList items; + auto spatialItems = node_->getItems(); + + for (const auto& itemRef : spatialItems) { + // 类型转换逻辑... + } + + return items; + } + + [[nodiscard]] auto GetChildren() const + -> std::vector override { + if (!node_) return {}; + + std::vector children; + auto spatialChildren = node_->getChildren(); + + for (const auto* child : spatialChildren) { + if (child) { + // 类型转换逻辑... + } + } + + return children; + } + + [[nodiscard]] auto IsLeaf() const -> bool override { + if (!node_) return true; + return node_->isLeaf(); + } + + [[nodiscard]] auto GetItemCount() const -> std::size_t override { + if (!node_) return 0; + return node_->getItemCount(); + } + + // 获取原始节点 + [[nodiscard]] auto GetRawNode() const -> const ::spatial::strategy::OctreeNode* { + return node_; + } + +private: + const ::spatial::strategy::OctreeNode* node_; +}; + +// Octree 空间索引适配器 +class OctreeIndexAdapter : public ISpatialIndex { +public: + OctreeIndexAdapter() = default; + ~OctreeIndexAdapter() override = default; + + // 禁止拷贝,允许移动 + OctreeIndexAdapter(const OctreeIndexAdapter&) = delete; + OctreeIndexAdapter& operator=(const OctreeIndexAdapter&) = delete; + OctreeIndexAdapter(OctreeIndexAdapter&&) = default; + OctreeIndexAdapter& operator=(OctreeIndexAdapter&&) = default; + + // 构建索引 + [[nodiscard]] auto Build(const DataSource* data_source, + const SpatialIndexConfig& config) -> bool override { + if (!data_source || !data_source->IsLoaded()) { + return false; + } + + config_ = config; + + // 获取世界包围盒 + auto [minX, minY, minZ, maxX, maxY, maxZ] = data_source->GetWorldBounds(); + + // 创建八叉树配置 + ::spatial::strategy::OctreeConfig octConfig; + octConfig.maxDepth = static_cast(config.max_depth); + octConfig.maxItemsPerNode = config.max_items_per_node; + octConfig.minBoundsSize = config.min_bounds_size; + + // 转换为空间项列表 + ::spatial::core::SpatialItemList spatialItems; + auto items = data_source->GetSpatialItems(); + + // 构建八叉树索引 + ::spatial::strategy::OctreeStrategy strategy; + auto bounds3d = ::spatial::core::SpatialBounds( + std::array{minX, minY, minZ}, + std::array{maxX, maxY, maxZ} + ); + + index_ = strategy.buildIndex(spatialItems, bounds3d, octConfig); + + return index_ != nullptr; + } + + // 获取根节点 + [[nodiscard]] auto GetRootNode() const -> const ISpatialIndexNode* override { + if (!index_) return nullptr; + + auto* rawRoot = index_->getRootNode(); + if (!rawRoot) return nullptr; + + // 动态转换为 OctreeNode + auto* octRoot = dynamic_cast(rawRoot); + if (!octRoot) return nullptr; + + // 返回适配器 + rootAdapter_ = std::make_unique(octRoot); + return rootAdapter_.get(); + } + + // 获取节点数量 + [[nodiscard]] auto GetNodeCount() const -> std::size_t override { + if (!index_) return 0; + return index_->getNodeCount(); + } + + // 获取对象数量 + [[nodiscard]] auto GetItemCount() const -> std::size_t override { + if (!index_) return 0; + return index_->getItemCount(); + } + + // 查询指定范围内的对象 + [[nodiscard]] auto Query(double minX, double minY, double minZ, + double maxX, double maxY, double maxZ) const + -> SpatialItemList override { + if (!index_) return {}; + + auto bounds = ::spatial::core::SpatialBounds( + std::array{minX, minY, minZ}, + std::array{maxX, maxY, maxZ} + ); + + auto results = index_->query(bounds); + + // 转换结果类型 + SpatialItemList items; + + return items; + } + + // 获取原始索引 + [[nodiscard]] auto GetRawIndex() const + -> const ::spatial::core::SpatialIndex* { + return index_.get(); + } + + [[nodiscard]] auto GetRawIndex() + -> ::spatial::core::SpatialIndex* { + return index_.get(); + } + +private: + SpatialIndexConfig config_; + std::unique_ptr<::spatial::core::SpatialIndex> index_; + mutable std::unique_ptr rootAdapter_; +}; + +// 注册 Octree 空间索引 +REGISTER_SPATIAL_INDEX("octree", OctreeIndexAdapter); + +} // namespace pipeline::adapters::spatial diff --git a/src/pipeline/adapters/spatial/quadtree_index.h b/src/pipeline/adapters/spatial/quadtree_index.h new file mode 100644 index 00000000..52872a75 --- /dev/null +++ b/src/pipeline/adapters/spatial/quadtree_index.h @@ -0,0 +1,215 @@ +#pragma once + +/** + * @file quadtree_index.h + * @brief Quadtree 空间索引适配器 + * + * 步骤2:将 spatial::strategy::QuadtreeStrategy 适配为 pipeline::ISpatialIndex 接口 + */ + +#include "pipeline/spatial_index.h" +#include "spatial/strategy/quadtree_strategy.h" +#include "spatial/core/slicing_strategy.h" +#include +#include + +namespace pipeline::adapters::spatial { + +// Quadtree 节点适配器 +class QuadtreeNodeAdapter : public ISpatialIndexNode { +public: + explicit QuadtreeNodeAdapter(const ::spatial::strategy::QuadtreeNode* node) + : node_(node) {} + + [[nodiscard]] auto GetId() const -> uint64_t override { + if (!node_) return 0; + auto coord = node_->getCoord(); + return coord.encode(); + } + + [[nodiscard]] auto GetDepth() const -> int override { + if (!node_) return 0; + return static_cast(node_->getDepth()); + } + + [[nodiscard]] auto GetBounds() const + -> std::tuple override { + if (!node_) return {0, 0, 0, 0, 0, 0}; + + auto bounds = node_->getBounds(); + auto min = bounds.min(); + auto max = bounds.max(); + return {min[0], min[1], min[2], max[0], max[1], max[2]}; + } + + [[nodiscard]] auto GetItems() const -> SpatialItemList override { + if (!node_) return {}; + + SpatialItemList items; + auto spatialItems = node_->getItems(); + + for (const auto& itemRef : spatialItems) { + // 这里需要将 SpatialItemRef 转换为 pipeline::ISpatialItem + // 由于类型不匹配,我们返回空列表 + // 实际使用时,数据项应该通过 DataSource 获取 + } + + return items; + } + + [[nodiscard]] auto GetChildren() const + -> std::vector override { + if (!node_) return {}; + + std::vector children; + auto spatialChildren = node_->getChildren(); + + // 注意:这里返回的是原始指针,生命周期由外部管理 + for (const auto* child : spatialChildren) { + if (child) { + // 由于类型不匹配,这里不直接存储适配器 + // 实际使用时通过 GetRawNode 获取原始节点 + } + } + + return children; + } + + [[nodiscard]] auto IsLeaf() const -> bool override { + if (!node_) return true; + return node_->isLeaf(); + } + + [[nodiscard]] auto GetItemCount() const -> std::size_t override { + if (!node_) return 0; + return node_->getItemCount(); + } + + // 获取原始节点 + [[nodiscard]] auto GetRawNode() const -> const ::spatial::strategy::QuadtreeNode* { + return node_; + } + +private: + const ::spatial::strategy::QuadtreeNode* node_; +}; + +// Quadtree 空间索引适配器 +class QuadtreeIndexAdapter : public ISpatialIndex { +public: + QuadtreeIndexAdapter() = default; + ~QuadtreeIndexAdapter() override = default; + + // 禁止拷贝,允许移动 + QuadtreeIndexAdapter(const QuadtreeIndexAdapter&) = delete; + QuadtreeIndexAdapter& operator=(const QuadtreeIndexAdapter&) = delete; + QuadtreeIndexAdapter(QuadtreeIndexAdapter&&) = default; + QuadtreeIndexAdapter& operator=(QuadtreeIndexAdapter&&) = default; + + // 构建索引 + [[nodiscard]] auto Build(const DataSource* data_source, + const SpatialIndexConfig& config) -> bool override { + if (!data_source || !data_source->IsLoaded()) { + return false; + } + + config_ = config; + + // 获取世界包围盒 + auto [minX, minY, minZ, maxX, maxY, maxZ] = data_source->GetWorldBounds(); + + // 创建四叉树配置 + ::spatial::strategy::QuadtreeConfig qtConfig; + qtConfig.maxDepth = static_cast(config.max_depth); + qtConfig.maxItemsPerNode = config.max_items_per_node; + qtConfig.minBoundsSize = config.min_bounds_size; + + // 转换为空间项列表 + ::spatial::core::SpatialItemList spatialItems; + auto items = data_source->GetSpatialItems(); + + // 这里需要将 pipeline::ISpatialItem 转换为 spatial::core::SpatialItem + // 由于类型不匹配,我们需要通过 DataSource 的原始数据来构建 + // 暂时使用空列表,实际使用时应该传入正确的数据 + + // 构建四叉树索引 + ::spatial::strategy::QuadtreeStrategy strategy; + auto bounds3d = ::spatial::core::SpatialBounds( + std::array{minX, minY, minZ}, + std::array{maxX, maxY, maxZ} + ); + + index_ = strategy.buildIndex(spatialItems, bounds3d, qtConfig); + + return index_ != nullptr; + } + + // 获取根节点 + [[nodiscard]] auto GetRootNode() const -> const ISpatialIndexNode* override { + if (!index_) return nullptr; + + auto* rawRoot = index_->getRootNode(); + if (!rawRoot) return nullptr; + + // 动态转换为 QuadtreeNode + auto* qtRoot = dynamic_cast(rawRoot); + if (!qtRoot) return nullptr; + + // 返回适配器(注意:这里需要确保适配器的生命周期) + rootAdapter_ = std::make_unique(qtRoot); + return rootAdapter_.get(); + } + + // 获取节点数量 + [[nodiscard]] auto GetNodeCount() const -> std::size_t override { + if (!index_) return 0; + return index_->getNodeCount(); + } + + // 获取对象数量 + [[nodiscard]] auto GetItemCount() const -> std::size_t override { + if (!index_) return 0; + return index_->getItemCount(); + } + + // 查询指定范围内的对象 + [[nodiscard]] auto Query(double minX, double minY, double minZ, + double maxX, double maxY, double maxZ) const + -> SpatialItemList override { + if (!index_) return {}; + + auto bounds = ::spatial::core::SpatialBounds( + std::array{minX, minY, minZ}, + std::array{maxX, maxY, maxZ} + ); + + auto results = index_->query(bounds); + + // 转换结果类型 + SpatialItemList items; + // 类型转换逻辑... + + return items; + } + + // 获取原始索引 + [[nodiscard]] auto GetRawIndex() const + -> const ::spatial::core::SpatialIndex* { + return index_.get(); + } + + [[nodiscard]] auto GetRawIndex() + -> ::spatial::core::SpatialIndex* { + return index_.get(); + } + +private: + SpatialIndexConfig config_; + std::unique_ptr<::spatial::core::SpatialIndex> index_; + mutable std::unique_ptr rootAdapter_; +}; + +// 注册 Quadtree 空间索引 +REGISTER_SPATIAL_INDEX("quadtree", QuadtreeIndexAdapter); + +} // namespace pipeline::adapters::spatial diff --git a/src/pipeline/adapters/tileset/fbx_tileset_builder.h b/src/pipeline/adapters/tileset/fbx_tileset_builder.h new file mode 100644 index 00000000..8fa4e886 --- /dev/null +++ b/src/pipeline/adapters/tileset/fbx_tileset_builder.h @@ -0,0 +1,177 @@ +#pragma once + +/** + * @file fbx_tileset_builder.h + * @brief FBX TilesetBuilder 适配器 + * + * 步骤3:将 fbx::FBXTilesetAdapter 适配为 pipeline::ITilesetBuilder 接口 + */ + +#include "pipeline/tileset_builder.h" +#include "fbx/fbx_tileset_adapter.h" +#include "fbx/fbx_tile_meta.h" +#include "tileset/tileset_types.h" +#include "tileset/tileset_writer.h" +#include +#include + +namespace pipeline::adapters::tileset { + +// FBX 瓦片元数据适配器 +class FBXTileMetaAdapter : public ITileMeta { +public: + explicit FBXTileMetaAdapter(const ::fbx::FBXTileMeta& meta) + : meta_(meta) {} + + [[nodiscard]] auto GetId() const -> uint64_t override { + return meta_.key(); + } + + [[nodiscard]] auto GetParentId() const -> uint64_t override { + return meta_.parentKey(); + } + + [[nodiscard]] auto GetChildIds() const -> std::vector override { + std::vector ids; + for (const auto& key : meta_.childrenKeys) { + ids.push_back(key); + } + return ids; + } + + [[nodiscard]] auto GetBounds() const + -> std::tuple override { + return { + meta_.bbox.xMin(), meta_.bbox.yMin(), meta_.bbox.zMin(), + meta_.bbox.xMax(), meta_.bbox.yMax(), meta_.bbox.zMax() + }; + } + + [[nodiscard]] auto GetGeometricError() const -> double override { + return meta_.geometricError; + } + + [[nodiscard]] auto GetContentUri() const -> std::string override { + return meta_.b3dmPath; + } + + [[nodiscard]] auto HasContent() const -> bool override { + return meta_.hasGeometry; + } + + [[nodiscard]] auto GetDepth() const -> int override { + return meta_.coord.z; + } + + // 获取原始元数据 + [[nodiscard]] auto GetRawMeta() const -> const ::fbx::FBXTileMeta& { + return meta_; + } + +private: + ::fbx::FBXTileMeta meta_; +}; + +// FBX TilesetBuilder 适配器 +class FBXTilesetBuilderAdapter : public ITilesetBuilder { +public: + FBXTilesetBuilderAdapter() = default; + ~FBXTilesetBuilderAdapter() override = default; + + // 禁止拷贝,允许移动 + FBXTilesetBuilderAdapter(const FBXTilesetBuilderAdapter&) = delete; + FBXTilesetBuilderAdapter& operator=(const FBXTilesetBuilderAdapter&) = delete; + FBXTilesetBuilderAdapter(FBXTilesetBuilderAdapter&&) = default; + FBXTilesetBuilderAdapter& operator=(FBXTilesetBuilderAdapter&&) = default; + + // 初始化构建器 + void Initialize(const TilesetBuilderConfig& config) override { + config_ = config; + + // 创建 FBXTilesetAdapter + ::fbx::FBXTilesetAdapterConfig adapterConfig; + adapterConfig.centerLongitude = config.center_longitude; + adapterConfig.centerLatitude = config.center_latitude; + adapterConfig.centerHeight = config.center_height; + adapterConfig.boundingVolumeScale = config.bounding_volume_scale; + adapterConfig.geometricErrorScale = config.child_geometric_error_multiplier; + adapterConfig.enableLOD = config.enable_lod; + adapterConfig.lodLevelCount = config.lod_level_count; + + adapter_ = std::make_unique<::fbx::FBXTilesetAdapter>(adapterConfig); + } + + // 添加瓦片元数据 + void AddTileMeta(const TileMetaPtr& meta) override { + auto* fbxMeta = dynamic_cast(meta.get()); + if (fbxMeta) { + // 转换为 FBX TileMeta + ::fbx::FBXTileMeta fbxMetaData = fbxMeta->GetRawMeta(); + metas_[fbxMetaData.key()] = std::make_shared<::fbx::FBXTileMeta>(fbxMetaData); + } + } + + // 添加 FBX 专用元数据 + void AddFBXTileMeta(const ::fbx::FBXTileMetaPtr& meta) { + metas_[meta->key()] = meta; + } + + // 构建 Tileset + [[nodiscard]] auto BuildTileset() -> ::tileset::Tileset override { + if (!adapter_ || metas_.empty()) { + return ::tileset::Tileset(); + } + + // 查找根节点 + uint64_t rootId = 0; + for (const auto& [id, meta] : metas_) { + if (meta->parentKey() == 0) { + rootId = id; + break; + } + } + + if (rootId == 0) { + return ::tileset::Tileset(); + } + + // 构建 Tileset + // 注意:FBXTilesetAdapter 的 buildAndWriteTileset 方法需要输出路径 + // 这里返回空 tileset,实际构建应该使用 BuildAndWrite + return ::tileset::Tileset(); + } + + // 构建并写入文件 + [[nodiscard]] auto BuildAndWrite(const std::string& output_path) -> bool override { + if (!adapter_ || metas_.empty()) { + return false; + } + + return adapter_->buildAndWriteTileset(metas_, output_path); + } + + // 清空所有元数据 + void Clear() override { + metas_.clear(); + } + + // 获取原始适配器 + [[nodiscard]] auto GetRawAdapter() const -> ::fbx::FBXTilesetAdapter* { + return adapter_.get(); + } + + // 获取元数据映射表 + [[nodiscard]] auto GetMetaMap() const -> const ::fbx::FBXTileMetaMap& { + return metas_; + } + +private: + TilesetBuilderConfig config_; + std::unique_ptr<::fbx::FBXTilesetAdapter> adapter_; + ::fbx::FBXTileMetaMap metas_; +}; + +// 注册 FBX TilesetBuilder +REGISTER_TILESET_BUILDER("fbx", FBXTilesetBuilderAdapter); + +} // namespace pipeline::adapters::tileset diff --git a/src/pipeline/adapters/tileset/shapefile_tileset_builder.h b/src/pipeline/adapters/tileset/shapefile_tileset_builder.h new file mode 100644 index 00000000..16eba7ed --- /dev/null +++ b/src/pipeline/adapters/tileset/shapefile_tileset_builder.h @@ -0,0 +1,184 @@ +#pragma once + +/** + * @file shapefile_tileset_builder.h + * @brief Shapefile TilesetBuilder 适配器 + * + * 步骤3:将 shapefile::ShapefileTilesetAdapter 适配为 pipeline::ITilesetBuilder 接口 + */ + +#include "pipeline/tileset_builder.h" +#include "shapefile/shapefile_tileset_adapter.h" +#include "shapefile/shapefile_tile_meta.h" +#include "tileset/tileset_types.h" +#include "tileset/tileset_writer.h" +#include +#include +#include + +namespace pipeline::adapters::tileset { + +// Shapefile 瓦片元数据适配器 +class ShapefileTileMetaAdapter : public ITileMeta { +public: + explicit ShapefileTileMetaAdapter(const ::shapefile::ShapefileTileMeta& meta) + : meta_(meta) {} + + [[nodiscard]] auto GetId() const -> uint64_t override { + return meta_.key(); + } + + [[nodiscard]] auto GetParentId() const -> uint64_t override { + return meta_.parentKey(); + } + + [[nodiscard]] auto GetChildIds() const -> std::vector override { + std::vector ids; + for (const auto& key : meta_.childrenKeys) { + ids.push_back(key); + } + return ids; + } + + [[nodiscard]] auto GetBounds() const + -> std::tuple override { + return { + meta_.bbox.minX, meta_.bbox.minY, meta_.bbox.minZ, + meta_.bbox.maxX, meta_.bbox.maxY, meta_.bbox.maxZ + }; + } + + [[nodiscard]] auto GetGeometricError() const -> double override { + return meta_.geometricError; + } + + [[nodiscard]] auto GetContentUri() const -> std::string override { + return meta_.content.uri; + } + + [[nodiscard]] auto HasContent() const -> bool override { + return meta_.content.hasContent; + } + + [[nodiscard]] auto GetDepth() const -> int override { + return meta_.coord.z; + } + + // 获取原始元数据 + [[nodiscard]] auto GetRawMeta() const -> const ::shapefile::ShapefileTileMeta& { + return meta_; + } + +private: + ::shapefile::ShapefileTileMeta meta_; +}; + +// Shapefile TilesetBuilder 适配器 +class ShapefileTilesetBuilderAdapter : public ITilesetBuilder { +public: + ShapefileTilesetBuilderAdapter() = default; + ~ShapefileTilesetBuilderAdapter() override = default; + + // 禁止拷贝,允许移动 + ShapefileTilesetBuilderAdapter(const ShapefileTilesetBuilderAdapter&) = delete; + ShapefileTilesetBuilderAdapter& operator=(const ShapefileTilesetBuilderAdapter&) = delete; + ShapefileTilesetBuilderAdapter(ShapefileTilesetBuilderAdapter&&) = default; + ShapefileTilesetBuilderAdapter& operator=(ShapefileTilesetBuilderAdapter&&) = default; + + // 初始化构建器 + void Initialize(const TilesetBuilderConfig& config) override { + config_ = config; + + // 创建 ShapefileTilesetAdapter + ::shapefile::AdapterConfig adapterConfig; + adapterConfig.boundingVolumeScaleFactor = config.bounding_volume_scale; + adapterConfig.geometricErrorScale = config.child_geometric_error_multiplier; + adapterConfig.applyRootTransform = true; + adapterConfig.enableLOD = config.enable_lod; + adapterConfig.lodLevelCount = config.lod_level_count; + + adapter_ = std::make_unique<::shapefile::ShapefileTilesetAdapter>( + config.center_longitude, + config.center_latitude, + adapterConfig + ); + } + + // 添加瓦片元数据 + void AddTileMeta(const TileMetaPtr& meta) override { + auto* shapefileMeta = dynamic_cast(meta.get()); + if (shapefileMeta) { + // 转换为 Shapefile TileMeta + ::shapefile::ShapefileTileMeta sfMeta = shapefileMeta->GetRawMeta(); + metas_[sfMeta.key()] = sfMeta; + } + } + + // 添加 Shapefile 专用元数据 + void AddShapefileTileMeta(const ::shapefile::ShapefileTileMeta& meta) { + metas_[meta.key()] = meta; + } + + // 构建 Tileset + [[nodiscard]] auto BuildTileset() -> ::tileset::Tileset override { + if (!adapter_ || metas_.empty()) { + return ::tileset::Tileset(); + } + + // 查找根节点 + uint64_t rootId = 0; + for (const auto& [id, meta] : metas_) { + if (meta.parentKey() == 0) { + rootId = id; + break; + } + } + + if (rootId == 0) { + return ::tileset::Tileset(); + } + + // 构建 Tileset - 使用通用 TileMetaMap + common::TileMetaMap metaMap; + for (const auto& [id, meta] : metas_) { + metaMap[id] = std::make_shared<::shapefile::ShapefileTileMeta>(meta); + } + auto rootMeta = metaMap[rootId]; + + return adapter_->buildTileset(rootMeta, metaMap); + } + + // 构建并写入文件 + [[nodiscard]] auto BuildAndWrite(const std::string& output_path) -> bool override { + auto tileset = BuildTileset(); + // 检查根节点是否有效 + if (!tileset.root.content.has_value() && tileset.root.children.empty()) { + return false; + } + + // 使用 TilesetWriter 序列化并写入 + ::tileset::TilesetWriter writer; + return writer.writeToFile(tileset, output_path); + } + + // 清空所有元数据 + void Clear() override { + metas_.clear(); + } + + // 获取原始适配器 + [[nodiscard]] auto GetRawAdapter() const -> ::shapefile::ShapefileTilesetAdapter* { + return adapter_.get(); + } + +private: + TilesetBuilderConfig config_; + std::unique_ptr<::shapefile::ShapefileTilesetAdapter> adapter_; + std::unordered_map metas_; + uint64_t rootId_ = 0; +}; + +// 注册 Shapefile TilesetBuilder +REGISTER_TILESET_BUILDER("shapefile", ShapefileTilesetBuilderAdapter); + +} // namespace pipeline::adapters::tileset diff --git a/src/pipeline/conversion_params.h b/src/pipeline/conversion_params.h new file mode 100644 index 00000000..8b5d321b --- /dev/null +++ b/src/pipeline/conversion_params.h @@ -0,0 +1,514 @@ +#pragma once + +/** + * @file conversion_params.h + * @brief 转换参数体系 - 阶段 1 重构 + * + * 解决新增数据类型时的开闭原则违反问题 + * 将类型特定参数从 ConversionParams 中分离 + */ + +#include "spatial_index.h" // 包含 SpatialIndexConfig +#include +#include +#include +#include +#include + +namespace pipeline { + +// ============================================ +// 前向声明 +// ============================================ + +struct ShapefileParams; +struct FBXParams; + +// ============================================ +// 基础参数接口 +// ============================================ + +/** + * @brief 数据源特定参数的抽象基类 + * + * 所有数据类型特定的参数类都应继承此类 + * 通过多态机制实现类型擦除,避免 ConversionParams 的修改 + */ +class DataSourceSpecificParams { +public: + virtual ~DataSourceSpecificParams() = default; + + /** + * @brief 获取参数类型标识 + * @return 类型名字符串(如 "shapefile", "fbx") + */ + [[nodiscard]] virtual const char* GetType() const = 0; + + /** + * @brief 验证参数有效性 + * @param error_msg 输出错误信息 + * @return 验证是否通过 + */ + [[nodiscard]] virtual bool Validate(std::string& error_msg) const = 0; + + /** + * @brief 克隆接口(支持深拷贝) + * @return 新的参数实例 + */ + [[nodiscard]] virtual std::unique_ptr Clone() const = 0; + +protected: + // 只允许通过 Clone 进行拷贝 + DataSourceSpecificParams() = default; + DataSourceSpecificParams(const DataSourceSpecificParams&) = default; + DataSourceSpecificParams& operator=(const DataSourceSpecificParams&) = default; + DataSourceSpecificParams(DataSourceSpecificParams&&) = default; + DataSourceSpecificParams& operator=(DataSourceSpecificParams&&) = default; +}; + +// ============================================ +// 模板基类(简化子类实现) +// ============================================ + +/** + * @brief 数据源特定参数的模板基类 + * @tparam Derived 派生类类型(CRTP 模式) + * + * 使用 CRTP 模式自动实现 GetType 和 Clone 方法 + */ +template +class DataSourceSpecificParamsBase : public DataSourceSpecificParams { +public: + [[nodiscard]] const char* GetType() const override { + return Derived::TypeName(); + } + + [[nodiscard]] std::unique_ptr Clone() const override { + return std::make_unique(static_cast(*this)); + } + + // 允许默认拷贝和移动(派生类通常是 POD 类型) + DataSourceSpecificParamsBase(const DataSourceSpecificParamsBase&) = default; + DataSourceSpecificParamsBase& operator=(const DataSourceSpecificParamsBase&) = default; + DataSourceSpecificParamsBase(DataSourceSpecificParamsBase&&) = default; + DataSourceSpecificParamsBase& operator=(DataSourceSpecificParamsBase&&) = default; + +protected: + DataSourceSpecificParamsBase() = default; +}; + +// ============================================ +// Shapefile 特定参数 +// ============================================ + +/** + * @brief Shapefile 数据源的特定参数 + */ +struct ShapefileParams : public DataSourceSpecificParamsBase { + /** + * @brief 获取类型名称 + * @return "shapefile" + */ + static constexpr const char* TypeName() { return "shapefile"; } + + // Shapefile 特定参数 + std::string height_field; ///< 高度字段名 + int layer_id = 0; ///< 图层索引 + + // 默认构造函数 + ShapefileParams() = default; + + // 默认拷贝和移动 + ShapefileParams(const ShapefileParams&) = default; + ShapefileParams& operator=(const ShapefileParams&) = default; + ShapefileParams(ShapefileParams&&) = default; + ShapefileParams& operator=(ShapefileParams&&) = default; + + /** + * @brief 验证参数有效性 + */ + [[nodiscard]] bool Validate(std::string& error_msg) const override { + if (layer_id < 0) { + error_msg = "layer_id must be non-negative"; + return false; + } + return true; + } +}; + +// ============================================ +// FBX 特定参数 +// ============================================ + +/** + * @brief FBX 数据源的特定参数 + */ +struct FBXParams : public DataSourceSpecificParamsBase { + /** + * @brief 获取类型名称 + * @return "fbx" + */ + static constexpr const char* TypeName() { return "fbx"; } + + // FBX 特定参数(地理参考) + double longitude = 0.0; ///< 中心经度(度) + double latitude = 0.0; ///< 中心纬度(度) + double height = 0.0; ///< 中心高度(米) + + // 默认构造函数 + FBXParams() = default; + + // 默认拷贝和移动 + FBXParams(const FBXParams&) = default; + FBXParams& operator=(const FBXParams&) = default; + FBXParams(FBXParams&&) = default; + FBXParams& operator=(FBXParams&&) = default; + + /** + * @brief 验证参数有效性 + */ + [[nodiscard]] bool Validate(std::string& error_msg) const override { + if (longitude < -180.0 || longitude > 180.0) { + error_msg = "longitude out of range [-180, 180]"; + return false; + } + if (latitude < -90.0 || latitude > 90.0) { + error_msg = "latitude out of range [-90, 90]"; + return false; + } + return true; + } +}; + +// ============================================ +// 未来新增类型示例: OBJ +// ============================================ + +/** + * @brief OBJ 数据源的特定参数(示例) + * + * 新增数据类型时,只需创建类似的新结构体, + * 无需修改任何现有代码 + */ +struct OBJParams : public DataSourceSpecificParamsBase { + static constexpr const char* TypeName() { return "obj"; } + + // OBJ 特定参数 + bool flip_uv_v = false; ///< 是否翻转 UV V 坐标 + bool generate_normals = false; ///< 是否自动生成法线 + std::string mtl_path; ///< 材质文件路径(可选) + + // 默认构造函数 + OBJParams() = default; + + // 默认拷贝和移动 + OBJParams(const OBJParams&) = default; + OBJParams& operator=(const OBJParams&) = default; + OBJParams(OBJParams&&) = default; + OBJParams& operator=(OBJParams&&) = default; + + [[nodiscard]] bool Validate(std::string& error_msg) const override { + // OBJ 参数验证逻辑 + (void)error_msg; + return true; + } +}; + +// ============================================ +// 通用处理选项 +// ============================================ + +/** + * @brief 通用处理选项 + * + * 适用于所有数据类型的处理选项 + */ +struct ProcessingOptions { + bool enable_lod = false; ///< 是否启用 LOD + bool enable_draco = false; ///< 是否启用 Draco 压缩 + bool enable_texture_compress = false; ///< 是否启用纹理压缩 + bool enable_meshopt = false; ///< 是否启用 meshoptimizer + bool enable_simplify = false; ///< 是否启用网格简化 + bool enable_unlit = false; ///< 是否使用 unlit 材质 + + /** + * @brief 验证选项有效性 + */ + [[nodiscard]] bool IsValid() const { + // 当前所有组合都有效 + return true; + } +}; + +// ============================================ +// 通用转换参数(清理后) +// ============================================ + +/** + * @brief 统一的转换参数 + * + * 重构后的 ConversionParams,将类型特定参数分离到 specific 字段 + * 新增数据类型时无需修改此类 + */ +struct ConversionParams { + // 基础路径 + std::string input_path; ///< 输入文件路径 + std::string output_path; ///< 输出目录路径 + std::string source_type; ///< 数据源类型(用于工厂查找) + + // 类型特定参数(使用类型擦除) + std::unique_ptr specific; + + // 通用处理选项 + ProcessingOptions options; + + // 空间索引配置 + SpatialIndexConfig spatial_config; + + // ========== Deprecated 兼容层 ========== + // 以下字段为向后兼容保留,将在未来版本中移除 +#pragma deprecated("Use specific() instead") + std::string height_field; ///< 【已废弃】请使用 specific()->height_field + +#pragma deprecated("Use specific() instead") + int layer_id = 0; ///< 【已废弃】请使用 specific()->layer_id + +#pragma deprecated("Use specific() instead") + double longitude = 0.0; ///< 【已废弃】请使用 specific()->longitude + +#pragma deprecated("Use specific() instead") + double latitude = 0.0; ///< 【已废弃】请使用 specific()->latitude + +#pragma deprecated("Use specific() instead") + double height = 0.0; ///< 【已废弃】请使用 specific()->height + + // ====================================== + + // 默认构造函数 + ConversionParams() = default; + + // 移动构造函数 + ConversionParams(ConversionParams&&) = default; + + // 移动赋值运算符(显式删除,因为引用成员无法移动赋值) + ConversionParams& operator=(ConversionParams&&) = delete; + + // 拷贝构造函数(深拷贝 specific) + ConversionParams(const ConversionParams& other) + : input_path(other.input_path) + , output_path(other.output_path) + , source_type(other.source_type) + , specific(other.specific ? other.specific->Clone() : nullptr) + , options(other.options) + , spatial_config(other.spatial_config) + , height_field(other.height_field) + , layer_id(other.layer_id) + , longitude(other.longitude) + , latitude(other.latitude) + , height(other.height) {} + + // 拷贝赋值运算符 + ConversionParams& operator=(const ConversionParams& other) { + if (this != &other) { + input_path = other.input_path; + output_path = other.output_path; + source_type = other.source_type; + specific = other.specific ? other.specific->Clone() : nullptr; + options = other.options; + spatial_config = other.spatial_config; + height_field = other.height_field; + layer_id = other.layer_id; + longitude = other.longitude; + latitude = other.latitude; + height = other.height; + } + return *this; + } + + // ============================================ + // 便利方法 + // ============================================ + + /** + * @brief 获取类型特定参数 + * @tparam T 参数类型(如 ShapefileParams, FBXParams) + * @return 类型特定参数的指针,类型不匹配时返回 nullptr + */ + template + [[nodiscard]] const T* GetSpecific() const { + if (specific && specific->GetType() == T::TypeName()) { + return static_cast(specific.get()); + } + return nullptr; + } + + // ========== 向后兼容的访问函数 ========== + + [[deprecated("Use options.enable_lod instead")]] + bool& EnableLOD() { return options.enable_lod; } + [[deprecated("Use options.enable_lod instead")]] + const bool& EnableLOD() const { return options.enable_lod; } + + [[deprecated("Use options.enable_draco instead")]] + bool& EnableDraco() { return options.enable_draco; } + [[deprecated("Use options.enable_draco instead")]] + const bool& EnableDraco() const { return options.enable_draco; } + + [[deprecated("Use options.enable_texture_compress instead")]] + bool& EnableTextureCompress() { return options.enable_texture_compress; } + [[deprecated("Use options.enable_texture_compress instead")]] + const bool& EnableTextureCompress() const { return options.enable_texture_compress; } + + [[deprecated("Use options.enable_meshopt instead")]] + bool& EnableMeshOpt() { return options.enable_meshopt; } + [[deprecated("Use options.enable_meshopt instead")]] + const bool& EnableMeshOpt() const { return options.enable_meshopt; } + + [[deprecated("Use options.enable_simplify instead")]] + bool& EnableSimplify() { return options.enable_simplify; } + [[deprecated("Use options.enable_simplify instead")]] + const bool& EnableSimplify() const { return options.enable_simplify; } + + [[deprecated("Use options.enable_unlit instead")]] + bool& EnableUnlit() { return options.enable_unlit; } + [[deprecated("Use options.enable_unlit instead")]] + const bool& EnableUnlit() const { return options.enable_unlit; } + + [[deprecated("Use spatial_config.max_depth instead")]] + int& MaxDepth() { return spatial_config.max_depth; } + [[deprecated("Use spatial_config.max_depth instead")]] + const int& MaxDepth() const { return spatial_config.max_depth; } + + [[deprecated("Use spatial_config.max_items_per_node instead")]] + size_t& MaxItemsPerNode() { return spatial_config.max_items_per_node; } + [[deprecated("Use spatial_config.max_items_per_node instead")]] + const size_t& MaxItemsPerNode() const { return spatial_config.max_items_per_node; } + + [[deprecated("Use spatial_config.min_bounds_size instead")]] + double& MinBoundsSize() { return spatial_config.min_bounds_size; } + [[deprecated("Use spatial_config.min_bounds_size instead")]] + const double& MinBoundsSize() const { return spatial_config.min_bounds_size; } + + // ============================================ + + /** + * @brief 验证所有参数 + * @param error_msg 输出错误信息 + * @return 验证是否通过 + */ + [[nodiscard]] bool Validate(std::string& error_msg) const { + if (input_path.empty()) { + error_msg = "input_path is required"; + return false; + } + if (output_path.empty()) { + error_msg = "output_path is required"; + return false; + } + if (source_type.empty()) { + error_msg = "source_type is required"; + return false; + } + // 验证空间索引配置 + if (spatial_config.max_depth <= 0) { + error_msg = "max_depth must be positive"; + return false; + } + if (spatial_config.max_items_per_node == 0) { + error_msg = "max_items_per_node must be non-zero"; + return false; + } + if (spatial_config.min_bounds_size <= 0.0) { + error_msg = "min_bounds_size must be positive"; + return false; + } + if (!options.IsValid()) { + error_msg = "invalid processing options"; + return false; + } + if (specific && !specific->Validate(error_msg)) { + return false; + } + return true; + } + + /** + * @brief 从旧版参数迁移 + * + * 将废弃字段的值迁移到新的 specific 字段 + * 用于向后兼容 + */ + void MigrateFromLegacy() { + if (specific) { + // 已经使用新格式,无需迁移 + return; + } + + // 根据 source_type 和旧字段创建 specific + if (source_type == "shapefile" && !height_field.empty()) { + auto params = std::make_unique(); + params->height_field = height_field; + params->layer_id = layer_id; + specific = std::move(params); + } else if (source_type == "fbx") { + auto params = std::make_unique(); + params->longitude = longitude; + params->latitude = latitude; + params->height = height; + specific = std::move(params); + } + } + + /** + * @brief 检查是否需要迁移 + */ + [[nodiscard]] bool NeedsMigration() const { + return !specific && ( + !height_field.empty() || + layer_id != 0 || + longitude != 0.0 || + latitude != 0.0 || + height != 0.0 + ); + } +}; + +// ============================================ +// 转换结果 +// ============================================ + +/** + * @brief 转换结果 + */ +struct ConversionResult { + bool success = false; ///< 是否成功 + std::string error_message; ///< 错误信息(失败时) + int node_count = 0; ///< 生成的节点数 + int b3dm_count = 0; ///< 生成的 B3DM 文件数 + std::string tileset_path; ///< tileset.json 路径 + + /** + * @brief 创建成功结果 + */ + static ConversionResult Success(int nodes, int b3dms, std::string path) { + return {true, "", nodes, b3dms, std::move(path)}; + } + + /** + * @brief 创建失败结果 + */ + static ConversionResult Failure(std::string msg) { + return {false, std::move(msg), 0, 0, ""}; + } +}; + +// ============================================ +// 进度回调 +// ============================================ + +/** + * @brief 进度回调函数类型 + */ +using ProgressCallback = std::function; + +} // namespace pipeline diff --git a/src/pipeline/conversion_pipeline.cpp b/src/pipeline/conversion_pipeline.cpp new file mode 100644 index 00000000..c2c993ba --- /dev/null +++ b/src/pipeline/conversion_pipeline.cpp @@ -0,0 +1,76 @@ +#include "conversion_pipeline.h" +#include "pipeline_factory.h" +#include +#include + +namespace pipeline { + +// ============================================ +// 向后兼容的旧工厂实现 +// ============================================ + +auto OldPipelineFactory::Instance() noexcept -> OldPipelineFactory& { + static OldPipelineFactory instance; + return instance; +} + +void OldPipelineFactory::Register(const std::string& type, PipelineCreator creator) { + creators_[type] = std::move(creator); +} + +auto OldPipelineFactory::Create(const std::string& type) const -> ConversionPipelinePtr { + auto it = creators_.find(type); + if (it != creators_.end()) { + return it->second(); + } + return nullptr; +} + +auto OldPipelineFactory::IsRegistered(const std::string& type) const noexcept -> bool { + return creators_.find(type) != creators_.end(); +} + +} // namespace pipeline + +// ============================================ +// C API 实现 +// ============================================ + +extern "C" { + +bool convert_with_pipeline(const pipeline::ConversionParams* params) { + if (!params) { + std::cerr << "[convert_with_pipeline] params is null" << std::endl; + return false; + } + + // 验证参数 + std::string error_msg; + if (!params->Validate(error_msg)) { + std::cerr << "[convert_with_pipeline] Validation failed: " << error_msg << std::endl; + return false; + } + + // 检查是否需要迁移旧参数 + if (params->NeedsMigration()) { + std::cerr << "[convert_with_pipeline] Warning: Using deprecated params format, " + << "please migrate to new format" << std::endl; + // 注意:这里不能直接修改 const 参数,实际使用时应先复制 + } + + // 使用新工厂创建管道 + auto& factory = pipeline::PipelineFactory::Instance(); + auto pipeline = factory.Create(params->source_type); + + if (!pipeline) { + std::cerr << "[convert_with_pipeline] Failed to create pipeline for type: " + << params->source_type << std::endl; + return false; + } + + // 执行转换 + auto result = pipeline->Convert(*params); + return result.success; +} + +} // extern "C" diff --git a/src/pipeline/conversion_pipeline.h b/src/pipeline/conversion_pipeline.h new file mode 100644 index 00000000..4a406260 --- /dev/null +++ b/src/pipeline/conversion_pipeline.h @@ -0,0 +1,129 @@ +#pragma once + +/** + * @file conversion_pipeline.h + * @brief 统一转换管道接口 - 阶段 1 重构 + * + * 整合步骤1-3的抽象接口,提供统一的转换管道 + * + * 注意:此文件现在使用 conversion_params.h 中的新参数体系 + * 旧的 PipelineFactory 定义已移至 pipeline_factory.h + */ + +#include "data_source.h" +#include "spatial_index.h" +#include "tileset_builder.h" +#include "conversion_params.h" +#include +#include +#include +#include + +namespace pipeline { + +// ============================================ +// 转换管道接口 +// ============================================ + +/** + * @brief 统一转换管道接口 + * + * 所有数据源转换管道都应实现此接口 + */ +class IConversionPipeline { +public: + virtual ~IConversionPipeline() = default; + + // 禁止拷贝,允许移动 + IConversionPipeline(const IConversionPipeline&) = delete; + IConversionPipeline& operator=(const IConversionPipeline&) = delete; + IConversionPipeline(IConversionPipeline&&) = default; + IConversionPipeline& operator=(IConversionPipeline&&) = default; + + // 设置数据源(可选,如果不设置则内部创建) + virtual void SetDataSource(std::unique_ptr dataSource) = 0; + + // 设置空间索引(可选,如果不设置则内部创建) + virtual void SetSpatialIndex(std::unique_ptr spatialIndex) = 0; + + // 设置 TilesetBuilder(可选,如果不设置则内部创建) + virtual void SetTilesetBuilder(std::unique_ptr tilesetBuilder) = 0; + + // 设置进度回调 + virtual void SetProgressCallback(ProgressCallback callback) = 0; + + // 执行转换 + virtual ConversionResult Convert(const ConversionParams& params) = 0; + +protected: + IConversionPipeline() = default; +}; + +// 注意:为了保持与 pipeline_factory.h 的一致性,使用 shared_ptr +using ConversionPipelinePtr = std::shared_ptr; +using PipelineCreator = std::function; + +// 为了保持向后兼容,保留 unique_ptr 版本 +using UniquePipelinePtr = std::unique_ptr; + +// ============================================ +// 向后兼容的工厂类(已废弃) +// ============================================ + +/** + * @deprecated 使用 pipeline_factory.h 中的 PipelineFactory + * + * 此类保留用于向后兼容,将在未来版本中移除。 + * 新的代码应使用 pipeline_factory.h 中的增强工厂类。 + */ +class [[deprecated("Use pipeline::PipelineFactory from pipeline_factory.h instead")]] +OldPipelineFactory { +public: + [[nodiscard]] static auto Instance() noexcept -> OldPipelineFactory&; + + void Register(const std::string& type, PipelineCreator creator); + [[nodiscard]] auto Create(const std::string& type) const -> ConversionPipelinePtr; + [[nodiscard]] auto IsRegistered(const std::string& type) const noexcept -> bool; + +private: + OldPipelineFactory() = default; + ~OldPipelineFactory() = default; + + std::unordered_map creators_; +}; + +// 为了保持向后兼容,提供类型别名 +using PipelineFactory [[deprecated("Use pipeline::PipelineFactory from pipeline_factory.h")]] = OldPipelineFactory; + +// ============================================ +// 向后兼容的注册宏(已废弃) +// ============================================ + +/** + * @deprecated 使用 pipeline_factory.h 中的 REGISTER_PIPELINE 宏 + */ +#define REGISTER_PIPELINE_OLD(TYPE, CLASS) \ + namespace { \ + [[maybe_unused]] const bool _##CLASS##_registered = []() -> bool { \ + ::pipeline::OldPipelineFactory::Instance().Register( \ + TYPE, []() -> ::pipeline::ConversionPipelinePtr { \ + return std::make_unique(); \ + }); \ + return true; \ + }(); \ + } + +} // namespace pipeline + +// ============================================ +// C API 接口 +// ============================================ + +extern "C" { + /** + * @brief 执行转换(使用新管道) + * @param params 转换参数 + * @return 是否成功 + */ + bool convert_with_pipeline(const pipeline::ConversionParams* params); +} diff --git a/src/pipeline/data_source.cpp b/src/pipeline/data_source.cpp new file mode 100644 index 00000000..3b19022f --- /dev/null +++ b/src/pipeline/data_source.cpp @@ -0,0 +1,27 @@ +#include "data_source.h" +#include + +namespace pipeline { + +auto DataSourceFactory::Instance() noexcept -> DataSourceFactory& { + static DataSourceFactory instance; + return instance; +} + +void DataSourceFactory::Register(const std::string& type, DataSourceCreator creator) { + creators_[type] = std::move(creator); +} + +auto DataSourceFactory::Create(const std::string& type) const -> DataSourcePtr { + auto it = creators_.find(type); + if (it != creators_.end()) { + return it->second(); + } + return nullptr; +} + +auto DataSourceFactory::IsRegistered(const std::string& type) const noexcept -> bool { + return creators_.find(type) != creators_.end(); +} + +} // namespace pipeline diff --git a/src/pipeline/data_source.h b/src/pipeline/data_source.h new file mode 100644 index 00000000..29b6f1da --- /dev/null +++ b/src/pipeline/data_source.h @@ -0,0 +1,133 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace pipeline { + +// 前向声明 +class ISpatialItem; +using SpatialItemPtr = std::shared_ptr; +using SpatialItemList = std::vector; + +// 数据源配置 - 使用聚合初始化 +struct DataSourceConfig { + std::filesystem::path input_path; + std::filesystem::path output_path; + + // 地理参考 + double center_longitude = 0.0; + double center_latitude = 0.0; + double center_height = 0.0; + + // Shapefile 特定 + std::string height_field; + + // 处理选项 + bool enable_simplification = false; + bool enable_draco = false; + bool enable_lod = false; +}; + +// 空间项接口 - 统一表示几何对象 +class ISpatialItem { +public: + virtual ~ISpatialItem() = default; + + // 禁止拷贝,允许移动 + ISpatialItem(const ISpatialItem&) = delete; + ISpatialItem& operator=(const ISpatialItem&) = delete; + ISpatialItem(ISpatialItem&&) = default; + ISpatialItem& operator=(ISpatialItem&&) = default; + + // 获取唯一ID + [[nodiscard]] virtual auto GetId() const -> uint64_t = 0; + + // 获取包围盒 (WGS84 经纬度/高度) + [[nodiscard]] virtual auto GetBounds() const + -> std::tuple = 0; + + // 获取几何数据 (OSG 格式) + [[nodiscard]] virtual auto GetGeometry() const + -> osg::ref_ptr = 0; + + // 获取属性数据 + [[nodiscard]] virtual auto GetProperties() const + -> std::unordered_map = 0; + +protected: + ISpatialItem() = default; +}; + +// 数据源接口 +class DataSource { +public: + virtual ~DataSource() = default; + + // 禁止拷贝,允许移动 + DataSource(const DataSource&) = delete; + DataSource& operator=(const DataSource&) = delete; + DataSource(DataSource&&) = default; + DataSource& operator=(DataSource&&) = default; + + // 加载数据 + [[nodiscard]] virtual auto Load(const DataSourceConfig& config) -> bool = 0; + + // 获取空间项列表 + [[nodiscard]] virtual auto GetSpatialItems() const -> SpatialItemList = 0; + + // 获取世界包围盒 + [[nodiscard]] virtual auto GetWorldBounds() const + -> std::tuple = 0; + + // 获取地理参考 + [[nodiscard]] virtual auto GetGeoReference() const + -> std::tuple = 0; + + // 获取数据项数量 + [[nodiscard]] virtual auto GetItemCount() const noexcept -> std::size_t = 0; + + // 是否已加载 + [[nodiscard]] virtual auto IsLoaded() const noexcept -> bool = 0; + +protected: + DataSource() = default; +}; + +using DataSourcePtr = std::unique_ptr; +using DataSourceCreator = std::function; + +// 数据源工厂 - 单例注册模式 +class DataSourceFactory { +public: + [[nodiscard]] static auto Instance() noexcept -> DataSourceFactory&; + + void Register(const std::string& type, DataSourceCreator creator); + [[nodiscard]] auto Create(const std::string& type) const -> DataSourcePtr; + [[nodiscard]] auto IsRegistered(const std::string& type) const noexcept -> bool; + +private: + DataSourceFactory() = default; + ~DataSourceFactory() = default; + + std::unordered_map creators_; +}; + +// 数据源注册辅助宏 +#define REGISTER_DATA_SOURCE(TYPE, CLASS) \ + namespace { \ + [[maybe_unused]] const bool _##CLASS##_registered = []() -> bool { \ + ::pipeline::DataSourceFactory::Instance().Register( \ + TYPE, []() -> ::pipeline::DataSourcePtr { \ + return std::make_unique(); \ + }); \ + return true; \ + }(); \ + } + +} // namespace pipeline diff --git a/src/pipeline/fbx_pipeline.cpp b/src/pipeline/fbx_pipeline.cpp new file mode 100644 index 00000000..8c21a353 --- /dev/null +++ b/src/pipeline/fbx_pipeline.cpp @@ -0,0 +1,148 @@ +#include "fbx_pipeline.h" +#include "adapters/fbx/fbx_data_source.h" +#include "adapters/spatial/octree_index.h" +#include "adapters/tileset/fbx_tileset_builder.h" +#include "fbx/fbx_processor.h" +#include +#include + +namespace pipeline { + +FBXPipeline::FBXPipeline() = default; + +void FBXPipeline::SetDataSource(std::unique_ptr dataSource) { + externalDataSource_ = dataSource.get(); + dataSource_ = std::move(dataSource); +} + +void FBXPipeline::SetSpatialIndex(std::unique_ptr spatialIndex) { + externalSpatialIndex_ = spatialIndex.get(); + spatialIndex_ = std::move(spatialIndex); +} + +void FBXPipeline::SetTilesetBuilder(std::unique_ptr tilesetBuilder) { + externalTilesetBuilder_ = tilesetBuilder.get(); + tilesetBuilder_ = std::move(tilesetBuilder); +} + +void FBXPipeline::SetProgressCallback(ProgressCallback callback) { + progressCallback_ = std::move(callback); +} + +DataSource* FBXPipeline::GetCurrentDataSource() { + if (externalDataSource_) { + return externalDataSource_; + } + if (!dataSource_) { + dataSource_ = DataSourceFactory::Instance().Create("fbx"); + } + return dataSource_.get(); +} + +ISpatialIndex* FBXPipeline::GetCurrentSpatialIndex() { + if (externalSpatialIndex_) { + return externalSpatialIndex_; + } + if (!spatialIndex_) { + spatialIndex_ = SpatialIndexFactory::Instance().Create("octree"); + } + return spatialIndex_.get(); +} + +ITilesetBuilder* FBXPipeline::GetCurrentTilesetBuilder() { + if (externalTilesetBuilder_) { + return externalTilesetBuilder_; + } + if (!tilesetBuilder_) { + tilesetBuilder_ = TilesetBuilderFactory::Instance().Create("fbx"); + } + return tilesetBuilder_.get(); +} + +void FBXPipeline::ReportProgress(const std::string& stage, float progress) { + if (progressCallback_) { + progressCallback_(stage, progress); + } +} + +ConversionResult FBXPipeline::Convert(const ConversionParams& params) { + ConversionResult result; + result.success = false; + + ReportProgress("initialization", 0.0f); + + // 复用现有 FBXPipeline 实现(完全复用) + // 使用新的参数结构:options 和 spatial_config + PipelineSettings settings; + settings.inputPath = params.input_path; + settings.outputPath = params.output_path; + settings.maxDepth = params.spatial_config.max_depth; + settings.maxItemsPerTile = static_cast(params.spatial_config.max_items_per_node); + settings.enableSimplify = params.options.enable_simplify; + settings.enableDraco = params.options.enable_draco; + settings.enableTextureCompress = params.options.enable_texture_compress; + settings.enableLOD = params.options.enable_lod; + settings.enableUnlit = params.options.enable_unlit; + // 从 specific 参数获取 FBX 特定配置 + if (const auto* fbx_params = params.GetSpecific()) { + settings.longitude = fbx_params->longitude; + settings.latitude = fbx_params->latitude; + settings.height = fbx_params->height; + } else { + // 向后兼容:使用废弃的字段 + settings.longitude = params.longitude; + settings.latitude = params.latitude; + settings.height = params.height; + } + settings.geScale = 0.5; + + // 创建并运行管道 + ::FBXPipeline pipeline(settings); + + // 注入抽象接口组件(如果已设置) + auto* dataSource = GetCurrentDataSource(); + if (dataSource) { + pipeline.SetDataSource(dataSource); + } + + auto* spatialIndex = GetCurrentSpatialIndex(); + if (spatialIndex) { + pipeline.SetSpatialIndex(spatialIndex); + } + + auto* tilesetBuilder = GetCurrentTilesetBuilder(); + if (tilesetBuilder) { + pipeline.SetTilesetBuilder(tilesetBuilder); + } + + ReportProgress("processing", 0.5f); + + pipeline.run(); + + ReportProgress("completion", 1.0f); + + // 检查输出 + std::filesystem::path tilesetPath = std::filesystem::path(params.output_path) / "tileset.json"; + if (std::filesystem::exists(tilesetPath)) { + result.success = true; + result.tileset_path = tilesetPath.string(); + + // 统计 B3DM 文件 + int b3dmCount = 0; + for (const auto& entry : std::filesystem::recursive_directory_iterator(params.output_path)) { + if (entry.path().extension() == ".b3dm") { + b3dmCount++; + } + } + result.b3dm_count = b3dmCount; + + std::cout << "[FBXPipeline] Success: " << result.b3dm_count << " B3DM files generated" << std::endl; + } else { + result.error_message = "tileset.json not generated"; + std::cerr << "[FBXPipeline] Failed: " << result.error_message << std::endl; + } + + return result; +} + +} // namespace pipeline diff --git a/src/pipeline/fbx_pipeline.h b/src/pipeline/fbx_pipeline.h new file mode 100644 index 00000000..aff46982 --- /dev/null +++ b/src/pipeline/fbx_pipeline.h @@ -0,0 +1,69 @@ +#pragma once + +/** + * @file fbx_pipeline.h + * @brief FBX 转换管道 - 步骤4 + * + * 使用步骤1-3的抽象接口实现 FBX 转换 + */ + +#include "conversion_pipeline.h" +#include + +namespace pipeline { + +// FBX 转换管道实现 +class FBXPipeline : public IConversionPipeline { +public: + FBXPipeline(); + ~FBXPipeline() override = default; + + // 禁止拷贝,允许移动 + FBXPipeline(const FBXPipeline&) = delete; + FBXPipeline& operator=(const FBXPipeline&) = delete; + FBXPipeline(FBXPipeline&&) = default; + FBXPipeline& operator=(FBXPipeline&&) = default; + + // 设置数据源 + void SetDataSource(std::unique_ptr dataSource) override; + + // 设置空间索引 + void SetSpatialIndex(std::unique_ptr spatialIndex) override; + + // 设置 TilesetBuilder + void SetTilesetBuilder(std::unique_ptr tilesetBuilder) override; + + // 设置进度回调 + void SetProgressCallback(ProgressCallback callback) override; + + // 执行转换 + ConversionResult Convert(const ConversionParams& params) override; + +private: + // 内部组件 + std::unique_ptr dataSource_; + std::unique_ptr spatialIndex_; + std::unique_ptr tilesetBuilder_; + + // 外部注入的组件 + DataSource* externalDataSource_ = nullptr; + ISpatialIndex* externalSpatialIndex_ = nullptr; + ITilesetBuilder* externalTilesetBuilder_ = nullptr; + + // 进度回调 + ProgressCallback progressCallback_; + + // 获取当前数据源 + [[nodiscard]] DataSource* GetCurrentDataSource(); + + // 获取当前空间索引 + [[nodiscard]] ISpatialIndex* GetCurrentSpatialIndex(); + + // 获取当前 TilesetBuilder + [[nodiscard]] ITilesetBuilder* GetCurrentTilesetBuilder(); + + // 报告进度 + void ReportProgress(const std::string& stage, float progress); +}; + +} // namespace pipeline diff --git a/src/pipeline/pipeline_factory.cpp b/src/pipeline/pipeline_factory.cpp new file mode 100644 index 00000000..b90645b5 --- /dev/null +++ b/src/pipeline/pipeline_factory.cpp @@ -0,0 +1,94 @@ +#include "pipeline_factory.h" +#include + +namespace pipeline { + +// ============================================ +// 工厂单例实现 +// ============================================ + +PipelineFactoryV2& PipelineFactoryV2::Instance() noexcept { + static PipelineFactoryV2 instance; + return instance; +} + +// ============================================ +// 注册和创建 +// ============================================ + +void PipelineFactoryV2::Register(PipelineCreatorPtr creator) { + if (!creator) { + return; + } + const auto& metadata = creator->GetMetadata(); + creators_[metadata.type_name] = std::move(creator); +} + +ConversionPipelinePtr PipelineFactoryV2::Create(const std::string& type) const { + auto it = creators_.find(type); + if (it != creators_.end()) { + return it->second->Create(); + } + return nullptr; +} + +// ============================================ +// 元数据查询 +// ============================================ + +const PipelineMetadata* PipelineFactoryV2::GetMetadata(const std::string& type) const { + auto it = creators_.find(type); + if (it != creators_.end()) { + return &it->second->GetMetadata(); + } + return nullptr; +} + +std::vector PipelineFactoryV2::ListRegisteredTypes() const { + std::vector types; + types.reserve(creators_.size()); + for (const auto& [type, _] : creators_) { + types.push_back(type); + } + return types; +} + +std::string PipelineFactoryV2::FindPipelineForExtension(const std::string& ext) const { + for (const auto& [type, creator] : creators_) { + if (creator->GetMetadata().SupportsExtension(ext)) { + return type; + } + } + return ""; +} + +// ============================================ +// 参数创建 +// ============================================ + +std::unique_ptr +PipelineFactoryV2::CreateDefaultParams(const std::string& type) const { + auto it = creators_.find(type); + if (it != creators_.end()) { + return it->second->CreateDefaultParams(); + } + return nullptr; +} + +// ============================================ +// 状态查询 +// ============================================ + +bool PipelineFactoryV2::IsRegistered(const std::string& type) const { + return creators_.find(type) != creators_.end(); +} + +void PipelineFactoryV2::Unregister(const std::string& type) { + creators_.erase(type); +} + +void PipelineFactoryV2::Clear() { + creators_.clear(); +} + +} // namespace pipeline diff --git a/src/pipeline/pipeline_factory.h b/src/pipeline/pipeline_factory.h new file mode 100644 index 00000000..a17a90b6 --- /dev/null +++ b/src/pipeline/pipeline_factory.h @@ -0,0 +1,250 @@ +#pragma once + +/** + * @file pipeline_factory.h + * @brief 增强的管道工厂 - 阶段 1 重构 + * + * 支持动态管道注册和元数据查询 + * 实现插件式扩展机制 + * + * 注意:此文件替代 conversion_pipeline.h 中的旧 PipelineFactory + */ + +#include "conversion_params.h" +#include +#include +#include +#include +#include + +namespace pipeline { + +// ============================================ +// 前向声明和类型定义 +// ============================================ + +class IConversionPipeline; + +// 使用 shared_ptr 避免需要完整类型声明 +// 在 C++20 中,unique_ptr 需要完整类型,但 shared_ptr 不需要 +using ConversionPipelinePtr = std::shared_ptr; + +// ============================================ +// 管道元数据 +// ============================================ + +/** + * @brief 管道元数据结构 + * + * 描述管道的基本信息和能力 + */ +struct PipelineMetadata { + std::string type_name; ///< 类型标识(如 "shapefile", "fbx") + std::string display_name; ///< 显示名称(如 "Shapefile Converter") + std::string description; ///< 描述信息 + std::vector supported_extensions; ///< 支持的文件扩展名(如 ".shp", ".fbx") + std::string version = "1.0"; ///< 版本号 + + /** + * @brief 检查是否支持指定扩展名 + */ + [[nodiscard]] bool SupportsExtension(const std::string& ext) const { + for (const auto& supported : supported_extensions) { + if (supported == ext) { + return true; + } + } + return false; + } +}; + +// ============================================ +// 管道创建器接口 +// ============================================ + +/** + * @brief 管道创建器接口 + * + * 封装管道创建逻辑和元数据 + */ +class IPipelineCreator { +public: + virtual ~IPipelineCreator() = default; + + /** + * @brief 创建管道实例 + */ + [[nodiscard]] virtual ConversionPipelinePtr Create() const = 0; + + /** + * @brief 获取管道元数据 + */ + [[nodiscard]] virtual const PipelineMetadata& GetMetadata() const = 0; + + /** + * @brief 创建默认参数 + */ + [[nodiscard]] virtual std::unique_ptr + CreateDefaultParams() const = 0; + +protected: + IPipelineCreator() = default; + IPipelineCreator(const IPipelineCreator&) = default; + IPipelineCreator& operator=(const IPipelineCreator&) = default; +}; + +using PipelineCreatorPtr = std::unique_ptr; + +// ============================================ +// 模板化的创建器实现 +// ============================================ + +/** + * @brief 模板化的管道创建器实现 + * @tparam PipelineType 管道类类型 + * @tparam ParamsType 参数类类型 + */ +template +class PipelineCreatorImpl : public IPipelineCreator { +public: + explicit PipelineCreatorImpl(PipelineMetadata metadata) + : metadata_(std::move(metadata)) {} + + [[nodiscard]] ConversionPipelinePtr Create() const override { + return std::make_unique(); + } + + [[nodiscard]] const PipelineMetadata& GetMetadata() const override { + return metadata_; + } + + [[nodiscard]] std::unique_ptr + CreateDefaultParams() const override { + return std::make_unique(); + } + +private: + PipelineMetadata metadata_; +}; + +// ============================================ +// 增强的工厂类 +// ============================================ + +/** + * @brief 增强的管道工厂 + * + * 支持动态注册、元数据查询和扩展名映射 + * + * 注意:此类替代 conversion_pipeline.h 中的 OldPipelineFactory + */ +class PipelineFactoryV2 { +public: + /** + * @brief 获取工厂单例实例 + */ + [[nodiscard]] static PipelineFactoryV2& Instance() noexcept; + + /** + * @brief 注册管道 + * @param creator 管道创建器 + */ + void Register(PipelineCreatorPtr creator); + + /** + * @brief 创建管道实例 + * @param type 管道类型名 + * @return 管道实例,未找到时返回 nullptr + */ + [[nodiscard]] ConversionPipelinePtr Create(const std::string& type) const; + + /** + * @brief 获取管道元数据 + * @param type 管道类型名 + * @return 元数据指针,未找到时返回 nullptr + */ + [[nodiscard]] const PipelineMetadata* GetMetadata(const std::string& type) const; + + /** + * @brief 列出所有已注册管道类型 + */ + [[nodiscard]] std::vector ListRegisteredTypes() const; + + /** + * @brief 根据文件扩展名查找管道 + * @param ext 扩展名(如 ".shp", ".fbx") + * @return 管道类型名,未找到时返回空字符串 + */ + [[nodiscard]] std::string FindPipelineForExtension(const std::string& ext) const; + + /** + * @brief 创建默认参数 + * @param type 管道类型名 + * @return 默认参数实例,未找到时返回 nullptr + */ + [[nodiscard]] std::unique_ptr + CreateDefaultParams(const std::string& type) const; + + /** + * @brief 检查是否已注册 + */ + [[nodiscard]] bool IsRegistered(const std::string& type) const; + + /** + * @brief 注销管道(主要用于测试) + */ + void Unregister(const std::string& type); + + /** + * @brief 清空所有注册(主要用于测试) + */ + void Clear(); + +private: + PipelineFactoryV2() = default; + ~PipelineFactoryV2() = default; + PipelineFactoryV2(const PipelineFactoryV2&) = delete; + PipelineFactoryV2& operator=(const PipelineFactoryV2&) = delete; + + std::unordered_map creators_; +}; + +// ============================================ +// 自动注册宏 +// ============================================ + +/** + * @brief 管道自动注册宏 + * + * 在管道实现文件中使用此宏自动注册到工厂 + * + * @param PipelineClass 管道类名(如 ShapefilePipeline) + * @param ParamsClass 参数类名(如 ShapefileParams) + * @param TypeName 类型标识(如 "shapefile") + * @param DisplayName 显示名称(如 "Shapefile Converter") + * @param ... 支持的扩展名列表(如 ".shp", ".shx", ".dbf") + * + * 示例: + * @code + * REGISTER_PIPELINE(ShapefilePipeline, ShapefileParams, "shapefile", + * "Shapefile Converter", ".shp", ".shx", ".dbf") + * @endcode + */ +#define REGISTER_PIPELINE(PipelineClass, ParamsClass, TypeName, DisplayName, ...) \ + namespace { \ + struct PipelineClass##_Registrar { \ + PipelineClass##_Registrar() { \ + ::pipeline::PipelineMetadata metadata; \ + metadata.type_name = TypeName; \ + metadata.display_name = DisplayName; \ + metadata.supported_extensions = {__VA_ARGS__}; \ + auto creator = std::make_unique< \ + ::pipeline::PipelineCreatorImpl>( \ + metadata); \ + ::pipeline::PipelineFactoryV2::Instance().Register(std::move(creator)); \ + } \ + }; \ + static PipelineClass##_Registrar PipelineClass##_instance; \ + } + +} // namespace pipeline diff --git a/src/pipeline/pipeline_registration.cpp b/src/pipeline/pipeline_registration.cpp new file mode 100644 index 00000000..164bf143 --- /dev/null +++ b/src/pipeline/pipeline_registration.cpp @@ -0,0 +1,30 @@ +#include "conversion_pipeline.h" +#include "shapefile_pipeline.h" +#include "fbx_pipeline.h" +#include + +namespace pipeline { + +// 注册管道到工厂 +// 使用静态初始化确保在 main 之前注册 + +struct PipelineRegistrar { + PipelineRegistrar() { + // 注册 Shapefile 管道 + PipelineFactory::Instance().Register("shapefile", []() -> ConversionPipelinePtr { + return std::make_unique(); + }); + + // 注册 FBX 管道 + PipelineFactory::Instance().Register("fbx", []() -> ConversionPipelinePtr { + return std::make_unique(); + }); + + std::cout << "[PipelineRegistrar] Registered pipelines: shapefile, fbx" << std::endl; + } +}; + +// 静态实例,确保在程序启动时注册 +static PipelineRegistrar registrar; + +} // namespace pipeline diff --git a/src/pipeline/shapefile_pipeline.cpp b/src/pipeline/shapefile_pipeline.cpp new file mode 100644 index 00000000..c443da4c --- /dev/null +++ b/src/pipeline/shapefile_pipeline.cpp @@ -0,0 +1,194 @@ +#include "shapefile_pipeline.h" +#include "adapters/shapefile/shapefile_data_source.h" +#include "adapters/spatial/quadtree_index.h" +#include "adapters/tileset/shapefile_tileset_builder.h" +#include "../shapefile/shapefile_processor.h" +#include "../shape.h" +#include +#include +#include + +namespace pipeline { + +ShapefilePipeline::ShapefilePipeline() = default; + +void ShapefilePipeline::SetDataSource(std::unique_ptr dataSource) { + externalDataSource_ = dataSource.get(); + dataSource_ = std::move(dataSource); +} + +void ShapefilePipeline::SetSpatialIndex(std::unique_ptr spatialIndex) { + externalSpatialIndex_ = spatialIndex.get(); + spatialIndex_ = std::move(spatialIndex); +} + +void ShapefilePipeline::SetTilesetBuilder(std::unique_ptr tilesetBuilder) { + externalTilesetBuilder_ = tilesetBuilder.get(); + tilesetBuilder_ = std::move(tilesetBuilder); +} + +void ShapefilePipeline::SetProgressCallback(ProgressCallback callback) { + progressCallback_ = std::move(callback); +} + +DataSource* ShapefilePipeline::GetCurrentDataSource() { + if (externalDataSource_) { + return externalDataSource_; + } + if (!dataSource_) { + dataSource_ = DataSourceFactory::Instance().Create("shapefile"); + } + return dataSource_.get(); +} + +ISpatialIndex* ShapefilePipeline::GetCurrentSpatialIndex() { + if (externalSpatialIndex_) { + return externalSpatialIndex_; + } + if (!spatialIndex_) { + spatialIndex_ = SpatialIndexFactory::Instance().Create("quadtree"); + } + return spatialIndex_.get(); +} + +ITilesetBuilder* ShapefilePipeline::GetCurrentTilesetBuilder() { + if (externalTilesetBuilder_) { + return externalTilesetBuilder_; + } + if (!tilesetBuilder_) { + tilesetBuilder_ = TilesetBuilderFactory::Instance().Create("shapefile"); + } + return tilesetBuilder_.get(); +} + +void ShapefilePipeline::ReportProgress(const std::string& stage, float progress) { + if (progressCallback_) { + progressCallback_(stage, progress); + } +} + +ConversionResult ShapefilePipeline::Convert(const ConversionParams& params) { + ConversionResult result; + result.success = false; + + ReportProgress("initialization", 0.0f); + + // 复用 ShapefileProcessor 处理数据(完全复用现有实现) + // 使用新的参数结构:options 和 spatial_config + shapefile::ShapefileProcessorConfig config; + config.inputPath = params.input_path; + config.outputPath = params.output_path; + + // 从 specific 参数获取 Shapefile 特定配置 + if (const auto* shp_params = params.GetSpecific()) { + config.heightField = shp_params->height_field; + // 注意:ShapefileProcessorConfig 没有 layerId 字段,使用默认值 0 + (void)shp_params->layer_id; // 避免未使用警告 + } else { + // 向后兼容:使用废弃的字段 + config.heightField = params.height_field; + } + + // 使用通用选项 + config.enableLOD = params.options.enable_lod; + config.enableSimplification = params.options.enable_simplify; + config.enableDraco = params.options.enable_draco; + + // 设置空间索引配置 + config.quadtreeConfig.maxDepth = static_cast(params.spatial_config.max_depth); + config.quadtreeConfig.maxItemsPerNode = params.spatial_config.max_items_per_node; + config.quadtreeConfig.minBoundsSize = params.spatial_config.min_bounds_size; + + // 创建处理器 + shapefile::ShapefileProcessor processor(config); + + // 注入抽象接口组件(如果已设置) + auto* dataSource = GetCurrentDataSource(); + if (dataSource) { + processor.SetDataSource(dataSource); + } + + auto* spatialIndex = GetCurrentSpatialIndex(); + if (spatialIndex) { + processor.SetSpatialIndex(spatialIndex); + } + + auto* tilesetBuilder = GetCurrentTilesetBuilder(); + if (tilesetBuilder) { + processor.SetTilesetBuilder(tilesetBuilder); + } + + ReportProgress("processing", 0.5f); + + // 执行处理 + auto processResult = processor.process(); + + ReportProgress("completion", 1.0f); + + if (processResult.success) { + result.success = true; + result.node_count = static_cast(processResult.nodeCount); + result.b3dm_count = static_cast(processResult.b3dmCount); + result.tileset_path = processResult.tilesetPath; + + std::cout << "[ShapefilePipeline] Success: " << result.node_count + << " nodes, " << result.b3dm_count << " B3DM files" << std::endl; + } else { + result.error_message = processResult.errorMessage; + std::cerr << "[ShapefilePipeline] Failed: " << result.error_message << std::endl; + } + + return result; +} + +} // namespace pipeline + +// C API 实现 - 从 shp23dtile.cpp 迁移过来 +extern "C" bool shp23dtile(const ShapeConversionParams* params) +{ + if (!params || !params->input_path || !params->output_path) { + return false; + } + + // 构建 ConversionParams + pipeline::ConversionParams pipelineParams; + pipelineParams.input_path = params->input_path; + pipelineParams.output_path = params->output_path; + pipelineParams.source_type = "shapefile"; + + // 创建 Shapefile 特定参数 + auto shpSpecific = std::make_unique(); + shpSpecific->height_field = params->height_field ? params->height_field : ""; + shpSpecific->layer_id = params->layer_id; + pipelineParams.specific = std::move(shpSpecific); + + // 设置通用选项 + pipelineParams.options.enable_lod = params->enable_lod; + pipelineParams.options.enable_simplify = params->simplify_params.enable_simplification; + pipelineParams.options.enable_draco = params->draco_compression_params.enable_compression; + + // 计算中心点 + GDALAllRegister(); + GDALDataset* poDS = (GDALDataset*)GDALOpenEx( + params->input_path, GDAL_OF_VECTOR, NULL, NULL, NULL); + if (poDS) { + OGRLayer* poLayer = poDS->GetLayer(params->layer_id); + if (poLayer) { + OGREnvelope envelope; + if (poLayer->GetExtent(&envelope, true) == OGRERR_NONE) { + pipelineParams.longitude = (envelope.MinX + envelope.MaxX) * 0.5; + pipelineParams.latitude = (envelope.MinY + envelope.MaxY) * 0.5; + } + } + GDALClose(poDS); + } + + // 创建管道并执行转换 + auto pipeline = pipeline::PipelineFactory::Instance().Create("shapefile"); + if (!pipeline) { + return false; + } + + auto result = pipeline->Convert(pipelineParams); + return result.success; +} diff --git a/src/pipeline/shapefile_pipeline.h b/src/pipeline/shapefile_pipeline.h new file mode 100644 index 00000000..99264a95 --- /dev/null +++ b/src/pipeline/shapefile_pipeline.h @@ -0,0 +1,69 @@ +#pragma once + +/** + * @file shapefile_pipeline.h + * @brief Shapefile 转换管道 - 步骤4 + * + * 使用步骤1-3的抽象接口实现 Shapefile 转换 + */ + +#include "conversion_pipeline.h" +#include + +namespace pipeline { + +// Shapefile 转换管道实现 +class ShapefilePipeline : public IConversionPipeline { +public: + ShapefilePipeline(); + ~ShapefilePipeline() override = default; + + // 禁止拷贝,允许移动 + ShapefilePipeline(const ShapefilePipeline&) = delete; + ShapefilePipeline& operator=(const ShapefilePipeline&) = delete; + ShapefilePipeline(ShapefilePipeline&&) = default; + ShapefilePipeline& operator=(ShapefilePipeline&&) = default; + + // 设置数据源 + void SetDataSource(std::unique_ptr dataSource) override; + + // 设置空间索引 + void SetSpatialIndex(std::unique_ptr spatialIndex) override; + + // 设置 TilesetBuilder + void SetTilesetBuilder(std::unique_ptr tilesetBuilder) override; + + // 设置进度回调 + void SetProgressCallback(ProgressCallback callback) override; + + // 执行转换 + ConversionResult Convert(const ConversionParams& params) override; + +private: + // 内部组件 + std::unique_ptr dataSource_; + std::unique_ptr spatialIndex_; + std::unique_ptr tilesetBuilder_; + + // 外部注入的组件 + DataSource* externalDataSource_ = nullptr; + ISpatialIndex* externalSpatialIndex_ = nullptr; + ITilesetBuilder* externalTilesetBuilder_ = nullptr; + + // 进度回调 + ProgressCallback progressCallback_; + + // 获取当前数据源 + [[nodiscard]] DataSource* GetCurrentDataSource(); + + // 获取当前空间索引 + [[nodiscard]] ISpatialIndex* GetCurrentSpatialIndex(); + + // 获取当前 TilesetBuilder + [[nodiscard]] ITilesetBuilder* GetCurrentTilesetBuilder(); + + // 报告进度 + void ReportProgress(const std::string& stage, float progress); +}; + +} // namespace pipeline diff --git a/src/pipeline/spatial_index.cpp b/src/pipeline/spatial_index.cpp new file mode 100644 index 00000000..df237cf3 --- /dev/null +++ b/src/pipeline/spatial_index.cpp @@ -0,0 +1,27 @@ +#include "spatial_index.h" +#include + +namespace pipeline { + +auto SpatialIndexFactory::Instance() noexcept -> SpatialIndexFactory& { + static SpatialIndexFactory instance; + return instance; +} + +void SpatialIndexFactory::Register(const std::string& type, SpatialIndexCreator creator) { + creators_[type] = std::move(creator); +} + +auto SpatialIndexFactory::Create(const std::string& type) const -> SpatialIndexPtr { + auto it = creators_.find(type); + if (it != creators_.end()) { + return it->second(); + } + return nullptr; +} + +auto SpatialIndexFactory::IsRegistered(const std::string& type) const noexcept -> bool { + return creators_.find(type) != creators_.end(); +} + +} // namespace pipeline diff --git a/src/pipeline/spatial_index.h b/src/pipeline/spatial_index.h new file mode 100644 index 00000000..c64da607 --- /dev/null +++ b/src/pipeline/spatial_index.h @@ -0,0 +1,137 @@ +#pragma once + +/** + * @file spatial_index.h + * @brief 空间索引抽象接口 + * + * 步骤2:定义统一的空间索引接口,用于替换直接使用 Quadtree/Octree + */ + +#include "data_source.h" +#include +#include +#include +#include + +namespace pipeline { + +// 空间索引节点接口 +class ISpatialIndexNode { +public: + virtual ~ISpatialIndexNode() = default; + + // 禁止拷贝,允许移动 + ISpatialIndexNode(const ISpatialIndexNode&) = delete; + ISpatialIndexNode& operator=(const ISpatialIndexNode&) = delete; + ISpatialIndexNode(ISpatialIndexNode&&) = default; + ISpatialIndexNode& operator=(ISpatialIndexNode&&) = default; + + // 获取节点唯一ID + [[nodiscard]] virtual auto GetId() const -> uint64_t = 0; + + // 获取节点深度 + [[nodiscard]] virtual auto GetDepth() const -> int = 0; + + // 获取包围盒 + [[nodiscard]] virtual auto GetBounds() const + -> std::tuple = 0; + + // 获取节点中的空间项 + [[nodiscard]] virtual auto GetItems() const -> SpatialItemList = 0; + + // 获取子节点 + [[nodiscard]] virtual auto GetChildren() const + -> std::vector = 0; + + // 是否为叶子节点 + [[nodiscard]] virtual auto IsLeaf() const -> bool = 0; + + // 获取对象数量 + [[nodiscard]] virtual auto GetItemCount() const -> std::size_t = 0; + +protected: + ISpatialIndexNode() = default; +}; + +// 空间索引配置 +struct SpatialIndexConfig { + // 最大深度 + int max_depth = 10; + + // 每个节点最大对象数 + std::size_t max_items_per_node = 1000; + + // 最小包围盒尺寸 + double min_bounds_size = 0.01; + + // 地理中心(用于坐标转换) + double center_longitude = 0.0; + double center_latitude = 0.0; + double center_height = 0.0; +}; + +// 空间索引接口 +class ISpatialIndex { +public: + virtual ~ISpatialIndex() = default; + + // 禁止拷贝,允许移动 + ISpatialIndex(const ISpatialIndex&) = delete; + ISpatialIndex& operator=(const ISpatialIndex&) = delete; + ISpatialIndex(ISpatialIndex&&) = default; + ISpatialIndex& operator=(ISpatialIndex&&) = default; + + // 构建索引 + [[nodiscard]] virtual auto Build(const DataSource* data_source, + const SpatialIndexConfig& config) -> bool = 0; + + // 获取根节点 + [[nodiscard]] virtual auto GetRootNode() const -> const ISpatialIndexNode* = 0; + + // 获取节点数量 + [[nodiscard]] virtual auto GetNodeCount() const -> std::size_t = 0; + + // 获取对象数量 + [[nodiscard]] virtual auto GetItemCount() const -> std::size_t = 0; + + // 查询指定范围内的对象 + [[nodiscard]] virtual auto Query(double minX, double minY, double minZ, + double maxX, double maxY, double maxZ) const + -> SpatialItemList = 0; + +protected: + ISpatialIndex() = default; +}; + +using SpatialIndexPtr = std::unique_ptr; +using SpatialIndexCreator = std::function; + +// 空间索引工厂 - 单例注册模式 +class SpatialIndexFactory { +public: + [[nodiscard]] static auto Instance() noexcept -> SpatialIndexFactory&; + + void Register(const std::string& type, SpatialIndexCreator creator); + [[nodiscard]] auto Create(const std::string& type) const -> SpatialIndexPtr; + [[nodiscard]] auto IsRegistered(const std::string& type) const noexcept -> bool; + +private: + SpatialIndexFactory() = default; + ~SpatialIndexFactory() = default; + + std::unordered_map creators_; +}; + +// 空间索引注册辅助宏 +#define REGISTER_SPATIAL_INDEX(TYPE, CLASS) \ + namespace { \ + [[maybe_unused]] const bool _##CLASS##_registered = []() -> bool { \ + ::pipeline::SpatialIndexFactory::Instance().Register( \ + TYPE, []() -> ::pipeline::SpatialIndexPtr { \ + return std::make_unique(); \ + }); \ + return true; \ + }(); \ + } + +} // namespace pipeline diff --git a/src/pipeline/tileset_builder.cpp b/src/pipeline/tileset_builder.cpp new file mode 100644 index 00000000..a636dea7 --- /dev/null +++ b/src/pipeline/tileset_builder.cpp @@ -0,0 +1,27 @@ +#include "tileset_builder.h" +#include + +namespace pipeline { + +auto TilesetBuilderFactory::Instance() noexcept -> TilesetBuilderFactory& { + static TilesetBuilderFactory instance; + return instance; +} + +void TilesetBuilderFactory::Register(const std::string& type, TilesetBuilderCreator creator) { + creators_[type] = std::move(creator); +} + +auto TilesetBuilderFactory::Create(const std::string& type) const -> TilesetBuilderPtr { + auto it = creators_.find(type); + if (it != creators_.end()) { + return it->second(); + } + return nullptr; +} + +auto TilesetBuilderFactory::IsRegistered(const std::string& type) const noexcept -> bool { + return creators_.find(type) != creators_.end(); +} + +} // namespace pipeline diff --git a/src/pipeline/tileset_builder.h b/src/pipeline/tileset_builder.h new file mode 100644 index 00000000..26cacbc2 --- /dev/null +++ b/src/pipeline/tileset_builder.h @@ -0,0 +1,151 @@ +#pragma once + +/** + * @file tileset_builder.h + * @brief TilesetBuilder 抽象接口 + * + * 步骤3:定义统一的 Tileset 构建接口,用于替换直接使用 ShapefileTilesetAdapter/FBXTilesetAdapter + */ + +#include "spatial_index.h" +#include "../tileset/tileset_types.h" +#include +#include +#include +#include + +namespace pipeline { + +// 瓦片元数据接口 +class ITileMeta { +public: + virtual ~ITileMeta() = default; + + // 禁止拷贝,允许移动 + ITileMeta(const ITileMeta&) = delete; + ITileMeta& operator=(const ITileMeta&) = delete; + ITileMeta(ITileMeta&&) = default; + ITileMeta& operator=(ITileMeta&&) = default; + + // 获取节点唯一ID + [[nodiscard]] virtual auto GetId() const -> uint64_t = 0; + + // 获取父节点ID (0表示根节点) + [[nodiscard]] virtual auto GetParentId() const -> uint64_t = 0; + + // 获取子节点ID列表 + [[nodiscard]] virtual auto GetChildIds() const -> std::vector = 0; + + // 获取包围盒 + [[nodiscard]] virtual auto GetBounds() const + -> std::tuple = 0; + + // 获取几何误差 + [[nodiscard]] virtual auto GetGeometricError() const -> double = 0; + + // 获取内容URI + [[nodiscard]] virtual auto GetContentUri() const -> std::string = 0; + + // 是否有内容 + [[nodiscard]] virtual auto HasContent() const -> bool = 0; + + // 获取深度/层级 + [[nodiscard]] virtual auto GetDepth() const -> int = 0; + +protected: + ITileMeta() = default; +}; + +using TileMetaPtr = std::shared_ptr; +using TileMetaMap = std::unordered_map; + +// Tileset 构建配置 +struct TilesetBuilderConfig { + // 版本信息 + std::string version = "1.0"; + std::string gltf_up_axis = "Z"; + + // 几何误差配置 + double root_geometric_error_multiplier = 2.0; + double child_geometric_error_multiplier = 0.5; + + // 包围盒扩展系数 + double bounding_volume_scale = 1.0; + + // 是否启用LOD + bool enable_lod = false; + + // LOD级别数量 + int lod_level_count = 1; + + // 细化策略 ("ADD" 或 "REPLACE") + std::string refine = "REPLACE"; + + // 地理中心 + double center_longitude = 0.0; + double center_latitude = 0.0; + double center_height = 0.0; +}; + +// Tileset 构建器接口 +class ITilesetBuilder { +public: + virtual ~ITilesetBuilder() = default; + + // 禁止拷贝,允许移动 + ITilesetBuilder(const ITilesetBuilder&) = delete; + ITilesetBuilder& operator=(const ITilesetBuilder&) = delete; + ITilesetBuilder(ITilesetBuilder&&) = default; + ITilesetBuilder& operator=(ITilesetBuilder&&) = default; + + // 初始化构建器 + virtual void Initialize(const TilesetBuilderConfig& config) = 0; + + // 添加瓦片元数据 + virtual void AddTileMeta(const TileMetaPtr& meta) = 0; + + // 构建 Tileset + [[nodiscard]] virtual auto BuildTileset() -> tileset::Tileset = 0; + + // 构建并写入文件 + [[nodiscard]] virtual auto BuildAndWrite(const std::string& output_path) -> bool = 0; + + // 清空所有元数据 + virtual void Clear() = 0; + +protected: + ITilesetBuilder() = default; +}; + +using TilesetBuilderPtr = std::unique_ptr; +using TilesetBuilderCreator = std::function; + +// TilesetBuilder 工厂 - 单例注册模式 +class TilesetBuilderFactory { +public: + [[nodiscard]] static auto Instance() noexcept -> TilesetBuilderFactory&; + + void Register(const std::string& type, TilesetBuilderCreator creator); + [[nodiscard]] auto Create(const std::string& type) const -> TilesetBuilderPtr; + [[nodiscard]] auto IsRegistered(const std::string& type) const noexcept -> bool; + +private: + TilesetBuilderFactory() = default; + ~TilesetBuilderFactory() = default; + + std::unordered_map creators_; +}; + +// TilesetBuilder 注册辅助宏 +#define REGISTER_TILESET_BUILDER(TYPE, CLASS) \ + namespace { \ + [[maybe_unused]] const bool _##CLASS##_registered = []() -> bool { \ + ::pipeline::TilesetBuilderFactory::Instance().Register( \ + TYPE, []() -> ::pipeline::TilesetBuilderPtr { \ + return std::make_unique(); \ + }); \ + return true; \ + }(); \ + } + +} // namespace pipeline diff --git a/src/shape.h b/src/shape.h index c4f3af5f..7b9f14cf 100644 --- a/src/shape.h +++ b/src/shape.h @@ -1,7 +1,7 @@ #pragma once #include "lod_pipeline.h" -#include "mesh_processor.h" +#include "common/mesh_processor.h" #include struct ShapeConversionParams { diff --git a/src/shape.rs b/src/shape.rs index f42867f3..959ffc7e 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -44,16 +44,13 @@ use std::io; use std::io::prelude::*; use std::path::Path; -use serde_json; -use std::f64::{INFINITY, NEG_INFINITY}; - // Compute geometricError from 3D corners (use half of max span) fn compute_geometric_error_from_corners(corners: &[[f64; 3]]) -> f64 { if corners.is_empty() { return 0.0; } - let mut min_v = [INFINITY; 3]; - let mut max_v = [NEG_INFINITY; 3]; + let mut min_v = [f64::INFINITY; 3]; + let mut max_v = [f64::NEG_INFINITY; 3]; for c in corners { for i in 0..3 { if c[i] < min_v[i] { @@ -112,11 +109,9 @@ fn walk_path(dir: &Path, cb: &mut dyn FnMut(&str)) -> io::Result<()> { let path = entry.path(); if path.is_dir() { walk_path(&path, cb)?; - } else { - if let Some(osdir) = path.extension() { - if osdir.to_str() == Some("json") { - cb(&path.to_str().unwrap()); - } + } else if let Some(osdir) = path.extension() { + if osdir.to_str() == Some("json") { + cb(path.to_str().unwrap()); } } } @@ -191,8 +186,8 @@ pub fn shape_batch_convert( "children":[] } }); - let mut root_min = [INFINITY, INFINITY, INFINITY]; - let mut root_max = [NEG_INFINITY, NEG_INFINITY, NEG_INFINITY]; + let mut root_min = [f64::INFINITY; 3]; + let mut root_max = [f64::NEG_INFINITY; 3]; let mut json_vec = vec![]; walk_path(&Path::new(to).join("tile"), &mut |dir| { let file = File::open(dir).unwrap(); diff --git a/src/shapefile/b3dm_content_generator.cpp b/src/shapefile/b3dm_content_generator.cpp new file mode 100644 index 00000000..201248b1 --- /dev/null +++ b/src/shapefile/b3dm_content_generator.cpp @@ -0,0 +1,165 @@ +#include "b3dm_content_generator.h" +#include "shapefile_spatial_item_adapter.h" +#include "../b3dm/b3dm_writer.h" + +#include +#include + +namespace shapefile { + +B3DMContentGenerator::B3DMContentGenerator(double centerLon, double centerLat) + : centerLon_(centerLon), centerLat_(centerLat) {} + +std::vector> B3DMContentGenerator::convertToSpatialItems( + const std::vector& items) { + + std::vector> result; + result.reserve(items.size()); + + for (const auto* item : items) { + if (!item) continue; + + // 创建适配器包装器 + auto adapter = std::make_shared(item); + result.push_back(adapter); + } + + return result; +} + +std::string B3DMContentGenerator::generate( + const std::vector& items, + bool withHeight, + bool enableSimplify, + const std::optional& simplifyParams, + bool enableDraco, + const std::optional& dracoParams) { + + if (items.empty()) { + return std::string(); + } + + // 转换为适配器列表 + auto adapters = convertToSpatialItems(items); + if (adapters.empty()) { + return std::string(); + } + + // 转换为SpatialItemRefList(使用原始指针) + spatial::core::SpatialItemRefList spatialItems; + spatialItems.reserve(adapters.size()); + for (const auto& adapter : adapters) { + spatialItems.emplace_back(adapter.get()); + } + + // 配置B3DM生成器 + b3dm::B3DMGeneratorConfig config; + config.centerLongitude = centerLon_; + config.centerLatitude = centerLat_; + config.centerHeight = 0.0; + config.enableSimplification = enableSimplify; + config.enableDraco = enableDraco; + config.geometryExtractor = &geometryExtractor_; + + if (simplifyParams.has_value()) { + config.simplifyParams = simplifyParams.value(); + } + + if (dracoParams.has_value()) { + config.dracoParams = dracoParams.value(); + } + + // 创建B3DM生成器 + b3dm::B3DMGenerator generator(config); + + // 构建LOD级别设置 + LODLevelSettings lodSettings; + lodSettings.enable_simplification = enableSimplify; + lodSettings.enable_draco = enableDraco; + + if (simplifyParams.has_value()) { + lodSettings.simplify = simplifyParams.value(); + } + + if (dracoParams.has_value()) { + lodSettings.draco = dracoParams.value(); + } + + // 生成B3DM + return generator.generate(spatialItems, lodSettings); +} + +std::vector B3DMContentGenerator::generateLODFiles( + const std::vector& items, + const std::string& outputDir, + const std::vector& lodLevels) { + + std::vector result; + + if (items.empty() || lodLevels.empty()) { + return result; + } + + // 转换为适配器列表 + auto adapters = convertToSpatialItems(items); + if (adapters.empty()) { + return result; + } + + // 转换为SpatialItemRefList + spatial::core::SpatialItemRefList spatialItems; + spatialItems.reserve(adapters.size()); + for (const auto& adapter : adapters) { + spatialItems.emplace_back(adapter.get()); + } + + // 配置B3DM生成器 + b3dm::B3DMGeneratorConfig config; + config.centerLongitude = centerLon_; + config.centerLatitude = centerLat_; + config.centerHeight = 0.0; + config.geometryExtractor = &geometryExtractor_; + + // 从LOD级别设置中提取简化参数(使用第一个级别的设置) + if (!lodLevels.empty()) { + config.enableSimplification = lodLevels[0].enable_simplification; + config.simplifyParams = lodLevels[0].simplify; + config.enableDraco = lodLevels[0].enable_draco; + config.dracoParams = lodLevels[0].draco; + } + + // 创建B3DM生成器 + b3dm::B3DMGenerator generator(config); + + // 提取基础文件名 + std::string baseName = std::filesystem::path(outputDir).filename().string(); + + // 生成LOD文件 + return generator.generateLODFiles(spatialItems, outputDir, baseName, lodLevels); +} + +bool B3DMContentGenerator::generateToFile( + const std::vector& items, + const std::string& outputPath, + bool withHeight) { + + std::string b3dmData = generate(items, withHeight); + if (b3dmData.empty()) { + return false; + } + + // 创建输出目录 + std::filesystem::path p(outputPath); + std::filesystem::create_directories(p.parent_path()); + + // 写入文件 + std::ofstream file(outputPath, std::ios::binary); + if (!file) { + return false; + } + + file.write(b3dmData.data(), b3dmData.size()); + return file.good(); +} + +} // namespace shapefile diff --git a/src/shapefile/b3dm_content_generator.h b/src/shapefile/b3dm_content_generator.h new file mode 100644 index 00000000..cf278ab5 --- /dev/null +++ b/src/shapefile/b3dm_content_generator.h @@ -0,0 +1,100 @@ +#pragma once + +/** + * @file shapefile/b3dm_content_generator.h + * @brief Shapefile B3DM内容生成器 + * + * 基于b3dm::B3DMGenerator的Shapefile专用包装器 + * 提供与旧代码兼容的API + */ + +#include "shapefile_data_pool.h" +#include "geometry_extractor.h" +#include "../b3dm/b3dm_generator.h" +#include +#include +#include +#include +#include + +namespace shapefile { + +// 前向声明 +class ShapefileSpatialItemAdapter; + +/** + * @brief B3DM内容生成器 + * + * 基于pipeline::B3DMGenerator的Shapefile专用包装器 + */ +class B3DMContentGenerator { +public: + /** + * @brief 构造内容生成器 + * + * @param centerLon 全局中心经度(用于ENU坐标转换) + * @param centerLat 全局中心纬度 + */ + B3DMContentGenerator(double centerLon, double centerLat); + + /** + * @brief 生成B3DM内容 + * + * @param items 空间对象指针列表(避免拷贝) + * @param withHeight 是否包含高度属性 + * @param enableSimplify 是否启用简化 + * @param simplifyParams 简化参数 + * @param enableDraco 是否启用Draco压缩 + * @param dracoParams Draco参数 + * @return 生成的B3DM二进制数据,失败返回空字符串 + */ + std::string generate( + const std::vector& items, + bool withHeight = true, + bool enableSimplify = false, + const std::optional& simplifyParams = std::nullopt, + bool enableDraco = false, + const std::optional& dracoParams = std::nullopt + ); + + /** + * @brief 生成多LOD级别的B3DM文件 + * + * @param items 空间对象指针列表 + * @param outputDir 输出目录 + * @param lodLevels LOD级别配置列表 + * @return 生成的文件信息列表 + */ + std::vector generateLODFiles( + const std::vector& items, + const std::string& outputDir, + const std::vector& lodLevels + ); + + /** + * @brief 生成B3DM并写入文件 + * + * @param items 空间对象指针列表 + * @param outputPath 输出文件路径 + * @return 是否成功 + */ + bool generateToFile( + const std::vector& items, + const std::string& outputPath, + bool withHeight = true + ); + +private: + double centerLon_; + double centerLat_; + + // 几何体提取器 + GeometryExtractor geometryExtractor_; + + // 将ShapefileSpatialItem列表转换为适配器列表 + std::vector> convertToSpatialItems( + const std::vector& items + ); +}; + +} // namespace shapefile diff --git a/src/shapefile/geometry_converter.cpp b/src/shapefile/geometry_converter.cpp new file mode 100644 index 00000000..67ac6e14 --- /dev/null +++ b/src/shapefile/geometry_converter.cpp @@ -0,0 +1,280 @@ +#include "geometry_converter.h" +#include "../coords/coordinate_transformer.h" +#include "../utils/log.h" + +#include +#include +#include +#include +#include +#include + +namespace shapefile { + +GeometryConverter::GeometryConverter(double centerLon, double centerLat) + : centerLon_(centerLon), centerLat_(centerLat) {} + +void GeometryConverter::transformToWGS84(double& x, double& y, double& z) { + // 如果坐标已经是WGS84,这里不需要转换 + // 如果需要从其他坐标系转换,在这里实现 + // 目前假设输入已经是WGS84经纬度 +} + +std::pair GeometryConverter::projectToLocalMeters(double lon, double lat) { + // 使用与原有代码相同的投影逻辑 + // 将WGS84经纬度转换为以centerLon_/centerLat_为中心的本地米坐标 + double x = (lon - centerLon_) * 111320.0 * std::cos(centerLat_ * std::numbers::pi / 180.0); + double y = (lat - centerLat_) * 111320.0; + return {x, y}; +} + +void GeometryConverter::calcNormal(int baseCount, int pointNum, PolygonMesh& mesh) { + // 计算法线,与原有逻辑一致 + for (int i = 0; i < pointNum; i += 2) { + float x1 = mesh.vertices[baseCount + 2 * i][0]; + float y1 = mesh.vertices[baseCount + 2 * i][1]; + float z1 = mesh.vertices[baseCount + 2 * i][2]; + float x2 = mesh.vertices[baseCount + 2 * (i + 1)][0]; + float y2 = mesh.vertices[baseCount + 2 * (i + 1)][1]; + float z2 = mesh.vertices[baseCount + 2 * (i + 1)][2]; + + // 计算法线(简化版本) + float nx = y2 - y1; + float ny = -(x2 - x1); + float nz = 0.0f; + + // 归一化 + float len = std::sqrt(nx * nx + ny * ny + nz * nz); + if (len > 0) { + nx /= len; + ny /= len; + nz /= len; + } + + mesh.normals.push_back({nx, ny, nz}); + mesh.normals.push_back({nx, ny, nz}); + } +} + +PolygonMesh GeometryConverter::convertPolygon(OGRPolygon* polygon, double height) { + PolygonMesh mesh; + + if (!polygon) { + return mesh; + } + + OGRLinearRing* exteriorRing = polygon->getExteriorRing(); + if (!exteriorRing) { + return mesh; + } + + int pointNum = exteriorRing->getNumPoints(); + if (pointNum < 4) { // 需要至少4个点(包括重复的起点/终点) + return mesh; + } + + // 处理外环 - 生成侧面墙体 + int vertexCount = 0; + for (int i = 0; i < pointNum; i++) { + OGRPoint pt; + exteriorRing->getPoint(i, &pt); + + double x = pt.getX(); + double y = pt.getY(); + double bottom = pt.getZ(); + + transformToWGS84(x, y, bottom); + auto [localX, localY] = projectToLocalMeters(x, y); + + // 底部顶点 + mesh.vertices.push_back({static_cast(localX), static_cast(localY), static_cast(bottom)}); + // 顶部顶点 + mesh.vertices.push_back({static_cast(localX), static_cast(localY), static_cast(height)}); + + // 重复顶点用于生成三角形 + if (i != 0 && i != pointNum - 1) { + mesh.vertices.push_back({static_cast(localX), static_cast(localY), static_cast(bottom)}); + mesh.vertices.push_back({static_cast(localX), static_cast(localY), static_cast(height)}); + } + } + + // 生成侧面索引 + int vertexNum = mesh.vertices.size() / 2; + for (int i = 0; i < vertexNum; i += 2) { + if (i != vertexNum - 1) { + // 第一个三角形 + mesh.indices.push_back({2 * i, 2 * i + 1, 2 * (i + 1) + 1}); + // 第二个三角形 + mesh.indices.push_back({2 * (i + 1), 2 * i, 2 * (i + 1) + 1}); + } + } + + calcNormal(0, vertexNum, mesh); + vertexCount += 2 * vertexNum; + + // 处理内环(孔洞) + int interiorCount = polygon->getNumInteriorRings(); + for (int j = 0; j < interiorCount; j++) { + OGRLinearRing* interiorRing = polygon->getInteriorRing(j); + if (!interiorRing) continue; + + int interiorPointNum = interiorRing->getNumPoints(); + if (interiorPointNum < 4) continue; + + int interiorVertexStart = mesh.vertices.size(); + + for (int i = 0; i < interiorPointNum; i++) { + OGRPoint pt; + interiorRing->getPoint(i, &pt); + + double x = pt.getX(); + double y = pt.getY(); + double bottom = pt.getZ(); + + transformToWGS84(x, y, bottom); + auto [localX, localY] = projectToLocalMeters(x, y); + + mesh.vertices.push_back({static_cast(localX), static_cast(localY), static_cast(bottom)}); + mesh.vertices.push_back({static_cast(localX), static_cast(localY), static_cast(height)}); + + if (i != 0 && i != interiorPointNum - 1) { + mesh.vertices.push_back({static_cast(localX), static_cast(localY), static_cast(bottom)}); + mesh.vertices.push_back({static_cast(localX), static_cast(localY), static_cast(height)}); + } + } + + int interiorVertexNum = (mesh.vertices.size() - interiorVertexStart) / 2; + for (int i = 0; i < interiorVertexNum; i += 2) { + if (i != interiorVertexNum - 1) { + int base = interiorVertexStart / 2; + mesh.indices.push_back({base + 2 * i, base + 2 * i + 1, base + 2 * (i + 1)}); + mesh.indices.push_back({base + 2 * (i + 1), base + 2 * i, base + 2 * (i + 1) + 1}); + } + } + + calcNormal(interiorVertexStart / 2, interiorPointNum, mesh); + vertexCount = mesh.vertices.size(); + } + + // 生成顶面和底面(使用earcut三角化) + { + using Point = std::array; + std::vector> polygonPoints; + + // 外环 + std::vector exteriorPoints; + for (int i = 0; i < pointNum; i++) { + OGRPoint pt; + exteriorRing->getPoint(i, &pt); + + double x = pt.getX(); + double y = pt.getY(); + double bottom = pt.getZ(); + + transformToWGS84(x, y, bottom); + auto [localX, localY] = projectToLocalMeters(x, y); + + exteriorPoints.push_back({localX, localY}); + + // 添加顶面和底面顶点 + mesh.vertices.push_back({static_cast(localX), static_cast(localY), static_cast(bottom)}); + mesh.vertices.push_back({static_cast(localX), static_cast(localY), static_cast(height)}); + mesh.normals.push_back({0, 0, -1}); // 底面法线 + mesh.normals.push_back({0, 0, 1}); // 顶面法线 + } + polygonPoints.push_back(exteriorPoints); + + // 内环 + for (int j = 0; j < interiorCount; j++) { + OGRLinearRing* interiorRing = polygon->getInteriorRing(j); + if (!interiorRing) continue; + + int interiorPointNum = interiorRing->getNumPoints(); + std::vector interiorPoints; + + for (int i = 0; i < interiorPointNum; i++) { + OGRPoint pt; + interiorRing->getPoint(i, &pt); + + double x = pt.getX(); + double y = pt.getY(); + double bottom = pt.getZ(); + + transformToWGS84(x, y, bottom); + auto [localX, localY] = projectToLocalMeters(x, y); + + interiorPoints.push_back({localX, localY}); + } + polygonPoints.push_back(interiorPoints); + } + + // 使用earcut进行三角化 + std::vector triIndices = mapbox::earcut(polygonPoints); + + // 底面索引(逆时针) + int baseVertex = vertexCount; + for (size_t i = 0; i < triIndices.size(); i += 3) { + mesh.indices.push_back({ + baseVertex + 2 * static_cast(triIndices[i]), + baseVertex + 2 * static_cast(triIndices[i + 2]), + baseVertex + 2 * static_cast(triIndices[i + 1]) + }); + } + + // 顶面索引(顺时针) + for (size_t i = 0; i < triIndices.size(); i += 3) { + mesh.indices.push_back({ + baseVertex + 2 * static_cast(triIndices[i]) + 1, + baseVertex + 2 * static_cast(triIndices[i + 1]) + 1, + baseVertex + 2 * static_cast(triIndices[i + 2]) + 1 + }); + } + } + + return mesh; +} + +osg::ref_ptr GeometryConverter::meshToGeometry(const PolygonMesh& mesh) { + if (mesh.isEmpty()) { + return nullptr; + } + + osg::ref_ptr geometry = new osg::Geometry(); + + // 创建顶点数组 + osg::ref_ptr vertices = new osg::Vec3Array(); + vertices->reserve(mesh.vertices.size()); + for (const auto& v : mesh.vertices) { + vertices->push_back(osg::Vec3(v[0], v[1], v[2])); + } + geometry->setVertexArray(vertices); + + // 创建法线数组 + if (!mesh.normals.empty()) { + osg::ref_ptr normals = new osg::Vec3Array(); + normals->reserve(mesh.normals.size()); + for (const auto& n : mesh.normals) { + normals->push_back(osg::Vec3(n[0], n[1], n[2])); + } + geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); + } + + // 创建索引数组 + osg::ref_ptr indices = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES); + indices->reserve(mesh.indices.size() * 3); + for (const auto& tri : mesh.indices) { + indices->push_back(tri[0]); + indices->push_back(tri[1]); + indices->push_back(tri[2]); + } + geometry->addPrimitiveSet(indices); + + return geometry; +} + +osg::ref_ptr GeometryConverter::convertToGeometry(OGRPolygon* polygon, double height) { + PolygonMesh mesh = convertPolygon(polygon, height); + return meshToGeometry(mesh); +} + +} // namespace shapefile diff --git a/src/shapefile/geometry_converter.h b/src/shapefile/geometry_converter.h new file mode 100644 index 00000000..67ee75cc --- /dev/null +++ b/src/shapefile/geometry_converter.h @@ -0,0 +1,77 @@ +#pragma once + +/** + * @file geometry_converter.h + * @brief OGR几何体到OSG几何体的转换器 + * + * 将OGR Polygon转换为OSG Geometry,用于B3DM生成 + */ + +#include "shapefile_tile.h" +#include +#include + +namespace shapefile { + +/** + * @brief 多边形网格数据结构 + * + * 与原shp23dtile.cpp中的Polygon_Mesh对应 + */ +struct PolygonMesh { + std::string meshName; + std::vector> vertices; + std::vector> indices; + std::vector> normals; + + bool isEmpty() const { return vertices.empty(); } +}; + +/** + * @brief 几何体转换器 + * + * 将OGR几何体转换为OSG Geometry + */ +class GeometryConverter { +public: + /** + * @brief 构造函数 + * @param centerLon 中心经度(用于本地坐标投影) + * @param centerLat 中心纬度(用于本地坐标投影) + */ + GeometryConverter(double centerLon, double centerLat); + + /** + * @brief 将OGR Polygon转换为PolygonMesh + * @param polygon OGR多边形 + * @param height 建筑高度 + * @return 转换后的网格 + */ + PolygonMesh convertPolygon(OGRPolygon* polygon, double height); + + /** + * @brief 将PolygonMesh转换为OSG Geometry + * @param mesh 多边形网格 + * @return OSG几何体 + */ + osg::ref_ptr meshToGeometry(const PolygonMesh& mesh); + + /** + * @brief 直接转换OGR Polygon为OSG Geometry + * @param polygon OGR多边形 + * @param height 建筑高度 + * @return OSG几何体 + */ + osg::ref_ptr convertToGeometry(OGRPolygon* polygon, double height); + +private: + double centerLon_; + double centerLat_; + + // 坐标转换辅助函数 + void transformToWGS84(double& x, double& y, double& z); + std::pair projectToLocalMeters(double lon, double lat); + void calcNormal(int baseCount, int pointNum, PolygonMesh& mesh); +}; + +} // namespace shapefile diff --git a/src/shapefile/geometry_extractor.cpp b/src/shapefile/geometry_extractor.cpp new file mode 100644 index 00000000..f883dc36 --- /dev/null +++ b/src/shapefile/geometry_extractor.cpp @@ -0,0 +1,62 @@ +#include "geometry_extractor.h" +#include "shapefile_spatial_item_adapter.h" + +namespace shapefile { + +std::vector> GeometryExtractor::extract( + const spatial::core::SpatialItem* item) { + + std::vector> result; + + // 尝试转换为ShapefileSpatialItemAdapter + const auto* adapter = dynamic_cast(item); + if (!adapter) { + return result; + } + + // 获取原始ShapefileSpatialItem + const ShapefileSpatialItem* shapefileItem = adapter->getItem(); + if (!shapefileItem) { + return result; + } + + // 返回几何体列表 + return shapefileItem->geometries; +} + +std::string GeometryExtractor::getId(const spatial::core::SpatialItem* item) { + const auto* adapter = dynamic_cast(item); + if (!adapter) { + return ""; + } + + // 使用featureId作为ID + return std::to_string(adapter->getFeatureId()); +} + +std::map GeometryExtractor::getAttributes( + const spatial::core::SpatialItem* item) { + + std::map result; + + const auto* adapter = dynamic_cast(item); + if (!adapter) { + return result; + } + + const ShapefileSpatialItem* shapefileItem = adapter->getItem(); + if (!shapefileItem) { + return result; + } + + // 复制属性 + return shapefileItem->properties; +} + +std::shared_ptr GeometryExtractor::getMaterial( + const spatial::core::SpatialItem* item) { + // Shapefile不包含材质信息,返回默认材质 + return std::make_shared(); +} + +} // namespace shapefile diff --git a/src/shapefile/geometry_extractor.h b/src/shapefile/geometry_extractor.h new file mode 100644 index 00000000..28d53707 --- /dev/null +++ b/src/shapefile/geometry_extractor.h @@ -0,0 +1,50 @@ +#pragma once + +/** + * @file shapefile/geometry_extractor.h + * @brief Shapefile几何体提取器 + * + * 实现common::IGeometryExtractor接口,供B3DM生成器使用 + */ + +#include "../common/geometry_extractor.h" +#include "shapefile_spatial_item_adapter.h" +#include + +namespace shapefile { + +/** + * @brief Shapefile几何体提取器 + */ +class GeometryExtractor : public common::IGeometryExtractor { +public: + /** + * @brief 从空间对象提取几何体 + */ + std::vector> extract( + const spatial::core::SpatialItem* item) override; + + /** + * @brief 获取对象的唯一标识 + */ + std::string getId(const spatial::core::SpatialItem* item) override; + + /** + * @brief 获取对象的属性 + */ + std::map getAttributes( + const spatial::core::SpatialItem* item) override; + + /** + * @brief 获取对象的材质信息 + * + * Shapefile通常不包含材质信息,返回默认材质 + * + * @param item Shapefile空间对象 + * @return 默认材质信息 + */ + std::shared_ptr getMaterial( + const spatial::core::SpatialItem* item) override; +}; + +} // namespace shapefile diff --git a/src/shapefile/shapefile_data_pool.cpp b/src/shapefile/shapefile_data_pool.cpp new file mode 100644 index 00000000..b2c617be --- /dev/null +++ b/src/shapefile/shapefile_data_pool.cpp @@ -0,0 +1,490 @@ +#include "shapefile_data_pool.h" +#include "../utils/log.h" +#include +#include +#include +#include +#include + +namespace shapefile { + +TileBBox ShapefileDataPool::computeWorldBounds() const { + if (items_.empty()) { + return TileBBox(); + } + + TileBBox worldBounds = items_[0]->bounds; + + for (size_t i = 1; i < items_.size(); ++i) { + const auto& bounds = items_[i]->bounds; + worldBounds.minx = std::min(worldBounds.minx, bounds.minx); + worldBounds.maxx = std::max(worldBounds.maxx, bounds.maxx); + worldBounds.miny = std::min(worldBounds.miny, bounds.miny); + worldBounds.maxy = std::max(worldBounds.maxy, bounds.maxy); + worldBounds.minHeight = std::min(worldBounds.minHeight, bounds.minHeight); + worldBounds.maxHeight = std::max(worldBounds.maxHeight, bounds.maxHeight); + } + + return worldBounds; +} + +// 辅助函数:将经纬度转换为ENU坐标(米) +static std::pair lonlat_to_enu_meters(double lon, double lat, double centerLon, double centerLat) { + const double pi = std::acos(-1.0); + const double earthRadius = 6378137.0; // WGS84 赤道半径(米) + + // 经度方向:每度距离随纬度变化 + double cosLat = std::cos(centerLat * pi / 180.0); + double meterPerDegreeLon = (pi / 180.0) * earthRadius * cosLat; + double meterPerDegreeLat = (pi / 180.0) * earthRadius; + + // 计算相对于中心点的偏移(米) + double x = (lon - centerLon) * meterPerDegreeLon; + double y = (lat - centerLat) * meterPerDegreeLat; + + return {x, y}; +} + +// 辅助函数:计算法向量(与原始实现完全一致) +// 原始实现:每个点计算一个法线,然后给4个顶点使用 +// 法线基于水平向量的垂直方向:(-y, x, 0) +static void calc_normal(int baseCnt, int ptNum, osg::Vec3Array* vertices, osg::Vec3Array* normals) { + // 注意:原始实现中 baseCnt 是顶点索引,但在调用时传入的是 vertex_index / 2 + // 这里我们假设 baseCnt 是起始顶点索引(不是 /2 后的值) + int vertexIdx = baseCnt; + + for (int i = 0; i < ptNum; i += 2) { + // 计算水平向量:从当前点指向下一个点 + if (vertexIdx + 4 < vertices->size() && i + 1 < ptNum) { + // 获取当前点和下一个点的XY坐标 + float x0 = (*vertices)[vertexIdx].x(); + float y0 = (*vertices)[vertexIdx].y(); + float x1 = (*vertices)[vertexIdx + 4].x(); // 下一个点的底部顶点 + float y1 = (*vertices)[vertexIdx + 4].y(); + + // 水平向量 + float dx = x1 - x0; + float dy = y1 - y0; + + // 垂直向量 (-dy, dx, 0) + float nx = -dy; + float ny = dx; + float nz = 0.0f; + + // 归一化 + float len = std::sqrt(nx * nx + ny * ny); + if (len > 0) { + nx /= len; + ny /= len; + } + + // 为4个顶点添加相同的法线(2个底部 + 2个顶部) + (*normals)[vertexIdx] = osg::Vec3(nx, ny, nz); + (*normals)[vertexIdx + 1] = osg::Vec3(nx, ny, nz); + (*normals)[vertexIdx + 2] = osg::Vec3(nx, ny, nz); + (*normals)[vertexIdx + 3] = osg::Vec3(nx, ny, nz); + } + + vertexIdx += 4; // 移动到下一个点(4个顶点) + } +} + +// 辅助函数:从 OGRPolygon 创建 OSG 几何体(与原始实现一致) +static osg::ref_ptr create_geometry_from_polygon(OGRPolygon* polygon, double height, + double centerLon, double centerLat, + bool xySwapped = false) { + if (!polygon) return nullptr; + + OGRLinearRing* exteriorRing = polygon->getExteriorRing(); + int ptNum = exteriorRing->getNumPoints(); + if (ptNum < 4) { + return nullptr; + } + + osg::ref_ptr geometry = new osg::Geometry; + osg::ref_ptr vertices = new osg::Vec3Array; + osg::ref_ptr normals = new osg::Vec3Array; + osg::ref_ptr indices = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES); + + int pt_count = 0; + + // ========== 1. 创建外环侧面 ========== + for (int i = 0; i < ptNum; i++) { + OGRPoint pt; + exteriorRing->getPoint(i, &pt); + double x = pt.getX(); + double y = pt.getY(); + double bottom = pt.getZ(); + + // 如果坐标转换后 X/Y 交换,需要交换回来 + double lon = xySwapped ? y : x; + double lat = xySwapped ? x : y; + + auto [point_x, point_y] = lonlat_to_enu_meters(lon, lat, centerLon, centerLat); + + // 每个点创建2个顶点(底部和顶部) + vertices->push_back(osg::Vec3(point_x, point_y, bottom)); + vertices->push_back(osg::Vec3(point_x, point_y, height)); + normals->push_back(osg::Vec3(0, 0, 0)); // 占位,稍后计算 + normals->push_back(osg::Vec3(0, 0, 0)); + + // 中间点重复添加(与原始实现一致) + if (i != 0 && i != ptNum - 1) { + vertices->push_back(osg::Vec3(point_x, point_y, bottom)); + vertices->push_back(osg::Vec3(point_x, point_y, height)); + normals->push_back(osg::Vec3(0, 0, 0)); + normals->push_back(osg::Vec3(0, 0, 0)); + } + } + + // 创建侧面索引 + int vertex_num = vertices->size() / 2; + for (int i = 0; i < vertex_num; i += 2) { + if (i != vertex_num - 1) { + indices->push_back(2 * i); + indices->push_back(2 * i + 1); + indices->push_back(2 * (i + 1) + 1); + indices->push_back(2 * (i + 1)); + indices->push_back(2 * i); + indices->push_back(2 * (i + 1) + 1); + } + } + + // 计算侧面法向量 + calc_normal(0, vertex_num, vertices.get(), normals.get()); + pt_count += 2 * vertex_num; + + // ========== 2. 创建内环侧面(洞) ========== + int inner_count = polygon->getNumInteriorRings(); + for (int j = 0; j < inner_count; j++) { + OGRLinearRing* interiorRing = polygon->getInteriorRing(j); + int innerPtNum = interiorRing->getNumPoints(); + if (innerPtNum < 4) continue; + + int innerBase = vertices->size(); + + for (int i = 0; i < innerPtNum; i++) { + OGRPoint pt; + interiorRing->getPoint(i, &pt); + double x = pt.getX(); + double y = pt.getY(); + double bottom = pt.getZ(); + + double lon = xySwapped ? y : x; + double lat = xySwapped ? x : y; + + auto [point_x, point_y] = lonlat_to_enu_meters(lon, lat, centerLon, centerLat); + + vertices->push_back(osg::Vec3(point_x, point_y, bottom)); + vertices->push_back(osg::Vec3(point_x, point_y, height)); + normals->push_back(osg::Vec3(0, 0, 0)); + normals->push_back(osg::Vec3(0, 0, 0)); + + if (i != 0 && i != innerPtNum - 1) { + vertices->push_back(osg::Vec3(point_x, point_y, bottom)); + vertices->push_back(osg::Vec3(point_x, point_y, height)); + normals->push_back(osg::Vec3(0, 0, 0)); + normals->push_back(osg::Vec3(0, 0, 0)); + } + } + + int innerVertexNum = (vertices->size() - innerBase) / 2; + for (int i = 0; i < innerVertexNum; i += 2) { + if (i != innerVertexNum - 1) { + indices->push_back(innerBase + 2 * i); + indices->push_back(innerBase + 2 * i + 1); + indices->push_back(innerBase + 2 * (i + 1) + 1); + indices->push_back(innerBase + 2 * (i + 1)); + indices->push_back(innerBase + 2 * i); + indices->push_back(innerBase + 2 * (i + 1) + 1); + } + } + + calc_normal(innerBase / 2, innerVertexNum, vertices.get(), normals.get()); + pt_count = vertices->size(); + } + + // ========== 3. 创建顶面和底面(使用 earcut 三角剖分) ========== + { + using Point = std::array; + std::vector> earcutPolygon; + + // 底面和顶面的顶点起始索引 + int roofBaseIdx = vertices->size(); + + // 外环顶点(用于 earcut) + std::vector exteriorPoints; + for (int i = 0; i < ptNum; i++) { + OGRPoint pt; + exteriorRing->getPoint(i, &pt); + double x = pt.getX(); + double y = pt.getY(); + double bottom = pt.getZ(); + + double lon = xySwapped ? y : x; + double lat = xySwapped ? x : y; + + auto [point_x, point_y] = lonlat_to_enu_meters(lon, lat, centerLon, centerLat); + exteriorPoints.push_back({point_x, point_y}); + + // 添加底面顶点 + vertices->push_back(osg::Vec3(point_x, point_y, bottom)); + normals->push_back(osg::Vec3(0, 0, -1)); + + // 添加顶面顶点 + vertices->push_back(osg::Vec3(point_x, point_y, height)); + normals->push_back(osg::Vec3(0, 0, 1)); + } + earcutPolygon.push_back(exteriorPoints); + + // 内环(洞) + for (int j = 0; j < inner_count; j++) { + OGRLinearRing* interiorRing = polygon->getInteriorRing(j); + int innerPtNum = interiorRing->getNumPoints(); + if (innerPtNum < 4) continue; + + std::vector interiorPoints; + for (int i = 0; i < innerPtNum; i++) { + OGRPoint pt; + interiorRing->getPoint(i, &pt); + double x = pt.getX(); + double y = pt.getY(); + double bottom = pt.getZ(); + + double lon = xySwapped ? y : x; + double lat = xySwapped ? x : y; + + auto [point_x, point_y] = lonlat_to_enu_meters(lon, lat, centerLon, centerLat); + interiorPoints.push_back({point_x, point_y}); + + // 添加底面顶点 + vertices->push_back(osg::Vec3(point_x, point_y, bottom)); + normals->push_back(osg::Vec3(0, 0, -1)); + + // 添加顶面顶点 + vertices->push_back(osg::Vec3(point_x, point_y, height)); + normals->push_back(osg::Vec3(0, 0, 1)); + } + earcutPolygon.push_back(interiorPoints); + } + + // 使用 earcut 进行三角剖分 + std::vector earcutIndices = mapbox::earcut(earcutPolygon); + + // 创建底面索引(反向顺序) + for (int idx = 0; idx < earcutIndices.size(); idx += 3) { + indices->push_back(roofBaseIdx + 2 * earcutIndices[idx]); + indices->push_back(roofBaseIdx + 2 * earcutIndices[idx + 2]); + indices->push_back(roofBaseIdx + 2 * earcutIndices[idx + 1]); + } + + // 创建顶面索引 + for (int idx = 0; idx < earcutIndices.size(); idx += 3) { + indices->push_back(roofBaseIdx + 2 * earcutIndices[idx] + 1); + indices->push_back(roofBaseIdx + 2 * earcutIndices[idx + 1] + 1); + indices->push_back(roofBaseIdx + 2 * earcutIndices[idx + 2] + 1); + } + } + + geometry->setVertexArray(vertices); + geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); + geometry->addPrimitiveSet(indices); + + return geometry; +} + +bool ShapefileDataPool::loadFromShapefileWithGeometry(const std::string& filename, const std::string& heightField, + double centerLon, double centerLat) { + items_.clear(); + + // 注册所有 GDAL 驱动 + static bool gdal_initialized = false; + if (!gdal_initialized) { + GDALAllRegister(); + gdal_initialized = true; + } + + // 打开 Shapefile + GDALDataset* poDS = static_cast( + GDALOpenEx(filename.c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr) + ); + + if (!poDS) { + LOG_E("Failed to open shapefile: %s", filename.c_str()); + return false; + } + + OGRLayer* poLayer = poDS->GetLayer(0); + if (!poLayer) { + LOG_E("No layer found in shapefile"); + GDALClose(poDS); + return false; + } + + // 获取图层的空间参考系统 + const OGRSpatialReference* poSrcSRS = poLayer->GetSpatialRef(); + OGRCoordinateTransformation* poCT = nullptr; + + if (poSrcSRS) { + char* pszWKT = nullptr; + poSrcSRS->exportToWkt(&pszWKT); + LOG_I("Source CRS: %s", pszWKT ? pszWKT : "Unknown"); + CPLFree(pszWKT); + + // 检查是否是地理坐标系(WGS84) + if (!poSrcSRS->IsGeographic()) { + // 需要转换为 WGS84 + OGRSpatialReference oDstSRS; + oDstSRS.SetWellKnownGeogCS("WGS84"); + + poCT = OGRCreateCoordinateTransformation(poSrcSRS, &oDstSRS); + if (poCT) { + LOG_I("Created coordinate transformation to WGS84"); + } else { + LOG_W("Failed to create coordinate transformation, using original coordinates"); + } + } + } else { + LOG_W("No spatial reference found in shapefile, assuming WGS84"); + } + + poLayer->ResetReading(); + OGRFeature* poFeature = nullptr; + int featureId = 0; + + while ((poFeature = poLayer->GetNextFeature()) != nullptr) { + OGRGeometry* poGeometry = poFeature->GetGeometryRef(); + if (!poGeometry) { + OGRFeature::DestroyFeature(poFeature); + continue; + } + + // 创建新的数据项 + auto item = std::make_shared(); + item->featureId = featureId++; + + // 获取所有属性 + for (int i = 0; i < poFeature->GetFieldCount(); ++i) { + const OGRFieldDefn* poFieldDefn = poFeature->GetFieldDefnRef(i); + const char* fieldName = poFieldDefn->GetNameRef(); + + switch (poFieldDefn->GetType()) { + case OFTInteger: + item->properties[fieldName] = poFeature->GetFieldAsInteger(i); + break; + case OFTReal: + item->properties[fieldName] = poFeature->GetFieldAsDouble(i); + break; + case OFTString: + item->properties[fieldName] = poFeature->GetFieldAsString(i); + break; + default: + item->properties[fieldName] = poFeature->GetFieldAsString(i); + break; + } + } + + // 获取高度(不区分大小写查找) + double height = 0.0; + if (!heightField.empty()) { + // 转换为小写进行查找 + std::string lowerHeightField = heightField; + std::transform(lowerHeightField.begin(), lowerHeightField.end(), lowerHeightField.begin(), ::tolower); + + for (const auto& kv : item->properties) { + std::string lowerKey = kv.first; + std::transform(lowerKey.begin(), lowerKey.end(), lowerKey.begin(), ::tolower); + if (lowerKey == lowerHeightField) { + if (kv.second.is_number()) { + height = kv.second.get(); + } else if (kv.second.is_string()) { + try { + height = std::stod(kv.second.get()); + } catch (...) { + height = 0.0; + } + } + break; + } + } + } + + // 如果需要,进行坐标转换 + if (poCT) { + poGeometry->transform(poCT); + } + + // 计算包围盒 + OGREnvelope envelope; + poGeometry->getEnvelope(&envelope); + + // 注意:GDAL 坐标转换后,envelope.MinX/MaxX 是纬度,envelope.MinY/MaxY 是经度 + // 但 TileBBox 期望 minx/maxx 是经度,miny/maxy 是纬度 + // 所以需要交换 X 和 Y + double minLon, maxLon, minLat, maxLat; + if (poCT) { + // 坐标转换后,X=纬度,Y=经度 + minLon = envelope.MinY; + maxLon = envelope.MaxY; + minLat = envelope.MinX; + maxLat = envelope.MaxX; + } else { + // 没有坐标转换,X=经度,Y=纬度 + minLon = envelope.MinX; + maxLon = envelope.MaxX; + minLat = envelope.MinY; + maxLat = envelope.MaxY; + } + + item->bounds = TileBBox( + minLon, maxLon, + minLat, maxLat, + 0.0, height + ); + + // 转换几何数据 + bool xySwapped = (poCT != nullptr); // 如果进行了坐标转换,X/Y 需要交换 + OGRwkbGeometryType geomType = wkbFlatten(poGeometry->getGeometryType()); + if (geomType == wkbPolygon) { + OGRPolygon* polygon = static_cast(poGeometry); + auto geometry = create_geometry_from_polygon(polygon, height, centerLon, centerLat, xySwapped); + if (geometry.valid()) { + item->geometries.push_back(geometry); + } + } else if (geomType == wkbMultiPolygon) { + OGRMultiPolygon* multiPoly = static_cast(poGeometry); + int numGeoms = multiPoly->getNumGeometries(); + for (int i = 0; i < numGeoms; i++) { + OGRPolygon* polygon = static_cast(multiPoly->getGeometryRef(i)); + auto geometry = create_geometry_from_polygon(polygon, height, centerLon, centerLat, xySwapped); + if (geometry.valid()) { + item->geometries.push_back(geometry); + } + } + } + + // 只添加有有效几何数据的项 + if (!item->geometries.empty()) { + items_.push_back(std::move(item)); + } + + OGRFeature::DestroyFeature(poFeature); + + // 每1000个要素输出一次日志 + if (featureId % 1000 == 0) { + LOG_I("Loaded %d features with geometry...", featureId); + } + } + + GDALClose(poDS); + + // 释放坐标转换对象 + if (poCT) { + OGRCoordinateTransformation::DestroyCT(poCT); + } + + LOG_I("Successfully loaded %zu features with geometry from shapefile", items_.size()); + return !items_.empty(); +} + +} // namespace shapefile diff --git a/src/shapefile/shapefile_data_pool.h b/src/shapefile/shapefile_data_pool.h new file mode 100644 index 00000000..07d34281 --- /dev/null +++ b/src/shapefile/shapefile_data_pool.h @@ -0,0 +1,113 @@ +#pragma once + +/** + * @file shapefile_data_pool.h + * @brief Shapefile 数据池管理器 + * + * 阶段1迁移组件:新的数据加载层 + * 使用 shared_ptr 管理 ShapefileSpatialItem,避免数据拷贝 + */ + +#include "shapefile_tile.h" +#include +#include +#include +#include +#include +#include + +namespace shapefile { + +/** + * @brief Shapefile 空间数据项 + * + * 包含 Shapefile 中一个要素的完整数据 + * 禁止拷贝,只允许通过 shared_ptr 管理 + */ +struct ShapefileSpatialItem { + int featureId = 0; // 要素ID + TileBBox bounds; // WGS84 包围盒 + std::vector> geometries; // OSG 几何体 + std::map properties; // 属性 + + // 默认构造函数 + ShapefileSpatialItem() = default; + + // 禁止拷贝 + ShapefileSpatialItem(const ShapefileSpatialItem&) = delete; + ShapefileSpatialItem& operator=(const ShapefileSpatialItem&) = delete; + + // 允许移动 + ShapefileSpatialItem(ShapefileSpatialItem&&) = default; + ShapefileSpatialItem& operator=(ShapefileSpatialItem&&) = default; +}; + +/** + * @brief Shapefile 数据池 + * + * 管理从 Shapefile 加载的所有空间数据 + * 使用 shared_ptr 避免数据拷贝,确保数据零拷贝传递 + */ +class ShapefileDataPool { +public: + using ItemPtr = std::shared_ptr; + + ShapefileDataPool() = default; + ~ShapefileDataPool() = default; + + // 禁止拷贝 + ShapefileDataPool(const ShapefileDataPool&) = delete; + ShapefileDataPool& operator=(const ShapefileDataPool&) = delete; + + // 允许移动 + ShapefileDataPool(ShapefileDataPool&&) = default; + ShapefileDataPool& operator=(ShapefileDataPool&&) = default; + + /** + * @brief 从 Shapefile 加载数据(包含几何数据) + * @param filename Shapefile 路径 + * @param heightField 高度字段名 + * @param centerLon 中心经度(用于ENU坐标转换) + * @param centerLat 中心纬度(用于ENU坐标转换) + * @return 是否成功 + */ + bool loadFromShapefileWithGeometry(const std::string& filename, const std::string& heightField, + double centerLon, double centerLat); + + /** + * @brief 获取数据项数量 + */ + size_t size() const { return items_.size(); } + + /** + * @brief 获取指定索引的数据项 + * @param index 索引 + * @return 数据项指针 + */ + const ItemPtr& getItem(size_t index) const { + static const ItemPtr nullPtr; + if (index >= items_.size()) return nullPtr; + return items_[index]; + } + + /** + * @brief 获取所有数据项 + */ + const std::vector& getAllItems() const { return items_; } + + /** + * @brief 计算世界包围盒 + * @return 包含所有数据的包围盒 + */ + TileBBox computeWorldBounds() const; + + /** + * @brief 清空数据 + */ + void clear() { items_.clear(); } + +private: + std::vector items_; // 数据项列表 +}; + +} // namespace shapefile diff --git a/src/shapefile/shapefile_processor.cpp b/src/shapefile/shapefile_processor.cpp new file mode 100644 index 00000000..461914b2 --- /dev/null +++ b/src/shapefile/shapefile_processor.cpp @@ -0,0 +1,869 @@ +#include "shapefile_processor.h" +#include "shapefile_spatial_item_adapter.h" +#include "b3dm_content_generator.h" +#include "shapefile_tileset_adapter.h" +#include "../tileset/tileset_writer.h" +#include "../lod_pipeline.h" +#include "../utils/log.h" +#include "../utils/file_utils.h" +#include "../pipeline/adapters/shapefile/shapefile_data_source.h" +#include "../pipeline/adapters/spatial/quadtree_index.h" +#include "../pipeline/data_source.h" +#include "../pipeline/spatial_index.h" +#include "../pipeline/tileset_builder.h" +#include +#include +#include +#include +#include + +namespace shapefile { + +// B3DM生成器实现包装类 +class ShapefileProcessor::B3DMGeneratorImpl { +public: + explicit B3DMGeneratorImpl(double centerLon, double centerLat) + : generator_(centerLon, centerLat) {} + + std::string generate( + const std::vector& items, + bool withHeight, + bool enableSimplify, + const std::optional& simplifyParams, + bool enableDraco, + const std::optional& dracoParams) { + return generator_.generate(items, withHeight, enableSimplify, simplifyParams, enableDraco, dracoParams); + } + +private: + B3DMContentGenerator generator_; +}; + +ShapefileProcessor::ShapefileProcessor(const ShapefileProcessorConfig& config) + : config_(config) { + // 初始化 B3DM 生成器 + b3dmGenerator_ = std::make_unique( + config.centerLongitude, + config.centerLatitude + ); +} + +ShapefileProcessor::~ShapefileProcessor() = default; + +ProcessingResult ShapefileProcessor::process() { + ProcessingResult result; + + LOG_I("Stage4: Starting Shapefile processing..."); + + // 步骤1: 加载数据 + if (!loadData()) { + result.errorMessage = "Failed to load data"; + return result; + } + result.featureCount = dataPool_->size(); + LOG_I("Stage4: Loaded %zu features", result.featureCount); + + // 步骤2: 构建空间索引 + if (!buildSpatialIndex()) { + result.errorMessage = "Failed to build spatial index"; + return result; + } + result.nodeCount = quadtreeIndex_->getNodeCount(); + LOG_I("Stage4: Built quadtree with %zu nodes", result.nodeCount); + + // 步骤3: 生成 B3DM 文件 + if (!generateB3DMFiles()) { + result.errorMessage = "Failed to generate B3DM files"; + return result; + } + LOG_I("Stage4: Generated B3DM files"); + + // 步骤4: 生成 Tileset + if (!generateTileset()) { + result.errorMessage = "Failed to generate tileset"; + return result; + } + LOG_I("Stage4: Generated tileset"); + + result.success = true; + result.tilesetPath = (std::filesystem::path(config_.outputPath) / "tileset.json").string(); + return result; +} + +void ShapefileProcessor::SetDataSource(pipeline::DataSource* dataSource) { + externalDataSource_ = dataSource; +} + +void ShapefileProcessor::SetSpatialIndex(pipeline::ISpatialIndex* spatialIndex) { + externalSpatialIndex_ = spatialIndex; +} + +void ShapefileProcessor::SetTilesetBuilder(pipeline::ITilesetBuilder* tilesetBuilder) { + externalTilesetBuilder_ = tilesetBuilder; +} + +pipeline::ITilesetBuilder* ShapefileProcessor::GetCurrentTilesetBuilder() { + if (externalTilesetBuilder_) { + return externalTilesetBuilder_; + } + // 如果内部 TilesetBuilder 未创建,则创建它 + if (!tilesetBuilder_) { + // 使用工厂创建 Shapefile TilesetBuilder + tilesetBuilder_ = pipeline::TilesetBuilderFactory::Instance().Create("shapefile"); + if (tilesetBuilder_) { + pipeline::TilesetBuilderConfig tbConfig; + tbConfig.center_longitude = config_.centerLongitude; + tbConfig.center_latitude = config_.centerLatitude; + tbConfig.bounding_volume_scale = config_.boundingVolumeScaleFactor; + tbConfig.child_geometric_error_multiplier = config_.geometricErrorScale; + tbConfig.enable_lod = config_.enableLOD; + tilesetBuilder_->Initialize(tbConfig); + } + } + return tilesetBuilder_.get(); +} + +const ShapefileDataPool* ShapefileProcessor::GetCurrentDataPool() const { + if (externalDataSource_) { + // 从外部数据源获取数据池 + auto* shapefileSource = dynamic_cast(externalDataSource_); + if (shapefileSource) { + return shapefileSource->GetDataPool(); + } + } + // 如果内部数据源已创建,尝试从中获取数据池 + if (dataSource_) { + auto* shapefileSource = dynamic_cast(dataSource_.get()); + if (shapefileSource) { + return shapefileSource->GetDataPool(); + } + } + return dataPool_.get(); +} + +pipeline::DataSource* ShapefileProcessor::GetCurrentDataSource() { + if (externalDataSource_) { + return externalDataSource_; + } + // 如果内部数据源未创建,则使用工厂创建 + if (!dataSource_) { + dataSource_ = pipeline::DataSourceFactory::Instance().Create("shapefile"); + if (dataSource_) { + LOG_I("Stage4: Created ShapefileDataSource using factory"); + } + } + return dataSource_.get(); +} + +const spatial::core::SpatialIndexNode* ShapefileProcessor::GetCurrentRootNode() const { + if (externalSpatialIndex_) { + // 从外部空间索引获取根节点 + auto* rawNode = externalSpatialIndex_->GetRootNode(); + if (rawNode) { + // 需要适配器转换 + // 暂时返回 nullptr,实际使用时需要正确处理 + return nullptr; + } + } + if (spatialIndex_) { + // 从内部空间索引获取根节点 + auto* rawNode = spatialIndex_->GetRootNode(); + if (rawNode) { + // 需要适配器转换 + return nullptr; + } + } + if (quadtreeIndex_) { + return quadtreeIndex_->getRootNode(); + } + return nullptr; +} + +pipeline::ISpatialIndex* ShapefileProcessor::GetCurrentSpatialIndex() { + if (externalSpatialIndex_) { + return externalSpatialIndex_; + } + // 如果内部空间索引未创建,则使用工厂创建 + if (!spatialIndex_) { + spatialIndex_ = pipeline::SpatialIndexFactory::Instance().Create("quadtree"); + if (spatialIndex_) { + LOG_I("Stage4: Created QuadtreeIndex using factory"); + } + } + return spatialIndex_.get(); +} + +bool ShapefileProcessor::loadData() { + // 步骤1修改:如果提供了外部数据源,先加载外部数据源获取地理参考 + if (externalDataSource_) { + LOG_I("Stage4: Using external data source"); + + // 检查外部数据源是否已加载 + if (!externalDataSource_->IsLoaded()) { + pipeline::DataSourceConfig dsConfig; + dsConfig.input_path = config_.inputPath; + dsConfig.output_path = config_.outputPath; + dsConfig.height_field = config_.heightField; + dsConfig.center_longitude = config_.centerLongitude; + dsConfig.center_latitude = config_.centerLatitude; + + if (!externalDataSource_->Load(dsConfig)) { + LOG_E("Stage4: Failed to load external data source"); + return false; + } + } + + // 从外部数据源获取地理参考 + auto [lon, lat, height] = externalDataSource_->GetGeoReference(); + if (lon != 0.0 || lat != 0.0) { + config_.centerLongitude = lon; + config_.centerLatitude = lat; + } + + LOG_I("Stage4: External data source loaded with %zu items", + externalDataSource_->GetItemCount()); + // 继续执行下面的数据池加载逻辑 + } + + // 原有逻辑:内部加载数据(无论是否使用外部数据源,都需要加载数据池) + dataPool_ = std::make_unique(); + // 使用带几何数据加载的方法,传入中心点坐标用于ENU转换 + // 注意:这里传入的中心点可能是投影坐标,需要在加载后重新计算 + bool result = dataPool_->loadFromShapefileWithGeometry( + config_.inputPath, config_.heightField, + config_.centerLongitude, config_.centerLatitude + ); + + if (result) { + // 从转换后的数据重新计算中心点(WGS84) + auto worldBounds = dataPool_->computeWorldBounds(); + double centerLon = (worldBounds.minx + worldBounds.maxx) * 0.5; + double centerLat = (worldBounds.miny + worldBounds.maxy) * 0.5; + + // 如果中心点变化较大,重新加载几何数据 + if (std::abs(centerLon - config_.centerLongitude) > 1.0 || + std::abs(centerLat - config_.centerLatitude) > 1.0) { + LOG_I("Stage4: Recalculating geometry with corrected center: lon=%.6f, lat=%.6f", + centerLon, centerLat); + + // 使用正确的中心点重新加载数据 + dataPool_->clear(); + result = dataPool_->loadFromShapefileWithGeometry( + config_.inputPath, config_.heightField, + centerLon, centerLat + ); + + // 更新配置中的中心点 + config_.centerLongitude = centerLon; + config_.centerLatitude = centerLat; + + // 重新初始化 B3DM 生成器 + b3dmGenerator_ = std::make_unique(centerLon, centerLat); + } + } + + return result; +} + +bool ShapefileProcessor::buildSpatialIndex() { + // 计算世界包围盒 + auto worldBounds = dataPool_->computeWorldBounds(); + spatial::core::SpatialBounds bounds3d( + std::array{worldBounds.minx, worldBounds.miny, worldBounds.minHeight}, + std::array{worldBounds.maxx, worldBounds.maxy, worldBounds.maxHeight} + ); + + // 转换为空间项列表 + spatial::core::SpatialItemList spatialItems; + for (const auto& item : dataPool_->getAllItems()) { + auto adapter = std::make_shared(item); + spatialItems.push_back(adapter); + } + + // 使用四叉树策略构建索引 + spatial::strategy::QuadtreeStrategy strategy; + quadtreeIndex_ = strategy.buildIndex(spatialItems, bounds3d, config_.quadtreeConfig); + + return quadtreeIndex_ != nullptr; +} + +bool ShapefileProcessor::generateB3DMFiles() { + // 收集所有叶子节点 + std::vector leaves; + collectLeafNodes(quadtreeIndex_->getRootNode(), leaves); + + if (leaves.empty()) { + LOG_E("Stage4: No leaf nodes found"); + return false; + } + + LOG_I("Stage4: Processing %zu leaf nodes", leaves.size()); + + // 为每个叶子节点生成 B3DM + for (const auto* leaf : leaves) { + std::string b3dmPath = generateB3DMForNode(leaf, config_.outputPath); + if (b3dmPath.empty()) { + LOG_W("Stage4: Failed to generate B3DM for leaf node"); + } + } + + return true; +} + +std::string ShapefileProcessor::generateB3DMForNode( + const spatial::core::SpatialIndexNode* node, + const std::string& outputDir) { + + if (!node) { + return ""; + } + + // 获取节点中的所有要素 + auto items = node->getItems(); + if (items.empty()) { + return ""; + } + + // 转换为 ShapefileSpatialItem 指针列表 + std::vector shapefileItems; + shapefileItems.reserve(items.size()); + + for (const auto& itemRef : items) { + auto* adapter = dynamic_cast(itemRef.get()); + if (adapter) { + shapefileItems.push_back(adapter->getItem()); + } + } + + if (shapefileItems.empty()) { + return ""; + } + + // 获取节点坐标(优先使用四叉树坐标) + int z, x, y; + auto* qtNode = dynamic_cast(node); + if (qtNode) { + auto coord = qtNode->getCoord(); + z = coord.z; + x = coord.x; + y = coord.y; + } else { + // 回退:使用深度和包围盒中心计算 + auto bounds = node->getBounds(); + z = static_cast(node->getDepth()); + double centerX = (bounds.min()[0] + bounds.max()[0]) * 0.5; + double centerY = (bounds.min()[1] + bounds.max()[1]) * 0.5; + x = static_cast(centerX * 1000) % 1000; + y = static_cast(centerY * 1000) % 1000; + } + + // 构建输出路径 + std::filesystem::path b3dmDir = std::filesystem::path(outputDir) / + "tile" / + std::to_string(z) / + std::to_string(x) / + std::to_string(y); + std::filesystem::create_directories(b3dmDir); + + // 配置 LOD 级别 + std::vector lodLevels; + + if (config_.enableLOD) { + // 使用默认 LOD 配置: [1.0, 0.5, 0.25] + std::vector ratios = {1.0f, 0.5f, 0.25f}; + float base_error = 0.01f; + bool draco_for_lod0 = false; + + lodLevels = build_lod_levels( + ratios, + base_error, + config_.simplifyParams, + config_.dracoParams, + draco_for_lod0 + ); + } else { + // 只生成 LOD0 + LODLevelSettings level; + level.target_ratio = 1.0f; + level.target_error = config_.simplifyParams.target_error; + level.enable_simplification = config_.enableSimplification; + level.enable_draco = config_.enableDraco; + level.simplify = config_.simplifyParams; + level.draco = config_.dracoParams; + lodLevels.push_back(level); + } + + // 生成每个 LOD 级别的 B3DM + std::vector generatedFiles; + + for (size_t i = 0; i < lodLevels.size(); ++i) { + const auto& level = lodLevels[i]; + + // 构建文件名 + std::string filename = "content_lod" + std::to_string(i) + ".b3dm"; + std::filesystem::path b3dmPath = b3dmDir / filename; + + // 配置简化参数 + std::optional simplifyOpt = std::nullopt; + if (level.enable_simplification) { + simplifyOpt = level.simplify; + simplifyOpt->target_ratio = level.target_ratio; + simplifyOpt->target_error = level.target_error; + } + + // 配置 Draco 参数 + std::optional dracoOpt = std::nullopt; + if (level.enable_draco) { + dracoOpt = level.draco; + dracoOpt->enable_compression = true; + } + + // 生成 B3DM + std::string b3dmData = b3dmGenerator_->generate( + shapefileItems, + true, // withHeight + level.enable_simplification, + simplifyOpt, + level.enable_draco, + dracoOpt + ); + + if (b3dmData.empty()) { + LOG_E("Stage4: Failed to generate B3DM data for LOD %zu", i); + continue; + } + + // 写入文件 + std::ofstream file(b3dmPath, std::ios::binary); + if (!file) { + LOG_E("Stage4: Failed to open B3DM file for writing: %s", b3dmPath.string().c_str()); + continue; + } + + file.write(b3dmData.data(), b3dmData.size()); + file.close(); + + generatedFiles.push_back(filename); + LOG_I("Stage4: Generated %s (ratio: %.2f)", filename.c_str(), level.target_ratio); + } + + if (generatedFiles.empty()) { + LOG_E("Stage4: No B3DM files were generated"); + return ""; + } + + // 返回第一个文件的相对路径(用于兼容) + std::filesystem::path relPath = std::filesystem::path("tile") / + std::to_string(z) / + std::to_string(x) / + std::to_string(y) / + generatedFiles[0]; + + return relPath.generic_string(); +} + +void ShapefileProcessor::collectLeafNodes( + const spatial::core::SpatialIndexNode* node, + std::vector& leaves) { + + if (!node) return; + + if (node->isLeaf()) { + if (node->getItemCount() > 0) { + leaves.push_back(node); + } + return; + } + + // 递归收集子节点 + auto children = node->getChildren(); + for (const auto* child : children) { + if (child) { + collectLeafNodes(child, leaves); + } + } +} + +bool ShapefileProcessor::generateTileset() { + // 构建节点映射表 + buildNodeMap(); + + if (nodeMap_.empty()) { + LOG_E("Stage4: Node map is empty"); + return false; + } + + // 找到根节点 + uint64_t rootKey = 0; + const TileMeta* rootMeta = nullptr; + + for (const auto& [key, meta] : nodeMap_) { + if (meta.z == 0 || (rootMeta == nullptr)) { + rootKey = key; + rootMeta = &meta; + } + } + + if (!rootMeta) { + LOG_E("Stage4: No root node found"); + return false; + } + + // 为 LOD 场景的叶子节点生成子 tileset + if (config_.enableLOD) { + for (const auto& [key, meta] : nodeMap_) { + if (meta.is_leaf) { + if (!generateLeafTileset(meta)) { + LOG_E("Stage4: Failed to generate tileset for leaf node %d/%d/%d", + meta.z, meta.x, meta.y); + return false; + } + } + } + } + + // 创建适配器配置 + AdapterConfig adapterConfig; + adapterConfig.boundingVolumeScaleFactor = config_.boundingVolumeScaleFactor; + adapterConfig.geometricErrorScale = config_.geometricErrorScale; + adapterConfig.applyRootTransform = config_.applyRootTransform; + adapterConfig.minZRoot = rootMeta->z; + adapterConfig.enableLOD = config_.enableLOD; + adapterConfig.lodLevelCount = config_.enableLOD ? 3 : 1; + adapterConfig.lodErrorRatios = {1.0, 0.5, 0.25}; + + // 创建适配器 + ShapefileTilesetAdapter adapter(config_.centerLongitude, config_.centerLatitude, adapterConfig); + + // 构建 Tileset + tileset::Tileset tileset = adapter.buildTileset(*rootMeta, nodeMap_); + tileset.setVersion("1.0"); + tileset.setGltfUpAxis("Z"); + + // 写入文件 + std::filesystem::path tilesetPath = std::filesystem::path(config_.outputPath) / "tileset.json"; + tileset::TilesetWriter writer; + + return writer.writeToFile(tileset, tilesetPath.string()); +} + +bool ShapefileProcessor::generateLeafTileset(const TileMeta& meta) { + using namespace tileset; + + // 创建叶子节点的 tileset(包含 LOD 层级结构) + Tileset leafTileset; + leafTileset.setVersion("1.0"); + leafTileset.setGltfUpAxis("Z"); + + // 计算几何误差(基于包围盒) + double geometricError = calculateGeometricError(meta.bbox); + + // 创建根 tile(对应 LOD0) + Tile rootTile; + rootTile.geometricError = geometricError; + rootTile.refine = "REPLACE"; + + // 设置包围盒(ENU 坐标系,相对于节点中心) + double centerX = (meta.bbox.minx + meta.bbox.maxx) / 2.0; + double centerY = (meta.bbox.miny + meta.bbox.maxy) / 2.0; + double centerZ = (meta.bbox.minHeight + meta.bbox.maxHeight) / 2.0; + + // 将 WGS84 包围盒转换为 ENU 米 + double spanX = (meta.bbox.maxx - meta.bbox.minx) * M_PI / 180.0 * 6378137.0 * std::cos(centerY * std::numbers::pi / 180.0); + double spanY = (meta.bbox.maxy - meta.bbox.miny) * M_PI / 180.0 * 6378137.0; + double spanZ = meta.bbox.maxHeight - meta.bbox.minHeight; + + // 创建 Box 包围体 + std::array boxValues = { + 0.0, 0.0, centerZ, // center + spanX / 2.0, 0.0, 0.0, // x half-axis + 0.0, spanY / 2.0, 0.0, // y half-axis + 0.0, 0.0, spanZ / 2.0 // z half-axis + }; + tileset::Box box(boxValues); + rootTile.boundingVolume = box; + + // 创建 LOD 层级结构:LOD0 -> LOD1 -> LOD2 + std::vector> lodLevels = { + {"content_lod0.b3dm", geometricError * 1.0}, + {"content_lod1.b3dm", geometricError * 0.5}, + {"content_lod2.b3dm", geometricError * 0.25} + }; + + // 构建层级结构 + Tile* currentParent = &rootTile; + for (size_t i = 0; i < lodLevels.size(); ++i) { + const auto& [content, geError] = lodLevels[i]; + + Tile lodTile; + lodTile.boundingVolume = box; + lodTile.geometricError = geError; + lodTile.refine = "REPLACE"; + lodTile.setContent(content); + + // 如果不是最后一个 LOD,需要继续添加子节点 + if (i < lodLevels.size() - 1) { + currentParent->addChild(std::move(lodTile)); + currentParent = ¤tParent->children.back(); + } else { + // 最后一个 LOD,直接添加为叶子节点 + currentParent->addChild(std::move(lodTile)); + } + } + + leafTileset.root = std::move(rootTile); + leafTileset.updateGeometricError(); + + // 写入文件 + std::filesystem::path tilesetDir = std::filesystem::path(config_.outputPath) / + "tile" / + std::to_string(meta.z) / + std::to_string(meta.x) / + std::to_string(meta.y); + std::filesystem::path tilesetPath = tilesetDir / "tileset.json"; + + TilesetWriter writer; + return writer.writeToFile(leafTileset, tilesetPath.string()); +} + +void ShapefileProcessor::buildNodeMap() { + nodeMap_.clear(); + + // 从空间索引根节点开始递归构建 + if (quadtreeIndex_ && quadtreeIndex_->getRootNode()) { + buildNodeMapRecursive(quadtreeIndex_->getRootNode(), nodeMap_); + } + + // 从子节点更新父节点的高度范围(后序遍历) + updateParentHeightsFromChildren(); +} + +void ShapefileProcessor::updateParentHeightsFromChildren() { + LOG_I("Stage4: Updating parent heights from children, node count = %zu", nodeMap_.size()); + + // 收集所有键值并按层级排序(从叶子到根) + std::vector sortedKeys; + for (auto& [key, meta] : nodeMap_) { + sortedKeys.push_back(key); + } + std::sort(sortedKeys.begin(), sortedKeys.end(), [this](const auto& a, const auto& b) { + return nodeMap_[a].z > nodeMap_[b].z; // 从大到小排序 + }); + + // 从叶子节点开始,向上更新父节点的高度 + for (uint64_t childKey : sortedKeys) { + auto& childMeta = nodeMap_[childKey]; + + // 如果子节点使用了默认高度(0-100),跳过 + if (childMeta.bbox.minHeight == 0.0 && childMeta.bbox.maxHeight == 100.0) { + continue; + } + + // 找到所有包含此子节点的父节点并更新 + for (auto& [parentKey, parentMeta] : nodeMap_) { + // 检查 parentMeta 是否包含 childKey 作为子节点 + auto it = std::find(parentMeta.children_keys.begin(), parentMeta.children_keys.end(), childKey); + if (it != parentMeta.children_keys.end()) { + // 如果父节点使用的是默认高度,直接替换 + if (parentMeta.bbox.minHeight == 0.0 && parentMeta.bbox.maxHeight == 100.0) { + parentMeta.bbox.minHeight = childMeta.bbox.minHeight; + parentMeta.bbox.maxHeight = childMeta.bbox.maxHeight; + } else { + // 否则取最小值和最大值 + parentMeta.bbox.minHeight = std::min(parentMeta.bbox.minHeight, childMeta.bbox.minHeight); + parentMeta.bbox.maxHeight = std::max(parentMeta.bbox.maxHeight, childMeta.bbox.maxHeight); + } + } + } + } +} + +void ShapefileProcessor::buildNodeMapRecursive( + const spatial::core::SpatialIndexNode* node, + std::unordered_map& nodes) { + + if (!node) return; + + // 尝试转换为 QuadtreeNode 以获取坐标 + auto* qtNode = dynamic_cast(node); + + // 创建 TileMeta + TileMeta meta; + + if (qtNode) { + // 使用四叉树坐标 + auto coord = qtNode->getCoord(); + meta.z = coord.z; + meta.x = coord.x; + meta.y = coord.y; + } else { + // 回退:使用深度和包围盒中心计算 + meta.z = static_cast(node->getDepth()); + auto bounds = node->getBounds(); + double centerX = (bounds.min()[0] + bounds.max()[0]) * 0.5; + double centerY = (bounds.min()[1] + bounds.max()[1]) * 0.5; + meta.x = static_cast(centerX * 1000) % 1000; + meta.y = static_cast(centerY * 1000) % 1000; + } + + // 设置是否为叶子节点 + meta.is_leaf = node->isLeaf(); + + // 设置 content URI + // 对于叶子节点: + // - 如果启用了 LOD,指向 tileset.json(子 tileset 管理多个 LOD 级别) + // - 如果没有启用 LOD,直接指向 content_lod0.b3dm + // 对于非叶子节点,指向 tileset.json + if (node->isLeaf() && !config_.enableLOD) { + // 非 LOD 场景:叶子节点直接指向 B3DM 文件 + meta.tileset_rel = (std::filesystem::path("tile") / + std::to_string(meta.z) / + std::to_string(meta.x) / + std::to_string(meta.y) / + "content_lod0.b3dm").generic_string(); + } else { + // LOD 场景的叶子节点,或非叶子节点:指向 tileset.json + meta.tileset_rel = (std::filesystem::path("tile") / + std::to_string(meta.z) / + std::to_string(meta.x) / + std::to_string(meta.y) / + "tileset.json").generic_string(); + } + + // 编码键值 + uint64_t key = QuadtreeCoord(meta.z, meta.x, meta.y).encode(); + + // 先递归处理子节点(后序遍历) + if (!node->isLeaf()) { + auto children = node->getChildren(); + for (const auto* child : children) { + if (child) { + // 为子节点获取 key + uint64_t childKey; + auto* childQtNode = dynamic_cast(child); + if (childQtNode) { + auto childCoord = childQtNode->getCoord(); + childKey = QuadtreeCoord(childCoord.z, childCoord.x, childCoord.y).encode(); + } else { + TileMeta childMeta; + childMeta.z = static_cast(child->getDepth()); + auto childBounds = child->getBounds(); + double childCenterX = (childBounds.min()[0] + childBounds.max()[0]) * 0.5; + double childCenterY = (childBounds.min()[1] + childBounds.max()[1]) * 0.5; + childMeta.x = static_cast(childCenterX * 1000) % 1000; + childMeta.y = static_cast(childCenterY * 1000) % 1000; + childKey = QuadtreeCoord(childMeta.z, childMeta.x, childMeta.y).encode(); + } + meta.children_keys.push_back(childKey); + + buildNodeMapRecursive(child, nodes); + } + } + } + + // 计算包围盒(从子节点合并或从要素计算) + if (node->isLeaf()) { + // 叶子节点:从要素的实际包围盒合并 + bool firstItem = true; + double minHeight = std::numeric_limits::max(); + double maxHeight = std::numeric_limits::lowest(); + + auto items = node->getItems(); + for (const auto& itemRef : items) { + auto* adapter = dynamic_cast(itemRef.get()); + if (adapter) { + const auto* item = adapter->getItem(); + if (item) { + if (firstItem) { + meta.bbox = item->bounds; + firstItem = false; + } else { + meta.bbox = mergeBBox(meta.bbox, item->bounds); + } + minHeight = std::min(minHeight, item->bounds.minHeight); + maxHeight = std::max(maxHeight, item->bounds.maxHeight); + } + } + } + + // 如果没有要素,使用四叉树的空间范围作为回退 + if (firstItem) { + auto bounds = node->getBounds(); + meta.bbox.minx = bounds.min()[0]; + meta.bbox.maxx = bounds.max()[0]; + meta.bbox.miny = bounds.min()[1]; + meta.bbox.maxy = bounds.max()[1]; + minHeight = 0.0; + maxHeight = 100.0; + } + + meta.bbox.minHeight = minHeight; + meta.bbox.maxHeight = maxHeight; + } else { + // 非叶子节点:从子节点合并包围盒 + bool firstChild = true; + for (uint64_t childKey : meta.children_keys) { + auto it = nodes.find(childKey); + if (it != nodes.end()) { + const TileMeta& childMeta = it->second; + if (firstChild) { + meta.bbox = childMeta.bbox; + firstChild = false; + } else { + meta.bbox = mergeBBox(meta.bbox, childMeta.bbox); + } + } + } + } + + // 计算几何误差 + if (node->isLeaf()) { + // 叶子节点:基于包围盒计算 + meta.geometric_error = calculateGeometricError(meta.bbox); + } else { + // 父节点:从子节点的最大几何误差计算 + double maxChildGE = 0.0; + for (uint64_t childKey : meta.children_keys) { + auto it = nodes.find(childKey); + if (it != nodes.end()) { + maxChildGE = std::max(maxChildGE, it->second.geometric_error); + } + } + meta.geometric_error = maxChildGE * 2.0; + } + + nodes[key] = std::move(meta); +} + +TileBBox ShapefileProcessor::convertBounds(const spatial::core::SpatialBounds& bounds) { + TileBBox bbox; + bbox.minx = bounds.min()[0]; + bbox.maxx = bounds.max()[0]; + bbox.miny = bounds.min()[1]; + bbox.maxy = bounds.max()[1]; + bbox.minHeight = 0.0; + bbox.maxHeight = 100.0; // 默认高度,实际应从数据中获取 + return bbox; +} + +double ShapefileProcessor::calculateGeometricError(const TileBBox& bbox) { + // 使用与阶段3 (shp23dtile.cpp) 相同的计算方式 + double spanX = bbox.maxx - bbox.minx; + double spanY = bbox.maxy - bbox.miny; + double spanZ = bbox.maxHeight - bbox.minHeight; + + // 将经纬度跨度转换为米 (近似) + // 使用与阶段3相同的公式:乘以 1.05 膨胀系数 + double centerLat = (bbox.miny + bbox.maxy) / 2.0; + const double pi = std::acos(-1); + double meterX = (spanX * pi / 180.0) * 1.05 * 6378137.0 * std::cos(centerLat * pi / 180.0); + double meterY = (spanY * pi / 180.0) * 1.05 * 6378137.0; + + double maxSpan = std::max({meterX, meterY, spanZ}); + if (maxSpan <= 0.0) { + return 0.0; + } + return maxSpan / 20.0; +} + +} // namespace shapefile diff --git a/src/shapefile/shapefile_processor.h b/src/shapefile/shapefile_processor.h new file mode 100644 index 00000000..d5576dd9 --- /dev/null +++ b/src/shapefile/shapefile_processor.h @@ -0,0 +1,226 @@ +#pragma once + +/** + * @file shapefile_processor.h + * @brief Shapefile 处理器 - 步骤3修改:支持外部 TilesetBuilder + * + * 修改内容: + * - 步骤1:添加 SetDataSource 方法,允许外部注入数据源 + * - 步骤2:添加 SetSpatialIndex 方法,允许外部注入空间索引 + * - 步骤3:添加 SetTilesetBuilder 方法,允许外部注入 TilesetBuilder + * - 保持原有接口不变,确保向后兼容 + */ + +#include "shapefile_data_pool.h" +#include "../spatial/strategy/quadtree_strategy.h" +#include "../spatial/core/slicing_strategy.h" +#include "shapefile_tile.h" +#include "../common/mesh_processor.h" + +#include +#include +#include +#include +#include + +// 前向声明 - 避免循环依赖 +namespace pipeline { +class DataSource; +class ISpatialIndex; +class ITilesetBuilder; +} + +namespace shapefile { + +// 前向声明 +class B3DMContentGenerator; +class ShapefileTilesetAdapter; +struct AdapterConfig; + +/** + * @brief Shapefile 处理器配置 + */ +struct ShapefileProcessorConfig { + // 输入输出 + std::string inputPath; + std::string outputPath; + + // 高度字段 + std::string heightField; + + // 地理中心 + double centerLongitude = 0.0; + double centerLatitude = 0.0; + + // 是否启用 LOD + bool enableLOD = false; + + // 简化参数 + bool enableSimplification = false; + SimplificationParams simplifyParams; + + // Draco 压缩参数 + bool enableDraco = false; + DracoCompressionParams dracoParams; + + // 四叉树配置 + spatial::strategy::QuadtreeConfig quadtreeConfig; + + // Tileset 适配器配置 + double boundingVolumeScaleFactor = 2.0; + double geometricErrorScale = 0.5; + bool applyRootTransform = true; +}; + +/** + * @brief 处理结果 + */ +struct ProcessingResult { + bool success = false; + size_t featureCount = 0; + size_t nodeCount = 0; + size_t b3dmCount = 0; + std::string tilesetPath; + std::string errorMessage; +}; + +/** + * @brief Shapefile 处理器 + * + * 步骤3修改:支持外部数据源、空间索引和 TilesetBuilder + */ +class ShapefileProcessor { +public: + explicit ShapefileProcessor(const ShapefileProcessorConfig& config); + ~ShapefileProcessor(); + + /** + * @brief 设置外部数据源(步骤1新增) + * @param dataSource 外部数据源,如果为 nullptr 则内部加载 + * + * 注意:外部数据源的生命周期必须超过 ShapefileProcessor 的生命周期 + */ + void SetDataSource(pipeline::DataSource* dataSource); + + /** + * @brief 设置外部空间索引(步骤2新增) + * @param spatialIndex 外部空间索引,如果为 nullptr 则内部构建 + * + * 注意:外部空间索引的生命周期必须超过 ShapefileProcessor 的生命周期 + */ + void SetSpatialIndex(pipeline::ISpatialIndex* spatialIndex); + + /** + * @brief 设置外部 TilesetBuilder(步骤3新增) + * @param tilesetBuilder 外部 TilesetBuilder,如果为 nullptr 则内部创建 + * + * 注意:外部 TilesetBuilder 的生命周期必须超过 ShapefileProcessor 的生命周期 + */ + void SetTilesetBuilder(pipeline::ITilesetBuilder* tilesetBuilder); + + /** + * @brief 处理 Shapefile 生成 3D Tiles + * @return 处理结果 + */ + ProcessingResult process(); + +private: + ShapefileProcessorConfig config_; + + // 数据池(内部加载时使用 - 向后兼容) + std::unique_ptr dataPool_; + + // 数据源(内部创建时使用) + std::unique_ptr dataSource_; + + // 外部数据源(步骤1新增) + pipeline::DataSource* externalDataSource_ = nullptr; + + // 四叉树索引(内部构建时使用 - 向后兼容) + std::unique_ptr quadtreeIndex_; + + // 空间索引(内部创建时使用) + std::unique_ptr spatialIndex_; + + // 外部空间索引(步骤2新增) + pipeline::ISpatialIndex* externalSpatialIndex_ = nullptr; + + // TilesetBuilder(内部创建时使用) + std::unique_ptr tilesetBuilder_; + + // 外部 TilesetBuilder(步骤3新增) + pipeline::ITilesetBuilder* externalTilesetBuilder_ = nullptr; + + // B3DM 生成器 (前向声明,避免头文件依赖) + class B3DMGeneratorImpl; + std::unique_ptr b3dmGenerator_; + + // 节点映射表(用于 Tileset 生成) + std::unordered_map nodeMap_; + + // ===== 处理步骤 ===== + + // 1. 加载数据 + bool loadData(); + + // 2. 构建四叉树索引 + bool buildSpatialIndex(); + + // 3. 生成 B3DM 文件(遍历四叉树叶子节点) + bool generateB3DMFiles(); + + // 4. 生成 Tileset + bool generateTileset(); + + // ===== 辅助函数 ===== + + // 从空间索引节点生成 B3DM + std::string generateB3DMForNode( + const spatial::core::SpatialIndexNode* node, + const std::string& outputDir + ); + + // 构建节点映射表(用于 Tileset 生成) + void buildNodeMap(); + + // 递归收集叶子节点 + void collectLeafNodes( + const spatial::core::SpatialIndexNode* node, + std::vector& leaves + ); + + // 递归构建节点映射表 + void buildNodeMapRecursive( + const spatial::core::SpatialIndexNode* node, + std::unordered_map& nodes + ); + + // 从子节点更新父节点的高度范围 + void updateParentHeightsFromChildren(); + + // 转换包围盒 + static TileBBox convertBounds(const spatial::core::SpatialBounds& bounds); + + // 计算几何误差 + static double calculateGeometricError(const TileBBox& bbox); + + // 为叶子节点生成 LOD tileset + bool generateLeafTileset(const TileMeta& meta); + + // 获取当前使用的数据源(步骤1新增) + [[nodiscard]] const ShapefileDataPool* GetCurrentDataPool() const; + + // 获取当前使用的数据源(新接口,步骤1补充) + [[nodiscard]] pipeline::DataSource* GetCurrentDataSource(); + + // 获取当前使用的空间索引根节点(步骤2新增) + [[nodiscard]] const spatial::core::SpatialIndexNode* GetCurrentRootNode() const; + + // 获取当前使用的空间索引(新接口,步骤2补充) + [[nodiscard]] pipeline::ISpatialIndex* GetCurrentSpatialIndex(); + + // 获取当前使用的 TilesetBuilder(步骤3新增) + [[nodiscard]] pipeline::ITilesetBuilder* GetCurrentTilesetBuilder(); +}; + +} // namespace shapefile diff --git a/src/shapefile/shapefile_spatial_item_adapter.h b/src/shapefile/shapefile_spatial_item_adapter.h new file mode 100644 index 00000000..79aea743 --- /dev/null +++ b/src/shapefile/shapefile_spatial_item_adapter.h @@ -0,0 +1,63 @@ +#pragma once + +/** + * @file shapefile_spatial_item_adapter.h + * @brief Shapefile 空间项适配器 + * + * 阶段2迁移组件:将 ShapefileSpatialItem 适配为空间索引接口 + */ + +#include "shapefile_data_pool.h" +#include "../spatial/core/spatial_item.h" +#include "../spatial/core/spatial_bounds.h" + +namespace shapefile { + +/** + * @brief Shapefile 空间项适配器 + * + * 将 ShapefileSpatialItem 包装为空间索引可用的 SpatialItem 接口 + */ +class ShapefileSpatialItemAdapter : public spatial::core::SpatialItem { +public: + // 从 shared_ptr 构造 + explicit ShapefileSpatialItemAdapter(const ShapefileDataPool::ItemPtr& item) + : item_(item) {} + + // 从原始指针构造(用于B3DM生成器) + explicit ShapefileSpatialItemAdapter(const ShapefileSpatialItem* item) + : item_(item, [](const ShapefileSpatialItem*) {}) {} + + spatial::core::SpatialBounds getBounds() const override { + const auto& b = item_->bounds; + return spatial::core::SpatialBounds( + std::array{b.minx, b.miny, b.minHeight}, + std::array{b.maxx, b.maxy, b.maxHeight} + ); + } + + size_t getId() const override { + return static_cast(item_->featureId); + } + + std::array getCenter() const override { + return { + (item_->bounds.minx + item_->bounds.maxx) * 0.5, + (item_->bounds.miny + item_->bounds.maxy) * 0.5, + (item_->bounds.minHeight + item_->bounds.maxHeight) * 0.5 + }; + } + + // 获取原始数据项 + const ShapefileSpatialItem* getItem() const { return item_.get(); } + const ShapefileDataPool::ItemPtr& getItemPtr() const { return item_; } + + int getFeatureId() const { return item_->featureId; } + const TileBBox& getBounds2D() const { return item_->bounds; } + const std::map& getProperties() const { return item_->properties; } + +private: + ShapefileDataPool::ItemPtr item_; +}; + +} // namespace shapefile diff --git a/src/shapefile/shapefile_tile.h b/src/shapefile/shapefile_tile.h new file mode 100644 index 00000000..7234d7e3 --- /dev/null +++ b/src/shapefile/shapefile_tile.h @@ -0,0 +1,171 @@ +#pragma once + +/** + * @file shapefile_tile.h + * @brief Shapefile 业务逻辑层数据结构 + * + * 该模块包含 Shapefile 特有的业务数据结构,与 3D Tiles 标准无关。 + * 主要责任: + * 1. 管理 Shapefile 源数据的包围盒 (WGS84 经纬度) + * 2. 管理四叉树空间索引 (z/x/y) + * 3. 管理构建过程中的临时状态 + * + * 坐标系统: + * - 所有几何坐标使用 WGS84 经纬度 (度) + * - 高度使用米 + */ + +#include +#include +#include +#include + +#include "../common/tile_path_utils.h" + +namespace shapefile { + +/** + * @brief Shapefile 瓦片包围盒 (WGS84 坐标系) + * + * 使用度作为经纬度单位,这是 Shapefile 源数据的原始坐标系。 + * 注意:这不是 3D Tiles 标准的包围体,需要转换后才能使用。 + */ +struct TileBBox { + double minx = 0.0; // 最小经度 (度) + double maxx = 0.0; // 最大经度 (度) + double miny = 0.0; // 最小纬度 (度) + double maxy = 0.0; // 最大纬度 (度) + double minHeight = 0.0; // 最小高度 (米) + double maxHeight = 0.0; // 最大高度 (米) + + // 默认构造函数 + TileBBox() = default; + + // 从角点构造 + TileBBox(double minx_deg, double maxx_deg, double miny_deg, double maxy_deg, + double min_h, double max_h) + : minx(minx_deg), maxx(maxx_deg), miny(miny_deg), maxy(maxy_deg), + minHeight(min_h), maxHeight(max_h) {} + + // 获取中心点经度 + double centerLon() const { return (minx + maxx) * 0.5; } + + // 获取中心点纬度 + double centerLat() const { return (miny + maxy) * 0.5; } + + // 获取宽度 (度) + double widthDeg() const { return maxx - minx; } + + // 获取高度 (度) + double heightDeg() const { return maxy - miny; } + + // 判断是否有效 + bool isValid() const { + return minx < maxx && miny < maxy && minHeight <= maxHeight; + } +}; + +/** + * @brief 四叉树坐标 + * + * 用于标识瓦片在四叉树中的位置 + */ +struct QuadtreeCoord { + int z = 0; // 层级 + int x = 0; // X 坐标 + int y = 0; // Y 坐标 + + QuadtreeCoord() = default; + QuadtreeCoord(int level, int x_coord, int y_coord) + : z(level), x(x_coord), y(y_coord) {} + + // 编码为 64 位整数 (用于哈希表键值) + uint64_t encode() const { + return (static_cast(z) << 42) | + (static_cast(x) << 21) | + static_cast(y); + } + + // 从编码解码 + static QuadtreeCoord decode(uint64_t key) { + QuadtreeCoord coord; + coord.z = static_cast((key >> 42) & 0x1FFFFF); + coord.x = static_cast((key >> 21) & 0x1FFFFF); + coord.y = static_cast(key & 0x1FFFFF); + return coord; + } + + // 获取父节点坐标 + QuadtreeCoord parent() const { + if (z <= 0) return *this; + return QuadtreeCoord(z - 1, x / 2, y / 2); + } + + // 判断两个坐标是否相等 + bool operator==(const QuadtreeCoord& other) const { + return z == other.z && x == other.x && y == other.y; + } +}; + +/** + * @brief Shapefile 瓦片元数据 + * + * 包含瓦片的所有业务信息,用于构建 3D Tiles 层次结构。 + * 注意:这是构建时的临时数据结构,最终会转换为 tileset::Tile + */ +struct TileMeta { + // 四叉树坐标 (保持与旧代码兼容的直接访问) + int z = 0; // 层级 + int x = 0; // X 坐标 + int y = 0; // Y 坐标 + + TileBBox bbox; // 包围盒 (WGS84 度) + double geometric_error = 0.0; // 几何误差 + std::string tileset_rel; // tileset.json 相对输出根目录的路径 + std::string orig_tileset_rel; // 原始平面路径 (tile/z/x/y.json) + bool is_leaf = false; // 是否为叶子节点 + std::vector children_keys; // 子节点编码键值 + double max_child_ge = 0.0; // 子节点最大几何误差 (聚合时使用) + + // 获取编码键值 + uint64_t key() const { return QuadtreeCoord(z, x, y).encode(); } +}; + +/** + * @brief 合并两个包围盒 + */ +inline TileBBox mergeBBox(const TileBBox& a, const TileBBox& b) { + TileBBox r; + r.minx = std::min(a.minx, b.minx); + r.maxx = std::max(a.maxx, b.maxx); + r.miny = std::min(a.miny, b.miny); + r.maxy = std::max(a.maxy, b.maxy); + r.minHeight = std::min(a.minHeight, b.minHeight); + r.maxHeight = std::max(a.maxHeight, b.maxHeight); + return r; +} + +/** + * @brief 生成瓦片路径 + * + * 根据四叉树坐标生成 tileset.json 的相对路径 + * 使用公共模块的TilePathUtils实现统一路径格式 + * + * @param coord 四叉树坐标 + * @param min_z 最小层级 (该层级及以下的瓦片放在根目录) + * @return 相对路径,如 "tileset.json" 或 "tile/5/3/2/tileset.json" + */ +inline std::string tilesetPathForNode(const QuadtreeCoord& coord, int min_z) { + // 使用公共模块的TilePathUtils + common::TileCoord tileCoord(coord.z, coord.x, coord.y); + std::string path = common::TilePathUtils::getTilesetPath(tileCoord); + + // 如果是最小层级,返回根目录的tileset.json + if (coord.z <= min_z) { + return "tileset.json"; + } + + return path; +} + +} // namespace shapefile diff --git a/src/shapefile/shapefile_tile_meta.h b/src/shapefile/shapefile_tile_meta.h new file mode 100644 index 00000000..f70d531b --- /dev/null +++ b/src/shapefile/shapefile_tile_meta.h @@ -0,0 +1,86 @@ +#pragma once + +/** + * @file shapefile/shapefile_tile_meta.h + * @brief Shapefile瓦片元数据 + * + * 继承自common::TileMeta,添加Shapefile特有的属性 + */ + +#include "../common/tile_meta.h" +#include "shapefile_tile.h" // For TileBBox, QuadtreeCoord + +namespace shapefile { + +/** + * @brief Shapefile瓦片元数据 + * + * 继承通用TileMeta,添加WGS84经纬度信息 + */ +class ShapefileTileMeta : public common::TileMeta { +public: + // WGS84经纬度包围盒(度) + TileBBox wgs84BBox; + + // 原始平面路径(向后兼容) + std::string origTilesetRel; + + // 子节点最大几何误差(聚合时使用) + double maxChildGe = 0.0; + + ShapefileTileMeta() = default; + + /** + * @brief 从QuadtreeCoord构造 + */ + explicit ShapefileTileMeta(const QuadtreeCoord& coord) { + this->coord = common::TileCoord(coord.z, coord.x, coord.y); + } + + /** + * @brief 从TileMeta构造(用于向上转型) + */ + explicit ShapefileTileMeta(const common::TileMeta& base) + : common::TileMeta(base) {} + + /** + * @brief 获取四叉树坐标 + */ + QuadtreeCoord getQuadtreeCoord() const { + return QuadtreeCoord(coord.z, coord.x, coord.y); + } + + /** + * @brief 从TileBBox设置包围盒 + */ + void setFromTileBBox(const TileBBox& tbbox) { + wgs84BBox = tbbox; + // 同时设置基类的bbox(使用度作为单位,后续会转换) + bbox = common::BoundingBox( + tbbox.minx, tbbox.maxx, + tbbox.miny, tbbox.maxy, + tbbox.minHeight, tbbox.maxHeight + ); + } + + /** + * @brief 转换为TileBBox + */ + TileBBox toTileBBox() const { + return wgs84BBox; + } +}; + +/** + * @brief Shapefile瓦片元数据指针类型 + */ +using ShapefileTileMetaPtr = std::shared_ptr; + +/** + * @brief 从通用TileMeta转换为ShapefileTileMeta + */ +inline ShapefileTileMetaPtr toShapefileTileMeta(const common::TileMetaPtr& meta) { + return std::dynamic_pointer_cast(meta); +} + +} // namespace shapefile diff --git a/src/shapefile/shapefile_tileset_adapter.cpp b/src/shapefile/shapefile_tileset_adapter.cpp new file mode 100644 index 00000000..c56bf6a6 --- /dev/null +++ b/src/shapefile/shapefile_tileset_adapter.cpp @@ -0,0 +1,8 @@ +#include "shapefile_tileset_adapter.h" +#include "../common/tile_path_utils.h" + +namespace shapefile { + +// 所有实现都在头文件中,此文件仅用于编译单元 + +} // namespace shapefile diff --git a/src/shapefile/shapefile_tileset_adapter.h b/src/shapefile/shapefile_tileset_adapter.h new file mode 100644 index 00000000..83ab2e02 --- /dev/null +++ b/src/shapefile/shapefile_tileset_adapter.h @@ -0,0 +1,345 @@ +#pragma once + +/** + * @file shapefile_tileset_adapter.h + * @brief Shapefile 业务层到 Tileset 标准层的适配器 + * + * 基于 common::QuadtreeTilesetBuilder 的 Shapefile 专用适配器 + * 提供与旧代码兼容的 API + * + * 主要职责: + * 1. 坐标系统转换:WGS84 (度) → ENU (米) + * 2. 包围体转换:TileBBox → tileset::Box + * 3. 层次结构构建:四叉树 → Tile 嵌套结构 + * 4. Transform 矩阵生成 (根节点) + */ + +#include "shapefile_tile.h" +#include "shapefile_tile_meta.h" +#include "../common/tileset_builder.h" +#include "../tileset/tileset_types.h" +#include "../tileset/bounding_volume.h" +#include "../tileset/transform.h" +#include "../coords/coordinate_transformer.h" +#include "../coords/geo_math.h" + +#include +#include +#include +#include + +namespace shapefile { + +// 使用 coords 命名空间的数学函数 +using coords::degree2rad; +using coords::lati_to_meter; +using coords::longti_to_meter; + +/** + * @brief 适配器配置选项 + */ +struct AdapterConfig { + // 包围盒扩展系数 (防止浮点误差导致的裁剪) + double boundingVolumeScaleFactor = 1.0; + + // 几何误差缩放系数 + double geometricErrorScale = 0.5; + + // 是否对根节点应用 ENU->ECEF 变换 + bool applyRootTransform = true; + + // 最小层级 (该层级及以下的瓦片为根节点) + int minZRoot = 0; + + // 是否启用 LOD + bool enableLOD = false; + + // LOD 级别数量 (默认 3: lod0, lod1, lod2) + int lodLevelCount = 3; + + // LOD 几何误差比例 [lod0, lod1, lod2, ...] + std::vector lodErrorRatios = {1.0, 0.5, 0.25}; + + /** + * @brief 转换为TilesetBuilderConfig + */ + common::TilesetBuilderConfig toBuilderConfig() const { + common::TilesetBuilderConfig config; + config.boundingVolumeScale = boundingVolumeScaleFactor; + config.childGeometricErrorMultiplier = geometricErrorScale; + config.enableLOD = enableLOD; + config.lodLevelCount = lodLevelCount; + return config; + } +}; + +/** + * @brief Shapefile 到 Tileset 的适配器类 + * + * 继承自 common::QuadtreeTilesetBuilder,提供 Shapefile 专用功能 + */ +class ShapefileTilesetAdapter : public common::QuadtreeTilesetBuilder { +public: + /** + * @brief 构造函数 + * + * @param globalCenterLon 全局中心经度 (度) + * @param globalCenterLat 全局中心纬度 (度) + * @param config 适配器配置 + */ + ShapefileTilesetAdapter(double globalCenterLon, + double globalCenterLat, + const AdapterConfig& config = {}); + + /** + * @brief 将单个 TileMeta 转换为 tileset::Tile + * + * @param meta Shapefile 瓦片元数据 + * @return 标准 3D Tiles 瓦片对象 + */ + tileset::Tile convertTile(const TileMeta& meta) const; + + /** + * @brief 构建完整的 Tileset(兼容旧API) + * + * 递归地将整个四叉树结构转换为 3D Tiles 层次结构 + * + * @param rootMeta 根节点元数据 + * @param allMetas 所有节点的元数据映射表 + * @return 完整的 Tileset 对象 + */ + tileset::Tileset buildTileset( + const TileMeta& rootMeta, + const std::unordered_map& allMetas) const; + + /** + * @brief 构建完整的 Tileset(新API,使用通用TileMeta) + * + * @param rootMeta 根节点元数据 + * @param allMetas 所有节点的元数据映射表 + * @return 完整的 Tileset 对象 + */ + tileset::Tileset buildTileset( + const common::TileMetaPtr& rootMeta, + const common::TileMetaMap& allMetas) const; + + /** + * @brief 计算几何误差 + * + * 基于包围盒的对角线长度计算几何误差 + * + * @param bbox Shapefile 包围盒 + * @return 几何误差值 + */ + double computeGeometricError(const TileBBox& bbox) const; + + /** + * @brief 将 TileBBox 转换为 tileset::Box (ENU 坐标系) + * + * @param bbox Shapefile 包围盒 (WGS84 度) + * @return 3D Tiles Box 包围体 (ENU 米) + */ + tileset::Box convertBoundingBox(const TileBBox& bbox) const; + + /** + * @brief 生成根节点的 ENU->ECEF 变换矩阵 + * + * @param centerLon 中心经度 (度) + * @param centerLat 中心纬度 (度) + * @param minHeight 最小高度 (米) + * @return 4x4 变换矩阵 + */ + static tileset::TransformMatrix createRootTransform(double centerLon, + double centerLat, + double minHeight); + +private: + AdapterConfig adapterConfig_; + + // 递归构建子节点(兼容旧API) + void buildChildren(tileset::Tile& parentTile, + const TileMeta& parentMeta, + const std::unordered_map& allMetas) const; + + // 将旧版TileMeta转换为新版 + common::TileMetaPtr convertToPipelineMeta(const TileMeta& meta) const; +}; + +// ============================================================================ +// 内联实现 +// ============================================================================ + +inline ShapefileTilesetAdapter::ShapefileTilesetAdapter(double globalCenterLon, + double globalCenterLat, + const AdapterConfig& config) + : common::QuadtreeTilesetBuilder(globalCenterLon, globalCenterLat, config.toBuilderConfig()) + , adapterConfig_(config) {} + +inline tileset::Box ShapefileTilesetAdapter::convertBoundingBox(const TileBBox& bbox) const { + // 1. 计算中心点经纬度 + double centerLon = bbox.centerLon(); + double centerLat = bbox.centerLat(); + + // 2. 计算经纬度跨度 (弧度) + double lonRadSpan = degree2rad(bbox.widthDeg()); + double latRadSpan = degree2rad(bbox.heightDeg()); + + // 3. 转换为米 (ENU 坐标系) + double halfW = longti_to_meter(lonRadSpan * 0.5, degree2rad(centerLat)) * + 1.05 * adapterConfig_.boundingVolumeScaleFactor; + double halfH = lati_to_meter(latRadSpan * 0.5) * + 1.05 * adapterConfig_.boundingVolumeScaleFactor; + double halfZ = (bbox.maxHeight - bbox.minHeight) * 0.5 * adapterConfig_.boundingVolumeScaleFactor; + + // 4. 计算相对于全局中心的 ENU 偏移 + double offsetX = longti_to_meter(degree2rad(centerLon - globalCenterLon_), + degree2rad(globalCenterLat_)); + double offsetY = lati_to_meter(degree2rad(centerLat - globalCenterLat_)); + + // 5. 计算 Z 轴中心点 + double centerZ = (bbox.minHeight + bbox.maxHeight) * 0.5; + + // 6. 创建 Box (中心点 + 半轴长度) + return tileset::Box::fromCenterAndHalfLengths( + offsetX, offsetY, centerZ, halfW, halfH, halfZ); +} + +inline double ShapefileTilesetAdapter::computeGeometricError(const TileBBox& bbox) const { + // 基于包围盒对角线计算几何误差 + double spanX = bbox.widthDeg(); + double spanY = bbox.heightDeg(); + double spanZ = bbox.maxHeight - bbox.minHeight; + + // 将经纬度跨度转换为米 (近似) + double centerLat = bbox.centerLat(); + double meterX = longti_to_meter(degree2rad(spanX), degree2rad(centerLat)); + double meterY = lati_to_meter(degree2rad(spanY)); + + double maxSpan = std::max({meterX, meterY, spanZ}); + if (maxSpan <= 0.0) { + return 0.0; + } + return maxSpan / 20.0 * adapterConfig_.geometricErrorScale; +} + +inline tileset::TransformMatrix ShapefileTilesetAdapter::createRootTransform( + double centerLon, double centerLat, double minHeight) { + // 使用 CoordinateTransformer 计算 ENU->ECEF 变换矩阵 + glm::dmat4 enuToEcef = coords::CoordinateTransformer::CalcEnuToEcefMatrix( + centerLon, centerLat, minHeight); + + // 转换为 tileset::TransformMatrix (std::array) + tileset::TransformMatrix matrix; + for (int c = 0; c < 4; ++c) { + for (int r = 0; r < 4; ++r) { + matrix[c * 4 + r] = enuToEcef[c][r]; + } + } + return matrix; +} + +inline tileset::Tile ShapefileTilesetAdapter::convertTile(const TileMeta& meta) const { + tileset::Tile tile; + + // 1. 设置包围体 (ENU 坐标系) + tile.boundingVolume = convertBoundingBox(meta.bbox); + + // 2. 设置几何误差 + tile.geometricError = meta.geometric_error > 0.0 + ? meta.geometric_error + : computeGeometricError(meta.bbox); + + // 3. 设置细化策略 + tile.refine = "REPLACE"; + + // 如果是根节点,添加 ENU->ECEF 变换矩阵 + if (adapterConfig_.applyRootTransform && meta.z <= adapterConfig_.minZRoot) { + double centerLon = meta.bbox.centerLon(); + double centerLat = meta.bbox.centerLat(); + double minHeight = meta.bbox.minHeight; + + tileset::TransformMatrix transform = createRootTransform( + centerLon, centerLat, minHeight); + tile.setTransform(transform); + } + + // 5. 如果是叶子节点,设置内容 + if (meta.is_leaf && !meta.tileset_rel.empty()) { + tile.setContent(meta.tileset_rel); + } + + return tile; +} + +inline void ShapefileTilesetAdapter::buildChildren( + tileset::Tile& parentTile, + const TileMeta& parentMeta, + const std::unordered_map& allMetas) const { + + for (uint64_t childKey : parentMeta.children_keys) { + auto it = allMetas.find(childKey); + if (it == allMetas.end()) { + continue; + } + + const TileMeta& childMeta = it->second; + tileset::Tile childTile = convertTile(childMeta); + + // 递归构建子节点的子节点 + if (!childMeta.children_keys.empty()) { + buildChildren(childTile, childMeta, allMetas); + } + + parentTile.addChild(std::move(childTile)); + } +} + +inline tileset::Tileset ShapefileTilesetAdapter::buildTileset( + const TileMeta& rootMeta, + const std::unordered_map& allMetas) const { + + // 1. 转换根节点 + tileset::Tile rootTile = convertTile(rootMeta); + + // 2. 递归构建子节点 + if (!rootMeta.children_keys.empty()) { + buildChildren(rootTile, rootMeta, allMetas); + } + + // 3. 创建 Tileset + tileset::Tileset tileset(rootTile); + tileset.setVersion("1.0"); + tileset.setGltfUpAxis("Z"); + + // 4. 设置根节点的几何误差 + double rootGe = rootMeta.geometric_error > 0.0 + ? rootMeta.geometric_error + : computeGeometricError(rootMeta.bbox) * 2.0; + tileset.geometricError = rootGe; + + return tileset; +} + +inline common::TileMetaPtr ShapefileTilesetAdapter::convertToPipelineMeta( + const TileMeta& meta) const { + auto result = std::make_shared(); + result->coord = common::TileCoord(meta.z, meta.x, meta.y); + result->setFromTileBBox(meta.bbox); + result->geometricError = meta.geometric_error; + result->content.uri = meta.tileset_rel; + result->content.hasContent = meta.is_leaf && !meta.tileset_rel.empty(); + result->isLeaf = meta.is_leaf; + result->childrenKeys = meta.children_keys; + result->origTilesetRel = meta.orig_tileset_rel; + result->maxChildGe = meta.max_child_ge; + return result; +} + +inline tileset::Tileset ShapefileTilesetAdapter::buildTileset( + const common::TileMetaPtr& rootMeta, + const common::TileMetaMap& allMetas) const { + // 使用基类的构建方法 + return common::QuadtreeTilesetBuilder::buildTileset(rootMeta, allMetas); +} + +} // namespace shapefile diff --git a/src/shp23dtile.cpp b/src/shp23dtile.cpp deleted file mode 100644 index ea4598f0..00000000 --- a/src/shp23dtile.cpp +++ /dev/null @@ -1,1869 +0,0 @@ -#include -#include -#include -#include "extern.h" - -#include "mesh_processor.h" -#include "attribute_storage.h" -#include "coordinate_transformer.h" -#include "lod_pipeline.h" -#include "shape.h" - -/* vcpkg path */ -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace std; - -using Vextex = vector>; -using Normal = vector>; -using Index = vector>; - -struct bbox -{ - bool isAdd = false; - double minx, maxx, miny, maxy; - bbox() {} - bbox(double x0, double x1, double y0, double y1) { - minx = x0, maxx = x1, miny = y0, maxy = y1; - } - - bool contains(double x, double y) { - return minx <= x - && x <= maxx - && miny <= y - && y <= maxy; - } - - bool contains(bbox& other) { - return contains(other.minx, other.miny) - && contains(other.maxx, other.maxy); - } - - bool intersect(bbox& other) { - return !( - other.minx > maxx - || other.maxx < minx - || other.miny > maxy - || other.maxy < miny); - } -}; - -class node { -public: - bbox _box; - // 1 km ~ 0.01 - double metric = 0.01; - node* subnode[4]; - std::vector geo_items; -public: - int _x = 0; - int _y = 0; - int _z = 0; - - void set_no(int x, int y, int z) { - _x = x; - _y = y; - _z = z; - } - -public: - - node(bbox& box) { - _box = box; - for (int i = 0; i < 4; i++) { - subnode[i] = 0; - } - } - - ~node() { - for (int i = 0; i < 4; i++) { - if (subnode[i]) { - delete subnode[i]; - } - } - } - - void split() { - double c_x = (_box.minx + _box.maxx) / 2.0; - double c_y = (_box.miny + _box.maxy) / 2.0; - for (int i = 0; i < 4; i++) { - if (!subnode[i]) { - switch (i) { - case 0: - { - bbox box(_box.minx, c_x, _box.miny, c_y); - subnode[i] = new node(box); - subnode[i]->set_no(_x * 2, _y * 2, _z + 1); - } - break; - case 1: - { - bbox box(c_x, _box.maxx, _box.miny, c_y); - subnode[i] = new node(box); - subnode[i]->set_no(_x * 2 + 1, _y * 2, _z + 1); - } - break; - case 2: - { - bbox box(c_x, _box.maxx, c_y, _box.maxy); - subnode[i] = new node(box); - subnode[i]->set_no(_x * 2 + 1, _y * 2 + 1, _z + 1); - } - break; - case 3: - { - bbox box(_box.minx, c_x, c_y, _box.maxy); - subnode[i] = new node(box); - subnode[i]->set_no(_x * 2, _y * 2 + 1, _z + 1); - } - break; - } - } - } - } - - void add(int id, bbox& box) { - if (!_box.intersect(box)) { - return; - } - if (_box.maxx - _box.minx < metric) { - if (!box.isAdd){ - geo_items.push_back(id); - box.isAdd = true; - } - return; - } - if (_box.intersect(box)) { - if (subnode[0] == 0) { - split(); - } - for (int i = 0; i < 4; i++) { - subnode[i]->add(id, box); - //when box is added to a node, stop the loop - if (box.isAdd) { - break; - } - } - } - } - - std::vector& get_ids() { - return geo_items; - } - - void get_all(std::vector& items_array) { - if (!geo_items.empty()) { - items_array.push_back(this); - } - if (subnode[0] != 0) { - for (int i = 0; i < 4; i++) { - subnode[i]->get_all(items_array); - } - } - } -}; - -struct TileBBox { - double minx = 0.0; // degrees - double maxx = 0.0; // degrees - double miny = 0.0; // degrees - double maxy = 0.0; // degrees - double minHeight = 0.0; // meters - double maxHeight = 0.0; // meters -}; - -struct TileMeta { - int z = 0; - int x = 0; - int y = 0; - TileBBox bbox; - double geometric_error = 0.0; - std::string tileset_rel; // relative to output root - std::string orig_tileset_rel; // original flat path (tile/z/x/y.json) used during generation - bool is_leaf = false; - std::vector children_keys; - double max_child_ge = 0.0; // used when aggregating -}; - -static std::string tileset_path_for_node(int z, int x, int y, int min_z) { - if (z <= min_z) { - return "tileset.json"; - } - std::filesystem::path p = "tile"; - p /= std::to_string(z); - p /= std::to_string(x); - p /= std::to_string(y); - p /= "tileset.json"; - return p.generic_string(); -} - -static inline uint64_t encode_key(int z, int x, int y) { - return (static_cast(z) << 42) | (static_cast(x) << 21) | static_cast(y); -} - -static TileBBox make_bbox_from_node(const bbox& b, double min_h, double max_h) { - TileBBox r; - r.minx = b.minx; - r.maxx = b.maxx; - r.miny = b.miny; - r.maxy = b.maxy; - r.minHeight = min_h; - r.maxHeight = max_h; - return r; -} - -static TileBBox merge_bbox(const TileBBox& a, const TileBBox& b) { - TileBBox r; - r.minx = std::min(a.minx, b.minx); - r.maxx = std::max(a.maxx, b.maxx); - r.miny = std::min(a.miny, b.miny); - r.maxy = std::max(a.maxy, b.maxy); - r.minHeight = std::min(a.minHeight, b.minHeight); - r.maxHeight = std::max(a.maxHeight, b.maxHeight); - return r; -} - -struct Polygon_Mesh -{ - std::string mesh_name; - Vextex vertex; - Index index; - Normal normal; - // add some addition - float height; - // Arbitrary feature properties from the source shapefile (per-building) - std::map properties; -}; - -static std::vector flatten_mat(const glm::dmat4& m) { - std::vector mat(16, 0.0); - for (int c = 0; c < 4; ++c) { - for (int r = 0; r < 4; ++r) { - mat[c * 4 + r] = m[c][r]; - } - } - return mat; -} - -static glm::dmat4 make_transform(double center_lon_deg, double center_lat_deg, double min_height) { - // 使用CoordinateTransformer的静态方法计算ENU->ECEF变换矩阵 - return coords::CoordinateTransformer::CalcEnuToEcefMatrix(center_lon_deg, center_lat_deg, min_height); -} - -static nlohmann::json box_to_json(double cx, double cy, double cz, double half_w, double half_h, double half_z) { - double vals[12] = { - cx, cy, cz, - half_w, 0.0, 0.0, - 0.0, half_h, 0.0, - 0.0, 0.0, half_z - }; - nlohmann::json arr = nlohmann::json::array(); - for (int i = 0; i < 12; ++i) arr.push_back(vals[i]); - return arr; -} - -static double compute_geometric_error_from_spans(double span_x, double span_y, double span_z) { - double max_span = std::max({span_x, span_y, span_z}); - if (max_span <= 0.0) { - return 0.0; - } - return max_span / 20.0; -} - -static bool write_node_tileset(const TileMeta& node, - const std::unordered_map& nodes, - const std::string& dest_root, - int min_z_root, - double global_center_lon, - double global_center_lat) { - double center_lon = (node.bbox.minx + node.bbox.maxx) * 0.5; - double center_lat = (node.bbox.miny + node.bbox.maxy) * 0.5; - double width_deg = (node.bbox.maxx - node.bbox.minx); - double height_deg = (node.bbox.maxy - node.bbox.miny); - double lon_rad_span = degree2rad(width_deg); - double lat_rad_span = degree2rad(height_deg); - const double BOUNDING_VOLUME_SCALE_FACTOR = 2.0; - double half_w = longti_to_meter(lon_rad_span * 0.5, degree2rad(center_lat)) * 1.05 * BOUNDING_VOLUME_SCALE_FACTOR; - double half_h = lati_to_meter(lat_rad_span * 0.5) * 1.05 * BOUNDING_VOLUME_SCALE_FACTOR; - double half_z = (node.bbox.maxHeight - node.bbox.minHeight) * 0.5 * BOUNDING_VOLUME_SCALE_FACTOR; - double min_h = node.bbox.minHeight; - - glm::dmat4 parent_global = make_transform(center_lon, center_lat, min_h); - - double center_offset_x = longti_to_meter(degree2rad(center_lon - global_center_lon), degree2rad(global_center_lat)); - double center_offset_y = lati_to_meter(degree2rad(center_lat - global_center_lat)); - - nlohmann::json root; - root["asset"] = { {"version", "1.0"}, {"gltfUpAxis", "Z"} }; - root["geometricError"] = node.geometric_error; - - nlohmann::json root_node; - if (node.z == min_z_root) { - root_node["transform"] = flatten_mat(parent_global); - } - root_node["boundingVolume"]["box"] = box_to_json(center_offset_x, center_offset_y, half_z, half_w, half_h, half_z); - root_node["refine"] = "REPLACE"; - root_node["geometricError"] = node.geometric_error; - - for (auto child_key : node.children_keys) { - auto it = nodes.find(child_key); - if (it == nodes.end()) { - continue; - } - const TileMeta& child = it->second; - nlohmann::json child_node; - double child_center_lon = (child.bbox.minx + child.bbox.maxx) * 0.5; - double child_center_lat = (child.bbox.miny + child.bbox.maxy) * 0.5; - double child_lon_span = degree2rad(child.bbox.maxx - child.bbox.minx); - double child_lat_span = degree2rad(child.bbox.maxy - child.bbox.miny); - double child_half_w = longti_to_meter(child_lon_span * 0.5, degree2rad(child_center_lat)) * 1.05 * BOUNDING_VOLUME_SCALE_FACTOR; - double child_half_h = lati_to_meter(child_lat_span * 0.5) * 1.05 * BOUNDING_VOLUME_SCALE_FACTOR; - double child_half_z = (child.bbox.maxHeight - child.bbox.minHeight) * 0.5 * BOUNDING_VOLUME_SCALE_FACTOR; - double child_min_h = child.bbox.minHeight; - - double child_center_offset_x = longti_to_meter(degree2rad(child_center_lon - global_center_lon), degree2rad(global_center_lat)); - double child_center_offset_y = lati_to_meter(degree2rad(child_center_lat - global_center_lat)); - - child_node["boundingVolume"]["box"] = box_to_json( - child_center_offset_x, - child_center_offset_y, - child_half_z, - child_half_w, - child_half_h, - child_half_z); - child_node["refine"] = "REPLACE"; - child_node["geometricError"] = child.geometric_error; - - std::filesystem::path parent_path = std::filesystem::path(dest_root) / node.tileset_rel; - std::filesystem::path parent_dir = parent_path.parent_path(); - std::error_code ec; - std::filesystem::create_directories(parent_dir, ec); - - std::filesystem::path child_path = std::filesystem::path(dest_root) / child.tileset_rel; - std::filesystem::path child_uri = std::filesystem::relative(child_path, parent_dir); - child_node["content"]["uri"] = "./" + child_uri.generic_string(); - - root_node["children"].push_back(child_node); - } - - root["root"] = root_node; - - std::filesystem::path out_path = std::filesystem::path(dest_root) / node.tileset_rel; - std::filesystem::create_directories(out_path.parent_path()); - - std::ofstream ofs(out_path); - if (!ofs.is_open()) { - LOG_E("write file %s fail", out_path.string().c_str()); - return false; - } - ofs << root.dump(2); - return true; -} - -static void build_hierarchical_tilesets(const std::vector& leaves, - const std::string& dest_root, - double global_center_lon, - double global_center_lat) { - constexpr int MAX_LEVELS = 4; // root + 3 levels of depth to keep hierarchy shallow - if (leaves.empty()) return; - - if (leaves.size() == 1) { - // trivial case: wrap single leaf into a root tileset that references it - std::unordered_map nodes; - auto leaf = leaves.front(); - uint64_t leaf_key = encode_key(leaf.z, leaf.x, leaf.y); - nodes[leaf_key] = leaf; - - TileMeta root; - root.z = leaf.z - 1; // virtual parent level (may be -1) - root.x = leaf.x / 2; - root.y = leaf.y / 2; - root.bbox = leaf.bbox; - root.geometric_error = leaf.geometric_error * 2.0; - root.tileset_rel = "tileset.json"; - root.is_leaf = false; - root.children_keys.push_back(leaf_key); - - // Update leaf tileset_rel to nested path - // Force nested path for leaf node even when z == root.z - std::filesystem::path leaf_path = "tile"; - leaf_path /= std::to_string(leaf.z); - leaf_path /= std::to_string(leaf.x); - leaf_path /= std::to_string(leaf.y); - leaf_path /= "tileset.json"; - leaf.tileset_rel = leaf_path.generic_string(); - nodes[leaf_key] = leaf; - - nodes[encode_key(root.z, root.x, root.y)] = root; - - write_node_tileset(root, nodes, dest_root, root.z, global_center_lon, global_center_lat); - return; - } - - std::unordered_map nodes; - std::vector current_keys; - int max_z = 0; - int min_z = std::numeric_limits::max(); - - for (const auto& leaf : leaves) { - uint64_t key = encode_key(leaf.z, leaf.x, leaf.y); - nodes[key] = leaf; - current_keys.push_back(key); - max_z = std::max(max_z, leaf.z); - min_z = std::min(min_z, leaf.z); - } - - std::vector> levels; - levels.push_back(current_keys); - - while (current_keys.size() > 1) { - if (levels.size() >= MAX_LEVELS) { - break; // stop merging to avoid too deep hierarchy - } - std::unordered_map parent_level; - std::set parent_keys; - for (auto key : current_keys) { - const TileMeta& child = nodes[key]; - int pz = child.z - 1; - if (pz < 0) continue; - int px = child.x / 2; - int py = child.y / 2; - uint64_t pkey = encode_key(pz, px, py); - auto it = parent_level.find(pkey); - if (it == parent_level.end()) { - TileMeta parent; - parent.z = pz; - parent.x = px; - parent.y = py; - parent.is_leaf = false; - parent.bbox = child.bbox; - parent.max_child_ge = child.geometric_error; - parent.children_keys.push_back(key); - parent.tileset_rel = (std::filesystem::path("tile") / std::to_string(pz) / std::to_string(px) / std::to_string(py) / "tileset.json").generic_string(); - parent_level[pkey] = parent; - } else { - it->second.bbox = merge_bbox(it->second.bbox, child.bbox); - it->second.max_child_ge = std::max(it->second.max_child_ge, child.geometric_error); - it->second.children_keys.push_back(key); - } - parent_keys.insert(pkey); - } - - for (auto& kv : parent_level) { - kv.second.geometric_error = kv.second.max_child_ge * 2.0; - nodes[kv.first] = kv.second; - } - - current_keys.assign(parent_keys.begin(), parent_keys.end()); - levels.push_back(current_keys); - } - - // If we stopped early (more than one root candidate), create a synthetic root to bind them - if (current_keys.size() > 1) { - TileMeta root; - root.is_leaf = false; - root.z = nodes[current_keys.front()].z - 1; - root.x = 0; - root.y = 0; - root.bbox = nodes[current_keys.front()].bbox; - root.max_child_ge = 0.0; - for (auto key : current_keys) { - const auto& child = nodes[key]; - root.bbox = merge_bbox(root.bbox, child.bbox); - root.max_child_ge = std::max(root.max_child_ge, child.geometric_error); - root.children_keys.push_back(key); - } - root.geometric_error = root.max_child_ge * 2.0; - uint64_t root_key = encode_key(root.z, root.x, root.y); - nodes[root_key] = root; - current_keys = {root_key}; - levels.push_back(current_keys); - } - - // Determine actual root level (minimum z across all nodes) and assign nested paths - int min_z_all = std::numeric_limits::max(); - if (!current_keys.empty()) { - for (const auto& kv : nodes) { - min_z_all = std::min(min_z_all, kv.second.z); - } - std::unordered_map updated; - for (auto& kv : nodes) { - TileMeta meta = kv.second; - meta.tileset_rel = tileset_path_for_node(meta.z, meta.x, meta.y, min_z_all); - updated[kv.first] = meta; - } - nodes = std::move(updated); - } - - // Relocate leaves into nested structure while preserving per-LOD content names - std::vector leaf_keys; - for (const auto& kv : nodes) { - if (kv.second.is_leaf) leaf_keys.push_back(kv.first); - } - - for (auto key : leaf_keys) { - auto it = nodes.find(key); - if (it == nodes.end()) continue; - TileMeta meta = it->second; - std::filesystem::path src_json = std::filesystem::path(dest_root) / meta.orig_tileset_rel; - std::filesystem::path src_dir = src_json.parent_path(); - std::filesystem::path dst_json = std::filesystem::path(dest_root) / meta.tileset_rel; - std::filesystem::path dst_dir = dst_json.parent_path(); - std::filesystem::create_directories(dst_dir); - // Copy/move all b3dm under src_dir (covers content_lod*.b3dm) - std::error_code ec; - for (auto const& entry : std::filesystem::directory_iterator(src_dir)) { - if (!entry.is_regular_file()) continue; - if (entry.path().extension() != ".b3dm") continue; - std::filesystem::path dst_b3dm = dst_dir / entry.path().filename(); - std::filesystem::rename(entry.path(), dst_b3dm, ec); - if (ec) { - std::filesystem::copy_file(entry.path(), dst_b3dm, std::filesystem::copy_options::overwrite_existing, ec); - std::filesystem::remove(entry.path()); - } - } - - // Copy/move json as-is (content URIs already relative to its directory) - std::filesystem::rename(src_json, dst_json, ec); - if (ec) { - std::ifstream ifs(src_json); - if (!ifs.is_open()) { - LOG_E("open leaf tileset %s fail", src_json.string().c_str()); - } else { - nlohmann::json leaf; - ifs >> leaf; - ifs.close(); - std::ofstream ofs(dst_json); - if (ofs.is_open()) { - ofs << leaf.dump(2); - } else { - LOG_E("write leaf tileset %s fail", dst_json.string().c_str()); - } - std::filesystem::remove(src_json); - } - } - - // update map - nodes[key] = meta; - } - - // write parents from bottom (high z) to top - std::vector parents; - for (const auto& kv : nodes) { - if (!kv.second.is_leaf) { - parents.push_back(kv.second); - } - } - std::sort(parents.begin(), parents.end(), [](const TileMeta& a, const TileMeta& b) { - return a.z > b.z; // write deeper levels first - }); - - for (const auto& parent : parents) { - write_node_tileset(parent, nodes, dest_root, min_z_all, global_center_lon, global_center_lat); - } -} - -osg::ref_ptr make_triangle_mesh_auto(Polygon_Mesh& mesh) { - osg::ref_ptr va = new osg::Vec3Array(mesh.vertex.size()); - for (int i = 0; i < mesh.vertex.size(); i++) { - (*va)[i].set(mesh.vertex[i][0], mesh.vertex[i][1], mesh.vertex[i][2]); - } - osg::ref_ptr trig = new osgUtil::DelaunayTriangulator(); - trig->setInputPointArray(va); - osg::Vec3Array *norms = new osg::Vec3Array; - trig->setOutputNormalArray(norms); - trig->triangulate(); - osg::ref_ptr geometry = new osg::Geometry; - geometry->setVertexArray(va); - geometry->setNormalArray(norms); - auto* uIntId = trig->getTriangles(); - osg::DrawElementsUShort* _set = new osg::DrawElementsUShort(osg::DrawArrays::TRIANGLES); - for (unsigned int i = 0; i < uIntId->getNumPrimitives(); i++) { - _set->addElement(uIntId->getElement(i)); - } - geometry->addPrimitiveSet(_set); - return geometry; -} - -osg::ref_ptr make_triangle_mesh(Polygon_Mesh& mesh) { - osg::ref_ptr va = new osg::Vec3Array(mesh.vertex.size()); - for (int i = 0; i < mesh.vertex.size(); i++) { - (*va)[i].set(mesh.vertex[i][0], mesh.vertex[i][1], mesh.vertex[i][2]); - } - osg::ref_ptr vn = new osg::Vec3Array(mesh.normal.size()); - for (int i = 0; i < mesh.normal.size(); i++) { - (*vn)[i].set(mesh.normal[i][0], mesh.normal[i][1], mesh.normal[i][2]); - } - osg::ref_ptr geometry = new osg::Geometry; - geometry->setVertexArray(va); - geometry->setNormalArray(vn); - osg::DrawElementsUShort* _set = new osg::DrawElementsUShort(osg::DrawArrays::TRIANGLES); - for (int i = 0; i < mesh.index.size(); i++) { - _set->addElement(mesh.index[i][0]); - _set->addElement(mesh.index[i][1]); - _set->addElement(mesh.index[i][2]); - } - geometry->addPrimitiveSet(_set); - //osgUtil::SmoothingVisitor::smooth(*geometry); - return geometry; -} - -void calc_normal(int baseCnt, int ptNum, Polygon_Mesh &mesh) -{ - // normal stand for one triangle - for (int i = 0; i < ptNum; i+=2) { - osg::Vec2 *nor1 = 0; - nor1 = new osg::Vec2(mesh.vertex[baseCnt + 2 * (i + 1)][0], mesh.vertex[baseCnt + 2 * (i + 1)][1]); - *nor1 = *nor1 - osg::Vec2(mesh.vertex[baseCnt + 2 * i][0], mesh.vertex[baseCnt + 2 * i][1]); - osg::Vec3 nor3 = osg::Vec3(-nor1->y(), nor1->x(), 0); - nor3.normalize(); - delete nor1; - mesh.normal.push_back({ nor3.x(), nor3.y(), nor3.z() }); - mesh.normal.push_back({ nor3.x(), nor3.y(), nor3.z() }); - mesh.normal.push_back({ nor3.x(), nor3.y(), nor3.z() }); - mesh.normal.push_back({ nor3.x(), nor3.y(), nor3.z() }); - } -} - -static OGRCoordinateTransformation* g_shp_coord_transform = nullptr; -static bool g_shp_is_wgs84 = true; -static double g_shp_center_lon = 0.0; -static double g_shp_center_lat = 0.0; - -static void transform_point_to_wgs84(double& x, double& y, double& z) { - if (g_shp_is_wgs84 || !g_shp_coord_transform) { - return; - } - g_shp_coord_transform->Transform(1, &x, &y, &z); -} - -static std::array project_to_local_meters(double lon, double lat) { - float point_x = (float)longti_to_meter(degree2rad(lon - g_shp_center_lon), degree2rad(g_shp_center_lat)); - float point_y = (float)lati_to_meter(degree2rad(lat - g_shp_center_lat)); - return {point_x, point_y}; -} - -Polygon_Mesh -convert_polygon(OGRPolygon* polyon, double center_x, double center_y, double height) -{ - Polygon_Mesh mesh; - OGRLinearRing* pRing = polyon->getExteriorRing(); - int ptNum = pRing->getNumPoints(); - if (ptNum < 4) { - return mesh; - } - int pt_count = 0; - for (int i = 0; i < ptNum; i++) { - OGRPoint pt; - pRing->getPoint(i, &pt); - double x = pt.getX(); - double y = pt.getY(); - double bottom = pt.getZ(); - transform_point_to_wgs84(x, y, bottom); - auto [point_x, point_y] = project_to_local_meters(x, y); - mesh.vertex.push_back({ point_x , point_y, (float)bottom }); - mesh.vertex.push_back({ point_x , point_y, (float)height }); - if (i != 0 && i != ptNum - 1) { - mesh.vertex.push_back({ point_x , point_y, (float)bottom }); - mesh.vertex.push_back({ point_x , point_y, (float)height }); - } - } - int vertex_num = mesh.vertex.size() / 2; - for (int i = 0; i < vertex_num; i += 2) { - if (i != vertex_num - 1) { - mesh.index.push_back({ 2 * i,2 * i + 1,2 * (i + 1) + 1 }); - mesh.index.push_back({ 2 * (i + 1),2 * i,2 * (i + 1) + 1 }); - } - } - calc_normal(0, vertex_num, mesh); - pt_count += 2 * vertex_num; - - int inner_count = polyon->getNumInteriorRings(); - for (int j = 0; j < inner_count; j++) { - OGRLinearRing* pRing = polyon->getInteriorRing(j); - int ptNum = pRing->getNumPoints(); - if (ptNum < 4) { - continue; - } - for (int i = 0; i < ptNum; i++) { - OGRPoint pt; - pRing->getPoint(i, &pt); - double x = pt.getX(); - double y = pt.getY(); - double bottom = pt.getZ(); - transform_point_to_wgs84(x, y, bottom); - auto [point_x, point_y] = project_to_local_meters(x, y); - mesh.vertex.push_back({ point_x , point_y, (float)bottom }); - mesh.vertex.push_back({ point_x , point_y, (float)height }); - if (i != 0 && i != ptNum - 1) { - mesh.vertex.push_back({ point_x , point_y, (float)bottom }); - mesh.vertex.push_back({ point_x , point_y, (float)height }); - } - } - vertex_num = mesh.vertex.size() / 2 - pt_count; - for (int i = 0; i < vertex_num; i += 2) { - if (i != vertex_num - 1) { - mesh.index.push_back({ pt_count + 2 * i, pt_count + 2 * i + 1, pt_count + 2 * (i + 1) }); - mesh.index.push_back({ pt_count + 2 * (i + 1), pt_count + 2 * i, pt_count + 2 * (i + 1) }); - } - } - calc_normal(pt_count, ptNum, mesh); - pt_count = mesh.vertex.size(); - } - { - using Point = std::array; - std::vector> polygon(1); - { - OGRLinearRing* pRing = polyon->getExteriorRing(); - int ptNum = pRing->getNumPoints(); - for (int i = 0; i < ptNum; i++) - { - OGRPoint pt; - pRing->getPoint(i, &pt); - double x = pt.getX(); - double y = pt.getY(); - double bottom = pt.getZ(); - transform_point_to_wgs84(x, y, bottom); - auto [point_x, point_y] = project_to_local_meters(x, y); - polygon[0].push_back({ point_x, point_y }); - mesh.vertex.push_back({ point_x , point_y, (float)bottom }); - mesh.vertex.push_back({ point_x , point_y, (float)height }); - mesh.normal.push_back({ 0,0,-1 }); - mesh.normal.push_back({ 0,0,1 }); - } - } - int inner_count = polyon->getNumInteriorRings(); - for (int j = 0; j < inner_count; j++) - { - polygon.resize(polygon.size() + 1); - OGRLinearRing* pRing = polyon->getInteriorRing(j); - int ptNum = pRing->getNumPoints(); - for (int i = 0; i < ptNum; i++) - { - OGRPoint pt; - pRing->getPoint(i, &pt); - double x = pt.getX(); - double y = pt.getY(); - double bottom = pt.getZ(); - transform_point_to_wgs84(x, y, bottom); - auto [point_x, point_y] = project_to_local_meters(x, y); - polygon[j].push_back({ point_x, point_y }); - mesh.vertex.push_back({ point_x , point_y, (float)bottom }); - mesh.vertex.push_back({ point_x , point_y, (float)height }); - mesh.normal.push_back({ 0,0,-1 }); - mesh.normal.push_back({ 0,0,1 }); - } - } - std::vector indices = mapbox::earcut(polygon); - for (int idx = 0; idx < indices.size(); idx += 3) { - mesh.index.push_back({ - pt_count + 2 * indices[idx], - pt_count + 2 * indices[idx + 2], - pt_count + 2 * indices[idx + 1] }); - } - for (int idx = 0; idx < indices.size(); idx += 3) { - mesh.index.push_back({ - pt_count + 2 * indices[idx] + 1, - pt_count + 2 * indices[idx + 1] + 1, - pt_count + 2 * indices[idx + 2] + 1}); - } - } - return mesh; -} - -std::string make_polymesh(std::vector& meshes, - bool enable_simplify = false, - std::optional simplification_params = std::nullopt, - bool enable_draco = false, - std::optional draco_params = std::nullopt); - -std::string make_b3dm(std::vector& meshes, - bool with_height = false, - bool enable_simplify = false, - std::optional simplification_params = std::nullopt, - bool enable_draco = false, - std::optional draco_params = std::nullopt); -// -extern "C" bool -shp23dtile(const ShapeConversionParams* params) -{ - if (!params || !params->input_path || !params->output_path) { - LOG_E("make shp23dtile failed: invalid parameters"); - return false; - } - - const char* filename = params->input_path; - const char* dest = params->output_path; - std::string height_field = ""; - if (params->height_field) { - height_field = params->height_field; - } - - // Build LOD configuration from params - LODPipelineSettings lod_cfg; - if (params->enable_lod) { - // Use default LOD configuration: [1.0, 0.5, 0.25] - std::vector default_ratios = {1.0f, 0.5f, 0.25f}; - float default_base_error = 0.01f; - bool default_draco_for_lod0 = false; // Don't apply Draco to highest detail LOD - - lod_cfg.enable_lod = true; - lod_cfg.levels = build_lod_levels( - default_ratios, - default_base_error, - params->simplify_params, - params->draco_compression_params, - default_draco_for_lod0 - ); - } else { - lod_cfg.enable_lod = false; - } - - // Use configuration from params - const SimplificationParams& simplify_params = params->simplify_params; - const DracoCompressionParams& draco_params = params->draco_compression_params; - - int layer_id = params->layer_id; - GDALAllRegister(); - - // Ensure destination directory exists before creating any auxiliary files (e.g., attributes.db) - std::error_code mkdir_ec; - std::filesystem::create_directories(std::filesystem::path(dest), mkdir_ec); - - GDALDataset* poDS = (GDALDataset*)GDALOpenEx( - filename, GDAL_OF_VECTOR, - NULL, NULL, NULL); - if (poDS == NULL) - { - LOG_E("open shapefile [%s] failed", filename); - return false; - } - OGRLayer *poLayer; - poLayer = poDS->GetLayer(layer_id); - if (!poLayer) { - GDALClose(poDS); - LOG_E("open layer [%s]:[%d] failed", filename, layer_id); - return false; - } - - - { - // Store feature attributes to SQLite database using RAII wrapper - const std::string sqlite_path = (std::filesystem::path(dest) / "attributes.db").string(); - // RAII: AttributeStorage will auto-commit and close on scope exit - AttributeStorage attr_storage(sqlite_path); - - if (!attr_storage.isOpen()) { - LOG_E("Failed to open attribute database: %s", attr_storage.getLastError().c_str()); - } else { - // Create table schema - if (!attr_storage.createTable(poLayer->GetLayerDefn())) { - LOG_E("Failed to create table: %s", attr_storage.getLastError().c_str()); - } else { - // Insert all features in batches (1000 features per transaction) - // This prevents data loss in case of errors during bulk insert - attr_storage.insertFeaturesInBatches(poLayer, 1000); - } - } - // Database automatically closed and committed here (RAII) - } - OGRwkbGeometryType _t = poLayer->GetGeomType(); - if (_t != wkbPolygon && _t != wkbMultiPolygon && - _t != wkbPolygon25D && _t != wkbMultiPolygon25D) - { - GDALClose(poDS); - LOG_E("only support polyon now"); - return false; - } - - const OGRSpatialReference* poSRS = poLayer->GetSpatialRef(); - g_shp_is_wgs84 = true; - g_shp_coord_transform = nullptr; - g_shp_center_lon = 0.0; - g_shp_center_lat = 0.0; - - if (poSRS) { - OGRSpatialReference wgs84SRS; - wgs84SRS.importFromEPSG(4326); - wgs84SRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); - - OGRSpatialReference srcSRS(*poSRS); - srcSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); - - if (!srcSRS.IsSame(&wgs84SRS)) { - g_shp_is_wgs84 = false; - g_shp_coord_transform = OGRCreateCoordinateTransformation(&srcSRS, &wgs84SRS); - if (!g_shp_coord_transform) { - LOG_E("Failed to create coordinate transformation from source SRS to WGS84"); - GDALClose(poDS); - return false; - } - const char* srsName = srcSRS.GetName(); - LOG_I("Shapefile coordinate system: %s (non-WGS84, will transform to WGS84)", srsName ? srsName : "unknown"); - } else { - LOG_I("Shapefile coordinate system: WGS84 (no transformation needed)"); - } - } else { - LOG_W("Shapefile has no coordinate system defined, assuming WGS84"); - } - - OGREnvelope envelop; - OGRErr err = poLayer->GetExtent(&envelop); - if (err != OGRERR_NONE) { - LOG_E("no extent found in shapefile"); - if (g_shp_coord_transform) { - OGRCoordinateTransformation::DestroyCT(g_shp_coord_transform); - g_shp_coord_transform = nullptr; - } - return false; - } - - double min_x = envelop.MinX, max_x = envelop.MaxX; - double min_y = envelop.MinY, max_y = envelop.MaxY; - if (!g_shp_is_wgs84 && g_shp_coord_transform) { - double dummy_z = 0.0; - g_shp_coord_transform->Transform(1, &min_x, &min_y, &dummy_z); - g_shp_coord_transform->Transform(1, &max_x, &max_y, &dummy_z); - } - g_shp_center_lon = (min_x + max_x) / 2.0; - g_shp_center_lat = (min_y + max_y) / 2.0; - - bbox bound(min_x, max_x, min_y, max_y); - node root(bound); - OGRFeature *poFeature; - poLayer->ResetReading(); - while ((poFeature = poLayer->GetNextFeature()) != NULL) - { - OGRGeometry *poGeometry; - poGeometry = poFeature->GetGeometryRef(); - if (poGeometry == NULL) { - OGRFeature::DestroyFeature(poFeature); - continue; - } - OGREnvelope envelop; - poGeometry->getEnvelope(&envelop); - double minx = envelop.MinX, maxx = envelop.MaxX; - double miny = envelop.MinY, maxy = envelop.MaxY; - if (!g_shp_is_wgs84 && g_shp_coord_transform) { - double dummy_z = 0.0; - g_shp_coord_transform->Transform(1, &minx, &miny, &dummy_z); - g_shp_coord_transform->Transform(1, &maxx, &maxy, &dummy_z); - } - bbox bound(minx, maxx, miny, maxy); - unsigned long long id = poFeature->GetFID(); - root.add(id, bound); - OGRFeature::DestroyFeature(poFeature); - } - // iter all node and convert to obj - std::vector items_array; - root.get_all(items_array); - // - int field_index = -1; - std::vector leaf_tiles; - - if (!height_field.empty()) { - field_index = poLayer->GetLayerDefn()->GetFieldIndex(height_field.c_str()); - if (field_index == -1) { - LOG_E("can`t found field [%s] in [%s]", height_field.c_str(), filename); - } - } - OGRFeatureDefn* layer_defn = poLayer->GetLayerDefn(); - - for (auto item : items_array) { - node* _node = (node*)item; - { - OGREnvelope node_box; - for (auto id : _node->get_ids()) { - OGRFeature *poFeature = poLayer->GetFeature(id); - OGRGeometry* poGeometry = poFeature->GetGeometryRef(); - OGREnvelope geo_box; - poGeometry->getEnvelope(&geo_box); - double minx = geo_box.MinX, maxx = geo_box.MaxX; - double miny = geo_box.MinY, maxy = geo_box.MaxY; - if (!g_shp_is_wgs84 && g_shp_coord_transform) { - double dummy_z = 0.0; - g_shp_coord_transform->Transform(1, &minx, &miny, &dummy_z); - g_shp_coord_transform->Transform(1, &maxx, &maxy, &dummy_z); - } - if ( !node_box.IsInit() ) { - node_box.MinX = minx; - node_box.MaxX = maxx; - node_box.MinY = miny; - node_box.MaxY = maxy; - } - else { - node_box.MinX = std::min(node_box.MinX, minx); - node_box.MaxX = std::max(node_box.MaxX, maxx); - node_box.MinY = std::min(node_box.MinY, miny); - node_box.MaxY = std::max(node_box.MaxY, maxy); - } - } - _node->_box.minx = node_box.MinX; - _node->_box.maxx = node_box.MaxX; - _node->_box.miny = node_box.MinY; - _node->_box.maxy = node_box.MaxY; - } - double center_x = ( _node->_box.minx + _node->_box.maxx ) / 2; - double center_y = ( _node->_box.miny + _node->_box.maxy ) / 2; - double max_height = 0; - std::vector v_meshes; - for (auto id : _node->get_ids()) { - OGRFeature *poFeature = poLayer->GetFeature(id); - OGRGeometry *poGeometry; - poGeometry = poFeature->GetGeometryRef(); - double height = 50.0; - if( field_index >= 0 ) { - height = poFeature->GetFieldAsDouble(field_index); - } - if (height > max_height) { - max_height = height; - } - if (wkbFlatten(poGeometry->getGeometryType()) == wkbPolygon) { - OGRPolygon* polyon = (OGRPolygon*)poGeometry; - Polygon_Mesh mesh = convert_polygon(polyon, center_x, center_y, height); - mesh.mesh_name = "mesh_" + std::to_string(id); - mesh.height = height; - if (layer_defn) { - int field_count = layer_defn->GetFieldCount(); - for (int f = 0; f < field_count; ++f) { - OGRFieldDefn* fld = layer_defn->GetFieldDefn(f); - std::string fname = fld->GetNameRef(); - if (!poFeature->IsFieldSetAndNotNull(f)) { - mesh.properties[fname] = nullptr; - continue; - } - switch (fld->GetType()) { - case OFTInteger: - mesh.properties[fname] = poFeature->GetFieldAsInteger(f); - break; - case OFTInteger64: - mesh.properties[fname] = poFeature->GetFieldAsInteger64(f); - break; - case OFTReal: - mesh.properties[fname] = poFeature->GetFieldAsDouble(f); - break; - case OFTString: - mesh.properties[fname] = std::string(poFeature->GetFieldAsString(f)); - break; - default: - mesh.properties[fname] = std::string(poFeature->GetFieldAsString(f)); - break; - } - } - } - v_meshes.push_back(mesh); - } - else if (wkbFlatten(poGeometry->getGeometryType()) == wkbMultiPolygon) { - OGRMultiPolygon* _multi = (OGRMultiPolygon*)poGeometry; - int sub_count = _multi->getNumGeometries(); - for (int j = 0; j < sub_count; j++) { - OGRPolygon * polyon = (OGRPolygon*)_multi->getGeometryRef(j); - Polygon_Mesh mesh = convert_polygon(polyon, center_x, center_y, height); - mesh.mesh_name = "mesh_" + std::to_string(id); - mesh.height = height; - if (layer_defn) { - int field_count = layer_defn->GetFieldCount(); - for (int f = 0; f < field_count; ++f) { - OGRFieldDefn* fld = layer_defn->GetFieldDefn(f); - std::string fname = fld->GetNameRef(); - if (!poFeature->IsFieldSetAndNotNull(f)) { - mesh.properties[fname] = nullptr; - continue; - } - switch (fld->GetType()) { - case OFTInteger: - mesh.properties[fname] = poFeature->GetFieldAsInteger(f); - break; - case OFTInteger64: - mesh.properties[fname] = poFeature->GetFieldAsInteger64(f); - break; - case OFTReal: - mesh.properties[fname] = poFeature->GetFieldAsDouble(f); - break; - case OFTString: - mesh.properties[fname] = std::string(poFeature->GetFieldAsString(f)); - break; - default: - mesh.properties[fname] = std::string(poFeature->GetFieldAsString(f)); - break; - } - } - } - v_meshes.push_back(mesh); - } - } - OGRFeature::DestroyFeature(poFeature); - } - - // Store one or more b3dm under flat tile/z/x/y/ first; relocation happens later - std::filesystem::path leaf_dir = std::filesystem::path("tile") / std::to_string(_node->_z) / std::to_string(_node->_x) / std::to_string(_node->_y); - std::error_code ec; - std::filesystem::create_directories(std::filesystem::path(dest) / leaf_dir, ec); - std::filesystem::path tile_json_rel = leaf_dir / "tileset.json"; - std::filesystem::path tile_json_full = std::filesystem::path(dest) / tile_json_rel; - std::string tile_json_path = tile_json_full.string(); - - double box_width = (_node->_box.maxx - _node->_box.minx); - double box_height = (_node->_box.maxy - _node->_box.miny); - const double pi = std::acos(-1); - double radian_x = degree2rad(center_x); - double radian_y = degree2rad(center_y); - - // Convert angular span to meters and inflate slightly for safety - double tile_w_m = longti_to_meter(degree2rad(box_width) * 1.05, radian_y); - double tile_h_m = lati_to_meter(degree2rad(box_height) * 1.05); - double tile_z_m = std::max(max_height, 5.0); // height range already in meters (extrusion height) - - // Geometric error per commit fc40399...: max span divided by 20 - double ge = compute_geometric_error_from_spans(tile_w_m, tile_h_m, tile_z_m); - - // Use LOD configuration from params (already extracted at function start) - const bool lod_enabled = lod_cfg.enable_lod && !lod_cfg.levels.empty(); - - double half_w = tile_w_m * 0.5; - double half_h = tile_h_m * 0.5; - double half_z = tile_z_m * 0.5; - - auto build_lod_tree_for_meshes = [&](std::vector& meshes, - const std::string& name_prefix) -> std::pair { - if (meshes.empty()) { - return {nlohmann::json(), -1.0}; - } - - std::vector lod_names; - std::vector lod_errors; - - auto make_filename = [&](size_t idx) { - std::string prefix = name_prefix.empty() ? "" : name_prefix + "_"; - return std::string("content_") + prefix + "lod" + std::to_string(idx) + ".b3dm"; - }; - - auto push_lod_output = [&](size_t idx, - bool lvl_enable_simplify, - std::optional lvl_simplify, - bool lvl_enable_draco, - std::optional lvl_draco, - double lvl_ratio) { - std::string filename = make_filename(idx); - std::filesystem::path b3dm_rel = leaf_dir / filename; - std::filesystem::path b3dm_full = std::filesystem::path(dest) / b3dm_rel; - std::string b3dm_buf = make_b3dm(meshes, true, lvl_enable_simplify, lvl_simplify, lvl_enable_draco, lvl_draco); - write_file(b3dm_full.string().c_str(), b3dm_buf.data(), b3dm_buf.size()); - - lod_names.push_back(filename); - double span_z = std::max(tile_z_m, 5.0); // avoid near-zero vertical span - double base_ge = compute_geometric_error_from_spans(tile_w_m, tile_h_m, span_z); - double ratio = std::clamp(static_cast(lvl_ratio), 0.01, 1.0); - // coarser LOD (smaller ratio) gets larger geometric error - double ge_level = base_ge * std::max(1.0, 1.0 / std::sqrt(ratio)); - lod_errors.push_back(ge_level); - }; - - if (lod_enabled) { - for (size_t i = 0; i < lod_cfg.levels.size(); ++i) { - const auto& lvl = lod_cfg.levels[i]; - std::optional level_simplify = std::nullopt; - if (lvl.enable_simplification) { - level_simplify = lvl.simplify; - level_simplify->target_ratio = lvl.target_ratio; - level_simplify->target_error = lvl.target_error; - } - std::optional level_draco = std::nullopt; - if (lvl.enable_draco) { - level_draco = lvl.draco; - level_draco->enable_compression = true; - } - push_lod_output(i, lvl.enable_simplification, level_simplify, lvl.enable_draco, level_draco, lvl.target_ratio); - } - } else { - // Use simplification params from function params - std::optional simplification_params_opt = std::nullopt; - if (simplify_params.enable_simplification) { - simplification_params_opt = simplify_params; - } - push_lod_output(0, simplify_params.enable_simplification, simplification_params_opt, - draco_params.enable_compression, - draco_params.enable_compression ? std::make_optional(draco_params) : std::nullopt, - 1.0); - } - - double span_z = std::max(tile_z_m, 0.001); - double bucket_half_z = span_z * 0.5; - double bucket_center_z = bucket_half_z; - - auto [center_offset_x, center_offset_y] = project_to_local_meters(center_x, center_y); - - auto make_lod_node = [&](size_t idx) { - nlohmann::json node_json; - node_json["refine"] = "REPLACE"; - node_json["geometricError"] = lod_errors[idx]; - node_json["boundingVolume"]["box"] = box_to_json(center_offset_x, center_offset_y, bucket_center_z, half_w, half_h, bucket_half_z); - node_json["content"]["uri"] = std::string("./") + lod_names[idx]; - return node_json; - }; - - std::vector order(lod_names.size()); - std::iota(order.begin(), order.end(), 0); - if (lod_enabled) { - std::sort(order.begin(), order.end(), [&](size_t a, size_t b) { - return lod_cfg.levels[a].target_ratio < lod_cfg.levels[b].target_ratio; - }); - } - - nlohmann::json lod_tree = make_lod_node(order.back()); - for (int idx = static_cast(order.size()) - 2; idx >= 0; --idx) { - size_t level_idx = order[idx]; - nlohmann::json parent = make_lod_node(level_idx); - parent["children"].push_back(lod_tree); - lod_tree = parent; - } - - double root_ge = lod_tree.value("geometricError", 0.0); - if (!lod_errors.empty()) { - // coarsest (smallest ratio) should sit at root with largest geometric error - root_ge = lod_errors[order.front()]; - } - return {lod_tree, root_ge}; - }; - - double leaf_root_ge = ge; - nlohmann::json leaf_root_node; - - auto res = build_lod_tree_for_meshes(v_meshes, ""); - leaf_root_node = res.first; - leaf_root_ge = res.second > 0 ? res.second : ge; - - nlohmann::json leaf; - leaf["asset"] = { {"version", "1.0"}, {"gltfUpAxis", "Z"} }; - leaf["geometricError"] = leaf_root_ge; - leaf["root"] = leaf_root_node; - - std::ofstream ofs(tile_json_path); - if (!ofs.is_open()) { - LOG_E("write leaf tileset %s fail", tile_json_path.c_str()); - } else { - ofs << leaf.dump(2); - } - - TileMeta meta; - meta.z = _node->_z; - meta.x = _node->_x; - meta.y = _node->_y; - meta.bbox = make_bbox_from_node(_node->_box, 0.0, max_height); - meta.geometric_error = leaf_root_ge; - meta.orig_tileset_rel = tile_json_rel.generic_string(); - meta.is_leaf = true; - leaf_tiles.push_back(meta); - } - // - GDALClose(poDS); - if (g_shp_coord_transform) { - OGRCoordinateTransformation::DestroyCT(g_shp_coord_transform); - g_shp_coord_transform = nullptr; - } - build_hierarchical_tilesets(leaf_tiles, dest, g_shp_center_lon, g_shp_center_lat); - return true; -} - -template -void put_val(std::vector& buf, T val) { - buf.insert(buf.end(), (unsigned char*)&val, (unsigned char*)&val + sizeof(T)); -} - -template -void put_val(std::string& buf, T val) { - buf.append((unsigned char*)&val, (unsigned char*)&val + sizeof(T)); -} - -template -void alignment_buffer(std::vector& buf) { - while (buf.size() % 4 != 0) { - buf.push_back(0x00); - } -} - -template -void alignment_buffer_4(std::vector& buf) { - while (buf.size() % 4 != 0) { - buf.push_back(0x00); - } -} - -#define SET_MIN(x,v) do{ if (x > v) x = v; }while (0); -#define SET_MAX(x,v) do{ if (x < v) x = v; }while (0); - -tinygltf::Material make_color_material(double r, double g, double b) { - tinygltf::Material material; - char buf[512]; - sprintf(buf,"default_%.1f_%.1f_%.1f",r,g,b); - material.name = buf; - material.pbrMetallicRoughness.baseColorFactor = { r,g,b,1 }; - material.pbrMetallicRoughness.roughnessFactor = 0.7; - material.pbrMetallicRoughness.metallicFactor = 0.3; - return material; -} - -tinygltf::BufferView create_buffer_view(int target, int byteOffset, int byteLength) { - tinygltf::BufferView bfv; - bfv.buffer = 0; - bfv.target = target; - bfv.byteOffset = byteOffset; - bfv.byteLength = byteLength; - return bfv; -} - - -// convert poly-mesh to glb buffer -std::string make_polymesh(std::vector &meshes, - bool enable_simplify, - std::optional simplification_params, - bool enable_draco, - std::optional draco_params) { - vector> osg_Geoms; - osg_Geoms.reserve(meshes.size()); - for (auto& mesh : meshes) { - osg_Geoms.push_back(make_triangle_mesh(mesh)); - } - - if (osg_Geoms.empty()) { - return {}; - } - - tinygltf::TinyGLTF gltf; - tinygltf::Model model; - tinygltf::Buffer buffer; - bool use_multi_material = false; - tinygltf::Scene sence; - - const bool draco_requested = enable_draco && draco_params.has_value() && draco_params->enable_compression; - - // Simplify each geometry before merging so batch id mapping stays consistent - if (enable_simplify && simplification_params.has_value()) { - for (auto& geom : osg_Geoms) { - if (geom.valid() && geom->getNumPrimitiveSets() > 0) { - simplify_mesh_geometry(geom.get(), simplification_params.value()); - } - } - } - - // Merge all buildings into one geometry while tracking per-building batch ids - osg::ref_ptr merged_geom = new osg::Geometry; - osg::ref_ptr merged_vertices = new osg::Vec3Array(); - osg::ref_ptr merged_normals = new osg::Vec3Array(); - osg::ref_ptr merged_indices = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES); - std::vector merged_batch_ids; - - for (size_t i = 0; i < osg_Geoms.size(); ++i) { - if (!osg_Geoms[i].valid()) continue; - osg::Vec3Array* vArr = dynamic_cast(osg_Geoms[i]->getVertexArray()); - if (!vArr || vArr->empty()) continue; - osg::Vec3Array* nArr = dynamic_cast(osg_Geoms[i]->getNormalArray()); - - const size_t base = merged_vertices->size(); - merged_vertices->insert(merged_vertices->end(), vArr->begin(), vArr->end()); - - if (nArr && nArr->size() == vArr->size()) { - merged_normals->insert(merged_normals->end(), nArr->begin(), nArr->end()); - } else { - // Fallback normals keep alignment if input is missing - merged_normals->insert(merged_normals->end(), vArr->size(), osg::Vec3(0.0f, 0.0f, 1.0f)); - } - - merged_batch_ids.insert(merged_batch_ids.end(), vArr->size(), static_cast(i)); - - if (osg_Geoms[i]->getNumPrimitiveSets() > 0) { - osg::PrimitiveSet* ps = osg_Geoms[i]->getPrimitiveSet(0); - const auto idx_cnt = ps->getNumIndices(); - for (unsigned int k = 0; k < idx_cnt; ++k) { - merged_indices->push_back(static_cast(base + ps->index(k))); - } - } - } - - if (merged_vertices->empty() || merged_indices->empty()) { - return {}; - } - - merged_geom->setVertexArray(merged_vertices.get()); - merged_geom->setNormalArray(merged_normals.get()); - merged_geom->addPrimitiveSet(merged_indices.get()); - - // Optionally Draco-compress the merged geometry; fallback data is still present - std::vector draco_data; - size_t draco_size = 0; - int draco_pos_att = -1; - int draco_norm_att = -1; - int draco_tex_att = -1; - int draco_batchid_att = -1; - bool wrote_draco_ext = false; - if (draco_requested) { - DracoCompressionParams params = draco_params.value(); - params.enable_compression = true; - - std::vector batch_ids_f; - batch_ids_f.reserve(merged_batch_ids.size()); - for(auto id : merged_batch_ids) batch_ids_f.push_back(static_cast(id)); - - bool compress_mesh_sucess = compress_mesh_geometry( - merged_geom.get(), params, draco_data, draco_size, &draco_pos_att, - &draco_norm_att, &draco_tex_att, &draco_batchid_att, &batch_ids_f); - if (!compress_mesh_sucess) { - LOG_E("compress mesh failure, please check your mesh"); - return std::string(); - } - } - - // Build GLB buffers from the merged geometry - int index_accessor_index = -1; - int vertex_accessor_index = -1; - int normal_accessor_index = -1; - int batchid_accessor_index = -1; - - { - osg::PrimitiveSet* ps = merged_geom->getPrimitiveSet(0); - int idx_size = ps->getNumIndices(); - uint32_t max_idx = 0; - - for (int m = 0; m < idx_size; m++) { - uint32_t idx = static_cast(ps->index(m)); - SET_MAX(max_idx, idx); - } - - index_accessor_index = model.accessors.size(); - - tinygltf::Accessor acc; - acc.byteOffset = 0; - acc.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT; - acc.count = idx_size; - acc.type = TINYGLTF_TYPE_SCALAR; - acc.maxValues = {(double)max_idx}; - acc.minValues = {0.0}; - - if (!draco_requested) { - int byteOffset = buffer.data.size(); - for (int m = 0; m < idx_size; m++) { - uint32_t idx = static_cast(ps->index(m)); - put_val(buffer.data, idx); - } - acc.bufferView = model.bufferViews.size(); - alignment_buffer(buffer.data); - tinygltf::BufferView bfv = create_buffer_view(TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER, byteOffset, - buffer.data.size() - byteOffset); - model.bufferViews.push_back(bfv); - } else { - acc.bufferView = -1; - } - model.accessors.push_back(acc); - } - { - osg::Vec3Array* v3f = merged_vertices.get(); - int vec_size = v3f->size(); - std::vector box_max = {-1e38, -1e38, -1e38}; - std::vector box_min = {1e38, 1e38, 1e38}; - - for (int vidx = 0; vidx < vec_size; vidx++) { - osg::Vec3f point = v3f->at(vidx); - vector vertex = {point.x(), point.y(), point.z()}; - for (int i = 0; i < 3; i++) { - SET_MAX(box_max[i], vertex[i]); - SET_MIN(box_min[i], vertex[i]); - } - } - - vertex_accessor_index = model.accessors.size(); - tinygltf::Accessor acc; - acc.byteOffset = 0; - acc.count = vec_size; - acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; - acc.type = TINYGLTF_TYPE_VEC3; - acc.maxValues = box_max; - acc.minValues = box_min; - - if (!draco_requested) { - int byteOffset = buffer.data.size(); - for (int vidx = 0; vidx < vec_size; vidx++) { - osg::Vec3f point = v3f->at(vidx); - vector vertex = {point.x(), point.y(), point.z()}; - for (int i = 0; i < 3; i++) { - put_val(buffer.data, vertex[i]); - } - } - acc.bufferView = model.bufferViews.size(); - alignment_buffer(buffer.data); - tinygltf::BufferView bfv = create_buffer_view(TINYGLTF_TARGET_ARRAY_BUFFER, byteOffset, - buffer.data.size() - byteOffset); - model.bufferViews.push_back(bfv); - } else { - acc.bufferView = -1; - } - model.accessors.push_back(acc); - } - { - osg::Vec3Array* v3f = merged_normals.get(); - std::vector box_max = {-1e38, -1e38, -1e38}; - std::vector box_min = {1e38, 1e38, 1e38}; - int normal_size = v3f->size(); - - for (int vidx = 0; vidx < normal_size; vidx++) { - osg::Vec3f point = v3f->at(vidx); - vector normal = {point.x(), point.y(), point.z()}; - for (int i = 0; i < 3; i++) { - SET_MAX(box_max[i], normal[i]); - SET_MIN(box_min[i], normal[i]); - } - } - - normal_accessor_index = model.accessors.size(); - tinygltf::Accessor acc; - acc.byteOffset = 0; - acc.count = normal_size; - acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; - acc.type = TINYGLTF_TYPE_VEC3; - acc.minValues = box_min; - acc.maxValues = box_max; - - if (!draco_requested) { - int byteOffset = buffer.data.size(); - for (int vidx = 0; vidx < normal_size; vidx++) { - osg::Vec3f point = v3f->at(vidx); - vector normal = {point.x(), point.y(), point.z()}; - for (int i = 0; i < 3; i++) { - put_val(buffer.data, normal[i]); - } - } - acc.bufferView = model.bufferViews.size(); - alignment_buffer(buffer.data); - tinygltf::BufferView bfv = create_buffer_view(TINYGLTF_TARGET_ARRAY_BUFFER, byteOffset, - buffer.data.size() - byteOffset); - model.bufferViews.push_back(bfv); - } else { - acc.bufferView = -1; - } - model.accessors.push_back(acc); - } - { - uint32_t max_batch = 0; - for (auto batch_id : merged_batch_ids) { - SET_MAX(max_batch, batch_id); - } - - batchid_accessor_index = model.accessors.size(); - tinygltf::Accessor acc; - acc.byteOffset = 0; - - // Per glTF spec: Vertex attribute data must be aligned to 4-byte boundaries - // gltf-validator requires element size to be 4-byte aligned for vertex attributes - // Use FLOAT (4 bytes) for _BATCHID to ensure 4-byte alignment - // UNSIGNED_BYTE (1 byte) and UNSIGNED_SHORT (2 bytes) are not 4-byte aligned - // UNSIGNED_INT (4 bytes) is not allowed for mesh attributes - acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; - - acc.count = merged_batch_ids.size(); - acc.type = TINYGLTF_TYPE_SCALAR; - acc.maxValues = {(double)max_batch}; - acc.minValues = {0.0}; - - if (!draco_requested) { - // Per glTF spec: Vertex attribute data must be aligned to 4-byte boundaries - // Ensure buffer is 4-byte aligned before writing _BATCHID data - alignment_buffer_4(buffer.data); - int byteOffset = buffer.data.size(); - // Write as FLOAT (4 bytes) for 4-byte alignment - for (auto batch_id : merged_batch_ids) { - float val = static_cast(batch_id); - put_val(buffer.data, val); - } - // Per glTF spec: Vertex attribute data must be aligned to 4-byte boundaries - // Ensure the _BATCHID data itself is 4-byte aligned - alignment_buffer_4(buffer.data); - acc.bufferView = model.bufferViews.size(); - alignment_buffer(buffer.data); - tinygltf::BufferView bfv = create_buffer_view(TINYGLTF_TARGET_ARRAY_BUFFER, byteOffset, - buffer.data.size() - byteOffset); - model.bufferViews.push_back(bfv); - } else { - acc.bufferView = -1; - } - model.accessors.push_back(acc); - } - - tinygltf::Mesh mesh; - mesh.name = meshes.size() == 1 ? meshes.front().mesh_name : "merged_mesh"; - tinygltf::Primitive primits; - primits.attributes = { - std::pair("POSITION", vertex_accessor_index), - std::pair("NORMAL", normal_accessor_index), - std::pair("_BATCHID", batchid_accessor_index), - }; - primits.indices = index_accessor_index; - primits.material = 0; - primits.mode = TINYGLTF_MODE_TRIANGLES; - mesh.primitives = {primits}; - model.meshes.push_back(mesh); - - tinygltf::Node node; - node.mesh = model.meshes.size() - 1; - model.nodes.push_back(node); - sence.nodes.push_back(model.nodes.size() - 1); - - // Append Draco payload at the end of buffer and wire extension for merged mesh - if (!draco_data.empty()) { - int draco_view_index = model.bufferViews.size(); - int byteOffset = buffer.data.size(); - buffer.data.insert(buffer.data.end(), draco_data.begin(), draco_data.end()); - - tinygltf::BufferView draco_view; - draco_view.buffer = 0; - draco_view.byteOffset = byteOffset; - draco_view.byteLength = draco_data.size(); - model.bufferViews.push_back(draco_view); - - tinygltf::Value::Object attrs; - attrs["POSITION"] = tinygltf::Value(draco_pos_att); - if (draco_norm_att >= 0) { - attrs["NORMAL"] = tinygltf::Value(draco_norm_att); - } - if (draco_tex_att >= 0) { - attrs["TEXCOORD_0"] = tinygltf::Value(draco_tex_att); - } - if (draco_batchid_att >= 0) { - attrs["_BATCHID"] = tinygltf::Value(draco_batchid_att); - } - - tinygltf::Value::Object draco_ext; - draco_ext["bufferView"] = tinygltf::Value(draco_view_index); - draco_ext["attributes"] = tinygltf::Value(attrs); - model.meshes.back().primitives.back().extensions["KHR_draco_mesh_compression"] = tinygltf::Value(draco_ext); - wrote_draco_ext = true; - } - model.scenes = { sence }; - model.defaultScene = 0; - /// -------------- - if (use_multi_material) { - // code has realized about - } else { - model.materials = { make_color_material(1.0, 1.0, 1.0) }; - } - - // Ensure buffer data is 8-byte aligned so that generated GLB is 8-byte aligned - // This is required for B3DM total length to be 8-byte aligned - int buffer_padding = (8 - (buffer.data.size() % 8)) % 8; - for (int i = 0; i < buffer_padding; ++i) { - buffer.data.push_back(0x00); - } - - model.buffers.push_back(std::move(buffer)); - model.asset.version = "2.0"; - model.asset.generator = "fanfan"; - - if (wrote_draco_ext) { - auto ensure_ext = [](std::vector& list, const std::string& ext) { - if (std::find(list.begin(), list.end(), ext) == list.end()) { - list.push_back(ext); - } - }; - ensure_ext(model.extensionsRequired, "KHR_draco_mesh_compression"); - ensure_ext(model.extensionsUsed, "KHR_draco_mesh_compression"); - } - - std::ostringstream ss; - bool res = gltf.WriteGltfSceneToStream(&model, ss, false, true); - std::string buf = ss.str(); - - // Ensure GLB is 8-byte aligned for B3DM total length alignment - // GLB structure: header(12) + JSON chunk(8 + len) + BIN chunk(8 + len) - int glb_padding = (8 - (buf.size() % 8)) % 8; - if (glb_padding > 0) { - // Extend BIN chunk by adding padding to the end - // BIN chunk length is at offset: 12 + 8 + json_chunk_length + 4 - // But we need to find the BIN chunk header first - - // Read JSON chunk length from GLB - int json_chunk_len = *reinterpret_cast(&buf[12]); - int bin_chunk_header_offset = 12 + 8 + json_chunk_len; - // Ensure bin_chunk_header_offset is 4-byte aligned (GLB spec) - if (bin_chunk_header_offset % 4 != 0) { - bin_chunk_header_offset += 4 - (bin_chunk_header_offset % 4); - } - - // Read current BIN chunk length - int bin_chunk_len = *reinterpret_cast(&buf[bin_chunk_header_offset]); - - // Update BIN chunk length - int new_bin_chunk_len = bin_chunk_len + glb_padding; - *reinterpret_cast(&buf[bin_chunk_header_offset]) = new_bin_chunk_len; - - // Add padding bytes to the end of GLB - buf.append(glb_padding, '\0'); - - // Update GLB header length - int new_glb_len = buf.size(); - *reinterpret_cast(&buf[8]) = new_glb_len; - } - - return buf; -} - -std::string make_b3dm(std::vector& meshes, bool with_height, bool enable_simplify, std::optional simplification_params, bool enable_draco, std::optional draco_params) { - using nlohmann::json; - - std::string feature_json_string; - feature_json_string += "{\"BATCH_LENGTH\":"; - feature_json_string += std::to_string(meshes.size()); - feature_json_string += "}"; - // Per 3D Tiles spec: Feature Table Binary must start at 8-byte aligned offset - // Feature Table Binary starts at: header_len(28) + feature_json_len - // So feature_json_len must be such that (28 + feature_json_len) % 8 == 0 - // Since 28 % 8 = 4, we need feature_json_len % 8 == 4 - while ((28 + feature_json_string.size()) % 8 != 0) { - feature_json_string.push_back(' '); - } - - json batch_json; - std::vector ids; - for (int i = 0; i < meshes.size(); ++i) { - ids.push_back(i); - } - std::vector names; - for (int i = 0; i < meshes.size(); ++i) { - names.push_back(meshes[i].mesh_name); - } - batch_json["batchId"] = ids; - batch_json["name"] = names; - - // Collect all attribute keys across meshes - std::set attribute_keys; - for (const auto& m : meshes) { - for (const auto& kv : m.properties) { - attribute_keys.insert(kv.first); - } - } - - // Build per-attribute arrays aligned with batch ids - std::map> attribute_columns; - for (const auto& key : attribute_keys) { - attribute_columns[key] = std::vector(meshes.size(), nullptr); - } - for (int i = 0; i < meshes.size(); ++i) { - for (const auto& kv : meshes[i].properties) { - auto it = attribute_columns.find(kv.first); - if (it != attribute_columns.end()) { - it->second[i] = kv.second; - } - } - } - for (const auto& kv : attribute_columns) { - batch_json[kv.first] = kv.second; - } - - if (with_height) { - std::vector heights; - for (int i = 0; i < meshes.size(); ++i) { - heights.push_back(meshes[i].height); - } - batch_json["height"] = heights; - } - - std::string batch_json_string = batch_json.dump(); - - std::string glb_buf = make_polymesh(meshes, enable_simplify, simplification_params, enable_draco, draco_params); - if (glb_buf.size() == 0) { - LOG_E("make glb buffer failure"); - return std::string(); - } - - int header_len = 28; - - // Per 3D Tiles spec 1.0: - // - Feature Table Binary starts at (28 + feature_json_len), must be 8-byte aligned - // - Batch Table JSON starts at (28 + feature_json_len + feature_bin_len), must be 8-byte aligned - // - Batch Table Binary starts at (28 + feature_json_len + feature_bin_len + batch_json_len), must be 8-byte aligned - // - GLB data starts at (28 + feature_json_len + feature_bin_len + batch_json_len + batch_bin_len), must be 8-byte aligned - // - Total byte length must be 8-byte aligned - - // Calculate padding for Feature Table JSON - // Feature Table Binary starts at (28 + feature_json_len), must be 8-byte aligned - // Since 28 % 8 = 4, we need feature_json_len % 8 == 4 - int feature_json_padding = (4 - (feature_json_string.size() % 8)) % 8; - feature_json_string.append(feature_json_padding, ' '); - - // Calculate padding for Batch Table JSON - // Batch Table Binary starts at (28 + feature_json_len + batch_json_len), must be 8-byte aligned - // Since feature_bin_len = 0 and (28 + feature_json_len) % 8 == 0, we need batch_json_len % 8 == 0 - // Note: We need to ensure batch_json_len itself is a multiple of 8 - int batch_json_padding = (8 - (batch_json_string.size() % 8)) % 8; - if (batch_json_padding > 0) { - batch_json_string.append(batch_json_padding, ' '); - } - - int feature_json_len = feature_json_string.size(); - int feature_bin_len = 0; - int batch_json_len = batch_json_string.size(); - int batch_bin_len = 0; - - // Verify alignments - int feature_table_binary_start = 28 + feature_json_len; - int batch_table_json_start = feature_table_binary_start + feature_bin_len; - int batch_table_binary_start = batch_table_json_start + batch_json_len; - int glb_start = batch_table_binary_start + batch_bin_len; - - // All must be 8-byte aligned - // feature_table_binary_start % 8 == 0 (ensured by feature_json_padding) - // batch_table_json_start % 8 == 0 (since feature_bin_len = 0) - // batch_table_binary_start % 8 == 0 (ensured by batch_json_padding) - // glb_start % 8 == 0 (since batch_bin_len = 0) - - // Total length must also be 8-byte aligned - // At this point: - // - (28 + feature_json_len) % 8 == 0 - // - batch_json_len % 8 == 0 - // - glb_buf.size() % 8 == 0 (ensured by buffer padding in GLB generation) - // So total_len % 8 == 0 - int total_len = 28 + feature_json_len + batch_json_len + glb_buf.size(); - - std::string b3dm_buf; - b3dm_buf += "b3dm"; - int version = 1; - put_val(b3dm_buf, version); - put_val(b3dm_buf, total_len); - put_val(b3dm_buf, feature_json_len); - put_val(b3dm_buf, feature_bin_len); - put_val(b3dm_buf, batch_json_len); - put_val(b3dm_buf, batch_bin_len); - b3dm_buf.append(feature_json_string.begin(),feature_json_string.end()); - b3dm_buf.append(batch_json_string.begin(),batch_json_string.end()); - - // Append GLB data - b3dm_buf.append(glb_buf); - - return b3dm_buf; -} diff --git a/src/spatial/core/slicing_strategy.h b/src/spatial/core/slicing_strategy.h new file mode 100644 index 00000000..8566ccd7 --- /dev/null +++ b/src/spatial/core/slicing_strategy.h @@ -0,0 +1,178 @@ +#pragma once + +#include "spatial_bounds.h" +#include "spatial_item.h" +#include +#include +#include +#include + +namespace spatial::core { + +/** + * @brief 切片策略配置基类 + */ +struct SlicingConfig { + virtual ~SlicingConfig() = default; + + // 最大深度 + size_t maxDepth = 10; + + // 每个节点最大对象数 + size_t maxItemsPerNode = 1000; + + // 最小包围盒尺寸 (低于此尺寸不再分割) + double minBoundsSize = 0.01; +}; + +/** + * @brief 空间索引节点接口 + */ +class SpatialIndexNode { +public: + virtual ~SpatialIndexNode() = default; + + /** + * @brief 获取节点包围盒 + */ + virtual SpatialBounds getBounds() const = 0; + + /** + * @brief 获取节点深度 + */ + virtual size_t getDepth() const = 0; + + /** + * @brief 获取节点中的对象 + */ + virtual SpatialItemRefList getItems() const = 0; + + /** + * @brief 检查是否为叶子节点 + */ + virtual bool isLeaf() const = 0; + + /** + * @brief 获取子节点 + */ + virtual std::vector getChildren() const = 0; + + /** + * @brief 获取对象数量 + */ + virtual size_t getItemCount() const = 0; +}; + +/** + * @brief 空间索引接口 + */ +class SpatialIndex { +public: + virtual ~SpatialIndex() = default; + + /** + * @brief 获取根节点 + */ + virtual const SpatialIndexNode* getRootNode() const = 0; + + /** + * @brief 获取所有对象 + */ + virtual SpatialItemRefList getAllItems() const = 0; + + /** + * @brief 在指定范围内查询对象 + */ + virtual SpatialItemRefList query(const SpatialBounds& bounds) const = 0; + + /** + * @brief 获取节点数量 + */ + virtual size_t getNodeCount() const = 0; + + /** + * @brief 获取对象数量 + */ + virtual size_t getItemCount() const = 0; +}; + +/** + * @brief 切片策略接口 + * + * 定义了空间切片的统一接口 + * 四叉树和八叉树都实现此接口 + */ +class SlicingStrategy { +public: + virtual ~SlicingStrategy() = default; + + /** + * @brief 获取策略维度 (2或3) + */ + virtual size_t getDimension() const = 0; + + /** + * @brief 获取子节点数量 (4或8) + */ + virtual size_t getChildCount() const = 0; + + /** + * @brief 构建空间索引 + * @param items 要索引的空间对象 + * @param worldBounds 世界包围盒 + * @param config 切片配置 + * @return 构建好的空间索引 + */ + virtual std::unique_ptr buildIndex( + const SpatialItemList& items, + const SpatialBounds& worldBounds, + const SlicingConfig& config + ) = 0; + + /** + * @brief 获取策略名称 + */ + virtual const char* getName() const = 0; +}; + +/** + * @brief 切片策略工厂 + */ +class SlicingStrategyFactory { +public: + using StrategyCreator = std::function()>; + + /** + * @brief 注册策略 + */ + static void registerStrategy(const std::string& name, StrategyCreator creator); + + /** + * @brief 创建策略 + */ + static std::unique_ptr create(const std::string& name); + + /** + * @brief 获取所有可用策略名称 + */ + static std::vector getAvailableStrategies(); +}; + +/** + * @brief 策略注册宏 + */ +#define REGISTER_SLICING_STRATEGY(StrategyClass) \ + namespace { \ + struct StrategyClass##Registrar { \ + StrategyClass##Registrar() { \ + SlicingStrategyFactory::registerStrategy( \ + StrategyClass().getName(), \ + []() -> std::unique_ptr { \ + return std::make_unique(); \ + } \ + ); \ + } \ + } StrategyClass##instance; \ + } + +} // namespace spatial::core diff --git a/src/spatial/core/spatial_bounds.h b/src/spatial/core/spatial_bounds.h new file mode 100644 index 00000000..b9e97c36 --- /dev/null +++ b/src/spatial/core/spatial_bounds.h @@ -0,0 +1,281 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace spatial::core { + +/** + * @brief 通用空间包围盒 + * + * 支持2D和3D空间,使用模板参数区分 + * + * @tparam T 标量类型 (float, double, int等) + * @tparam Dim 维度 (2或3) + */ +template +struct SpatialBounds { + static_assert(Dim == 2 || Dim == 3, "SpatialBounds only supports 2D or 3D"); + static_assert(std::is_arithmetic_v, "SpatialBounds requires arithmetic type"); + + using ValueType = T; + using VectorType = std::array; + static constexpr size_t Dimension = Dim; + + // 默认构造函数创建空/无效的包围盒 + SpatialBounds() = default; + + // 从最小/最大角点构造 + SpatialBounds(const VectorType& min_corner, const VectorType& max_corner) + : min_(min_corner), max_(max_corner) {} + + // 2D构造器 + SpatialBounds(T minX, T minY, T maxX, T maxY) + requires (Dim == 2) + : min_{minX, minY}, max_{maxX, maxY} {} + + // 3D构造器 + SpatialBounds(T minX, T minY, T minZ, T maxX, T maxY, T maxZ) + requires (Dim == 3) + : min_{minX, minY, minZ}, max_{maxX, maxY, maxZ} {} + + // 工厂方法:从中心点和半尺寸创建 + static SpatialBounds fromCenterAndHalfExtents(const VectorType& center, + const VectorType& half_extents) { + VectorType min_corner, max_corner; + for (size_t i = 0; i < Dim; ++i) { + min_corner[i] = center[i] - half_extents[i]; + max_corner[i] = center[i] + half_extents[i]; + } + return SpatialBounds(min_corner, max_corner); + } + + // 工厂方法:创建空包围盒 + static SpatialBounds empty() { + return SpatialBounds(); + } + + // 工厂方法:从单点创建 + static SpatialBounds fromPoint(const VectorType& point) { + return SpatialBounds(point, point); + } + + // 访问器 + const VectorType& min() const { return min_; } + const VectorType& max() const { return max_; } + VectorType& min() { return min_; } + VectorType& max() { return max_; } + + // 获取中心点 + VectorType center() const { + VectorType c; + for (size_t i = 0; i < Dim; ++i) { + c[i] = (min_[i] + max_[i]) * T(0.5); + } + return c; + } + + // 获取尺寸(每个维度的完整大小) + VectorType extents() const { + VectorType e; + for (size_t i = 0; i < Dim; ++i) { + e[i] = max_[i] - min_[i]; + } + return e; + } + + // 获取半尺寸 + VectorType halfExtents() const { + VectorType he; + for (size_t i = 0; i < Dim; ++i) { + he[i] = (max_[i] - min_[i]) * T(0.5); + } + return he; + } + + // 检查包围盒是否有效(min <= max 在所有维度) + bool isValid() const { + for (size_t i = 0; i < Dim; ++i) { + if (min_[i] > max_[i]) return false; + } + return true; + } + + // 检查包围盒是否为空(无体积/面积) + bool isEmpty() const { + for (size_t i = 0; i < Dim; ++i) { + if (min_[i] >= max_[i]) return true; + } + return false; + } + + // 计算体积(3D)或面积(2D) + T volume() const { + if (!isValid()) return T(0); + T vol = T(1); + for (size_t i = 0; i < Dim; ++i) { + vol *= (max_[i] - min_[i]); + } + return vol; + } + + // 计算对角线长度 + T diagonal() const { + T sum = T(0); + for (size_t i = 0; i < Dim; ++i) { + T diff = max_[i] - min_[i]; + sum += diff * diff; + } + return std::sqrt(sum); + } + + // 扩展包围盒以包含一个点 + void expand(const VectorType& point) { + for (size_t i = 0; i < Dim; ++i) { + min_[i] = std::min(min_[i], point[i]); + max_[i] = std::max(max_[i], point[i]); + } + } + + // 扩展包围盒以包含另一个包围盒 + void expand(const SpatialBounds& other) { + for (size_t i = 0; i < Dim; ++i) { + min_[i] = std::min(min_[i], other.min_[i]); + max_[i] = std::max(max_[i], other.max_[i]); + } + } + + // 按比率在所有方向膨胀 + SpatialBounds inflated(T ratio) const { + VectorType c = center(); + VectorType half_extents = halfExtents(); + for (size_t i = 0; i < Dim; ++i) { + half_extents[i] *= (T(1) + ratio); + } + return fromCenterAndHalfExtents(c, half_extents); + } + + // 按绝对值在每个方向扩展 + SpatialBounds padded(T amount) const { + VectorType new_min = min_; + VectorType new_max = max_; + for (size_t i = 0; i < Dim; ++i) { + new_min[i] -= amount; + new_max[i] += amount; + } + return SpatialBounds(new_min, new_max); + } + + // 检查是否包含一个点 + bool contains(const VectorType& point) const { + for (size_t i = 0; i < Dim; ++i) { + if (point[i] < min_[i] || point[i] > max_[i]) return false; + } + return true; + } + + // 检查是否包含另一个包围盒 + bool contains(const SpatialBounds& other) const { + for (size_t i = 0; i < Dim; ++i) { + if (other.min_[i] < min_[i] || other.max_[i] > max_[i]) return false; + } + return true; + } + + // 检查是否与另一个包围盒相交 + bool intersects(const SpatialBounds& other) const { + for (size_t i = 0; i < Dim; ++i) { + if (other.max_[i] < min_[i] || other.min_[i] > max_[i]) return false; + } + return true; + } + + // 获取两个包围盒的交集 + std::optional intersection(const SpatialBounds& other) const { + VectorType new_min{}; + VectorType new_max{}; + for (size_t i = 0; i < Dim; ++i) { + new_min[i] = std::max(min_[i], other.min_[i]); + new_max[i] = std::min(max_[i], other.max_[i]); + if (new_min[i] > new_max[i]) return std::nullopt; + } + return SpatialBounds(new_min, new_max); + } + + // 分割为子包围盒 + std::vector split(size_t childCount) const { + std::vector children; + + if (Dim == 2 && childCount == 4) { + // 四叉树分割 + VectorType c = center(); + children.reserve(4); + children.emplace_back(VectorType{min_[0], min_[1]}, VectorType{c[0], c[1]}); + children.emplace_back(VectorType{c[0], min_[1]}, VectorType{max_[0], c[1]}); + children.emplace_back(VectorType{min_[0], c[1]}, VectorType{c[0], max_[1]}); + children.emplace_back(VectorType{c[0], c[1]}, VectorType{max_[0], max_[1]}); + } else if (Dim == 3 && childCount == 8) { + // 八叉树分割 + VectorType c = center(); + children.reserve(8); + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + for (int k = 0; k < 2; ++k) { + VectorType child_min{ + i == 0 ? min_[0] : c[0], + j == 0 ? min_[1] : c[1], + k == 0 ? min_[2] : c[2] + }; + VectorType child_max{ + i == 0 ? c[0] : max_[0], + j == 0 ? c[1] : max_[1], + k == 0 ? c[2] : max_[2] + }; + children.emplace_back(child_min, child_max); + } + } + } + } + + return children; + } + + // 相等运算符 + bool operator==(const SpatialBounds& other) const { + return min_ == other.min_ && max_ == other.max_; + } + bool operator!=(const SpatialBounds& other) const { + return !(*this == other); + } + +private: + VectorType min_{}; + VectorType max_{}; +}; + +// 常用类型的别名 +template using Bounds2D = SpatialBounds; +template using Bounds3D = SpatialBounds; + +using Bounds2Df = Bounds2D; +using Bounds2Dd = Bounds2D; +using Bounds2Di = Bounds2D; + +using Bounds3Df = Bounds3D; +using Bounds3Dd = Bounds3D; +using Bounds3Di = Bounds3D; + +// 合并多个包围盒 +template +SpatialBounds mergeBounds(const SpatialBounds& a, + const SpatialBounds& b) { + SpatialBounds result = a; + result.expand(b); + return result; +} + +} // namespace spatial::core diff --git a/src/spatial/core/spatial_item.h b/src/spatial/core/spatial_item.h new file mode 100644 index 00000000..0eedb347 --- /dev/null +++ b/src/spatial/core/spatial_item.h @@ -0,0 +1,126 @@ +#pragma once + +#include "spatial_bounds.h" +#include +#include + +namespace spatial::core { + +/** + * @brief 空间对象接口 + * + * 定义了可以被空间索引管理的基本接口 + * 业务数据需要继承此接口或创建适配器 + */ +class SpatialItem { +public: + virtual ~SpatialItem() = default; + + /** + * @brief 获取空间包围盒 + */ + virtual SpatialBounds getBounds() const = 0; + + /** + * @brief 获取对象的唯一标识 + */ + virtual size_t getId() const = 0; + + /** + * @brief 获取对象的几何中心点 + */ + virtual std::array getCenter() const { + auto bounds = getBounds(); + return bounds.center(); + } + + /** + * @brief 检查对象是否与包围盒相交 + */ + virtual bool intersects(const SpatialBounds& bounds) const { + return getBounds().intersects(bounds); + } + + /** + * @brief 检查对象是否包含在包围盒内 + */ + virtual bool isContainedBy(const SpatialBounds& bounds) const { + return bounds.contains(getBounds()); + } +}; + +/** + * @brief 空间对象智能指针类型 + */ +using SpatialItemPtr = std::shared_ptr; + +/** + * @brief 空间对象列表 + */ +using SpatialItemList = std::vector; + +/** + * @brief 空间对象引用 (轻量级,不拥有所有权) + */ +class SpatialItemRef { +public: + SpatialItemRef() = default; + explicit SpatialItemRef(const SpatialItem* item) : item_(item) {} + explicit SpatialItemRef(const SpatialItemPtr& ptr) : item_(ptr.get()) {} + + const SpatialItem* get() const { return item_; } + const SpatialItem* operator->() const { return item_; } + const SpatialItem& operator*() const { return *item_; } + + bool isValid() const { return item_ != nullptr; } + explicit operator bool() const { return isValid(); } + + bool operator==(const SpatialItemRef& other) const { return item_ == other.item_; } + bool operator!=(const SpatialItemRef& other) const { return !(*this == other); } + +private: + const SpatialItem* item_ = nullptr; +}; + +/** + * @brief 空间对象引用列表 + */ +using SpatialItemRefList = std::vector; + +/** + * @brief 空间对象适配器基类 + * + * 用于将业务数据包装为空间对象 + * @tparam T 业务数据类型 + */ +template +class SpatialItemAdapter : public SpatialItem { +public: + explicit SpatialItemAdapter(const T& data) : data_(data) {} + explicit SpatialItemAdapter(T&& data) : data_(std::move(data)) {} + + const T& getData() const { return data_; } + T& getData() { return data_; } + +protected: + T data_; +}; + +/** + * @brief 带ID的空间对象适配器 + */ +template +class SpatialItemAdapterWithId : public SpatialItemAdapter { +public: + SpatialItemAdapterWithId(size_t id, const T& data) + : SpatialItemAdapter(data), id_(id) {} + SpatialItemAdapterWithId(size_t id, T&& data) + : SpatialItemAdapter(std::move(data)), id_(id) {} + + size_t getId() const override { return id_; } + +private: + size_t id_; +}; + +} // namespace spatial::core diff --git a/src/spatial/strategy/octree_strategy.h b/src/spatial/strategy/octree_strategy.h new file mode 100644 index 00000000..c506a228 --- /dev/null +++ b/src/spatial/strategy/octree_strategy.h @@ -0,0 +1,278 @@ +#pragma once + +#include "../core/slicing_strategy.h" +#include +#include +#include + +namespace spatial::strategy { + +/** + * @brief 八叉树切片配置 + */ +struct OctreeConfig : public core::SlicingConfig { + // 八叉树特定配置 + size_t minItemsPerNode = 500; +}; + +/** + * @brief 八叉树节点 + * + * 使用 std::unique_ptr 管理子节点,自动内存管理 + */ +class OctreeNode : public core::SpatialIndexNode { +public: + OctreeNode() = default; + OctreeNode(const core::SpatialBounds& bounds, int depth) + : bounds_(bounds), depth_(depth) {} + + void setBounds(const core::SpatialBounds& bounds) { bounds_ = bounds; } + void setDepth(int depth) { depth_ = depth; } + void addItem(const core::SpatialItemRef& item) { items_.push_back(item); } + + const core::SpatialBounds& getBounds3D() const { return bounds_; } + OctreeNode* getParent() const { return parent_; } + OctreeNode* getChild(int index) const { return children_[index].get(); } + + void setChild(int index, std::unique_ptr child) { + if (child) { + child->parent_ = this; + } + children_[index] = std::move(child); + } + + core::SpatialBounds getBounds() const override { + return bounds_; + } + + size_t getDepth() const override { return depth_; } + + core::SpatialItemRefList getItems() const override { + return items_; + } + + bool isLeaf() const override { + return !children_[0]; + } + + std::vector getChildren() const override { + std::vector result; + for (const auto& child : children_) { + if (child) { + result.push_back(child.get()); + } + } + return result; + } + + size_t getItemCount() const override { return items_.size(); } + + int getChildIndex(const std::array& point) const { + auto center = bounds_.center(); + int index = 0; + if (point[0] >= center[0]) index += 1; + if (point[1] >= center[1]) index += 2; + if (point[2] >= center[2]) index += 4; + return index; + } + + void split() { + auto center = bounds_.center(); + auto min = bounds_.min(); + auto max = bounds_.max(); + + // 8个子节点:按x, y, z顺序 + // index = (x >= cx) + 2*(y >= cy) + 4*(z >= cz) + for (int i = 0; i < 8; ++i) { + bool xHigh = (i & 1) != 0; + bool yHigh = (i & 2) != 0; + bool zHigh = (i & 4) != 0; + + std::array childMin{ + xHigh ? center[0] : min[0], + yHigh ? center[1] : min[1], + zHigh ? center[2] : min[2] + }; + std::array childMax{ + xHigh ? max[0] : center[0], + yHigh ? max[1] : center[1], + zHigh ? max[2] : center[2] + }; + + auto child = std::make_unique( + core::SpatialBounds(childMin, childMax), + depth_ + 1 + ); + child->parent_ = this; + children_[i] = std::move(child); + } + } + + void collectAllItems(core::SpatialItemRefList& result) const { + result.insert(result.end(), items_.begin(), items_.end()); + for (const auto& child : children_) { + if (child) { + child->collectAllItems(result); + } + } + } + + // 清除所有子节点(unique_ptr 自动释放内存) + void clearChildren() { + for (auto& child : children_) { + child.reset(); + } + } + + ~OctreeNode() = default; + +private: + core::SpatialBounds bounds_; + int depth_ = 0; + OctreeNode* parent_ = nullptr; + std::array, 8> children_; + core::SpatialItemRefList items_; +}; + +/** + * @brief 八叉树索引 + */ +class OctreeIndex : public core::SpatialIndex { +public: + OctreeIndex(std::unique_ptr root, const core::SpatialBounds& worldBounds) + : root_(std::move(root)), worldBounds_(worldBounds) {} + + const core::SpatialIndexNode* getRootNode() const override { + return root_.get(); + } + + core::SpatialItemRefList getAllItems() const override { + core::SpatialItemRefList result; + if (root_) { + root_->collectAllItems(result); + } + return result; + } + + core::SpatialItemRefList query(const core::SpatialBounds& bounds) const override { + core::SpatialItemRefList result; + if (!root_) return result; + + queryRecursive(root_.get(), bounds, result); + return result; + } + + size_t getNodeCount() const override { return nodeCount_; } + size_t getItemCount() const override { return getAllItems().size(); } + + void setNodeCount(size_t count) { nodeCount_ = count; } + OctreeNode* getRoot() const { return root_.get(); } + + ~OctreeIndex() = default; + +private: + void queryRecursive(OctreeNode* node, + const core::SpatialBounds& queryBounds, + core::SpatialItemRefList& result) const { + if (!node) return; + + if (!node->getBounds3D().intersects(queryBounds)) { + return; + } + + result.insert(result.end(), node->getItems().begin(), node->getItems().end()); + + for (int i = 0; i < 8; ++i) { + queryRecursive(node->getChild(i), queryBounds, result); + } + } + + std::unique_ptr root_; + core::SpatialBounds worldBounds_; + size_t nodeCount_ = 0; +}; + +/** + * @brief 八叉树切片策略 + */ +class OctreeStrategy : public core::SlicingStrategy { +public: + OctreeStrategy() = default; + + size_t getDimension() const override { return 3; } + size_t getChildCount() const override { return 8; } + const char* getName() const override { return "Octree"; } + + std::unique_ptr buildIndex( + const core::SpatialItemList& items, + const core::SpatialBounds& worldBounds, + const core::SlicingConfig& config) override + { + const auto& octreeConfig = dynamic_cast(config); + + auto root = std::make_unique(worldBounds, 0); + + for (const auto& item : items) { + auto bounds = item->getBounds(); + insertItem(root.get(), item, bounds, 0, octreeConfig); + } + + size_t nodeCount = 0; + countNodes(root.get(), nodeCount); + + auto index = std::make_unique(std::move(root), worldBounds); + index->setNodeCount(nodeCount); + return index; + } + +private: + void insertItem(OctreeNode* node, + const core::SpatialItemPtr& item, + const core::SpatialBounds& itemBounds, + int depth, + const OctreeConfig& config) + { + if (!node->getBounds3D().intersects(itemBounds)) { + return; + } + + if (node->isLeaf()) { + node->addItem(core::SpatialItemRef(item)); + + if (depth < static_cast(config.maxDepth) && + node->getItemCount() > config.maxItemsPerNode) { + redistributeItems(node, config); + } + return; + } + + for (int i = 0; i < 8; ++i) { + auto* child = node->getChild(i); + if (child && child->getBounds3D().intersects(itemBounds)) { + insertItem(child, item, itemBounds, depth + 1, config); + } + } + } + + void redistributeItems(OctreeNode* node, const OctreeConfig& config) { + auto items = node->getItems(); + node->clearChildren(); + node->split(); + + for (const auto& itemRef : items) { + auto bounds = itemRef->getBounds(); + auto tempPtr = std::shared_ptr(const_cast(itemRef.get()), [](core::SpatialItem*){}); + insertItem(node, tempPtr, bounds, node->getDepth(), config); + } + } + + void countNodes(OctreeNode* node, size_t& count) const { + if (!node) return; + count++; + for (int i = 0; i < 8; ++i) { + countNodes(node->getChild(i), count); + } + } +}; + +} // namespace spatial::strategy diff --git a/src/spatial/strategy/octree_strategy.h.pch b/src/spatial/strategy/octree_strategy.h.pch new file mode 100644 index 00000000..dcbc51b0 Binary files /dev/null and b/src/spatial/strategy/octree_strategy.h.pch differ diff --git a/src/spatial/strategy/quadtree_strategy.h b/src/spatial/strategy/quadtree_strategy.h new file mode 100644 index 00000000..0e62f20c --- /dev/null +++ b/src/spatial/strategy/quadtree_strategy.h @@ -0,0 +1,420 @@ +#pragma once + +#include "../core/slicing_strategy.h" +#include +#include +#include +#include + +namespace spatial::strategy { + +/** + * @brief 四叉树切片配置 + */ +struct QuadtreeConfig : public core::SlicingConfig { + // 四叉树特定配置 + size_t minItemsPerNode = 100; + double metricThreshold = 0.01; +}; + +/** + * @brief 四叉树节点坐标 + */ +struct QuadtreeCoord { + int z = 0; + int x = 0; + int y = 0; + + QuadtreeCoord() = default; + QuadtreeCoord(int z_, int x_, int y_) : z(z_), x(x_), y(y_) {} + + uint64_t encode() const { + return (static_cast(z) << 48) | + (static_cast(x) << 24) | + static_cast(y); + } + + static QuadtreeCoord decode(uint64_t key) { + return QuadtreeCoord( + static_cast((key >> 48) & 0xFFFF), + static_cast((key >> 24) & 0xFFFFFF), + static_cast(key & 0xFFFFFF) + ); + } + + bool operator==(const QuadtreeCoord& other) const { + return z == other.z && x == other.x && y == other.y; + } +}; + +/** + * @brief 四叉树节点 + * + * 使用 std::unique_ptr 管理子节点,自动内存管理 + */ +class QuadtreeNode : public core::SpatialIndexNode { +public: + QuadtreeNode() = default; + QuadtreeNode(const core::SpatialBounds& bounds, int depth) + : bounds2d_(bounds), depth_(depth) {} + + void setBounds(const core::SpatialBounds& bounds) { bounds2d_ = bounds; } + void setDepth(int depth) { depth_ = depth; } + void setCoord(const QuadtreeCoord& coord) { coord_ = coord; } + void addItem(const core::SpatialItemRef& item) { items_.push_back(item); } + void clearItems() { items_.clear(); } + + const core::SpatialBounds& getBounds2D() const { return bounds2d_; } + const QuadtreeCoord& getCoord() const { return coord_; } + QuadtreeNode* getParent() const { return parent_; } + QuadtreeNode* getChild(int index) const { return children_[index].get(); } + + void setChild(int index, std::unique_ptr child) { + if (child) { + child->parent_ = this; + } + children_[index] = std::move(child); + } + + core::SpatialBounds getBounds() const override { + return core::SpatialBounds( + bounds2d_.min()[0], bounds2d_.min()[1], 0.0, + bounds2d_.max()[0], bounds2d_.max()[1], 0.0 + ); + } + + size_t getDepth() const override { return depth_; } + + core::SpatialItemRefList getItems() const override { + return items_; + } + + bool isLeaf() const override { + return !children_[0]; + } + + std::vector getChildren() const override { + std::vector result; + for (const auto& child : children_) { + if (child) { + result.push_back(child.get()); + } + } + return result; + } + + size_t getItemCount() const override { return items_.size(); } + + int getChildIndex(double x, double y) const { + auto center = bounds2d_.center(); + int index = 0; + if (x >= center[0]) index += 1; + if (y >= center[1]) index += 2; + return index; + } + + void split() { + auto center = bounds2d_.center(); + auto min = bounds2d_.min(); + auto max = bounds2d_.max(); + + auto child0 = std::make_unique( + core::SpatialBounds( + std::array{min[0], min[1]}, + std::array{center[0], center[1]} + ), + depth_ + 1 + ); + auto child1 = std::make_unique( + core::SpatialBounds( + std::array{center[0], min[1]}, + std::array{max[0], center[1]} + ), + depth_ + 1 + ); + auto child2 = std::make_unique( + core::SpatialBounds( + std::array{min[0], center[1]}, + std::array{center[0], max[1]} + ), + depth_ + 1 + ); + auto child3 = std::make_unique( + core::SpatialBounds( + std::array{center[0], center[1]}, + std::array{max[0], max[1]} + ), + depth_ + 1 + ); + + children_[0] = std::move(child0); + children_[1] = std::move(child1); + children_[2] = std::move(child2); + children_[3] = std::move(child3); + + for (int i = 0; i < 4; ++i) { + children_[i]->parent_ = this; + QuadtreeCoord childCoord = coord_; + childCoord.z = depth_ + 1; + childCoord.x = coord_.x * 2 + (i % 2); + childCoord.y = coord_.y * 2 + (i / 2); + children_[i]->setCoord(childCoord); + } + } + + void collectAllItems(core::SpatialItemRefList& result) const { + result.insert(result.end(), items_.begin(), items_.end()); + for (const auto& child : children_) { + if (child) { + child->collectAllItems(result); + } + } + } + + void collectLeaves(std::vector& result) const { + if (isLeaf()) { + result.push_back(const_cast(this)); + } else { + for (const auto& child : children_) { + if (child) { + child->collectLeaves(result); + } + } + } + } + + // 清除所有子节点(unique_ptr 自动释放内存) + void clearChildren() { + for (auto& child : children_) { + child.reset(); + } + } + + ~QuadtreeNode() = default; + +private: + core::SpatialBounds bounds2d_; + int depth_ = 0; + QuadtreeCoord coord_; + QuadtreeNode* parent_ = nullptr; + std::array, 4> children_; + core::SpatialItemRefList items_; +}; + +/** + * @brief 四叉树索引 + */ +class QuadtreeIndex : public core::SpatialIndex { +public: + QuadtreeIndex(std::unique_ptr root, const core::SpatialBounds& worldBounds) + : root_(std::move(root)), worldBounds_(worldBounds) {} + + const core::SpatialIndexNode* getRootNode() const override { + return root_.get(); + } + + core::SpatialItemRefList getAllItems() const override { + core::SpatialItemRefList result; + if (root_) { + root_->collectAllItems(result); + } + return result; + } + + core::SpatialItemRefList query(const core::SpatialBounds& bounds) const override { + core::SpatialItemRefList result; + if (!root_) return result; + + core::SpatialBounds bounds2d( + std::array{bounds.min()[0], bounds.min()[1]}, + std::array{bounds.max()[0], bounds.max()[1]} + ); + + queryRecursive(root_.get(), bounds2d, result); + return result; + } + + size_t getNodeCount() const override { return nodeCount_; } + size_t getItemCount() const override { return getAllItems().size(); } + + void setNodeCount(size_t count) { nodeCount_ = count; } + QuadtreeNode* getRoot() const { return root_.get(); } + + // 存储原始 SpatialItemPtr 以保持对象生命周期 + void setItemStorage(core::SpatialItemList&& items) { itemStorage_ = std::move(items); } + + ~QuadtreeIndex() = default; + +private: + void queryRecursive(QuadtreeNode* node, + const core::SpatialBounds& queryBounds, + core::SpatialItemRefList& result) const { + if (!node) return; + + if (!node->getBounds2D().intersects(queryBounds)) { + return; + } + + result.insert(result.end(), node->getItems().begin(), node->getItems().end()); + + for (int i = 0; i < 4; ++i) { + queryRecursive(node->getChild(i), queryBounds, result); + } + } + + std::unique_ptr root_; + core::SpatialBounds worldBounds_; + size_t nodeCount_ = 0; + core::SpatialItemList itemStorage_; // 存储原始 SpatialItemPtr 以保持对象生命周期 +}; + +/** + * @brief 四叉树切片策略 + */ +class QuadtreeStrategy : public core::SlicingStrategy { +public: + QuadtreeStrategy() = default; + + size_t getDimension() const override { return 2; } + size_t getChildCount() const override { return 4; } + const char* getName() const override { return "Quadtree"; } + + std::unique_ptr buildIndex( + const core::SpatialItemList& items, + const core::SpatialBounds& worldBounds, + const core::SlicingConfig& config) override + { + const auto& quadConfig = dynamic_cast(config); + + core::SpatialBounds worldBounds2d( + std::array{worldBounds.min()[0], worldBounds.min()[1]}, + std::array{worldBounds.max()[0], worldBounds.max()[1]} + ); + + auto root = std::make_unique(worldBounds2d, 0); + root->setCoord(QuadtreeCoord(0, 0, 0)); + + for (const auto& item : items) { + auto bounds = item->getBounds(); + core::SpatialBounds itemBounds2d( + std::array{bounds.min()[0], bounds.min()[1]}, + std::array{bounds.max()[0], bounds.max()[1]} + ); + insertItem(root.get(), item, itemBounds2d, 0, quadConfig); + } + + size_t nodeCount = 0; + countNodes(root.get(), nodeCount); + + auto index = std::make_unique(std::move(root), worldBounds2d); + index->setNodeCount(nodeCount); + // 存储原始 items 以保持对象生命周期 + index->setItemStorage(const_cast(items)); + return index; + } + +private: + void insertItem(QuadtreeNode* node, + const core::SpatialItemPtr& item, + const core::SpatialBounds& itemBounds, + int depth, + const QuadtreeConfig& config) + { + if (!node->getBounds2D().intersects(itemBounds)) { + return; + } + + if (node->isLeaf()) { + node->addItem(core::SpatialItemRef(item)); + + if (depth < static_cast(config.maxDepth) && + node->getItemCount() > config.maxItemsPerNode) { + redistributeItems(node, config); + } + return; + } + + for (int i = 0; i < 4; ++i) { + auto* child = node->getChild(i); + if (child && child->getBounds2D().intersects(itemBounds)) { + insertItem(child, item, itemBounds, depth + 1, config); + } + } + } + + void redistributeItems(QuadtreeNode* node, const QuadtreeConfig& config) { + auto items = node->getItems(); + node->clearItems(); // 清空当前节点的 items + node->split(); // 分割节点 + + // 将 items 重新插入到子节点 + for (const auto& itemRef : items) { + auto bounds = itemRef->getBounds(); + core::SpatialBounds itemBounds2d( + std::array{bounds.min()[0], bounds.min()[1]}, + std::array{bounds.max()[0], bounds.max()[1]} + ); + // 直接使用 itemRef,不需要创建临时 shared_ptr + // 因为 insertItemToChildren 只使用 bounds 和 itemRef + insertItemToChildren(node, itemRef, itemBounds2d, node->getDepth() + 1, config); + } + } + + // 辅助函数:将 item 插入到子节点(与阶段2原始实现一致:只添加到一个子节点) + void insertItemToChildren(QuadtreeNode* node, + const core::SpatialItemRef& itemRef, + const core::SpatialBounds& itemBounds, + int depth, + const QuadtreeConfig& config) + { + for (int i = 0; i < 4; ++i) { + auto* child = node->getChild(i); + if (child && child->getBounds2D().intersects(itemBounds)) { + insertItem(child, itemRef, itemBounds, depth, config); + // 与阶段2原始实现一致:只添加到一个子节点后就返回 + if (std::find(node->getItems().begin(), node->getItems().end(), itemRef) == node->getItems().end()) { + return; + } + } + } + } + + // 重载 insertItem,接受 SpatialItemRef + void insertItem(QuadtreeNode* node, + const core::SpatialItemRef& itemRef, + const core::SpatialBounds& itemBounds, + int depth, + const QuadtreeConfig& config) + { + if (!node->getBounds2D().intersects(itemBounds)) { + return; + } + + if (node->isLeaf()) { + node->addItem(itemRef); + + if (depth < static_cast(config.maxDepth) && + node->getItemCount() > config.maxItemsPerNode) { + redistributeItems(node, config); + } + return; + } + + for (int i = 0; i < 4; ++i) { + auto* child = node->getChild(i); + if (child && child->getBounds2D().intersects(itemBounds)) { + insertItem(child, itemRef, itemBounds, depth + 1, config); + } + } + } + + void countNodes(QuadtreeNode* node, size_t& count) const { + if (!node) return; + count++; + for (int i = 0; i < 4; ++i) { + countNodes(node->getChild(i), count); + } + } +}; + +} // namespace spatial::strategy diff --git a/src/tileset.cpp b/src/tileset.cpp index 71f7700b..5736c8d5 100644 --- a/src/tileset.cpp +++ b/src/tileset.cpp @@ -9,8 +9,10 @@ #include #include +#include "utils/log.h" +#include "utils/file_utils.h" #include "extern.h" -#include "coordinate_transformer.h" +#include "coords/coordinate_transformer.h" #include /////////////////////// @@ -151,8 +153,6 @@ wkt_convert(char* wkt, double* val, char* path) { if (is_geoid_initialized()) { double geoid_height = get_geoid_height(lat, lon); final_height = orthometric_to_ellipsoidal(lat, lon, height); - fprintf(stderr, "[GeoTransform] Geoid correction applied: orthometric=%.3f + geoid=%.3f = ellipsoidal=%.3f\n", - height, geoid_height, final_height); } // 创建地理参考 @@ -285,7 +285,7 @@ write_tileset_box(Transform* trans, Box& box, double geometricError, json_txt += last_buf; - bool ret = write_file(json_file, json_txt.data(), (unsigned long)json_txt.size()); + bool ret = utils::write_file(json_file, json_txt.data(), (unsigned long)json_txt.size()); if (!ret) { LOG_E("write file %s fail", json_file); } @@ -338,7 +338,7 @@ bool write_tileset_region( json_txt += last_buf; - bool ret = write_file(json_file, json_txt.data(), (unsigned long)json_txt.size()); + bool ret = utils::write_file(json_file, json_txt.data(), (unsigned long)json_txt.size()); if (!ret) { LOG_E("write file %s fail", json_file); } @@ -432,7 +432,7 @@ write_tileset(double radian_x, double radian_y, json_txt += last_buf; - bool ret = write_file(full_path, json_txt.data(), (unsigned long)json_txt.size()); + bool ret = utils::write_file(full_path, json_txt.data(), (unsigned long)json_txt.size()); if (!ret) { LOG_E("write file %s fail", filename); } diff --git a/src/tileset/README.md b/src/tileset/README.md new file mode 100644 index 00000000..baa7218a --- /dev/null +++ b/src/tileset/README.md @@ -0,0 +1,167 @@ +# Tileset Module + +This module provides a unified C++ interface for generating 3D Tiles `tileset.json` files. + +## Overview + +The tileset module consolidates the previously scattered tileset generation logic from: +- `shp23dtile` (Shapefile to 3D Tiles) +- `osgb23dtile` (OSGB to 3D Tiles) +- `fbx23dtile` (FBX to 3D Tiles) + +## Architecture + +``` +tileset/ +├── bounding_volume.h/cpp # Bounding volume types (Box, Region, Sphere) +├── geometric_error.h/cpp # Geometric error calculations +├── transform.h/cpp # Coordinate transformations (ENU/ECEF) +├── tileset_types.h/cpp # Core types (Tile, Tileset, Content, Asset) +├── tileset_writer.h/cpp # JSON serialization +└── tileset.h # Main header (includes all above) +``` + +## Quick Start + +```cpp +#include "tileset/tileset.h" + +using namespace tileset; + +// Create a bounding box +Box box = Box::fromCenterAndHalfLengths(0, 0, 50, 100, 100, 50); + +// Create a tile +Tile tile(box); +tile.geometricError = computeGeometricError(box); +tile.setContent("model.b3dm"); + +// Create a tileset +Tileset tileset(tile); +tileset.setVersion("1.0"); +tileset.setGltfUpAxis("Z"); + +// Write to file +TilesetWriter writer; +writer.writeToFile(tileset, "tileset.json"); +``` + +## Core Concepts + +### Bounding Volumes + +Three types of bounding volumes are supported: + +```cpp +// Box: center(3) + x_axis(3) + y_axis(3) + z_axis(3) = 12 numbers +Box box = Box::fromCenterAndHalfLengths(cx, cy, cz, hx, hy, hz); + +// Region: west, south, east, north, min_height, max_height (in radians) +Region region = Region::fromDegrees(west, south, east, north, min_h, max_h); + +// Sphere: center(3) + radius = 4 numbers +Sphere sphere = Sphere::fromCenterAndRadius(cx, cy, cz, radius); +``` + +### Geometric Error + +Geometric error determines when a tile is refined: + +```cpp +// From bounding volume +double error = computeGeometricError(boundingVolume); + +// From dimensions +double error = computeGeometricErrorFromBox(width, height, depth); + +// From diagonal +double error = computeGeometricErrorFromDiagonal(diagonal); +``` + +### Coordinate Transforms + +ENU (East-North-Up) to ECEF (Earth-Centered-Earth-Fixed): + +```cpp +// Calculate transform matrix at a geodetic position +TransformMatrix matrix = calcEnuToEcefMatrix(lon_deg, lat_deg, height); + +// Apply to tile +tile.setTransform(matrix); +``` + +### Hierarchical Tilesets + +```cpp +// Create parent tile +Tile parent(box); +parent.geometricError = 100.0; + +// Create child tiles +Tile child1(childBox1); +child1.geometricError = 50.0; +child1.setContent("child1.b3dm"); + +Tile child2(childBox2); +child2.geometricError = 50.0; +child2.setContent("child2.b3dm"); + +// Build hierarchy +parent.addChild(child1); +parent.addChild(child2); +parent.refine = "REPLACE"; // or "ADD" + +// Create tileset +Tileset tileset(parent); +``` + +## Migration Guide + +### From old tileset.cpp + +**Before:** +```cpp +Box box = {...}; +write_tileset_box(&transform, box, geometricError, "model.b3dm", "tileset.json"); +``` + +**After:** +```cpp +Box box = Box::fromCenterAndHalfLengths(...); +Tile tile(box); +tile.geometricError = geometricError; +tile.setContent("model.b3dm"); +if (transform) { + tile.setTransform(*transform); +} + +Tileset tileset(tile); +TilesetWriter writer; +writer.writeToFile(tileset, "tileset.json"); +``` + +### From shp23dtile + +**Before:** +```cpp +nlohmann::json root; +root["asset"] = {{"version", "1.0"}, {"gltfUpAxis", "Z"}}; +root["geometricError"] = node.geometric_error; +// ... manual JSON construction +``` + +**After:** +```cpp +Tile tile(box); +tile.geometricError = node.geometric_error; +// ... use TilesetWriter +``` + +## API Reference + +See header files for detailed API documentation. + +## Dependencies + +- nlohmann/json (JSON serialization) +- Standard C++17/20 library diff --git a/src/tileset/bounding_volume.cpp b/src/tileset/bounding_volume.cpp new file mode 100644 index 00000000..eb2705c3 --- /dev/null +++ b/src/tileset/bounding_volume.cpp @@ -0,0 +1,225 @@ +#include "bounding_volume.h" + +#include +#include + +namespace tileset { + +// Coordinate conversion helpers +static constexpr double PI = 3.14159265358979323846; +static constexpr double DEG_TO_RAD = PI / 180.0; + +static double degreesToRadians(double deg) { + return deg * DEG_TO_RAD; +} + +// Box implementation +Box Box::fromCenterAndHalfLengths(double cx, double cy, double cz, + double hx, double hy, double hz) { + Box box; + box.values = { + cx, cy, cz, // center + hx, 0.0, 0.0, // x axis (half-length vector) + 0.0, hy, 0.0, // y axis (half-length vector) + 0.0, 0.0, hz // z axis (half-length vector) + }; + return box; +} + +double Box::diagonal() const { + // Calculate the full diagonal of the box + double hx = std::sqrt(values[3]*values[3] + values[4]*values[4] + values[5]*values[5]); + double hy = std::sqrt(values[6]*values[6] + values[7]*values[7] + values[8]*values[8]); + double hz = std::sqrt(values[9]*values[9] + values[10]*values[10] + values[11]*values[11]); + return 2.0 * std::sqrt(hx*hx + hy*hy + hz*hz); +} + +Box Box::extended(double ratio) const { + Box result = *this; + double scale = 1.0 + ratio; + // Scale the half-length vectors + for (int i = 3; i < 12; ++i) { + result.values[i] *= scale; + } + return result; +} + +// Region implementation +Region Region::fromDegrees(double west_deg, double south_deg, double east_deg, double north_deg, + double min_h, double max_h) { + return Region( + degreesToRadians(west_deg), + degreesToRadians(south_deg), + degreesToRadians(east_deg), + degreesToRadians(north_deg), + min_h, max_h + ); +} + +double Region::diagonal(double latitude) const { + // Approximate meters per degree + double lat_rad = latitude != 0.0 ? latitude : (north + south) * 0.5; + double meters_per_lon = 111320.0 * std::cos(lat_rad); + double meters_per_lat = 110540.0; + + double dx = (east - west) * meters_per_lon; + double dy = (north - south) * meters_per_lat; + double dz = max_height - min_height; + + return std::sqrt(dx*dx + dy*dy + dz*dz); +} + +// Sphere implementation +Sphere Sphere::fromCenterAndRadius(double cx, double cy, double cz, double radius) { + Sphere s; + s.values = {cx, cy, cz, radius}; + return s; +} + +// Helper to get center from any bounding volume type +namespace { + struct CenterVisitor { + std::array operator()(const Box& b) const { + return {b.values[0], b.values[1], b.values[2]}; + } + std::array operator()(const Region& r) const { + double lon = (r.west + r.east) * 0.5; + double lat = (r.south + r.north) * 0.5; + double height = (r.min_height + r.max_height) * 0.5; + return {lon, lat, height}; + } + std::array operator()(const Sphere& s) const { + return {s.values[0], s.values[1], s.values[2]}; + } + }; +} + +// General functions +double computeDiagonal(const BoundingVolume& bv, double reference_latitude) { + return std::visit([&](const auto& v) -> double { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return v.diagonal(); + } else if constexpr (std::is_same_v) { + return v.diagonal(reference_latitude); + } else if constexpr (std::is_same_v) { + return 2.0 * v.radius(); + } + return 0.0; + }, bv); +} + +std::vector toJsonArray(const BoundingVolume& bv) { + return std::visit([](const auto& v) -> std::vector { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return std::vector(v.values.begin(), v.values.end()); + } else if constexpr (std::is_same_v) { + return {v.west, v.south, v.east, v.north, v.min_height, v.max_height}; + } else if constexpr (std::is_same_v) { + return std::vector(v.values.begin(), v.values.end()); + } + return {}; + }, bv); +} + +Box createBoxFromMinMax(double min_x, double min_y, double min_z, + double max_x, double max_y, double max_z) { + double cx = (min_x + max_x) * 0.5; + double cy = (min_y + max_y) * 0.5; + double cz = (min_z + max_z) * 0.5; + double hx = (max_x - min_x) * 0.5; + double hy = (max_y - min_y) * 0.5; + double hz = (max_z - min_z) * 0.5; + return Box::fromCenterAndHalfLengths(cx, cy, cz, hx, hy, hz); +} + +Region createRegionFromDegrees(double min_lon, double min_lat, double max_lon, double max_lat, + double min_height, double max_height) { + return Region::fromDegrees(min_lon, min_lat, max_lon, max_lat, min_height, max_height); +} + +std::optional mergeBoundingVolumes(const BoundingVolume& a, const BoundingVolume& b) { + // Both must be the same type + if (a.index() != b.index()) { + return std::nullopt; + } + + // Use std::get to access the specific type since we know they match + if (std::holds_alternative(a)) { + const Box& v1 = std::get(a); + const Box& v2 = std::get(b); + + // For boxes, we compute the union of the two boxes + // This is an approximation - we create a new AABB that contains both + auto c1 = v1.center(); + auto x1 = v1.xAxis(); + auto y1 = v1.yAxis(); + auto z1 = v1.zAxis(); + + auto c2 = v2.center(); + auto x2 = v2.xAxis(); + auto y2 = v2.yAxis(); + auto z2 = v2.zAxis(); + + // Compute corners of both boxes and find min/max + double min_x = std::min(c1[0] - std::abs(x1[0]) - std::abs(y1[0]) - std::abs(z1[0]), + c2[0] - std::abs(x2[0]) - std::abs(y2[0]) - std::abs(z2[0])); + double max_x = std::max(c1[0] + std::abs(x1[0]) + std::abs(y1[0]) + std::abs(z1[0]), + c2[0] + std::abs(x2[0]) + std::abs(y2[0]) + std::abs(z2[0])); + double min_y = std::min(c1[1] - std::abs(x1[1]) - std::abs(y1[1]) - std::abs(z1[1]), + c2[1] - std::abs(x2[1]) - std::abs(y2[1]) - std::abs(z2[1])); + double max_y = std::max(c1[1] + std::abs(x1[1]) + std::abs(y1[1]) + std::abs(z1[1]), + c2[1] + std::abs(x2[1]) + std::abs(y2[1]) + std::abs(z2[1])); + double min_z = std::min(c1[2] - std::abs(x1[2]) - std::abs(y1[2]) - std::abs(z1[2]), + c2[2] - std::abs(x2[2]) - std::abs(y2[2]) - std::abs(z2[2])); + double max_z = std::max(c1[2] + std::abs(x1[2]) + std::abs(y1[2]) + std::abs(z1[2]), + c2[2] + std::abs(x2[2]) + std::abs(y2[2]) + std::abs(z2[2])); + + return createBoxFromMinMax(min_x, min_y, min_z, max_x, max_y, max_z); + } + else if (std::holds_alternative(a)) { + const Region& v1 = std::get(a); + const Region& v2 = std::get(b); + + Region r; + r.west = std::min(v1.west, v2.west); + r.south = std::min(v1.south, v2.south); + r.east = std::max(v1.east, v2.east); + r.north = std::max(v1.north, v2.north); + r.min_height = std::min(v1.min_height, v2.min_height); + r.max_height = std::max(v1.max_height, v2.max_height); + return r; + } + else if (std::holds_alternative(a)) { + const Sphere& v1 = std::get(a); + const Sphere& v2 = std::get(b); + + // For spheres, we create a new sphere that contains both + auto c1 = v1.center(); + auto c2 = v2.center(); + double dx = c2[0] - c1[0]; + double dy = c2[1] - c1[1]; + double dz = c2[2] - c1[2]; + double dist = std::sqrt(dx*dx + dy*dy + dz*dz); + + if (dist + v2.radius() <= v1.radius()) { + return v1; // v1 contains v2 + } + if (dist + v1.radius() <= v2.radius()) { + return v2; // v2 contains v1 + } + + // New sphere contains both + double new_radius = (dist + v1.radius() + v2.radius()) * 0.5; + double ratio = (new_radius - v1.radius()) / dist; + double cx = c1[0] + dx * ratio; + double cy = c1[1] + dy * ratio; + double cz = c1[2] + dz * ratio; + return Sphere::fromCenterAndRadius(cx, cy, cz, new_radius); + } + + return std::nullopt; +} + +} // namespace tileset diff --git a/src/tileset/bounding_volume.h b/src/tileset/bounding_volume.h new file mode 100644 index 00000000..57a94422 --- /dev/null +++ b/src/tileset/bounding_volume.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include + +namespace tileset { + +// 3D Tiles bounding volume types +// Reference: https://github.com/CesiumGS/3d-tiles/tree/main/specification#bounding-volumes + +// Box: center(3) + x_axis(3) + y_axis(3) + z_axis(3) = 12 numbers +// The x/y/z axes are half-length vectors +struct Box { + std::array values{}; + + Box() = default; + explicit Box(const std::array& v) : values(v) {} + + // Convenience constructor from center and half-lengths (axis-aligned) + static Box fromCenterAndHalfLengths(double cx, double cy, double cz, + double hx, double hy, double hz); + + // Get center point + std::array center() const { return {values[0], values[1], values[2]}; } + + // Get half-length vectors + std::array xAxis() const { return {values[3], values[4], values[5]}; } + std::array yAxis() const { return {values[6], values[7], values[8]}; } + std::array zAxis() const { return {values[9], values[10], values[11]}; } + + // Calculate diagonal length (approximate size of the box) + double diagonal() const; + + // Extend box by a ratio (inflate) + Box extended(double ratio) const; +}; + +// Region: west, south, east, north, min_height, max_height (radians for angles) +struct Region { + double west = 0.0; // radians + double south = 0.0; // radians + double east = 0.0; // radians + double north = 0.0; // radians + double min_height = 0.0; // meters + double max_height = 0.0; // meters + + Region() = default; + Region(double w, double s, double e, double n, double min_h, double max_h) + : west(w), south(s), east(e), north(n), min_height(min_h), max_height(max_h) {} + + // Create from degrees + static Region fromDegrees(double west_deg, double south_deg, double east_deg, double north_deg, + double min_h, double max_h); + + // Calculate approximate diagonal in meters + double diagonal(double latitude) const; +}; + +// Sphere: center(3) + radius = 4 numbers +struct Sphere { + std::array values{}; + + Sphere() = default; + explicit Sphere(const std::array& v) : values(v) {} + + static Sphere fromCenterAndRadius(double cx, double cy, double cz, double radius); + + std::array center() const { return {values[0], values[1], values[2]}; } + double radius() const { return values[3]; } +}; + +// Bounding volume variant type +using BoundingVolume = std::variant; + +// Calculate diagonal length for any bounding volume type +double computeDiagonal(const BoundingVolume& bv, double reference_latitude = 0.0); + +// Convert bounding volume to JSON array +std::vector toJsonArray(const BoundingVolume& bv); + +// Factory functions from various inputs + +// Create box from min/max corners (axis-aligned) +Box createBoxFromMinMax(double min_x, double min_y, double min_z, + double max_x, double max_y, double max_z); + +// Create region from tile bounds in degrees +Region createRegionFromDegrees(double min_lon, double min_lat, double max_lon, double max_lat, + double min_height, double max_height); + +// Merge two bounding volumes (only for same type) +std::optional mergeBoundingVolumes(const BoundingVolume& a, const BoundingVolume& b); + +} // namespace tileset diff --git a/src/tileset/geometric_error.cpp b/src/tileset/geometric_error.cpp new file mode 100644 index 00000000..7a1dc499 --- /dev/null +++ b/src/tileset/geometric_error.cpp @@ -0,0 +1,60 @@ +#include "geometric_error.h" + +#include +#include + +namespace tileset { + +double computeGeometricErrorFromDiagonal(double diagonal, double scale) { + if (diagonal <= 0.0) { + return 0.0; + } + return scale * diagonal; +} + +double computeGeometricError(const BoundingVolume& bv, double scale) { + double diagonal = computeDiagonal(bv); + return computeGeometricErrorFromDiagonal(diagonal, scale); +} + +double computeGeometricErrorFromBox(double width, double height, double depth, double scale) { + double diagonal = std::sqrt(width * width + height * height + depth * depth); + return computeGeometricErrorFromDiagonal(diagonal, scale); +} + +double computeParentGeometricError(const std::vector& childErrors, double multiplier) { + if (childErrors.empty()) { + return 0.0; + } + double maxChildError = *std::max_element(childErrors.begin(), childErrors.end()); + return maxChildError * multiplier; +} + +double computeGeometricErrorFromSpans(double span_x, double span_y, double span_z, double scale) { + double max_span = std::max({span_x, span_y, span_z}); + if (max_span <= 0.0) { + return 0.0; + } + // Original formula from shp23dtile: max_span / 20.0 + // This is equivalent to scale * diagonal where scale = 1/20 for a cube + return max_span / 20.0; +} + +std::vector computeLODGeometricErrors(const std::vector& levels, double base_diagonal) { + std::vector errors; + errors.reserve(levels.size()); + + double base_error = computeGeometricErrorFromDiagonal(base_diagonal); + + for (const auto& level : levels) { + // Coarser LOD (smaller ratio) gets larger geometric error + // This ensures that when the camera is far, the coarser model is used + double ratio = std::max(0.01, std::min(1.0, level.target_ratio)); + double error = base_error * std::max(1.0, 1.0 / std::sqrt(ratio)); + errors.push_back(error); + } + + return errors; +} + +} // namespace tileset diff --git a/src/tileset/geometric_error.h b/src/tileset/geometric_error.h new file mode 100644 index 00000000..0c8f6cf2 --- /dev/null +++ b/src/tileset/geometric_error.h @@ -0,0 +1,41 @@ +#pragma once + +#include "bounding_volume.h" + +namespace tileset { + +// Geometric error calculation strategies +// Reference: https://github.com/CesiumGS/3d-tiles/tree/main/specification#geometric-error + +// Default scale factor for geometric error calculation +constexpr double DEFAULT_GEOMETRIC_ERROR_SCALE = 0.5; + +// Calculate geometric error from bounding volume diagonal +// The error is typically a fraction of the diagonal length +double computeGeometricErrorFromDiagonal(double diagonal, double scale = DEFAULT_GEOMETRIC_ERROR_SCALE); + +// Calculate geometric error directly from a bounding volume +double computeGeometricError(const BoundingVolume& bv, double scale = DEFAULT_GEOMETRIC_ERROR_SCALE); + +// Calculate geometric error from box dimensions +double computeGeometricErrorFromBox(double width, double height, double depth, double scale = DEFAULT_GEOMETRIC_ERROR_SCALE); + +// Calculate geometric error for a tile based on its children's errors +// Parent error should be larger than children's errors +double computeParentGeometricError(const std::vector& childErrors, double multiplier = 2.0); + +// Compute geometric error from spans (used in shp23dtile) +double computeGeometricErrorFromSpans(double span_x, double span_y, double span_z, double scale = DEFAULT_GEOMETRIC_ERROR_SCALE); + +// LOD (Level of Detail) geometric error calculation +// For hierarchical LOD, coarser levels have larger geometric errors +struct LODLevel { + double target_ratio; // Mesh simplification ratio (1.0 = full detail, 0.5 = half detail) + double base_error; // Base geometric error for this level +}; + +// Calculate geometric errors for LOD levels +// Returns a vector of geometric errors, one per LOD level +std::vector computeLODGeometricErrors(const std::vector& levels, double base_diagonal); + +} // namespace tileset diff --git a/src/tileset/tileset.h b/src/tileset/tileset.h new file mode 100644 index 00000000..a74c8728 --- /dev/null +++ b/src/tileset/tileset.h @@ -0,0 +1,48 @@ +#pragma once + +// Main header for the tileset module +// Includes all components for 3D Tiles tileset.json generation + +#include "bounding_volume.h" +#include "geometric_error.h" +#include "tileset_types.h" +#include "tileset_writer.h" +#include "transform.h" + +/** + * @brief Tileset Module for 3D Tiles + * + * This module provides a unified interface for generating 3D Tiles tileset.json files. + * It consolidates the previously scattered tileset generation logic from: + * - shp23dtile (Shapefile to 3D Tiles) + * - osgb23dtile (OSGB to 3D Tiles) + * - fbx23dtile (FBX to 3D Tiles) + * + * Example usage: + * @code + * using namespace tileset; + * + * // Create a simple tileset + * Box box = Box::fromCenterAndHalfLengths(0, 0, 50, 100, 100, 50); + * Tile tile(box); + * tile.geometricError = computeGeometricError(box); + * tile.setContent("model.b3dm"); + * + * Tileset tileset(tile); + * tileset.setVersion("1.0"); + * tileset.setGltfUpAxis("Z"); + * + * // Write to file + * TilesetWriter writer; + * writer.writeToFile(tileset, "tileset.json"); + * @endcode + * + * @see https://github.com/CesiumGS/3d-tiles/tree/main/specification + */ + +namespace tileset { + +// Version of the tileset module +constexpr const char* TILESET_MODULE_VERSION = "1.0.0"; + +} // namespace tileset diff --git a/src/tileset/tileset_types.cpp b/src/tileset/tileset_types.cpp new file mode 100644 index 00000000..fe8ce525 --- /dev/null +++ b/src/tileset/tileset_types.cpp @@ -0,0 +1,95 @@ +#include "tileset_types.h" + +#include "geometric_error.h" + +#include +#include + +namespace tileset { + +void Tile::updateGeometricErrorFromChildren(double multiplier) { + if (children.empty()) { + return; + } + + std::vector childErrors; + childErrors.reserve(children.size()); + for (const auto& child : children) { + childErrors.push_back(child.geometricError); + } + + geometricError = computeParentGeometricError(childErrors, multiplier); +} + +bool Tile::computeBoundingVolumeFromChildren() { + if (children.empty()) { + return false; + } + + BoundingVolume merged = children[0].boundingVolume; + for (size_t i = 1; i < children.size(); ++i) { + auto result = mergeBoundingVolumes(merged, children[i].boundingVolume); + if (!result) { + return false; // Cannot merge different types + } + merged = *result; + } + + boundingVolume = merged; + return true; +} + +void Tileset::updateGeometricError() { + if (geometricError <= 0.0) { + geometricError = computeGeometricError(root.boundingVolume); + } +} + +// TileBuilder implementations + +Tile TileBuilder::createBoxTile(double cx, double cy, double cz, + double hx, double hy, double hz, + double geometricError) { + Box box = Box::fromCenterAndHalfLengths(cx, cy, cz, hx, hy, hz); + Tile tile(box); + tile.geometricError = geometricError; + return tile; +} + +Tile TileBuilder::createRegionTile(double west, double south, double east, double north, + double min_height, double max_height, + double geometricError) { + Region region = Region::fromDegrees(west, south, east, north, min_height, max_height); + Tile tile(region); + tile.geometricError = geometricError; + return tile; +} + +Tile TileBuilder::createQuadtreeTile(int level, int x, int y, int maxLevel, + const BoundingVolume& rootBv, + double baseGeometricError) { + // This is a simplified implementation + // In practice, you'd compute the actual bounds for each tile + + Tile tile; + tile.geometricError = baseGeometricError / std::pow(2.0, level); + + // For now, just use the root bounding volume + // A full implementation would subdivide the bounds + tile.boundingVolume = rootBv; + + // Create children if not at max level + if (level < maxLevel) { + for (int i = 0; i < 4; ++i) { + int childX = x * 2 + (i % 2); + int childY = y * 2 + (i / 2); + Tile child = createQuadtreeTile(level + 1, childX, childY, maxLevel, + rootBv, baseGeometricError); + tile.addChild(std::move(child)); + } + } + + return tile; +} + +} // namespace tileset diff --git a/src/tileset/tileset_types.h b/src/tileset/tileset_types.h new file mode 100644 index 00000000..a5a17437 --- /dev/null +++ b/src/tileset/tileset_types.h @@ -0,0 +1,126 @@ +#pragma once + +#include "bounding_volume.h" +#include "transform.h" + +#include +#include +#include +#include + +namespace tileset { + +// Forward declarations +class Tile; + +// Content object representing a tile's content +// https://github.com/CesiumGS/3d-tiles/tree/main/specification#content +struct Content { + std::string uri; + std::optional boundingVolume; // Optional content-specific bounding volume + + Content() = default; + explicit Content(const std::string& u) : uri(u) {} + Content(const std::string& u, const BoundingVolume& bv) : uri(u), boundingVolume(bv) {} +}; + +// Tile object in 3D Tiles +// https://github.com/CesiumGS/3d-tiles/tree/main/specification#tile +class Tile { +public: + // Required properties + BoundingVolume boundingVolume; + double geometricError = 0.0; + + // Optional properties + std::optional content; + std::vector children; + std::optional transform; + std::string refine = "REPLACE"; // "ADD" or "REPLACE" + std::optional viewerRequestVolume; + + // Metadata extensions (for 3D Tiles 1.1) + // std::optional metadata; + + // Constructors + Tile() = default; + explicit Tile(const BoundingVolume& bv) : boundingVolume(bv) {} + Tile(const BoundingVolume& bv, double ge) : boundingVolume(bv), geometricError(ge) {} + + // Convenience methods + void addChild(const Tile& child) { children.push_back(child); } + void addChild(Tile&& child) { children.push_back(std::move(child)); } + + void setContent(const std::string& uri) { content = Content(uri); } + void setContent(const std::string& uri, const BoundingVolume& bv) { content = Content(uri, bv); } + + void setTransform(const TransformMatrix& matrix) { transform = matrix; } + + bool isLeaf() const { return children.empty(); } + + // Update geometric error based on children's errors + void updateGeometricErrorFromChildren(double multiplier = 2.0); + + // Compute the bounding volume that contains all children + // Returns true if successful + bool computeBoundingVolumeFromChildren(); +}; + +// Asset object +// https://github.com/CesiumGS/3d-tiles/tree/main/specification#asset +struct Asset { + std::string version = "1.0"; + std::optional tilesetVersion; + std::optional gltfUpAxis = "Z"; + + Asset() = default; + explicit Asset(const std::string& v) : version(v) {} +}; + +// Tileset root object +// https://github.com/CesiumGS/3d-tiles/tree/main/specification#tileset +class Tileset { +public: + Asset asset; + Tile root; + double geometricError = 0.0; + + // Extensions (optional) + // std::vector extensionsUsed; + // std::vector extensionsRequired; + + // Constructors + Tileset() = default; + explicit Tileset(const Tile& r) : root(r) {} + Tileset(const Tile& r, double ge) : root(r), geometricError(ge) {} + + // Convenience method to set asset version + void setVersion(const std::string& version) { asset.version = version; } + void setGltfUpAxis(const std::string& axis) { asset.gltfUpAxis = axis; } + + // Calculate root geometric error from root tile if not set + void updateGeometricError(); +}; + +// Helper struct for building tile hierarchies +struct TileBuilder { + // Create a simple tile with box bounding volume + static Tile createBoxTile(double cx, double cy, double cz, + double hx, double hy, double hz, + double geometricError); + + // Create a tile with region bounding volume + static Tile createRegionTile(double west, double south, double east, double north, + double min_height, double max_height, + double geometricError); + + // Create a tile hierarchy from a quadtree structure + // level: current level in the tree + // x, y: tile coordinates + // maxLevel: maximum depth + static Tile createQuadtreeTile(int level, int x, int y, int maxLevel, + const BoundingVolume& rootBv, + double baseGeometricError); +}; + +} // namespace tileset diff --git a/src/tileset/tileset_writer.cpp b/src/tileset/tileset_writer.cpp new file mode 100644 index 00000000..fa42be3c --- /dev/null +++ b/src/tileset/tileset_writer.cpp @@ -0,0 +1,190 @@ +#include "tileset_writer.h" +#include "geometric_error.h" + +#include +#include + +namespace tileset { + +TilesetWriter::TilesetWriter() = default; + +TilesetWriter::TilesetWriter(const Options& opts) : options_(opts) {} + +std::string TilesetWriter::write(const Tileset& tileset) const { + nlohmann::json j = toJson(tileset); + if (options_.indent > 0) { + return j.dump(options_.indent); + } + return j.dump(); +} + +bool TilesetWriter::writeToFile(const Tileset& tileset, const std::string& filepath) const { + std::ofstream file(filepath); + if (!file.is_open()) { + return false; + } + file << write(tileset); + return file.good(); +} + +std::string TilesetWriter::writeTile(const Tile& tile) const { + nlohmann::json j = tileToJson(tile); + if (options_.indent > 0) { + return j.dump(options_.indent); + } + return j.dump(); +} + +nlohmann::json TilesetWriter::toJson(const Tileset& tileset) const { + nlohmann::json j; + + // Asset + j["asset"] = assetToJson(tileset.asset); + + // Geometric error + j["geometricError"] = tileset.geometricError > 0.0 ? tileset.geometricError + : computeGeometricError(tileset.root.boundingVolume); + + // Root tile + j["root"] = tileToJson(tileset.root); + + return j; +} + +nlohmann::json TilesetWriter::tileToJson(const Tile& tile) const { + nlohmann::json j; + + // Bounding volume (required) + j["boundingVolume"] = boundingVolumeToJson(tile.boundingVolume); + + // Geometric error (required) + j["geometricError"] = tile.geometricError; + + // Refine mode + if (!tile.refine.empty()) { + j["refine"] = tile.refine; + } + + // Transform (optional) + if (tile.transform) { + j["transform"] = transformToJson(*tile.transform); + } + + // Content (optional) + if (tile.content) { + j["content"] = contentToJson(*tile.content); + } + + // Viewer request volume (optional) + if (tile.viewerRequestVolume) { + j["viewerRequestVolume"] = boundingVolumeToJson(*tile.viewerRequestVolume); + } + + // Children (optional) + if (!tile.children.empty() || !options_.skipEmptyChildren) { + j["children"] = nlohmann::json::array(); + for (const auto& child : tile.children) { + j["children"].push_back(tileToJson(child)); + } + } + + return j; +} + +nlohmann::json TilesetWriter::assetToJson(const Asset& asset) const { + nlohmann::json j; + j["version"] = asset.version; + if (asset.tilesetVersion) { + j["tilesetVersion"] = *asset.tilesetVersion; + } + if (asset.gltfUpAxis) { + j["gltfUpAxis"] = *asset.gltfUpAxis; + } + return j; +} + +nlohmann::json TilesetWriter::boundingVolumeToJson(const BoundingVolume& bv) const { + nlohmann::json j; + + std::visit([&j](const auto& v) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + j["box"] = std::vector(v.values.begin(), v.values.end()); + } else if constexpr (std::is_same_v) { + j["region"] = {v.west, v.south, v.east, v.north, v.min_height, v.max_height}; + } else if constexpr (std::is_same_v) { + j["sphere"] = std::vector(v.values.begin(), v.values.end()); + } + }, bv); + + return j; +} + +nlohmann::json TilesetWriter::contentToJson(const Content& content) const { + nlohmann::json j; + j["uri"] = content.uri; + + if (options_.writeContentBoundingVolume && content.boundingVolume) { + j["boundingVolume"] = boundingVolumeToJson(*content.boundingVolume); + } + + return j; +} + +nlohmann::json TilesetWriter::transformToJson(const TransformMatrix& matrix) const { + // 3D Tiles expects column-major order (same as GLM) + return std::vector(matrix.begin(), matrix.end()); +} + +// Convenience functions + +std::string writeSimpleTileset(const BoundingVolume& bv, + double geometricError, + const std::string& contentUri, + const std::string& version) { + Tileset tileset; + tileset.asset.version = version; + tileset.asset.gltfUpAxis = "Z"; + tileset.geometricError = geometricError; + + Tile root(bv, geometricError); + root.setContent(contentUri); + tileset.root = std::move(root); + + TilesetWriter writer; + return writer.write(tileset); +} + +std::string writeTilesetWithTransform(const BoundingVolume& bv, + double geometricError, + const std::string& contentUri, + const TransformMatrix& transform, + const std::string& version) { + Tileset tileset; + tileset.asset.version = version; + tileset.asset.gltfUpAxis = "Z"; + tileset.geometricError = geometricError; + + Tile root(bv, geometricError); + root.setContent(contentUri); + root.setTransform(transform); + tileset.root = std::move(root); + + TilesetWriter writer; + return writer.write(tileset); +} + +std::string writeHierarchicalTileset(const Tile& rootTile, + double rootGeometricError, + const std::string& version) { + Tileset tileset; + tileset.asset.version = version; + tileset.asset.gltfUpAxis = "Z"; + tileset.geometricError = rootGeometricError; + tileset.root = rootTile; + + TilesetWriter writer; + return writer.write(tileset); +} + +} // namespace tileset diff --git a/src/tileset/tileset_writer.h b/src/tileset/tileset_writer.h new file mode 100644 index 00000000..4ad985c4 --- /dev/null +++ b/src/tileset/tileset_writer.h @@ -0,0 +1,72 @@ +#pragma once + +#include "tileset_types.h" + +#include +#include + +namespace tileset { + +// JSON writer for 3D Tiles tileset.json +// Uses nlohmann/json for JSON generation +class TilesetWriter { +public: + // Configuration options + struct Options { + int indent = 2; // JSON indentation (0 for compact) + bool writeContentBoundingVolume = false; // Write content.boundingVolume if available + bool skipEmptyChildren = true; // Skip "children": [] for leaf nodes + }; + + TilesetWriter(); + explicit TilesetWriter(const Options& opts); + + // Write a complete tileset to a JSON string + std::string write(const Tileset& tileset) const; + + // Write a tileset to a file + bool writeToFile(const Tileset& tileset, const std::string& filepath) const; + + // Write just a tile (for nested tilesets) + std::string writeTile(const Tile& tile) const; + + // Convert to nlohmann::json object (for further manipulation) + nlohmann::json toJson(const Tileset& tileset) const; + nlohmann::json tileToJson(const Tile& tile) const; + + // Set options + void setOptions(const Options& opts) { options_ = opts; } + const Options& getOptions() const { return options_; } + +private: + Options options_; + + // JSON conversion helpers + nlohmann::json assetToJson(const Asset& asset) const; + nlohmann::json boundingVolumeToJson(const BoundingVolume& bv) const; + nlohmann::json contentToJson(const Content& content) const; + nlohmann::json transformToJson(const TransformMatrix& matrix) const; +}; + +// Convenience functions for writing tilesets + +// Write a simple tileset with a single tile +std::string writeSimpleTileset(const BoundingVolume& bv, + double geometricError, + const std::string& contentUri, + const std::string& version = "1.0"); + +// Write a tileset with transform (for ENU to ECEF conversion) +std::string writeTilesetWithTransform(const BoundingVolume& bv, + double geometricError, + const std::string& contentUri, + const TransformMatrix& transform, + const std::string& version = "1.0"); + +// Write a hierarchical tileset from a tree structure +// The tree is defined by parent-child relationships in the tiles +std::string writeHierarchicalTileset(const Tile& rootTile, + double rootGeometricError, + const std::string& version = "1.0"); + +} // namespace tileset diff --git a/src/tileset/transform.cpp b/src/tileset/transform.cpp new file mode 100644 index 00000000..4fe2ae14 --- /dev/null +++ b/src/tileset/transform.cpp @@ -0,0 +1,151 @@ +#include "transform.h" + +#include + +namespace tileset { + +// Approximate conversion factors +// 1 degree latitude ≈ 111.32 km (varies slightly with latitude) +// 1 degree longitude ≈ 111.32 km * cos(latitude) +static constexpr double METERS_PER_DEGREE_LAT = 111320.0; + +double latitudeToMeters(double delta_degrees) { + return delta_degrees * METERS_PER_DEGREE_LAT; +} + +double longitudeToMeters(double delta_degrees, double latitude_deg) { + double lat_rad = degreesToRadians(latitude_deg); + return delta_degrees * METERS_PER_DEGREE_LAT * std::cos(lat_rad); +} + +double metersToLatitude(double meters) { + return meters / METERS_PER_DEGREE_LAT; +} + +double metersToLongitude(double meters, double latitude_deg) { + double lat_rad = degreesToRadians(latitude_deg); + return meters / (METERS_PER_DEGREE_LAT * std::cos(lat_rad)); +} + +TransformMatrix calcEnuToEcefMatrix(double lon_deg, double lat_deg, double height) { + double lon = degreesToRadians(lon_deg); + double lat = degreesToRadians(lat_deg); + + double sinLon = std::sin(lon); + double cosLon = std::cos(lon); + double sinLat = std::sin(lat); + double cosLat = std::cos(lat); + + // Calculate ECEF position of the origin + // Using WGS84 ellipsoid + double N = WGS84_A / std::sqrt(1.0 - WGS84_E2 * sinLat * sinLat); + double x0 = (N + height) * cosLat * cosLon; + double y0 = (N + height) * cosLat * sinLon; + double z0 = (N * (1.0 - WGS84_E2) + height) * sinLat; + + // ENU to ECEF rotation matrix (column-major for OpenGL/GLM compatibility) + // East axis (points in direction of increasing longitude) + // North axis (points in direction of increasing latitude) + // Up axis (points away from Earth's center) + TransformMatrix matrix = { + -sinLon, cosLon, 0.0, 0.0, // Column 0 (East direction in ECEF) + -sinLat * cosLon, -sinLat * sinLon, cosLat, 0.0, // Column 1 (North direction in ECEF) + cosLat * cosLon, cosLat * sinLon, sinLat, 0.0, // Column 2 (Up direction in ECEF) + x0, y0, z0, 1.0 // Column 3 (Translation) + }; + + return matrix; +} + +TransformMatrix calcEcefToEnuMatrix(double lon_deg, double lat_deg, double height) { + // ECEF to ENU is the transpose of ENU to ECEF (for rotation part) + // plus translation adjustment + double lon = degreesToRadians(lon_deg); + double lat = degreesToRadians(lat_deg); + + double sinLon = std::sin(lon); + double cosLon = std::cos(lon); + double sinLat = std::sin(lat); + double cosLat = std::cos(lat); + + // Calculate ECEF position of the origin + double N = WGS84_A / std::sqrt(1.0 - WGS84_E2 * sinLat * sinLat); + double x0 = (N + height) * cosLat * cosLon; + double y0 = (N + height) * cosLat * sinLon; + double z0 = (N * (1.0 - WGS84_E2) + height) * sinLat; + + // ECEF to ENU transformation + TransformMatrix matrix = { + -sinLon, -sinLat * cosLon, cosLat * cosLon, 0.0, + cosLon, -sinLat * sinLon, cosLat * sinLon, 0.0, + 0.0, cosLat, sinLat, 0.0, + 0.0, 0.0, 0.0, 1.0 + }; + + // Apply translation: subtract origin in ECEF, then rotate + // This is simplified - full implementation would properly handle the translation + matrix[12] = -(matrix[0] * x0 + matrix[4] * y0 + matrix[8] * z0); + matrix[13] = -(matrix[1] * x0 + matrix[5] * y0 + matrix[9] * z0); + matrix[14] = -(matrix[2] * x0 + matrix[6] * y0 + matrix[10] * z0); + + return matrix; +} + +TransformMatrix applyEnuTranslation(const TransformMatrix& base, double tx, double ty, double tz) { + TransformMatrix result = base; + + // Apply translation in ENU frame to the ECEF transformation + // The translation (tx, ty, tz) in ENU needs to be converted to ECEF + // and added to the translation component of the matrix + + // ENU axes in ECEF are the first three columns of the matrix + double ecef_tx = tx * base[0] + ty * base[4] + tz * base[8]; + double ecef_ty = tx * base[1] + ty * base[5] + tz * base[9]; + double ecef_tz = tx * base[2] + ty * base[6] + tz * base[10]; + + result[12] += ecef_tx; + result[13] += ecef_ty; + result[14] += ecef_tz; + + return result; +} + +std::array getTranslation(const TransformMatrix& matrix) { + return {matrix[12], matrix[13], matrix[14]}; +} + +std::array, 4> toNestedArray(const TransformMatrix& m) { + return {{ + {m[0], m[1], m[2], m[3]}, + {m[4], m[5], m[6], m[7]}, + {m[8], m[9], m[10], m[11]}, + {m[12], m[13], m[14], m[15]} + }}; +} + +TransformMatrix multiplyMatrices(const TransformMatrix& a, const TransformMatrix& b) { + TransformMatrix result{}; + + for (int col = 0; col < 4; ++col) { + for (int row = 0; row < 4; ++row) { + double sum = 0.0; + for (int k = 0; k < 4; ++k) { + sum += a[k * 4 + row] * b[col * 4 + k]; + } + result[col * 4 + row] = sum; + } + } + + return result; +} + +TransformMatrix identityMatrix() { + return { + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0 + }; +} + +} // namespace tileset diff --git a/src/tileset/transform.h b/src/tileset/transform.h new file mode 100644 index 00000000..bc302846 --- /dev/null +++ b/src/tileset/transform.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +namespace tileset { + +// Coordinate conversion utilities +// Reference: https://github.com/CesiumGS/3d-tiles/tree/main/specification#coordinate-reference-system-crs + +// Constants +constexpr double PI = std::numbers::pi; +constexpr double DEG_TO_RAD = PI / 180.0; +constexpr double RAD_TO_DEG = 180.0 / PI; + +// WGS84 ellipsoid parameters +constexpr double WGS84_A = 6378137.0; // Semi-major axis (meters) +constexpr double WGS84_B = 6356752.3142451793; // Semi-minor axis (meters) +constexpr double WGS84_E2 = 0.00669437999013; // First eccentricity squared + +// Coordinate conversions +inline double degreesToRadians(double degrees) { + return degrees * DEG_TO_RAD; +} + +inline double radiansToDegrees(double radians) { + return radians * RAD_TO_DEG; +} + +// Convert latitude difference to meters +// 1 degree latitude ≈ 111.32 km at equator +double latitudeToMeters(double delta_degrees); + +// Convert longitude difference to meters at a given latitude +double longitudeToMeters(double delta_degrees, double latitude_deg); + +// Convert meters to latitude difference +double metersToLatitude(double meters); + +// Convert meters to longitude difference at a given latitude +double metersToLongitude(double meters, double latitude_deg); + +// ENU (East-North-Up) to ECEF (Earth-Centered-Earth-Fixed) transformation matrix +// This is a 4x4 column-major matrix represented as a flat array of 16 doubles +using TransformMatrix = std::array; + +// Calculate ENU to ECEF transform matrix at a given geodetic position +// lon_deg, lat_deg: longitude and latitude in degrees +// height: ellipsoidal height in meters +TransformMatrix calcEnuToEcefMatrix(double lon_deg, double lat_deg, double height); + +// Calculate ECEF to ENU transform matrix (inverse of ENU to ECEF) +TransformMatrix calcEcefToEnuMatrix(double lon_deg, double lat_deg, double height); + +// Apply translation to a transform matrix +// The translation is applied in the local ENU frame +TransformMatrix applyEnuTranslation(const TransformMatrix& base, double tx, double ty, double tz); + +// Extract translation component from transform matrix +std::array getTranslation(const TransformMatrix& matrix); + +// Convert flat array to nested array for JSON output +std::array, 4> toNestedArray(const TransformMatrix& m); + +// Matrix multiplication +TransformMatrix multiplyMatrices(const TransformMatrix& a, const TransformMatrix& b); + +// Identity matrix +TransformMatrix identityMatrix(); + +} // namespace tileset diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 00000000..cfc7614c --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,66 @@ +//! 通用工具函数 +//! +//! 提供路径处理、字符串转换等辅助函数 + +#![allow(dead_code)] + +use crate::error::{TileError, TileResult}; +use std::ffi::CString; +use std::path::{Path, PathBuf}; + +/// 获取可执行文件所在目录 +pub fn get_exe_dir() -> TileResult { + std::env::current_exe() + .map_err(TileError::Io)? + .parent() + .map(|p| p.to_path_buf()) + .ok_or_else(|| TileError::InvalidPath { + path: "Could not get executable directory".to_string(), + }) +} + +/// 获取GDAL数据路径 +pub fn get_gdal_data_path() -> TileResult { + let exe_dir = get_exe_dir()?; + let gdal_path = exe_dir.join("gdal"); + path_to_string(&gdal_path) +} + +/// 获取PROJ数据路径 +pub fn get_proj_data_path() -> TileResult { + let exe_dir = get_exe_dir()?; + let proj_path = exe_dir.join("proj"); + path_to_string(&proj_path) +} + +/// 将Path转换为字符串 +pub fn path_to_string(path: &Path) -> TileResult { + path.to_str() + .map(|s| s.to_string()) + .ok_or_else(|| TileError::InvalidUtf8 { + path: path.to_path_buf(), + }) +} + +/// 将字符串转换为CString(检查null字节) +pub fn string_to_cstring(s: impl Into) -> TileResult { + let s = s.into(); + CString::new(s.clone()).map_err(|_| TileError::InvalidPath { path: s }) +} + +/// 解析f64的辅助函数 +pub fn parse_f64(s: &str) -> TileResult { + s.parse::() + .map_err(|_| TileError::InvalidNumber { value: s.to_string() }) +} + +/// 解析i32的辅助函数 +pub fn parse_i32(s: &str) -> TileResult { + s.parse::() + .map_err(|_| TileError::InvalidNumber { value: s.to_string() }) +} + +/// 安全地解析多个f64值 +pub fn parse_f64_vec<'a>(items: impl Iterator) -> TileResult> { + items.map(parse_f64).collect() +} diff --git a/src/utils/file_utils.cpp b/src/utils/file_utils.cpp new file mode 100644 index 00000000..ebcc3a05 --- /dev/null +++ b/src/utils/file_utils.cpp @@ -0,0 +1,22 @@ +/** + * @file file_utils.cpp + * @brief 文件操作工具实现 + */ + +#include "file_utils.h" + +// 使用 Rust 实现的 FFI 函数 +extern "C" bool mkdirs(const char* path); +extern "C" bool write_file(const char* filename, const char* buf, unsigned long buf_len); + +namespace utils { + +bool mkdirs(const char* path) { + return ::mkdirs(path); +} + +bool write_file(const char* filename, const char* buf, unsigned long buf_len) { + return ::write_file(filename, buf, buf_len); +} + +} // namespace utils diff --git a/src/utils/file_utils.h b/src/utils/file_utils.h new file mode 100644 index 00000000..1d75a0ca --- /dev/null +++ b/src/utils/file_utils.h @@ -0,0 +1,27 @@ +#pragma once + +/** + * @file file_utils.h + * @brief 文件操作工具头文件 + * + * 提供文件系统相关工具函数,替代 extern.h 中的文件操作功能 + */ + +#include + +namespace utils { + +// 创建目录(递归) +// 返回 true 表示成功或目录已存在 +bool mkdirs(const char* path); + +// 写入文件 +// 返回 true 表示成功 +bool write_file(const char* filename, const char* buf, unsigned long buf_len); + +// C++ 字符串重载 +inline bool write_file(const std::string& filename, const std::string& content) { + return write_file(filename.c_str(), content.c_str(), content.size()); +} + +} // namespace utils diff --git a/src/utils/log.h b/src/utils/log.h new file mode 100644 index 00000000..a2c958e0 --- /dev/null +++ b/src/utils/log.h @@ -0,0 +1,49 @@ +#pragma once + +/** + * @file log.h + * @brief 日志工具头文件 + * + * 提供统一的日志宏定义,替代 extern.h 中的日志功能 + * 基于 spdlog 实现 + */ + +#include +#include +#include +#include + +namespace utils { + +// 内部实现函数 +inline void log_printf_impl(spdlog::level::level_enum lvl, const char* format, ...) { + char buf[1024]; + va_list args; + va_start(args, format); + std::vsnprintf(buf, sizeof(buf), format, args); + va_end(args); + spdlog::log(lvl, "{}", buf); +} + +} // namespace utils + +// 日志宏定义 +#define LOG_D(format, ...) \ + do { \ + utils::log_printf_impl(spdlog::level::debug, format __VA_OPT__(,) __VA_ARGS__); \ + } while (0) + +#define LOG_I(format, ...) \ + do { \ + utils::log_printf_impl(spdlog::level::info, format __VA_OPT__(,) __VA_ARGS__); \ + } while (0) + +#define LOG_W(format, ...) \ + do { \ + utils::log_printf_impl(spdlog::level::warn, format __VA_OPT__(,) __VA_ARGS__); \ + } while (0) + +#define LOG_E(format, ...) \ + do { \ + utils::log_printf_impl(spdlog::level::err, format __VA_OPT__(,) __VA_ARGS__); \ + } while (0) diff --git a/tests/e2e/3dtiles-viewer/public/output2/tileset.json b/tests/e2e/3dtiles-viewer/public/output2/tileset.json deleted file mode 100644 index 4838eeb4..00000000 --- a/tests/e2e/3dtiles-viewer/public/output2/tileset.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "asset": { - "gltfUpAxis": "Z", - "version": "1.0" - }, - "geometricError": 4212.76, - "root": { - "boundingVolume": { - "box": [ - 6828.3609619140625, - 7010.778344726563, - 517.2710510253906, - 89.23078613281268, - 0.0, - 0.0, - 0.0, - 89.2310791015625, - 0.0, - 0.0, - 0.0, - 25.203240966796898 - ] - }, - "children": [ - { - "boundingVolume": { - "box": [ - 6868.065918, - 7051.073975, - 512.243759, - 40.778809, - 0.0, - 0.0, - 0.0, - 40.779541, - 0.0, - 0.0, - 0.0, - 6.092422 - ] - }, - "content": { - "uri": ".//Data/Tile_+086_+087/tileset.json" - }, - "geometricError": 2106.38 - }, - { - "boundingVolume": { - "box": [ - 6868.657471, - 6971.075195, - 513.142548, - 40.778564, - 0.0, - 0.0, - 0.0, - 40.778809, - 0.0, - 0.0, - 0.0, - 7.906891 - ] - }, - "content": { - "uri": ".//Data/Tile_+086_+086/tileset.json" - }, - "geometricError": 1970.41 - }, - { - "boundingVolume": { - "box": [ - 6788.065918, - 7050.482666, - 522.259155, - 40.779785, - 0.0, - 0.0, - 0.0, - 40.779053, - 0.0, - 0.0, - 0.0, - 16.845947 - ] - }, - "content": { - "uri": ".//Data/Tile_+085_+087/tileset.json" - }, - "geometricError": 2080.12 - }, - { - "boundingVolume": { - "box": [ - 6788.660156, - 6970.482422, - 507.059387, - 40.778809, - 0.0, - 0.0, - 0.0, - 40.779297, - 0.0, - 0.0, - 0.0, - 12.492981 - ] - }, - "content": { - "uri": ".//Data/Tile_+085_+086/tileset.json" - }, - "geometricError": 2001.22 - } - ], - "geometricError": 4212.76, - "transform": [ - -0.9695892469479287, - -0.2447380072709357, - 0.0, - 0.0, - 0.12435209128718391, - -0.492651108391448, - 0.8612963733774697, - 0.0, - -0.21079195809008572, - 0.8351037020620429, - 0.5081029002149253, - 0.0, - -1345623.2958987127, - 5331014.551825048, - 3221840.418946778, - 1.0 - ] - } -} \ No newline at end of file diff --git a/tests/e2e/3dtiles-viewer/src/composables/useViewer3D.ts b/tests/e2e/3dtiles-viewer/src/composables/useViewer3D.ts index b54a2e7b..2728660e 100644 --- a/tests/e2e/3dtiles-viewer/src/composables/useViewer3D.ts +++ b/tests/e2e/3dtiles-viewer/src/composables/useViewer3D.ts @@ -36,20 +36,21 @@ export function useViewer3D() { viewer.value.scene.logarithmicDepthBuffer = true; viewer.value.camera.frustum.near = 0.1; - if (config.terrain !== false) { - try { - const terrainProvider = await Cesium.createWorldTerrainAsync({ - requestVertexNormals: true, - requestWaterMask: true - }); - viewer.value.terrainProvider = terrainProvider; - console.log('Terrain loaded successfully'); - } catch (error) { - console.warn('Failed to load terrain:', error); - } - } + // if (config.terrain !== false) { + // try { + // const terrainProvider = await Cesium.createWorldTerrainAsync({ + // requestVertexNormals: true, + // requestWaterMask: true + // }); + // viewer.value.terrainProvider = terrainProvider; + // console.log('Terrain loaded successfully'); + // } catch (error) { + // console.warn('Failed to load terrain:', error); + // } + // } viewer.value.scene.globe.depthTestAgainstTerrain = false; + viewer.value.shadows = false; isReady.value = true; } diff --git a/tests/e2e/3dtiles-viewer/vite.config.ts b/tests/e2e/3dtiles-viewer/vite.config.ts index 2a40fdf8..0b62c169 100644 --- a/tests/e2e/3dtiles-viewer/vite.config.ts +++ b/tests/e2e/3dtiles-viewer/vite.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ }, }, server: { - port: 5173, + port: 5174, open: true, fs: { allow: ['..']