diff --git a/.github/workflows/c-bindings.yml b/.github/workflows/c-bindings.yml new file mode 100644 index 000000000..df4ebcc5b --- /dev/null +++ b/.github/workflows/c-bindings.yml @@ -0,0 +1,81 @@ +name: C bindings +on: + push: + branches: [master] + paths: ['c/**', 'src/**', 'crates/rapier*/**', 'Cargo.toml', '.github/workflows/c-bindings.yml'] + pull_request: + branches: [master] + paths: ['c/**', 'src/**', 'crates/rapier*/**', 'Cargo.toml', '.github/workflows/c-bindings.yml'] + workflow_dispatch: +jobs: + native: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + variant: + - {dimension: 2, parallel: "OFF"} + - {dimension: 3, parallel: "ON"} + precision: [32, 64] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Linux graphics build prerequisites + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev + - name: Configure + run: cmake -S c -B build/c -DRAPIER_DIMENSION=${{ matrix.variant.dimension }} -DRAPIER_PRECISION=${{ matrix.precision }} -DRAPIER_ENABLE_PARALLEL=${{ matrix.variant.parallel }} -DRAPIER_BUILD_TESTBED=ON -DRAPIER_PROFILE=debug -DRAPIER_CARGO_TARGET_DIR=${{ github.workspace }}/target + - name: Build + run: cmake --build build/c --config Release --parallel 2 + - name: Test + run: ctest --test-dir build/c -C Release --output-on-failure + - name: Install + run: cmake --install build/c --config Release --prefix build/install + - name: Consume installed package + run: | + cmake -S c/tests/consumer -B build/consumer -DCMAKE_PREFIX_PATH=${{ github.workspace }}/build/install + cmake --build build/consumer --config Release + ctest --test-dir build/consumer -C Release --output-on-failure + abi-and-features: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo install cbindgen --version 0.29.4 --locked + - name: Header matches source + run: | + sh c/tools/generate-header.sh + git diff --exit-code -- c/include/rapier.h + - run: cargo fmt -p rapier-c-macros -p rapier3d-ffi -- --check + - run: cargo test -p rapier-c-macros -p rapier2d-ffi -p rapier3d-ffi -p rapier2d-f64-ffi -p rapier3d-f64-ffi + - run: cargo clippy -p rapier-c-macros -p rapier2d-ffi -p rapier3d-ffi -p rapier2d-f64-ffi -p rapier3d-f64-ffi --no-deps -- -D warnings + - name: Every ABI symbol and both linkage modes + run: python3 c/tools/test-native.py + - name: Optional feature combinations + run: cargo check -p rapier2d-ffi -p rapier3d-ffi -p rapier2d-f64-ffi -p rapier3d-f64-ffi --features parallel,fem,enhanced-determinism + - name: Eight-lane SIMD with parallel execution + run: | + cmake -S c -B build/simd8 -DRAPIER_BUILD_TESTBED=ON -DRAPIER_TESTBED_GRAPHICS=OFF -DRAPIER_PROFILE=debug -DRAPIER_SIMD_LANES=8 -DRAPIER_ENABLE_PARALLEL=ON -DRAPIER_CARGO_TARGET_DIR=${{ github.workspace }}/target + cmake --build build/simd8 --parallel 2 + ctest --test-dir build/simd8 --output-on-failure + + documentation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install documentation tools + run: sudo apt-get update && sudo apt-get install -y doxygen + - name: Generate and validate all API variants + run: | + cmake -S c/doxygen -B build/c-docs + cmake --build build/c-docs --target rapier_docs --parallel 2 + - name: Upload HTML reference + uses: actions/upload-artifact@v4 + with: + name: rapier-c-api-docs + path: build/c-docs/html diff --git a/Cargo.toml b/Cargo.toml index 6b54e9610..48cd43870 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,11 @@ [workspace] members = [ + "c/testbed/tools/logo-mesh", + "c/rapier-c-macros", + "c/rapier2d-ffi", + "c/rapier3d-ffi", + "c/rapier2d-f64-ffi", + "c/rapier3d-f64-ffi", "crates/rapier2d", "crates/rapier2d-f64", "crates/rapier_testbed2d", @@ -21,7 +27,8 @@ members = [ # pure-Rust crates. The Python-binding crates need a Python build env # (pyo3 `extension-module`) and are exercised by the dedicated # `.github/workflows/python-bindings.yml` workflow via explicit -# `-p rapier-py-*` invocations. +# `-p rapier-py-*` invocations. C ABI crates are tested separately by +# `.github/workflows/c-bindings.yml`, including native C/C++ consumers. default-members = [ "crates/rapier2d", "crates/rapier2d-f64", diff --git a/README.md b/README.md index 40ab5ffe8..67c247978 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,11 @@ also enable: rapier3d = { version = "*", features = ["parallel"] } ``` +## C and C++ bindings + +See [`c/README.md`](c/README.md) for the C ABI, C++ ownership helpers, native build instructions, +and Unity/Unreal integration guidance. The bindings cover 2D/3D and f32/f64, including soft bodies. + ## Python bindings Python bindings are under development. They ship as four PyPI packages — diff --git a/c/CMakeLists.txt b/c/CMakeLists.txt new file mode 100644 index 000000000..dcc26e74f --- /dev/null +++ b/c/CMakeLists.txt @@ -0,0 +1,206 @@ +cmake_minimum_required(VERSION 3.20) +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" RAPIER_C_VERSION) +string(STRIP "${RAPIER_C_VERSION}" RAPIER_C_VERSION) +if(NOT RAPIER_C_VERSION MATCHES "^([0-9]+[.][0-9]+[.][0-9]+)[+]c[.](0|[1-9][0-9]*)$") + message(FATAL_ERROR "VERSION must be +c.") +endif() +# CMake package version comparisons use the numeric Rust version only. +project(RapierC VERSION ${CMAKE_MATCH_1} LANGUAGES C CXX) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION") +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) +set(RAPIER_DIMENSION "3" CACHE STRING "2 or 3") +set_property(CACHE RAPIER_DIMENSION PROPERTY STRINGS 2 3) +set(RAPIER_PRECISION "32" CACHE STRING "32 or 64") +set_property(CACHE RAPIER_PRECISION PROPERTY STRINGS 32 64) +option(RAPIER_SHARED "Link the shared library (otherwise static)" ON) +option(RAPIER_BUILD_DOCS "Enable the optional Doxygen API reference target" OFF) +if(RAPIER_BUILD_DOCS) + add_subdirectory(doxygen) +endif() +option(RAPIER_BUILD_TESTS "Build C/C++ integration tests" ON) +option(RAPIER_BUILD_EXAMPLES "Build the falling-ball example" ON) +option(RAPIER_BUILD_TESTBED "Build the C example testbed" OFF) +option(RAPIER_TESTBED_GRAPHICS "Include the raylib viewer (fetches graphics dependencies)" ON) +set(RAPIER_PROFILE "release" CACHE STRING "Cargo release or debug profile") +set_property(CACHE RAPIER_PROFILE PROPERTY STRINGS release debug) +set(RAPIER_FEATURES "" CACHE STRING "Additional Cargo features: enhanced-determinism,fem,profiler,robotics") +# Accept the earlier parallel/simd8 feature spelling as defaults, while the +# explicit options below determine the final effective feature set. +set(RAPIER_DEFAULT_PARALLEL "${RAPIER_BUILD_TESTBED}") +set(RAPIER_DEFAULT_SIMD_LANES "4") +if(RAPIER_FEATURES MATCHES "(^|,)parallel(,|$)") + set(RAPIER_DEFAULT_PARALLEL ON) +endif() +if(RAPIER_FEATURES MATCHES "(^|,)simd8(,|$)") + set(RAPIER_DEFAULT_SIMD_LANES "8") +endif() +option(RAPIER_ENABLE_PARALLEL "Enable Rapier parallel execution and thread-pool control" ${RAPIER_DEFAULT_PARALLEL}) +set(RAPIER_SIMD_LANES "${RAPIER_DEFAULT_SIMD_LANES}" CACHE STRING "Solver SIMD width: 4 or 8 (8 requires f32)") +set_property(CACHE RAPIER_SIMD_LANES PROPERTY STRINGS 4 8) +if(NOT RAPIER_SIMD_LANES MATCHES "^(4|8)$") + message(FATAL_ERROR "RAPIER_SIMD_LANES must be 4 or 8; this Rapier branch has no scalar solver build") +endif() +if(RAPIER_SIMD_LANES STREQUAL "8" AND RAPIER_PRECISION STREQUAL "64") + message(FATAL_ERROR "8-lane SIMD requires RAPIER_PRECISION=32; f64 uses 4 lanes") +endif() +if(RAPIER_SIMD_LANES STREQUAL "8" AND RAPIER_FEATURES MATCHES "(^|,)enhanced-determinism(,|$)") + message(FATAL_ERROR "8-lane SIMD is incompatible with enhanced-determinism; use RAPIER_SIMD_LANES=4") +endif() +string(REPLACE "," ";" RAPIER_FEATURE_LIST "${RAPIER_FEATURES}") +list(REMOVE_ITEM RAPIER_FEATURE_LIST parallel simd8) +if(RAPIER_ENABLE_PARALLEL) + list(APPEND RAPIER_FEATURE_LIST parallel) +endif() +if(RAPIER_SIMD_LANES STREQUAL "8") + list(APPEND RAPIER_FEATURE_LIST simd8) +endif() +if(RAPIER_BUILD_TESTBED) + list(APPEND RAPIER_FEATURE_LIST profiler) +endif() +list(REMOVE_DUPLICATES RAPIER_FEATURE_LIST) +list(JOIN RAPIER_FEATURE_LIST "," RAPIER_EFFECTIVE_FEATURES) +message(STATUS "Rapier physics: ${RAPIER_PROFILE}; SIMD ${RAPIER_SIMD_LANES} lanes; parallel ${RAPIER_ENABLE_PARALLEL}") +set(RAPIER_TARGET "" CACHE STRING "Optional Rust target triple for cross compilation") +if(NOT RAPIER_DIMENSION MATCHES "^[23]$" OR NOT RAPIER_PRECISION MATCHES "^(32|64)$") + message(FATAL_ERROR "RAPIER_DIMENSION must be 2/3 and RAPIER_PRECISION must be 32/64") +endif() +if(NOT RAPIER_PROFILE MATCHES "^(release|debug)$") + message(FATAL_ERROR "RAPIER_PROFILE must be release/debug") +endif() +find_program(CARGO_EXECUTABLE cargo REQUIRED) +get_filename_component(RAPIER_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE) +set(RAPIER_CRATE "rapier${RAPIER_DIMENSION}d") +if(RAPIER_PRECISION STREQUAL "64") + string(APPEND RAPIER_CRATE "-f64") +endif() +string(APPEND RAPIER_CRATE "-ffi") +string(REPLACE "-" "_" RAPIER_LIBNAME "${RAPIER_CRATE}") +set(RAPIER_CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargo" CACHE PATH "Cargo target directory") +set(RAPIER_CARGO_ARGS build --manifest-path "${RAPIER_ROOT}/Cargo.toml" -p "${RAPIER_CRATE}" --target-dir "${RAPIER_CARGO_TARGET_DIR}") +set(RAPIER_OUTPUT "${RAPIER_CARGO_TARGET_DIR}") +if(RAPIER_TARGET) + list(APPEND RAPIER_CARGO_ARGS --target "${RAPIER_TARGET}") + string(APPEND RAPIER_OUTPUT "/${RAPIER_TARGET}") +endif() +if(RAPIER_PROFILE STREQUAL "release") + list(APPEND RAPIER_CARGO_ARGS --release) +endif() +if(RAPIER_EFFECTIVE_FEATURES) + list(APPEND RAPIER_CARGO_ARGS --features "${RAPIER_EFFECTIVE_FEATURES}") +endif() +string(APPEND RAPIER_OUTPUT "/${RAPIER_PROFILE}") +set(RAPIER_DEFINITIONS "RAPIER_DIM${RAPIER_DIMENSION};RAPIER_F${RAPIER_PRECISION}") +if(RAPIER_EFFECTIVE_FEATURES MATCHES "(^|,)fem(,|$)") + list(APPEND RAPIER_DEFINITIONS RAPIER_FEM) +endif() +if(RAPIER_EFFECTIVE_FEATURES MATCHES "(^|,)robotics(,|$)") + if(NOT RAPIER_DIMENSION STREQUAL "3" OR NOT RAPIER_PRECISION STREQUAL "32") + message(FATAL_ERROR "The native URDF/MJCF importers currently support only 3D f32") + endif() + list(APPEND RAPIER_DEFINITIONS RAPIER_ROBOTICS) +endif() +if(RAPIER_ENABLE_PARALLEL) + list(APPEND RAPIER_DEFINITIONS RAPIER_PARALLEL) +endif() +set(RAPIER_SYSTEM_LIBS "") +if(UNIX) + list(APPEND RAPIER_SYSTEM_LIBS m) # Inline functions in rapier_math.h. +endif() +if(RAPIER_SHARED) + set(RAPIER_KIND SHARED) + if(WIN32) + set(RAPIER_FILENAME "${RAPIER_LIBNAME}.dll") + set(RAPIER_IMPLIB "${RAPIER_LIBNAME}.dll.lib") + elseif(APPLE) + set(RAPIER_FILENAME "lib${RAPIER_LIBNAME}.dylib") + else() + set(RAPIER_FILENAME "lib${RAPIER_LIBNAME}.so") + endif() +else() + set(RAPIER_KIND STATIC) + list(APPEND RAPIER_DEFINITIONS RAPIER_STATIC) + if(WIN32) + set(RAPIER_FILENAME "${RAPIER_LIBNAME}.lib") + set(RAPIER_SYSTEM_LIBS ws2_32 userenv bcrypt ntdll advapi32) + else() + set(RAPIER_FILENAME "lib${RAPIER_LIBNAME}.a") + find_package(Threads REQUIRED) + set(RAPIER_SYSTEM_LIBS Threads::Threads "${CMAKE_DL_LIBS}" m) + if(APPLE) + list(APPEND RAPIER_SYSTEM_LIBS "-framework Security" "-framework CoreFoundation") + endif() + endif() +endif() +set(RAPIER_LIBRARY "${RAPIER_OUTPUT}/${RAPIER_FILENAME}") +set(RAPIER_BYPRODUCTS "${RAPIER_LIBRARY}") +if(RAPIER_IMPLIB) + list(APPEND RAPIER_BYPRODUCTS "${RAPIER_OUTPUT}/${RAPIER_IMPLIB}") +endif() +add_custom_target(rapier_cargo_build + COMMAND "${CARGO_EXECUTABLE}" ${RAPIER_CARGO_ARGS} + WORKING_DIRECTORY "${RAPIER_ROOT}" + BYPRODUCTS ${RAPIER_BYPRODUCTS} + VERBATIM USES_TERMINAL) +add_library(rapier_native ${RAPIER_KIND} IMPORTED GLOBAL) +add_library(Rapier::rapier ALIAS rapier_native) +set_target_properties(rapier_native PROPERTIES + IMPORTED_LOCATION "${RAPIER_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include" + INTERFACE_COMPILE_DEFINITIONS "${RAPIER_DEFINITIONS}" + INTERFACE_LINK_LIBRARIES "${RAPIER_SYSTEM_LIBS}") +if(RAPIER_IMPLIB) + set_property(TARGET rapier_native PROPERTY IMPORTED_IMPLIB "${RAPIER_OUTPUT}/${RAPIER_IMPLIB}") +endif() +if(APPLE AND RAPIER_SHARED) + set_property(TARGET rapier_native PROPERTY IMPORTED_SONAME "@rpath/${RAPIER_FILENAME}") +endif() +add_dependencies(rapier_native rapier_cargo_build) +function(rapier_executable name source) + add_executable(${name} ${source}) + target_link_libraries(${name} PRIVATE Rapier::rapier) + target_compile_features(${name} PRIVATE c_std_11 cxx_std_17) + if(MSVC) + target_compile_options(${name} PRIVATE /W4 /WX /UNDEBUG) + else() + target_compile_options(${name} PRIVATE -Wall -Wextra -Werror -UNDEBUG) + endif() + if(WIN32 AND RAPIER_SHARED) + add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${RAPIER_LIBRARY}" "$" VERBATIM) + endif() +endfunction() +if(RAPIER_BUILD_TESTS) + enable_testing() + rapier_executable(rapier_c_integration tests/integration.c) + target_compile_definitions(rapier_c_integration PRIVATE RAPIER_EXPECTED_VERSION="${RAPIER_C_VERSION}" RAPIER_EXPECTED_PROFILE="${RAPIER_PROFILE}" RAPIER_EXPECTED_SIMD_LANES=${RAPIER_SIMD_LANES}) + rapier_executable(rapier_cpp_integration tests/cpp.cpp) + rapier_executable(rapier_pod_integration tests/pod.c) + rapier_executable(rapier_handle_integration tests/handles.c) + rapier_executable(rapier_initializer_integration tests/initializers.c) + add_test(NAME initializer_abi COMMAND rapier_initializer_integration) + rapier_executable(rapier_array_view_integration tests/array_views.c) + add_test(NAME array_view_abi COMMAND rapier_array_view_integration) + add_test(NAME handle_abi COMMAND rapier_handle_integration) + add_test(NAME pod_abi COMMAND rapier_pod_integration) + add_test(NAME c_abi COMMAND rapier_c_integration) + add_test(NAME cpp_abi COMMAND rapier_cpp_integration) +endif() +if(RAPIER_BUILD_EXAMPLES) + rapier_executable(rapier_falling_ball examples/falling_ball.c) +endif() +if(WIN32 AND RAPIER_SHARED) + set(RAPIER_INSTALL_LIBRARY_DIR "${CMAKE_INSTALL_BINDIR}") + install(FILES "${RAPIER_OUTPUT}/${RAPIER_IMPLIB}" DESTINATION "${CMAKE_INSTALL_LIBDIR}") +else() + set(RAPIER_INSTALL_LIBRARY_DIR "${CMAKE_INSTALL_LIBDIR}") +endif() +install(FILES "${RAPIER_LIBRARY}" DESTINATION "${RAPIER_INSTALL_LIBRARY_DIR}") +install(FILES include/rapier.h include/rapier.hpp include/rapier_math.h include/rapier_helpers.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") +set(RAPIER_INSTALL_NAME "@rpath/${RAPIER_FILENAME}") +configure_package_config_file(cmake/RapierConfig.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/RapierConfig.cmake" INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/Rapier") +write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/RapierConfigVersion.cmake" VERSION ${PROJECT_VERSION} COMPATIBILITY ExactVersion) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/RapierConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/RapierConfigVersion.cmake" DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/Rapier") + +if(RAPIER_BUILD_TESTBED) + add_subdirectory(testbed) +endif() diff --git a/c/README.md b/c/README.md new file mode 100644 index 000000000..8efdd7140 --- /dev/null +++ b/c/README.md @@ -0,0 +1,102 @@ +# Rapier C bindings + +C11 bindings for Rapier, with optional C++17 helpers, 2D/3D support, and single or +double precision. + +You need Rust/Cargo, a C/C++ compiler, and CMake 3.25+. Run the commands below from +the repository root. + +## Build the library + +```sh +cmake -S c -B build/c -DCMAKE_BUILD_TYPE=Release +cmake --build build/c --config Release --parallel +``` + +This builds the 3D, single-precision shared library, a small C example, and tests. +Both the Rust library and C/C++ code use release mode. CMake runs Cargo for you; +the library is under `build/c/cargo/release/`. + +Add these options to the configuration command as needed: + +| Option | Purpose | +| --- | --- | +| `-DRAPIER_DIMENSION=2` | Build 2D instead of 3D. | +| `-DRAPIER_PRECISION=64` | Use double precision instead of single precision. | +| `-DRAPIER_SHARED=OFF` | Link the static library. | +| `-DRAPIER_ENABLE_PARALLEL=ON` | Enable multithreaded physics; on by default for the testbed. | +| `-DRAPIER_SIMD_LANES=8` | Use eight SIMD lanes instead of four; requires single precision and excludes `enhanced-determinism`. | +| `-DRAPIER_FEATURES=fem,robotics` | Enable optional features; robotics requires 3D/single precision. | + +Use separate build directories for different configurations. For debug builds, +set both `-DRAPIER_PROFILE=debug` and `-DCMAKE_BUILD_TYPE=Debug`, then build with +`--config Debug`. + +## Install and use from CMake + +```sh +cmake --install build/c --config Release --prefix /path/to/rapier-sdk +``` + +Configure your application with `-DCMAKE_PREFIX_PATH=/path/to/rapier-sdk`, then link: + +```cmake +find_package(Rapier CONFIG REQUIRED) +target_link_libraries(your_app PRIVATE Rapier::rapier) +``` + +The target supplies the matching headers, compile definitions, and libraries. +Install each dimension/precision configuration to a separate prefix. When shipping +a shared-library build, include the library and configure its runtime search path +(on Windows, place the DLL beside your executable). + +## Run the testbed + +```sh +cmake -S c -B build/c3 -DRAPIER_BUILD_TESTBED=ON -DRAPIER_DIMENSION=3 \ + -DRAPIER_PROFILE=release -DCMAKE_BUILD_TYPE=Release +cmake --build build/c3 --target rapier_testbed --config Release --parallel +./build/c3/testbed/rapier_testbed +``` + +For 2D, use `build/c2` and `-DRAPIER_DIMENSION=2`. With a multi-configuration +generator such as Visual Studio, the executable is under `testbed/Release/`. + +CMake downloads the graphics dependencies on the first configuration. macOS uses +system frameworks; Linux needs X11/OpenGL development packages. +See the [testbed guide](testbed/README.md) for controls, scene selection, and +headless runs, and [dependencies](testbed/dependencies.md) for offline builds. + +## Documentation and examples + +Generate the searchable API reference and usage guides: + +```sh +cmake -S c/doxygen -B build/c-docs +cmake --build build/c-docs --target rapier_docs --parallel +``` + +Open `build/c-docs/html/index.html`. This requires Doxygen 1.9.4+, Python 3, and a +C compiler. It generates all four dimension/precision variants without building +the library or downloading testbed dependencies. Normal builds do not need Doxygen. + +The reference covers API usage, ownership, errors, callbacks, and snapshots. +The CI workflow also provides a `rapier-c-api-docs` HTML artifact. + +- [C example](examples/falling_ball.c) +- [C++ helpers](include/rapier.hpp) +- [C# interop example](examples/RapierNative.cs) + +## Tests and header generation + +```sh +ctest --test-dir build/c -C Release --output-on-failure +``` + +The headers are checked in; users do not need to generate them. When changing the +bindings or API comments in `c/src/`, regenerate the header with: + +```sh +cargo install cbindgen --version 0.29.4 --locked +python3 c/tools/generate-header.py +``` diff --git a/c/VERSION b/c/VERSION new file mode 100644 index 000000000..aee1388f5 --- /dev/null +++ b/c/VERSION @@ -0,0 +1 @@ +0.35.3+c.2 diff --git a/c/build.rs b/c/build.rs new file mode 100644 index 000000000..88fe52a2d --- /dev/null +++ b/c/build.rs @@ -0,0 +1,23 @@ +fn main() { + let version = std::fs::read_to_string("../VERSION").expect("read C bindings VERSION"); + let version = version.trim(); + let rust_version = std::env::var("CARGO_PKG_VERSION").expect("Cargo provides package version"); + let prefix = format!("{rust_version}+c."); + let revision = version + .strip_prefix(&prefix) + .and_then(|value| value.parse::().ok()) + .expect("C bindings VERSION must be +c."); + assert_eq!(version, format!("{prefix}{revision}"), "invalid C revision"); + println!("cargo:rustc-env=RAPIER_C_VERSION={version}"); + println!("cargo:rerun-if-changed=../VERSION"); + + // Report the library's Cargo profile, independent of its C/C++ consumer. + let profile = std::env::var("PROFILE").expect("Cargo provides PROFILE"); + println!("cargo:rustc-env=RAPIER_CARGO_PROFILE={profile}"); + // A distributable dylib must not retain Cargo's absolute target/deps install name. + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { + let name = std::env::var("CARGO_PKG_NAME").unwrap().replace('-', "_"); + println!("cargo:rustc-link-arg-cdylib=-Wl,-install_name,@rpath/lib{name}.dylib"); + } + println!("cargo:rerun-if-changed=../build.rs"); +} diff --git a/c/cbindgen.toml b/c/cbindgen.toml new file mode 100644 index 000000000..06bc33169 --- /dev/null +++ b/c/cbindgen.toml @@ -0,0 +1,72 @@ +language = "C" +include_guard = "RAPIER_H" +cpp_compat = true +usize_is_size_t = true +style = "both" +documentation_style = "doxy" +autogen_warning = "/* Generated by cbindgen. Regenerate with c/tools/generate-header.sh. */" +header = """ +/** @file + * Rapier C ABI. Select dimension and precision before including this file. + * Select one variant per translation unit; flags must match its linked library. + * Read README.md for ownership, pointer lifetime, callbacks and threading. */ +#if !defined(RAPIER_DIM2) && !defined(RAPIER_DIM3) +#define RAPIER_DIM3 +#endif +#if defined(RAPIER_DIM2) && defined(RAPIER_DIM3) +#error Select exactly one Rapier dimension +#endif +#if !defined(RAPIER_F32) && !defined(RAPIER_F64) +#define RAPIER_F32 +#endif +#if defined(RAPIER_F32) && defined(RAPIER_F64) +#error Select exactly one Rapier precision +#endif +#if defined(RAPIER_DIM2) +/** Spatial dimension selected by this header. */ +#define R2_DIMENSION 2 +/** Select an R2/R3 public type name. */ +#define RAPIER_TYPE(name) R2##name +/** Select an R2/R3 public constant name. */ +#define RAPIER_CONST(name) R2_##name +/** Select an r2/r3 public function name. */ +#define RAPIER_FN(name) r2##name +#else +/** Spatial dimension selected by this header. */ +#define R3_DIMENSION 3 +/** Select an R2/R3 public type name. */ +#define RAPIER_TYPE(name) R3##name +/** Select an R2/R3 public constant name. */ +#define RAPIER_CONST(name) R3_##name +/** Select an r2/r3 public function name. */ +#define RAPIER_FN(name) r3##name +#endif +#if defined(_WIN32) +/** C ABI calling convention. */ +#define RAPIER_CALL __cdecl +#else +/** C ABI calling convention. */ +#define RAPIER_CALL +#endif +#ifndef RAPIER_API +#if defined(_WIN32) && !defined(RAPIER_STATIC) +/** Shared-library import annotation. */ +#define RAPIER_API __declspec(dllimport) +#else +/** Shared-library import annotation. */ +#define RAPIER_API +#endif +#endif +""" +[defines] +"feature = dim2" = "RAPIER_DIM2" +"feature = dim3" = "RAPIER_DIM3" +"feature = f32" = "RAPIER_F32" +"feature = f64" = "RAPIER_F64" +"feature = fem" = "RAPIER_FEM" +"feature = parallel" = "RAPIER_PARALLEL" +"feature = robotics" = "RAPIER_ROBOTICS" +[fn] +prefix = "RAPIER_API" +[parse] +parse_deps = false diff --git a/c/cmake/RapierConfig.cmake.in b/c/cmake/RapierConfig.cmake.in new file mode 100644 index 000000000..912c44088 --- /dev/null +++ b/c/cmake/RapierConfig.cmake.in @@ -0,0 +1,20 @@ +@PACKAGE_INIT@ +set(Rapier_BINDINGS_VERSION "@RAPIER_C_VERSION@") +if(NOT TARGET Rapier::rapier) + include(CMakeFindDependencyMacro) + if(NOT "@RAPIER_SHARED@" AND NOT WIN32) + find_dependency(Threads) + endif() + add_library(Rapier::rapier @RAPIER_KIND@ IMPORTED) + set_target_properties(Rapier::rapier PROPERTIES + IMPORTED_LOCATION "${PACKAGE_PREFIX_DIR}/@RAPIER_INSTALL_LIBRARY_DIR@/@RAPIER_FILENAME@" + INTERFACE_INCLUDE_DIRECTORIES "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@" + INTERFACE_COMPILE_DEFINITIONS "@RAPIER_DEFINITIONS@" + INTERFACE_LINK_LIBRARIES "@RAPIER_SYSTEM_LIBS@") + if(APPLE AND "@RAPIER_SHARED@") + set_property(TARGET Rapier::rapier PROPERTY IMPORTED_SONAME "@RAPIER_INSTALL_NAME@") + endif() + if(NOT "@RAPIER_IMPLIB@" STREQUAL "") + set_property(TARGET Rapier::rapier PROPERTY IMPORTED_IMPLIB "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@/@RAPIER_IMPLIB@") + endif() +endif() diff --git a/c/doxygen/CMakeLists.txt b/c/doxygen/CMakeLists.txt new file mode 100644 index 000000000..da08962bf --- /dev/null +++ b/c/doxygen/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.20) +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(RapierCDocs LANGUAGES C) +endif() +find_package(Doxygen 1.9.4 REQUIRED) +find_package(Python3 REQUIRED COMPONENTS Interpreter) +get_filename_component(RAPIER_C_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE) +file(READ "${RAPIER_C_DIR}/VERSION" DOC_VERSION) +string(STRIP "${DOC_VERSION}" DOC_VERSION) +set(DOC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/html") +file(MAKE_DIRECTORY "${DOC_OUTPUT}") +configure_file(index.html.in "${DOC_OUTPUT}/index.html" @ONLY) +add_custom_target(rapier_docs) +foreach(DOC_DIM 2 3) + foreach(DOC_PRECISION 32 64) + set(DOC_VARIANT "${DOC_DIM}d-f${DOC_PRECISION}") + set(DOC_ROBOTICS "") + if(DOC_DIM EQUAL 3 AND DOC_PRECISION EQUAL 32) + set(DOC_ROBOTICS "RAPIER_ROBOTICS") + endif() + configure_file(Doxyfile.in "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile-${DOC_VARIANT}" @ONLY) + add_custom_target(rapier_docs_${DOC_VARIANT} + COMMAND "${DOXYGEN_EXECUTABLE}" "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile-${DOC_VARIANT}" + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/check.py" + "${DOC_OUTPUT}/${DOC_VARIANT}/xml" "${DOC_DIM}" "${DOC_PRECISION}" + "${CMAKE_C_COMPILER}" "${CMAKE_C_COMPILER_ID}" + WORKING_DIRECTORY "${RAPIER_C_DIR}" + COMMENT "Generating and checking ${DOC_VARIANT} API reference" + VERBATIM) + add_dependencies(rapier_docs rapier_docs_${DOC_VARIANT}) + endforeach() +endforeach() diff --git a/c/doxygen/Doxyfile.in b/c/doxygen/Doxyfile.in new file mode 100644 index 000000000..713068b3a --- /dev/null +++ b/c/doxygen/Doxyfile.in @@ -0,0 +1,43 @@ +PROJECT_NAME = "Rapier C — @DOC_DIM@D / f@DOC_PRECISION@" +PROJECT_NUMBER = "@DOC_VERSION@" +OUTPUT_DIRECTORY = "@DOC_OUTPUT@/@DOC_VARIANT@" +INPUT = "@RAPIER_C_DIR@/include/rapier.h" \ + "@RAPIER_C_DIR@/include/rapier_math.h" \ + "@RAPIER_C_DIR@/include/rapier_helpers.h" \ + "@RAPIER_C_DIR@/include/rapier.hpp" \ + "@RAPIER_C_DIR@/doxygen/reference.dox" +EXAMPLE_PATH = "@RAPIER_C_DIR@/examples" "@RAPIER_C_DIR@/testbed/examples@DOC_DIM@d" +EXAMPLE_RECURSIVE = YES +EXTRACT_ALL = NO +EXTRACT_STATIC = YES +EXTRACT_PRIVATE = NO +JAVADOC_AUTOBRIEF = YES +ALWAYS_DETAILED_SEC = YES +OPTIMIZE_OUTPUT_FOR_C = YES +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +EXPAND_AS_DEFINED = RAPIER_TYPE RAPIER_CONST RAPIER_FN +PREDEFINED = RAPIER_DIM@DOC_DIM@ RAPIER_F@DOC_PRECISION@ RAPIER_FEM RAPIER_PARALLEL @DOC_ROBOTICS@ \ + RAPIER_API= RAPIER_CALL= __cplusplus +INCLUDE_PATH = "@RAPIER_C_DIR@/include" +SKIP_FUNCTION_MACROS = NO +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_NO_PARAMDOC = NO +WARN_AS_ERROR = FAIL_ON_WARNINGS +QUIET = YES +GENERATE_HTML = YES +HTML_OUTPUT = . +GENERATE_TREEVIEW = YES +SEARCHENGINE = YES +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_PROGRAMLISTING = NO +HAVE_DOT = NO +SHOW_INCLUDE_FILES = NO +FULL_PATH_NAMES = NO +SORT_MEMBER_DOCS = YES + +EXCLUDE_SYMBOLS = RAPIER_H RAPIER_MATH_H RAPIER_HELPERS_H RAPIER_HPP diff --git a/c/doxygen/check.py b/c/doxygen/check.py new file mode 100644 index 000000000..a3be39742 --- /dev/null +++ b/c/doxygen/check.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Check that Doxygen documents every function visible to the C compiler.""" +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import xml.etree.ElementTree as ET + +xml_dir, dimension, precision, compiler, compiler_id = sys.argv[1:] +xml_dir = Path(xml_dir) +c_dir = Path(__file__).resolve().parents[1] +prefix = f"r{dimension}" +definitions = [f"RAPIER_DIM{dimension}", f"RAPIER_F{precision}", "RAPIER_FEM", "RAPIER_PARALLEL", "RAPIER_STATIC"] +if (dimension, precision) == ("3", "32"): + definitions.append("RAPIER_ROBOTICS") +with tempfile.TemporaryDirectory(prefix="rapier-doc-check-") as tmp: + source = Path(tmp) / "api.c" + source.write_text('#include "rapier_math.h"\n#include "rapier_helpers.h"\n') + if compiler_id == "MSVC": + args = [compiler, "/nologo", "/EP", "/TC", f"/I{c_dir / 'include'}", *[f"/D{x}" for x in definitions], str(source)] + else: + args = [compiler, "-E", "-P", "-x", "c", "-I", str(c_dir / "include"), *[f"-D{x}" for x in definitions], str(source)] + if sys.platform == "darwin": + sdk = subprocess.check_output(["xcrun", "--show-sdk-path"], text=True).strip() + args[1:1] = ["-isysroot", sdk] + preprocessed = subprocess.check_output(args, text=True) +expected = set(re.findall(rf"\b({prefix}\w+)\s*\(", preprocessed)) +index = ET.parse(xml_dir / "index.xml") +actual = {m.findtext("name") for m in index.findall('.//member[@kind="function"]') if m.findtext("name", "").startswith(prefix)} +if missing := expected - actual: + raise SystemExit(f"Functions missing from reference: {sorted(missing)}") +if extra := actual - expected: + raise SystemExit(f"Functions from the wrong build variant: {sorted(extra)}") +# Function comments need a brief, not just an @ingroup/@see tag. Also reject +# accidental internal Rust names leaking through the header generator. +checked = set() +for compound in index.findall("compound"): + path = xml_dir / f"{compound.attrib['refid']}.xml" + tree = ET.parse(path) + for member in tree.findall(".//memberdef"): + name = member.findtext("name", "") + if name not in actual or name in checked: + continue + checked.add(name) + brief = "".join(member.find("briefdescription").itertext()).strip() + detail = "".join(member.find("detaileddescription").itertext()).strip() + if not brief: + raise SystemExit(f"Missing brief description: {name}") + if re.search(r"\brpr_\w+|\bRpr\w+|\bRPR_\w+", brief + detail): + raise SystemExit(f"Internal Rust spelling in documentation: {name}") +if not expected: + raise SystemExit("No C API functions found during preprocessing") +print(f"Verified {len(expected)} documented functions for {dimension}D/f{precision}.") diff --git a/c/doxygen/index.html.in b/c/doxygen/index.html.in new file mode 100644 index 000000000..037b5bb70 --- /dev/null +++ b/c/doxygen/index.html.in @@ -0,0 +1,11 @@ + + +Rapier C API @DOC_VERSION@ + +

Rapier C API

Version @DOC_VERSION@. Choose the dimension and precision of your linked library.

+ + +
Dimension32-bit floats64-bit floats
2Dr2 / f32r2 / f64
3Dr3 / f32r3 / f64
+

References include parallel and FEM APIs. Robotics is available only in 3D/f32. +Your build must enable each optional feature before you use it.

+ diff --git a/c/doxygen/reference.dox b/c/doxygen/reference.dox new file mode 100644 index 000000000..adcbae312 --- /dev/null +++ b/c/doxygen/reference.dox @@ -0,0 +1,192 @@ +/** @mainpage Rapier C API + +Choose the reference matching your library's dimension and scalar precision. +Function names use `r2` or `r3`; types and constants use `R2` or `R3`. +Instance methods separate their receiver with an underscore, for example +`r3RigidBody_SetTranslation`. Include `rapier.h`; `rapier_math.h` adds inline math +and `rapier_helpers.h` adds explicit invalid-handle constants. + +Start with @ref ownership and @ref api_errors, then @ref falling_ball.c. +The topic groups list the API by purpose. The optional @ref cpp helpers provide +RAII ownership; they do not change the C ABI. + +@section selection Build selection +Define exactly one of `RAPIER_DIM2` / `RAPIER_DIM3` and one of +`RAPIER_F32` / `RAPIER_F64` before including the headers. The defaults are 3D/f32. +Flags must match the linked library; CheckAbi checks version and core layouts. +PodLayout exposes additional structure sizes for foreign-language bindings. +Use uint32_t for ABI booleans, not C/C++ bool; inputs must be 0 or 1. + +These references include `RAPIER_PARALLEL` and `RAPIER_FEM` declarations. +Enable the matching library features before using them. Robotics requires +`RAPIER_ROBOTICS`, 3D, and f32. Version returns the release string; +BuildInfo and BuildFeatures describe the binary actually loaded. + +@section units Coordinates and units +Positions and lengths use your chosen consistent length unit; time is seconds, +angles are radians. A 2D rotation is an angle; a 3D rotation is a quaternion in +x,y,z,w order, normalized on input. Zero-initialized 3D quaternions are invalid: +use description constructors or TranslationPose with a zero translation. +World-space coordinates are used unless a parameter or field says local/relative. +LengthUnit scales solver tolerances; it does not convert supplied geometry. +*/ + +/** @page ownership Ownership and lifetimes + +| Value | Ownership rule | +|---|---| +| World pointer | Owns inserted bodies, colliders, joints, and soft bodies. Release with FreeWorld. | +| Entity handle | Small borrowed value carrying its world pointer, index, and generation. Never free it. | +| Description/configuration | Copyable POD; initialize with its constructor/default function. No destructor. | +| Input array view | Borrows elements through the consuming build/insert call. Copying the view does not copy data. | +| SharedShape pointer | Owned reference-counted geometry wrapper. Release each wrapper with FreeSharedShape. | +| Controller/event collector/snapshot/mesh | Owned pointer with a matching Free function. | +| Callback context or MJCF visual pointer | Borrowed; never free or retain beyond its documented lifetime. | + +Constructors supply meaningful defaults; `{0}` is not a replacement for them. +Insertion borrows descriptions, copies their data, and retains shared geometry. +After insertion returns, input arrays and temporary shared-shape wrappers can be +released. Setters on descriptions may retain borrowed views until insertion. + +Removing an entity invalidates its handle. Generations detect stale entities only +while the owning world is alive: no API can safely validate a freed world pointer. +Destroying a world invalidates all its handles, including copies held elsewhere. +Use the explicit INVALID_*_HANDLE constants instead of assuming zero is invalid. +Opaque owned pointers must be released through their matching Rapier Free function, +never C `free`. Free functions accept NULL unless stated otherwise. + +@section threads Access and threading +Ordinary world reads may overlap. A mutation/step requires exclusive access; +conflicting or reentrant access reports WORLD_BUSY rather than waiting. +The caller must keep the world alive throughout every call and synchronize its +destruction with all users. Controllers, collectors, and other independently +owned objects require external synchronization when shared between threads. + +Callbacks may run on physics worker threads in parallel builds. Keep callback code +and user data alive until the operation returns. Use the callback's ReadContext +with Read* accessors; ordinary world access may report WORLD_BUSY during stepping. +Do not mutate the world from callbacks. Never unwind a C++ exception or longjmp +through Rust frames. Copy callback data if it is needed after return. +*/ + +/** @page api_errors Errors and returned values + +Status-returning calls report OK or an error code. Fallible value-returning calls +report failure through LastStatus and LastError on the calling thread. Check +LastStatus immediately after such a call: later fallible calls replace it. +Value constructors that only initialize POD data do not change the status. + +A failed value-returning call returns a default value (often zero, NULL, or an +invalid handle). That value alone does not distinguish success from failure. +LastError is a borrowed UTF-8 string valid until the next fallible call on that +thread; copy it before making another call. Reading LastStatus/LastError does not +clear them. + +SetErrorHandler installs a thread-local synchronous notification callback and +returns the previous handler. A callback that returns does not suppress the error. +Nested failures do not recursively invoke it. NOT_FOUND also invokes this handler; +use TryCastRay or CastRayToi if an ordinary ray miss should return OK with found = 0. + +Pointer checks cannot prove that memory is live, large enough, or owned by the +caller. Supply correctly aligned buffers and valid pointers for their documented +lifetimes. A caught PANIC is not transactional: discard objects mutated by that +call. Validation errors are not a general rollback guarantee either. +*/ + +/** @page output_buffers Arrays and output buffers + +Input views pair a pointer with an element count. NULL is allowed with count zero. +Counts for typed views are elements: TriangleView counts triangles, not scalar +indices. Description setters borrow the view; they need not validate geometry +until a later build/insert call. + +Functions accepting `buffer` and `capacity` and returning a size use this protocol: +1. Pass NULL and capacity zero to get the required element count (OK). +2. Allocate that many elements and call again to copy them. +3. If too small, the function returns the required count, reports BUFFER_TOO_SMALL, + and writes no elements. Resize and retry. + +Check LastStatus after both calls. Arrays may grow between calls if the application +mutates the source. Returned counts use the declared element type; some geometry +outputs are flattened scalar indices, explicitly documented by that function. +String-copy functions count bytes including the terminating NUL. ByteView is an +exception: it borrows snapshot storage directly until FreeBytes, with no copy. +*/ + +/** @page snapshots Snapshots +SerializeWorld returns owned Bytes; Bytes_Data borrows their byte array. +Release the owner with FreeBytes after copying or consuming it. +DeserializeWorld creates a new owned world. DeserializeRigidState is a separate +importer for legacy Rust testbed rigid-state snapshots. Only load trusted +snapshots produced by the identical Rapier build. Snapshots are not a durable interchange format. + +Old handles still point to the original world. Discard them and enumerate handles +from the restored world. Configure any thread pool and external callback/user +state again; application-owned state is not reconstructed automatically. +*/ + +/** @defgroup errors ABI and errors +Build identity, layout checks, status codes, and thread-local diagnostics. +See @ref api_errors for the return-value convention. +*/ +/** @defgroup worlds Worlds and stepping +Create a world, configure integration, step, and serialize it. +Setters change the named setting for subsequent operations. +*/ +/** @defgroup math Math and array types +Plain values and inline arithmetic. Constructors do not allocate. +*/ +/** @defgroup rigid_bodies Rigid bodies +Descriptions and live, world-bound handles. Accessors return copies. +Handle operations report INVALID_HANDLE for a removed/stale entity in a live world. +*/ +/** @defgroup colliders Colliders +Collider descriptions, material properties, sensor flags, and filtering. +A parented collider's description pose is relative to its rigid body. +*/ +/** @defgroup shapes Shapes and mesh data +Shared immutable geometry and copied rendering data. Owned pointers require their +matching Free functions; description views remain borrowed until build/insert. +*/ +/** @defgroup joints Joints +Impulse joints and reduced-coordinate articulations. Axis indices are translation +X/Y[/Z], then rotation Z in 2D or X/Y/Z in 3D. Masks use one bit per axis. +*/ +/** @defgroup queries Scene queries +Queries read the broad phase from the latest Step or DetectCollisions. After +insertion or manual pose changes, update collision detection before querying. +NULL QueryOptions uses native defaults. Predicates borrow their ReadContext. +*/ +/** @defgroup callbacks Callback read access +Read* functions resolve handles through the callback context. This context is +borrowed for that callback only and cannot mutate the world. +*/ +/** @defgroup events Events, contacts, and debug drawing +Event collectors own their recorded events. Enable event flags on colliders and +pass a collector when stepping. Copying events does not drain the collector; +Clear discards them. Contact-force events also honor the collider force threshold. +*/ +/** @defgroup soft_bodies Soft bodies +Particle recipes, materials, clusters, deformable meshes, and tearing. +Mesh indices and particle indices can change after tearing; use topology versions +and tear remapping events to maintain application data. FEM requires RAPIER_FEM. +*/ +/** @defgroup controllers Character, vehicle, and PID controllers +Controllers compute motion/corrections or apply forces; they do not replace Step. +Keep referenced worlds and bodies alive for controller operations. +*/ +/** @defgroup robotics URDF and MJCF import +Available only with RAPIER_ROBOTICS in 3D/f32. Loaded robots and insertion-result +containers are independently owned; freeing these containers does not remove the +world's inserted entities. Visual pointers borrow the loaded MJCF robot. +*/ +/** @defgroup cpp C++ ownership helpers +`rapier.hpp` supplies unique_ptr-based Owner aliases and small value factories. +Keep owners alive while using their handles. `check` throws on non-OK status; +callers with exceptions disabled should use the C API and status checks directly. +Deleters cannot report failures: destroy owners only when no operation is active. +*/ +/** @example falling_ball.c +A complete dimension/precision-independent simulation using the same source built +by the CMake example target. The RAPIER_* macros select concrete r2/r3 names. +*/ diff --git a/c/examples/RapierNative.cs b/c/examples/RapierNative.cs new file mode 100644 index 000000000..0bcc4daa9 --- /dev/null +++ b/c/examples/RapierNative.cs @@ -0,0 +1,89 @@ +// Minimal standalone C# / Unity-compatible 3D-f32 example. Uses only blittable C ABI types. +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +public static class RapierNative +{ + const string Library = "rapier3d_ffi"; + [StructLayout(LayoutKind.Sequential)] public struct Vector { public float x, y, z; } + [StructLayout(LayoutKind.Sequential)] public struct Rotation { public float x, y, z, w; } + [StructLayout(LayoutKind.Sequential)] public struct Pose { public Vector translation; public Rotation rotation; } + [StructLayout(LayoutKind.Sequential)] public struct BodyHandle { public IntPtr world; public uint index, generation; } + [StructLayout(LayoutKind.Sequential)] public struct UserData { public ulong low, high; } + [StructLayout(LayoutKind.Sequential)] public struct MassProperties + { + public Vector localCom; + public float mass; + public Vector principalInertia; + public Rotation principalInertiaLocalFrame; + } + // R3Bool is uint32_t. Do not marshal these fields as C# bool. + [StructLayout(LayoutKind.Sequential)] public struct RigidBodyDesc + { + public Pose position; + public Vector linvel, angvel; + public uint bodyType; + public float gravityScale, linearDamping, angularDamping, additionalMass; + public uint useAdditionalMassProperties; + public MassProperties additionalMassProperties; + public byte lockedAxes; + public uint canSleep, sleeping, ccdEnabled; + public float softCcdPrediction; + public uint allowFastRotation, enabled; + public sbyte dominanceGroup; + public UIntPtr additionalSolverIterations, additionalPgsIterations; + public uint gyroscopicForcesEnabled; + public UserData userData; + } + [StructLayout(LayoutKind.Sequential)] struct PodLayout + { + public UIntPtr rigidBodyDesc, colliderDesc, shapeDesc, jointDesc; + public UIntPtr softBodyMaterial, integrationParameters, softBodyDesc; + public UIntPtr softMeshBindingDesc, queryOptions, urdfLoaderOptions, mjcfLoaderOptions; + } + public sealed class World : SafeHandleZeroOrMinusOneIsInvalid + { + public World() : base(true) { } + protected override bool ReleaseHandle() { return r3FreeWorld(handle) == 0; } + } + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern uint r3CheckAbi(uint version,uint dimension,UIntPtr realSize,UIntPtr vectorSize,UIntPtr poseSize); + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern IntPtr r3LastError(); + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern uint r3LastStatus(); + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern World r3NewWorld(); + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern uint r3FreeWorld(IntPtr world); + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern uint r3Step(World world,IntPtr hooks,IntPtr events); + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern PodLayout r3PodLayout(); + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern RigidBodyDesc r3DynamicRigidBodyDesc(); + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern BodyHandle r3InsertRigidBody(World world,in RigidBodyDesc desc); + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern Vector r3RigidBody_Translation(BodyHandle handle); + static void Check(uint status) + { + if(status!=0) throw new InvalidOperationException(Marshal.PtrToStringUTF8(r3LastError())); + } + // Call from Unity's main thread, or any thread with exclusive access to this world. + // The native library must be installed for the process architecture before invoking this method. + public static Vector SimulateOneSecond() + { + Check(r3CheckAbi(1,3,(UIntPtr)4,(UIntPtr)Marshal.SizeOf(),(UIntPtr)Marshal.SizeOf())); + PodLayout layout = r3PodLayout(); + if (layout.rigidBodyDesc != (UIntPtr)Marshal.SizeOf()) + throw new InvalidOperationException("RigidBodyDesc layout does not match the native library."); + World world = r3NewWorld(); + Check(r3LastStatus()); + using(world) + { + RigidBodyDesc body = r3DynamicRigidBodyDesc(); + body.additionalMass = 1; + body.position.translation.y = 5; + BodyHandle handle = r3InsertRigidBody(world,in body); + Check(r3LastStatus()); + // The description is a value. Only the world needs disposal. + for(int i=0;i<60;++i) Check(r3Step(world,IntPtr.Zero,IntPtr.Zero)); + Vector position = r3RigidBody_Translation(handle); + // Handles contain a raw pointer; keep the owning SafeHandle alive through the call. + GC.KeepAlive(world); + Check(r3LastStatus()); + return position; + } + } +} diff --git a/c/examples/compare_steps.rs b/c/examples/compare_steps.rs new file mode 100644 index 000000000..ed25fe160 --- /dev/null +++ b/c/examples/compare_steps.rs @@ -0,0 +1,61 @@ +//! Replay trusted C-testbed snapshots with the native Rust API, in the same Cargo +//! configuration. See c/testbed/tools/README.md. This is a manual benchmark. +use bincode::Options; +use rapier::prelude::*; +use std::{path::Path, time::Instant}; + +fn read(path: &Path) -> PhysicsWorld { + let data = std::fs::read(path).unwrap(); + assert_eq!(&data[..3], b"RPR"); + assert_eq!(data[3], 1); + assert_eq!(data[4], rapier::math::DIM as u8); + assert_eq!(data[5], std::mem::size_of::() as u8); + bincode::DefaultOptions::new() + .with_fixint_encoding() + .with_limit(256 * 1024 * 1024) + .reject_trailing_bytes() + .deserialize(&data[6..]) + .unwrap() +} +fn main() { + let args: Vec<_> = std::env::args_os().collect(); + assert_eq!(args.len(), 3, "arguments: INITIAL_SNAPSHOT FINAL_SNAPSHOT"); + let mut world = read(Path::new(&args[1])); + let expected = read(Path::new(&args[2])); + #[cfg(feature = "parallel")] + world.configure_thread_pool(1).unwrap(); + world.physics_pipeline.counters.enable(); + let mut wall = 0.0; + let mut engine = 0.0; + for step in 0..420 { + let start = Instant::now(); + world.step(); + let elapsed = start.elapsed().as_secs_f64() * 1000.0; + if step >= 120 { + wall += elapsed; + engine += world.physics_pipeline.counters.step_time_ms(); + } + } + assert_eq!(world.bodies.len(), expected.bodies.len()); + let mut active = 0; + // Exact final poses and velocities verify the same physical trajectory. + for (handle, body) in world.bodies.iter() { + let other = &expected.bodies[handle]; + assert_eq!(body.position(), other.position(), "pose {handle:?}"); + assert_eq!(body.linvel(), other.linvel(), "linear velocity {handle:?}"); + assert_eq!(body.angvel(), other.angvel(), "angular velocity {handle:?}"); + if body.is_dynamic() { + assert!(!body.is_sleeping()); + assert!(body.activation().normalized_linear_threshold < 0.0); + active += 1; + } + } + println!( + "Rust bodies={} awake_dynamic={} SIMD={} workers=1 warmup=120 measured=300 wall_ms={:.6} engine_ms={:.6} final_state=exact_match", + world.bodies.len(), + active, + rapier::math::SIMD_WIDTH, + wall / 300.0, + engine / 300.0 + ); +} diff --git a/c/examples/falling_ball.c b/c/examples/falling_ball.c new file mode 100644 index 000000000..741e6f95f --- /dev/null +++ b/c/examples/falling_ball.c @@ -0,0 +1,49 @@ +#include "rapier_helpers.h" +#include "rapier_math.h" +#include +#include + +#define CHECK(call) \ + do { \ + if ((call) != RAPIER_CONST(OK)) { \ + fprintf(stderr, "%s: %s\n", #call, RAPIER_FN(LastError)()); \ + exit(EXIT_FAILURE); \ + } \ + } while (0) + +int main(void) { + CHECK(RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), RAPIER_CONST(DIMENSION), + sizeof(RAPIER_TYPE(Real)), sizeof(RAPIER_TYPE(Vector)), + sizeof(RAPIER_TYPE(Pose)))); + + RAPIER_TYPE(World) *world = RAPIER_FN(NewWorld)(); + CHECK(RAPIER_FN(LastStatus)()); + + RAPIER_TYPE(RigidBodyDesc) rigid_body = RAPIER_FN(DynamicRigidBodyDesc)(); +#if defined(RAPIER_DIM2) + RAPIER_TYPE(Vector) position = RAPIER_FN(Vector)(0.0, 5.0); +#else + RAPIER_TYPE(Vector) position = RAPIER_FN(Vector)(0.0, 5.0, 0.0); +#endif + rigid_body.position.translation = position; + + RAPIER_TYPE(ColliderDesc) collider = RAPIER_FN(BallColliderDesc)(0.5); + RAPIER_TYPE(RigidBodyHandle) handle = RAPIER_FN(InsertRigidBody)(world, &rigid_body); + CHECK(RAPIER_FN(LastStatus)()); + RAPIER_FN(InsertCollider)(handle, &collider); + CHECK(RAPIER_FN(LastStatus)()); + + /* Descriptions own no resources. The world owns the inserted objects. */ + + for (int i = 0; i < 60; i++) { + CHECK(RAPIER_FN(Step)(world, NULL, NULL)); + } + + position = RAPIER_FN(RigidBody_Translation)(handle); + CHECK(RAPIER_FN(LastStatus)()); + printf("Ball y after one second: %.3f\n", (double)position.y); + + /* Only the world owns native resources. */ + CHECK(RAPIER_FN(FreeWorld)(world)); + return EXIT_SUCCESS; +} diff --git a/c/include/rapier.h b/c/include/rapier.h new file mode 100644 index 000000000..8a8c476bf --- /dev/null +++ b/c/include/rapier.h @@ -0,0 +1,17979 @@ +/** @file + * Rapier C ABI. Select dimension and precision before including this file. + * Select one variant per translation unit; flags must match its linked library. + * Read README.md for ownership, pointer lifetime, callbacks and threading. */ +#if !defined(RAPIER_DIM2) && !defined(RAPIER_DIM3) +#define RAPIER_DIM3 +#endif +#if defined(RAPIER_DIM2) && defined(RAPIER_DIM3) +#error Select exactly one Rapier dimension +#endif +#if !defined(RAPIER_F32) && !defined(RAPIER_F64) +#define RAPIER_F32 +#endif +#if defined(RAPIER_F32) && defined(RAPIER_F64) +#error Select exactly one Rapier precision +#endif +#if defined(RAPIER_DIM2) +/** Spatial dimension selected by this header. */ +#define R2_DIMENSION 2 +/** Select an R2/R3 public type name. */ +#define RAPIER_TYPE(name) R2##name +/** Select an R2/R3 public constant name. */ +#define RAPIER_CONST(name) R2_##name +/** Select an r2/r3 public function name. */ +#define RAPIER_FN(name) r2##name +#else +/** Spatial dimension selected by this header. */ +#define R3_DIMENSION 3 +/** Select an R2/R3 public type name. */ +#define RAPIER_TYPE(name) R3##name +/** Select an R2/R3 public constant name. */ +#define RAPIER_CONST(name) R3_##name +/** Select an r2/r3 public function name. */ +#define RAPIER_FN(name) r3##name +#endif +#if defined(_WIN32) +/** C ABI calling convention. */ +#define RAPIER_CALL __cdecl +#else +/** C ABI calling convention. */ +#define RAPIER_CALL +#endif +#ifndef RAPIER_API +#if defined(_WIN32) && !defined(RAPIER_STATIC) +/** Shared-library import annotation. */ +#define RAPIER_API __declspec(dllimport) +#else +/** Shared-library import annotation. */ +#define RAPIER_API +#endif +#endif + + +#ifndef RAPIER_H +#define RAPIER_H + +/* Generated by cbindgen. Regenerate with c/tools/generate-header.sh. */ + +#include +#include +#include +#include +#include + +#if defined(RAPIER_DIM2) + +#if defined(RAPIER_DIM2) +/** + * @ingroup joints + * Number of translational and angular joint axes in this dimension. + */ +#define R2_JOINT_DOF_COUNT 3 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Number of translational and angular joint axes in this dimension. + */ +#define R2_JOINT_DOF_COUNT 6 +#endif + +/** + * @ingroup soft_bodies + * Soft-body selector: desc particles. + */ +#define R2_SOFT_DESC_PARTICLES 0 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc rope. + */ +#define R2_SOFT_DESC_ROPE 1 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc grid. + */ +#define R2_SOFT_DESC_GRID 2 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc cloth. + */ +#define R2_SOFT_DESC_CLOTH 3 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc cuboid. + */ +#define R2_SOFT_DESC_CUBOID 4 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc surface. + */ +#define R2_SOFT_DESC_SURFACE 5 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc disk. + */ +#define R2_SOFT_DESC_DISK 6 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc sphere. + */ +#define R2_SOFT_DESC_SPHERE 7 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc cloth tube. + */ +#define R2_SOFT_DESC_CLOTH_TUBE 8 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc volumetric. + */ +#define R2_SOFT_DESC_VOLUMETRIC 9 + +/** + * @ingroup soft_bodies + * Soft-body selector: binding skinned. + */ +#define R2_SOFT_BINDING_SKINNED 0 + +/** + * @ingroup soft_bodies + * Soft-body selector: binding direct. + */ +#define R2_SOFT_BINDING_DIRECT 1 + +/** + * @ingroup soft_bodies + * Soft-body selector: binding direct by position. + */ +#define R2_SOFT_BINDING_DIRECT_BY_POSITION 2 + +#if defined(RAPIER_DIM2) +/** + * @ingroup shapes + * Treat the 2D polyline as oriented when generating contact normals. + */ +#define R2_POLYLINE_ORIENTED 1 +#endif + +/** + * @ingroup shapes + * Prepare polyline acceleration data for deformation. + */ +#define R2_POLYLINE_DEFORMABLE 2 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a ball. + */ +#define R2_SHAPE_DESC_BALL 0 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a cuboid. + */ +#define R2_SHAPE_DESC_CUBOID 1 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a round cuboid. + */ +#define R2_SHAPE_DESC_ROUND_CUBOID 2 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a capsule. + */ +#define R2_SHAPE_DESC_CAPSULE 3 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a segment. + */ +#define R2_SHAPE_DESC_SEGMENT 4 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a triangle. + */ +#define R2_SHAPE_DESC_TRIANGLE 5 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a half-space. + */ +#define R2_SHAPE_DESC_HALFSPACE 6 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a convex hull. + */ +#define R2_SHAPE_DESC_CONVEX_HULL 7 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a triangle mesh. + */ +#define R2_SHAPE_DESC_TRIMESH 8 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a polyline. + */ +#define R2_SHAPE_DESC_POLYLINE 9 + +/** + * @ingroup shapes + * ShapeDesc kind selecting borrowed shared geometry. + */ +#define R2_SHAPE_DESC_SHARED 10 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a heightfield. + */ +#define R2_SHAPE_DESC_HEIGHTFIELD 11 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a cylinder. + */ +#define R2_SHAPE_DESC_CYLINDER 12 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a cone. + */ +#define R2_SHAPE_DESC_CONE 13 + +/** + * @ingroup shapes + * ShapeDesc kind selecting compound child shapes. + */ +#define R2_SHAPE_DESC_COMPOUND 14 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a round cylinder. + */ +#define R2_SHAPE_DESC_ROUND_CYLINDER 15 + +/** + * @ingroup colliders + * Mass density. + */ +#define R2_MASS_DENSITY 0 + +/** + * @ingroup colliders + * Mass total. + */ +#define R2_MASS_TOTAL 1 + +/** + * @ingroup colliders + * Mass properties. + */ +#define R2_MASS_PROPERTIES 2 + +/** + * @ingroup errors + * C binary ABI revision expected by this header. + */ +#define R2_ABI_VERSION 1 + +/** + * @ingroup rigid_bodies + * Dynamic body affected by forces and contacts. + */ +#define R2_DYNAMIC 0 + +/** + * @ingroup rigid_bodies + * Immovable body. + */ +#define R2_FIXED 1 + +/** + * @ingroup rigid_bodies + * Kinematic body controlled by its next pose. + */ +#define R2_KINEMATIC_POSITION_BASED 2 + +/** + * @ingroup rigid_bodies + * Kinematic body controlled by its velocity. + */ +#define R2_KINEMATIC_VELOCITY_BASED 3 + +/** + * @ingroup colliders + * Enable collision-start and collision-stop events for this collider. + */ +#define R2_COLLISION_EVENTS 1 + +/** + * @ingroup colliders + * Enable contact-force events for this collider, subject to its force threshold. + */ +#define R2_CONTACT_FORCE_EVENTS 2 + +/** + * @ingroup colliders + * Combine the two material coefficients by their arithmetic mean. + */ +#define R2_COMBINE_AVERAGE 0 + +/** + * @ingroup colliders + * Use the smaller of the two material coefficients. + */ +#define R2_COMBINE_MIN 1 + +/** + * @ingroup colliders + * Multiply the two material coefficients. + */ +#define R2_COMBINE_MULTIPLY 2 + +/** + * @ingroup colliders + * Use the larger of the two material coefficients. + */ +#define R2_COMBINE_MAX 3 + +/** + * @ingroup colliders + * Invoke the contact-pair filtering hook for this collider. + */ +#define R2_FILTER_CONTACT_PAIRS 1 + +/** + * @ingroup colliders + * Invoke the sensor-intersection filtering hook for this collider. + */ +#define R2_FILTER_INTERSECTION_PAIR 2 + +/** + * @ingroup colliders + * Invoke the solver-contact modification hook for this collider. + */ +#define R2_MODIFY_SOLVER_CONTACTS 4 + +/** + * @ingroup queries + * Query filter: exclude fixed. + */ +#define R2_QUERY_EXCLUDE_FIXED 1 + +/** + * @ingroup queries + * Query filter: exclude kinematic. + */ +#define R2_QUERY_EXCLUDE_KINEMATIC 2 + +/** + * @ingroup queries + * Query filter: exclude dynamic. + */ +#define R2_QUERY_EXCLUDE_DYNAMIC 4 + +/** + * @ingroup queries + * Query filter: exclude sensors. + */ +#define R2_QUERY_EXCLUDE_SENSORS 8 + +/** + * @ingroup queries + * Query filter: exclude solids. + */ +#define R2_QUERY_EXCLUDE_SOLIDS 16 + +/** + * @ingroup queries + * Query filter: only dynamic. + */ +#define R2_QUERY_ONLY_DYNAMIC 3 + +/** + * @ingroup queries + * Query filter: only kinematic. + */ +#define R2_QUERY_ONLY_KINEMATIC 5 + +/** + * @ingroup queries + * Query filter: only fixed. + */ +#define R2_QUERY_ONLY_FIXED 6 + +/** + * @ingroup events + * Debug-render flag: collider shapes. + */ +#define R2_DEBUG_COLLIDER_SHAPES 1 + +/** + * @ingroup events + * Debug-render flag: rigid body axes. + */ +#define R2_DEBUG_RIGID_BODY_AXES 2 + +/** + * @ingroup events + * Debug-render flag: multibody joints. + */ +#define R2_DEBUG_MULTIBODY_JOINTS 4 + +/** + * @ingroup events + * Debug-render flag: impulse joints. + */ +#define R2_DEBUG_IMPULSE_JOINTS 8 + +/** + * @ingroup events + * Debug-render flag: solver contacts. + */ +#define R2_DEBUG_SOLVER_CONTACTS 16 + +/** + * @ingroup events + * Debug-render flag: contacts. + */ +#define R2_DEBUG_CONTACTS 32 + +/** + * @ingroup events + * Debug-render flag: collider aabbs. + */ +#define R2_DEBUG_COLLIDER_AABBS 64 + +/** + * @ingroup events + * Debug-render flag: soft bodies. + */ +#define R2_DEBUG_SOFT_BODIES 128 + +/** + * @ingroup events + * Debug-render flag: pseudo normals. + */ +#define R2_DEBUG_PSEUDO_NORMALS 256 + +/** + * @ingroup events + * Debug-render flag: soft volume contacts. + */ +#define R2_DEBUG_SOFT_VOLUME_CONTACTS 512 + +/** + * @ingroup events + * Debug-render flag: soft body stress. + */ +#define R2_DEBUG_SOFT_BODY_STRESS 1024 + +/** + * @ingroup colliders + * Require both membership/filter intersections to be nonempty. + */ +#define R2_GROUPS_AND 0 + +/** + * @ingroup colliders + * Accept either membership/filter intersection if both participants select OR; otherwise use AND. + */ +#define R2_GROUPS_OR 1 + +/** + * @ingroup joints + * Motor stiffness and damping are acceleration-based, independent of mass. + */ +#define R2_MOTOR_ACCELERATION_BASED 0 + +/** + * @ingroup joints + * Motor stiffness and damping are force-based, so response depends on mass. + */ +#define R2_MOTOR_FORCE_BASED 1 + +/** + * @ingroup soft_bodies + * Cell model that constrains volume without elastic shear response. + */ +#define R2_SOFT_CELL_VOLUME 0 + +/** + * @ingroup soft_bodies + * Corotational elastic cell model. + */ +#define R2_SOFT_CELL_COROTATIONAL 1 + +/** + * @ingroup soft_bodies + * Neo-Hookean elastic cell model. + */ +#define R2_SOFT_CELL_NEO_HOOKEAN 2 + +/** + * @ingroup soft_bodies + * Use the constraint-based soft-body solver. + */ +#define R2_SOFT_SOLVER_CONSTRAINTS 0 + +/** + * @ingroup soft_bodies + * Use the finite-element solver; requires RAPIER_FEM. + */ +#define R2_SOFT_SOLVER_FEM 1 + +/** + * @ingroup joints + * Joint axis index for translation along local X. + */ +#define R2_AXIS_LIN_X 0 + +/** + * @ingroup joints + * Joint axis index for translation along local Y. + */ +#define R2_AXIS_LIN_Y 1 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: translation x. + */ +#define R2_LOCK_TRANSLATION_X 1 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: translation y. + */ +#define R2_LOCK_TRANSLATION_Y 2 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: translation z. + */ +#define R2_LOCK_TRANSLATION_Z 4 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: rotation x. + */ +#define R2_LOCK_ROTATION_X 8 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: rotation y. + */ +#define R2_LOCK_ROTATION_Y 16 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: rotation z. + */ +#define R2_LOCK_ROTATION_Z 32 + +#if defined(RAPIER_DIM2) +/** + * @ingroup joints + * Joint axis index for the first rotation: Z in 2D, local X in 3D. + */ +#define R2_AXIS_ANG_X 2 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Joint axis index for the first rotation: Z in 2D, local X in 3D. + */ +#define R2_AXIS_ANG_X 3 +#endif + +#if defined(RAPIER_DIM2) +/** + * @ingroup joints + * Locked-axis mask for a fixed joint: all translations and rotations. + */ +#define R2_JOINT_FIXED_AXES 7 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Locked-axis mask for a fixed joint: all translations and rotations. + */ +#define R2_JOINT_FIXED_AXES 63 +#endif + +#if defined(RAPIER_DIM2) +/** + * @ingroup joints + * Locked-axis mask for a revolute joint: only the first angular axis is free. + */ +#define R2_JOINT_REVOLUTE_AXES 3 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Locked-axis mask for a revolute joint: only the first angular axis is free. + */ +#define R2_JOINT_REVOLUTE_AXES 55 +#endif + +#if defined(RAPIER_DIM2) +/** + * @ingroup joints + * Locked-axis mask for a prismatic joint: only translation along local X is free. + */ +#define R2_JOINT_PRISMATIC_AXES 6 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Locked-axis mask for a prismatic joint: only translation along local X is free. + */ +#define R2_JOINT_PRISMATIC_AXES 62 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Joint axis index for translation along local Z. + */ +#define R2_AXIS_LIN_Z 2 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Joint axis index for rotation around local Y. + */ +#define R2_AXIS_ANG_Y 4 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Joint axis index for rotation around local Z. + */ +#define R2_AXIS_ANG_Z 5 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Locked-axis mask for a spherical joint: translations locked, rotations free. + */ +#define R2_JOINT_SPHERICAL_AXES 7 +#endif + +/** + * @ingroup soft_bodies + * Read-only body type of soft-body cluster proxies; cannot be used to construct a rigid body. + */ +#define R2_SOFT_FRAME 4 + +/** + * @ingroup queries + * Shape-cast iteration limit reached before convergence. + */ +#define R2_SHAPE_CAST_OUT_OF_ITERATIONS 0 + +/** + * @ingroup queries + * Shape cast converged to the reported impact. + */ +#define R2_SHAPE_CAST_CONVERGED 1 + +/** + * @ingroup queries + * Shape-cast numerical solver failed to converge. + */ +#define R2_SHAPE_CAST_FAILED 2 + +/** + * @ingroup queries + * Shapes overlap or are within the target distance at the start of the cast. + */ +#define R2_SHAPE_CAST_PENETRATING 3 + +/** + * @ingroup queries + * The hit feature is unspecified. + */ +#define R2_FEATURE_UNKNOWN 0 + +/** + * @ingroup queries + * The feature ID denotes a vertex. + */ +#define R2_FEATURE_VERTEX 1 + +/** + * @ingroup queries + * The feature ID denotes an edge. + */ +#define R2_FEATURE_EDGE 2 + +/** + * @ingroup queries + * The feature ID denotes a face. + */ +#define R2_FEATURE_FACE 3 + +/** + * @ingroup joints + * Insert reduced-coordinate joints as kinematic articulations. + */ +#define R2_MULTIBODY_JOINTS_ARE_KINEMATIC 1 + +/** + * @ingroup joints + * Disable contacts between colliders of the inserted articulation. + */ +#define R2_MULTIBODY_DISABLE_SELF_CONTACTS 2 + +/** + * @ingroup joints + * Skip joints that would close a loop in the articulation. + */ +#define R2_MULTIBODY_SKIP_LOOP_CLOSURES 4 + +/** + * @ingroup joints + * Do not import joint motors into the articulation. + */ +#define R2_MULTIBODY_SKIP_JOINT_MOTORS 8 + +/** + * @ingroup joints + * Do not import joint limits into the articulation. + */ +#define R2_MULTIBODY_SKIP_JOINT_LIMITS 16 + +/** + * @ingroup joints + * Do not import joint springs into the articulation. + */ +#define R2_MULTIBODY_SKIP_JOINT_SPRINGS 32 + +/** + * @ingroup shapes + * Merge triangle-mesh vertices with identical positions. + */ +#define R2_TRIMESH_MERGE_DUPLICATE_VERTICES 16 + +/** + * @ingroup shapes + * Correct contact normals at internal mesh edges; includes duplicate-vertex merging. + */ +#define R2_TRIMESH_FIX_INTERNAL_EDGES 144 + +/** + * @ingroup shapes + * Prepare triangle-mesh acceleration data for deformation. + */ +#define R2_TRIMESH_DEFORMABLE 256 + +/** + * @ingroup shapes + * Correct internal-edge contacts on both sides of the triangle mesh. + */ +#define R2_TRIMESH_FIX_INTERNAL_EDGES_TWO_SIDED 656 + +/** + * @ingroup shapes + * Correct contact normals at internal heightfield edges. + */ +#define R2_HEIGHTFIELD_FIX_INTERNAL_EDGES 1 + +/** + * Immutable owned byte buffer. Release with the matching FreeBytes function. + * @ingroup worlds + */ +typedef struct R2Bytes R2Bytes; + +/** + * Borrowed native contact context. Valid only during its callback; never retain or free it. + * @ingroup events + */ +typedef struct R2ContactModificationContext R2ContactModificationContext; + +#if defined(RAPIER_DIM3) +/** + * Vehicle controller borrowing its chassis world. Release with the matching Free function. + * @ingroup controllers + */ +typedef struct R2DynamicRayCastVehicleController R2DynamicRayCastVehicleController; +#endif + +/** + * Events accumulate until clear. Copying events never drains them, allowing two-call buffer + * sizing. + * @ingroup events + */ +typedef struct R2EventCollector R2EventCollector; + +/** + * Controller plus reusable collision output from the last move_shape call. + * Kinematic character controller and its last collision list. Release with the matching Free + * function. + * @ingroup controllers + */ +typedef struct R2KinematicCharacterController R2KinematicCharacterController; + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Loaded MJCF robot and its visual/keyframe data. Release with the matching Free function. + * @ingroup robotics + */ +typedef struct R2MjcfRobot R2MjcfRobot; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Owned container of borrowed handles and actuators of an inserted MJCF robot. Release with the + * matching Free function. + * @ingroup robotics + */ +typedef struct R2MjcfRobotHandles R2MjcfRobotHandles; +#endif + +/** + * PID controller with persistent integral state. + * Stateful proportional-integral-derivative controller. Release with the matching Free function. + * @ingroup controllers + */ +typedef struct R2PidController R2PidController; + +/** + * Callback-scoped read access to bodies and colliders. Never retain or free it. + * Only the Read* functions accept this context; it cannot mutate the world. + * @ingroup callbacks + */ +typedef struct R2ReadContext R2ReadContext; + +/** + * Owned tessellated shape: flat triangle vertices and independent line segments, in local space. + * Rounded 3D shapes use their inner surface (as in the Rust testbed). Halfspaces use a finite + * patch. + * @ingroup shapes + */ +typedef struct R2ShapeMesh R2ShapeMesh; + +/** + * Owned copy of a tear event. Read particle remapping before rebuilding render meshes. + * @ingroup events + */ +typedef struct R2SoftBodyTearEvent R2SoftBodyTearEvent; + +#if defined(RAPIER_DIM3) +/** + * Owned indexed geometry from Parry's shape tessellation, preserving its vertex order. + * @ingroup shapes + */ +typedef struct R2TriMeshData R2TriMeshData; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Loaded URDF robot; insertion clones its simulation objects. Release with the matching Free + * function. + * @ingroup robotics + */ +typedef struct R2UrdfRobot R2UrdfRobot; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Owned container of borrowed handles to an inserted URDF robot. Release with the matching Free + * function. + * @ingroup robotics + */ +typedef struct R2UrdfRobotHandles R2UrdfRobotHandles; +#endif + +/** + * Sole owner of simulation state. Handles belong to the world that created them. + * Ordinary reads may overlap. A mutation or step requires exclusive access. + * Destruction must be externally synchronized with all users of this pointer. + * @ingroup worlds + */ +typedef struct R2World R2World; + +#if defined(RAPIER_F32) +/** + * Floating-point scalar: float for f32, double for f64. + * @ingroup math + */ +typedef float R2Real; +#endif + +#if defined(RAPIER_F64) +/** + * Floating-point scalar: float for f32, double for f64. + * @ingroup math + */ +typedef double R2Real; +#endif + +/** + * Spring softness expressed as natural frequency and damping ratio. + * @ingroup math + */ +typedef struct R2SpringCoefficients { + /** + * Nonnegative spring natural frequency in Hz. + */ + R2Real natural_frequency; + /** + * Nonnegative damping ratio; 1 is critical damping. + */ + R2Real damping_ratio; +} R2SpringCoefficients; + +/** + * ABI booleans are uint32_t: zero is false, one is true. + * @ingroup math + */ +typedef uint32_t R2Bool; + +/** + * Optional scalar override; enabled = 0 selects no override. + * @ingroup math + */ +typedef struct R2OptionalReal { + /** + * Whether this setting/object is enabled (0 or 1). + */ + R2Bool enabled; + /** + * Value used when enabled is 1. + */ + R2Real value; +} R2OptionalReal; + +/** + * Optional unsigned integer override; enabled = 0 selects no override. + * @ingroup math + */ +typedef struct R2OptionalU32 { + /** + * Whether this setting/object is enabled (0 or 1). + */ + R2Bool enabled; + /** + * Value used when enabled is 1. + */ + uint32_t value; +} R2OptionalU32; + +/** + * Plain configuration data; initialize defaults, edit, then apply. No destructor. + * @ingroup soft_bodies + */ +typedef struct R2SoftBodyMaterial { + /** + * Spring coefficients for structural edge constraints. + */ + struct R2SpringCoefficients edgeSoftness; + /** + * Spring coefficients for bending edges and dihedrals. + */ + struct R2SpringCoefficients bendSoftness; + /** + * Spring coefficients for cell and global volume constraints. + */ + struct R2SpringCoefficients volumeSoftness; + /** + * Spring coefficients for shape-matching constraints. + */ + struct R2SpringCoefficients shapeMatchingSoftness; + /** + * Elastic modulus: force per area in 3D, force per length in 2D; nonnegative. + */ + R2Real youngModulus; + /** + * Poisson ratio for elastic cells, in [0, 0.5). + */ + R2Real poissonRatio; + /** + * Nonnegative damping ratio of elastic cells. + */ + R2Real elasticDampingRatio; + /** + * Cell strain threshold for plastic flow; zero disables plasticity. + */ + R2Real plasticYield; + /** + * Nonnegative rate per second at which excess cell strain becomes permanent. + */ + R2Real plasticCreep; + /** + * Maximum accumulated cell plastic stretch, measured by the norm of P - I. + */ + R2Real plasticMax; + /** + * Rate per second pulling particle velocities toward best-fit rigid motion; zero disables it. + */ + R2Real deformationDamping; + /** + * Edge strain threshold for plastic flow; zero disables plasticity. + */ + R2Real edgePlasticYield; + /** + * Nonnegative rate per second at which excess edge strain becomes permanent. + */ + R2Real edgePlasticCreep; + /** + * Maximum permanent edge-length change as a fraction of its initial length. + */ + R2Real edgePlasticMax; + /** + * Plastic flow direction: 0 both, 1 compression only, 2 tension only. + */ + uint32_t edgePlasticFlow; + /** + * Optional strain threshold for tearing; disabled means no strain-based tearing. + */ + struct R2OptionalReal tearStrain; + /** + * Optional tensile edge-force threshold for tearing. + */ + struct R2OptionalReal tearForce; + /** + * Exponential load-smoothing time constant in seconds; zero disables smoothing. + */ + R2Real tearSmoothing; + /** + * Tear-threshold multiplier for undamaged interior elements. + */ + R2Real interiorStrength; + /** + * Maximum ordinary edge tears per step; edges above twice their threshold bypass the limit. + */ + uint32_t maxTearsPerStep; + /** + * Optional minimum particle count of tear pieces. + */ + struct R2OptionalU32 minPiece; +} R2SoftBodyMaterial; + +/** + * Plain configuration data; initialize defaults, edit, then apply. No destructor. + * @ingroup soft_bodies + */ +typedef struct R2SoftRecoverySettings { + /** + * Expand speculative contact margins to cover particle velocities set between steps. + */ + R2Bool authoredVelocityMargin; + /** + * Enable speculative edge-edge collision constraints. + */ + R2Bool edgeSpeculation; + /** + * Detect inverted cells to support self-contact recovery. + */ + R2Bool invertedCellDetection; + /** + * Detect surface self-crossings each step. + */ + R2Bool selfCrossingDetection; + /** + * Skip self-crossing detection when accumulated motion cannot have created a crossing. + */ + R2Bool detectionMotionGating; + /** + * Detect boundary crossings between soft bodies. + */ + R2Bool crossBodyDetection; + /** + * Disable contacts on tangled features so elasticity can untangle them. + */ + R2Bool selfStandDown; + /** + * Allow contacts at cross-body crossings to expel, but not hold, the intruder. + */ + R2Bool crossBodyExpelGate; + /** + * Disable edge constraints touching cross-body crossings. + */ + R2Bool edgeStandDown; + /** + * Repel crossing features toward their neighborhood's side of the surface. + */ + R2Bool crossingRepulsion; + /** + * Guide cross-body repulsion by overlap-volume normals; closed meshes only. + */ + R2Bool crossingRepulsionGuide; + /** + * Guide self-crossing repulsion by self-intersection-volume normals; closed meshes only. + */ + R2Bool crossingRepulsionSelfGuide; + /** + * Maximum recovery rate in length units per second, scaled by lengthUnit. + */ + R2Real recoveryPace; + /** + * Enable intersection-volume constraints for overlapping closed surfaces. + */ + R2Bool overlapConstraints; + /** + * Enable intersection-volume constraints against rigid colliders. + */ + R2Bool overlapRigid; + /** + * Skip pair overlap constraints for self-crossed meshes. + */ + R2Bool overlapSkipSelfTangled; + /** + * Disable 3D closed-surface edge constraints where overlap constraints take over. + */ + R2Bool overlapEdgeStandDown; + /** + * Velocity-change limit per step, as a multiple of recoveryPace. + */ + R2Real overlapConstraintPace; + /** + * Per-point constraints inside overlap patches: 0 keep, 1 stand down, 2 align with overlap + * normal. + */ + uint32_t overlapPatchConstraints; + /** + * Measure overlap on contact-skin surfaces rather than bare geometry. + */ + R2Bool overlapSkinVolume; + /** + * Overlap depth retained by recovery, as a fraction of the pair's contact skins. + */ + R2Real overlapKeptDepth; + /** + * Enable recovery of self-intersection regions. + */ + R2Bool overlapSelfRegions; + /** + * Use the overlap normal for recovery pushes. + */ + R2Bool overlapNormalPush; + /** + * Use spatially split overlap-volume constraints. + */ + R2Bool overlapMultiVolume; + /** + * Cells per tangent axis of the multi-volume grid. + */ + uint32_t overlapSplit; + /** + * Recovery progress patience in steps. + */ + uint32_t overlapPatience; + /** + * Relative overlap-volume decrease that counts as recovery progress. + */ + R2Real overlapProgressMargin; +} R2SoftRecoverySettings; + +#if defined(RAPIER_FEM) +/** + * Plain configuration data; initialize defaults, edit, then apply. No destructor. + * @ingroup soft_bodies + */ +typedef struct R2SoftFemParameters { + /** + * FEM iterative linear-solver tolerance. + */ + R2Real linearTolerance; + /** + * Maximum FEM linear-solver iterations. + */ + size_t maxLinearIterations; + /** + * Maximum degrees of freedom solved by the dense FEM solver. + */ + size_t maxDenseDofs; +} R2SoftFemParameters; +#endif + +/** + * Plain configuration data; initialize defaults, edit, then apply. No destructor. + * @ingroup soft_bodies + */ +typedef struct R2SoftBodiesSettings { + /** + * Soft-body crossing detection and recovery settings. + */ + struct R2SoftRecoverySettings recovery; + /** + * Strain threshold for re-solving soft constraints after contacts within a substep. + */ + R2Real resweepStrain; + /** + * Maximum additional substeps requested by soft-body motion. + */ + size_t maxExtraSubsteps; + /** + * Multiplier on contact natural frequency for soft-body contacts. + */ + R2Real contactStiffening; +#if defined(RAPIER_FEM) + /** + * FEM linear-solver settings, present only when RAPIER_FEM is enabled. + */ + struct R2SoftFemParameters fem; +#endif +} R2SoftBodiesSettings; + +/** + * Plain configuration data; initialize defaults, edit, then apply. No destructor. + * @ingroup worlds + */ +typedef struct R2IntegrationParameters { + /** + * Simulation step duration in seconds. + */ + R2Real dt; + /** + * Minimum CCD substep duration in seconds. + */ + R2Real minCcdDt; + /** + * Spring coefficients for dynamic contact constraints. + */ + struct R2SpringCoefficients contactSoftness; + /** + * Spring coefficients for contacts against fixed bodies. + */ + struct R2SpringCoefficients staticContactSoftness; + /** + * Scale applied to cached impulses when warmstarting. + */ + R2Real warmstartCoefficient; + /** + * Typical world-space length of one meter; scales solver tolerances, not geometry. + */ + R2Real lengthUnit; + /** + * Soft-body integration and recovery settings. + */ + struct R2SoftBodiesSettings softBodies; + /** + * Allowed penetration divided by lengthUnit. + */ + R2Real normalizedAllowedLinearError; + /** + * Maximum penetration-correction speed divided by lengthUnit. + */ + R2Real normalizedMaxCorrectiveVelocity; + /** + * Speculative-contact distance divided by lengthUnit. + */ + R2Real normalizedPredictionDistance; + /** + * Maximum linear speed divided by lengthUnit. + */ + R2Real normalizedMaxLinearVelocity; + /** + * Number of solver substeps/iterations; must be positive. + */ + size_t numSolverIterations; + /** + * PGS iterations per solver substep. + */ + size_t numInternalPgsIterations; + /** + * Stabilization iterations after velocity solving. + */ + size_t numInternalStabilizationIterations; + /** + * Maximum CCD substeps; 0 disables all CCD for the world. + */ + size_t maxCcdSubsteps; + /** + * Whether to cluster contacts for solving. + */ + R2Bool contactClustering; + /** + * Whether to reuse nearby contacts between steps. + */ + R2Bool contactRecycling; + /** + * Contact recycling distance divided by lengthUnit. + */ + R2Real normalizedContactRecycleDistance; + /** + * Whether to solve friction in the bias pass. + */ + R2Bool frictionInBiasPass; + /** + * Whether to warmstart joint constraints. + */ + R2Bool warmstartJoints; +#if defined(RAPIER_DIM3) + /** + * Friction model: 0 simplified, 1 Coulomb (3D only). + */ + uint32_t frictionModel; +#endif +} R2IntegrationParameters; + +/** + * Status-returning operations use these integer codes. + * @ingroup errors + */ +typedef uint32_t R2Status; + +/** + * Cartesian vector with two or three components. + * @ingroup math + */ +typedef struct R2Vector { + /** + * X component. + */ + R2Real x; + /** + * Y component. + */ + R2Real y; +#if defined(RAPIER_DIM3) + /** + * Z component. + */ + R2Real z; +#endif +} R2Vector; + +/** + * 2D: angle in radians. 3D: unit quaternion in x,y,z,w order (normalized on input). + * @ingroup math + */ +typedef struct R2Rotation { +#if defined(RAPIER_DIM2) + /** + * Rotation angle in radians. + */ + R2Real angle; +#endif +#if defined(RAPIER_DIM3) + /** + * X component. + */ + R2Real x; +#endif +#if defined(RAPIER_DIM3) + /** + * Y component. + */ + R2Real y; +#endif +#if defined(RAPIER_DIM3) + /** + * Z component. + */ + R2Real z; +#endif +#if defined(RAPIER_DIM3) + /** + * Quaternion scalar component. + */ + R2Real w; +#endif +} R2Rotation; + +/** + * Rigid transform combining translation and rotation. Use TranslationPose with a zero vector for + * identity. + * @ingroup math + */ +typedef struct R2Pose { + /** + * Translation vector. + */ + struct R2Vector translation; + /** + * Rotation value. + */ + struct R2Rotation rotation; +} R2Pose; + +/** + * Lower and upper axis limits in length units or radians. + * @ingroup joints + */ +typedef struct R2JointLimits { + /** + * Minimum allowed axis displacement (length or radians). + */ + R2Real min; + /** + * Maximum allowed axis displacement (length or radians). + */ + R2Real max; +} R2JointLimits; + +/** + * Position/velocity motor settings for one joint axis. + * @ingroup joints + */ +typedef struct R2JointMotor { + /** + * Motor target velocity (length per second or radians per second). + */ + R2Real targetVel; + /** + * Motor target position (length or radians). + */ + R2Real targetPos; + /** + * Nonnegative motor spring stiffness. + */ + R2Real stiffness; + /** + * Nonnegative motor damping. + */ + R2Real damping; + /** + * Nonnegative maximum force or torque. + */ + R2Real maxForce; + /** + * Motor model: 0 acceleration-based, 1 force-based. + */ + uint32_t model; +} R2JointMotor; + +/** + * Application-defined 128-bit value. Contains no owned pointers. + * @ingroup math + */ +typedef struct R2UserData { + /** + * Low 64 bits of the application value. + */ + uint64_t low; + /** + * High 64 bits of the application value. + */ + uint64_t high; +} R2UserData; + +/** + * Copyable joint configuration. Limits/motors take effect when their axis mask is enabled. + * Solver impulses are deliberately excluded. Applying data resets cached limit and motor impulses. + * @ingroup joints + */ +typedef struct R2JointDesc { + /** + * Joint frame in body 1 local coordinates. + */ + struct R2Pose localFrame1; + /** + * Joint frame in body 2 local coordinates. + */ + struct R2Pose localFrame2; + /** + * Locked joint degrees of freedom; translations precede rotations. + */ + uint8_t lockedAxes; + /** + * Axis mask enabling corresponding limits entries. + */ + uint8_t limitAxes; + /** + * Axis mask enabling corresponding motors entries. + */ + uint8_t motorAxes; + /** + * Axis mask sharing a coupled constraint. + */ + uint8_t coupledAxes; + /** + * Axis limits in translation-then-rotation order. + */ + struct R2JointLimits limits[R2_JOINT_DOF_COUNT]; + /** + * Axis motors in translation-then-rotation order. + */ + struct R2JointMotor motors[R2_JOINT_DOF_COUNT]; + /** + * Spring coefficients for constraint correction. + */ + struct R2SpringCoefficients softness; + /** + * Whether connected bodies may collide. + */ + R2Bool contactsEnabled; + /** + * Whether this setting/object is enabled (0 or 1). + */ + R2Bool enabled; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + struct R2UserData userData; +} R2JointDesc; + +/** + * Copyable non-owning handle: world pointer plus entity index and generation. + * The world must remain alive throughout every use. Copying does not retain it. + * UINT32_MAX/UINT32_MAX with a NULL world is invalid. + * @ingroup joints + */ +typedef struct R2ImpulseJointHandle { + /** + * Borrowed owning world. Never use this handle after freeing that world. + */ + struct R2World *world; + /** + * Zero-based element index. + */ + uint32_t index; + /** + * Slot generation used to reject stale handles. Do not modify it. + */ + uint32_t generation; +} R2ImpulseJointHandle; + +/** + * Copyable non-owning handle: world pointer plus entity index and generation. + * The world must remain alive throughout every use. Copying does not retain it. + * UINT32_MAX/UINT32_MAX with a NULL world is invalid. + * @ingroup rigid_bodies + */ +typedef struct R2RigidBodyHandle { + /** + * Borrowed owning world. Never use this handle after freeing that world. + */ + struct R2World *world; + /** + * Slot index; UINT32_MAX denotes the explicit invalid handle. + */ + uint32_t index; + /** + * Slot generation used to reject stale handles. Do not modify it. + */ + uint32_t generation; +} R2RigidBodyHandle; + +/** + * Copyable non-owning handle: world pointer plus entity index and generation. + * The world must remain alive throughout every use. Copying does not retain it. + * UINT32_MAX/UINT32_MAX with a NULL world is invalid. + * @ingroup joints + */ +typedef struct R2MultibodyJointHandle { + /** + * Borrowed owning world. Never use this handle after freeing that world. + */ + struct R2World *world; + /** + * Zero-based element index. + */ + uint32_t index; + /** + * Slot generation used to reject stale handles. Do not modify it. + */ + uint32_t generation; +} R2MultibodyJointHandle; + +/** + * Parameters for the native volumetric mesher. Enclosure: 0 cover, 1 crust (3D). + * @ingroup shapes + */ +typedef struct R2VolumeMeshParameters { + /** + * Positive target cell size for volume meshing. + */ + R2Real cell_size; +#if defined(RAPIER_DIM2) + /** + * Minimum target element angle in radians; above pi/6, refinement may stop early. + */ + R2Real min_angle; +#endif +#if defined(RAPIER_DIM3) + /** + * Enclosure strategy: 0 covers the whole shape, 1 encloses only its surface. + */ + uint32_t enclosure; +#endif +#if defined(RAPIER_DIM3) + /** + * Number of cover-smoothing passes. + */ + uint32_t cover_smoothing; +#endif +#if defined(RAPIER_DIM3) + /** + * Minimum smoothed-cover distance from the shape, as a fraction of local cell size. + */ + R2Real cover_guard; +#endif +#if defined(RAPIER_DIM3) + /** + * Maximum boundary-cell halvings below cell_size before cover smoothing. + */ + uint32_t cover_subdivisions; +#endif +} R2VolumeMeshParameters; + +/** + * Borrowed array of vector elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R2VectorView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R2Vector *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2VectorView; + +/** + * Borrowed array of real elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R2RealView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const R2Real *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2RealView; + +/** + * Borrowed array of index elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R2IndexView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const uint32_t *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2IndexView; + +/** + * Vertex indices for one edge; contiguous u32 fields with no padding. + * @ingroup math + */ +typedef struct R2Edge { + /** + * First vertex index. + */ + uint32_t a; + /** + * Second vertex index. + */ + uint32_t b; +} R2Edge; + +/** + * Borrowed array of edge elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R2EdgeView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R2Edge *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2EdgeView; + +/** + * Spring-coefficient override for one soft-body edge. + * @ingroup soft_bodies + */ +typedef struct R2SoftEdgeSoftness { + /** + * Zero-based edge index. + */ + uint32_t edge; + /** + * Spring coefficients for constraint correction. + */ + struct R2SpringCoefficients softness; +} R2SoftEdgeSoftness; + +/** + * Borrowed elements; count counts elements. Data must remain live through insertion. + * @ingroup soft_bodies + */ +typedef struct R2SoftEdgeSoftnessView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R2SoftEdgeSoftness *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2SoftEdgeSoftnessView; + +/** + * Tear-resistance override for one soft-body edge. + * @ingroup soft_bodies + */ +typedef struct R2SoftEdgeTear { + /** + * Zero-based edge index. + */ + uint32_t edge; + /** + * Nonnegative edge tear-resistance multiplier. + */ + R2Real resistance; +} R2SoftEdgeTear; + +/** + * Borrowed elements; count counts elements. Data must remain live through insertion. + * @ingroup soft_bodies + */ +typedef struct R2SoftEdgeTearView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R2SoftEdgeTear *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2SoftEdgeTearView; + +/** + * Vertex indices for one triangle; contiguous u32 fields with no padding. + * @ingroup math + */ +typedef struct R2Triangle { + /** + * First vertex index. + */ + uint32_t a; + /** + * Second vertex index. + */ + uint32_t b; + /** + * Third vertex index. + */ + uint32_t c; +} R2Triangle; + +/** + * Borrowed array of triangle elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R2TriangleView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R2Triangle *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2TriangleView; + +/** + * Vertex indices for one tetrahedron; contiguous u32 fields with no padding. + * @ingroup math + */ +typedef struct R2Tetrahedron { + /** + * First vertex index. + */ + uint32_t a; + /** + * Second vertex index. + */ + uint32_t b; + /** + * Third vertex index. + */ + uint32_t c; + /** + * Fourth vertex index. + */ + uint32_t d; +} R2Tetrahedron; + +/** + * Borrowed array of tetrahedron elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R2TetrahedronView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R2Tetrahedron *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2TetrahedronView; + +#if defined(RAPIER_DIM2) +/** + * Borrowed cell array: triangles in 2D, tetrahedra in 3D. + * @ingroup math + */ +typedef struct R2TriangleView R2CellView; +#endif + +#if defined(RAPIER_DIM3) +/** + * Borrowed cell array: triangles in 2D, tetrahedra in 3D. + * @ingroup math + */ +typedef struct R2TetrahedronView R2CellView; +#endif + +#if defined(RAPIER_DIM2) +/** + * Borrowed surface array: edges in 2D, triangles in 3D. + * @ingroup math + */ +typedef struct R2EdgeView R2SurfaceElementView; +#endif + +#if defined(RAPIER_DIM3) +/** + * Borrowed surface array: edges in 2D, triangles in 3D. + * @ingroup math + */ +typedef struct R2TriangleView R2SurfaceElementView; +#endif + +/** + * Vertex indices for one dihedral; contiguous u32 fields with no padding. + * @ingroup math + */ +typedef struct R2Dihedral { + /** + * First vertex index. + */ + uint32_t a; + /** + * Second vertex index. + */ + uint32_t b; + /** + * Third vertex index. + */ + uint32_t c; + /** + * Fourth vertex index. + */ + uint32_t d; +} R2Dihedral; + +/** + * Borrowed array of dihedral elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R2DihedralView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R2Dihedral *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2DihedralView; + +/** + * Optional boolean override. When disabled, retain the recipe's native default. + * @ingroup math + */ +typedef struct R2OptionalBool { + /** + * Whether this setting/object is enabled (0 or 1). + */ + R2Bool enabled; + /** + * Value used when enabled is 1. + */ + R2Bool value; +} R2OptionalBool; + +/** + * Opaque SharedShape. See the ownership and borrowing contract in README.md. + * @ingroup shapes + */ +typedef struct R2SharedShape R2SharedShape; + + +/** + * Borrowed elements; count counts elements. Data must remain live through insertion. + * @ingroup shapes + */ +typedef struct R2CompoundShapeView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R2CompoundShapeDesc *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2CompoundShapeView; + +/** + * Non-owning shape description. Only fields selected by kind are read. + * a = cuboid half extents, capsule/segment endpoint, triangle vertex, or halfspace normal. + * b/c = remaining endpoints/vertices. radius is also the rounded-cuboid border radius. + * Mesh views count edges or triangles; heightfields are column-major. + * Arrays, compound children, and sharedShape remain borrowed until build/insert returns. + * @ingroup shapes + */ +typedef struct R2ShapeDesc { + /** + * Discriminant selecting which description fields are read. + */ + uint32_t kind; + /** + * Half extents, endpoint/vertex, or halfspace normal, selected by kind. + */ + struct R2Vector a; + /** + * Second endpoint or triangle vertex, selected by kind. + */ + struct R2Vector b; + /** + * Third triangle vertex. + */ + struct R2Vector c; + /** + * Radius for the selected primitive or recipe. + */ + R2Real radius; + /** + * Half the height of a cylinder or cone. + */ + R2Real halfHeight; + /** + * Rounding radius for a rounded shape. + */ + R2Real borderRadius; + /** + * Borrowed vertex positions. + */ + struct R2VectorView vertices; + /** + * Borrowed triangle topology; count is triangles. + */ + struct R2TriangleView triangles; + /** + * Borrowed edge topology; count is edges. + */ + struct R2EdgeView edges; + /** + * TRIMESH_*, POLYLINE_*, or HEIGHTFIELD_* bitmask selected by kind. + */ + uint32_t flags; + /** + * Borrowed height samples; 3D uses column-major rows * columns samples. + */ + struct R2RealView heights; + /** + * Number of heightfield rows. + */ + size_t rows; + /** + * Number of heightfield columns. + */ + size_t columns; + /** + * Shape scale along each axis. + */ + struct R2Vector scale; + /** + * Borrowed shared geometry; keep its wrapper alive through build/insert. + */ + const R2SharedShape *sharedShape; + /** + * Borrowed compound children and their nested geometry views. + */ + struct R2CompoundShapeView children; +} R2ShapeDesc; + +/** + * One compound child with a local pose and borrowed geometry description. + * @ingroup shapes + */ +typedef struct R2CompoundShapeDesc { + /** + * Pose relative to the compound parent. + */ + struct R2Pose pose; + /** + * Non-owning shape description; build/insert consumes its views synchronously. + */ + struct R2ShapeDesc shape; +} R2CompoundShapeDesc; + + +#if defined(RAPIER_DIM2) +/** + * Angular scalar in 2D or vector in 3D, in radians for angular displacement. + * @ingroup math + */ +typedef R2Real R2AngVector; +#endif + +#if defined(RAPIER_DIM3) +/** + * Angular scalar in 2D or vector in 3D, in radians for angular displacement. + * @ingroup math + */ +typedef struct R2Vector R2AngVector; +#endif + +/** + * Explicit mass and principal inertia, matching MassProperties constructors. Zero mass/inertia + * means infinite. + * @ingroup math + */ +typedef struct R2MassProperties { + /** + * Center of mass in local coordinates. + */ + struct R2Vector local_com; + /** + * Mass; nonnegative when supplied as input. + */ + R2Real mass; + /** + * Principal angular inertia; scalar in 2D, three diagonal entries in 3D. + */ + R2AngVector principal_inertia; +#if defined(RAPIER_DIM3) + /** + * Orientation of principal inertia axes in local coordinates (3D). + */ + struct R2Rotation principal_inertia_local_frame; +#endif +} R2MassProperties; + +/** + * Collision membership/filter masks and their pairwise combination rule. + * @ingroup math + */ +typedef struct R2InteractionGroups { + /** + * Groups this object belongs to, as a 32-bit mask. + */ + uint32_t memberships; + /** + * Membership groups accepted by this object, as a 32-bit mask. + */ + uint32_t filter; + /** + * 0 requires both membership/filter tests; 1 accepts either test. + */ + uint32_t test_mode; +} R2InteractionGroups; + +/** + * Copyable collider construction data. Shape inputs are borrowed, never owned. + * @ingroup colliders + */ +typedef struct R2ColliderDesc { + /** + * Non-owning shape description; build/insert consumes its views synchronously. + */ + struct R2ShapeDesc shape; + /** + * Pose relative to the parent body; world-space for an unparented collider. + */ + struct R2Pose position; + /** + * R2_MASS_DENSITY, R2_MASS_TOTAL, or R2_MASS_PROPERTIES. + */ + uint32_t massMode; + /** + * Nonnegative mass per unit volume. + */ + R2Real density; + /** + * Mass; nonnegative when supplied as input. + */ + R2Real mass; + /** + * Explicit local mass and inertia when massMode selects them. + */ + struct R2MassProperties massProperties; + /** + * Nonnegative friction coefficient. + */ + R2Real friction; + /** + * Nonnegative restitution coefficient. + */ + R2Real restitution; + /** + * R2_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + */ + uint32_t frictionCombineRule; + /** + * R2_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + */ + uint32_t restitutionCombineRule; + /** + * 1 detects intersections without generating contact forces. + */ + R2Bool isSensor; + /** + * Whether this setting/object is enabled (0 or 1). + */ + R2Bool enabled; + /** + * Groups controlling collision detection. + */ + struct R2InteractionGroups collisionGroups; + /** + * Groups controlling contact-force solving. + */ + struct R2InteractionGroups solverGroups; + /** + * Bitmask of body-type pairs allowed to collide. + */ + uint16_t activeCollisionTypes; + /** + * Hook flags enabling pair filtering/contact modification. + */ + uint32_t activeHooks; + /** + * R2_COLLISION_EVENTS and/or R2_CONTACT_FORCE_EVENTS. + */ + uint32_t activeEvents; + /** + * Nonnegative force threshold for force events. + */ + R2Real contactForceEventThreshold; + /** + * Nonnegative extra separation distance around the collider. + */ + R2Real contactSkin; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + struct R2UserData userData; +} R2ColliderDesc; + +/** + * Copyable recipe, not an owned procedural builder. Initialize before editing. + * All array views borrow caller data until build/insert returns; counts + * for topology arrays are element counts (edges, triangles, or tetrahedra). + * Nonempty topology overrides the generator's topology. Zero counts retain it. + * Generator inputs: a/b are rope ends or center/half-extents; cloth uses a/du/dv. + * @ingroup soft_bodies + */ +typedef struct R2SoftBodyDesc { + /** + * Discriminant selecting which description fields are read. + */ + uint32_t kind; + /** + * Recipe origin/center or first rope endpoint; use a recipe constructor. + */ + struct R2Vector a; + /** + * Recipe half extents, second rope endpoint, or tube axis; use a recipe constructor. + */ + struct R2Vector b; + /** + * Cloth basis step along its first parameter axis. + */ + struct R2Vector du; + /** + * Cloth basis step along its second parameter axis. + */ + struct R2Vector dv; + /** + * First recipe resolution; interpretation depends on kind. + */ + size_t nx; + /** + * Second recipe resolution; interpretation depends on kind. + */ + size_t ny; + /** + * Third recipe resolution; interpretation depends on kind. + */ + size_t nz; + /** + * Radius for the selected primitive or recipe. + */ + R2Real radius; + /** + * Radius at the second end of a tapered cloth tube. + */ + R2Real radiusEnd; + /** + * Translation vector. + */ + struct R2Vector translation; + /** + * Optional total mass override for generated particles. + */ + struct R2OptionalReal totalMass; + /** + * Volume-meshing parameters for a volumetric recipe. + */ + struct R2VolumeMeshParameters meshing; + /** + * Borrowed initial particle positions. + */ + struct R2VectorView positions; + /** + * Borrowed per-particle masses; when empty, particleMass is used. + */ + struct R2RealView masses; + /** + * Borrowed indices of pinned particles. + */ + struct R2IndexView pinned; + /** + * Borrowed edge topology; count is edges. + */ + struct R2EdgeView edges; + /** + * Borrowed bending edge constraints. + */ + struct R2EdgeView bendEdges; + /** + * Borrowed indices of edges that resist tension only. + */ + struct R2IndexView tensionOnlyEdges; + /** + * Borrowed per-edge softness overrides. + */ + struct R2SoftEdgeSoftnessView edgeSoftness; + /** + * Borrowed per-edge tear-resistance overrides. + */ + struct R2SoftEdgeTearView edgeTearResistance; + /** + * Borrowed triangles in 2D or tetrahedra in 3D. + */ + R2CellView cells; + /** + * Borrowed boundary edges in 2D or triangles in 3D. + */ + R2SurfaceElementView surface; +#if defined(RAPIER_DIM3) + /** + * Borrowed four-vertex bending constraints. + */ + struct R2DihedralView dihedrals; +#endif +#if defined(RAPIER_DIM3) + /** + * Borrowed wire edges for a surface recipe. + */ + struct R2EdgeView wire; +#endif + /** + * Borrowed skin vertex positions. + */ + struct R2VectorView skinVertices; + /** + * Borrowed skin topology. + */ + R2SurfaceElementView skinIndices; + /** + * Soft-body material coefficients. + */ + struct R2SoftBodyMaterial material; + /** + * R2_SOFT_CELL_VOLUME, R2_SOFT_CELL_COROTATIONAL, or R2_SOFT_CELL_NEO_HOOKEAN. + */ + uint32_t cellModel; + /** + * 0 = constraints, 1 = FEM (requires a library built with FEM). + */ + uint32_t solver; + /** + * Default nonnegative particle mass. + */ + R2Real particleMass; + /** + * Disabled by default: retain the radius computed by the generator. + */ + struct R2OptionalReal particleRadius; + /** + * Whether to preserve volume. + */ + R2Bool volumePreservation; + /** + * Target volume multiplier. + */ + R2Real volumeFactor; + /** + * Optional shape-matching override; disabled retains recipe defaults. + */ + struct R2OptionalBool shapeMatching; + /** + * Whether self-collision is enabled. + */ + R2Bool selfContacts; + /** + * Whether skin elements participate in collision detection. + */ + R2Bool skinCollision; + /** + * Whether the generated collision geometry is enabled. + */ + R2Bool collisionEnabled; + /** + * Collider configuration used by the recipe; shape comes from the generated geometry. + */ + struct R2ColliderDesc collider; + /** + * Nonnegative linear damping coefficient. + */ + R2Real linearDamping; + /** + * Multiplier applied to world gravity. + */ + R2Real gravityScale; + /** + * Extra solver iterations for this body and connected bodies. + */ + size_t additionalSolverIterations; + /** + * Extra PGS iterations for this body. + */ + size_t additionalPgsIterations; + /** + * Whether automatic sleeping is allowed. + */ + R2Bool canSleep; + /** + * Signed dominance group; larger groups dominate smaller groups. + */ + int8_t dominanceGroup; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + struct R2UserData userData; +} R2SoftBodyDesc; + +/** + * Copyable non-owning handle: world pointer plus entity index and generation. + * The world must remain alive throughout every use. Copying does not retain it. + * UINT32_MAX/UINT32_MAX with a NULL world is invalid. + * @ingroup soft_bodies + */ +typedef struct R2SoftBodyHandle { + /** + * Borrowed owning world. Never use this handle after freeing that world. + */ + struct R2World *world; + /** + * Zero-based element index. + */ + uint32_t index; + /** + * Slot generation used to reject stale handles. Do not modify it. + */ + uint32_t generation; +} R2SoftBodyHandle; + +/** + * Non-owning deformable binding description. Direct particle indices are borrowed. + * @ingroup soft_bodies + */ +typedef struct R2SoftMeshBindingDesc { + /** + * Discriminant selecting which description fields are read. + */ + uint32_t kind; + /** + * Borrowed particle indices used for a direct mesh binding. + */ + struct R2IndexView particles; + /** + * Nonnegative positional tolerance for direct-by-position binding. + */ + R2Real epsilon; + /** + * Whether self-collision is enabled. + */ + R2Bool selfContacts; +} R2SoftMeshBindingDesc; + +/** + * Copyable non-owning handle: world pointer plus entity index and generation. + * The world must remain alive throughout every use. Copying does not retain it. + * UINT32_MAX/UINT32_MAX with a NULL world is invalid. + * @ingroup colliders + */ +typedef struct R2ColliderHandle { + /** + * Borrowed owning world. Never use this handle after freeing that world. + */ + struct R2World *world; + /** + * Slot index; UINT32_MAX denotes the explicit invalid handle. + */ + uint32_t index; + /** + * Slot generation used to reject stale handles. Do not modify it. + */ + uint32_t generation; +} R2ColliderHandle; + +/** + * Scene-query flags, groups, and excluded handles. Initialize with r2DefaultQueryFilter. + * @ingroup queries + */ +typedef struct R2QueryFilter { + /** + * R2_QUERY_EXCLUDE_* bitmask selecting body types and sensors/solids. + */ + uint32_t flags; + /** + * Whether to apply the groups filter. + */ + R2Bool use_groups; + /** + * Groups to test when use_groups is 1. + */ + struct R2InteractionGroups groups; + /** + * Collider to exclude; use the explicit invalid handle to exclude none. + */ + struct R2ColliderHandle exclude_collider; + /** + * Body whose colliders are excluded; use the explicit invalid handle for none. + */ + struct R2RigidBodyHandle exclude_rigid_body; +} R2QueryFilter; + +/** + * Called with scoped read access and a collider handle. Shared queries may nest; + * world mutations are rejected until the outer query returns. Never retain the context. + * @ingroup queries + */ +typedef R2Bool (RAPIER_CALL *R2QueryPredicate)(void *user_data, + const struct R2ReadContext *read, + struct R2ColliderHandle handle); + +/** + * Copyable query settings. They borrow callback data, never world components. + * @ingroup queries + */ +typedef struct R2QueryOptions { + /** + * Query selection settings. + */ + struct R2QueryFilter filter; + /** + * Optional additional query filter; nonzero accepts a collider. + */ + R2QueryPredicate predicate; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + void *userData; +} R2QueryOptions; + +/** + * Closest ray intersection, with a world-space normal. + * @ingroup queries + */ +typedef struct R2RayHit { + /** + * World-bound collider handle. + */ + struct R2ColliderHandle collider; + /** + * Ray/sweep parameter at first impact, bounded by the query options. + */ + R2Real time_of_impact; + /** + * World-space contact or surface normal. + */ + struct R2Vector normal; + /** + * Shape feature kind: R2_FEATURE_UNKNOWN, R2_FEATURE_VERTEX, R2_FEATURE_EDGE, or + * R2_FEATURE_FACE. + */ + uint32_t feature_type; + /** + * Index within the feature kind; zero for unknown. + */ + uint32_t feature_id; +} R2RayHit; + +/** + * Closest projected world-space point and its collider. + * @ingroup queries + */ +typedef struct R2PointProjection { + /** + * World-bound collider handle. + */ + struct R2ColliderHandle collider; + /** + * Projected world-space point. + */ + struct R2Vector point; + /** + * Whether the original point was inside the collider. + */ + R2Bool is_inside; +} R2PointProjection; + +/** + * Shape-cast impact geometry; collider-side data is world-space, moving-shape data is local. + * @ingroup queries + */ +typedef struct R2ShapeCastHit { + /** + * World-bound collider handle. + */ + struct R2ColliderHandle collider; + /** + * Ray/sweep parameter at first impact, bounded by the query options. + */ + R2Real time_of_impact; + /** + * Impact witness on the collider, in world coordinates. + */ + struct R2Vector witness1; + /** + * Impact witness on the moving shape, in its local coordinates. + */ + struct R2Vector witness2; + /** + * Impact normal on the collider, in world coordinates. + */ + struct R2Vector normal1; + /** + * Impact normal on the moving shape, in its local coordinates. + */ + struct R2Vector normal2; + /** + * Native result: 0 out of iterations, 1 converged, 2 failed, 3 penetrating or within target + * distance. + */ + uint32_t status; +} R2ShapeCastHit; + +/** + * Sweep termination settings. Initialize with r2DefaultShapeCastOptions. + * @ingroup queries + */ +typedef struct R2ShapeCastOptions { + /** + * Maximum sweep parameter; movement is velocity multiplied by this time. + */ + R2Real max_time_of_impact; + /** + * Nonnegative separation at which a shape cast counts as a hit. + */ + R2Real target_distance; + /** + * Whether to stop at t = 0 for an initial overlap. + */ + R2Bool stop_at_penetration; + /** + * Whether to compute witness points/normals for an initial overlap. + */ + R2Bool compute_impact_geometry_on_penetration; +} R2ShapeCastOptions; + +/** + * Axis-aligned bounding box. + * @ingroup math + */ +typedef struct R2Aabb { + /** + * Minimum corner in each coordinate. + */ + struct R2Vector mins; + /** + * Maximum corner in each coordinate. + */ + struct R2Vector maxs; +} R2Aabb; + +/** + * Optional ray collider/time result. A miss is found = 0 with status OK. + * @ingroup queries + */ +typedef struct R2RayToi { + /** + * World-bound collider handle. + */ + struct R2ColliderHandle collider; + /** + * Ray parameter t at impact: origin + direction * t. + */ + R2Real toi; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R2Bool found; +} R2RayToi; + +/** + * Optional full ray result. A miss is found = 0 with status OK. + * @ingroup queries + */ +typedef struct R2OptionalRayHit { + /** + * Shape/ray impact details. + */ + struct R2RayHit hit; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R2Bool found; +} R2OptionalRayHit; + +/** + * Stack-allocated rigid-body construction data. Initialize with r2DynamicRigidBodyDesc, + * r2FixedRigidBodyDesc, or a kinematic description constructor. + * Copying this value is safe; it owns no resources and must never be freed by Rapier. + * @ingroup rigid_bodies + */ +typedef struct R2RigidBodyDesc { + /** + * World-space pose. + */ + struct R2Pose position; + /** + * World-space linear velocity. + */ + struct R2Vector linvel; + /** + * World-space angular velocity in radians per second. + */ + R2AngVector angvel; + /** + * R2_DYNAMIC, R2_FIXED, R2_KINEMATIC_POSITION_BASED, or R2_KINEMATIC_VELOCITY_BASED. + */ + uint32_t bodyType; + /** + * Multiplier applied to world gravity. + */ + R2Real gravityScale; + /** + * Nonnegative linear damping coefficient. + */ + R2Real linearDamping; + /** + * Nonnegative angular damping coefficient. + */ + R2Real angularDamping; + /** + * Nonnegative mass added to attached collider contributions. + */ + R2Real additionalMass; + /** + * 1 uses additionalMassProperties; 0 uses additionalMass. + */ + R2Bool useAdditionalMassProperties; + /** + * Additional body-local mass and inertia when enabled. + */ + struct R2MassProperties additionalMassProperties; + /** + * Locked-axis bitmask; translations precede rotations. + */ + uint8_t lockedAxes; + /** + * Whether automatic sleeping is allowed. + */ + R2Bool canSleep; + /** + * Whether the body starts/is asleep. + */ + R2Bool sleeping; + /** + * Whether continuous collision detection is enabled. + */ + R2Bool ccdEnabled; + /** + * Nonnegative prediction distance for soft CCD. + */ + R2Real softCcdPrediction; + /** + * Whether to allow fast rotations without the native angular-motion clamp. + */ + R2Bool allowFastRotation; + /** + * Whether this setting/object is enabled (0 or 1). + */ + R2Bool enabled; + /** + * Signed dominance group; larger groups dominate smaller groups. + */ + int8_t dominanceGroup; + /** + * Extra solver iterations for this body and connected bodies. + */ + size_t additionalSolverIterations; + /** + * Extra PGS iterations for this body. + */ + size_t additionalPgsIterations; + /** + * Whether to include gyroscopic forces (3D). + */ + R2Bool gyroscopicForcesEnabled; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + struct R2UserData userData; +} R2RigidBodyDesc; + +/** + * Sizes of the POD types in this library build, for foreign-language layout checks. + * @ingroup math + */ +typedef struct R2PodLayout { + /** + * Size in bytes of the rigidBodyDesc structure; zero when unavailable. + */ + size_t rigidBodyDesc; + /** + * Size in bytes of the colliderDesc structure; zero when unavailable. + */ + size_t colliderDesc; + /** + * Size in bytes of the shapeDesc structure; zero when unavailable. + */ + size_t shapeDesc; + /** + * Size in bytes of the jointDesc structure; zero when unavailable. + */ + size_t jointDesc; + /** + * Size in bytes of the softBodyMaterial structure; zero when unavailable. + */ + size_t softBodyMaterial; + /** + * Size in bytes of the integrationParameters structure; zero when unavailable. + */ + size_t integrationParameters; + /** + * Size in bytes of the softBodyDesc structure; zero when unavailable. + */ + size_t softBodyDesc; + /** + * Size in bytes of the softMeshBindingDesc structure; zero when unavailable. + */ + size_t softMeshBindingDesc; + /** + * Size in bytes of the queryOptions structure; zero when unavailable. + */ + size_t queryOptions; + /** + * Zero unless 3D f32 robotics is enabled. + */ + size_t urdfLoaderOptions; + /** + * Zero unless 3D f32 robotics is enabled. + */ + size_t mjcfLoaderOptions; +} R2PodLayout; + +/** + * CharacterLength counterpart: relative=1 scales with character height, relative=0 uses world + * units. + * @ingroup controllers + */ +typedef struct R2CharacterLength { + /** + * Value used when enabled is 1. + */ + R2Real value; + /** + * 1 scales value by the character shape size; 0 uses an absolute length. + */ + R2Bool relative; +} R2CharacterLength; + +/** + * Allowed character motion and ground-contact state. + * @ingroup controllers + */ +typedef struct R2CharacterMovement { + /** + * Allowed world-space displacement; not applied automatically. + */ + struct R2Vector translation; + /** + * Whether the character touches the ground after movement. + */ + R2Bool grounded; + /** + * Whether motion includes sliding down a non-climbable slope. + */ + R2Bool is_sliding_down_slope; +} R2CharacterMovement; + +/** + * Collision recorded during character movement. + * @ingroup controllers + */ +typedef struct R2CharacterCollision { + /** + * World-bound collider handle. + */ + struct R2ColliderHandle collider; + /** + * World-space character pose at collision. + */ + struct R2Pose character_pos; + /** + * World-space translation already applied before collision. + */ + struct R2Vector translation_applied; + /** + * World-space translation remaining at collision. + */ + struct R2Vector translation_remaining; + /** + * Shape/ray impact details. + */ + struct R2ShapeCastHit hit; +} R2CharacterCollision; + +/** + * Per-axis proportional, integral, and derivative controller gains. + * @ingroup controllers + */ +typedef struct R2PidGains { + /** + * Linear proportional gain per axis. + */ + struct R2Vector lin_kp; + /** + * Linear integral gain per axis. + */ + struct R2Vector lin_ki; + /** + * Linear derivative gain per axis. + */ + struct R2Vector lin_kd; + /** + * Angular proportional gain per axis. + */ + R2AngVector ang_kp; + /** + * Angular integral gain per axis. + */ + R2AngVector ang_ki; + /** + * Angular derivative gain per axis. + */ + R2AngVector ang_kd; +} R2PidGains; + +/** + * Linear and angular velocity correction computed by a controller. + * @ingroup controllers + */ +typedef struct R2VelocityCorrection { + /** + * World-space linear velocity correction. + */ + struct R2Vector linear; + /** + * World-space angular velocity correction, in radians per second. + */ + R2AngVector angularVelocity; +} R2VelocityCorrection; + +/** + * Copy of character sliding, slope, and snapping settings. + * @ingroup controllers + */ +typedef struct R2CharacterControllerSettings { + /** + * Whether obstacle sliding is enabled. + */ + R2Bool slide; + /** + * Maximum climbable slope angle in radians. + */ + R2Real max_slope_climb_angle; + /** + * Minimum slope angle for sliding, in radians. + */ + R2Real min_slope_slide_angle; + /** + * Whether downward ground snapping is enabled. + */ + R2Bool snap_to_ground; + /** + * Maximum downward snapping distance. + */ + struct R2CharacterLength snap_distance; +} R2CharacterControllerSettings; + +#if defined(RAPIER_DIM3) +/** + * Wheel suspension/friction parameters. Initialize with r2DefaultWheelTuning. + * @ingroup controllers + */ +typedef struct R2WheelTuning { + /** + * Nonnegative suspension spring stiffness. + */ + R2Real suspension_stiffness; + /** + * Nonnegative damping coefficient during suspension compression. + */ + R2Real suspension_compression; + /** + * Nonnegative suspension relaxation damping. + */ + R2Real suspension_damping; + /** + * Maximum suspension travel in length units. + */ + R2Real max_suspension_travel; + /** + * Maximum tire friction/slip coefficient. + */ + R2Real friction_slip; + /** + * Maximum force exerted by suspension. + */ + R2Real max_suspension_force; + /** + * Sideways tire friction stiffness. + */ + R2Real side_friction_stiffness; +} R2WheelTuning; +#endif + +#if defined(RAPIER_DIM3) +/** + * Copy of the current wheel pose, suspension, and contact state. + * @ingroup controllers + */ +typedef struct R2WheelState { + /** + * World-space wheel center. + */ + struct R2Vector center; + /** + * World-space suspension direction. + */ + struct R2Vector suspension; + /** + * World-space axle direction. + */ + struct R2Vector axle; + /** + * Wheel rotation angle in radians. + */ + R2Real rotation; + /** + * Current suspension force. + */ + R2Real suspension_force; + /** + * Current suspension length. + */ + R2Real suspension_length; + /** + * Whether the wheel has ground contact. + */ + R2Bool is_in_contact; + /** + * Ground collider handle, invalid when there is no contact. + */ + struct R2ColliderHandle ground_object; + /** + * World-space ground contact point. + */ + struct R2Vector contact_point; + /** + * World-space ground contact normal. + */ + struct R2Vector contact_normal; +} R2WheelState; +#endif + +/** + * Called synchronously on the calling thread when an operation reports an error. + * The diagnostic is borrowed for the duration of the callback. The callback + * must return normally or terminate the process: never throw or longjmp across + * the Rust/C boundary. Nested failing calls do not invoke the handler recursively. + * @ingroup errors + */ +typedef void (RAPIER_CALL *R2ErrorCallback)(R2Status, const char*, void*); + +/** + * An optional thread-local error handler. A null callback disables reporting. + * Keep the callback and user_data alive until the handler is replaced. + * @ingroup errors + */ +typedef struct R2ErrorHandler { + /** + * Optional error callback; NULL disables notifications. + */ + R2ErrorCallback callback; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + void *user_data; +} R2ErrorHandler; + +/** + * World-bound handles of the two connected bodies. + * @ingroup joints + */ +typedef struct R2JointBodies { + /** + * First connected body. + */ + struct R2RigidBodyHandle body1; + /** + * Second connected body. + */ + struct R2RigidBodyHandle body2; +} R2JointBodies; + +/** + * Damped least-squares inverse-kinematics parameters. + * @ingroup math + */ +typedef struct R2InverseKinematicsOptions { + /** + * Nonnegative motor damping. + */ + R2Real damping; + /** + * Maximum inverse-kinematics iterations. + */ + size_t max_iters; + /** + * Controlled-axis bitmask; translations precede rotations. + */ + uint8_t constrained_axes; + /** + * Linear convergence tolerance. + */ + R2Real epsilon_linear; + /** + * Angular convergence tolerance in radians. + */ + R2Real epsilon_angular; +} R2InverseKinematicsOptions; + +/** + * Optional per-link filter, called synchronously. Must not reenter or retain physics objects. + * @ingroup joints + */ +typedef R2Bool (RAPIER_CALL *R2IkJointCanMove)(void*, struct R2RigidBodyHandle); + +/** + * Collision start/stop flags match Rapier CollisionEventFlags. + * @ingroup events + */ +typedef struct R2CollisionEvent { + /** + * First collider in the pair. + */ + struct R2ColliderHandle collider1; + /** + * Second collider in the pair. + */ + struct R2ColliderHandle collider2; + /** + * 1 for a starting event, 0 for a stopping event. + */ + R2Bool started; + /** + * Event flags: bit 0 sensor pair, bit 1 removed collider. + */ + uint32_t flags; +} R2CollisionEvent; + +/** + * Contact-force event, enabled by flags and the collider force threshold. + * @ingroup events + */ +typedef struct R2ContactForceEvent { + /** + * First collider in the pair. + */ + struct R2ColliderHandle collider1; + /** + * Second collider in the pair. + */ + struct R2ColliderHandle collider2; + /** + * Sum of world-space contact forces. + */ + struct R2Vector total_force; + /** + * Sum of contact force magnitudes. + */ + R2Real total_force_magnitude; + /** + * World-space direction of the strongest contact force. + */ + struct R2Vector max_force_direction; + /** + * Magnitude of the strongest contact force. + */ + R2Real max_force_magnitude; + /** + * 1 for a starting event, 0 for a stopping event. + */ + R2Bool started; +} R2ContactForceEvent; + +/** + * Pair callback: -1 rejects a contact pair; 0 detects contacts without impulses; 1 computes + * impulses. + * For sensor intersections only, zero rejects and any positive value accepts. + * @ingroup math + */ +typedef int32_t (RAPIER_CALL *R2PairFilter)(void *user_data, + const struct R2ReadContext *read, + struct R2ColliderHandle collider1, + struct R2ColliderHandle collider2, + struct R2RigidBodyHandle body1, + struct R2RigidBodyHandle body2); + +/** + * Mutable per-manifold properties. Set enabled=0 to discard all its solver contacts. + * @ingroup events + */ +typedef struct R2ContactModification { + /** + * World-space contact or surface normal. + */ + struct R2Vector normal; + /** + * Nonnegative friction coefficient. + */ + R2Real friction; + /** + * Nonnegative restitution coefficient. + */ + R2Real restitution; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + uint32_t user_data; + /** + * Whether this setting/object is enabled (0 or 1). + */ + R2Bool enabled; +} R2ContactModification; + +/** + * Modify aggregate contact properties for one pair; contact is mutable only during this callback. + * @ingroup callbacks + */ +typedef void (RAPIER_CALL *R2ModifyContacts)(void *user_data, + const struct R2ReadContext *read, + struct R2ColliderHandle collider1, + struct R2ColliderHandle collider2, + struct R2ContactModification *contact); + +/** + * Modify individual solver contacts through a borrowed context, valid only during the callback. + * @ingroup callbacks + */ +typedef void (RAPIER_CALL *R2ModifyContactContext)(void *user_data, + const struct R2ReadContext *read, + struct R2ColliderHandle collider1, + struct R2ColliderHandle collider2, + struct R2ContactModificationContext *context); + +/** + * Callbacks must not unwind or retain arguments. Use their ReadContext to inspect bodies and + * colliders; ordinary access to the stepping world returns WORLD_BUSY. Mutations must be + * performed after stepping. With parallel builds + * callbacks and their user_data must be safe for concurrent invocation. NULL callbacks use + * defaults. + * @ingroup callbacks + */ +typedef struct R2PhysicsHooks { + /** + * Application data; Rapier does not own pointers encoded in it. + */ + void *user_data; + /** + * Optional contact-pair filter, called only for colliders enabling the hook. + */ + R2PairFilter filter_contact_pair; + /** + * Optional sensor-pair filter, called only for colliders enabling the hook. + */ + R2PairFilter filter_intersection_pair; + /** + * Optional legacy aggregate contact-edit callback. + */ + R2ModifyContacts modify_solver_contacts; + /** + * Runs after the legacy property callback. Context accessors may be called here. + */ + R2ModifyContactContext modify_solver_contacts_context; +} R2PhysicsHooks; + +/** + * Borrowed bytes. Valid while the source Bytes object remains alive; never free data. + * @ingroup math + */ +typedef struct R2ByteView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const uint8_t *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2ByteView; + +/** + * World-space line segment produced by physics debug rendering. + * @ingroup events + */ +typedef struct R2DebugLine { + /** + * World-space start point. + */ + struct R2Vector a; + /** + * World-space end point. + */ + struct R2Vector b; + /** + * RGBA color, four floats. + */ + float color[4]; +} R2DebugLine; + +/** + * Particle remapping after tearing. + * @ingroup math + */ +typedef struct R2ParticleDestination { + /** + * World-bound rigid/soft-body handle, as selected by the field type. + */ + struct R2SoftBodyHandle body; + /** + * Zero-based element index. + */ + uint32_t index; +} R2ParticleDestination; + +/** + * Cluster and proxy remapping after a soft-body split. + * @ingroup soft_bodies + */ +typedef struct R2SoftClusterSplit { + /** + * Original cluster index before splitting. + */ + uint32_t source_cluster; + /** + * Resulting soft-body handle. + */ + struct R2SoftBodyHandle soft_body; + /** + * Cluster index within its soft body. + */ + uint32_t cluster; + /** + * Cluster rigid-proxy body handle. + */ + struct R2RigidBodyHandle proxy; + /** + * Whether this split retains the original proxy. + */ + R2Bool keeps_proxy; +} R2SoftClusterSplit; + +/** + * Impulse-joint movement between rigid proxies after tearing. + * @ingroup soft_bodies + */ +typedef struct R2SoftJointMove { + /** + * Impulse joint that moved between proxies. + */ + struct R2ImpulseJointHandle joint; + /** + * Original rigid-proxy body. + */ + struct R2RigidBodyHandle from; + /** + * Destination rigid-proxy body. + */ + struct R2RigidBodyHandle to; +} R2SoftJointMove; + +/** + * Optional particle remapping; check found before reading the destination. + * @ingroup math + */ +typedef struct R2OptionalParticleDestination { + /** + * World-bound rigid/soft-body handle, as selected by the field type. + */ + struct R2SoftBodyHandle body; + /** + * Zero-based element index. + */ + uint32_t index; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R2Bool found; +} R2OptionalParticleDestination; + +/** + * Runtime ABI identity and fundamental type sizes. + * @ingroup errors + */ +typedef struct R2BuildInfo { + /** + * Binary ABI revision; compare with the header ABI_VERSION. + */ + uint32_t abi_version; + /** + * Spatial dimension, 2 or 3. + */ + uint32_t dimension; + /** + * Size of Real in bytes, 4 or 8. + */ + uint32_t real_size; + /** + * Pointer size in bytes. + */ + uint32_t pointer_size; +} R2BuildInfo; + +/** + * Features available through the loaded C library, independent of consumer defines. + * @ingroup errors + */ +typedef struct R2BuildFeatures { + /** + * Whether native profiling timers were compiled in. + */ + R2Bool profiling; + /** + * Solver SIMD lane count. Hardware instruction width depends on the target CPU. + */ + uint32_t simd_lanes; + /** + * Whether this library exposes Rapier's parallel execution and thread-pool APIs. + */ + R2Bool parallel; +} R2BuildFeatures; + +/** + * Current narrow-phase pair and accumulated solver impulse summary. + * @ingroup events + */ +typedef struct R2ContactPair { + /** + * First collider in the pair. + */ + struct R2ColliderHandle collider1; + /** + * Second collider in the pair. + */ + struct R2ColliderHandle collider2; + /** + * Whether the pair has an active solver contact. + */ + R2Bool has_any_active_contact; + /** + * Sum of world-space contact impulse vectors. + */ + struct R2Vector total_impulse; + /** + * Sum of contact impulse magnitudes. + */ + R2Real total_impulse_magnitude; + /** + * Largest contact impulse magnitude. + */ + R2Real max_impulse; + /** + * World-space direction of the largest contact impulse. + */ + struct R2Vector max_impulse_direction; +} R2ContactPair; + +/** + * Sensor intersection state for a collider pair. + * @ingroup math + */ +typedef struct R2IntersectionPair { + /** + * First collider in the pair. + */ + struct R2ColliderHandle collider1; + /** + * Second collider in the pair. + */ + struct R2ColliderHandle collider2; + /** + * Whether the two sensor/collider shapes intersect. + */ + R2Bool intersecting; +} R2IntersectionPair; + +/** + * One manifold contact and its normal solver impulse. + * @ingroup events + */ +typedef struct R2ContactPoint { + /** + * Index of the contact manifold within its pair. + */ + size_t manifold_index; + /** + * Contact point in collider 1 local coordinates. + */ + struct R2Vector local_p1; + /** + * Contact point in collider 2 local coordinates. + */ + struct R2Vector local_p2; + /** + * World-space contact or surface normal. + */ + struct R2Vector normal; + /** + * Signed separation; negative means penetration. + */ + R2Real distance; + /** + * Normal impulse applied at this contact. + */ + R2Real impulse; +} R2ContactPoint; + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Loader configuration. Initialize with DefaultUrdfLoaderOptions; no destructor. + * Blueprint array views and shared shapes are borrowed through the load call. + * @ingroup robotics + */ +typedef struct R2UrdfLoaderOptions { + /** + * Whether to build colliders from collision geometry. + */ + R2Bool createCollidersFromCollisionShapes; + /** + * Whether to also build colliders from visual geometry. + */ + R2Bool createCollidersFromVisualShapes; + /** + * Whether to use imported mass/inertia properties. + */ + R2Bool applyImportedMassProps; + /** + * Whether bodies connected by imported joints may collide. + */ + R2Bool enableJointCollisions; + /** + * Whether imported root bodies are fixed. + */ + R2Bool makeRootsFixed; + /** + * Whether to merge empty fixed URDF links. + */ + R2Bool squeezeEmptyFixedLinks; + /** + * Transform applied to the imported model. + */ + struct R2Pose shift; + /** + * Shape scale along each axis. + */ + R2Real scale; + /** + * Default collider description; nested geometry resources are borrowed through loading. + */ + struct R2ColliderDesc colliderBlueprint; + /** + * Default rigid-body description used by the importer. + */ + struct R2RigidBodyDesc rigidBodyBlueprint; +} R2UrdfLoaderOptions; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Loader configuration. Initialize with DefaultMjcfLoaderOptions; no destructor. + * Blueprint array views and shared shapes are borrowed through the load call. + * @ingroup robotics + */ +typedef struct R2MjcfLoaderOptions { + /** + * Whether to build colliders from collision geometry. + */ + R2Bool createCollidersFromCollisionShapes; + /** + * Whether to also build colliders from visual geometry. + */ + R2Bool createCollidersFromVisualShapes; + /** + * Whether to use imported mass/inertia properties. + */ + R2Bool applyImportedMassProps; + /** + * Whether bodies connected by imported joints may collide. + */ + R2Bool enableJointCollisions; + /** + * Whether imported root bodies are fixed. + */ + R2Bool makeRootsFixed; + /** + * Whether to omit MJCF plane geometry. + */ + R2Bool skipPlaneGeoms; + /** + * Whether imported joint motors are disabled. + */ + R2Bool disableJointMotors; + /** + * Transform applied to the imported model. + */ + struct R2Pose shift; + /** + * Shape scale along each axis. + */ + R2Real scale; + /** + * Default collider description; nested geometry resources are borrowed through loading. + */ + struct R2ColliderDesc colliderBlueprint; + /** + * Default rigid-body description used by the importer. + */ + struct R2RigidBodyDesc rigidBodyBlueprint; +} R2MjcfLoaderOptions; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * A borrowed visual declaration, valid until its robot is freed or its body storage changes. + * @ingroup robotics + */ +typedef struct R2MjcfVisualMesh R2MjcfVisualMesh; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Imported physically based visual material; no texture ownership. + * @ingroup robotics + */ +typedef struct R2RenderMaterial { + /** + * Material metallic factor. + */ + float metallic; + /** + * Material roughness factor. + */ + float roughness; + /** + * Material reflectance factor. + */ + float reflectance; + /** + * RGB emissive color. + */ + float emissive[3]; +} R2RenderMaterial; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copied metadata for a borrowed MJCF visual mesh. + * @ingroup robotics + */ +typedef struct R2MjcfVisualMeshInfo { + /** + * Visual pose relative to its source body. + */ + struct R2Pose local_pose; + /** + * RGBA visual color. + */ + float rgba[4]; + /** + * Copied render material; meaningful when has_material is 1. + */ + struct R2RenderMaterial material; + /** + * Whether rgba contains an authored color. + */ + R2Bool has_color; + /** + * Whether material contains authored material data. + */ + R2Bool has_material; + /** + * Whether the visual geometry is a triangle mesh. + */ + R2Bool is_trimesh; +} R2MjcfVisualMeshInfo; +#endif + +/** + * A copied state snapshot, with no pointers or ownership obligations. + * @ingroup rigid_bodies + */ +typedef struct R2RigidBodyState { + /** + * World-space pose. + */ + struct R2Pose position; + /** + * World-space linear velocity. + */ + struct R2Vector linvel; + /** + * World-space angular velocity in radians per second. + */ + R2AngVector angvel; + /** + * Whether the body starts/is asleep. + */ + R2Bool sleeping; + /** + * Whether this setting/object is enabled (0 or 1). + */ + R2Bool enabled; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + struct R2UserData userData; +} R2RigidBodyState; + +/** + * Stable identity of a live mesh within one soft body; matches Rapier's SoftMeshId. + * @ingroup soft_bodies + */ +typedef struct R2SoftMeshId { + /** + * Cluster index within its soft body. + */ + uint32_t cluster; + /** + * Mesh index within the cluster. + */ + uint32_t mesh; +} R2SoftMeshId; + +/** + * Mesh identity and rendering metadata. A render-only mesh has an invalid collider handle. + * @ingroup soft_bodies + */ +typedef struct R2SoftMeshInfo { + /** + * Cluster/mesh pair identifying this collision mesh. + */ + struct R2SoftMeshId id; + /** + * World-bound collider handle. + */ + struct R2ColliderHandle collider; + /** + * Indices per mesh element: 2 for an edge or 3 for a triangle. + */ + size_t arity; + /** + * Whether vertex positions are obtained by skinning. + */ + R2Bool is_skinned; + /** + * Whether collision detection is enabled for this mesh. + */ + R2Bool collision_enabled; +} R2SoftMeshInfo; + +#if defined(RAPIER_F32) +/** + * Voxel coordinates have DIM signed integer components. + * @ingroup math + */ +typedef int32_t R2VoxelCoord; +#endif + +#if defined(RAPIER_F64) +/** + * Signed voxel coordinate integer. + * @ingroup math + */ +typedef int64_t R2VoxelCoord; +#endif + +/** + * Integer coordinates of a voxel cell. + * @ingroup math + */ +typedef struct R2VoxelKey { + /** + * X component. + */ + R2VoxelCoord x; + /** + * Y component. + */ + R2VoxelCoord y; +#if defined(RAPIER_DIM3) + /** + * Z component. + */ + R2VoxelCoord z; +#endif +} R2VoxelKey; + +/** + * Optional voxel lookup result; check found before reading voxel data. + * @ingroup queries + */ +typedef struct R2VoxelQuery { + /** + * Integer voxel coordinates. + */ + struct R2VoxelKey key; + /** + * World-space wheel center. + */ + struct R2Vector center; + /** + * Voxel dimensions along each axis. + */ + struct R2Vector size; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R2Bool found; +} R2VoxelQuery; + +/** + * @ingroup errors + * Operation succeeded. + */ +#define R2_OK 0 + +/** + * @ingroup errors + * A required pointer was NULL. + */ +#define R2_NULL_POINTER 1 + +/** + * @ingroup errors + * An argument failed validation. + */ +#define R2_INVALID_ARGUMENT 2 + +/** + * @ingroup errors + * The entity handle is stale, invalid, or belongs to another world. + */ +#define R2_INVALID_HANDLE 3 + +/** + * @ingroup errors + * Output capacity is insufficient; the returned count is the required capacity. + */ +#define R2_BUFFER_TOO_SMALL 4 + +/** + * @ingroup errors + * This build or object does not support the operation. + */ +#define R2_UNSUPPORTED 5 + +/** + * @ingroup errors + * Rust panicked; discard objects mutated by the call. + */ +#define R2_PANIC 6 + +/** + * @ingroup errors + * No matching query result or object was found. + */ +#define R2_NOT_FOUND 7 + +/** + * @ingroup errors + * Conflicting or reentrant access to simulation state. No mutation was performed. + */ +#define R2_WORLD_BUSY 8 + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Return native default soft body material. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SoftBodyMaterial RAPIER_CALL r2DefaultSoftBodyMaterial(void); + +/** + * Return native default soft recovery settings. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SoftRecoverySettings RAPIER_CALL r2DefaultSoftRecoverySettings(void); + +#if defined(RAPIER_FEM) +/** + * Return native default soft fem parameters. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SoftFemParameters RAPIER_CALL r2DefaultSoftFemParameters(void); +#endif + +/** + * Return native default soft bodies settings. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SoftBodiesSettings RAPIER_CALL r2DefaultSoftBodiesSettings(void); + +/** + * Return native default integration parameters. This POD value owns no resources. + * @ingroup worlds + */ +RAPIER_API struct R2IntegrationParameters RAPIER_CALL r2DefaultIntegrationParameters(void); + +/** + * Return a copy of all world integration settings. + * @ingroup worlds + */ +RAPIER_API struct R2IntegrationParameters RAPIER_CALL r2IntegrationParameters(const struct R2World *world); + +/** + * Copies validated values; does not expose a writable alias to Rust memory. + * @ingroup worlds + */ +RAPIER_API +R2Status RAPIER_CALL r2SetIntegrationParameters(struct R2World *world, + const struct R2IntegrationParameters *data); + +/** + * Return native default joint desc. This POD value owns no resources. + * @ingroup joints + */ +RAPIER_API struct R2JointDesc RAPIER_CALL r2DefaultJointDesc(void); + +/** + * Return a fixed joint description with native defaults; no allocation. + * @ingroup joints + */ +RAPIER_API struct R2JointDesc RAPIER_CALL r2FixedJointDesc(void); + +#if defined(RAPIER_DIM2) +/** + * Return a revolute joint description with native defaults; no allocation. + * @ingroup joints + */ +RAPIER_API struct R2JointDesc RAPIER_CALL r2RevoluteJointDesc(void); +#endif + +#if defined(RAPIER_DIM3) +/** + * Returns a joint description. Invalid axes produce nonfinite frames, rejected on insertion. + * @ingroup joints + */ +RAPIER_API struct R2JointDesc RAPIER_CALL r2RevoluteJointDesc(struct R2Vector axis_vector); +#endif + +/** + * Returns a joint description. Invalid axes produce nonfinite frames, rejected on insertion. + * @ingroup joints + */ +RAPIER_API struct R2JointDesc RAPIER_CALL r2PrismaticJointDesc(struct R2Vector axis_vector); + +/** + * Return a rope joint description with native defaults; no allocation. + * @ingroup joints + */ +RAPIER_API struct R2JointDesc RAPIER_CALL r2RopeJointDesc(R2Real length); + +/** + * Return a spring joint description with native defaults; no allocation. + * @ingroup joints + */ +RAPIER_API +struct R2JointDesc RAPIER_CALL r2SpringJointDesc(R2Real length, + R2Real stiffness, + R2Real damping); + +#if defined(RAPIER_DIM3) +/** + * Return a spherical joint description with native defaults; no allocation. + * @ingroup joints + */ +RAPIER_API struct R2JointDesc RAPIER_CALL r2SphericalJointDesc(void); +#endif + +#if defined(RAPIER_DIM2) +/** + * Returns a joint description. Invalid axes produce nonfinite frames, rejected on insertion. + * @ingroup joints + */ +RAPIER_API struct R2JointDesc RAPIER_CALL r2PinSlotJointDesc(struct R2Vector axis_vector); +#endif + +/** + * Create an impulse joint connecting two bodies in the same world. The world owns the joint; + * wake_up wakes the connected bodies. + * @ingroup joints + */ +RAPIER_API +struct R2ImpulseJointHandle RAPIER_CALL r2InsertImpulseJoint(struct R2RigidBodyHandle body1, + struct R2RigidBodyHandle body2, + const struct R2JointDesc *joint); + +/** + * Create an articulation joint between bodies in the same world. Returns an invalid handle on + * failure; check r2LastStatus. + * @ingroup joints + */ +RAPIER_API +struct R2MultibodyJointHandle RAPIER_CALL r2InsertMultibodyJoint(struct R2RigidBodyHandle body1, + struct R2RigidBodyHandle body2, + const struct R2JointDesc *joint); + +/** + * Return native default soft body desc. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SoftBodyDesc RAPIER_CALL r2DefaultSoftBodyDesc(void); + +/** + * Consumes no caller-owned resources. All borrowed arrays may be released on return. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyHandle RAPIER_CALL r2InsertSoftBody(struct R2World *world, + const struct R2SoftBodyDesc *desc); + +/** + * Return native default soft mesh binding desc. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SoftMeshBindingDesc RAPIER_CALL r2DefaultSoftMeshBindingDesc(void); + +/** + * Create a deformable collider bound to a soft-body cluster. The world owns the collider; binding + * arrays are borrowed only during insertion. + * @ingroup colliders + */ +RAPIER_API +struct R2ColliderHandle RAPIER_CALL r2InsertDeformableCollider(const struct R2ColliderDesc *collider, + const struct R2SoftMeshBindingDesc *binding, + struct R2RigidBodyHandle parent); + +/** + * Return native default query options. This POD value owns no resources. + * @ingroup queries + */ +RAPIER_API struct R2QueryOptions RAPIER_CALL r2DefaultQueryOptions(void); + +/** + * Return the closest ray hit, or report R2_NOT_FOUND on a miss. The ray is origin + direction * t + * for 0 <= t <= max_toi; direction need not be normalized. solid treats an interior origin as a + * hit at t = 0. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R2RayHit RAPIER_CALL r2CastRay(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Vector origin, + struct R2Vector direction, + R2Real max_toi, + R2Bool solid); + +/** + * Return the closest surface projection within max_distance, or report R2_NOT_FOUND. With solid = + * 1, an interior point projects to itself. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R2PointProjection RAPIER_CALL r2ProjectPoint(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Vector point, + R2Real max_distance, + R2Bool solid); + +/** + * Sweep shape from pose along velocity and return the first hit; report R2_NOT_FOUND on a miss. + * Time is bounded by options.max_time_of_impact. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup shapes + */ +RAPIER_API +struct R2ShapeCastHit RAPIER_CALL r2CastShape(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Pose pose, + struct R2Vector velocity, + const R2SharedShape *shape, + struct R2ShapeCastOptions options); + +/** + * Copy handles of colliders containing the world-space point. + * @see @ref output_buffers + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +size_t RAPIER_CALL r2IntersectPoint(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Vector point, + struct R2ColliderHandle *buffer, + size_t capacity); + +/** + * Copy handles of colliders intersecting the shape at its world-space pose. The shape is borrowed + * for this call. + * @see @ref output_buffers + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup shapes + */ +RAPIER_API +size_t RAPIER_CALL r2IntersectShape(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Pose pose, + const R2SharedShape *shape, + struct R2ColliderHandle *buffer, + size_t capacity); + +/** + * Copy broad-phase candidates whose bounding boxes overlap the world-space AABB. Results may + * include false positives. + * @see @ref output_buffers + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +size_t RAPIER_CALL r2IntersectAabbConservative(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Aabb aabb, + struct R2ColliderHandle *buffer, + size_t capacity); + +/** + * Return the closest ray collider and time, with found = 0 on a miss (R2_OK). The ray is origin + + * direction * t; max_toi bounds t. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R2RayToi RAPIER_CALL r2CastRayToi(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Vector origin, + struct R2Vector direction, + R2Real max_toi, + R2Bool solid); + +/** + * Return the closest ray hit with found = 0 on a miss (R2_OK). The ray is origin + direction * t; + * solid treats an interior origin as a hit at t = 0. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R2OptionalRayHit RAPIER_CALL r2TryCastRay(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Vector origin, + struct R2Vector direction, + R2Real max_toi, + R2Bool solid); + +/** + * Return a dynamic rigid-body description with native defaults; no allocation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2RigidBodyDesc RAPIER_CALL r2DynamicRigidBodyDesc(void); + +/** + * Return a fixed rigid-body description with native defaults; no allocation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2RigidBodyDesc RAPIER_CALL r2FixedRigidBodyDesc(void); + +/** + * Return a kinematic position based rigid-body description with native defaults; no allocation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2RigidBodyDesc RAPIER_CALL r2KinematicPositionBasedRigidBodyDesc(void); + +/** + * Return a kinematic velocity based rigid-body description with native defaults; no allocation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2RigidBodyDesc RAPIER_CALL r2KinematicVelocityBasedRigidBodyDesc(void); + +/** + * Return native default shape desc. This POD value owns no resources. + * @ingroup shapes + */ +RAPIER_API struct R2ShapeDesc RAPIER_CALL r2DefaultShapeDesc(void); + +/** + * Build an owned shared shape from a description; release it with r2FreeSharedShape. Borrowed + * inputs may be released after this call. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2ShapeDesc_Build(const struct R2ShapeDesc *desc); + +/** + * Return native default collider desc. This POD value owns no resources. + * @ingroup colliders + */ +RAPIER_API struct R2ColliderDesc RAPIER_CALL r2DefaultColliderDesc(void); + +/** + * Return a ball description with the supplied radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R2ColliderDesc RAPIER_CALL r2BallColliderDesc(R2Real radius); + +/** + * Return an axis-aligned box description with the supplied half-extents. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R2ColliderDesc RAPIER_CALL r2CuboidColliderDesc(struct R2Vector half_extents); + +/** + * Create a body from the description and return its world-bound handle. The world owns the body. + * @ingroup rigid_bodies + */ +RAPIER_API +struct R2RigidBodyHandle RAPIER_CALL r2InsertRigidBody(struct R2World *world, + const struct R2RigidBodyDesc *desc); + +/** + * Insert a collider attached to a rigid body, using the world stored in its handle. + * The parent handle is copied by value. The description is borrowed through this call. + * Invalid or removed parents fail without inserting a collider. + * @ingroup colliders + */ +RAPIER_API +struct R2ColliderHandle RAPIER_CALL r2InsertCollider(struct R2RigidBodyHandle parent, + const struct R2ColliderDesc *desc); + +/** + * Insert a collider without a rigid-body parent. The world owns the collider. + * The description is borrowed through this call. + * @ingroup colliders + */ +RAPIER_API +struct R2ColliderHandle RAPIER_CALL r2InsertColliderWithoutParent(struct R2World *world, + const struct R2ColliderDesc *desc); + +/** + * Return POD structure sizes for checking foreign-language layouts against this library. + * @ingroup errors + */ +RAPIER_API struct R2PodLayout RAPIER_CALL r2PodLayout(void); + +/** + * Allocate a character controller with native defaults; release it with + * r2FreeKinematicCharacterController. + * @ingroup controllers + */ +RAPIER_API struct R2KinematicCharacterController *RAPIER_CALL r2NewKinematicCharacterController(void); + +/** + * Release an owned kinematic character controller. NULL is allowed. Do not pass borrowed pointers + * or free the object twice. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2FreeKinematicCharacterController(struct R2KinematicCharacterController *controller); + +/** + * Set the up direction; it must be finite and nonzero and is normalized on input. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2KinematicCharacterController_SetUp(struct R2KinematicCharacterController *controller, + struct R2Vector up); + +/** + * Set the collision separation margin; use a positive absolute or relative character length. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2KinematicCharacterController_SetOffset(struct R2KinematicCharacterController *controller, + struct R2CharacterLength offset); + +/** + * Enable or disable sliding along obstacles. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2KinematicCharacterController_SetSlide(struct R2KinematicCharacterController *controller, + R2Bool enabled); + +/** + * Set the maximum climb angle and minimum slide angle, in radians. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2KinematicCharacterController_SetSlopes(struct R2KinematicCharacterController *controller, + R2Real max_climb_angle, + R2Real min_slide_angle); + +/** + * Configure automatic stepping over obstacles. enabled = 0 disables it. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2KinematicCharacterController_SetAutostep(struct R2KinematicCharacterController *controller, + R2Bool enabled, + struct R2CharacterLength max_height, + struct R2CharacterLength min_width, + R2Bool include_dynamic_bodies); + +/** + * Configure downward ground snapping. enabled = 0 disables it. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2KinematicCharacterController_SetSnapToGround(struct R2KinematicCharacterController *controller, + R2Bool enabled, + struct R2CharacterLength distance); + +/** + * Computes movement without moving any collider. Use the returned translation to set the character + * target. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup controllers + */ +RAPIER_API +struct R2CharacterMovement RAPIER_CALL r2KinematicCharacterController_MoveShape(const struct R2World *world, + const struct R2QueryOptions *options, + struct R2KinematicCharacterController *controller, + R2Real dt, + const R2SharedShape *shape, + struct R2Pose pose, + struct R2Vector desired_translation); + +/** + * Copy collisions recorded by the most recent MoveShape call. + * @see @ref output_buffers + * @ingroup controllers + */ +RAPIER_API +size_t RAPIER_CALL r2KinematicCharacterController_Collisions(const struct R2KinematicCharacterController *controller, + struct R2CharacterCollision *buffer, + size_t capacity); + +/** + * Applies impulses for the most recent move_shape collisions. Use the same world, shape, dt and + * filter. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2KinematicCharacterController_SolveCharacterCollisionImpulses(const struct R2KinematicCharacterController *controller, + const R2SharedShape *shape, + R2Real dt, + R2Real mass, + const struct R2QueryFilter *filter); + +/** + * Allocate a PID controller with supplied gains and controlled axes. Release with + * r2FreePidController. + * @ingroup controllers + */ +RAPIER_API struct R2PidController *RAPIER_CALL r2NewPidController(void); + +/** + * Release an owned pid controller. NULL is allowed. Do not pass borrowed pointers or free the + * object twice. + * @ingroup controllers + */ +RAPIER_API R2Status RAPIER_CALL r2FreePidController(struct R2PidController *controller); + +/** + * Return a copy of the proportional, integral, and derivative gains. + * @ingroup controllers + */ +RAPIER_API struct R2PidGains RAPIER_CALL r2PidController_Gains(const struct R2PidController *controller); + +/** + * Replace the proportional, integral, and derivative gains. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2PidController_SetGains(struct R2PidController *controller, + struct R2PidGains gains); + +/** + * AxesMask bits match Rapier: linear X/Y/Z are 1/2/4, angular X/Y/Z are 8/16/32. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2PidController_SetAxes(struct R2PidController *controller, + uint32_t axes); + +/** + * Compute a velocity correction, preserving the body's state and updating PID integrals. + * @ingroup controllers + */ +RAPIER_API +struct R2VelocityCorrection RAPIER_CALL r2PidController_RigidBodyCorrection(struct R2PidController *controller, + R2Real dt, + struct R2RigidBodyHandle body, + struct R2Pose target_pose, + struct R2Vector target_linvel, + R2AngVector target_angvel); + +/** + * Return a copy of slide, slope, and ground-snap settings. + * @ingroup controllers + */ +RAPIER_API +struct R2CharacterControllerSettings RAPIER_CALL r2KinematicCharacterController_Settings(const struct R2KinematicCharacterController *controller); + +#if defined(RAPIER_DIM3) +/** + * Return native default wheel tuning. This POD value owns no resources. + * @ingroup controllers + */ +RAPIER_API struct R2WheelTuning RAPIER_CALL r2DefaultWheelTuning(void); +#endif + +#if defined(RAPIER_DIM3) +/** + * Allocate a vehicle controller bound to its chassis body. The chassis world must outlive the + * controller. Release with r2FreeDynamicRayCastVehicleController. + * @ingroup controllers + */ +RAPIER_API +struct R2DynamicRayCastVehicleController *RAPIER_CALL r2NewDynamicRayCastVehicleController(struct R2RigidBodyHandle chassis); +#endif + +#if defined(RAPIER_DIM3) +/** + * Release an owned dynamic ray cast vehicle controller. NULL is allowed. Do not pass borrowed + * pointers or free the object twice. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2FreeDynamicRayCastVehicleController(struct R2DynamicRayCastVehicleController *controller); +#endif + +#if defined(RAPIER_DIM3) +/** + * Append a wheel and return its zero-based index. Connection, suspension direction, and axle + * are in chassis-local coordinates. + * @ingroup controllers + */ +RAPIER_API +size_t RAPIER_CALL r2DynamicRayCastVehicleController_AddWheel(struct R2DynamicRayCastVehicleController *controller, + struct R2Vector connection, + struct R2Vector direction, + struct R2Vector axle, + R2Real rest_length, + R2Real radius, + const struct R2WheelTuning *tuning); +#endif + +#if defined(RAPIER_DIM3) +/** + * Set the chassis up/forward axis indices (0 = X, 1 = Y, 2 = Z). + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2DynamicRayCastVehicleController_SetAxes(struct R2DynamicRayCastVehicleController *controller, + size_t up, + size_t forward); +#endif + +#if defined(RAPIER_DIM3) +/** + * Set a wheel engine force, brake force, and steering angle in radians. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2DynamicRayCastVehicleController_SetWheelControls(struct R2DynamicRayCastVehicleController *controller, + size_t index, + R2Real steering, + R2Real engine_force, + R2Real brake); +#endif + +#if defined(RAPIER_DIM3) +/** + * Ray-cast wheel contacts and apply vehicle forces for dt seconds. Does not step the world. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2DynamicRayCastVehicleController_UpdateVehicle(struct R2DynamicRayCastVehicleController *controller, + R2Real dt, + const struct R2QueryFilter *filter); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return signed chassis speed along its forward direction. + * @ingroup controllers + */ +RAPIER_API +R2Real RAPIER_CALL r2DynamicRayCastVehicleController_CurrentVehicleSpeed(const struct R2DynamicRayCastVehicleController *controller); +#endif + +#if defined(RAPIER_DIM3) +/** + * Copy current wheel state in wheel insertion order. + * @see @ref output_buffers + * @ingroup controllers + */ +RAPIER_API +size_t RAPIER_CALL r2DynamicRayCastVehicleController_Wheels(const struct R2DynamicRayCastVehicleController *controller, + struct R2WheelState *buffer, + size_t capacity); +#endif + +/** + * Propagate all modified body poses to attached colliders. Run collision detection or step before + * querying the broad phase. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBodyPropagateModifiedBodyPositionsToColliders(struct R2World *world); + +/** + * Copies the island manager's active body handles. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r2ActiveRigidBodies(const struct R2World *world, + struct R2RigidBodyHandle *buffer, + size_t capacity); + +/** + * Wake a body by handle, including a soft-body cluster proxy. + * @ingroup rigid_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RigidBody_WakeUp(struct R2RigidBodyHandle handle, R2Bool strong); + +/** + * Replace this thread's error handler and return the previous handler so it can + * be restored at the end of a scope. Status returns are unchanged. A handler + * that returns lets the caller recover by checking the status; a fail-fast + * handler may terminate the process. Includes R2_NOT_FOUND query misses. + * @ingroup errors + */ +RAPIER_API struct R2ErrorHandler RAPIER_CALL r2SetErrorHandler(struct R2ErrorHandler handler); + +/** + * Status of the most recent fallible operation on this thread. Reading this or + * LastError does not clear it. Infallible value constructors do not change it. + * Check immediately after a fallible value-returning operation when recovering + * from errors instead of using a fail-fast error callback. + * @ingroup errors + */ +RAPIER_API R2Status RAPIER_CALL r2LastStatus(void); + +/** + * Thread-local UTF-8 diagnostic, valid until the next fallible call on this thread. + * @ingroup errors + */ +RAPIER_API const char *RAPIER_CALL r2LastError(void); + +/** + * Create an owned ball shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2BallSharedShape(R2Real radius); + +/** + * Create an owned cuboid shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2CuboidSharedShape(struct R2Vector half_extents); + +/** + * Create an owned round cuboid shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2RoundCuboidSharedShape(struct R2Vector half_extents, + R2Real border_radius); + +/** + * Create an owned capsule shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2CapsuleSharedShape(struct R2Vector a, + struct R2Vector b, + R2Real radius); + +/** + * Create an owned segment shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2SegmentSharedShape(struct R2Vector a, struct R2Vector b); + +/** + * Create an owned triangle shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2TriangleSharedShape(struct R2Vector a, + struct R2Vector b, + struct R2Vector c); + +/** + * Create an owned halfspace shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2HalfspaceSharedShape(struct R2Vector normal); + +#if defined(RAPIER_DIM3) +/** + * Create an owned cylinder shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2CylinderSharedShape(R2Real half_height, R2Real radius); +#endif + +#if defined(RAPIER_DIM3) +/** + * Create an owned cone shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2ConeSharedShape(R2Real half_height, R2Real radius); +#endif + +/** + * Create an owned compound shape; each child pose is relative to the compound. Child shapes are + * shared, not consumed. Release with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2CompoundSharedShape(struct R2CompoundShapeView children); + +/** + * Remove the collider and update its parent body mass properties. wake_up wakes the parent. + * @ingroup colliders + */ +RAPIER_API R2Status RAPIER_CALL r2RemoveCollider(struct R2ColliderHandle handle, R2Bool wake_up); + +/** + * Remove an impulse joint. wake_up wakes its connected bodies. + * @ingroup joints + */ +RAPIER_API R2Status RAPIER_CALL r2RemoveImpulseJoint(struct R2ImpulseJointHandle handle, R2Bool wake_up); + +/** + * Copy entity handles. + * @see @ref output_buffers + * @ingroup joints + */ +RAPIER_API +size_t RAPIER_CALL r2ImpulseJointHandles(const struct R2World *world, + struct R2ImpulseJointHandle *buffer, + size_t capacity); + +/** + * Remove an articulation joint. wake_up wakes affected bodies. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2RemoveMultibodyJoint(struct R2MultibodyJointHandle handle, + R2Bool wake_up); + +/** + * Copy entity handles. + * @see @ref output_buffers + * @ingroup joints + */ +RAPIER_API +size_t RAPIER_CALL r2MultibodyJointHandles(const struct R2World *world, + struct R2MultibodyJointHandle *buffer, + size_t capacity); + +/** + * Return the two bodies connected by an impulse joint. + * @ingroup joints + */ +RAPIER_API struct R2JointBodies RAPIER_CALL r2ImpulseJoint_Bodies(struct R2ImpulseJointHandle handle); + +/** + * Return native default inverse kinematics options. This POD value owns no resources. + * @ingroup joints + */ +RAPIER_API struct R2InverseKinematicsOptions RAPIER_CALL r2DefaultInverseKinematicsOptions(void); + +/** + * Return the articulation degrees of freedom associated with the joint. + * @ingroup joints + */ +RAPIER_API size_t RAPIER_CALL r2MultibodyJoint_Ndofs(struct R2MultibodyJointHandle handle); + +/** + * Read/write displacement buffer must contain exactly ndofs entries; zero it for a fresh solve. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2MultibodyJoint_InverseKinematics(struct R2MultibodyJointHandle handle, + const struct R2InverseKinematicsOptions *options, + struct R2Pose target, + R2IkJointCanMove can_move, + void *user_data, + R2Real *displacements, + size_t count); + +/** + * Apply generalized articulation displacements in native degree-of-freedom order. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2MultibodyJoint_ApplyDisplacements(struct R2MultibodyJointHandle handle, + const R2Real *displacements, + size_t count); + +/** + * Frees an owned object; NULL is allowed. Never free a borrowed pointer. + * @ingroup shapes + */ +RAPIER_API R2Status RAPIER_CALL r2FreeSharedShape(R2SharedShape *object); + +/** + * Create an owned wrapper sharing the same immutable geometry. Release it with + * r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2SharedShape_Clone(const R2SharedShape *object); + +/** + * Return the number of rigid body objects in the world. + * @ingroup rigid_bodies + */ +RAPIER_API size_t RAPIER_CALL r2RigidBodyCount(const struct R2World *world); + +/** + * Copy entity handles. + * @see @ref output_buffers + * @ingroup rigid_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2RigidBodyHandles(const struct R2World *world, + struct R2RigidBodyHandle *buffer, + size_t capacity); + +/** + * Test whether the live world contains this rigid body handle. A removed/stale handle returns + * false. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_Contains(struct R2RigidBodyHandle handle); + +/** + * Return the number of collider objects in the world. + * @ingroup colliders + */ +RAPIER_API size_t RAPIER_CALL r2ColliderCount(const struct R2World *world); + +/** + * Copy entity handles. + * @see @ref output_buffers + * @ingroup colliders + */ +RAPIER_API +size_t RAPIER_CALL r2ColliderHandles(const struct R2World *world, + struct R2ColliderHandle *buffer, + size_t capacity); + +/** + * Test whether the live world contains this collider handle. A removed/stale handle returns false. + * @ingroup colliders + */ +RAPIER_API R2Bool RAPIER_CALL r2Collider_Contains(struct R2ColliderHandle handle); + +/** + * Return the number of soft body objects in the world. + * @ingroup soft_bodies + */ +RAPIER_API size_t RAPIER_CALL r2SoftBodyCount(const struct R2World *world); + +/** + * Copy entity handles. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyHandles(const struct R2World *world, + struct R2SoftBodyHandle *buffer, + size_t capacity); + +/** + * Test whether the live world contains this soft body handle. A removed/stale handle returns + * false. + * @ingroup soft_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2SoftBody_Contains(struct R2SoftBodyHandle handle); + +/** + * Remove a body and its joints, optionally keeping colliders as standalone objects. + * Returns whether a body was removed; a stale handle returns false without error. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Bool RAPIER_CALL r2RemoveRigidBody(struct R2RigidBodyHandle handle, + R2Bool remove_attached_colliders); + +/** + * Return the world setting documented by R2IntegrationParameters::dt. + * @ingroup worlds + */ +RAPIER_API R2Real RAPIER_CALL r2TimeStep(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::dt. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetTimeStep(struct R2World *world, R2Real value); + +/** + * Return the world setting documented by R2IntegrationParameters::minCcdDt. + * @ingroup worlds + */ +RAPIER_API R2Real RAPIER_CALL r2MinCcdDt(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::minCcdDt. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetMinCcdDt(struct R2World *world, R2Real value); + +/** + * Return the world setting documented by R2IntegrationParameters::lengthUnit. + * @ingroup worlds + */ +RAPIER_API R2Real RAPIER_CALL r2LengthUnit(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::lengthUnit. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetLengthUnit(struct R2World *world, R2Real value); + +/** + * Return the world setting documented by R2IntegrationParameters::warmstartCoefficient. + * @ingroup worlds + */ +RAPIER_API R2Real RAPIER_CALL r2WarmstartCoefficient(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::warmstartCoefficient. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetWarmstartCoefficient(struct R2World *world, R2Real value); + +/** + * Return the world setting documented by R2IntegrationParameters::normalizedAllowedLinearError. + * @ingroup worlds + */ +RAPIER_API R2Real RAPIER_CALL r2NormalizedAllowedLinearError(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::normalizedAllowedLinearError. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetNormalizedAllowedLinearError(struct R2World *world, R2Real value); + +/** + * Return the world setting documented by + * R2IntegrationParameters::normalizedMaxCorrectiveVelocity. + * @ingroup worlds + */ +RAPIER_API R2Real RAPIER_CALL r2NormalizedMaxCorrectiveVelocity(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::normalizedMaxCorrectiveVelocity. + * @ingroup worlds + */ +RAPIER_API +R2Status RAPIER_CALL r2SetNormalizedMaxCorrectiveVelocity(struct R2World *world, + R2Real value); + +/** + * Return the world setting documented by R2IntegrationParameters::normalizedPredictionDistance. + * @ingroup worlds + */ +RAPIER_API R2Real RAPIER_CALL r2NormalizedPredictionDistance(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::normalizedPredictionDistance. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetNormalizedPredictionDistance(struct R2World *world, R2Real value); + +/** + * Return the world setting documented by R2IntegrationParameters::normalizedMaxLinearVelocity. + * @ingroup worlds + */ +RAPIER_API R2Real RAPIER_CALL r2NormalizedMaxLinearVelocity(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::normalizedMaxLinearVelocity. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetNormalizedMaxLinearVelocity(struct R2World *world, R2Real value); + +/** + * Return the world setting documented by + * R2IntegrationParameters::normalizedContactRecycleDistance. + * @ingroup worlds + */ +RAPIER_API R2Real RAPIER_CALL r2NormalizedContactRecycleDistance(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::normalizedContactRecycleDistance. + * @ingroup worlds + */ +RAPIER_API +R2Status RAPIER_CALL r2SetNormalizedContactRecycleDistance(struct R2World *world, + R2Real value); + +/** + * Return the world setting documented by R2IntegrationParameters::numSolverIterations. + * @ingroup worlds + */ +RAPIER_API size_t RAPIER_CALL r2NumSolverIterations(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::numSolverIterations. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetNumSolverIterations(struct R2World *world, size_t value); + +/** + * Return the world setting documented by R2IntegrationParameters::numInternalPgsIterations. + * @ingroup worlds + */ +RAPIER_API size_t RAPIER_CALL r2NumInternalPgsIterations(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::numInternalPgsIterations. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetNumInternalPgsIterations(struct R2World *world, size_t value); + +/** + * Return the world setting documented by + * R2IntegrationParameters::numInternalStabilizationIterations. + * @ingroup errors + */ +RAPIER_API size_t RAPIER_CALL r2NumInternalStabilizationIterations(const struct R2World *world); + +/** + * Set the world setting documented by + * R2IntegrationParameters::numInternalStabilizationIterations. + * @ingroup errors + */ +RAPIER_API +R2Status RAPIER_CALL r2SetNumInternalStabilizationIterations(struct R2World *world, + size_t value); + +/** + * Return the world setting documented by R2IntegrationParameters::maxCcdSubsteps. + * @ingroup worlds + */ +RAPIER_API size_t RAPIER_CALL r2MaxCcdSubsteps(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::maxCcdSubsteps. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetMaxCcdSubsteps(struct R2World *world, size_t value); + +/** + * Return the world setting documented by R2IntegrationParameters::contactClustering. + * @ingroup worlds + */ +RAPIER_API R2Bool RAPIER_CALL r2ContactClustering(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::contactClustering. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetContactClustering(struct R2World *world, R2Bool value); + +/** + * Return the world setting documented by R2IntegrationParameters::contactRecycling. + * @ingroup worlds + */ +RAPIER_API R2Bool RAPIER_CALL r2ContactRecycling(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::contactRecycling. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetContactRecycling(struct R2World *world, R2Bool value); + +/** + * Return the world setting documented by R2IntegrationParameters::frictionInBiasPass. + * @ingroup worlds + */ +RAPIER_API R2Bool RAPIER_CALL r2FrictionInBiasPass(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::frictionInBiasPass. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetFrictionInBiasPass(struct R2World *world, R2Bool value); + +/** + * Return the world setting documented by R2IntegrationParameters::warmstartJoints. + * @ingroup joints + */ +RAPIER_API R2Bool RAPIER_CALL r2WarmstartJoints(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::warmstartJoints. + * @ingroup joints + */ +RAPIER_API R2Status RAPIER_CALL r2SetWarmstartJoints(struct R2World *world, R2Bool value); + +/** + * Return the world setting documented by R2IntegrationParameters::contactSoftness. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SpringCoefficients RAPIER_CALL r2ContactSoftness(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::contactSoftness. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SetContactSoftness(struct R2World *world, + struct R2SpringCoefficients value); + +/** + * Return the world setting documented by R2IntegrationParameters::staticContactSoftness. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SpringCoefficients RAPIER_CALL r2StaticContactSoftness(const struct R2World *world); + +/** + * Set the world setting documented by R2IntegrationParameters::staticContactSoftness. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SetStaticContactSoftness(struct R2World *world, + struct R2SpringCoefficients value); + +/** + * Applies Rapier's persistent one-way platform logic to the borrowed manifold. + * @ingroup worlds + */ +RAPIER_API +R2Status RAPIER_CALL r2ContactModificationContext_UpdateAsOnewayPlatform(struct R2ContactModificationContext *context, + struct R2Vector allowed_local_n1, + R2Real allowed_angle); + +/** + * Sets the tangent velocity of every rigid solver contact in this manifold. + * @ingroup worlds + */ +RAPIER_API +R2Status RAPIER_CALL r2ContactModificationContext_SetTangentVelocity(struct R2ContactModificationContext *context, + struct R2Vector velocity); + +/** + * Allocate an empty event collector; release it with r2FreeEventCollector. + * @ingroup events + */ +RAPIER_API struct R2EventCollector *RAPIER_CALL r2NewEventCollector(void); + +/** + * Release an owned event collector. NULL is allowed. Do not pass borrowed pointers or free the + * object twice. + * @ingroup events + */ +RAPIER_API R2Status RAPIER_CALL r2FreeEventCollector(struct R2EventCollector *events); + +/** + * Discard all collected events. Does not change the world. + * @ingroup events + */ +RAPIER_API R2Status RAPIER_CALL r2EventCollector_Clear(struct R2EventCollector *events); + +/** + * Copy the collected collision start/stop events without removing them. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r2EventCollector_CollisionEvents(const struct R2EventCollector *events, + struct R2CollisionEvent *buffer, + size_t capacity); + +/** + * Copy the collected contact-force events without removing them. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r2EventCollector_ContactForceEvents(const struct R2EventCollector *events, + struct R2ContactForceEvent *buffer, + size_t capacity); + +/** + * Return the number of queued soft-body tear events. + * @ingroup events + */ +RAPIER_API size_t RAPIER_CALL r2EventCollector_TearEventCount(const struct R2EventCollector *events); + +/** + * Return an owned copy of a queued tear event; release with r2FreeSoftBodyTearEvent. Does + * not remove the queued event. + * @ingroup events + */ +RAPIER_API +struct R2SoftBodyTearEvent *RAPIER_CALL r2EventCollector_TearEvent(const struct R2EventCollector *events, + size_t index); + +/** + * Return the world-space gravitational acceleration. + * @ingroup worlds + */ +RAPIER_API struct R2Vector RAPIER_CALL r2Gravity(const struct R2World *world); + +/** + * Set the world-space gravitational acceleration. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetGravity(struct R2World *world, struct R2Vector value); + +/** + * Hooks and events may be NULL. This call invalidates all borrowed set-element pointers. + * Advance simulation by one timestep. Hooks and events may be NULL. + * @ingroup worlds + */ +RAPIER_API +R2Status RAPIER_CALL r2Step(struct R2World *world, + const struct R2PhysicsHooks *hooks, + const struct R2EventCollector *events); + +/** + * Refresh collision detection without advancing simulation. Hooks and events may be NULL. + * @ingroup worlds + */ +RAPIER_API +R2Status RAPIER_CALL r2DetectCollisions(struct R2World *world, + const struct R2PhysicsHooks *hooks, + const struct R2EventCollector *events); + +/** + * Borrow snapshot bytes without copying; valid until r2FreeBytes. Never free the returned data + * pointer. + * @ingroup worlds + */ +RAPIER_API struct R2ByteView RAPIER_CALL r2Bytes_Data(const struct R2Bytes *bytes); + +/** + * Release an owned snapshot byte buffer. NULL is allowed. Do not pass borrowed pointers or free + * the object twice. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2FreeBytes(struct R2Bytes *bytes); + +/** + * Return owned snapshot bytes; release them with r2FreeBytes. See @ref snapshots for + * restoration and handle lifetimes. + * @ingroup worlds + */ +RAPIER_API struct R2Bytes *RAPIER_CALL r2SerializeWorld(const struct R2World *world); + +/** + * Restore ONLY trusted snapshots produced by the identical Rapier build. Snapshots are not a + * stable file format. + * @ingroup worlds + */ +RAPIER_API struct R2World *RAPIER_CALL r2DeserializeWorld(const uint8_t *data, size_t count); + +/** + * Color is HSLA (hue in degrees), matching Rapier DebugColor. mode uses DebugRenderMode bits. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r2DebugRender(const struct R2World *world, + uint32_t mode, + struct R2DebugLine *buffer, + size_t capacity); + +/** + * Set the world setting documented by R2SoftBodiesSettings::resweepStrain. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBodiesSetResweepStrain(struct R2World *world, R2Real value); + +/** + * Return the world setting documented by R2SoftBodiesSettings::resweepStrain. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2SoftBodiesResweepStrain(const struct R2World *world); + +/** + * Set the world setting documented by R2SoftBodiesSettings::contactStiffening. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBodiesSetContactStiffening(struct R2World *world, R2Real value); + +/** + * Return the world setting documented by R2SoftBodiesSettings::contactStiffening. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2SoftBodiesContactStiffening(const struct R2World *world); + +/** + * Set the world setting documented by R2SoftBodiesSettings::maxExtraSubsteps. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBodiesSetMaxExtraSubsteps(struct R2World *world, size_t value); + +/** + * Return the world setting documented by R2SoftBodiesSettings::maxExtraSubsteps. + * @ingroup soft_bodies + */ +RAPIER_API size_t RAPIER_CALL r2SoftBodiesMaxExtraSubsteps(const struct R2World *world); + +/** + * Set the world setting documented by R2SoftRecoverySettings::authoredVelocityMargin. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RecoverySetAuthoredVelocityMargin(struct R2World *world, + R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::edgeSpeculation. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetEdgeSpeculation(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::invertedCellDetection. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RecoverySetInvertedCellDetection(struct R2World *world, + R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::selfCrossingDetection. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RecoverySetSelfCrossingDetection(struct R2World *world, + R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::detectionMotionGating. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RecoverySetDetectionMotionGating(struct R2World *world, + R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::crossBodyDetection. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetCrossBodyDetection(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::selfStandDown. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetSelfStandDown(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::crossBodyExpelGate. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetCrossBodyExpelGate(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::edgeStandDown. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetEdgeStandDown(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::crossingRepulsion. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetCrossingRepulsion(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::crossingRepulsionGuide. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RecoverySetCrossingRepulsionGuide(struct R2World *world, + R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::crossingRepulsionSelfGuide. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RecoverySetCrossingRepulsionSelfGuide(struct R2World *world, + R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::recoveryPace. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetRecoveryPace(struct R2World *world, R2Real value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapConstraints. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetOverlapConstraints(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapRigid. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetOverlapRigid(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapSkipSelfTangled. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RecoverySetOverlapSkipSelfTangled(struct R2World *world, + R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapEdgeStandDown. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RecoverySetOverlapEdgeStandDown(struct R2World *world, + R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapConstraintPace. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RecoverySetOverlapConstraintPace(struct R2World *world, + R2Real value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapSkinVolume. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetOverlapSkinVolume(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapKeptDepth. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetOverlapKeptDepth(struct R2World *world, R2Real value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapSelfRegions. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetOverlapSelfRegions(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapNormalPush. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetOverlapNormalPush(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapMultiVolume. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetOverlapMultiVolume(struct R2World *world, R2Bool value); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapProgressMargin. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RecoverySetOverlapProgressMargin(struct R2World *world, + R2Real value); + +#if defined(RAPIER_FEM) +/** + * Set the world setting documented by R2SoftFemParameters::linearTolerance. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2FemSetLinearTolerance(struct R2World *world, R2Real value); +#endif + +#if defined(RAPIER_FEM) +/** + * Set the world setting documented by R2SoftFemParameters::maxLinearIterations. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2FemSetMaxLinearIterations(struct R2World *world, size_t value); +#endif + +#if defined(RAPIER_FEM) +/** + * Set the world setting documented by R2SoftFemParameters::maxDenseDofs. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2FemSetMaxDenseDofs(struct R2World *world, size_t value); +#endif + +/** + * Configures a dedicated pool for this world's parallel work. Zero selects Rayon's default. + * Takes effect on the next step. Reconfiguration must not race with a step or callback. + * Returns R2_UNSUPPORTED in builds without the parallel feature; keeps the previous + * pool when constructing the new one fails. The pool is not included in snapshots. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetNumThreads(struct R2World *world, size_t num_threads); + +/** + * Removes the world's dedicated pool. A parallel build then uses the calling + * context's Rayon pool (normally the global pool), not a single worker. + * Returns R2_UNSUPPORTED in a build without the parallel feature. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2ClearThreadPool(struct R2World *world); + +/** + * Size of the world's dedicated pool, or zero if a parallel build has no dedicated + * pool configured. Returns one for a build without the parallel feature. + * @ingroup worlds + */ +RAPIER_API size_t RAPIER_CALL r2NumThreads(const struct R2World *world); + +/** + * Enable or disable the native pipeline profiling counters. Enabling returns + * R2_UNSUPPORTED if the library was built without the profiler feature. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetCountersEnabled(struct R2World *world, R2Bool enabled); + +/** + * Native engine time of the most recent step, in milliseconds, as in the Rust testbed. + * Enable counters before stepping. Excludes C callbacks outside the step, rendering, + * and dispatch into a dedicated thread pool; remains unchanged while paused. + * @ingroup worlds + */ +RAPIER_API double RAPIER_CALL r2StepTimeMs(const struct R2World *world); + +/** + * Read ONLY trusted legacy rigid-world snapshots from debug_deserialize3.rs, + * produced by the identical Rapier build. This is not a stable interchange format. + * + * Import trusted legacy Rust testbed rigid-state bytes into a new owned world. Release with + * r2FreeWorld; see @ref snapshots. + * @ingroup worlds + */ +RAPIER_API struct R2World *RAPIER_CALL r2DeserializeRigidState(const uint8_t *data, size_t count); + +/** + * Return native default query filter. This POD value owns no resources. + * @ingroup queries + */ +RAPIER_API struct R2QueryFilter RAPIER_CALL r2DefaultQueryFilter(void); + +/** + * Return native default shape cast options. This POD value owns no resources. + * @ingroup queries + */ +RAPIER_API struct R2ShapeCastOptions RAPIER_CALL r2DefaultShapeCastOptions(void); + +/** + * Remove a soft body and its associated simulation objects. Invalidates its handle. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RemoveSoftBody(struct R2SoftBodyHandle handle); + +/** + * Wake the soft body and its rigid proxies. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBody_WakeUp(struct R2SoftBodyHandle handle); + +/** + * Release an owned soft body tear event. NULL is allowed. Do not pass borrowed pointers or free + * the object twice. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2FreeSoftBodyTearEvent(struct R2SoftBodyTearEvent *event); + +/** + * Return the source soft-body handle for this tear event. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyHandle RAPIER_CALL r2SoftBodyTearEvent_SoftBody(const struct R2SoftBodyTearEvent *event); + +/** + * Copy the soft-body handles produced by the tear. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyTearEvent_Bodies(const struct R2SoftBodyTearEvent *event, + struct R2SoftBodyHandle *buffer, + size_t capacity); + +/** + * Return the destination body and particle index for an original particle. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2ParticleDestination RAPIER_CALL r2SoftBodyTearEvent_ParticleDestination(const struct R2SoftBodyTearEvent *event, + uint32_t particle); + +/** + * Flat indices; element arity follows the corresponding Rust event field. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyTearEvent_TornEdges(const struct R2SoftBodyTearEvent *event, + uint32_t *buffer, + size_t capacity); + +/** + * Flat indices; element arity follows the corresponding Rust event field. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyTearEvent_TornCells(const struct R2SoftBodyTearEvent *event, + uint32_t *buffer, + size_t capacity); + +/** + * Flat indices; element arity follows the corresponding Rust event field. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyTearEvent_RemovedEdges(const struct R2SoftBodyTearEvent *event, + uint32_t *buffer, + size_t capacity); + +/** + * Flat indices; element arity follows the corresponding Rust event field. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyTearEvent_SplitParticles(const struct R2SoftBodyTearEvent *event, + uint32_t *buffer, + size_t capacity); + +/** + * Flat indices; element arity follows the corresponding Rust event field. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyTearEvent_InsertedParticles(const struct R2SoftBodyTearEvent *event, + uint32_t *buffer, + size_t capacity); + +/** + * Copy original particle indices belonging to a resulting piece. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyTearEvent_PieceParticles(const struct R2SoftBodyTearEvent *event, + size_t piece_index, + uint32_t *buffer, + size_t capacity); + +/** + * Copy cluster-to-piece and rigid-proxy remapping records. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyTearEvent_Clusters(const struct R2SoftBodyTearEvent *event, + struct R2SoftClusterSplit *buffer, + size_t capacity); + +/** + * Copy impulse-joint remapping records produced by the tear. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyTearEvent_MovedJoints(const struct R2SoftBodyTearEvent *event, + struct R2SoftJointMove *buffer, + size_t capacity); + +/** + * Tear the selected edges and return an owned remapping event. Release it with + * r2FreeSoftBodyTearEvent. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyTearEvent *RAPIER_CALL r2SoftBody_Tear(struct R2SoftBodyHandle handle, + const uint32_t *edges, + size_t edge_count, + const uint32_t *cells, + size_t cell_count); + +/** + * Create a rigid proxy cluster from the supplied particle indices and return its cluster index. + * @ingroup soft_bodies + */ +RAPIER_API +uint32_t RAPIER_CALL r2SoftBody_AddCluster(struct R2SoftBodyHandle handle, + const uint32_t *particles, + size_t count); + +/** + * Remove the selected cluster and its rigid proxy. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_RemoveCluster(struct R2SoftBodyHandle handle, + uint32_t cluster); + +/** + * Optional particle destination after a tear. Missing destinations are normal and set + * found to false; body/index are only written when a destination exists. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2OptionalParticleDestination RAPIER_CALL r2SoftBodyTearEvent_TryParticleDestination(const struct R2SoftBodyTearEvent *event, + uint32_t particle); + +/** + * Cut using DIM points (a segment in 2D, triangle in 3D). A no-op returns a null event. + * The optional owned event must be freed with FreeSoftBodyTearEvent. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyTearEvent *RAPIER_CALL r2CutSoftBody(struct R2SoftBodyHandle handle, + const struct R2Vector *blade); + +/** + * Return volume-meshing settings for the supplied cell size; this POD value requires no + * destructor. + * @ingroup worlds + */ +RAPIER_API struct R2VolumeMeshParameters RAPIER_CALL r2NewVolumeMeshParameters(R2Real cell_size); + +/** + * Return ABI version, dimension, scalar size, and pointer size of the linked library. + * @ingroup errors + */ +RAPIER_API struct R2BuildInfo RAPIER_CALL r2BuildInfo(void); + +/** + * Release version of the loaded C bindings, e.g. "0.35.3+c.2". + * The suffix identifies the C bindings revision for the Rust crate version. + * The UTF-8, NUL-terminated string is borrowed for the library's lifetime; do not free it. + * This release identifier is independent of the ABI compatibility version. + * @ingroup errors + */ +RAPIER_API const char *RAPIER_CALL r2Version(void); + +/** + * Cargo profile of the loaded physics library: "debug" or "release". + * Custom profiles report the corresponding inherited Cargo profile category. + * The UTF-8, NUL-terminated string is borrowed for the library's lifetime; do not free it. + * This is independent of the consumer's build mode and of per-package optimization overrides. + * @ingroup errors + */ +RAPIER_API const char *RAPIER_CALL r2BuildProfile(void); + +/** + * Return profiling, SIMD width, and parallelism of the linked library. + * @ingroup errors + */ +RAPIER_API struct R2BuildFeatures RAPIER_CALL r2BuildFeatures(void); + +/** + * Create an owned heightfield shape from copied samples. 3D samples are column-major, with rows * + * columns entries. Release with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2HeightfieldSharedShape(struct R2RealView heights, + size_t rows, + size_t columns, + struct R2Vector scale); + +/** + * Compute the shape axis-aligned bounds at the supplied world-space pose. + * @ingroup shapes + */ +RAPIER_API +struct R2Aabb RAPIER_CALL r2SharedShape_ComputeAabb(const R2SharedShape *shape, + struct R2Pose pose); + +/** + * Compute local mass properties for the supplied nonnegative density. + * @ingroup shapes + */ +RAPIER_API +struct R2MassProperties RAPIER_CALL r2SharedShape_MassProperties(const R2SharedShape *shape, + R2Real density); + +/** + * Test whether the world-space point lies inside the shape at pose. + * @ingroup shapes + */ +RAPIER_API +R2Bool RAPIER_CALL r2SharedShape_ContainsPoint(const R2SharedShape *shape, + struct R2Pose pose, + struct R2Vector point); + +/** + * Copy current narrow-phase contact pairs, including pairs without active solver contacts. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r2ContactPairs(const struct R2World *world, + struct R2ContactPair *buffer, + size_t capacity); + +/** + * Return the narrow-phase contact pair for two colliders, or report R2_NOT_FOUND. + * @ingroup events + */ +RAPIER_API +struct R2ContactPair RAPIER_CALL r2ContactPair(struct R2ColliderHandle collider1, + struct R2ColliderHandle collider2); + +/** + * Copy current sensor intersection pairs from the narrow phase. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r2IntersectionPairs(const struct R2World *world, + struct R2IntersectionPair *buffer, + size_t capacity); + +/** + * Contact points in collider-local space; normal in world space. Geometric manifolds may be + * recycled. + * For clustered solver impulses use contact pair totals. Soft pairs have no rigid manifolds. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r2ContactPoints(struct R2ColliderHandle collider1, + struct R2ColliderHandle collider2, + struct R2ContactPoint *buffer, + size_t capacity); + +/** + * Copy the articulation generalized velocities in native degree-of-freedom order. + * @see @ref output_buffers + * @ingroup joints + */ +RAPIER_API +size_t RAPIER_CALL r2MultibodyJoint_GeneralizedVelocity(struct R2MultibodyJointHandle handle, + R2Real *buffer, + size_t capacity); + +/** + * Replace articulation generalized velocities; the array length must match its degrees of freedom. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2MultibodyJoint_SetGeneralizedVelocity(struct R2MultibodyJointHandle handle, + const R2Real *values, + size_t count); + +/** + * Check this before passing any dimension/precision-dependent structs across the ABI. + * @ingroup errors + */ +RAPIER_API +R2Status RAPIER_CALL r2CheckAbi(uint32_t version, + uint32_t dimension, + size_t real_size, + size_t vector_size, + size_t pose_size); + +/** + * Return owned local-space rendering geometry; release it with r2FreeShapeMesh. subdivisions + * controls curved-shape resolution. + * @ingroup shapes + */ +RAPIER_API +struct R2ShapeMesh *RAPIER_CALL r2SharedShape_Tessellate(const R2SharedShape *shape, + uint32_t subdivisions); + +/** + * Flat groups of three vertices. Standard output-buffer convention. + * @see @ref output_buffers + * @ingroup shapes + */ +RAPIER_API +size_t RAPIER_CALL r2ShapeMesh_Triangles(const struct R2ShapeMesh *mesh, + struct R2Vector *buffer, + size_t capacity); + +/** + * Flat groups of two vertices. Standard output-buffer convention. + * @see @ref output_buffers + * @ingroup shapes + */ +RAPIER_API +size_t RAPIER_CALL r2ShapeMesh_Lines(const struct R2ShapeMesh *mesh, + struct R2Vector *buffer, + size_t capacity); + +/** + * Release an owned shape mesh. NULL is allowed. Do not pass borrowed pointers or free the object + * twice. + * @ingroup shapes + */ +RAPIER_API R2Status RAPIER_CALL r2FreeShapeMesh(struct R2ShapeMesh *mesh); + +#if defined(RAPIER_DIM3) +/** + * Create an owned round cylinder shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2RoundCylinderSharedShape(R2Real half_height, + R2Real radius, + R2Real border_radius); +#endif + +#if defined(RAPIER_DIM3) +/** + * Tessellate a ball or capsule with independent longitude/latitude subdivision counts. + * Cuboids, cones, cylinders, convex polyhedra, trimeshes, and heightfields are also supported. + * @ingroup shapes + */ +RAPIER_API +struct R2TriMeshData *RAPIER_CALL r2SharedShape_ToTrimesh(const R2SharedShape *shape, + uint32_t ntheta, + uint32_t nphi); +#endif + +#if defined(RAPIER_DIM3) +/** + * Copy vertices. + * @see @ref output_buffers + * @ingroup shapes + */ +RAPIER_API +size_t RAPIER_CALL r2TriMeshData_Vertices(const struct R2TriMeshData *mesh, + struct R2Vector *buffer, + size_t capacity); +#endif + +#if defined(RAPIER_DIM3) +/** + * Flat triangle indices; count and capacity are numbers of u32 entries. + * @see @ref output_buffers + * @ingroup shapes + */ +RAPIER_API +size_t RAPIER_CALL r2TriMeshData_Indices(const struct R2TriMeshData *mesh, + uint32_t *buffer, + size_t capacity); +#endif + +#if defined(RAPIER_DIM3) +/** + * Release an owned tri mesh data. NULL is allowed. Do not pass borrowed pointers or free the + * object twice. + * @ingroup shapes + */ +RAPIER_API R2Status RAPIER_CALL r2FreeTriMeshData(struct R2TriMeshData *mesh); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return native default urdf loader options. This POD value owns no resources. + * @ingroup robotics + */ +RAPIER_API struct R2UrdfLoaderOptions RAPIER_CALL r2DefaultUrdfLoaderOptions(void); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release an owned urdf robot. NULL is allowed. Do not pass borrowed pointers or free the object + * twice. + * @ingroup robotics + */ +RAPIER_API R2Status RAPIER_CALL r2FreeUrdfRobot(struct R2UrdfRobot *object); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Load from a UTF-8 path. Validates options before reading the file. + * Options and their blueprint resources are borrowed through this call; the robot is owned. + * @ingroup robotics + */ +RAPIER_API +struct R2UrdfRobot *RAPIER_CALL r2UrdfRobotFromFile(const char *path, + const struct R2UrdfLoaderOptions *options); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Apply an additional transform to the loaded robot before insertion. + * @ingroup robotics + */ +RAPIER_API +R2Status RAPIER_CALL r2UrdfRobot_AppendTransform(struct R2UrdfRobot *robot, + struct R2Pose transform); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release an owned urdf robot handles. NULL is allowed. Do not pass borrowed pointers or free the + * object twice. + * @ingroup robotics + */ +RAPIER_API R2Status RAPIER_CALL r2FreeUrdfRobotHandles(struct R2UrdfRobotHandles *handles); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Inserts a clone; the source robot remains owned by the caller. Returns owned handles. + * @ingroup robotics + */ +RAPIER_API +struct R2UrdfRobotHandles *RAPIER_CALL r2UrdfRobot_InsertUsingImpulseJoints(struct R2World *world, + const struct R2UrdfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Inserts a clone; the source robot remains owned by the caller. Returns owned handles. + * @ingroup robotics + */ +RAPIER_API +struct R2UrdfRobotHandles *RAPIER_CALL r2UrdfRobot_InsertUsingMultibodyJoints(struct R2World *world, + const struct R2UrdfRobot *robot, + uint8_t options); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Body handles in source order; absent MJCF bodies have invalid handles. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r2UrdfRobotHandles_Bodies(const struct R2UrdfRobotHandles *handles, + struct R2RigidBodyHandle *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return native default mjcf loader options. This POD value owns no resources. + * @ingroup robotics + */ +RAPIER_API struct R2MjcfLoaderOptions RAPIER_CALL r2DefaultMjcfLoaderOptions(void); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release an owned mjcf robot. NULL is allowed. Do not pass borrowed pointers or free the object + * twice. + * @ingroup robotics + */ +RAPIER_API R2Status RAPIER_CALL r2FreeMjcfRobot(struct R2MjcfRobot *object); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Load from a UTF-8 path. Validates options before reading the file. + * Options and their blueprint resources are borrowed through this call; the robot is owned. + * @ingroup robotics + */ +RAPIER_API +struct R2MjcfRobot *RAPIER_CALL r2MjcfRobotFromFile(const char *path, + const struct R2MjcfLoaderOptions *options); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Apply an additional transform to the loaded robot before insertion. + * @ingroup robotics + */ +RAPIER_API +R2Status RAPIER_CALL r2MjcfRobot_AppendTransform(struct R2MjcfRobot *robot, + struct R2Pose transform); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release an owned mjcf robot handles. NULL is allowed. Do not pass borrowed pointers or free the + * object twice. + * @ingroup robotics + */ +RAPIER_API R2Status RAPIER_CALL r2FreeMjcfRobotHandles(struct R2MjcfRobotHandles *handles); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Inserts a clone; the source robot remains owned by the caller. Returns owned handles. + * @ingroup robotics + */ +RAPIER_API +struct R2MjcfRobotHandles *RAPIER_CALL r2MjcfRobot_InsertUsingImpulseJoints(struct R2World *world, + const struct R2MjcfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Inserts a clone; the source robot remains owned by the caller. Returns owned handles. + * @ingroup robotics + */ +RAPIER_API +struct R2MjcfRobotHandles *RAPIER_CALL r2MjcfRobot_InsertUsingMultibodyJoints(struct R2World *world, + const struct R2MjcfRobot *robot, + uint8_t options); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Body handles in source order; absent MJCF bodies have invalid handles. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r2MjcfRobotHandles_Bodies(const struct R2MjcfRobotHandles *handles, + struct R2RigidBodyHandle *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Resolved model gravity before the caller chooses a world convention. + * @ingroup robotics + */ +RAPIER_API struct R2Vector RAPIER_CALL r2MjcfRobot_Gravity(const struct R2MjcfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the number of source MJCF bodies. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r2MjcfRobot_BodyCount(const struct R2MjcfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the collider count for a source body index. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r2MjcfRobot_BodyColliderCount(const struct R2MjcfRobot *robot, size_t body); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Set collision groups on a collider in the loaded robot, before insertion. + * @ingroup robotics + */ +RAPIER_API +R2Status RAPIER_CALL r2MjcfRobot_SetBodyColliderCollisionGroups(struct R2MjcfRobot *robot, + size_t body, + size_t collider, + struct R2InteractionGroups groups); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the number of imported keyframes. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r2MjcfRobot_KeyframeCount(const struct R2MjcfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copies a NUL-terminated UTF-8 name. Count includes NUL; unnamed keys return an empty string. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r2MjcfRobot_KeyframeName(const struct R2MjcfRobot *robot, + size_t key, + char *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Append a keyframe from the source MJCF model to the loaded robot. + * @ingroup robotics + */ +RAPIER_API +R2Status RAPIER_CALL r2MjcfRobot_AppendKeyframe(struct R2MjcfRobot *robot, + const struct R2MjcfRobot *source, + size_t key); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copy actuator controls for the selected keyframe. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r2MjcfRobot_KeyframeControls(const struct R2MjcfRobot *robot, + size_t key, + R2Real *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the number of imported actuators. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r2MjcfRobotHandles_ActuatorCount(const struct R2MjcfRobotHandles *handles); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Apply the selected keyframe to the inserted robot. + * @ingroup robotics + */ +RAPIER_API +R2Status RAPIER_CALL r2MjcfRobotHandles_ApplyKeyframe(const struct R2MjcfRobotHandles *handles, + const struct R2MjcfRobot *robot, + size_t key); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Apply actuator controls with per-actuator scaling to the inserted robot. + * @ingroup robotics + */ +RAPIER_API +R2Status RAPIER_CALL r2MjcfRobotHandles_ApplyControlsScaled(const struct R2MjcfRobotHandles *handles, + const R2Real *controls, + size_t count, + R2Real gain); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the number of visual meshes for a source body. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r2MjcfRobot_BodyVisualCount(const struct R2MjcfRobot *robot, size_t body); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Borrow a visual mesh by body/visual index. Valid until the robot is freed or its storage + * changes; never free this pointer. + * @ingroup robotics + */ +RAPIER_API +const R2MjcfVisualMesh *RAPIER_CALL r2MjcfRobot_BodyVisual(const struct R2MjcfRobot *robot, + size_t body, + size_t visual); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return a copy of visual pose, color, material, and geometry-kind flags. + * @ingroup robotics + */ +RAPIER_API struct R2MjcfVisualMeshInfo RAPIER_CALL r2MjcfVisualMesh_Info(const R2MjcfVisualMesh *visual); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Returns an owned shared shape reference. + * Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. + * @ingroup robotics + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2MjcfVisualMesh_CloneShape(const R2MjcfVisualMesh *visual); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copies flattened pairs of per-vertex UV coordinates. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r2MjcfVisualMesh_Uvs(const R2MjcfVisualMesh *visual, + float *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copies flattened triples of per-vertex normals. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r2MjcfVisualMesh_Normals(const R2MjcfVisualMesh *visual, + float *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copies a NUL-terminated texture path, or an empty string for untextured meshes. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r2MjcfVisualMesh_Texture(const R2MjcfVisualMesh *visual, + char *buffer, + size_t capacity); +#endif + +/** + * Return the rigid body world-space pose. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2Pose RAPIER_CALL r2RigidBody_Position(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body world-space translation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2Vector RAPIER_CALL r2RigidBody_Translation(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body world-space linear velocity. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2Vector RAPIER_CALL r2RigidBody_Linvel(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body world-space angular velocity (radians per second). + * @ingroup rigid_bodies + */ +RAPIER_API R2AngVector RAPIER_CALL r2RigidBody_Angvel(struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is sleeping. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_IsSleeping(struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is enabled. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_IsEnabled(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body application-owned 128-bit user value. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2UserData RAPIER_CALL r2RigidBody_UserData(struct R2RigidBodyHandle handle); + +/** + * Set the rigid body world-space pose. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetPosition(struct R2RigidBodyHandle handle, + struct R2Pose value, + R2Bool wake_up); + +/** + * Set the rigid body world-space translation. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetTranslation(struct R2RigidBodyHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Set the rigid body world-space linear velocity. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetLinvel(struct R2RigidBodyHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Set the rigid body world-space angular velocity (radians per second). + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetAngvel(struct R2RigidBodyHandle handle, + R2AngVector value, + R2Bool wake_up); + +/** + * Set the rigid body next kinematic world-space pose. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetNextKinematicPosition(struct R2RigidBodyHandle handle, + struct R2Pose value); + +/** + * Set the rigid body next kinematic world-space translation. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetNextKinematicTranslation(struct R2RigidBodyHandle handle, + struct R2Vector value); + +/** + * Set the rigid body gravity multiplier. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetGravityScale(struct R2RigidBodyHandle handle, + R2Real value, + R2Bool wake_up); + +/** + * Set the rigid body linear damping coefficient. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetLinearDamping(struct R2RigidBodyHandle handle, + R2Real value); + +/** + * Set the rigid body angular damping coefficient. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetAngularDamping(struct R2RigidBodyHandle handle, + R2Real value); + +/** + * Enable or disable the rigid body. + * @ingroup rigid_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RigidBody_SetEnabled(struct R2RigidBodyHandle handle, R2Bool value); + +/** + * Set the rigid body application-owned 128-bit user value. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetUserData(struct R2RigidBodyHandle handle, + struct R2UserData value); + +/** + * Apply a world-space linear impulse. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_ApplyImpulse(struct R2RigidBodyHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Apply a world-space impulse at a world-space point. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_ApplyImpulseAtPoint(struct R2RigidBodyHandle handle, + struct R2Vector value, + struct R2Vector point, + R2Bool wake_up); + +/** + * Accumulate a world-space force; it persists until reset. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_AddForce(struct R2RigidBodyHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Clear accumulated user forces. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RigidBody_ResetForces(struct R2RigidBodyHandle handle, R2Bool wake_up); + +/** + * Put the body to sleep. + * @ingroup rigid_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RigidBody_Sleep(struct R2RigidBodyHandle handle); + +/** + * Return the collider world-space pose. + * @ingroup colliders + */ +RAPIER_API struct R2Pose RAPIER_CALL r2Collider_Position(struct R2ColliderHandle handle); + +/** + * Return the collider world-space translation. + * @ingroup colliders + */ +RAPIER_API struct R2Vector RAPIER_CALL r2Collider_Translation(struct R2ColliderHandle handle); + +/** + * Return the collider friction coefficient. + * @ingroup colliders + */ +RAPIER_API R2Real RAPIER_CALL r2Collider_Friction(struct R2ColliderHandle handle); + +/** + * Return the collider restitution coefficient. + * @ingroup colliders + */ +RAPIER_API R2Real RAPIER_CALL r2Collider_Restitution(struct R2ColliderHandle handle); + +/** + * Return whether the collider is a sensor (detects overlaps without contact forces). + * @ingroup colliders + */ +RAPIER_API R2Bool RAPIER_CALL r2Collider_IsSensor(struct R2ColliderHandle handle); + +/** + * Return the parent body handle, or an invalid handle with OK status for a standalone collider. + * @ingroup colliders + */ +RAPIER_API struct R2RigidBodyHandle RAPIER_CALL r2Collider_Parent(struct R2ColliderHandle handle); + +/** + * Set the collider world-space pose. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetPosition(struct R2ColliderHandle handle, + struct R2Pose value); + +/** + * Set the collider world-space translation. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetTranslation(struct R2ColliderHandle handle, + struct R2Vector value); + +/** + * Set the collider friction coefficient. + * @ingroup colliders + */ +RAPIER_API R2Status RAPIER_CALL r2Collider_SetFriction(struct R2ColliderHandle handle, R2Real value); + +/** + * Set the collider restitution coefficient. + * @ingroup colliders + */ +RAPIER_API R2Status RAPIER_CALL r2Collider_SetRestitution(struct R2ColliderHandle handle, R2Real value); + +/** + * Enable or disable a sensor (detects overlaps without contact forces) for the collider. + * @ingroup colliders + */ +RAPIER_API R2Status RAPIER_CALL r2Collider_SetSensor(struct R2ColliderHandle handle, R2Bool value); + +/** + * Set the collider collision filtering groups. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetCollisionGroups(struct R2ColliderHandle handle, + struct R2InteractionGroups value); + +/** + * Set the collider application-owned 128-bit user value. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetUserData(struct R2ColliderHandle handle, + struct R2UserData value); + +/** + * Return the world-space position of the indexed particle. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2Vector RAPIER_CALL r2SoftBody_ParticlePosition(struct R2SoftBodyHandle handle, + size_t index); + +/** + * Copy world-space particle positions. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_ParticlePositions(struct R2SoftBodyHandle handle, + struct R2Vector *buffer, + size_t capacity); + +/** + * Return a copy of the soft body material parameters. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SoftBodyMaterial RAPIER_CALL r2SoftBody_Material(struct R2SoftBodyHandle handle); + +/** + * Set the world-space position of the indexed particle. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetParticlePosition(struct R2SoftBodyHandle handle, + size_t index, + struct R2Vector value); + +/** + * Copy material parameters into the soft body. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetMaterial(struct R2SoftBodyHandle handle, + const struct R2SoftBodyMaterial *data); + +/** + * Add a world-space force to the indexed particle. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_AddParticleForce(struct R2SoftBodyHandle handle, + size_t index, + struct R2Vector value, + R2Bool wake_up); + +/** + * Copies states in the same order as handles, without allocating temporary storage. + * All handles are validated before writing. On INVALID_HANDLE outputs are unchanged. + * NULL/0 is a size query. BUFFER_TOO_SMALL returns the required count and leaves states untouched. + * @ingroup rigid_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2RigidBodyReadStates(const struct R2World *world, + const struct R2RigidBodyHandle *handles, + size_t handle_count, + struct R2RigidBodyState *states, + size_t capacity); + +/** + * Copies joint configuration without returning a borrowed joint pointer. + * @ingroup joints + */ +RAPIER_API struct R2JointDesc RAPIER_CALL r2ImpulseJoint_Desc(struct R2ImpulseJointHandle handle); + +/** + * Replaces configuration after validation, resetting cached limit/motor impulses. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetDesc(struct R2ImpulseJointHandle handle, + const struct R2JointDesc *desc, + R2Bool wake_up); + +/** + * Replace the shape geometry with a borrowed tri mesh. Counts are elements. + * Copies no arrays. Invalid view metadata leaves the description unchanged. + * Geometry and flags are validated when the description is built or inserted. + * @ingroup shapes + */ +RAPIER_API +R2Status RAPIER_CALL r2ShapeDesc_SetTrimesh(struct R2ShapeDesc *desc, + struct R2VectorView vertices, + struct R2TriangleView indices, + uint32_t flags); + +/** + * Replace the shape geometry with a borrowed polyline. Counts are elements. + * Copies no arrays. Invalid view metadata leaves the description unchanged. + * Geometry and flags are validated when the description is built or inserted. + * @ingroup shapes + */ +RAPIER_API +R2Status RAPIER_CALL r2ShapeDesc_SetPolyline(struct R2ShapeDesc *desc, + struct R2VectorView vertices, + struct R2EdgeView indices, + uint32_t flags); + +/** + * Replace the shape geometry with a borrowed convex hull point cloud. + * @ingroup shapes + */ +RAPIER_API +R2Status RAPIER_CALL r2ShapeDesc_SetConvexHull(struct R2ShapeDesc *desc, + struct R2VectorView vertices); + +/** + * Select an explicit particle recipe and borrow its positions. Other fields are preserved. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetParticles(struct R2SoftBodyDesc *desc, + struct R2VectorView positions); + +/** + * Select a surface recipe and borrow its vertices and elements. Other fields are preserved. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetSurfaceMesh(struct R2SoftBodyDesc *desc, + struct R2VectorView vertices, + R2SurfaceElementView elements); + +/** + * Borrow skin geometry. Other fields, including skinCollision, are preserved. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetSkin(struct R2SoftBodyDesc *desc, + struct R2VectorView vertices, + R2SurfaceElementView elements); + +/** + * Borrow masses; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetMasses(struct R2SoftBodyDesc *desc, + struct R2RealView view); + +/** + * Borrow pinned particles; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetPinnedParticles(struct R2SoftBodyDesc *desc, + struct R2IndexView view); + +/** + * Borrow edges; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetEdges(struct R2SoftBodyDesc *desc, + struct R2EdgeView view); + +/** + * Borrow bend edges; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetBendEdges(struct R2SoftBodyDesc *desc, + struct R2EdgeView view); + +/** + * Borrow cells; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBodyDesc_SetCells(struct R2SoftBodyDesc *desc, R2CellView view); + +/** + * Borrow surface; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetSurface(struct R2SoftBodyDesc *desc, + R2SurfaceElementView view); + +/** + * Borrow tension only edges; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetTensionOnlyEdges(struct R2SoftBodyDesc *desc, + struct R2IndexView view); + +#if defined(RAPIER_DIM3) +/** + * Borrow dihedrals; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetDihedrals(struct R2SoftBodyDesc *desc, + struct R2DihedralView view); +#endif + +#if defined(RAPIER_DIM3) +/** + * Borrow wire; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetWire(struct R2SoftBodyDesc *desc, + struct R2EdgeView view); +#endif + +/** + * Return a rounded box description; half_extents exclude the added border_radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API +struct R2ColliderDesc RAPIER_CALL r2RoundCuboidColliderDesc(struct R2Vector half_extents, + R2Real border_radius); + +/** + * Return a capsule description with segment endpoints a/b and the supplied radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API +struct R2ColliderDesc RAPIER_CALL r2CapsuleColliderDesc(struct R2Vector a, + struct R2Vector b, + R2Real radius); + +/** + * Return a segment description with endpoints a and b. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R2ColliderDesc RAPIER_CALL r2SegmentColliderDesc(struct R2Vector a, struct R2Vector b); + +/** + * Return a triangle description with vertices a, b, and c. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API +struct R2ColliderDesc RAPIER_CALL r2TriangleColliderDesc(struct R2Vector a, + struct R2Vector b, + struct R2Vector c); + +/** + * Return a half-space description bounded by a plane through the origin; normal points outward. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R2ColliderDesc RAPIER_CALL r2HalfspaceColliderDesc(struct R2Vector normal); + +#if defined(RAPIER_DIM3) +/** + * Return a Y-aligned cylinder description with the supplied half-height and radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R2ColliderDesc RAPIER_CALL r2CylinderColliderDesc(R2Real half_height, R2Real radius); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a Y-aligned cone description with the supplied half-height and base radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R2ColliderDesc RAPIER_CALL r2ConeColliderDesc(R2Real half_height, R2Real radius); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a rounded Y-aligned cylinder description; dimensions exclude border_radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API +struct R2ColliderDesc RAPIER_CALL r2RoundCylinderColliderDesc(R2Real half_height, + R2Real radius, + R2Real border_radius); +#endif + +/** + * Return a X-aligned capsule description; half_height is half the segment length, excluding caps. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R2ColliderDesc RAPIER_CALL r2CapsuleXColliderDesc(R2Real half_height, R2Real radius); + +/** + * Return a Y-aligned capsule description; half_height is half the segment length, excluding caps. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R2ColliderDesc RAPIER_CALL r2CapsuleYColliderDesc(R2Real half_height, R2Real radius); + +#if defined(RAPIER_DIM3) +/** + * Return a Z-aligned capsule description; half_height is half the segment length, excluding caps. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R2ColliderDesc RAPIER_CALL r2CapsuleZColliderDesc(R2Real half_height, R2Real radius); +#endif + +/** + * Return a rope recipe with particles evenly spaced from a to b, including both endpoints. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyDesc RAPIER_CALL r2RopeSoftBodyDesc(struct R2Vector a, + struct R2Vector b, + size_t particles); + +#if defined(RAPIER_DIM2) +/** + * Return a solid rectangle recipe on an nx by ny particle grid. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyDesc RAPIER_CALL r2GridSoftBodyDesc(struct R2Vector center, + struct R2Vector half_extents, + size_t nx, + size_t ny); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a solid box recipe on an nx by ny by nz particle grid, subdivided into tetrahedra. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyDesc RAPIER_CALL r2CuboidSoftBodyDesc(struct R2Vector center, + struct R2Vector half_extents, + size_t nx, + size_t ny, + size_t nz); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a cloth recipe with nu by nv particles at origin + i * du + j * dv. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyDesc RAPIER_CALL r2ClothSoftBodyDesc(struct R2Vector origin, + struct R2Vector du, + struct R2Vector dv, + size_t nu, + size_t nv); +#endif + +#if defined(RAPIER_DIM2) +/** + * Return a closed regular polygon recipe with the specified boundary particle count and area + * preservation. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyDesc RAPIER_CALL r2DiskSoftBodyDesc(struct R2Vector center, + R2Real radius, + size_t particles); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a hollow icosphere recipe with the specified refinement levels and volume preservation. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyDesc RAPIER_CALL r2SphereSoftBodyDesc(struct R2Vector center, + R2Real radius, + uint32_t subdivisions); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a cloth tube recipe from origin to origin + axis with num_along rings of num_around + * particles; radius varies linearly between the ends. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyDesc RAPIER_CALL r2ClothTubeSoftBodyDesc(struct R2Vector origin, + struct R2Vector axis, + R2Real radius_start, + R2Real radius_end, + size_t num_around, + size_t num_along); +#endif + +/** + * Initializes a borrowed meshing recipe. Mesh generation happens on preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyDesc RAPIER_CALL r2VolumetricSoftBodyDesc(struct R2VectorView vertices, + R2SurfaceElementView surface, + struct R2VolumeMeshParameters parameters); + +/** + * Returns a material with the same softness for each constraint family. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyMaterial RAPIER_CALL r2UniformSoftBodyMaterial(struct R2SpringCoefficients value); + +/** + * Copies generated particle positions into caller-owned storage; no persistent builder. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyDesc_ParticlePositions(const struct R2SoftBodyDesc *desc, + struct R2Vector *buffer, + size_t capacity); + +/** + * Copies generated cell indices into caller-owned storage. Counts scalar indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBodyDesc_CellIndices(const struct R2SoftBodyDesc *desc, + uint32_t *buffer, + size_t capacity); + +/** + * Return a process-local geometry identity for caching, not a serializable ID. Keep a shared-shape + * clone alive while using it as a cache key. + * @ingroup shapes + */ +RAPIER_API size_t RAPIER_CALL r2Collider_ShapeIdentity(struct R2ColliderHandle handle); + +/** + * Return the soft body particle count. + * @ingroup soft_bodies + */ +RAPIER_API size_t RAPIER_CALL r2SoftBody_NumParticles(struct R2SoftBodyHandle handle); + +/** + * Return a counter that changes when particle connectivity changes; use it to invalidate mesh + * caches. + * @ingroup soft_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r2SoftBody_TopologyVersion(struct R2SoftBodyHandle handle); + +/** + * Return the soft body mass. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2SoftBody_Mass(struct R2SoftBodyHandle handle); + +/** + * Return the soft body current volume. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2SoftBody_Volume(struct R2SoftBodyHandle handle); + +/** + * Return the soft body undeformed volume. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2SoftBody_RestVolume(struct R2SoftBodyHandle handle); + +/** + * Return the soft body target volume multiplier. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2SoftBody_VolumeFactor(struct R2SoftBodyHandle handle); + +/** + * Return the soft body world-space center of mass. + * @ingroup soft_bodies + */ +RAPIER_API struct R2Vector RAPIER_CALL r2SoftBody_CenterOfMass(struct R2SoftBodyHandle handle); + +/** + * Return the soft body root rigid-proxy handle. + * @ingroup soft_bodies + */ +RAPIER_API struct R2RigidBodyHandle RAPIER_CALL r2SoftBody_RootBody(struct R2SoftBodyHandle handle); + +/** + * Return whether the soft body is enabled. + * @ingroup soft_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2SoftBody_IsEnabled(struct R2SoftBodyHandle handle); + +/** + * Return whether the soft body is sleeping. + * @ingroup soft_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2SoftBody_IsSleeping(struct R2SoftBodyHandle handle); + +/** + * Copy world-space particle velocities. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_ParticleVelocities(struct R2SoftBodyHandle handle, + struct R2Vector *buffer, + size_t capacity); + +/** + * Copy flattened edge vertex indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Edges(struct R2SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Copy flattened cell vertex indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Cells(struct R2SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Copy flattened boundary element indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Boundary(struct R2SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Copy piece identifiers. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Pieces(struct R2SoftBodyHandle handle, + struct R2SoftBodyHandle *buffer, + size_t capacity); + +/** + * Set the soft body particle world-space velocity. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetParticleVelocity(struct R2SoftBodyHandle handle, + size_t index, + struct R2Vector value); + +/** + * Set the next world-space target position of a pinned particle. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetParticleKinematicTarget(struct R2SoftBodyHandle handle, + size_t index, + struct R2Vector value); + +/** + * Enable or disable pinning the particle for the soft body. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetParticlePinned(struct R2SoftBodyHandle handle, + size_t index, + R2Bool value); + +/** + * Apply a world-space impulse to one particle. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_ApplyParticleImpulse(struct R2SoftBodyHandle handle, + size_t index, + struct R2Vector value, + R2Bool wake_up); + +/** + * Accumulate a world-space force; it persists until reset. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_AddForce(struct R2SoftBodyHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Apply a world-space linear impulse. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_ApplyImpulse(struct R2SoftBodyHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Clear accumulated user forces. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBody_ResetForces(struct R2SoftBodyHandle handle, R2Bool wake_up); + +/** + * Enable or disable the soft body. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBody_SetEnabled(struct R2SoftBodyHandle handle, R2Bool value); + +/** + * Set the soft body target volume multiplier. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetVolumeFactor(struct R2SoftBodyHandle handle, + R2Real value); + +/** + * Attach a particle to a rigid body at the supplied body-local anchor. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_AttachParticle(struct R2SoftBodyHandle handle, + size_t index, + struct R2RigidBodyHandle rigid_body); + +/** + * Remove a particle attachment to a rigid body. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBody_DetachParticle(struct R2SoftBodyHandle handle, size_t index); + +/** + * Copy cluster indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Clusters(struct R2SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Return the rigid proxy for the selected cluster. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2RigidBodyHandle RAPIER_CALL r2SoftBody_ClusterProxy(struct R2SoftBodyHandle handle, + uint32_t cluster); + +/** + * Copy particle indices for a cluster. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_ClusterParticles(struct R2SoftBodyHandle handle, + uint32_t cluster, + uint32_t *buffer, + size_t capacity); + +/** + * Enable or disable pinning the cluster for the soft body. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetClusterPinned(struct R2SoftBodyHandle handle, + uint32_t cluster, + R2Bool value); + +/** + * Set the next world-space target pose of a pinned cluster. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetClusterKinematicTarget(struct R2SoftBodyHandle handle, + uint32_t cluster, + struct R2Pose value); + +/** + * Enable or disable using cluster shape matching for the soft body. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetClusterShapeMatchingEnabled(struct R2SoftBodyHandle handle, + uint32_t cluster, + R2Bool value); + +/** + * Set the soft body cluster shape-matching stiffness multiplier. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetClusterStiffnessScale(struct R2SoftBodyHandle handle, + uint32_t cluster, + R2Real value); + +/** + * Set the soft body cluster tear-resistance multiplier. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetClusterTearResistance(struct R2SoftBodyHandle handle, + uint32_t cluster, + R2Real value); + +/** + * Copy collision mesh metadata. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Meshes(struct R2SoftBodyHandle handle, + struct R2SoftMeshInfo *buffer, + size_t capacity); + +/** + * Copy world-space vertices for a mesh ID. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_MeshVerticesById(struct R2SoftBodyHandle handle, + struct R2SoftMeshId id, + struct R2Vector *buffer, + size_t capacity); + +/** + * Copy flattened indices for a mesh ID. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_MeshIndicesById(struct R2SoftBodyHandle handle, + struct R2SoftMeshId id, + uint32_t *buffer, + size_t capacity); + +/** + * Copy collision mesh collider handles. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_MeshColliders(struct R2SoftBodyHandle handle, + struct R2ColliderHandle *buffer, + size_t capacity); + +/** + * Copy world-space collision mesh vertices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_MeshVertices(struct R2SoftBodyHandle handle, + struct R2ColliderHandle collider, + struct R2Vector *buffer, + size_t capacity); + +/** + * Copy flattened collision mesh indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_MeshIndices(struct R2SoftBodyHandle handle, + struct R2ColliderHandle collider, + uint32_t *buffer, + size_t capacity); + +/** + * Return indices per collision-mesh element (2 for segments, 3 for triangles). + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_MeshArity(struct R2SoftBodyHandle handle, + struct R2ColliderHandle collider); + +/** + * Return the selected collision mesh topology revision for cache invalidation. + * @ingroup soft_bodies + */ +RAPIER_API +uint32_t RAPIER_CALL r2SoftBody_MeshTopologyVersion(struct R2SoftBodyHandle handle, + struct R2ColliderHandle collider); + +#if defined(RAPIER_FEM) +/** + * Set the soft body soft solver kind (R2_SOFT_SOLVER_*). + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBody_SetSolver(struct R2SoftBodyHandle handle, uint32_t solver); +#endif + +/** + * Set the soft body cluster shape-matching target pose. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetClusterShapeMatchingTarget(struct R2SoftBodyHandle handle, + uint32_t cluster, + const struct R2Pose *target); + +/** + * Set the soft body edge tear-resistance multiplier. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetEdgeTearResistance(struct R2SoftBodyHandle handle, + size_t index, + R2Real resistance); + +/** + * Return whether the selected collision mesh is closed. + * @ingroup soft_bodies + */ +RAPIER_API +R2Bool RAPIER_CALL r2SoftBody_MeshIsClosed(struct R2SoftBodyHandle handle, + struct R2ColliderHandle collider); + +/** + * Set the rigid body local mass properties added to collider contributions. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetAdditionalMassProperties(struct R2RigidBodyHandle handle, + struct R2MassProperties properties, + R2Bool wake_up); + +/** + * Recompute body mass and inertia from attached colliders and additional mass properties. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_RecomputeMassPropertiesFromColliders(struct R2RigidBodyHandle handle); + +/** + * Set the collider local mass properties. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetMassProperties(struct R2ColliderHandle handle, + struct R2MassProperties properties); + +/** + * Return the collider local mass properties. + * @ingroup colliders + */ +RAPIER_API struct R2MassProperties RAPIER_CALL r2Collider_MassProperties(struct R2ColliderHandle handle); + +/** + * Set the rigid body translation/rotation lock bitmask. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetLockedAxes(struct R2RigidBodyHandle handle, + uint8_t axes, + R2Bool wake_up); + +/** + * Return the rigid body translation/rotation lock bitmask. + * @ingroup rigid_bodies + */ +RAPIER_API uint8_t RAPIER_CALL r2RigidBody_LockedAxes(struct R2RigidBodyHandle handle); + +/** + * Return whether the collider is a voxel shape. + * @ingroup colliders + */ +RAPIER_API R2Bool RAPIER_CALL r2Collider_IsVoxels(struct R2ColliderHandle handle); + +/** + * Return voxel information at a flat index; found = 0 if absent. + * @ingroup colliders + */ +RAPIER_API +struct R2VoxelQuery RAPIER_CALL r2Collider_VoxelAtFlatId(struct R2ColliderHandle handle, + uint32_t id); + +/** + * Fill or clear the voxel at key; the collider must have a voxel shape. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetVoxel(struct R2ColliderHandle handle, + struct R2VoxelKey key, + R2Bool filled); + +/** + * Return the rigid body next kinematic world-space pose. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2Pose RAPIER_CALL r2RigidBody_NextPosition(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body world-space rotation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2Rotation RAPIER_CALL r2RigidBody_Rotation(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body world-space center of mass. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2Vector RAPIER_CALL r2RigidBody_CenterOfMass(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body body-local center of mass. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2Vector RAPIER_CALL r2RigidBody_LocalCenterOfMass(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body accumulated user-applied world-space force. + * @ingroup rigid_bodies + */ +RAPIER_API struct R2Vector RAPIER_CALL r2RigidBody_UserForce(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body accumulated user-applied world-space torque. + * @ingroup rigid_bodies + */ +RAPIER_API R2AngVector RAPIER_CALL r2RigidBody_UserTorque(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body body type (R2_DYNAMIC, R2_FIXED, or a kinematic kind). + * @ingroup rigid_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r2RigidBody_BodyType(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body mass. + * @ingroup rigid_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2RigidBody_Mass(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body gravity multiplier. + * @ingroup rigid_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2RigidBody_GravityScale(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body linear damping coefficient. + * @ingroup rigid_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2RigidBody_LinearDamping(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body angular damping coefficient. + * @ingroup rigid_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2RigidBody_AngularDamping(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body kinetic energy. + * @ingroup rigid_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2RigidBody_KineticEnergy(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body soft-CCD prediction distance. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2RigidBody_SoftCcdPrediction(struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is using continuous collision detection. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_IsCcdEnabled(struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is dynamic. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_IsDynamic(struct R2RigidBodyHandle handle); + +/** + * Return the associated soft-body handle, or an invalid handle if this is not a soft proxy. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SoftBodyHandle RAPIER_CALL r2RigidBody_SoftBody(struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is a soft-body proxy. + * @ingroup soft_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_IsSoftFrame(struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is fixed. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_IsFixed(struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is kinematic. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_IsKinematic(struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is moving. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_IsMoving(struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is currently using CCD for its motion. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_IsCcdActive(struct R2RigidBodyHandle handle); + +/** + * Set the rigid body world-space rotation. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetRotation(struct R2RigidBodyHandle handle, + struct R2Rotation value, + R2Bool wake_up); + +/** + * Set the rigid body body type (R2_DYNAMIC, R2_FIXED, or a kinematic kind). + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetBodyType(struct R2RigidBodyHandle handle, + uint32_t value, + R2Bool wake_up); + +/** + * Set the rigid body next kinematic world-space rotation. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetNextKinematicRotation(struct R2RigidBodyHandle handle, + struct R2Rotation value); + +/** + * Set the rigid body mass added to collider contributions. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetAdditionalMass(struct R2RigidBodyHandle handle, + R2Real value, + R2Bool wake_up); + +/** + * Set the rigid body soft-CCD prediction distance. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetSoftCcdPrediction(struct R2RigidBodyHandle handle, + R2Real value); + +/** + * Enable or disable using continuous collision detection for the rigid body. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetCcdEnabled(struct R2RigidBodyHandle handle, + R2Bool value); + +/** + * Enable or disable locking translation for the rigid body. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetTranslationsLocked(struct R2RigidBodyHandle handle, + R2Bool value, + R2Bool wake_up); + +/** + * Enable or disable locking rotation for the rigid body. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetRotationsLocked(struct R2RigidBodyHandle handle, + R2Bool value, + R2Bool wake_up); + +/** + * Set the rigid body signed dominance group. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetDominanceGroup(struct R2RigidBodyHandle handle, + int8_t value); + +/** + * Set the rigid body additional solver iterations for connected bodies. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetAdditionalSolverIterations(struct R2RigidBodyHandle handle, + size_t value); + +/** + * Set the rigid body additional PGS iterations. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetAdditionalPgsIterations(struct R2RigidBodyHandle handle, + size_t value); + +/** + * Accumulate a world-space torque; it persists until reset. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_AddTorque(struct R2RigidBodyHandle handle, + R2AngVector value, + R2Bool wake_up); + +/** + * Apply a world-space angular impulse. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_ApplyTorqueImpulse(struct R2RigidBodyHandle handle, + R2AngVector value, + R2Bool wake_up); + +/** + * Accumulate a world-space force applied at a world-space point. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_AddForceAtPoint(struct R2RigidBodyHandle handle, + struct R2Vector value, + struct R2Vector point, + R2Bool wake_up); + +/** + * Clear accumulated user torques. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_ResetTorques(struct R2RigidBodyHandle handle, + R2Bool wake_up); + +/** + * Return world-space velocity at a world-space point, including angular motion. + * @ingroup rigid_bodies + */ +RAPIER_API +struct R2Vector RAPIER_CALL r2RigidBody_VelocityAtPoint(struct R2RigidBodyHandle handle, + struct R2Vector point); + +/** + * Copy attached collider handles. + * @see @ref output_buffers + * @ingroup rigid_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2RigidBody_Colliders(struct R2RigidBodyHandle handle, + struct R2ColliderHandle *buffer, + size_t capacity); + +#if defined(RAPIER_DIM3) +/** + * Return whether the rigid body is using gyroscopic forces. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_GyroscopicForcesEnabled(struct R2RigidBodyHandle handle); +#endif + +#if defined(RAPIER_DIM3) +/** + * Enable or disable using gyroscopic forces for the rigid body. + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetGyroscopicForcesEnabled(struct R2RigidBodyHandle handle, + R2Bool enabled); +#endif + +/** + * Set the collider mass per unit volume. + * @ingroup colliders + */ +RAPIER_API R2Status RAPIER_CALL r2Collider_SetDensity(struct R2ColliderHandle handle, R2Real value); + +/** + * Set the collider mass. + * @ingroup colliders + */ +RAPIER_API R2Status RAPIER_CALL r2Collider_SetMass(struct R2ColliderHandle handle, R2Real value); + +/** + * Enable or disable the collider. + * @ingroup colliders + */ +RAPIER_API R2Status RAPIER_CALL r2Collider_SetEnabled(struct R2ColliderHandle handle, R2Bool value); + +/** + * Set the collider contact-force filtering groups. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetSolverGroups(struct R2ColliderHandle handle, + struct R2InteractionGroups value); + +/** + * Set the collider friction combination rule (R2_COMBINE_*). + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetFrictionCombineRule(struct R2ColliderHandle handle, + uint32_t value); + +/** + * Set the collider restitution combination rule (R2_COMBINE_*). + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetRestitutionCombineRule(struct R2ColliderHandle handle, + uint32_t value); + +/** + * Set the collider extra separation skin around the shape. + * @ingroup colliders + */ +RAPIER_API R2Status RAPIER_CALL r2Collider_SetContactSkin(struct R2ColliderHandle handle, R2Real value); + +/** + * Set the collider force threshold for contact-force events. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetContactForceEventThreshold(struct R2ColliderHandle handle, + R2Real value); + +/** + * Set the collider event-generation bitmask (R2_COLLISION_EVENTS and R2_CONTACT_FORCE_EVENTS). + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetActiveEvents(struct R2ColliderHandle handle, + uint32_t value); + +/** + * Set the collider physics-hook activation bitmask. + * @ingroup colliders + */ +RAPIER_API R2Status RAPIER_CALL r2Collider_SetActiveHooks(struct R2ColliderHandle handle, uint32_t value); + +/** + * Set the collider body-type collision activation bitmask. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetActiveCollisionTypes(struct R2ColliderHandle handle, + uint16_t value); + +/** + * Return the collider world-space rotation. + * @ingroup colliders + */ +RAPIER_API struct R2Rotation RAPIER_CALL r2Collider_Rotation(struct R2ColliderHandle handle); + +/** + * Return the collider collision filtering groups. + * @ingroup colliders + */ +RAPIER_API +struct R2InteractionGroups RAPIER_CALL r2Collider_CollisionGroups(struct R2ColliderHandle handle); + +/** + * Return the collider contact-force filtering groups. + * @ingroup colliders + */ +RAPIER_API struct R2InteractionGroups RAPIER_CALL r2Collider_SolverGroups(struct R2ColliderHandle handle); + +/** + * Return the collider application-owned 128-bit user value. + * @ingroup colliders + */ +RAPIER_API struct R2UserData RAPIER_CALL r2Collider_UserData(struct R2ColliderHandle handle); + +/** + * Return the collider event-generation bitmask (R2_COLLISION_EVENTS and + * R2_CONTACT_FORCE_EVENTS). + * @ingroup colliders + */ +RAPIER_API uint32_t RAPIER_CALL r2Collider_ActiveEvents(struct R2ColliderHandle handle); + +/** + * Return the collider mass. + * @ingroup colliders + */ +RAPIER_API R2Real RAPIER_CALL r2Collider_Mass(struct R2ColliderHandle handle); + +/** + * Return the collider mass per unit volume. + * @ingroup colliders + */ +RAPIER_API R2Real RAPIER_CALL r2Collider_Density(struct R2ColliderHandle handle); + +/** + * Return the collider current volume. + * @ingroup colliders + */ +RAPIER_API R2Real RAPIER_CALL r2Collider_Volume(struct R2ColliderHandle handle); + +/** + * Return the collider extra separation skin around the shape. + * @ingroup colliders + */ +RAPIER_API R2Real RAPIER_CALL r2Collider_ContactSkin(struct R2ColliderHandle handle); + +/** + * Return the collider force threshold for contact-force events. + * @ingroup colliders + */ +RAPIER_API R2Real RAPIER_CALL r2Collider_ContactForceEventThreshold(struct R2ColliderHandle handle); + +/** + * Return whether the collider is enabled. + * @ingroup colliders + */ +RAPIER_API R2Bool RAPIER_CALL r2Collider_IsEnabled(struct R2ColliderHandle handle); + +/** + * Return the current world-space axis-aligned bounds. + * @ingroup colliders + */ +RAPIER_API struct R2Aabb RAPIER_CALL r2Collider_ComputeAabb(struct R2ColliderHandle handle); + +/** + * Return an owned wrapper sharing the collider geometry. Release with r2FreeSharedShape. + * Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2Collider_CloneShape(struct R2ColliderHandle handle); + +/** + * Replace collider geometry by sharing shape; the supplied wrapper is not consumed. + * @ingroup shapes + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetShape(struct R2ColliderHandle handle, + const R2SharedShape *shape); + +/** + * Set the collider pose relative to the parent rigid body. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetPositionWrtParent(struct R2ColliderHandle handle, + struct R2Pose value); + +/** + * Validate the index and generation in the live owning world. Cannot detect a world pointer that + * has already been freed. + * @ingroup rigid_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RigidBody_ValidateHandle(struct R2RigidBodyHandle handle); + +/** + * Validate the index and generation in the live owning world. Cannot detect a world pointer that + * has already been freed. + * @ingroup colliders + */ +RAPIER_API R2Status RAPIER_CALL r2Collider_ValidateHandle(struct R2ColliderHandle handle); + +/** + * Validate the index and generation in the live owning world. Cannot detect a world pointer that + * has already been freed. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBody_ValidateHandle(struct R2SoftBodyHandle handle); + +/** + * Set the joint desc joint frame relative to body 1. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetLocalFrame1(struct R2JointDesc *desc, + struct R2Pose value); + +/** + * Set the impulse joint joint frame relative to body 1. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetLocalFrame1(struct R2ImpulseJointHandle handle, + struct R2Pose value, + R2Bool wake_up); + +/** + * Set the joint desc joint frame relative to body 2. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetLocalFrame2(struct R2JointDesc *desc, + struct R2Pose value); + +/** + * Set the impulse joint joint frame relative to body 2. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetLocalFrame2(struct R2ImpulseJointHandle handle, + struct R2Pose value, + R2Bool wake_up); + +/** + * Set the joint desc joint anchor relative to body 1. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetLocalAnchor1(struct R2JointDesc *desc, + struct R2Vector value); + +/** + * Set the impulse joint joint anchor relative to body 1. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetLocalAnchor1(struct R2ImpulseJointHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Set the joint desc joint anchor relative to body 2. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetLocalAnchor2(struct R2JointDesc *desc, + struct R2Vector value); + +/** + * Set the impulse joint joint anchor relative to body 2. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetLocalAnchor2(struct R2ImpulseJointHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Enable or disable allowing contacts between connected bodies for the joint desc. + * @ingroup joints + */ +RAPIER_API R2Status RAPIER_CALL r2JointDesc_SetContactsEnabled(struct R2JointDesc *desc, R2Bool value); + +/** + * Enable or disable allowing contacts between connected bodies for the impulse joint. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetContactsEnabled(struct R2ImpulseJointHandle handle, + R2Bool value, + R2Bool wake_up); + +/** + * Enable or disable the joint desc. + * @ingroup joints + */ +RAPIER_API R2Status RAPIER_CALL r2JointDesc_SetEnabled(struct R2JointDesc *desc, R2Bool value); + +/** + * Enable or disable the impulse joint. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetEnabled(struct R2ImpulseJointHandle handle, + R2Bool value, + R2Bool wake_up); + +/** + * Set the joint desc joint spring coefficients. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetSoftness(struct R2JointDesc *desc, + struct R2SpringCoefficients value); + +/** + * Set the impulse joint joint spring coefficients. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetSoftness(struct R2ImpulseJointHandle handle, + struct R2SpringCoefficients value, + R2Bool wake_up); + +/** + * Set the joint desc translation/rotation lock bitmask. + * @ingroup joints + */ +RAPIER_API R2Status RAPIER_CALL r2JointDesc_SetLockedAxes(struct R2JointDesc *desc, uint8_t value); + +/** + * Set the impulse joint translation/rotation lock bitmask. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetLockedAxes(struct R2ImpulseJointHandle handle, + uint8_t value, + R2Bool wake_up); + +/** + * Set the joint desc joint axis mask with limits enabled. + * @ingroup joints + */ +RAPIER_API R2Status RAPIER_CALL r2JointDesc_SetLimitAxes(struct R2JointDesc *desc, uint8_t value); + +/** + * Set the impulse joint joint axis mask with limits enabled. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetLimitAxes(struct R2ImpulseJointHandle handle, + uint8_t value, + R2Bool wake_up); + +/** + * Set the joint desc joint axis mask with motors enabled. + * @ingroup joints + */ +RAPIER_API R2Status RAPIER_CALL r2JointDesc_SetMotorAxes(struct R2JointDesc *desc, uint8_t value); + +/** + * Set the impulse joint joint axis mask with motors enabled. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetMotorAxes(struct R2ImpulseJointHandle handle, + uint8_t value, + R2Bool wake_up); + +/** + * Set the joint desc coupled joint axis mask. + * @ingroup joints + */ +RAPIER_API R2Status RAPIER_CALL r2JointDesc_SetCoupledAxes(struct R2JointDesc *desc, uint8_t value); + +/** + * Set the impulse joint coupled joint axis mask. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetCoupledAxes(struct R2ImpulseJointHandle handle, + uint8_t value, + R2Bool wake_up); + +/** + * Set the joint desc joint principal axis in body 1 local coordinates. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetLocalAxis1(struct R2JointDesc *desc, + struct R2Vector value); + +/** + * Set the impulse joint joint principal axis in body 1 local coordinates. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetLocalAxis1(struct R2ImpulseJointHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Set the joint desc joint principal axis in body 2 local coordinates. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetLocalAxis2(struct R2JointDesc *desc, + struct R2Vector value); + +/** + * Set the impulse joint joint principal axis in body 2 local coordinates. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetLocalAxis2(struct R2ImpulseJointHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Set the joint desc minimum and maximum limits on an axis (linear distance or angular radians). + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetLimits(struct R2JointDesc *desc, + uint32_t joint_axis, + R2Real min, + R2Real max); + +/** + * Set the impulse joint minimum and maximum limits on an axis (linear distance or angular + * radians). + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetLimits(struct R2ImpulseJointHandle handle, + uint32_t joint_axis, + R2Real min, + R2Real max, + R2Bool wake_up); + +/** + * Set the joint desc motor position/velocity targets and spring coefficients on an axis. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetMotor(struct R2JointDesc *desc, + uint32_t joint_axis, + R2Real target_position, + R2Real target_velocity, + R2Real stiffness, + R2Real damping); + +/** + * Set the impulse joint motor position/velocity targets and spring coefficients on an axis. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetMotor(struct R2ImpulseJointHandle handle, + uint32_t joint_axis, + R2Real target_position, + R2Real target_velocity, + R2Real stiffness, + R2Real damping, + R2Bool wake_up); + +/** + * Set the joint desc maximum motor force or torque on an axis. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetMotorMaxForce(struct R2JointDesc *desc, + uint32_t joint_axis, + R2Real max_force); + +/** + * Set the impulse joint maximum motor force or torque on an axis. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetMotorMaxForce(struct R2ImpulseJointHandle handle, + uint32_t joint_axis, + R2Real max_force, + R2Bool wake_up); + +/** + * Set the joint desc motor model on an axis (0 = acceleration-based, 1 = force-based). + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetMotorModel(struct R2JointDesc *desc, + uint32_t joint_axis, + uint32_t model); + +/** + * Set the impulse joint motor model on an axis (0 = acceleration-based, 1 = force-based). + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetMotorModel(struct R2ImpulseJointHandle handle, + uint32_t joint_axis, + uint32_t model, + R2Bool wake_up); + +/** + * Set the joint desc application-owned 128-bit user value. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetUserData(struct R2JointDesc *desc, + struct R2UserData value); + +/** + * Set the impulse joint application-owned 128-bit user value. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetUserData(struct R2ImpulseJointHandle handle, + struct R2UserData value, + R2Bool wake_up); + +/** + * Set the joint desc motor position target and spring coefficients on an axis. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetMotorPosition(struct R2JointDesc *desc, + uint32_t joint_axis, + R2Real target_position, + R2Real stiffness, + R2Real damping); + +/** + * Set the impulse joint motor position target and spring coefficients on an axis. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetMotorPosition(struct R2ImpulseJointHandle handle, + uint32_t joint_axis, + R2Real target_position, + R2Real stiffness, + R2Real damping, + R2Bool wake_up); + +/** + * Set the joint desc motor velocity target and damping factor on an axis. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2JointDesc_SetMotorVelocity(struct R2JointDesc *desc, + uint32_t joint_axis, + R2Real target_velocity, + R2Real factor); + +/** + * Set the impulse joint motor velocity target and damping factor on an axis. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2ImpulseJoint_SetMotorVelocity(struct R2ImpulseJointHandle handle, + uint32_t joint_axis, + R2Real target_velocity, + R2Real factor, + R2Bool wake_up); + +/** + * Create an owned compound shape by convex decomposition of the input surface. Release it with + * r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2ConvexDecompositionSharedShape(struct R2VectorView vertices, + R2SurfaceElementView indices); + +/** + * Create an owned voxel shape by quantizing points with the supplied per-axis voxel size. Release + * it with r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2VoxelsSharedShapeFromPoints(struct R2Vector voxel_size, + struct R2VectorView points); + +/** + * Create an owned voxel shape from a surface mesh with the supplied uniform voxel size. Release it + * with r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2VoxelizedMeshSharedShape(struct R2VectorView vertices, + R2SurfaceElementView indices, + R2Real voxel_size); + +/** + * Create an owned convex hull of the supplied vertices. Release it with r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2ConvexHullSharedShape(struct R2VectorView vertices); + +/** + * Create an owned triangle mesh from vertices and triangle indices. Release it with + * r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2TrimeshSharedShape(struct R2VectorView vertices, + struct R2TriangleView indices); + +/** + * Create an owned polyline from vertices and edge indices. Release it with r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2PolylineSharedShape(struct R2VectorView vertices, + struct R2EdgeView indices); + +#if defined(RAPIER_DIM2) +/** + * Create an owned oriented 2D polyline from vertices and edge indices. Release it with + * r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2OrientedPolylineSharedShape(struct R2VectorView vertices, + struct R2EdgeView indices); +#endif + +#if defined(RAPIER_DIM2) +/** + * Create an owned convex polygon from vertices already ordered along its boundary. Release it with + * r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API R2SharedShape *RAPIER_CALL r2ConvexPolylineSharedShape(struct R2VectorView vertices); +#endif + +/** + * Create an owned round convex hull shape. Release it with r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2RoundConvexHullSharedShape(struct R2VectorView vertices, + R2Real border_radius); + +/** + * Create an owned triangle mesh with the supplied TRIMESH_* processing flags. Release it with + * r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2TrimeshSharedShapeWithFlags(struct R2VectorView vertices, + struct R2TriangleView indices, + uint32_t flags); + +/** + * Create an owned world. Release it with FreeWorld. + * @ingroup worlds + */ +RAPIER_API struct R2World *RAPIER_CALL r2NewWorld(void); + +/** + * Free a world. NULL is allowed. Rejects destruction from an active callback. + * The caller must prevent other threads from starting calls during destruction. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2FreeWorld(struct R2World *world); + +/** + * Compute a velocity correction from callback-visible body state, updating the PID controller + * history. The context is valid only during its callback. + * @ingroup callbacks + */ +RAPIER_API +struct R2VelocityCorrection RAPIER_CALL r2ReadPidController_RigidBodyCorrection(const struct R2ReadContext *context, + struct R2PidController *controller, + R2Real dt, + struct R2RigidBodyHandle body, + struct R2Pose target_pose, + struct R2Vector target_linvel, + R2AngVector target_angvel); + +/** + * Return the number of rigid body objects in the world. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API size_t RAPIER_CALL r2ReadRigidBodyCount(const struct R2ReadContext *context); + +/** + * Copy entity handles. Uses only the callback-scoped read context; never retain the context. + * @see @ref output_buffers + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r2ReadRigidBodyHandles(const struct R2ReadContext *context, + struct R2RigidBodyHandle *buffer, + size_t capacity); + +/** + * Test whether the live world contains this rigid body handle. A removed/stale handle returns + * false. Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_Contains(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the number of collider objects in the world. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API size_t RAPIER_CALL r2ReadColliderCount(const struct R2ReadContext *context); + +/** + * Copy entity handles. Uses only the callback-scoped read context; never retain the context. + * @see @ref output_buffers + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r2ReadColliderHandles(const struct R2ReadContext *context, + struct R2ColliderHandle *buffer, + size_t capacity); + +/** + * Test whether the live world contains this collider handle. A removed/stale handle returns false. + * Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadCollider_Contains(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return a process-local geometry identity for caching, not a serializable ID. Keep a shared-shape + * clone alive while using it as a cache key. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r2ReadCollider_ShapeIdentity(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider local mass properties. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2MassProperties RAPIER_CALL r2ReadCollider_MassProperties(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the rigid body translation/rotation lock bitmask. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint8_t RAPIER_CALL r2ReadRigidBody_LockedAxes(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the collider is a voxel shape. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadCollider_IsVoxels(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return voxel information at a flat index; found = 0 if absent. Uses only the callback-scoped + * read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2VoxelQuery RAPIER_CALL r2ReadCollider_VoxelAtFlatId(const struct R2ReadContext *context, + struct R2ColliderHandle handle, + uint32_t id); + +/** + * Return the rigid body next kinematic world-space pose. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Pose RAPIER_CALL r2ReadRigidBody_NextPosition(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body world-space rotation. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Rotation RAPIER_CALL r2ReadRigidBody_Rotation(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body world-space center of mass. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Vector RAPIER_CALL r2ReadRigidBody_CenterOfMass(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body body-local center of mass. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Vector RAPIER_CALL r2ReadRigidBody_LocalCenterOfMass(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body accumulated user-applied world-space force. Uses only the callback-scoped + * read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Vector RAPIER_CALL r2ReadRigidBody_UserForce(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body accumulated user-applied world-space torque. Uses only the callback-scoped + * read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2AngVector RAPIER_CALL r2ReadRigidBody_UserTorque(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body body type (R2_DYNAMIC, R2_FIXED, or a kinematic kind). Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint32_t RAPIER_CALL r2ReadRigidBody_BodyType(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body mass. Uses only the callback-scoped read context; never retain the + * context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadRigidBody_Mass(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body gravity multiplier. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadRigidBody_GravityScale(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body linear damping coefficient. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadRigidBody_LinearDamping(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body angular damping coefficient. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadRigidBody_AngularDamping(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body kinetic energy. Uses only the callback-scoped read context; never retain + * the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadRigidBody_KineticEnergy(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body soft-CCD prediction distance. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadRigidBody_SoftCcdPrediction(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is using continuous collision detection. Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_IsCcdEnabled(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is dynamic. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_IsDynamic(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the associated soft-body handle, or an invalid handle if this is not a soft proxy. Uses + * only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2SoftBodyHandle RAPIER_CALL r2ReadRigidBody_SoftBody(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is a soft-body proxy. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_IsSoftFrame(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is fixed. Uses only the callback-scoped read context; never retain + * the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_IsFixed(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is kinematic. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_IsKinematic(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is moving. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_IsMoving(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is currently using CCD for its motion. Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_IsCcdActive(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return world-space velocity at a world-space point, including angular motion. Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Vector RAPIER_CALL r2ReadRigidBody_VelocityAtPoint(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle, + struct R2Vector point); + +/** + * Copy attached collider handles. Uses only the callback-scoped read context; never retain the + * context. + * @see @ref output_buffers + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r2ReadRigidBody_Colliders(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle, + struct R2ColliderHandle *buffer, + size_t capacity); + +#if defined(RAPIER_DIM3) +/** + * Return whether the rigid body is using gyroscopic forces. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_GyroscopicForcesEnabled(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); +#endif + +/** + * Return the collider world-space rotation. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Rotation RAPIER_CALL r2ReadCollider_Rotation(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider collision filtering groups. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2InteractionGroups RAPIER_CALL r2ReadCollider_CollisionGroups(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider contact-force filtering groups. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2InteractionGroups RAPIER_CALL r2ReadCollider_SolverGroups(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider application-owned 128-bit user value. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2UserData RAPIER_CALL r2ReadCollider_UserData(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider event-generation bitmask (R2_COLLISION_EVENTS and + * R2_CONTACT_FORCE_EVENTS). Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint32_t RAPIER_CALL r2ReadCollider_ActiveEvents(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider mass. Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadCollider_Mass(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider mass per unit volume. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadCollider_Density(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider current volume. Uses only the callback-scoped read context; never retain the + * context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadCollider_Volume(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider extra separation skin around the shape. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadCollider_ContactSkin(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider force threshold for contact-force events. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadCollider_ContactForceEventThreshold(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return whether the collider is enabled. Uses only the callback-scoped read context; never retain + * the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadCollider_IsEnabled(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the current world-space axis-aligned bounds. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Aabb RAPIER_CALL r2ReadCollider_ComputeAabb(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return an owned wrapper sharing the collider geometry. Release with r2FreeSharedShape. Uses + * only the callback-scoped read context; never retain the context. + * Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. + * @ingroup callbacks + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2ReadCollider_CloneShape(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Validate the index and generation in the live owning world. Cannot detect a world pointer that + * has already been freed. Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Status RAPIER_CALL r2ReadRigidBody_ValidateHandle(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Validate the index and generation in the live owning world. Cannot detect a world pointer that + * has already been freed. Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Status RAPIER_CALL r2ReadCollider_ValidateHandle(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the rigid body world-space pose. Uses only the callback-scoped read context; never retain + * the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Pose RAPIER_CALL r2ReadRigidBody_Position(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body world-space translation. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Vector RAPIER_CALL r2ReadRigidBody_Translation(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body world-space linear velocity. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Vector RAPIER_CALL r2ReadRigidBody_Linvel(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body world-space angular velocity (radians per second). Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2AngVector RAPIER_CALL r2ReadRigidBody_Angvel(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is sleeping. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_IsSleeping(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body is enabled. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_IsEnabled(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body application-owned 128-bit user value. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2UserData RAPIER_CALL r2ReadRigidBody_UserData(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the collider world-space pose. Uses only the callback-scoped read context; never retain + * the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Pose RAPIER_CALL r2ReadCollider_Position(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider world-space translation. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Vector RAPIER_CALL r2ReadCollider_Translation(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider friction coefficient. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadCollider_Friction(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider restitution coefficient. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Real RAPIER_CALL r2ReadCollider_Restitution(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return whether the collider is a sensor (detects overlaps without contact forces). Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadCollider_IsSensor(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Read the parent body handle during a callback; a standalone collider returns an invalid handle + * with OK status. + * @ingroup callbacks + */ +RAPIER_API +struct R2RigidBodyHandle RAPIER_CALL r2ReadCollider_Parent(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Copy callback-visible body states in the supplied handle order. All handles must belong to the + * context world. + * @see @ref output_buffers + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r2ReadRigidBodyReadStates(const struct R2ReadContext *context, + const struct R2RigidBodyHandle *handles, + size_t handle_count, + struct R2RigidBodyState *states, + size_t capacity); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#else /* RAPIER_DIM3 */ + +#if defined(RAPIER_DIM2) +/** + * @ingroup joints + * Number of translational and angular joint axes in this dimension. + */ +#define R3_JOINT_DOF_COUNT 3 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Number of translational and angular joint axes in this dimension. + */ +#define R3_JOINT_DOF_COUNT 6 +#endif + +/** + * @ingroup soft_bodies + * Soft-body selector: desc particles. + */ +#define R3_SOFT_DESC_PARTICLES 0 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc rope. + */ +#define R3_SOFT_DESC_ROPE 1 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc grid. + */ +#define R3_SOFT_DESC_GRID 2 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc cloth. + */ +#define R3_SOFT_DESC_CLOTH 3 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc cuboid. + */ +#define R3_SOFT_DESC_CUBOID 4 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc surface. + */ +#define R3_SOFT_DESC_SURFACE 5 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc disk. + */ +#define R3_SOFT_DESC_DISK 6 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc sphere. + */ +#define R3_SOFT_DESC_SPHERE 7 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc cloth tube. + */ +#define R3_SOFT_DESC_CLOTH_TUBE 8 + +/** + * @ingroup soft_bodies + * Soft-body selector: desc volumetric. + */ +#define R3_SOFT_DESC_VOLUMETRIC 9 + +/** + * @ingroup soft_bodies + * Soft-body selector: binding skinned. + */ +#define R3_SOFT_BINDING_SKINNED 0 + +/** + * @ingroup soft_bodies + * Soft-body selector: binding direct. + */ +#define R3_SOFT_BINDING_DIRECT 1 + +/** + * @ingroup soft_bodies + * Soft-body selector: binding direct by position. + */ +#define R3_SOFT_BINDING_DIRECT_BY_POSITION 2 + +#if defined(RAPIER_DIM2) +/** + * @ingroup shapes + * Treat the 2D polyline as oriented when generating contact normals. + */ +#define R3_POLYLINE_ORIENTED 1 +#endif + +/** + * @ingroup shapes + * Prepare polyline acceleration data for deformation. + */ +#define R3_POLYLINE_DEFORMABLE 2 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a ball. + */ +#define R3_SHAPE_DESC_BALL 0 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a cuboid. + */ +#define R3_SHAPE_DESC_CUBOID 1 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a round cuboid. + */ +#define R3_SHAPE_DESC_ROUND_CUBOID 2 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a capsule. + */ +#define R3_SHAPE_DESC_CAPSULE 3 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a segment. + */ +#define R3_SHAPE_DESC_SEGMENT 4 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a triangle. + */ +#define R3_SHAPE_DESC_TRIANGLE 5 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a half-space. + */ +#define R3_SHAPE_DESC_HALFSPACE 6 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a convex hull. + */ +#define R3_SHAPE_DESC_CONVEX_HULL 7 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a triangle mesh. + */ +#define R3_SHAPE_DESC_TRIMESH 8 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a polyline. + */ +#define R3_SHAPE_DESC_POLYLINE 9 + +/** + * @ingroup shapes + * ShapeDesc kind selecting borrowed shared geometry. + */ +#define R3_SHAPE_DESC_SHARED 10 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a heightfield. + */ +#define R3_SHAPE_DESC_HEIGHTFIELD 11 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a cylinder. + */ +#define R3_SHAPE_DESC_CYLINDER 12 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a cone. + */ +#define R3_SHAPE_DESC_CONE 13 + +/** + * @ingroup shapes + * ShapeDesc kind selecting compound child shapes. + */ +#define R3_SHAPE_DESC_COMPOUND 14 + +/** + * @ingroup shapes + * ShapeDesc kind selecting a round cylinder. + */ +#define R3_SHAPE_DESC_ROUND_CYLINDER 15 + +/** + * @ingroup colliders + * Mass density. + */ +#define R3_MASS_DENSITY 0 + +/** + * @ingroup colliders + * Mass total. + */ +#define R3_MASS_TOTAL 1 + +/** + * @ingroup colliders + * Mass properties. + */ +#define R3_MASS_PROPERTIES 2 + +/** + * @ingroup errors + * C binary ABI revision expected by this header. + */ +#define R3_ABI_VERSION 1 + +/** + * @ingroup rigid_bodies + * Dynamic body affected by forces and contacts. + */ +#define R3_DYNAMIC 0 + +/** + * @ingroup rigid_bodies + * Immovable body. + */ +#define R3_FIXED 1 + +/** + * @ingroup rigid_bodies + * Kinematic body controlled by its next pose. + */ +#define R3_KINEMATIC_POSITION_BASED 2 + +/** + * @ingroup rigid_bodies + * Kinematic body controlled by its velocity. + */ +#define R3_KINEMATIC_VELOCITY_BASED 3 + +/** + * @ingroup colliders + * Enable collision-start and collision-stop events for this collider. + */ +#define R3_COLLISION_EVENTS 1 + +/** + * @ingroup colliders + * Enable contact-force events for this collider, subject to its force threshold. + */ +#define R3_CONTACT_FORCE_EVENTS 2 + +/** + * @ingroup colliders + * Combine the two material coefficients by their arithmetic mean. + */ +#define R3_COMBINE_AVERAGE 0 + +/** + * @ingroup colliders + * Use the smaller of the two material coefficients. + */ +#define R3_COMBINE_MIN 1 + +/** + * @ingroup colliders + * Multiply the two material coefficients. + */ +#define R3_COMBINE_MULTIPLY 2 + +/** + * @ingroup colliders + * Use the larger of the two material coefficients. + */ +#define R3_COMBINE_MAX 3 + +/** + * @ingroup colliders + * Invoke the contact-pair filtering hook for this collider. + */ +#define R3_FILTER_CONTACT_PAIRS 1 + +/** + * @ingroup colliders + * Invoke the sensor-intersection filtering hook for this collider. + */ +#define R3_FILTER_INTERSECTION_PAIR 2 + +/** + * @ingroup colliders + * Invoke the solver-contact modification hook for this collider. + */ +#define R3_MODIFY_SOLVER_CONTACTS 4 + +/** + * @ingroup queries + * Query filter: exclude fixed. + */ +#define R3_QUERY_EXCLUDE_FIXED 1 + +/** + * @ingroup queries + * Query filter: exclude kinematic. + */ +#define R3_QUERY_EXCLUDE_KINEMATIC 2 + +/** + * @ingroup queries + * Query filter: exclude dynamic. + */ +#define R3_QUERY_EXCLUDE_DYNAMIC 4 + +/** + * @ingroup queries + * Query filter: exclude sensors. + */ +#define R3_QUERY_EXCLUDE_SENSORS 8 + +/** + * @ingroup queries + * Query filter: exclude solids. + */ +#define R3_QUERY_EXCLUDE_SOLIDS 16 + +/** + * @ingroup queries + * Query filter: only dynamic. + */ +#define R3_QUERY_ONLY_DYNAMIC 3 + +/** + * @ingroup queries + * Query filter: only kinematic. + */ +#define R3_QUERY_ONLY_KINEMATIC 5 + +/** + * @ingroup queries + * Query filter: only fixed. + */ +#define R3_QUERY_ONLY_FIXED 6 + +/** + * @ingroup events + * Debug-render flag: collider shapes. + */ +#define R3_DEBUG_COLLIDER_SHAPES 1 + +/** + * @ingroup events + * Debug-render flag: rigid body axes. + */ +#define R3_DEBUG_RIGID_BODY_AXES 2 + +/** + * @ingroup events + * Debug-render flag: multibody joints. + */ +#define R3_DEBUG_MULTIBODY_JOINTS 4 + +/** + * @ingroup events + * Debug-render flag: impulse joints. + */ +#define R3_DEBUG_IMPULSE_JOINTS 8 + +/** + * @ingroup events + * Debug-render flag: solver contacts. + */ +#define R3_DEBUG_SOLVER_CONTACTS 16 + +/** + * @ingroup events + * Debug-render flag: contacts. + */ +#define R3_DEBUG_CONTACTS 32 + +/** + * @ingroup events + * Debug-render flag: collider aabbs. + */ +#define R3_DEBUG_COLLIDER_AABBS 64 + +/** + * @ingroup events + * Debug-render flag: soft bodies. + */ +#define R3_DEBUG_SOFT_BODIES 128 + +/** + * @ingroup events + * Debug-render flag: pseudo normals. + */ +#define R3_DEBUG_PSEUDO_NORMALS 256 + +/** + * @ingroup events + * Debug-render flag: soft volume contacts. + */ +#define R3_DEBUG_SOFT_VOLUME_CONTACTS 512 + +/** + * @ingroup events + * Debug-render flag: soft body stress. + */ +#define R3_DEBUG_SOFT_BODY_STRESS 1024 + +/** + * @ingroup colliders + * Require both membership/filter intersections to be nonempty. + */ +#define R3_GROUPS_AND 0 + +/** + * @ingroup colliders + * Accept either membership/filter intersection if both participants select OR; otherwise use AND. + */ +#define R3_GROUPS_OR 1 + +/** + * @ingroup joints + * Motor stiffness and damping are acceleration-based, independent of mass. + */ +#define R3_MOTOR_ACCELERATION_BASED 0 + +/** + * @ingroup joints + * Motor stiffness and damping are force-based, so response depends on mass. + */ +#define R3_MOTOR_FORCE_BASED 1 + +/** + * @ingroup soft_bodies + * Cell model that constrains volume without elastic shear response. + */ +#define R3_SOFT_CELL_VOLUME 0 + +/** + * @ingroup soft_bodies + * Corotational elastic cell model. + */ +#define R3_SOFT_CELL_COROTATIONAL 1 + +/** + * @ingroup soft_bodies + * Neo-Hookean elastic cell model. + */ +#define R3_SOFT_CELL_NEO_HOOKEAN 2 + +/** + * @ingroup soft_bodies + * Use the constraint-based soft-body solver. + */ +#define R3_SOFT_SOLVER_CONSTRAINTS 0 + +/** + * @ingroup soft_bodies + * Use the finite-element solver; requires RAPIER_FEM. + */ +#define R3_SOFT_SOLVER_FEM 1 + +/** + * @ingroup joints + * Joint axis index for translation along local X. + */ +#define R3_AXIS_LIN_X 0 + +/** + * @ingroup joints + * Joint axis index for translation along local Y. + */ +#define R3_AXIS_LIN_Y 1 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: translation x. + */ +#define R3_LOCK_TRANSLATION_X 1 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: translation y. + */ +#define R3_LOCK_TRANSLATION_Y 2 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: translation z. + */ +#define R3_LOCK_TRANSLATION_Z 4 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: rotation x. + */ +#define R3_LOCK_ROTATION_X 8 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: rotation y. + */ +#define R3_LOCK_ROTATION_Y 16 + +/** + * @ingroup rigid_bodies + * Rigid-body lock bit: rotation z. + */ +#define R3_LOCK_ROTATION_Z 32 + +#if defined(RAPIER_DIM2) +/** + * @ingroup joints + * Joint axis index for the first rotation: Z in 2D, local X in 3D. + */ +#define R3_AXIS_ANG_X 2 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Joint axis index for the first rotation: Z in 2D, local X in 3D. + */ +#define R3_AXIS_ANG_X 3 +#endif + +#if defined(RAPIER_DIM2) +/** + * @ingroup joints + * Locked-axis mask for a fixed joint: all translations and rotations. + */ +#define R3_JOINT_FIXED_AXES 7 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Locked-axis mask for a fixed joint: all translations and rotations. + */ +#define R3_JOINT_FIXED_AXES 63 +#endif + +#if defined(RAPIER_DIM2) +/** + * @ingroup joints + * Locked-axis mask for a revolute joint: only the first angular axis is free. + */ +#define R3_JOINT_REVOLUTE_AXES 3 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Locked-axis mask for a revolute joint: only the first angular axis is free. + */ +#define R3_JOINT_REVOLUTE_AXES 55 +#endif + +#if defined(RAPIER_DIM2) +/** + * @ingroup joints + * Locked-axis mask for a prismatic joint: only translation along local X is free. + */ +#define R3_JOINT_PRISMATIC_AXES 6 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Locked-axis mask for a prismatic joint: only translation along local X is free. + */ +#define R3_JOINT_PRISMATIC_AXES 62 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Joint axis index for translation along local Z. + */ +#define R3_AXIS_LIN_Z 2 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Joint axis index for rotation around local Y. + */ +#define R3_AXIS_ANG_Y 4 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Joint axis index for rotation around local Z. + */ +#define R3_AXIS_ANG_Z 5 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup joints + * Locked-axis mask for a spherical joint: translations locked, rotations free. + */ +#define R3_JOINT_SPHERICAL_AXES 7 +#endif + +/** + * @ingroup soft_bodies + * Read-only body type of soft-body cluster proxies; cannot be used to construct a rigid body. + */ +#define R3_SOFT_FRAME 4 + +/** + * @ingroup queries + * Shape-cast iteration limit reached before convergence. + */ +#define R3_SHAPE_CAST_OUT_OF_ITERATIONS 0 + +/** + * @ingroup queries + * Shape cast converged to the reported impact. + */ +#define R3_SHAPE_CAST_CONVERGED 1 + +/** + * @ingroup queries + * Shape-cast numerical solver failed to converge. + */ +#define R3_SHAPE_CAST_FAILED 2 + +/** + * @ingroup queries + * Shapes overlap or are within the target distance at the start of the cast. + */ +#define R3_SHAPE_CAST_PENETRATING 3 + +/** + * @ingroup queries + * The hit feature is unspecified. + */ +#define R3_FEATURE_UNKNOWN 0 + +/** + * @ingroup queries + * The feature ID denotes a vertex. + */ +#define R3_FEATURE_VERTEX 1 + +/** + * @ingroup queries + * The feature ID denotes an edge. + */ +#define R3_FEATURE_EDGE 2 + +/** + * @ingroup queries + * The feature ID denotes a face. + */ +#define R3_FEATURE_FACE 3 + +/** + * @ingroup joints + * Insert reduced-coordinate joints as kinematic articulations. + */ +#define R3_MULTIBODY_JOINTS_ARE_KINEMATIC 1 + +/** + * @ingroup joints + * Disable contacts between colliders of the inserted articulation. + */ +#define R3_MULTIBODY_DISABLE_SELF_CONTACTS 2 + +/** + * @ingroup joints + * Skip joints that would close a loop in the articulation. + */ +#define R3_MULTIBODY_SKIP_LOOP_CLOSURES 4 + +/** + * @ingroup joints + * Do not import joint motors into the articulation. + */ +#define R3_MULTIBODY_SKIP_JOINT_MOTORS 8 + +/** + * @ingroup joints + * Do not import joint limits into the articulation. + */ +#define R3_MULTIBODY_SKIP_JOINT_LIMITS 16 + +/** + * @ingroup joints + * Do not import joint springs into the articulation. + */ +#define R3_MULTIBODY_SKIP_JOINT_SPRINGS 32 + +/** + * @ingroup shapes + * Merge triangle-mesh vertices with identical positions. + */ +#define R3_TRIMESH_MERGE_DUPLICATE_VERTICES 16 + +/** + * @ingroup shapes + * Correct contact normals at internal mesh edges; includes duplicate-vertex merging. + */ +#define R3_TRIMESH_FIX_INTERNAL_EDGES 144 + +/** + * @ingroup shapes + * Prepare triangle-mesh acceleration data for deformation. + */ +#define R3_TRIMESH_DEFORMABLE 256 + +/** + * @ingroup shapes + * Correct internal-edge contacts on both sides of the triangle mesh. + */ +#define R3_TRIMESH_FIX_INTERNAL_EDGES_TWO_SIDED 656 + +/** + * @ingroup shapes + * Correct contact normals at internal heightfield edges. + */ +#define R3_HEIGHTFIELD_FIX_INTERNAL_EDGES 1 + +/** + * Immutable owned byte buffer. Release with the matching FreeBytes function. + * @ingroup worlds + */ +typedef struct R3Bytes R3Bytes; + +/** + * Borrowed native contact context. Valid only during its callback; never retain or free it. + * @ingroup events + */ +typedef struct R3ContactModificationContext R3ContactModificationContext; + +#if defined(RAPIER_DIM3) +/** + * Vehicle controller borrowing its chassis world. Release with the matching Free function. + * @ingroup controllers + */ +typedef struct R3DynamicRayCastVehicleController R3DynamicRayCastVehicleController; +#endif + +/** + * Events accumulate until clear. Copying events never drains them, allowing two-call buffer + * sizing. + * @ingroup events + */ +typedef struct R3EventCollector R3EventCollector; + +/** + * Controller plus reusable collision output from the last move_shape call. + * Kinematic character controller and its last collision list. Release with the matching Free + * function. + * @ingroup controllers + */ +typedef struct R3KinematicCharacterController R3KinematicCharacterController; + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Loaded MJCF robot and its visual/keyframe data. Release with the matching Free function. + * @ingroup robotics + */ +typedef struct R3MjcfRobot R3MjcfRobot; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Owned container of borrowed handles and actuators of an inserted MJCF robot. Release with the + * matching Free function. + * @ingroup robotics + */ +typedef struct R3MjcfRobotHandles R3MjcfRobotHandles; +#endif + +/** + * PID controller with persistent integral state. + * Stateful proportional-integral-derivative controller. Release with the matching Free function. + * @ingroup controllers + */ +typedef struct R3PidController R3PidController; + +/** + * Callback-scoped read access to bodies and colliders. Never retain or free it. + * Only the Read* functions accept this context; it cannot mutate the world. + * @ingroup callbacks + */ +typedef struct R3ReadContext R3ReadContext; + +/** + * Owned tessellated shape: flat triangle vertices and independent line segments, in local space. + * Rounded 3D shapes use their inner surface (as in the Rust testbed). Halfspaces use a finite + * patch. + * @ingroup shapes + */ +typedef struct R3ShapeMesh R3ShapeMesh; + +/** + * Owned copy of a tear event. Read particle remapping before rebuilding render meshes. + * @ingroup events + */ +typedef struct R3SoftBodyTearEvent R3SoftBodyTearEvent; + +#if defined(RAPIER_DIM3) +/** + * Owned indexed geometry from Parry's shape tessellation, preserving its vertex order. + * @ingroup shapes + */ +typedef struct R3TriMeshData R3TriMeshData; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Loaded URDF robot; insertion clones its simulation objects. Release with the matching Free + * function. + * @ingroup robotics + */ +typedef struct R3UrdfRobot R3UrdfRobot; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Owned container of borrowed handles to an inserted URDF robot. Release with the matching Free + * function. + * @ingroup robotics + */ +typedef struct R3UrdfRobotHandles R3UrdfRobotHandles; +#endif + +/** + * Sole owner of simulation state. Handles belong to the world that created them. + * Ordinary reads may overlap. A mutation or step requires exclusive access. + * Destruction must be externally synchronized with all users of this pointer. + * @ingroup worlds + */ +typedef struct R3World R3World; + +#if defined(RAPIER_F32) +/** + * Floating-point scalar: float for f32, double for f64. + * @ingroup math + */ +typedef float R3Real; +#endif + +#if defined(RAPIER_F64) +/** + * Floating-point scalar: float for f32, double for f64. + * @ingroup math + */ +typedef double R3Real; +#endif + +/** + * Spring softness expressed as natural frequency and damping ratio. + * @ingroup math + */ +typedef struct R3SpringCoefficients { + /** + * Nonnegative spring natural frequency in Hz. + */ + R3Real natural_frequency; + /** + * Nonnegative damping ratio; 1 is critical damping. + */ + R3Real damping_ratio; +} R3SpringCoefficients; + +/** + * ABI booleans are uint32_t: zero is false, one is true. + * @ingroup math + */ +typedef uint32_t R3Bool; + +/** + * Optional scalar override; enabled = 0 selects no override. + * @ingroup math + */ +typedef struct R3OptionalReal { + /** + * Whether this setting/object is enabled (0 or 1). + */ + R3Bool enabled; + /** + * Value used when enabled is 1. + */ + R3Real value; +} R3OptionalReal; + +/** + * Optional unsigned integer override; enabled = 0 selects no override. + * @ingroup math + */ +typedef struct R3OptionalU32 { + /** + * Whether this setting/object is enabled (0 or 1). + */ + R3Bool enabled; + /** + * Value used when enabled is 1. + */ + uint32_t value; +} R3OptionalU32; + +/** + * Plain configuration data; initialize defaults, edit, then apply. No destructor. + * @ingroup soft_bodies + */ +typedef struct R3SoftBodyMaterial { + /** + * Spring coefficients for structural edge constraints. + */ + struct R3SpringCoefficients edgeSoftness; + /** + * Spring coefficients for bending edges and dihedrals. + */ + struct R3SpringCoefficients bendSoftness; + /** + * Spring coefficients for cell and global volume constraints. + */ + struct R3SpringCoefficients volumeSoftness; + /** + * Spring coefficients for shape-matching constraints. + */ + struct R3SpringCoefficients shapeMatchingSoftness; + /** + * Elastic modulus: force per area in 3D, force per length in 2D; nonnegative. + */ + R3Real youngModulus; + /** + * Poisson ratio for elastic cells, in [0, 0.5). + */ + R3Real poissonRatio; + /** + * Nonnegative damping ratio of elastic cells. + */ + R3Real elasticDampingRatio; + /** + * Cell strain threshold for plastic flow; zero disables plasticity. + */ + R3Real plasticYield; + /** + * Nonnegative rate per second at which excess cell strain becomes permanent. + */ + R3Real plasticCreep; + /** + * Maximum accumulated cell plastic stretch, measured by the norm of P - I. + */ + R3Real plasticMax; + /** + * Rate per second pulling particle velocities toward best-fit rigid motion; zero disables it. + */ + R3Real deformationDamping; + /** + * Edge strain threshold for plastic flow; zero disables plasticity. + */ + R3Real edgePlasticYield; + /** + * Nonnegative rate per second at which excess edge strain becomes permanent. + */ + R3Real edgePlasticCreep; + /** + * Maximum permanent edge-length change as a fraction of its initial length. + */ + R3Real edgePlasticMax; + /** + * Plastic flow direction: 0 both, 1 compression only, 2 tension only. + */ + uint32_t edgePlasticFlow; + /** + * Optional strain threshold for tearing; disabled means no strain-based tearing. + */ + struct R3OptionalReal tearStrain; + /** + * Optional tensile edge-force threshold for tearing. + */ + struct R3OptionalReal tearForce; + /** + * Exponential load-smoothing time constant in seconds; zero disables smoothing. + */ + R3Real tearSmoothing; + /** + * Tear-threshold multiplier for undamaged interior elements. + */ + R3Real interiorStrength; + /** + * Maximum ordinary edge tears per step; edges above twice their threshold bypass the limit. + */ + uint32_t maxTearsPerStep; + /** + * Optional minimum particle count of tear pieces. + */ + struct R3OptionalU32 minPiece; +} R3SoftBodyMaterial; + +/** + * Plain configuration data; initialize defaults, edit, then apply. No destructor. + * @ingroup soft_bodies + */ +typedef struct R3SoftRecoverySettings { + /** + * Expand speculative contact margins to cover particle velocities set between steps. + */ + R3Bool authoredVelocityMargin; + /** + * Enable speculative edge-edge collision constraints. + */ + R3Bool edgeSpeculation; + /** + * Detect inverted cells to support self-contact recovery. + */ + R3Bool invertedCellDetection; + /** + * Detect surface self-crossings each step. + */ + R3Bool selfCrossingDetection; + /** + * Skip self-crossing detection when accumulated motion cannot have created a crossing. + */ + R3Bool detectionMotionGating; + /** + * Detect boundary crossings between soft bodies. + */ + R3Bool crossBodyDetection; + /** + * Disable contacts on tangled features so elasticity can untangle them. + */ + R3Bool selfStandDown; + /** + * Allow contacts at cross-body crossings to expel, but not hold, the intruder. + */ + R3Bool crossBodyExpelGate; + /** + * Disable edge constraints touching cross-body crossings. + */ + R3Bool edgeStandDown; + /** + * Repel crossing features toward their neighborhood's side of the surface. + */ + R3Bool crossingRepulsion; + /** + * Guide cross-body repulsion by overlap-volume normals; closed meshes only. + */ + R3Bool crossingRepulsionGuide; + /** + * Guide self-crossing repulsion by self-intersection-volume normals; closed meshes only. + */ + R3Bool crossingRepulsionSelfGuide; + /** + * Maximum recovery rate in length units per second, scaled by lengthUnit. + */ + R3Real recoveryPace; + /** + * Enable intersection-volume constraints for overlapping closed surfaces. + */ + R3Bool overlapConstraints; + /** + * Enable intersection-volume constraints against rigid colliders. + */ + R3Bool overlapRigid; + /** + * Skip pair overlap constraints for self-crossed meshes. + */ + R3Bool overlapSkipSelfTangled; + /** + * Disable 3D closed-surface edge constraints where overlap constraints take over. + */ + R3Bool overlapEdgeStandDown; + /** + * Velocity-change limit per step, as a multiple of recoveryPace. + */ + R3Real overlapConstraintPace; + /** + * Per-point constraints inside overlap patches: 0 keep, 1 stand down, 2 align with overlap + * normal. + */ + uint32_t overlapPatchConstraints; + /** + * Measure overlap on contact-skin surfaces rather than bare geometry. + */ + R3Bool overlapSkinVolume; + /** + * Overlap depth retained by recovery, as a fraction of the pair's contact skins. + */ + R3Real overlapKeptDepth; + /** + * Enable recovery of self-intersection regions. + */ + R3Bool overlapSelfRegions; + /** + * Use the overlap normal for recovery pushes. + */ + R3Bool overlapNormalPush; + /** + * Use spatially split overlap-volume constraints. + */ + R3Bool overlapMultiVolume; + /** + * Cells per tangent axis of the multi-volume grid. + */ + uint32_t overlapSplit; + /** + * Recovery progress patience in steps. + */ + uint32_t overlapPatience; + /** + * Relative overlap-volume decrease that counts as recovery progress. + */ + R3Real overlapProgressMargin; +} R3SoftRecoverySettings; + +#if defined(RAPIER_FEM) +/** + * Plain configuration data; initialize defaults, edit, then apply. No destructor. + * @ingroup soft_bodies + */ +typedef struct R3SoftFemParameters { + /** + * FEM iterative linear-solver tolerance. + */ + R3Real linearTolerance; + /** + * Maximum FEM linear-solver iterations. + */ + size_t maxLinearIterations; + /** + * Maximum degrees of freedom solved by the dense FEM solver. + */ + size_t maxDenseDofs; +} R3SoftFemParameters; +#endif + +/** + * Plain configuration data; initialize defaults, edit, then apply. No destructor. + * @ingroup soft_bodies + */ +typedef struct R3SoftBodiesSettings { + /** + * Soft-body crossing detection and recovery settings. + */ + struct R3SoftRecoverySettings recovery; + /** + * Strain threshold for re-solving soft constraints after contacts within a substep. + */ + R3Real resweepStrain; + /** + * Maximum additional substeps requested by soft-body motion. + */ + size_t maxExtraSubsteps; + /** + * Multiplier on contact natural frequency for soft-body contacts. + */ + R3Real contactStiffening; +#if defined(RAPIER_FEM) + /** + * FEM linear-solver settings, present only when RAPIER_FEM is enabled. + */ + struct R3SoftFemParameters fem; +#endif +} R3SoftBodiesSettings; + +/** + * Plain configuration data; initialize defaults, edit, then apply. No destructor. + * @ingroup worlds + */ +typedef struct R3IntegrationParameters { + /** + * Simulation step duration in seconds. + */ + R3Real dt; + /** + * Minimum CCD substep duration in seconds. + */ + R3Real minCcdDt; + /** + * Spring coefficients for dynamic contact constraints. + */ + struct R3SpringCoefficients contactSoftness; + /** + * Spring coefficients for contacts against fixed bodies. + */ + struct R3SpringCoefficients staticContactSoftness; + /** + * Scale applied to cached impulses when warmstarting. + */ + R3Real warmstartCoefficient; + /** + * Typical world-space length of one meter; scales solver tolerances, not geometry. + */ + R3Real lengthUnit; + /** + * Soft-body integration and recovery settings. + */ + struct R3SoftBodiesSettings softBodies; + /** + * Allowed penetration divided by lengthUnit. + */ + R3Real normalizedAllowedLinearError; + /** + * Maximum penetration-correction speed divided by lengthUnit. + */ + R3Real normalizedMaxCorrectiveVelocity; + /** + * Speculative-contact distance divided by lengthUnit. + */ + R3Real normalizedPredictionDistance; + /** + * Maximum linear speed divided by lengthUnit. + */ + R3Real normalizedMaxLinearVelocity; + /** + * Number of solver substeps/iterations; must be positive. + */ + size_t numSolverIterations; + /** + * PGS iterations per solver substep. + */ + size_t numInternalPgsIterations; + /** + * Stabilization iterations after velocity solving. + */ + size_t numInternalStabilizationIterations; + /** + * Maximum CCD substeps; 0 disables all CCD for the world. + */ + size_t maxCcdSubsteps; + /** + * Whether to cluster contacts for solving. + */ + R3Bool contactClustering; + /** + * Whether to reuse nearby contacts between steps. + */ + R3Bool contactRecycling; + /** + * Contact recycling distance divided by lengthUnit. + */ + R3Real normalizedContactRecycleDistance; + /** + * Whether to solve friction in the bias pass. + */ + R3Bool frictionInBiasPass; + /** + * Whether to warmstart joint constraints. + */ + R3Bool warmstartJoints; +#if defined(RAPIER_DIM3) + /** + * Friction model: 0 simplified, 1 Coulomb (3D only). + */ + uint32_t frictionModel; +#endif +} R3IntegrationParameters; + +/** + * Status-returning operations use these integer codes. + * @ingroup errors + */ +typedef uint32_t R3Status; + +/** + * Cartesian vector with two or three components. + * @ingroup math + */ +typedef struct R3Vector { + /** + * X component. + */ + R3Real x; + /** + * Y component. + */ + R3Real y; +#if defined(RAPIER_DIM3) + /** + * Z component. + */ + R3Real z; +#endif +} R3Vector; + +/** + * 2D: angle in radians. 3D: unit quaternion in x,y,z,w order (normalized on input). + * @ingroup math + */ +typedef struct R3Rotation { +#if defined(RAPIER_DIM2) + /** + * Rotation angle in radians. + */ + R3Real angle; +#endif +#if defined(RAPIER_DIM3) + /** + * X component. + */ + R3Real x; +#endif +#if defined(RAPIER_DIM3) + /** + * Y component. + */ + R3Real y; +#endif +#if defined(RAPIER_DIM3) + /** + * Z component. + */ + R3Real z; +#endif +#if defined(RAPIER_DIM3) + /** + * Quaternion scalar component. + */ + R3Real w; +#endif +} R3Rotation; + +/** + * Rigid transform combining translation and rotation. Use TranslationPose with a zero vector for + * identity. + * @ingroup math + */ +typedef struct R3Pose { + /** + * Translation vector. + */ + struct R3Vector translation; + /** + * Rotation value. + */ + struct R3Rotation rotation; +} R3Pose; + +/** + * Lower and upper axis limits in length units or radians. + * @ingroup joints + */ +typedef struct R3JointLimits { + /** + * Minimum allowed axis displacement (length or radians). + */ + R3Real min; + /** + * Maximum allowed axis displacement (length or radians). + */ + R3Real max; +} R3JointLimits; + +/** + * Position/velocity motor settings for one joint axis. + * @ingroup joints + */ +typedef struct R3JointMotor { + /** + * Motor target velocity (length per second or radians per second). + */ + R3Real targetVel; + /** + * Motor target position (length or radians). + */ + R3Real targetPos; + /** + * Nonnegative motor spring stiffness. + */ + R3Real stiffness; + /** + * Nonnegative motor damping. + */ + R3Real damping; + /** + * Nonnegative maximum force or torque. + */ + R3Real maxForce; + /** + * Motor model: 0 acceleration-based, 1 force-based. + */ + uint32_t model; +} R3JointMotor; + +/** + * Application-defined 128-bit value. Contains no owned pointers. + * @ingroup math + */ +typedef struct R3UserData { + /** + * Low 64 bits of the application value. + */ + uint64_t low; + /** + * High 64 bits of the application value. + */ + uint64_t high; +} R3UserData; + +/** + * Copyable joint configuration. Limits/motors take effect when their axis mask is enabled. + * Solver impulses are deliberately excluded. Applying data resets cached limit and motor impulses. + * @ingroup joints + */ +typedef struct R3JointDesc { + /** + * Joint frame in body 1 local coordinates. + */ + struct R3Pose localFrame1; + /** + * Joint frame in body 2 local coordinates. + */ + struct R3Pose localFrame2; + /** + * Locked joint degrees of freedom; translations precede rotations. + */ + uint8_t lockedAxes; + /** + * Axis mask enabling corresponding limits entries. + */ + uint8_t limitAxes; + /** + * Axis mask enabling corresponding motors entries. + */ + uint8_t motorAxes; + /** + * Axis mask sharing a coupled constraint. + */ + uint8_t coupledAxes; + /** + * Axis limits in translation-then-rotation order. + */ + struct R3JointLimits limits[R3_JOINT_DOF_COUNT]; + /** + * Axis motors in translation-then-rotation order. + */ + struct R3JointMotor motors[R3_JOINT_DOF_COUNT]; + /** + * Spring coefficients for constraint correction. + */ + struct R3SpringCoefficients softness; + /** + * Whether connected bodies may collide. + */ + R3Bool contactsEnabled; + /** + * Whether this setting/object is enabled (0 or 1). + */ + R3Bool enabled; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + struct R3UserData userData; +} R3JointDesc; + +/** + * Copyable non-owning handle: world pointer plus entity index and generation. + * The world must remain alive throughout every use. Copying does not retain it. + * UINT32_MAX/UINT32_MAX with a NULL world is invalid. + * @ingroup joints + */ +typedef struct R3ImpulseJointHandle { + /** + * Borrowed owning world. Never use this handle after freeing that world. + */ + struct R3World *world; + /** + * Zero-based element index. + */ + uint32_t index; + /** + * Slot generation used to reject stale handles. Do not modify it. + */ + uint32_t generation; +} R3ImpulseJointHandle; + +/** + * Copyable non-owning handle: world pointer plus entity index and generation. + * The world must remain alive throughout every use. Copying does not retain it. + * UINT32_MAX/UINT32_MAX with a NULL world is invalid. + * @ingroup rigid_bodies + */ +typedef struct R3RigidBodyHandle { + /** + * Borrowed owning world. Never use this handle after freeing that world. + */ + struct R3World *world; + /** + * Slot index; UINT32_MAX denotes the explicit invalid handle. + */ + uint32_t index; + /** + * Slot generation used to reject stale handles. Do not modify it. + */ + uint32_t generation; +} R3RigidBodyHandle; + +/** + * Copyable non-owning handle: world pointer plus entity index and generation. + * The world must remain alive throughout every use. Copying does not retain it. + * UINT32_MAX/UINT32_MAX with a NULL world is invalid. + * @ingroup joints + */ +typedef struct R3MultibodyJointHandle { + /** + * Borrowed owning world. Never use this handle after freeing that world. + */ + struct R3World *world; + /** + * Zero-based element index. + */ + uint32_t index; + /** + * Slot generation used to reject stale handles. Do not modify it. + */ + uint32_t generation; +} R3MultibodyJointHandle; + +/** + * Parameters for the native volumetric mesher. Enclosure: 0 cover, 1 crust (3D). + * @ingroup shapes + */ +typedef struct R3VolumeMeshParameters { + /** + * Positive target cell size for volume meshing. + */ + R3Real cell_size; +#if defined(RAPIER_DIM2) + /** + * Minimum target element angle in radians; above pi/6, refinement may stop early. + */ + R3Real min_angle; +#endif +#if defined(RAPIER_DIM3) + /** + * Enclosure strategy: 0 covers the whole shape, 1 encloses only its surface. + */ + uint32_t enclosure; +#endif +#if defined(RAPIER_DIM3) + /** + * Number of cover-smoothing passes. + */ + uint32_t cover_smoothing; +#endif +#if defined(RAPIER_DIM3) + /** + * Minimum smoothed-cover distance from the shape, as a fraction of local cell size. + */ + R3Real cover_guard; +#endif +#if defined(RAPIER_DIM3) + /** + * Maximum boundary-cell halvings below cell_size before cover smoothing. + */ + uint32_t cover_subdivisions; +#endif +} R3VolumeMeshParameters; + +/** + * Borrowed array of vector elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R3VectorView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R3Vector *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3VectorView; + +/** + * Borrowed array of real elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R3RealView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const R3Real *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3RealView; + +/** + * Borrowed array of index elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R3IndexView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const uint32_t *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3IndexView; + +/** + * Vertex indices for one edge; contiguous u32 fields with no padding. + * @ingroup math + */ +typedef struct R3Edge { + /** + * First vertex index. + */ + uint32_t a; + /** + * Second vertex index. + */ + uint32_t b; +} R3Edge; + +/** + * Borrowed array of edge elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R3EdgeView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R3Edge *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3EdgeView; + +/** + * Spring-coefficient override for one soft-body edge. + * @ingroup soft_bodies + */ +typedef struct R3SoftEdgeSoftness { + /** + * Zero-based edge index. + */ + uint32_t edge; + /** + * Spring coefficients for constraint correction. + */ + struct R3SpringCoefficients softness; +} R3SoftEdgeSoftness; + +/** + * Borrowed elements; count counts elements. Data must remain live through insertion. + * @ingroup soft_bodies + */ +typedef struct R3SoftEdgeSoftnessView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R3SoftEdgeSoftness *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3SoftEdgeSoftnessView; + +/** + * Tear-resistance override for one soft-body edge. + * @ingroup soft_bodies + */ +typedef struct R3SoftEdgeTear { + /** + * Zero-based edge index. + */ + uint32_t edge; + /** + * Nonnegative edge tear-resistance multiplier. + */ + R3Real resistance; +} R3SoftEdgeTear; + +/** + * Borrowed elements; count counts elements. Data must remain live through insertion. + * @ingroup soft_bodies + */ +typedef struct R3SoftEdgeTearView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R3SoftEdgeTear *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3SoftEdgeTearView; + +/** + * Vertex indices for one triangle; contiguous u32 fields with no padding. + * @ingroup math + */ +typedef struct R3Triangle { + /** + * First vertex index. + */ + uint32_t a; + /** + * Second vertex index. + */ + uint32_t b; + /** + * Third vertex index. + */ + uint32_t c; +} R3Triangle; + +/** + * Borrowed array of triangle elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R3TriangleView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R3Triangle *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3TriangleView; + +/** + * Vertex indices for one tetrahedron; contiguous u32 fields with no padding. + * @ingroup math + */ +typedef struct R3Tetrahedron { + /** + * First vertex index. + */ + uint32_t a; + /** + * Second vertex index. + */ + uint32_t b; + /** + * Third vertex index. + */ + uint32_t c; + /** + * Fourth vertex index. + */ + uint32_t d; +} R3Tetrahedron; + +/** + * Borrowed array of tetrahedron elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R3TetrahedronView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R3Tetrahedron *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3TetrahedronView; + +#if defined(RAPIER_DIM2) +/** + * Borrowed cell array: triangles in 2D, tetrahedra in 3D. + * @ingroup math + */ +typedef struct R3TriangleView R3CellView; +#endif + +#if defined(RAPIER_DIM3) +/** + * Borrowed cell array: triangles in 2D, tetrahedra in 3D. + * @ingroup math + */ +typedef struct R3TetrahedronView R3CellView; +#endif + +#if defined(RAPIER_DIM2) +/** + * Borrowed surface array: edges in 2D, triangles in 3D. + * @ingroup math + */ +typedef struct R3EdgeView R3SurfaceElementView; +#endif + +#if defined(RAPIER_DIM3) +/** + * Borrowed surface array: edges in 2D, triangles in 3D. + * @ingroup math + */ +typedef struct R3TriangleView R3SurfaceElementView; +#endif + +/** + * Vertex indices for one dihedral; contiguous u32 fields with no padding. + * @ingroup math + */ +typedef struct R3Dihedral { + /** + * First vertex index. + */ + uint32_t a; + /** + * Second vertex index. + */ + uint32_t b; + /** + * Third vertex index. + */ + uint32_t c; + /** + * Fourth vertex index. + */ + uint32_t d; +} R3Dihedral; + +/** + * Borrowed array of dihedral elements. count counts elements of the declared type. + * Copying this view does not copy its data or extend its lifetime. No Free is needed. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup math + */ +typedef struct R3DihedralView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R3Dihedral *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3DihedralView; + +/** + * Optional boolean override. When disabled, retain the recipe's native default. + * @ingroup math + */ +typedef struct R3OptionalBool { + /** + * Whether this setting/object is enabled (0 or 1). + */ + R3Bool enabled; + /** + * Value used when enabled is 1. + */ + R3Bool value; +} R3OptionalBool; + +/** + * Opaque SharedShape. See the ownership and borrowing contract in README.md. + * @ingroup shapes + */ +typedef struct R3SharedShape R3SharedShape; + + +/** + * Borrowed elements; count counts elements. Data must remain live through insertion. + * @ingroup shapes + */ +typedef struct R3CompoundShapeView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R3CompoundShapeDesc *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3CompoundShapeView; + +/** + * Non-owning shape description. Only fields selected by kind are read. + * a = cuboid half extents, capsule/segment endpoint, triangle vertex, or halfspace normal. + * b/c = remaining endpoints/vertices. radius is also the rounded-cuboid border radius. + * Mesh views count edges or triangles; heightfields are column-major. + * Arrays, compound children, and sharedShape remain borrowed until build/insert returns. + * @ingroup shapes + */ +typedef struct R3ShapeDesc { + /** + * Discriminant selecting which description fields are read. + */ + uint32_t kind; + /** + * Half extents, endpoint/vertex, or halfspace normal, selected by kind. + */ + struct R3Vector a; + /** + * Second endpoint or triangle vertex, selected by kind. + */ + struct R3Vector b; + /** + * Third triangle vertex. + */ + struct R3Vector c; + /** + * Radius for the selected primitive or recipe. + */ + R3Real radius; + /** + * Half the height of a cylinder or cone. + */ + R3Real halfHeight; + /** + * Rounding radius for a rounded shape. + */ + R3Real borderRadius; + /** + * Borrowed vertex positions. + */ + struct R3VectorView vertices; + /** + * Borrowed triangle topology; count is triangles. + */ + struct R3TriangleView triangles; + /** + * Borrowed edge topology; count is edges. + */ + struct R3EdgeView edges; + /** + * TRIMESH_*, POLYLINE_*, or HEIGHTFIELD_* bitmask selected by kind. + */ + uint32_t flags; + /** + * Borrowed height samples; 3D uses column-major rows * columns samples. + */ + struct R3RealView heights; + /** + * Number of heightfield rows. + */ + size_t rows; + /** + * Number of heightfield columns. + */ + size_t columns; + /** + * Shape scale along each axis. + */ + struct R3Vector scale; + /** + * Borrowed shared geometry; keep its wrapper alive through build/insert. + */ + const R3SharedShape *sharedShape; + /** + * Borrowed compound children and their nested geometry views. + */ + struct R3CompoundShapeView children; +} R3ShapeDesc; + +/** + * One compound child with a local pose and borrowed geometry description. + * @ingroup shapes + */ +typedef struct R3CompoundShapeDesc { + /** + * Pose relative to the compound parent. + */ + struct R3Pose pose; + /** + * Non-owning shape description; build/insert consumes its views synchronously. + */ + struct R3ShapeDesc shape; +} R3CompoundShapeDesc; + + +#if defined(RAPIER_DIM2) +/** + * Angular scalar in 2D or vector in 3D, in radians for angular displacement. + * @ingroup math + */ +typedef R3Real R3AngVector; +#endif + +#if defined(RAPIER_DIM3) +/** + * Angular scalar in 2D or vector in 3D, in radians for angular displacement. + * @ingroup math + */ +typedef struct R3Vector R3AngVector; +#endif + +/** + * Explicit mass and principal inertia, matching MassProperties constructors. Zero mass/inertia + * means infinite. + * @ingroup math + */ +typedef struct R3MassProperties { + /** + * Center of mass in local coordinates. + */ + struct R3Vector local_com; + /** + * Mass; nonnegative when supplied as input. + */ + R3Real mass; + /** + * Principal angular inertia; scalar in 2D, three diagonal entries in 3D. + */ + R3AngVector principal_inertia; +#if defined(RAPIER_DIM3) + /** + * Orientation of principal inertia axes in local coordinates (3D). + */ + struct R3Rotation principal_inertia_local_frame; +#endif +} R3MassProperties; + +/** + * Collision membership/filter masks and their pairwise combination rule. + * @ingroup math + */ +typedef struct R3InteractionGroups { + /** + * Groups this object belongs to, as a 32-bit mask. + */ + uint32_t memberships; + /** + * Membership groups accepted by this object, as a 32-bit mask. + */ + uint32_t filter; + /** + * 0 requires both membership/filter tests; 1 accepts either test. + */ + uint32_t test_mode; +} R3InteractionGroups; + +/** + * Copyable collider construction data. Shape inputs are borrowed, never owned. + * @ingroup colliders + */ +typedef struct R3ColliderDesc { + /** + * Non-owning shape description; build/insert consumes its views synchronously. + */ + struct R3ShapeDesc shape; + /** + * Pose relative to the parent body; world-space for an unparented collider. + */ + struct R3Pose position; + /** + * R3_MASS_DENSITY, R3_MASS_TOTAL, or R3_MASS_PROPERTIES. + */ + uint32_t massMode; + /** + * Nonnegative mass per unit volume. + */ + R3Real density; + /** + * Mass; nonnegative when supplied as input. + */ + R3Real mass; + /** + * Explicit local mass and inertia when massMode selects them. + */ + struct R3MassProperties massProperties; + /** + * Nonnegative friction coefficient. + */ + R3Real friction; + /** + * Nonnegative restitution coefficient. + */ + R3Real restitution; + /** + * R3_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + */ + uint32_t frictionCombineRule; + /** + * R3_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + */ + uint32_t restitutionCombineRule; + /** + * 1 detects intersections without generating contact forces. + */ + R3Bool isSensor; + /** + * Whether this setting/object is enabled (0 or 1). + */ + R3Bool enabled; + /** + * Groups controlling collision detection. + */ + struct R3InteractionGroups collisionGroups; + /** + * Groups controlling contact-force solving. + */ + struct R3InteractionGroups solverGroups; + /** + * Bitmask of body-type pairs allowed to collide. + */ + uint16_t activeCollisionTypes; + /** + * Hook flags enabling pair filtering/contact modification. + */ + uint32_t activeHooks; + /** + * R3_COLLISION_EVENTS and/or R3_CONTACT_FORCE_EVENTS. + */ + uint32_t activeEvents; + /** + * Nonnegative force threshold for force events. + */ + R3Real contactForceEventThreshold; + /** + * Nonnegative extra separation distance around the collider. + */ + R3Real contactSkin; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + struct R3UserData userData; +} R3ColliderDesc; + +/** + * Copyable recipe, not an owned procedural builder. Initialize before editing. + * All array views borrow caller data until build/insert returns; counts + * for topology arrays are element counts (edges, triangles, or tetrahedra). + * Nonempty topology overrides the generator's topology. Zero counts retain it. + * Generator inputs: a/b are rope ends or center/half-extents; cloth uses a/du/dv. + * @ingroup soft_bodies + */ +typedef struct R3SoftBodyDesc { + /** + * Discriminant selecting which description fields are read. + */ + uint32_t kind; + /** + * Recipe origin/center or first rope endpoint; use a recipe constructor. + */ + struct R3Vector a; + /** + * Recipe half extents, second rope endpoint, or tube axis; use a recipe constructor. + */ + struct R3Vector b; + /** + * Cloth basis step along its first parameter axis. + */ + struct R3Vector du; + /** + * Cloth basis step along its second parameter axis. + */ + struct R3Vector dv; + /** + * First recipe resolution; interpretation depends on kind. + */ + size_t nx; + /** + * Second recipe resolution; interpretation depends on kind. + */ + size_t ny; + /** + * Third recipe resolution; interpretation depends on kind. + */ + size_t nz; + /** + * Radius for the selected primitive or recipe. + */ + R3Real radius; + /** + * Radius at the second end of a tapered cloth tube. + */ + R3Real radiusEnd; + /** + * Translation vector. + */ + struct R3Vector translation; + /** + * Optional total mass override for generated particles. + */ + struct R3OptionalReal totalMass; + /** + * Volume-meshing parameters for a volumetric recipe. + */ + struct R3VolumeMeshParameters meshing; + /** + * Borrowed initial particle positions. + */ + struct R3VectorView positions; + /** + * Borrowed per-particle masses; when empty, particleMass is used. + */ + struct R3RealView masses; + /** + * Borrowed indices of pinned particles. + */ + struct R3IndexView pinned; + /** + * Borrowed edge topology; count is edges. + */ + struct R3EdgeView edges; + /** + * Borrowed bending edge constraints. + */ + struct R3EdgeView bendEdges; + /** + * Borrowed indices of edges that resist tension only. + */ + struct R3IndexView tensionOnlyEdges; + /** + * Borrowed per-edge softness overrides. + */ + struct R3SoftEdgeSoftnessView edgeSoftness; + /** + * Borrowed per-edge tear-resistance overrides. + */ + struct R3SoftEdgeTearView edgeTearResistance; + /** + * Borrowed triangles in 2D or tetrahedra in 3D. + */ + R3CellView cells; + /** + * Borrowed boundary edges in 2D or triangles in 3D. + */ + R3SurfaceElementView surface; +#if defined(RAPIER_DIM3) + /** + * Borrowed four-vertex bending constraints. + */ + struct R3DihedralView dihedrals; +#endif +#if defined(RAPIER_DIM3) + /** + * Borrowed wire edges for a surface recipe. + */ + struct R3EdgeView wire; +#endif + /** + * Borrowed skin vertex positions. + */ + struct R3VectorView skinVertices; + /** + * Borrowed skin topology. + */ + R3SurfaceElementView skinIndices; + /** + * Soft-body material coefficients. + */ + struct R3SoftBodyMaterial material; + /** + * R3_SOFT_CELL_VOLUME, R3_SOFT_CELL_COROTATIONAL, or R3_SOFT_CELL_NEO_HOOKEAN. + */ + uint32_t cellModel; + /** + * 0 = constraints, 1 = FEM (requires a library built with FEM). + */ + uint32_t solver; + /** + * Default nonnegative particle mass. + */ + R3Real particleMass; + /** + * Disabled by default: retain the radius computed by the generator. + */ + struct R3OptionalReal particleRadius; + /** + * Whether to preserve volume. + */ + R3Bool volumePreservation; + /** + * Target volume multiplier. + */ + R3Real volumeFactor; + /** + * Optional shape-matching override; disabled retains recipe defaults. + */ + struct R3OptionalBool shapeMatching; + /** + * Whether self-collision is enabled. + */ + R3Bool selfContacts; + /** + * Whether skin elements participate in collision detection. + */ + R3Bool skinCollision; + /** + * Whether the generated collision geometry is enabled. + */ + R3Bool collisionEnabled; + /** + * Collider configuration used by the recipe; shape comes from the generated geometry. + */ + struct R3ColliderDesc collider; + /** + * Nonnegative linear damping coefficient. + */ + R3Real linearDamping; + /** + * Multiplier applied to world gravity. + */ + R3Real gravityScale; + /** + * Extra solver iterations for this body and connected bodies. + */ + size_t additionalSolverIterations; + /** + * Extra PGS iterations for this body. + */ + size_t additionalPgsIterations; + /** + * Whether automatic sleeping is allowed. + */ + R3Bool canSleep; + /** + * Signed dominance group; larger groups dominate smaller groups. + */ + int8_t dominanceGroup; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + struct R3UserData userData; +} R3SoftBodyDesc; + +/** + * Copyable non-owning handle: world pointer plus entity index and generation. + * The world must remain alive throughout every use. Copying does not retain it. + * UINT32_MAX/UINT32_MAX with a NULL world is invalid. + * @ingroup soft_bodies + */ +typedef struct R3SoftBodyHandle { + /** + * Borrowed owning world. Never use this handle after freeing that world. + */ + struct R3World *world; + /** + * Zero-based element index. + */ + uint32_t index; + /** + * Slot generation used to reject stale handles. Do not modify it. + */ + uint32_t generation; +} R3SoftBodyHandle; + +/** + * Non-owning deformable binding description. Direct particle indices are borrowed. + * @ingroup soft_bodies + */ +typedef struct R3SoftMeshBindingDesc { + /** + * Discriminant selecting which description fields are read. + */ + uint32_t kind; + /** + * Borrowed particle indices used for a direct mesh binding. + */ + struct R3IndexView particles; + /** + * Nonnegative positional tolerance for direct-by-position binding. + */ + R3Real epsilon; + /** + * Whether self-collision is enabled. + */ + R3Bool selfContacts; +} R3SoftMeshBindingDesc; + +/** + * Copyable non-owning handle: world pointer plus entity index and generation. + * The world must remain alive throughout every use. Copying does not retain it. + * UINT32_MAX/UINT32_MAX with a NULL world is invalid. + * @ingroup colliders + */ +typedef struct R3ColliderHandle { + /** + * Borrowed owning world. Never use this handle after freeing that world. + */ + struct R3World *world; + /** + * Slot index; UINT32_MAX denotes the explicit invalid handle. + */ + uint32_t index; + /** + * Slot generation used to reject stale handles. Do not modify it. + */ + uint32_t generation; +} R3ColliderHandle; + +/** + * Scene-query flags, groups, and excluded handles. Initialize with r3DefaultQueryFilter. + * @ingroup queries + */ +typedef struct R3QueryFilter { + /** + * R3_QUERY_EXCLUDE_* bitmask selecting body types and sensors/solids. + */ + uint32_t flags; + /** + * Whether to apply the groups filter. + */ + R3Bool use_groups; + /** + * Groups to test when use_groups is 1. + */ + struct R3InteractionGroups groups; + /** + * Collider to exclude; use the explicit invalid handle to exclude none. + */ + struct R3ColliderHandle exclude_collider; + /** + * Body whose colliders are excluded; use the explicit invalid handle for none. + */ + struct R3RigidBodyHandle exclude_rigid_body; +} R3QueryFilter; + +/** + * Called with scoped read access and a collider handle. Shared queries may nest; + * world mutations are rejected until the outer query returns. Never retain the context. + * @ingroup queries + */ +typedef R3Bool (RAPIER_CALL *R3QueryPredicate)(void *user_data, + const struct R3ReadContext *read, + struct R3ColliderHandle handle); + +/** + * Copyable query settings. They borrow callback data, never world components. + * @ingroup queries + */ +typedef struct R3QueryOptions { + /** + * Query selection settings. + */ + struct R3QueryFilter filter; + /** + * Optional additional query filter; nonzero accepts a collider. + */ + R3QueryPredicate predicate; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + void *userData; +} R3QueryOptions; + +/** + * Closest ray intersection, with a world-space normal. + * @ingroup queries + */ +typedef struct R3RayHit { + /** + * World-bound collider handle. + */ + struct R3ColliderHandle collider; + /** + * Ray/sweep parameter at first impact, bounded by the query options. + */ + R3Real time_of_impact; + /** + * World-space contact or surface normal. + */ + struct R3Vector normal; + /** + * Shape feature kind: R3_FEATURE_UNKNOWN, R3_FEATURE_VERTEX, R3_FEATURE_EDGE, or + * R3_FEATURE_FACE. + */ + uint32_t feature_type; + /** + * Index within the feature kind; zero for unknown. + */ + uint32_t feature_id; +} R3RayHit; + +/** + * Closest projected world-space point and its collider. + * @ingroup queries + */ +typedef struct R3PointProjection { + /** + * World-bound collider handle. + */ + struct R3ColliderHandle collider; + /** + * Projected world-space point. + */ + struct R3Vector point; + /** + * Whether the original point was inside the collider. + */ + R3Bool is_inside; +} R3PointProjection; + +/** + * Shape-cast impact geometry; collider-side data is world-space, moving-shape data is local. + * @ingroup queries + */ +typedef struct R3ShapeCastHit { + /** + * World-bound collider handle. + */ + struct R3ColliderHandle collider; + /** + * Ray/sweep parameter at first impact, bounded by the query options. + */ + R3Real time_of_impact; + /** + * Impact witness on the collider, in world coordinates. + */ + struct R3Vector witness1; + /** + * Impact witness on the moving shape, in its local coordinates. + */ + struct R3Vector witness2; + /** + * Impact normal on the collider, in world coordinates. + */ + struct R3Vector normal1; + /** + * Impact normal on the moving shape, in its local coordinates. + */ + struct R3Vector normal2; + /** + * Native result: 0 out of iterations, 1 converged, 2 failed, 3 penetrating or within target + * distance. + */ + uint32_t status; +} R3ShapeCastHit; + +/** + * Sweep termination settings. Initialize with r3DefaultShapeCastOptions. + * @ingroup queries + */ +typedef struct R3ShapeCastOptions { + /** + * Maximum sweep parameter; movement is velocity multiplied by this time. + */ + R3Real max_time_of_impact; + /** + * Nonnegative separation at which a shape cast counts as a hit. + */ + R3Real target_distance; + /** + * Whether to stop at t = 0 for an initial overlap. + */ + R3Bool stop_at_penetration; + /** + * Whether to compute witness points/normals for an initial overlap. + */ + R3Bool compute_impact_geometry_on_penetration; +} R3ShapeCastOptions; + +/** + * Axis-aligned bounding box. + * @ingroup math + */ +typedef struct R3Aabb { + /** + * Minimum corner in each coordinate. + */ + struct R3Vector mins; + /** + * Maximum corner in each coordinate. + */ + struct R3Vector maxs; +} R3Aabb; + +/** + * Optional ray collider/time result. A miss is found = 0 with status OK. + * @ingroup queries + */ +typedef struct R3RayToi { + /** + * World-bound collider handle. + */ + struct R3ColliderHandle collider; + /** + * Ray parameter t at impact: origin + direction * t. + */ + R3Real toi; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R3Bool found; +} R3RayToi; + +/** + * Optional full ray result. A miss is found = 0 with status OK. + * @ingroup queries + */ +typedef struct R3OptionalRayHit { + /** + * Shape/ray impact details. + */ + struct R3RayHit hit; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R3Bool found; +} R3OptionalRayHit; + +/** + * Stack-allocated rigid-body construction data. Initialize with r3DynamicRigidBodyDesc, + * r3FixedRigidBodyDesc, or a kinematic description constructor. + * Copying this value is safe; it owns no resources and must never be freed by Rapier. + * @ingroup rigid_bodies + */ +typedef struct R3RigidBodyDesc { + /** + * World-space pose. + */ + struct R3Pose position; + /** + * World-space linear velocity. + */ + struct R3Vector linvel; + /** + * World-space angular velocity in radians per second. + */ + R3AngVector angvel; + /** + * R3_DYNAMIC, R3_FIXED, R3_KINEMATIC_POSITION_BASED, or R3_KINEMATIC_VELOCITY_BASED. + */ + uint32_t bodyType; + /** + * Multiplier applied to world gravity. + */ + R3Real gravityScale; + /** + * Nonnegative linear damping coefficient. + */ + R3Real linearDamping; + /** + * Nonnegative angular damping coefficient. + */ + R3Real angularDamping; + /** + * Nonnegative mass added to attached collider contributions. + */ + R3Real additionalMass; + /** + * 1 uses additionalMassProperties; 0 uses additionalMass. + */ + R3Bool useAdditionalMassProperties; + /** + * Additional body-local mass and inertia when enabled. + */ + struct R3MassProperties additionalMassProperties; + /** + * Locked-axis bitmask; translations precede rotations. + */ + uint8_t lockedAxes; + /** + * Whether automatic sleeping is allowed. + */ + R3Bool canSleep; + /** + * Whether the body starts/is asleep. + */ + R3Bool sleeping; + /** + * Whether continuous collision detection is enabled. + */ + R3Bool ccdEnabled; + /** + * Nonnegative prediction distance for soft CCD. + */ + R3Real softCcdPrediction; + /** + * Whether to allow fast rotations without the native angular-motion clamp. + */ + R3Bool allowFastRotation; + /** + * Whether this setting/object is enabled (0 or 1). + */ + R3Bool enabled; + /** + * Signed dominance group; larger groups dominate smaller groups. + */ + int8_t dominanceGroup; + /** + * Extra solver iterations for this body and connected bodies. + */ + size_t additionalSolverIterations; + /** + * Extra PGS iterations for this body. + */ + size_t additionalPgsIterations; + /** + * Whether to include gyroscopic forces (3D). + */ + R3Bool gyroscopicForcesEnabled; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + struct R3UserData userData; +} R3RigidBodyDesc; + +/** + * Sizes of the POD types in this library build, for foreign-language layout checks. + * @ingroup math + */ +typedef struct R3PodLayout { + /** + * Size in bytes of the rigidBodyDesc structure; zero when unavailable. + */ + size_t rigidBodyDesc; + /** + * Size in bytes of the colliderDesc structure; zero when unavailable. + */ + size_t colliderDesc; + /** + * Size in bytes of the shapeDesc structure; zero when unavailable. + */ + size_t shapeDesc; + /** + * Size in bytes of the jointDesc structure; zero when unavailable. + */ + size_t jointDesc; + /** + * Size in bytes of the softBodyMaterial structure; zero when unavailable. + */ + size_t softBodyMaterial; + /** + * Size in bytes of the integrationParameters structure; zero when unavailable. + */ + size_t integrationParameters; + /** + * Size in bytes of the softBodyDesc structure; zero when unavailable. + */ + size_t softBodyDesc; + /** + * Size in bytes of the softMeshBindingDesc structure; zero when unavailable. + */ + size_t softMeshBindingDesc; + /** + * Size in bytes of the queryOptions structure; zero when unavailable. + */ + size_t queryOptions; + /** + * Zero unless 3D f32 robotics is enabled. + */ + size_t urdfLoaderOptions; + /** + * Zero unless 3D f32 robotics is enabled. + */ + size_t mjcfLoaderOptions; +} R3PodLayout; + +/** + * CharacterLength counterpart: relative=1 scales with character height, relative=0 uses world + * units. + * @ingroup controllers + */ +typedef struct R3CharacterLength { + /** + * Value used when enabled is 1. + */ + R3Real value; + /** + * 1 scales value by the character shape size; 0 uses an absolute length. + */ + R3Bool relative; +} R3CharacterLength; + +/** + * Allowed character motion and ground-contact state. + * @ingroup controllers + */ +typedef struct R3CharacterMovement { + /** + * Allowed world-space displacement; not applied automatically. + */ + struct R3Vector translation; + /** + * Whether the character touches the ground after movement. + */ + R3Bool grounded; + /** + * Whether motion includes sliding down a non-climbable slope. + */ + R3Bool is_sliding_down_slope; +} R3CharacterMovement; + +/** + * Collision recorded during character movement. + * @ingroup controllers + */ +typedef struct R3CharacterCollision { + /** + * World-bound collider handle. + */ + struct R3ColliderHandle collider; + /** + * World-space character pose at collision. + */ + struct R3Pose character_pos; + /** + * World-space translation already applied before collision. + */ + struct R3Vector translation_applied; + /** + * World-space translation remaining at collision. + */ + struct R3Vector translation_remaining; + /** + * Shape/ray impact details. + */ + struct R3ShapeCastHit hit; +} R3CharacterCollision; + +/** + * Per-axis proportional, integral, and derivative controller gains. + * @ingroup controllers + */ +typedef struct R3PidGains { + /** + * Linear proportional gain per axis. + */ + struct R3Vector lin_kp; + /** + * Linear integral gain per axis. + */ + struct R3Vector lin_ki; + /** + * Linear derivative gain per axis. + */ + struct R3Vector lin_kd; + /** + * Angular proportional gain per axis. + */ + R3AngVector ang_kp; + /** + * Angular integral gain per axis. + */ + R3AngVector ang_ki; + /** + * Angular derivative gain per axis. + */ + R3AngVector ang_kd; +} R3PidGains; + +/** + * Linear and angular velocity correction computed by a controller. + * @ingroup controllers + */ +typedef struct R3VelocityCorrection { + /** + * World-space linear velocity correction. + */ + struct R3Vector linear; + /** + * World-space angular velocity correction, in radians per second. + */ + R3AngVector angularVelocity; +} R3VelocityCorrection; + +/** + * Copy of character sliding, slope, and snapping settings. + * @ingroup controllers + */ +typedef struct R3CharacterControllerSettings { + /** + * Whether obstacle sliding is enabled. + */ + R3Bool slide; + /** + * Maximum climbable slope angle in radians. + */ + R3Real max_slope_climb_angle; + /** + * Minimum slope angle for sliding, in radians. + */ + R3Real min_slope_slide_angle; + /** + * Whether downward ground snapping is enabled. + */ + R3Bool snap_to_ground; + /** + * Maximum downward snapping distance. + */ + struct R3CharacterLength snap_distance; +} R3CharacterControllerSettings; + +#if defined(RAPIER_DIM3) +/** + * Wheel suspension/friction parameters. Initialize with r3DefaultWheelTuning. + * @ingroup controllers + */ +typedef struct R3WheelTuning { + /** + * Nonnegative suspension spring stiffness. + */ + R3Real suspension_stiffness; + /** + * Nonnegative damping coefficient during suspension compression. + */ + R3Real suspension_compression; + /** + * Nonnegative suspension relaxation damping. + */ + R3Real suspension_damping; + /** + * Maximum suspension travel in length units. + */ + R3Real max_suspension_travel; + /** + * Maximum tire friction/slip coefficient. + */ + R3Real friction_slip; + /** + * Maximum force exerted by suspension. + */ + R3Real max_suspension_force; + /** + * Sideways tire friction stiffness. + */ + R3Real side_friction_stiffness; +} R3WheelTuning; +#endif + +#if defined(RAPIER_DIM3) +/** + * Copy of the current wheel pose, suspension, and contact state. + * @ingroup controllers + */ +typedef struct R3WheelState { + /** + * World-space wheel center. + */ + struct R3Vector center; + /** + * World-space suspension direction. + */ + struct R3Vector suspension; + /** + * World-space axle direction. + */ + struct R3Vector axle; + /** + * Wheel rotation angle in radians. + */ + R3Real rotation; + /** + * Current suspension force. + */ + R3Real suspension_force; + /** + * Current suspension length. + */ + R3Real suspension_length; + /** + * Whether the wheel has ground contact. + */ + R3Bool is_in_contact; + /** + * Ground collider handle, invalid when there is no contact. + */ + struct R3ColliderHandle ground_object; + /** + * World-space ground contact point. + */ + struct R3Vector contact_point; + /** + * World-space ground contact normal. + */ + struct R3Vector contact_normal; +} R3WheelState; +#endif + +/** + * Called synchronously on the calling thread when an operation reports an error. + * The diagnostic is borrowed for the duration of the callback. The callback + * must return normally or terminate the process: never throw or longjmp across + * the Rust/C boundary. Nested failing calls do not invoke the handler recursively. + * @ingroup errors + */ +typedef void (RAPIER_CALL *R3ErrorCallback)(R3Status, const char*, void*); + +/** + * An optional thread-local error handler. A null callback disables reporting. + * Keep the callback and user_data alive until the handler is replaced. + * @ingroup errors + */ +typedef struct R3ErrorHandler { + /** + * Optional error callback; NULL disables notifications. + */ + R3ErrorCallback callback; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + void *user_data; +} R3ErrorHandler; + +/** + * World-bound handles of the two connected bodies. + * @ingroup joints + */ +typedef struct R3JointBodies { + /** + * First connected body. + */ + struct R3RigidBodyHandle body1; + /** + * Second connected body. + */ + struct R3RigidBodyHandle body2; +} R3JointBodies; + +/** + * Damped least-squares inverse-kinematics parameters. + * @ingroup math + */ +typedef struct R3InverseKinematicsOptions { + /** + * Nonnegative motor damping. + */ + R3Real damping; + /** + * Maximum inverse-kinematics iterations. + */ + size_t max_iters; + /** + * Controlled-axis bitmask; translations precede rotations. + */ + uint8_t constrained_axes; + /** + * Linear convergence tolerance. + */ + R3Real epsilon_linear; + /** + * Angular convergence tolerance in radians. + */ + R3Real epsilon_angular; +} R3InverseKinematicsOptions; + +/** + * Optional per-link filter, called synchronously. Must not reenter or retain physics objects. + * @ingroup joints + */ +typedef R3Bool (RAPIER_CALL *R3IkJointCanMove)(void*, struct R3RigidBodyHandle); + +/** + * Collision start/stop flags match Rapier CollisionEventFlags. + * @ingroup events + */ +typedef struct R3CollisionEvent { + /** + * First collider in the pair. + */ + struct R3ColliderHandle collider1; + /** + * Second collider in the pair. + */ + struct R3ColliderHandle collider2; + /** + * 1 for a starting event, 0 for a stopping event. + */ + R3Bool started; + /** + * Event flags: bit 0 sensor pair, bit 1 removed collider. + */ + uint32_t flags; +} R3CollisionEvent; + +/** + * Contact-force event, enabled by flags and the collider force threshold. + * @ingroup events + */ +typedef struct R3ContactForceEvent { + /** + * First collider in the pair. + */ + struct R3ColliderHandle collider1; + /** + * Second collider in the pair. + */ + struct R3ColliderHandle collider2; + /** + * Sum of world-space contact forces. + */ + struct R3Vector total_force; + /** + * Sum of contact force magnitudes. + */ + R3Real total_force_magnitude; + /** + * World-space direction of the strongest contact force. + */ + struct R3Vector max_force_direction; + /** + * Magnitude of the strongest contact force. + */ + R3Real max_force_magnitude; + /** + * 1 for a starting event, 0 for a stopping event. + */ + R3Bool started; +} R3ContactForceEvent; + +/** + * Pair callback: -1 rejects a contact pair; 0 detects contacts without impulses; 1 computes + * impulses. + * For sensor intersections only, zero rejects and any positive value accepts. + * @ingroup math + */ +typedef int32_t (RAPIER_CALL *R3PairFilter)(void *user_data, + const struct R3ReadContext *read, + struct R3ColliderHandle collider1, + struct R3ColliderHandle collider2, + struct R3RigidBodyHandle body1, + struct R3RigidBodyHandle body2); + +/** + * Mutable per-manifold properties. Set enabled=0 to discard all its solver contacts. + * @ingroup events + */ +typedef struct R3ContactModification { + /** + * World-space contact or surface normal. + */ + struct R3Vector normal; + /** + * Nonnegative friction coefficient. + */ + R3Real friction; + /** + * Nonnegative restitution coefficient. + */ + R3Real restitution; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + uint32_t user_data; + /** + * Whether this setting/object is enabled (0 or 1). + */ + R3Bool enabled; +} R3ContactModification; + +/** + * Modify aggregate contact properties for one pair; contact is mutable only during this callback. + * @ingroup callbacks + */ +typedef void (RAPIER_CALL *R3ModifyContacts)(void *user_data, + const struct R3ReadContext *read, + struct R3ColliderHandle collider1, + struct R3ColliderHandle collider2, + struct R3ContactModification *contact); + +/** + * Modify individual solver contacts through a borrowed context, valid only during the callback. + * @ingroup callbacks + */ +typedef void (RAPIER_CALL *R3ModifyContactContext)(void *user_data, + const struct R3ReadContext *read, + struct R3ColliderHandle collider1, + struct R3ColliderHandle collider2, + struct R3ContactModificationContext *context); + +/** + * Callbacks must not unwind or retain arguments. Use their ReadContext to inspect bodies and + * colliders; ordinary access to the stepping world returns WORLD_BUSY. Mutations must be + * performed after stepping. With parallel builds + * callbacks and their user_data must be safe for concurrent invocation. NULL callbacks use + * defaults. + * @ingroup callbacks + */ +typedef struct R3PhysicsHooks { + /** + * Application data; Rapier does not own pointers encoded in it. + */ + void *user_data; + /** + * Optional contact-pair filter, called only for colliders enabling the hook. + */ + R3PairFilter filter_contact_pair; + /** + * Optional sensor-pair filter, called only for colliders enabling the hook. + */ + R3PairFilter filter_intersection_pair; + /** + * Optional legacy aggregate contact-edit callback. + */ + R3ModifyContacts modify_solver_contacts; + /** + * Runs after the legacy property callback. Context accessors may be called here. + */ + R3ModifyContactContext modify_solver_contacts_context; +} R3PhysicsHooks; + +/** + * Borrowed bytes. Valid while the source Bytes object remains alive; never free data. + * @ingroup math + */ +typedef struct R3ByteView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const uint8_t *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3ByteView; + +/** + * World-space line segment produced by physics debug rendering. + * @ingroup events + */ +typedef struct R3DebugLine { + /** + * World-space start point. + */ + struct R3Vector a; + /** + * World-space end point. + */ + struct R3Vector b; + /** + * RGBA color, four floats. + */ + float color[4]; +} R3DebugLine; + +/** + * Particle remapping after tearing. + * @ingroup math + */ +typedef struct R3ParticleDestination { + /** + * World-bound rigid/soft-body handle, as selected by the field type. + */ + struct R3SoftBodyHandle body; + /** + * Zero-based element index. + */ + uint32_t index; +} R3ParticleDestination; + +/** + * Cluster and proxy remapping after a soft-body split. + * @ingroup soft_bodies + */ +typedef struct R3SoftClusterSplit { + /** + * Original cluster index before splitting. + */ + uint32_t source_cluster; + /** + * Resulting soft-body handle. + */ + struct R3SoftBodyHandle soft_body; + /** + * Cluster index within its soft body. + */ + uint32_t cluster; + /** + * Cluster rigid-proxy body handle. + */ + struct R3RigidBodyHandle proxy; + /** + * Whether this split retains the original proxy. + */ + R3Bool keeps_proxy; +} R3SoftClusterSplit; + +/** + * Impulse-joint movement between rigid proxies after tearing. + * @ingroup soft_bodies + */ +typedef struct R3SoftJointMove { + /** + * Impulse joint that moved between proxies. + */ + struct R3ImpulseJointHandle joint; + /** + * Original rigid-proxy body. + */ + struct R3RigidBodyHandle from; + /** + * Destination rigid-proxy body. + */ + struct R3RigidBodyHandle to; +} R3SoftJointMove; + +/** + * Optional particle remapping; check found before reading the destination. + * @ingroup math + */ +typedef struct R3OptionalParticleDestination { + /** + * World-bound rigid/soft-body handle, as selected by the field type. + */ + struct R3SoftBodyHandle body; + /** + * Zero-based element index. + */ + uint32_t index; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R3Bool found; +} R3OptionalParticleDestination; + +/** + * Runtime ABI identity and fundamental type sizes. + * @ingroup errors + */ +typedef struct R3BuildInfo { + /** + * Binary ABI revision; compare with the header ABI_VERSION. + */ + uint32_t abi_version; + /** + * Spatial dimension, 2 or 3. + */ + uint32_t dimension; + /** + * Size of Real in bytes, 4 or 8. + */ + uint32_t real_size; + /** + * Pointer size in bytes. + */ + uint32_t pointer_size; +} R3BuildInfo; + +/** + * Features available through the loaded C library, independent of consumer defines. + * @ingroup errors + */ +typedef struct R3BuildFeatures { + /** + * Whether native profiling timers were compiled in. + */ + R3Bool profiling; + /** + * Solver SIMD lane count. Hardware instruction width depends on the target CPU. + */ + uint32_t simd_lanes; + /** + * Whether this library exposes Rapier's parallel execution and thread-pool APIs. + */ + R3Bool parallel; +} R3BuildFeatures; + +/** + * Current narrow-phase pair and accumulated solver impulse summary. + * @ingroup events + */ +typedef struct R3ContactPair { + /** + * First collider in the pair. + */ + struct R3ColliderHandle collider1; + /** + * Second collider in the pair. + */ + struct R3ColliderHandle collider2; + /** + * Whether the pair has an active solver contact. + */ + R3Bool has_any_active_contact; + /** + * Sum of world-space contact impulse vectors. + */ + struct R3Vector total_impulse; + /** + * Sum of contact impulse magnitudes. + */ + R3Real total_impulse_magnitude; + /** + * Largest contact impulse magnitude. + */ + R3Real max_impulse; + /** + * World-space direction of the largest contact impulse. + */ + struct R3Vector max_impulse_direction; +} R3ContactPair; + +/** + * Sensor intersection state for a collider pair. + * @ingroup math + */ +typedef struct R3IntersectionPair { + /** + * First collider in the pair. + */ + struct R3ColliderHandle collider1; + /** + * Second collider in the pair. + */ + struct R3ColliderHandle collider2; + /** + * Whether the two sensor/collider shapes intersect. + */ + R3Bool intersecting; +} R3IntersectionPair; + +/** + * One manifold contact and its normal solver impulse. + * @ingroup events + */ +typedef struct R3ContactPoint { + /** + * Index of the contact manifold within its pair. + */ + size_t manifold_index; + /** + * Contact point in collider 1 local coordinates. + */ + struct R3Vector local_p1; + /** + * Contact point in collider 2 local coordinates. + */ + struct R3Vector local_p2; + /** + * World-space contact or surface normal. + */ + struct R3Vector normal; + /** + * Signed separation; negative means penetration. + */ + R3Real distance; + /** + * Normal impulse applied at this contact. + */ + R3Real impulse; +} R3ContactPoint; + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Loader configuration. Initialize with DefaultUrdfLoaderOptions; no destructor. + * Blueprint array views and shared shapes are borrowed through the load call. + * @ingroup robotics + */ +typedef struct R3UrdfLoaderOptions { + /** + * Whether to build colliders from collision geometry. + */ + R3Bool createCollidersFromCollisionShapes; + /** + * Whether to also build colliders from visual geometry. + */ + R3Bool createCollidersFromVisualShapes; + /** + * Whether to use imported mass/inertia properties. + */ + R3Bool applyImportedMassProps; + /** + * Whether bodies connected by imported joints may collide. + */ + R3Bool enableJointCollisions; + /** + * Whether imported root bodies are fixed. + */ + R3Bool makeRootsFixed; + /** + * Whether to merge empty fixed URDF links. + */ + R3Bool squeezeEmptyFixedLinks; + /** + * Transform applied to the imported model. + */ + struct R3Pose shift; + /** + * Shape scale along each axis. + */ + R3Real scale; + /** + * Default collider description; nested geometry resources are borrowed through loading. + */ + struct R3ColliderDesc colliderBlueprint; + /** + * Default rigid-body description used by the importer. + */ + struct R3RigidBodyDesc rigidBodyBlueprint; +} R3UrdfLoaderOptions; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Loader configuration. Initialize with DefaultMjcfLoaderOptions; no destructor. + * Blueprint array views and shared shapes are borrowed through the load call. + * @ingroup robotics + */ +typedef struct R3MjcfLoaderOptions { + /** + * Whether to build colliders from collision geometry. + */ + R3Bool createCollidersFromCollisionShapes; + /** + * Whether to also build colliders from visual geometry. + */ + R3Bool createCollidersFromVisualShapes; + /** + * Whether to use imported mass/inertia properties. + */ + R3Bool applyImportedMassProps; + /** + * Whether bodies connected by imported joints may collide. + */ + R3Bool enableJointCollisions; + /** + * Whether imported root bodies are fixed. + */ + R3Bool makeRootsFixed; + /** + * Whether to omit MJCF plane geometry. + */ + R3Bool skipPlaneGeoms; + /** + * Whether imported joint motors are disabled. + */ + R3Bool disableJointMotors; + /** + * Transform applied to the imported model. + */ + struct R3Pose shift; + /** + * Shape scale along each axis. + */ + R3Real scale; + /** + * Default collider description; nested geometry resources are borrowed through loading. + */ + struct R3ColliderDesc colliderBlueprint; + /** + * Default rigid-body description used by the importer. + */ + struct R3RigidBodyDesc rigidBodyBlueprint; +} R3MjcfLoaderOptions; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * A borrowed visual declaration, valid until its robot is freed or its body storage changes. + * @ingroup robotics + */ +typedef struct R3MjcfVisualMesh R3MjcfVisualMesh; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Imported physically based visual material; no texture ownership. + * @ingroup robotics + */ +typedef struct R3RenderMaterial { + /** + * Material metallic factor. + */ + float metallic; + /** + * Material roughness factor. + */ + float roughness; + /** + * Material reflectance factor. + */ + float reflectance; + /** + * RGB emissive color. + */ + float emissive[3]; +} R3RenderMaterial; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copied metadata for a borrowed MJCF visual mesh. + * @ingroup robotics + */ +typedef struct R3MjcfVisualMeshInfo { + /** + * Visual pose relative to its source body. + */ + struct R3Pose local_pose; + /** + * RGBA visual color. + */ + float rgba[4]; + /** + * Copied render material; meaningful when has_material is 1. + */ + struct R3RenderMaterial material; + /** + * Whether rgba contains an authored color. + */ + R3Bool has_color; + /** + * Whether material contains authored material data. + */ + R3Bool has_material; + /** + * Whether the visual geometry is a triangle mesh. + */ + R3Bool is_trimesh; +} R3MjcfVisualMeshInfo; +#endif + +/** + * A copied state snapshot, with no pointers or ownership obligations. + * @ingroup rigid_bodies + */ +typedef struct R3RigidBodyState { + /** + * World-space pose. + */ + struct R3Pose position; + /** + * World-space linear velocity. + */ + struct R3Vector linvel; + /** + * World-space angular velocity in radians per second. + */ + R3AngVector angvel; + /** + * Whether the body starts/is asleep. + */ + R3Bool sleeping; + /** + * Whether this setting/object is enabled (0 or 1). + */ + R3Bool enabled; + /** + * Application data; Rapier does not own pointers encoded in it. + */ + struct R3UserData userData; +} R3RigidBodyState; + +/** + * Stable identity of a live mesh within one soft body; matches Rapier's SoftMeshId. + * @ingroup soft_bodies + */ +typedef struct R3SoftMeshId { + /** + * Cluster index within its soft body. + */ + uint32_t cluster; + /** + * Mesh index within the cluster. + */ + uint32_t mesh; +} R3SoftMeshId; + +/** + * Mesh identity and rendering metadata. A render-only mesh has an invalid collider handle. + * @ingroup soft_bodies + */ +typedef struct R3SoftMeshInfo { + /** + * Cluster/mesh pair identifying this collision mesh. + */ + struct R3SoftMeshId id; + /** + * World-bound collider handle. + */ + struct R3ColliderHandle collider; + /** + * Indices per mesh element: 2 for an edge or 3 for a triangle. + */ + size_t arity; + /** + * Whether vertex positions are obtained by skinning. + */ + R3Bool is_skinned; + /** + * Whether collision detection is enabled for this mesh. + */ + R3Bool collision_enabled; +} R3SoftMeshInfo; + +#if defined(RAPIER_F32) +/** + * Voxel coordinates have DIM signed integer components. + * @ingroup math + */ +typedef int32_t R3VoxelCoord; +#endif + +#if defined(RAPIER_F64) +/** + * Signed voxel coordinate integer. + * @ingroup math + */ +typedef int64_t R3VoxelCoord; +#endif + +/** + * Integer coordinates of a voxel cell. + * @ingroup math + */ +typedef struct R3VoxelKey { + /** + * X component. + */ + R3VoxelCoord x; + /** + * Y component. + */ + R3VoxelCoord y; +#if defined(RAPIER_DIM3) + /** + * Z component. + */ + R3VoxelCoord z; +#endif +} R3VoxelKey; + +/** + * Optional voxel lookup result; check found before reading voxel data. + * @ingroup queries + */ +typedef struct R3VoxelQuery { + /** + * Integer voxel coordinates. + */ + struct R3VoxelKey key; + /** + * World-space wheel center. + */ + struct R3Vector center; + /** + * Voxel dimensions along each axis. + */ + struct R3Vector size; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R3Bool found; +} R3VoxelQuery; + +/** + * @ingroup errors + * Operation succeeded. + */ +#define R3_OK 0 + +/** + * @ingroup errors + * A required pointer was NULL. + */ +#define R3_NULL_POINTER 1 + +/** + * @ingroup errors + * An argument failed validation. + */ +#define R3_INVALID_ARGUMENT 2 + +/** + * @ingroup errors + * The entity handle is stale, invalid, or belongs to another world. + */ +#define R3_INVALID_HANDLE 3 + +/** + * @ingroup errors + * Output capacity is insufficient; the returned count is the required capacity. + */ +#define R3_BUFFER_TOO_SMALL 4 + +/** + * @ingroup errors + * This build or object does not support the operation. + */ +#define R3_UNSUPPORTED 5 + +/** + * @ingroup errors + * Rust panicked; discard objects mutated by the call. + */ +#define R3_PANIC 6 + +/** + * @ingroup errors + * No matching query result or object was found. + */ +#define R3_NOT_FOUND 7 + +/** + * @ingroup errors + * Conflicting or reentrant access to simulation state. No mutation was performed. + */ +#define R3_WORLD_BUSY 8 + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Return native default soft body material. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SoftBodyMaterial RAPIER_CALL r3DefaultSoftBodyMaterial(void); + +/** + * Return native default soft recovery settings. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SoftRecoverySettings RAPIER_CALL r3DefaultSoftRecoverySettings(void); + +#if defined(RAPIER_FEM) +/** + * Return native default soft fem parameters. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SoftFemParameters RAPIER_CALL r3DefaultSoftFemParameters(void); +#endif + +/** + * Return native default soft bodies settings. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SoftBodiesSettings RAPIER_CALL r3DefaultSoftBodiesSettings(void); + +/** + * Return native default integration parameters. This POD value owns no resources. + * @ingroup worlds + */ +RAPIER_API struct R3IntegrationParameters RAPIER_CALL r3DefaultIntegrationParameters(void); + +/** + * Return a copy of all world integration settings. + * @ingroup worlds + */ +RAPIER_API struct R3IntegrationParameters RAPIER_CALL r3IntegrationParameters(const struct R3World *world); + +/** + * Copies validated values; does not expose a writable alias to Rust memory. + * @ingroup worlds + */ +RAPIER_API +R3Status RAPIER_CALL r3SetIntegrationParameters(struct R3World *world, + const struct R3IntegrationParameters *data); + +/** + * Return native default joint desc. This POD value owns no resources. + * @ingroup joints + */ +RAPIER_API struct R3JointDesc RAPIER_CALL r3DefaultJointDesc(void); + +/** + * Return a fixed joint description with native defaults; no allocation. + * @ingroup joints + */ +RAPIER_API struct R3JointDesc RAPIER_CALL r3FixedJointDesc(void); + +#if defined(RAPIER_DIM2) +/** + * Return a revolute joint description with native defaults; no allocation. + * @ingroup joints + */ +RAPIER_API struct R3JointDesc RAPIER_CALL r3RevoluteJointDesc(void); +#endif + +#if defined(RAPIER_DIM3) +/** + * Returns a joint description. Invalid axes produce nonfinite frames, rejected on insertion. + * @ingroup joints + */ +RAPIER_API struct R3JointDesc RAPIER_CALL r3RevoluteJointDesc(struct R3Vector axis_vector); +#endif + +/** + * Returns a joint description. Invalid axes produce nonfinite frames, rejected on insertion. + * @ingroup joints + */ +RAPIER_API struct R3JointDesc RAPIER_CALL r3PrismaticJointDesc(struct R3Vector axis_vector); + +/** + * Return a rope joint description with native defaults; no allocation. + * @ingroup joints + */ +RAPIER_API struct R3JointDesc RAPIER_CALL r3RopeJointDesc(R3Real length); + +/** + * Return a spring joint description with native defaults; no allocation. + * @ingroup joints + */ +RAPIER_API +struct R3JointDesc RAPIER_CALL r3SpringJointDesc(R3Real length, + R3Real stiffness, + R3Real damping); + +#if defined(RAPIER_DIM3) +/** + * Return a spherical joint description with native defaults; no allocation. + * @ingroup joints + */ +RAPIER_API struct R3JointDesc RAPIER_CALL r3SphericalJointDesc(void); +#endif + +#if defined(RAPIER_DIM2) +/** + * Returns a joint description. Invalid axes produce nonfinite frames, rejected on insertion. + * @ingroup joints + */ +RAPIER_API struct R3JointDesc RAPIER_CALL r3PinSlotJointDesc(struct R3Vector axis_vector); +#endif + +/** + * Create an impulse joint connecting two bodies in the same world. The world owns the joint; + * wake_up wakes the connected bodies. + * @ingroup joints + */ +RAPIER_API +struct R3ImpulseJointHandle RAPIER_CALL r3InsertImpulseJoint(struct R3RigidBodyHandle body1, + struct R3RigidBodyHandle body2, + const struct R3JointDesc *joint); + +/** + * Create an articulation joint between bodies in the same world. Returns an invalid handle on + * failure; check r3LastStatus. + * @ingroup joints + */ +RAPIER_API +struct R3MultibodyJointHandle RAPIER_CALL r3InsertMultibodyJoint(struct R3RigidBodyHandle body1, + struct R3RigidBodyHandle body2, + const struct R3JointDesc *joint); + +/** + * Return native default soft body desc. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SoftBodyDesc RAPIER_CALL r3DefaultSoftBodyDesc(void); + +/** + * Consumes no caller-owned resources. All borrowed arrays may be released on return. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyHandle RAPIER_CALL r3InsertSoftBody(struct R3World *world, + const struct R3SoftBodyDesc *desc); + +/** + * Return native default soft mesh binding desc. This POD value owns no resources. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SoftMeshBindingDesc RAPIER_CALL r3DefaultSoftMeshBindingDesc(void); + +/** + * Create a deformable collider bound to a soft-body cluster. The world owns the collider; binding + * arrays are borrowed only during insertion. + * @ingroup colliders + */ +RAPIER_API +struct R3ColliderHandle RAPIER_CALL r3InsertDeformableCollider(const struct R3ColliderDesc *collider, + const struct R3SoftMeshBindingDesc *binding, + struct R3RigidBodyHandle parent); + +/** + * Return native default query options. This POD value owns no resources. + * @ingroup queries + */ +RAPIER_API struct R3QueryOptions RAPIER_CALL r3DefaultQueryOptions(void); + +/** + * Return the closest ray hit, or report R3_NOT_FOUND on a miss. The ray is origin + direction * t + * for 0 <= t <= max_toi; direction need not be normalized. solid treats an interior origin as a + * hit at t = 0. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R3RayHit RAPIER_CALL r3CastRay(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Vector origin, + struct R3Vector direction, + R3Real max_toi, + R3Bool solid); + +/** + * Return the closest surface projection within max_distance, or report R3_NOT_FOUND. With solid = + * 1, an interior point projects to itself. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R3PointProjection RAPIER_CALL r3ProjectPoint(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Vector point, + R3Real max_distance, + R3Bool solid); + +/** + * Sweep shape from pose along velocity and return the first hit; report R3_NOT_FOUND on a miss. + * Time is bounded by options.max_time_of_impact. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup shapes + */ +RAPIER_API +struct R3ShapeCastHit RAPIER_CALL r3CastShape(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Pose pose, + struct R3Vector velocity, + const R3SharedShape *shape, + struct R3ShapeCastOptions options); + +/** + * Copy handles of colliders containing the world-space point. + * @see @ref output_buffers + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +size_t RAPIER_CALL r3IntersectPoint(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Vector point, + struct R3ColliderHandle *buffer, + size_t capacity); + +/** + * Copy handles of colliders intersecting the shape at its world-space pose. The shape is borrowed + * for this call. + * @see @ref output_buffers + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup shapes + */ +RAPIER_API +size_t RAPIER_CALL r3IntersectShape(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Pose pose, + const R3SharedShape *shape, + struct R3ColliderHandle *buffer, + size_t capacity); + +/** + * Copy broad-phase candidates whose bounding boxes overlap the world-space AABB. Results may + * include false positives. + * @see @ref output_buffers + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +size_t RAPIER_CALL r3IntersectAabbConservative(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Aabb aabb, + struct R3ColliderHandle *buffer, + size_t capacity); + +/** + * Return the closest ray collider and time, with found = 0 on a miss (R3_OK). The ray is origin + + * direction * t; max_toi bounds t. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R3RayToi RAPIER_CALL r3CastRayToi(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Vector origin, + struct R3Vector direction, + R3Real max_toi, + R3Bool solid); + +/** + * Return the closest ray hit with found = 0 on a miss (R3_OK). The ray is origin + direction * t; + * solid treats an interior origin as a hit at t = 0. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R3OptionalRayHit RAPIER_CALL r3TryCastRay(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Vector origin, + struct R3Vector direction, + R3Real max_toi, + R3Bool solid); + +/** + * Return a dynamic rigid-body description with native defaults; no allocation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3RigidBodyDesc RAPIER_CALL r3DynamicRigidBodyDesc(void); + +/** + * Return a fixed rigid-body description with native defaults; no allocation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3RigidBodyDesc RAPIER_CALL r3FixedRigidBodyDesc(void); + +/** + * Return a kinematic position based rigid-body description with native defaults; no allocation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3RigidBodyDesc RAPIER_CALL r3KinematicPositionBasedRigidBodyDesc(void); + +/** + * Return a kinematic velocity based rigid-body description with native defaults; no allocation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3RigidBodyDesc RAPIER_CALL r3KinematicVelocityBasedRigidBodyDesc(void); + +/** + * Return native default shape desc. This POD value owns no resources. + * @ingroup shapes + */ +RAPIER_API struct R3ShapeDesc RAPIER_CALL r3DefaultShapeDesc(void); + +/** + * Build an owned shared shape from a description; release it with r3FreeSharedShape. Borrowed + * inputs may be released after this call. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3ShapeDesc_Build(const struct R3ShapeDesc *desc); + +/** + * Return native default collider desc. This POD value owns no resources. + * @ingroup colliders + */ +RAPIER_API struct R3ColliderDesc RAPIER_CALL r3DefaultColliderDesc(void); + +/** + * Return a ball description with the supplied radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R3ColliderDesc RAPIER_CALL r3BallColliderDesc(R3Real radius); + +/** + * Return an axis-aligned box description with the supplied half-extents. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R3ColliderDesc RAPIER_CALL r3CuboidColliderDesc(struct R3Vector half_extents); + +/** + * Create a body from the description and return its world-bound handle. The world owns the body. + * @ingroup rigid_bodies + */ +RAPIER_API +struct R3RigidBodyHandle RAPIER_CALL r3InsertRigidBody(struct R3World *world, + const struct R3RigidBodyDesc *desc); + +/** + * Insert a collider attached to a rigid body, using the world stored in its handle. + * The parent handle is copied by value. The description is borrowed through this call. + * Invalid or removed parents fail without inserting a collider. + * @ingroup colliders + */ +RAPIER_API +struct R3ColliderHandle RAPIER_CALL r3InsertCollider(struct R3RigidBodyHandle parent, + const struct R3ColliderDesc *desc); + +/** + * Insert a collider without a rigid-body parent. The world owns the collider. + * The description is borrowed through this call. + * @ingroup colliders + */ +RAPIER_API +struct R3ColliderHandle RAPIER_CALL r3InsertColliderWithoutParent(struct R3World *world, + const struct R3ColliderDesc *desc); + +/** + * Return POD structure sizes for checking foreign-language layouts against this library. + * @ingroup errors + */ +RAPIER_API struct R3PodLayout RAPIER_CALL r3PodLayout(void); + +/** + * Allocate a character controller with native defaults; release it with + * r3FreeKinematicCharacterController. + * @ingroup controllers + */ +RAPIER_API struct R3KinematicCharacterController *RAPIER_CALL r3NewKinematicCharacterController(void); + +/** + * Release an owned kinematic character controller. NULL is allowed. Do not pass borrowed pointers + * or free the object twice. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3FreeKinematicCharacterController(struct R3KinematicCharacterController *controller); + +/** + * Set the up direction; it must be finite and nonzero and is normalized on input. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3KinematicCharacterController_SetUp(struct R3KinematicCharacterController *controller, + struct R3Vector up); + +/** + * Set the collision separation margin; use a positive absolute or relative character length. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3KinematicCharacterController_SetOffset(struct R3KinematicCharacterController *controller, + struct R3CharacterLength offset); + +/** + * Enable or disable sliding along obstacles. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3KinematicCharacterController_SetSlide(struct R3KinematicCharacterController *controller, + R3Bool enabled); + +/** + * Set the maximum climb angle and minimum slide angle, in radians. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3KinematicCharacterController_SetSlopes(struct R3KinematicCharacterController *controller, + R3Real max_climb_angle, + R3Real min_slide_angle); + +/** + * Configure automatic stepping over obstacles. enabled = 0 disables it. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3KinematicCharacterController_SetAutostep(struct R3KinematicCharacterController *controller, + R3Bool enabled, + struct R3CharacterLength max_height, + struct R3CharacterLength min_width, + R3Bool include_dynamic_bodies); + +/** + * Configure downward ground snapping. enabled = 0 disables it. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3KinematicCharacterController_SetSnapToGround(struct R3KinematicCharacterController *controller, + R3Bool enabled, + struct R3CharacterLength distance); + +/** + * Computes movement without moving any collider. Use the returned translation to set the character + * target. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup controllers + */ +RAPIER_API +struct R3CharacterMovement RAPIER_CALL r3KinematicCharacterController_MoveShape(const struct R3World *world, + const struct R3QueryOptions *options, + struct R3KinematicCharacterController *controller, + R3Real dt, + const R3SharedShape *shape, + struct R3Pose pose, + struct R3Vector desired_translation); + +/** + * Copy collisions recorded by the most recent MoveShape call. + * @see @ref output_buffers + * @ingroup controllers + */ +RAPIER_API +size_t RAPIER_CALL r3KinematicCharacterController_Collisions(const struct R3KinematicCharacterController *controller, + struct R3CharacterCollision *buffer, + size_t capacity); + +/** + * Applies impulses for the most recent move_shape collisions. Use the same world, shape, dt and + * filter. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3KinematicCharacterController_SolveCharacterCollisionImpulses(const struct R3KinematicCharacterController *controller, + const R3SharedShape *shape, + R3Real dt, + R3Real mass, + const struct R3QueryFilter *filter); + +/** + * Allocate a PID controller with supplied gains and controlled axes. Release with + * r3FreePidController. + * @ingroup controllers + */ +RAPIER_API struct R3PidController *RAPIER_CALL r3NewPidController(void); + +/** + * Release an owned pid controller. NULL is allowed. Do not pass borrowed pointers or free the + * object twice. + * @ingroup controllers + */ +RAPIER_API R3Status RAPIER_CALL r3FreePidController(struct R3PidController *controller); + +/** + * Return a copy of the proportional, integral, and derivative gains. + * @ingroup controllers + */ +RAPIER_API struct R3PidGains RAPIER_CALL r3PidController_Gains(const struct R3PidController *controller); + +/** + * Replace the proportional, integral, and derivative gains. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3PidController_SetGains(struct R3PidController *controller, + struct R3PidGains gains); + +/** + * AxesMask bits match Rapier: linear X/Y/Z are 1/2/4, angular X/Y/Z are 8/16/32. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3PidController_SetAxes(struct R3PidController *controller, + uint32_t axes); + +/** + * Compute a velocity correction, preserving the body's state and updating PID integrals. + * @ingroup controllers + */ +RAPIER_API +struct R3VelocityCorrection RAPIER_CALL r3PidController_RigidBodyCorrection(struct R3PidController *controller, + R3Real dt, + struct R3RigidBodyHandle body, + struct R3Pose target_pose, + struct R3Vector target_linvel, + R3AngVector target_angvel); + +/** + * Return a copy of slide, slope, and ground-snap settings. + * @ingroup controllers + */ +RAPIER_API +struct R3CharacterControllerSettings RAPIER_CALL r3KinematicCharacterController_Settings(const struct R3KinematicCharacterController *controller); + +#if defined(RAPIER_DIM3) +/** + * Return native default wheel tuning. This POD value owns no resources. + * @ingroup controllers + */ +RAPIER_API struct R3WheelTuning RAPIER_CALL r3DefaultWheelTuning(void); +#endif + +#if defined(RAPIER_DIM3) +/** + * Allocate a vehicle controller bound to its chassis body. The chassis world must outlive the + * controller. Release with r3FreeDynamicRayCastVehicleController. + * @ingroup controllers + */ +RAPIER_API +struct R3DynamicRayCastVehicleController *RAPIER_CALL r3NewDynamicRayCastVehicleController(struct R3RigidBodyHandle chassis); +#endif + +#if defined(RAPIER_DIM3) +/** + * Release an owned dynamic ray cast vehicle controller. NULL is allowed. Do not pass borrowed + * pointers or free the object twice. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3FreeDynamicRayCastVehicleController(struct R3DynamicRayCastVehicleController *controller); +#endif + +#if defined(RAPIER_DIM3) +/** + * Append a wheel and return its zero-based index. Connection, suspension direction, and axle + * are in chassis-local coordinates. + * @ingroup controllers + */ +RAPIER_API +size_t RAPIER_CALL r3DynamicRayCastVehicleController_AddWheel(struct R3DynamicRayCastVehicleController *controller, + struct R3Vector connection, + struct R3Vector direction, + struct R3Vector axle, + R3Real rest_length, + R3Real radius, + const struct R3WheelTuning *tuning); +#endif + +#if defined(RAPIER_DIM3) +/** + * Set the chassis up/forward axis indices (0 = X, 1 = Y, 2 = Z). + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3DynamicRayCastVehicleController_SetAxes(struct R3DynamicRayCastVehicleController *controller, + size_t up, + size_t forward); +#endif + +#if defined(RAPIER_DIM3) +/** + * Set a wheel engine force, brake force, and steering angle in radians. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3DynamicRayCastVehicleController_SetWheelControls(struct R3DynamicRayCastVehicleController *controller, + size_t index, + R3Real steering, + R3Real engine_force, + R3Real brake); +#endif + +#if defined(RAPIER_DIM3) +/** + * Ray-cast wheel contacts and apply vehicle forces for dt seconds. Does not step the world. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3DynamicRayCastVehicleController_UpdateVehicle(struct R3DynamicRayCastVehicleController *controller, + R3Real dt, + const struct R3QueryFilter *filter); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return signed chassis speed along its forward direction. + * @ingroup controllers + */ +RAPIER_API +R3Real RAPIER_CALL r3DynamicRayCastVehicleController_CurrentVehicleSpeed(const struct R3DynamicRayCastVehicleController *controller); +#endif + +#if defined(RAPIER_DIM3) +/** + * Copy current wheel state in wheel insertion order. + * @see @ref output_buffers + * @ingroup controllers + */ +RAPIER_API +size_t RAPIER_CALL r3DynamicRayCastVehicleController_Wheels(const struct R3DynamicRayCastVehicleController *controller, + struct R3WheelState *buffer, + size_t capacity); +#endif + +/** + * Propagate all modified body poses to attached colliders. Run collision detection or step before + * querying the broad phase. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBodyPropagateModifiedBodyPositionsToColliders(struct R3World *world); + +/** + * Copies the island manager's active body handles. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r3ActiveRigidBodies(const struct R3World *world, + struct R3RigidBodyHandle *buffer, + size_t capacity); + +/** + * Wake a body by handle, including a soft-body cluster proxy. + * @ingroup rigid_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RigidBody_WakeUp(struct R3RigidBodyHandle handle, R3Bool strong); + +/** + * Replace this thread's error handler and return the previous handler so it can + * be restored at the end of a scope. Status returns are unchanged. A handler + * that returns lets the caller recover by checking the status; a fail-fast + * handler may terminate the process. Includes R3_NOT_FOUND query misses. + * @ingroup errors + */ +RAPIER_API struct R3ErrorHandler RAPIER_CALL r3SetErrorHandler(struct R3ErrorHandler handler); + +/** + * Status of the most recent fallible operation on this thread. Reading this or + * LastError does not clear it. Infallible value constructors do not change it. + * Check immediately after a fallible value-returning operation when recovering + * from errors instead of using a fail-fast error callback. + * @ingroup errors + */ +RAPIER_API R3Status RAPIER_CALL r3LastStatus(void); + +/** + * Thread-local UTF-8 diagnostic, valid until the next fallible call on this thread. + * @ingroup errors + */ +RAPIER_API const char *RAPIER_CALL r3LastError(void); + +/** + * Create an owned ball shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3BallSharedShape(R3Real radius); + +/** + * Create an owned cuboid shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3CuboidSharedShape(struct R3Vector half_extents); + +/** + * Create an owned round cuboid shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3RoundCuboidSharedShape(struct R3Vector half_extents, + R3Real border_radius); + +/** + * Create an owned capsule shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3CapsuleSharedShape(struct R3Vector a, + struct R3Vector b, + R3Real radius); + +/** + * Create an owned segment shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3SegmentSharedShape(struct R3Vector a, struct R3Vector b); + +/** + * Create an owned triangle shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3TriangleSharedShape(struct R3Vector a, + struct R3Vector b, + struct R3Vector c); + +/** + * Create an owned halfspace shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3HalfspaceSharedShape(struct R3Vector normal); + +#if defined(RAPIER_DIM3) +/** + * Create an owned cylinder shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3CylinderSharedShape(R3Real half_height, R3Real radius); +#endif + +#if defined(RAPIER_DIM3) +/** + * Create an owned cone shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3ConeSharedShape(R3Real half_height, R3Real radius); +#endif + +/** + * Create an owned compound shape; each child pose is relative to the compound. Child shapes are + * shared, not consumed. Release with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3CompoundSharedShape(struct R3CompoundShapeView children); + +/** + * Remove the collider and update its parent body mass properties. wake_up wakes the parent. + * @ingroup colliders + */ +RAPIER_API R3Status RAPIER_CALL r3RemoveCollider(struct R3ColliderHandle handle, R3Bool wake_up); + +/** + * Remove an impulse joint. wake_up wakes its connected bodies. + * @ingroup joints + */ +RAPIER_API R3Status RAPIER_CALL r3RemoveImpulseJoint(struct R3ImpulseJointHandle handle, R3Bool wake_up); + +/** + * Copy entity handles. + * @see @ref output_buffers + * @ingroup joints + */ +RAPIER_API +size_t RAPIER_CALL r3ImpulseJointHandles(const struct R3World *world, + struct R3ImpulseJointHandle *buffer, + size_t capacity); + +/** + * Remove an articulation joint. wake_up wakes affected bodies. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3RemoveMultibodyJoint(struct R3MultibodyJointHandle handle, + R3Bool wake_up); + +/** + * Copy entity handles. + * @see @ref output_buffers + * @ingroup joints + */ +RAPIER_API +size_t RAPIER_CALL r3MultibodyJointHandles(const struct R3World *world, + struct R3MultibodyJointHandle *buffer, + size_t capacity); + +/** + * Return the two bodies connected by an impulse joint. + * @ingroup joints + */ +RAPIER_API struct R3JointBodies RAPIER_CALL r3ImpulseJoint_Bodies(struct R3ImpulseJointHandle handle); + +/** + * Return native default inverse kinematics options. This POD value owns no resources. + * @ingroup joints + */ +RAPIER_API struct R3InverseKinematicsOptions RAPIER_CALL r3DefaultInverseKinematicsOptions(void); + +/** + * Return the articulation degrees of freedom associated with the joint. + * @ingroup joints + */ +RAPIER_API size_t RAPIER_CALL r3MultibodyJoint_Ndofs(struct R3MultibodyJointHandle handle); + +/** + * Read/write displacement buffer must contain exactly ndofs entries; zero it for a fresh solve. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3MultibodyJoint_InverseKinematics(struct R3MultibodyJointHandle handle, + const struct R3InverseKinematicsOptions *options, + struct R3Pose target, + R3IkJointCanMove can_move, + void *user_data, + R3Real *displacements, + size_t count); + +/** + * Apply generalized articulation displacements in native degree-of-freedom order. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3MultibodyJoint_ApplyDisplacements(struct R3MultibodyJointHandle handle, + const R3Real *displacements, + size_t count); + +/** + * Frees an owned object; NULL is allowed. Never free a borrowed pointer. + * @ingroup shapes + */ +RAPIER_API R3Status RAPIER_CALL r3FreeSharedShape(R3SharedShape *object); + +/** + * Create an owned wrapper sharing the same immutable geometry. Release it with + * r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3SharedShape_Clone(const R3SharedShape *object); + +/** + * Return the number of rigid body objects in the world. + * @ingroup rigid_bodies + */ +RAPIER_API size_t RAPIER_CALL r3RigidBodyCount(const struct R3World *world); + +/** + * Copy entity handles. + * @see @ref output_buffers + * @ingroup rigid_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3RigidBodyHandles(const struct R3World *world, + struct R3RigidBodyHandle *buffer, + size_t capacity); + +/** + * Test whether the live world contains this rigid body handle. A removed/stale handle returns + * false. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_Contains(struct R3RigidBodyHandle handle); + +/** + * Return the number of collider objects in the world. + * @ingroup colliders + */ +RAPIER_API size_t RAPIER_CALL r3ColliderCount(const struct R3World *world); + +/** + * Copy entity handles. + * @see @ref output_buffers + * @ingroup colliders + */ +RAPIER_API +size_t RAPIER_CALL r3ColliderHandles(const struct R3World *world, + struct R3ColliderHandle *buffer, + size_t capacity); + +/** + * Test whether the live world contains this collider handle. A removed/stale handle returns false. + * @ingroup colliders + */ +RAPIER_API R3Bool RAPIER_CALL r3Collider_Contains(struct R3ColliderHandle handle); + +/** + * Return the number of soft body objects in the world. + * @ingroup soft_bodies + */ +RAPIER_API size_t RAPIER_CALL r3SoftBodyCount(const struct R3World *world); + +/** + * Copy entity handles. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyHandles(const struct R3World *world, + struct R3SoftBodyHandle *buffer, + size_t capacity); + +/** + * Test whether the live world contains this soft body handle. A removed/stale handle returns + * false. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3SoftBody_Contains(struct R3SoftBodyHandle handle); + +/** + * Remove a body and its joints, optionally keeping colliders as standalone objects. + * Returns whether a body was removed; a stale handle returns false without error. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Bool RAPIER_CALL r3RemoveRigidBody(struct R3RigidBodyHandle handle, + R3Bool remove_attached_colliders); + +/** + * Return the world setting documented by R3IntegrationParameters::dt. + * @ingroup worlds + */ +RAPIER_API R3Real RAPIER_CALL r3TimeStep(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::dt. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetTimeStep(struct R3World *world, R3Real value); + +/** + * Return the world setting documented by R3IntegrationParameters::minCcdDt. + * @ingroup worlds + */ +RAPIER_API R3Real RAPIER_CALL r3MinCcdDt(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::minCcdDt. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetMinCcdDt(struct R3World *world, R3Real value); + +/** + * Return the world setting documented by R3IntegrationParameters::lengthUnit. + * @ingroup worlds + */ +RAPIER_API R3Real RAPIER_CALL r3LengthUnit(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::lengthUnit. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetLengthUnit(struct R3World *world, R3Real value); + +/** + * Return the world setting documented by R3IntegrationParameters::warmstartCoefficient. + * @ingroup worlds + */ +RAPIER_API R3Real RAPIER_CALL r3WarmstartCoefficient(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::warmstartCoefficient. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetWarmstartCoefficient(struct R3World *world, R3Real value); + +/** + * Return the world setting documented by R3IntegrationParameters::normalizedAllowedLinearError. + * @ingroup worlds + */ +RAPIER_API R3Real RAPIER_CALL r3NormalizedAllowedLinearError(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::normalizedAllowedLinearError. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetNormalizedAllowedLinearError(struct R3World *world, R3Real value); + +/** + * Return the world setting documented by + * R3IntegrationParameters::normalizedMaxCorrectiveVelocity. + * @ingroup worlds + */ +RAPIER_API R3Real RAPIER_CALL r3NormalizedMaxCorrectiveVelocity(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::normalizedMaxCorrectiveVelocity. + * @ingroup worlds + */ +RAPIER_API +R3Status RAPIER_CALL r3SetNormalizedMaxCorrectiveVelocity(struct R3World *world, + R3Real value); + +/** + * Return the world setting documented by R3IntegrationParameters::normalizedPredictionDistance. + * @ingroup worlds + */ +RAPIER_API R3Real RAPIER_CALL r3NormalizedPredictionDistance(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::normalizedPredictionDistance. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetNormalizedPredictionDistance(struct R3World *world, R3Real value); + +/** + * Return the world setting documented by R3IntegrationParameters::normalizedMaxLinearVelocity. + * @ingroup worlds + */ +RAPIER_API R3Real RAPIER_CALL r3NormalizedMaxLinearVelocity(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::normalizedMaxLinearVelocity. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetNormalizedMaxLinearVelocity(struct R3World *world, R3Real value); + +/** + * Return the world setting documented by + * R3IntegrationParameters::normalizedContactRecycleDistance. + * @ingroup worlds + */ +RAPIER_API R3Real RAPIER_CALL r3NormalizedContactRecycleDistance(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::normalizedContactRecycleDistance. + * @ingroup worlds + */ +RAPIER_API +R3Status RAPIER_CALL r3SetNormalizedContactRecycleDistance(struct R3World *world, + R3Real value); + +/** + * Return the world setting documented by R3IntegrationParameters::numSolverIterations. + * @ingroup worlds + */ +RAPIER_API size_t RAPIER_CALL r3NumSolverIterations(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::numSolverIterations. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetNumSolverIterations(struct R3World *world, size_t value); + +/** + * Return the world setting documented by R3IntegrationParameters::numInternalPgsIterations. + * @ingroup worlds + */ +RAPIER_API size_t RAPIER_CALL r3NumInternalPgsIterations(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::numInternalPgsIterations. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetNumInternalPgsIterations(struct R3World *world, size_t value); + +/** + * Return the world setting documented by + * R3IntegrationParameters::numInternalStabilizationIterations. + * @ingroup errors + */ +RAPIER_API size_t RAPIER_CALL r3NumInternalStabilizationIterations(const struct R3World *world); + +/** + * Set the world setting documented by + * R3IntegrationParameters::numInternalStabilizationIterations. + * @ingroup errors + */ +RAPIER_API +R3Status RAPIER_CALL r3SetNumInternalStabilizationIterations(struct R3World *world, + size_t value); + +/** + * Return the world setting documented by R3IntegrationParameters::maxCcdSubsteps. + * @ingroup worlds + */ +RAPIER_API size_t RAPIER_CALL r3MaxCcdSubsteps(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::maxCcdSubsteps. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetMaxCcdSubsteps(struct R3World *world, size_t value); + +/** + * Return the world setting documented by R3IntegrationParameters::contactClustering. + * @ingroup worlds + */ +RAPIER_API R3Bool RAPIER_CALL r3ContactClustering(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::contactClustering. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetContactClustering(struct R3World *world, R3Bool value); + +/** + * Return the world setting documented by R3IntegrationParameters::contactRecycling. + * @ingroup worlds + */ +RAPIER_API R3Bool RAPIER_CALL r3ContactRecycling(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::contactRecycling. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetContactRecycling(struct R3World *world, R3Bool value); + +/** + * Return the world setting documented by R3IntegrationParameters::frictionInBiasPass. + * @ingroup worlds + */ +RAPIER_API R3Bool RAPIER_CALL r3FrictionInBiasPass(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::frictionInBiasPass. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetFrictionInBiasPass(struct R3World *world, R3Bool value); + +/** + * Return the world setting documented by R3IntegrationParameters::warmstartJoints. + * @ingroup joints + */ +RAPIER_API R3Bool RAPIER_CALL r3WarmstartJoints(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::warmstartJoints. + * @ingroup joints + */ +RAPIER_API R3Status RAPIER_CALL r3SetWarmstartJoints(struct R3World *world, R3Bool value); + +/** + * Return the world setting documented by R3IntegrationParameters::contactSoftness. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SpringCoefficients RAPIER_CALL r3ContactSoftness(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::contactSoftness. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SetContactSoftness(struct R3World *world, + struct R3SpringCoefficients value); + +/** + * Return the world setting documented by R3IntegrationParameters::staticContactSoftness. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SpringCoefficients RAPIER_CALL r3StaticContactSoftness(const struct R3World *world); + +/** + * Set the world setting documented by R3IntegrationParameters::staticContactSoftness. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SetStaticContactSoftness(struct R3World *world, + struct R3SpringCoefficients value); + +/** + * Applies Rapier's persistent one-way platform logic to the borrowed manifold. + * @ingroup worlds + */ +RAPIER_API +R3Status RAPIER_CALL r3ContactModificationContext_UpdateAsOnewayPlatform(struct R3ContactModificationContext *context, + struct R3Vector allowed_local_n1, + R3Real allowed_angle); + +/** + * Sets the tangent velocity of every rigid solver contact in this manifold. + * @ingroup worlds + */ +RAPIER_API +R3Status RAPIER_CALL r3ContactModificationContext_SetTangentVelocity(struct R3ContactModificationContext *context, + struct R3Vector velocity); + +/** + * Allocate an empty event collector; release it with r3FreeEventCollector. + * @ingroup events + */ +RAPIER_API struct R3EventCollector *RAPIER_CALL r3NewEventCollector(void); + +/** + * Release an owned event collector. NULL is allowed. Do not pass borrowed pointers or free the + * object twice. + * @ingroup events + */ +RAPIER_API R3Status RAPIER_CALL r3FreeEventCollector(struct R3EventCollector *events); + +/** + * Discard all collected events. Does not change the world. + * @ingroup events + */ +RAPIER_API R3Status RAPIER_CALL r3EventCollector_Clear(struct R3EventCollector *events); + +/** + * Copy the collected collision start/stop events without removing them. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r3EventCollector_CollisionEvents(const struct R3EventCollector *events, + struct R3CollisionEvent *buffer, + size_t capacity); + +/** + * Copy the collected contact-force events without removing them. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r3EventCollector_ContactForceEvents(const struct R3EventCollector *events, + struct R3ContactForceEvent *buffer, + size_t capacity); + +/** + * Return the number of queued soft-body tear events. + * @ingroup events + */ +RAPIER_API size_t RAPIER_CALL r3EventCollector_TearEventCount(const struct R3EventCollector *events); + +/** + * Return an owned copy of a queued tear event; release with r3FreeSoftBodyTearEvent. Does + * not remove the queued event. + * @ingroup events + */ +RAPIER_API +struct R3SoftBodyTearEvent *RAPIER_CALL r3EventCollector_TearEvent(const struct R3EventCollector *events, + size_t index); + +/** + * Return the world-space gravitational acceleration. + * @ingroup worlds + */ +RAPIER_API struct R3Vector RAPIER_CALL r3Gravity(const struct R3World *world); + +/** + * Set the world-space gravitational acceleration. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetGravity(struct R3World *world, struct R3Vector value); + +/** + * Hooks and events may be NULL. This call invalidates all borrowed set-element pointers. + * Advance simulation by one timestep. Hooks and events may be NULL. + * @ingroup worlds + */ +RAPIER_API +R3Status RAPIER_CALL r3Step(struct R3World *world, + const struct R3PhysicsHooks *hooks, + const struct R3EventCollector *events); + +/** + * Refresh collision detection without advancing simulation. Hooks and events may be NULL. + * @ingroup worlds + */ +RAPIER_API +R3Status RAPIER_CALL r3DetectCollisions(struct R3World *world, + const struct R3PhysicsHooks *hooks, + const struct R3EventCollector *events); + +/** + * Borrow snapshot bytes without copying; valid until r3FreeBytes. Never free the returned data + * pointer. + * @ingroup worlds + */ +RAPIER_API struct R3ByteView RAPIER_CALL r3Bytes_Data(const struct R3Bytes *bytes); + +/** + * Release an owned snapshot byte buffer. NULL is allowed. Do not pass borrowed pointers or free + * the object twice. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3FreeBytes(struct R3Bytes *bytes); + +/** + * Return owned snapshot bytes; release them with r3FreeBytes. See @ref snapshots for + * restoration and handle lifetimes. + * @ingroup worlds + */ +RAPIER_API struct R3Bytes *RAPIER_CALL r3SerializeWorld(const struct R3World *world); + +/** + * Restore ONLY trusted snapshots produced by the identical Rapier build. Snapshots are not a + * stable file format. + * @ingroup worlds + */ +RAPIER_API struct R3World *RAPIER_CALL r3DeserializeWorld(const uint8_t *data, size_t count); + +/** + * Color is HSLA (hue in degrees), matching Rapier DebugColor. mode uses DebugRenderMode bits. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r3DebugRender(const struct R3World *world, + uint32_t mode, + struct R3DebugLine *buffer, + size_t capacity); + +/** + * Set the world setting documented by R3SoftBodiesSettings::resweepStrain. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBodiesSetResweepStrain(struct R3World *world, R3Real value); + +/** + * Return the world setting documented by R3SoftBodiesSettings::resweepStrain. + * @ingroup soft_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3SoftBodiesResweepStrain(const struct R3World *world); + +/** + * Set the world setting documented by R3SoftBodiesSettings::contactStiffening. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBodiesSetContactStiffening(struct R3World *world, R3Real value); + +/** + * Return the world setting documented by R3SoftBodiesSettings::contactStiffening. + * @ingroup soft_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3SoftBodiesContactStiffening(const struct R3World *world); + +/** + * Set the world setting documented by R3SoftBodiesSettings::maxExtraSubsteps. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBodiesSetMaxExtraSubsteps(struct R3World *world, size_t value); + +/** + * Return the world setting documented by R3SoftBodiesSettings::maxExtraSubsteps. + * @ingroup soft_bodies + */ +RAPIER_API size_t RAPIER_CALL r3SoftBodiesMaxExtraSubsteps(const struct R3World *world); + +/** + * Set the world setting documented by R3SoftRecoverySettings::authoredVelocityMargin. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetAuthoredVelocityMargin(struct R3World *world, + R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::edgeSpeculation. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetEdgeSpeculation(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::invertedCellDetection. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetInvertedCellDetection(struct R3World *world, + R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::selfCrossingDetection. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetSelfCrossingDetection(struct R3World *world, + R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::detectionMotionGating. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetDetectionMotionGating(struct R3World *world, + R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::crossBodyDetection. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetCrossBodyDetection(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::selfStandDown. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetSelfStandDown(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::crossBodyExpelGate. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetCrossBodyExpelGate(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::edgeStandDown. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetEdgeStandDown(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::crossingRepulsion. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetCrossingRepulsion(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::crossingRepulsionGuide. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetCrossingRepulsionGuide(struct R3World *world, + R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::crossingRepulsionSelfGuide. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetCrossingRepulsionSelfGuide(struct R3World *world, + R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::recoveryPace. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetRecoveryPace(struct R3World *world, R3Real value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapConstraints. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetOverlapConstraints(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapRigid. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetOverlapRigid(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapSkipSelfTangled. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetOverlapSkipSelfTangled(struct R3World *world, + R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapEdgeStandDown. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetOverlapEdgeStandDown(struct R3World *world, + R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapConstraintPace. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetOverlapConstraintPace(struct R3World *world, + R3Real value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapSkinVolume. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetOverlapSkinVolume(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapKeptDepth. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetOverlapKeptDepth(struct R3World *world, R3Real value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapSelfRegions. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetOverlapSelfRegions(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapNormalPush. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetOverlapNormalPush(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapMultiVolume. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetOverlapMultiVolume(struct R3World *world, R3Bool value); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapProgressMargin. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetOverlapProgressMargin(struct R3World *world, + R3Real value); + +#if defined(RAPIER_FEM) +/** + * Set the world setting documented by R3SoftFemParameters::linearTolerance. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3FemSetLinearTolerance(struct R3World *world, R3Real value); +#endif + +#if defined(RAPIER_FEM) +/** + * Set the world setting documented by R3SoftFemParameters::maxLinearIterations. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3FemSetMaxLinearIterations(struct R3World *world, size_t value); +#endif + +#if defined(RAPIER_FEM) +/** + * Set the world setting documented by R3SoftFemParameters::maxDenseDofs. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3FemSetMaxDenseDofs(struct R3World *world, size_t value); +#endif + +/** + * Configures a dedicated pool for this world's parallel work. Zero selects Rayon's default. + * Takes effect on the next step. Reconfiguration must not race with a step or callback. + * Returns R3_UNSUPPORTED in builds without the parallel feature; keeps the previous + * pool when constructing the new one fails. The pool is not included in snapshots. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetNumThreads(struct R3World *world, size_t num_threads); + +/** + * Removes the world's dedicated pool. A parallel build then uses the calling + * context's Rayon pool (normally the global pool), not a single worker. + * Returns R3_UNSUPPORTED in a build without the parallel feature. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3ClearThreadPool(struct R3World *world); + +/** + * Size of the world's dedicated pool, or zero if a parallel build has no dedicated + * pool configured. Returns one for a build without the parallel feature. + * @ingroup worlds + */ +RAPIER_API size_t RAPIER_CALL r3NumThreads(const struct R3World *world); + +/** + * Enable or disable the native pipeline profiling counters. Enabling returns + * R3_UNSUPPORTED if the library was built without the profiler feature. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetCountersEnabled(struct R3World *world, R3Bool enabled); + +/** + * Native engine time of the most recent step, in milliseconds, as in the Rust testbed. + * Enable counters before stepping. Excludes C callbacks outside the step, rendering, + * and dispatch into a dedicated thread pool; remains unchanged while paused. + * @ingroup worlds + */ +RAPIER_API double RAPIER_CALL r3StepTimeMs(const struct R3World *world); + +/** + * Read ONLY trusted legacy rigid-world snapshots from debug_deserialize3.rs, + * produced by the identical Rapier build. This is not a stable interchange format. + * + * Import trusted legacy Rust testbed rigid-state bytes into a new owned world. Release with + * r3FreeWorld; see @ref snapshots. + * @ingroup worlds + */ +RAPIER_API struct R3World *RAPIER_CALL r3DeserializeRigidState(const uint8_t *data, size_t count); + +/** + * Return native default query filter. This POD value owns no resources. + * @ingroup queries + */ +RAPIER_API struct R3QueryFilter RAPIER_CALL r3DefaultQueryFilter(void); + +/** + * Return native default shape cast options. This POD value owns no resources. + * @ingroup queries + */ +RAPIER_API struct R3ShapeCastOptions RAPIER_CALL r3DefaultShapeCastOptions(void); + +/** + * Remove a soft body and its associated simulation objects. Invalidates its handle. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RemoveSoftBody(struct R3SoftBodyHandle handle); + +/** + * Wake the soft body and its rigid proxies. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBody_WakeUp(struct R3SoftBodyHandle handle); + +/** + * Release an owned soft body tear event. NULL is allowed. Do not pass borrowed pointers or free + * the object twice. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3FreeSoftBodyTearEvent(struct R3SoftBodyTearEvent *event); + +/** + * Return the source soft-body handle for this tear event. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyHandle RAPIER_CALL r3SoftBodyTearEvent_SoftBody(const struct R3SoftBodyTearEvent *event); + +/** + * Copy the soft-body handles produced by the tear. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyTearEvent_Bodies(const struct R3SoftBodyTearEvent *event, + struct R3SoftBodyHandle *buffer, + size_t capacity); + +/** + * Return the destination body and particle index for an original particle. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3ParticleDestination RAPIER_CALL r3SoftBodyTearEvent_ParticleDestination(const struct R3SoftBodyTearEvent *event, + uint32_t particle); + +/** + * Flat indices; element arity follows the corresponding Rust event field. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyTearEvent_TornEdges(const struct R3SoftBodyTearEvent *event, + uint32_t *buffer, + size_t capacity); + +/** + * Flat indices; element arity follows the corresponding Rust event field. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyTearEvent_TornCells(const struct R3SoftBodyTearEvent *event, + uint32_t *buffer, + size_t capacity); + +/** + * Flat indices; element arity follows the corresponding Rust event field. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyTearEvent_RemovedEdges(const struct R3SoftBodyTearEvent *event, + uint32_t *buffer, + size_t capacity); + +/** + * Flat indices; element arity follows the corresponding Rust event field. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyTearEvent_SplitParticles(const struct R3SoftBodyTearEvent *event, + uint32_t *buffer, + size_t capacity); + +/** + * Flat indices; element arity follows the corresponding Rust event field. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyTearEvent_InsertedParticles(const struct R3SoftBodyTearEvent *event, + uint32_t *buffer, + size_t capacity); + +/** + * Copy original particle indices belonging to a resulting piece. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyTearEvent_PieceParticles(const struct R3SoftBodyTearEvent *event, + size_t piece_index, + uint32_t *buffer, + size_t capacity); + +/** + * Copy cluster-to-piece and rigid-proxy remapping records. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyTearEvent_Clusters(const struct R3SoftBodyTearEvent *event, + struct R3SoftClusterSplit *buffer, + size_t capacity); + +/** + * Copy impulse-joint remapping records produced by the tear. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyTearEvent_MovedJoints(const struct R3SoftBodyTearEvent *event, + struct R3SoftJointMove *buffer, + size_t capacity); + +/** + * Tear the selected edges and return an owned remapping event. Release it with + * r3FreeSoftBodyTearEvent. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyTearEvent *RAPIER_CALL r3SoftBody_Tear(struct R3SoftBodyHandle handle, + const uint32_t *edges, + size_t edge_count, + const uint32_t *cells, + size_t cell_count); + +/** + * Create a rigid proxy cluster from the supplied particle indices and return its cluster index. + * @ingroup soft_bodies + */ +RAPIER_API +uint32_t RAPIER_CALL r3SoftBody_AddCluster(struct R3SoftBodyHandle handle, + const uint32_t *particles, + size_t count); + +/** + * Remove the selected cluster and its rigid proxy. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_RemoveCluster(struct R3SoftBodyHandle handle, + uint32_t cluster); + +/** + * Optional particle destination after a tear. Missing destinations are normal and set + * found to false; body/index are only written when a destination exists. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3OptionalParticleDestination RAPIER_CALL r3SoftBodyTearEvent_TryParticleDestination(const struct R3SoftBodyTearEvent *event, + uint32_t particle); + +/** + * Cut using DIM points (a segment in 2D, triangle in 3D). A no-op returns a null event. + * The optional owned event must be freed with FreeSoftBodyTearEvent. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyTearEvent *RAPIER_CALL r3CutSoftBody(struct R3SoftBodyHandle handle, + const struct R3Vector *blade); + +/** + * Return volume-meshing settings for the supplied cell size; this POD value requires no + * destructor. + * @ingroup worlds + */ +RAPIER_API struct R3VolumeMeshParameters RAPIER_CALL r3NewVolumeMeshParameters(R3Real cell_size); + +/** + * Return ABI version, dimension, scalar size, and pointer size of the linked library. + * @ingroup errors + */ +RAPIER_API struct R3BuildInfo RAPIER_CALL r3BuildInfo(void); + +/** + * Release version of the loaded C bindings, e.g. "0.35.3+c.2". + * The suffix identifies the C bindings revision for the Rust crate version. + * The UTF-8, NUL-terminated string is borrowed for the library's lifetime; do not free it. + * This release identifier is independent of the ABI compatibility version. + * @ingroup errors + */ +RAPIER_API const char *RAPIER_CALL r3Version(void); + +/** + * Cargo profile of the loaded physics library: "debug" or "release". + * Custom profiles report the corresponding inherited Cargo profile category. + * The UTF-8, NUL-terminated string is borrowed for the library's lifetime; do not free it. + * This is independent of the consumer's build mode and of per-package optimization overrides. + * @ingroup errors + */ +RAPIER_API const char *RAPIER_CALL r3BuildProfile(void); + +/** + * Return profiling, SIMD width, and parallelism of the linked library. + * @ingroup errors + */ +RAPIER_API struct R3BuildFeatures RAPIER_CALL r3BuildFeatures(void); + +/** + * Create an owned heightfield shape from copied samples. 3D samples are column-major, with rows * + * columns entries. Release with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3HeightfieldSharedShape(struct R3RealView heights, + size_t rows, + size_t columns, + struct R3Vector scale); + +/** + * Compute the shape axis-aligned bounds at the supplied world-space pose. + * @ingroup shapes + */ +RAPIER_API +struct R3Aabb RAPIER_CALL r3SharedShape_ComputeAabb(const R3SharedShape *shape, + struct R3Pose pose); + +/** + * Compute local mass properties for the supplied nonnegative density. + * @ingroup shapes + */ +RAPIER_API +struct R3MassProperties RAPIER_CALL r3SharedShape_MassProperties(const R3SharedShape *shape, + R3Real density); + +/** + * Test whether the world-space point lies inside the shape at pose. + * @ingroup shapes + */ +RAPIER_API +R3Bool RAPIER_CALL r3SharedShape_ContainsPoint(const R3SharedShape *shape, + struct R3Pose pose, + struct R3Vector point); + +/** + * Copy current narrow-phase contact pairs, including pairs without active solver contacts. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r3ContactPairs(const struct R3World *world, + struct R3ContactPair *buffer, + size_t capacity); + +/** + * Return the narrow-phase contact pair for two colliders, or report R3_NOT_FOUND. + * @ingroup events + */ +RAPIER_API +struct R3ContactPair RAPIER_CALL r3ContactPair(struct R3ColliderHandle collider1, + struct R3ColliderHandle collider2); + +/** + * Copy current sensor intersection pairs from the narrow phase. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r3IntersectionPairs(const struct R3World *world, + struct R3IntersectionPair *buffer, + size_t capacity); + +/** + * Contact points in collider-local space; normal in world space. Geometric manifolds may be + * recycled. + * For clustered solver impulses use contact pair totals. Soft pairs have no rigid manifolds. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r3ContactPoints(struct R3ColliderHandle collider1, + struct R3ColliderHandle collider2, + struct R3ContactPoint *buffer, + size_t capacity); + +/** + * Copy the articulation generalized velocities in native degree-of-freedom order. + * @see @ref output_buffers + * @ingroup joints + */ +RAPIER_API +size_t RAPIER_CALL r3MultibodyJoint_GeneralizedVelocity(struct R3MultibodyJointHandle handle, + R3Real *buffer, + size_t capacity); + +/** + * Replace articulation generalized velocities; the array length must match its degrees of freedom. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3MultibodyJoint_SetGeneralizedVelocity(struct R3MultibodyJointHandle handle, + const R3Real *values, + size_t count); + +/** + * Check this before passing any dimension/precision-dependent structs across the ABI. + * @ingroup errors + */ +RAPIER_API +R3Status RAPIER_CALL r3CheckAbi(uint32_t version, + uint32_t dimension, + size_t real_size, + size_t vector_size, + size_t pose_size); + +/** + * Return owned local-space rendering geometry; release it with r3FreeShapeMesh. subdivisions + * controls curved-shape resolution. + * @ingroup shapes + */ +RAPIER_API +struct R3ShapeMesh *RAPIER_CALL r3SharedShape_Tessellate(const R3SharedShape *shape, + uint32_t subdivisions); + +/** + * Flat groups of three vertices. Standard output-buffer convention. + * @see @ref output_buffers + * @ingroup shapes + */ +RAPIER_API +size_t RAPIER_CALL r3ShapeMesh_Triangles(const struct R3ShapeMesh *mesh, + struct R3Vector *buffer, + size_t capacity); + +/** + * Flat groups of two vertices. Standard output-buffer convention. + * @see @ref output_buffers + * @ingroup shapes + */ +RAPIER_API +size_t RAPIER_CALL r3ShapeMesh_Lines(const struct R3ShapeMesh *mesh, + struct R3Vector *buffer, + size_t capacity); + +/** + * Release an owned shape mesh. NULL is allowed. Do not pass borrowed pointers or free the object + * twice. + * @ingroup shapes + */ +RAPIER_API R3Status RAPIER_CALL r3FreeShapeMesh(struct R3ShapeMesh *mesh); + +#if defined(RAPIER_DIM3) +/** + * Create an owned round cylinder shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3RoundCylinderSharedShape(R3Real half_height, + R3Real radius, + R3Real border_radius); +#endif + +#if defined(RAPIER_DIM3) +/** + * Tessellate a ball or capsule with independent longitude/latitude subdivision counts. + * Cuboids, cones, cylinders, convex polyhedra, trimeshes, and heightfields are also supported. + * @ingroup shapes + */ +RAPIER_API +struct R3TriMeshData *RAPIER_CALL r3SharedShape_ToTrimesh(const R3SharedShape *shape, + uint32_t ntheta, + uint32_t nphi); +#endif + +#if defined(RAPIER_DIM3) +/** + * Copy vertices. + * @see @ref output_buffers + * @ingroup shapes + */ +RAPIER_API +size_t RAPIER_CALL r3TriMeshData_Vertices(const struct R3TriMeshData *mesh, + struct R3Vector *buffer, + size_t capacity); +#endif + +#if defined(RAPIER_DIM3) +/** + * Flat triangle indices; count and capacity are numbers of u32 entries. + * @see @ref output_buffers + * @ingroup shapes + */ +RAPIER_API +size_t RAPIER_CALL r3TriMeshData_Indices(const struct R3TriMeshData *mesh, + uint32_t *buffer, + size_t capacity); +#endif + +#if defined(RAPIER_DIM3) +/** + * Release an owned tri mesh data. NULL is allowed. Do not pass borrowed pointers or free the + * object twice. + * @ingroup shapes + */ +RAPIER_API R3Status RAPIER_CALL r3FreeTriMeshData(struct R3TriMeshData *mesh); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return native default urdf loader options. This POD value owns no resources. + * @ingroup robotics + */ +RAPIER_API struct R3UrdfLoaderOptions RAPIER_CALL r3DefaultUrdfLoaderOptions(void); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release an owned urdf robot. NULL is allowed. Do not pass borrowed pointers or free the object + * twice. + * @ingroup robotics + */ +RAPIER_API R3Status RAPIER_CALL r3FreeUrdfRobot(struct R3UrdfRobot *object); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Load from a UTF-8 path. Validates options before reading the file. + * Options and their blueprint resources are borrowed through this call; the robot is owned. + * @ingroup robotics + */ +RAPIER_API +struct R3UrdfRobot *RAPIER_CALL r3UrdfRobotFromFile(const char *path, + const struct R3UrdfLoaderOptions *options); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Apply an additional transform to the loaded robot before insertion. + * @ingroup robotics + */ +RAPIER_API +R3Status RAPIER_CALL r3UrdfRobot_AppendTransform(struct R3UrdfRobot *robot, + struct R3Pose transform); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release an owned urdf robot handles. NULL is allowed. Do not pass borrowed pointers or free the + * object twice. + * @ingroup robotics + */ +RAPIER_API R3Status RAPIER_CALL r3FreeUrdfRobotHandles(struct R3UrdfRobotHandles *handles); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Inserts a clone; the source robot remains owned by the caller. Returns owned handles. + * @ingroup robotics + */ +RAPIER_API +struct R3UrdfRobotHandles *RAPIER_CALL r3UrdfRobot_InsertUsingImpulseJoints(struct R3World *world, + const struct R3UrdfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Inserts a clone; the source robot remains owned by the caller. Returns owned handles. + * @ingroup robotics + */ +RAPIER_API +struct R3UrdfRobotHandles *RAPIER_CALL r3UrdfRobot_InsertUsingMultibodyJoints(struct R3World *world, + const struct R3UrdfRobot *robot, + uint8_t options); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Body handles in source order; absent MJCF bodies have invalid handles. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r3UrdfRobotHandles_Bodies(const struct R3UrdfRobotHandles *handles, + struct R3RigidBodyHandle *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return native default mjcf loader options. This POD value owns no resources. + * @ingroup robotics + */ +RAPIER_API struct R3MjcfLoaderOptions RAPIER_CALL r3DefaultMjcfLoaderOptions(void); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release an owned mjcf robot. NULL is allowed. Do not pass borrowed pointers or free the object + * twice. + * @ingroup robotics + */ +RAPIER_API R3Status RAPIER_CALL r3FreeMjcfRobot(struct R3MjcfRobot *object); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Load from a UTF-8 path. Validates options before reading the file. + * Options and their blueprint resources are borrowed through this call; the robot is owned. + * @ingroup robotics + */ +RAPIER_API +struct R3MjcfRobot *RAPIER_CALL r3MjcfRobotFromFile(const char *path, + const struct R3MjcfLoaderOptions *options); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Apply an additional transform to the loaded robot before insertion. + * @ingroup robotics + */ +RAPIER_API +R3Status RAPIER_CALL r3MjcfRobot_AppendTransform(struct R3MjcfRobot *robot, + struct R3Pose transform); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release an owned mjcf robot handles. NULL is allowed. Do not pass borrowed pointers or free the + * object twice. + * @ingroup robotics + */ +RAPIER_API R3Status RAPIER_CALL r3FreeMjcfRobotHandles(struct R3MjcfRobotHandles *handles); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Inserts a clone; the source robot remains owned by the caller. Returns owned handles. + * @ingroup robotics + */ +RAPIER_API +struct R3MjcfRobotHandles *RAPIER_CALL r3MjcfRobot_InsertUsingImpulseJoints(struct R3World *world, + const struct R3MjcfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Inserts a clone; the source robot remains owned by the caller. Returns owned handles. + * @ingroup robotics + */ +RAPIER_API +struct R3MjcfRobotHandles *RAPIER_CALL r3MjcfRobot_InsertUsingMultibodyJoints(struct R3World *world, + const struct R3MjcfRobot *robot, + uint8_t options); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Body handles in source order; absent MJCF bodies have invalid handles. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r3MjcfRobotHandles_Bodies(const struct R3MjcfRobotHandles *handles, + struct R3RigidBodyHandle *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Resolved model gravity before the caller chooses a world convention. + * @ingroup robotics + */ +RAPIER_API struct R3Vector RAPIER_CALL r3MjcfRobot_Gravity(const struct R3MjcfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the number of source MJCF bodies. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r3MjcfRobot_BodyCount(const struct R3MjcfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the collider count for a source body index. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r3MjcfRobot_BodyColliderCount(const struct R3MjcfRobot *robot, size_t body); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Set collision groups on a collider in the loaded robot, before insertion. + * @ingroup robotics + */ +RAPIER_API +R3Status RAPIER_CALL r3MjcfRobot_SetBodyColliderCollisionGroups(struct R3MjcfRobot *robot, + size_t body, + size_t collider, + struct R3InteractionGroups groups); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the number of imported keyframes. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r3MjcfRobot_KeyframeCount(const struct R3MjcfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copies a NUL-terminated UTF-8 name. Count includes NUL; unnamed keys return an empty string. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r3MjcfRobot_KeyframeName(const struct R3MjcfRobot *robot, + size_t key, + char *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Append a keyframe from the source MJCF model to the loaded robot. + * @ingroup robotics + */ +RAPIER_API +R3Status RAPIER_CALL r3MjcfRobot_AppendKeyframe(struct R3MjcfRobot *robot, + const struct R3MjcfRobot *source, + size_t key); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copy actuator controls for the selected keyframe. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r3MjcfRobot_KeyframeControls(const struct R3MjcfRobot *robot, + size_t key, + R3Real *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the number of imported actuators. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r3MjcfRobotHandles_ActuatorCount(const struct R3MjcfRobotHandles *handles); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Apply the selected keyframe to the inserted robot. + * @ingroup robotics + */ +RAPIER_API +R3Status RAPIER_CALL r3MjcfRobotHandles_ApplyKeyframe(const struct R3MjcfRobotHandles *handles, + const struct R3MjcfRobot *robot, + size_t key); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Apply actuator controls with per-actuator scaling to the inserted robot. + * @ingroup robotics + */ +RAPIER_API +R3Status RAPIER_CALL r3MjcfRobotHandles_ApplyControlsScaled(const struct R3MjcfRobotHandles *handles, + const R3Real *controls, + size_t count, + R3Real gain); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the number of visual meshes for a source body. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r3MjcfRobot_BodyVisualCount(const struct R3MjcfRobot *robot, size_t body); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Borrow a visual mesh by body/visual index. Valid until the robot is freed or its storage + * changes; never free this pointer. + * @ingroup robotics + */ +RAPIER_API +const R3MjcfVisualMesh *RAPIER_CALL r3MjcfRobot_BodyVisual(const struct R3MjcfRobot *robot, + size_t body, + size_t visual); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return a copy of visual pose, color, material, and geometry-kind flags. + * @ingroup robotics + */ +RAPIER_API struct R3MjcfVisualMeshInfo RAPIER_CALL r3MjcfVisualMesh_Info(const R3MjcfVisualMesh *visual); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Returns an owned shared shape reference. + * Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. + * @ingroup robotics + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3MjcfVisualMesh_CloneShape(const R3MjcfVisualMesh *visual); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copies flattened pairs of per-vertex UV coordinates. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r3MjcfVisualMesh_Uvs(const R3MjcfVisualMesh *visual, + float *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copies flattened triples of per-vertex normals. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r3MjcfVisualMesh_Normals(const R3MjcfVisualMesh *visual, + float *buffer, + size_t capacity); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Copies a NUL-terminated texture path, or an empty string for untextured meshes. + * @see @ref output_buffers + * @ingroup robotics + */ +RAPIER_API +size_t RAPIER_CALL r3MjcfVisualMesh_Texture(const R3MjcfVisualMesh *visual, + char *buffer, + size_t capacity); +#endif + +/** + * Return the rigid body world-space pose. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3Pose RAPIER_CALL r3RigidBody_Position(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body world-space translation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3Vector RAPIER_CALL r3RigidBody_Translation(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body world-space linear velocity. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3Vector RAPIER_CALL r3RigidBody_Linvel(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body world-space angular velocity (radians per second). + * @ingroup rigid_bodies + */ +RAPIER_API R3AngVector RAPIER_CALL r3RigidBody_Angvel(struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is sleeping. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_IsSleeping(struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is enabled. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_IsEnabled(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body application-owned 128-bit user value. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3UserData RAPIER_CALL r3RigidBody_UserData(struct R3RigidBodyHandle handle); + +/** + * Set the rigid body world-space pose. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetPosition(struct R3RigidBodyHandle handle, + struct R3Pose value, + R3Bool wake_up); + +/** + * Set the rigid body world-space translation. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetTranslation(struct R3RigidBodyHandle handle, + struct R3Vector value, + R3Bool wake_up); + +/** + * Set the rigid body world-space linear velocity. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetLinvel(struct R3RigidBodyHandle handle, + struct R3Vector value, + R3Bool wake_up); + +/** + * Set the rigid body world-space angular velocity (radians per second). + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetAngvel(struct R3RigidBodyHandle handle, + R3AngVector value, + R3Bool wake_up); + +/** + * Set the rigid body next kinematic world-space pose. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetNextKinematicPosition(struct R3RigidBodyHandle handle, + struct R3Pose value); + +/** + * Set the rigid body next kinematic world-space translation. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetNextKinematicTranslation(struct R3RigidBodyHandle handle, + struct R3Vector value); + +/** + * Set the rigid body gravity multiplier. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetGravityScale(struct R3RigidBodyHandle handle, + R3Real value, + R3Bool wake_up); + +/** + * Set the rigid body linear damping coefficient. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetLinearDamping(struct R3RigidBodyHandle handle, + R3Real value); + +/** + * Set the rigid body angular damping coefficient. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetAngularDamping(struct R3RigidBodyHandle handle, + R3Real value); + +/** + * Enable or disable the rigid body. + * @ingroup rigid_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RigidBody_SetEnabled(struct R3RigidBodyHandle handle, R3Bool value); + +/** + * Set the rigid body application-owned 128-bit user value. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetUserData(struct R3RigidBodyHandle handle, + struct R3UserData value); + +/** + * Apply a world-space linear impulse. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_ApplyImpulse(struct R3RigidBodyHandle handle, + struct R3Vector value, + R3Bool wake_up); + +/** + * Apply a world-space impulse at a world-space point. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_ApplyImpulseAtPoint(struct R3RigidBodyHandle handle, + struct R3Vector value, + struct R3Vector point, + R3Bool wake_up); + +/** + * Accumulate a world-space force; it persists until reset. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_AddForce(struct R3RigidBodyHandle handle, + struct R3Vector value, + R3Bool wake_up); + +/** + * Clear accumulated user forces. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RigidBody_ResetForces(struct R3RigidBodyHandle handle, R3Bool wake_up); + +/** + * Put the body to sleep. + * @ingroup rigid_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RigidBody_Sleep(struct R3RigidBodyHandle handle); + +/** + * Return the collider world-space pose. + * @ingroup colliders + */ +RAPIER_API struct R3Pose RAPIER_CALL r3Collider_Position(struct R3ColliderHandle handle); + +/** + * Return the collider world-space translation. + * @ingroup colliders + */ +RAPIER_API struct R3Vector RAPIER_CALL r3Collider_Translation(struct R3ColliderHandle handle); + +/** + * Return the collider friction coefficient. + * @ingroup colliders + */ +RAPIER_API R3Real RAPIER_CALL r3Collider_Friction(struct R3ColliderHandle handle); + +/** + * Return the collider restitution coefficient. + * @ingroup colliders + */ +RAPIER_API R3Real RAPIER_CALL r3Collider_Restitution(struct R3ColliderHandle handle); + +/** + * Return whether the collider is a sensor (detects overlaps without contact forces). + * @ingroup colliders + */ +RAPIER_API R3Bool RAPIER_CALL r3Collider_IsSensor(struct R3ColliderHandle handle); + +/** + * Return the parent body handle, or an invalid handle with OK status for a standalone collider. + * @ingroup colliders + */ +RAPIER_API struct R3RigidBodyHandle RAPIER_CALL r3Collider_Parent(struct R3ColliderHandle handle); + +/** + * Set the collider world-space pose. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetPosition(struct R3ColliderHandle handle, + struct R3Pose value); + +/** + * Set the collider world-space translation. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetTranslation(struct R3ColliderHandle handle, + struct R3Vector value); + +/** + * Set the collider friction coefficient. + * @ingroup colliders + */ +RAPIER_API R3Status RAPIER_CALL r3Collider_SetFriction(struct R3ColliderHandle handle, R3Real value); + +/** + * Set the collider restitution coefficient. + * @ingroup colliders + */ +RAPIER_API R3Status RAPIER_CALL r3Collider_SetRestitution(struct R3ColliderHandle handle, R3Real value); + +/** + * Enable or disable a sensor (detects overlaps without contact forces) for the collider. + * @ingroup colliders + */ +RAPIER_API R3Status RAPIER_CALL r3Collider_SetSensor(struct R3ColliderHandle handle, R3Bool value); + +/** + * Set the collider collision filtering groups. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetCollisionGroups(struct R3ColliderHandle handle, + struct R3InteractionGroups value); + +/** + * Set the collider application-owned 128-bit user value. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetUserData(struct R3ColliderHandle handle, + struct R3UserData value); + +/** + * Return the world-space position of the indexed particle. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3SoftBody_ParticlePosition(struct R3SoftBodyHandle handle, + size_t index); + +/** + * Copy world-space particle positions. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_ParticlePositions(struct R3SoftBodyHandle handle, + struct R3Vector *buffer, + size_t capacity); + +/** + * Return a copy of the soft body material parameters. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SoftBodyMaterial RAPIER_CALL r3SoftBody_Material(struct R3SoftBodyHandle handle); + +/** + * Set the world-space position of the indexed particle. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetParticlePosition(struct R3SoftBodyHandle handle, + size_t index, + struct R3Vector value); + +/** + * Copy material parameters into the soft body. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetMaterial(struct R3SoftBodyHandle handle, + const struct R3SoftBodyMaterial *data); + +/** + * Add a world-space force to the indexed particle. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_AddParticleForce(struct R3SoftBodyHandle handle, + size_t index, + struct R3Vector value, + R3Bool wake_up); + +/** + * Copies states in the same order as handles, without allocating temporary storage. + * All handles are validated before writing. On INVALID_HANDLE outputs are unchanged. + * NULL/0 is a size query. BUFFER_TOO_SMALL returns the required count and leaves states untouched. + * @ingroup rigid_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3RigidBodyReadStates(const struct R3World *world, + const struct R3RigidBodyHandle *handles, + size_t handle_count, + struct R3RigidBodyState *states, + size_t capacity); + +/** + * Copies joint configuration without returning a borrowed joint pointer. + * @ingroup joints + */ +RAPIER_API struct R3JointDesc RAPIER_CALL r3ImpulseJoint_Desc(struct R3ImpulseJointHandle handle); + +/** + * Replaces configuration after validation, resetting cached limit/motor impulses. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetDesc(struct R3ImpulseJointHandle handle, + const struct R3JointDesc *desc, + R3Bool wake_up); + +/** + * Replace the shape geometry with a borrowed tri mesh. Counts are elements. + * Copies no arrays. Invalid view metadata leaves the description unchanged. + * Geometry and flags are validated when the description is built or inserted. + * @ingroup shapes + */ +RAPIER_API +R3Status RAPIER_CALL r3ShapeDesc_SetTrimesh(struct R3ShapeDesc *desc, + struct R3VectorView vertices, + struct R3TriangleView indices, + uint32_t flags); + +/** + * Replace the shape geometry with a borrowed polyline. Counts are elements. + * Copies no arrays. Invalid view metadata leaves the description unchanged. + * Geometry and flags are validated when the description is built or inserted. + * @ingroup shapes + */ +RAPIER_API +R3Status RAPIER_CALL r3ShapeDesc_SetPolyline(struct R3ShapeDesc *desc, + struct R3VectorView vertices, + struct R3EdgeView indices, + uint32_t flags); + +/** + * Replace the shape geometry with a borrowed convex hull point cloud. + * @ingroup shapes + */ +RAPIER_API +R3Status RAPIER_CALL r3ShapeDesc_SetConvexHull(struct R3ShapeDesc *desc, + struct R3VectorView vertices); + +/** + * Select an explicit particle recipe and borrow its positions. Other fields are preserved. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetParticles(struct R3SoftBodyDesc *desc, + struct R3VectorView positions); + +/** + * Select a surface recipe and borrow its vertices and elements. Other fields are preserved. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetSurfaceMesh(struct R3SoftBodyDesc *desc, + struct R3VectorView vertices, + R3SurfaceElementView elements); + +/** + * Borrow skin geometry. Other fields, including skinCollision, are preserved. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetSkin(struct R3SoftBodyDesc *desc, + struct R3VectorView vertices, + R3SurfaceElementView elements); + +/** + * Borrow masses; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetMasses(struct R3SoftBodyDesc *desc, + struct R3RealView view); + +/** + * Borrow pinned particles; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetPinnedParticles(struct R3SoftBodyDesc *desc, + struct R3IndexView view); + +/** + * Borrow edges; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetEdges(struct R3SoftBodyDesc *desc, + struct R3EdgeView view); + +/** + * Borrow bend edges; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetBendEdges(struct R3SoftBodyDesc *desc, + struct R3EdgeView view); + +/** + * Borrow cells; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBodyDesc_SetCells(struct R3SoftBodyDesc *desc, R3CellView view); + +/** + * Borrow surface; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetSurface(struct R3SoftBodyDesc *desc, + R3SurfaceElementView view); + +/** + * Borrow tension only edges; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetTensionOnlyEdges(struct R3SoftBodyDesc *desc, + struct R3IndexView view); + +#if defined(RAPIER_DIM3) +/** + * Borrow dihedrals; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetDihedrals(struct R3SoftBodyDesc *desc, + struct R3DihedralView view); +#endif + +#if defined(RAPIER_DIM3) +/** + * Borrow wire; preserve all other fields. No allocation or element reads. + * Zero counts retain the recipe's generated defaults at insertion, as with directly assigned + * views. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetWire(struct R3SoftBodyDesc *desc, + struct R3EdgeView view); +#endif + +/** + * Return a rounded box description; half_extents exclude the added border_radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API +struct R3ColliderDesc RAPIER_CALL r3RoundCuboidColliderDesc(struct R3Vector half_extents, + R3Real border_radius); + +/** + * Return a capsule description with segment endpoints a/b and the supplied radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API +struct R3ColliderDesc RAPIER_CALL r3CapsuleColliderDesc(struct R3Vector a, + struct R3Vector b, + R3Real radius); + +/** + * Return a segment description with endpoints a and b. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R3ColliderDesc RAPIER_CALL r3SegmentColliderDesc(struct R3Vector a, struct R3Vector b); + +/** + * Return a triangle description with vertices a, b, and c. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API +struct R3ColliderDesc RAPIER_CALL r3TriangleColliderDesc(struct R3Vector a, + struct R3Vector b, + struct R3Vector c); + +/** + * Return a half-space description bounded by a plane through the origin; normal points outward. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R3ColliderDesc RAPIER_CALL r3HalfspaceColliderDesc(struct R3Vector normal); + +#if defined(RAPIER_DIM3) +/** + * Return a Y-aligned cylinder description with the supplied half-height and radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R3ColliderDesc RAPIER_CALL r3CylinderColliderDesc(R3Real half_height, R3Real radius); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a Y-aligned cone description with the supplied half-height and base radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R3ColliderDesc RAPIER_CALL r3ConeColliderDesc(R3Real half_height, R3Real radius); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a rounded Y-aligned cylinder description; dimensions exclude border_radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API +struct R3ColliderDesc RAPIER_CALL r3RoundCylinderColliderDesc(R3Real half_height, + R3Real radius, + R3Real border_radius); +#endif + +/** + * Return a X-aligned capsule description; half_height is half the segment length, excluding caps. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R3ColliderDesc RAPIER_CALL r3CapsuleXColliderDesc(R3Real half_height, R3Real radius); + +/** + * Return a Y-aligned capsule description; half_height is half the segment length, excluding caps. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R3ColliderDesc RAPIER_CALL r3CapsuleYColliderDesc(R3Real half_height, R3Real radius); + +#if defined(RAPIER_DIM3) +/** + * Return a Z-aligned capsule description; half_height is half the segment length, excluding caps. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API struct R3ColliderDesc RAPIER_CALL r3CapsuleZColliderDesc(R3Real half_height, R3Real radius); +#endif + +/** + * Return a rope recipe with particles evenly spaced from a to b, including both endpoints. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyDesc RAPIER_CALL r3RopeSoftBodyDesc(struct R3Vector a, + struct R3Vector b, + size_t particles); + +#if defined(RAPIER_DIM2) +/** + * Return a solid rectangle recipe on an nx by ny particle grid. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyDesc RAPIER_CALL r3GridSoftBodyDesc(struct R3Vector center, + struct R3Vector half_extents, + size_t nx, + size_t ny); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a solid box recipe on an nx by ny by nz particle grid, subdivided into tetrahedra. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyDesc RAPIER_CALL r3CuboidSoftBodyDesc(struct R3Vector center, + struct R3Vector half_extents, + size_t nx, + size_t ny, + size_t nz); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a cloth recipe with nu by nv particles at origin + i * du + j * dv. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyDesc RAPIER_CALL r3ClothSoftBodyDesc(struct R3Vector origin, + struct R3Vector du, + struct R3Vector dv, + size_t nu, + size_t nv); +#endif + +#if defined(RAPIER_DIM2) +/** + * Return a closed regular polygon recipe with the specified boundary particle count and area + * preservation. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyDesc RAPIER_CALL r3DiskSoftBodyDesc(struct R3Vector center, + R3Real radius, + size_t particles); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a hollow icosphere recipe with the specified refinement levels and volume preservation. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyDesc RAPIER_CALL r3SphereSoftBodyDesc(struct R3Vector center, + R3Real radius, + uint32_t subdivisions); +#endif + +#if defined(RAPIER_DIM3) +/** + * Return a cloth tube recipe from origin to origin + axis with num_along rings of num_around + * particles; radius varies linearly between the ends. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyDesc RAPIER_CALL r3ClothTubeSoftBodyDesc(struct R3Vector origin, + struct R3Vector axis, + R3Real radius_start, + R3Real radius_end, + size_t num_around, + size_t num_along); +#endif + +/** + * Initializes a borrowed meshing recipe. Mesh generation happens on preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyDesc RAPIER_CALL r3VolumetricSoftBodyDesc(struct R3VectorView vertices, + R3SurfaceElementView surface, + struct R3VolumeMeshParameters parameters); + +/** + * Returns a material with the same softness for each constraint family. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyMaterial RAPIER_CALL r3UniformSoftBodyMaterial(struct R3SpringCoefficients value); + +/** + * Copies generated particle positions into caller-owned storage; no persistent builder. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyDesc_ParticlePositions(const struct R3SoftBodyDesc *desc, + struct R3Vector *buffer, + size_t capacity); + +/** + * Copies generated cell indices into caller-owned storage. Counts scalar indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyDesc_CellIndices(const struct R3SoftBodyDesc *desc, + uint32_t *buffer, + size_t capacity); + +/** + * Return a process-local geometry identity for caching, not a serializable ID. Keep a shared-shape + * clone alive while using it as a cache key. + * @ingroup shapes + */ +RAPIER_API size_t RAPIER_CALL r3Collider_ShapeIdentity(struct R3ColliderHandle handle); + +/** + * Return the soft body particle count. + * @ingroup soft_bodies + */ +RAPIER_API size_t RAPIER_CALL r3SoftBody_NumParticles(struct R3SoftBodyHandle handle); + +/** + * Return a counter that changes when particle connectivity changes; use it to invalidate mesh + * caches. + * @ingroup soft_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r3SoftBody_TopologyVersion(struct R3SoftBodyHandle handle); + +/** + * Return the soft body mass. + * @ingroup soft_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3SoftBody_Mass(struct R3SoftBodyHandle handle); + +/** + * Return the soft body current volume. + * @ingroup soft_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3SoftBody_Volume(struct R3SoftBodyHandle handle); + +/** + * Return the soft body undeformed volume. + * @ingroup soft_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3SoftBody_RestVolume(struct R3SoftBodyHandle handle); + +/** + * Return the soft body target volume multiplier. + * @ingroup soft_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3SoftBody_VolumeFactor(struct R3SoftBodyHandle handle); + +/** + * Return the soft body world-space center of mass. + * @ingroup soft_bodies + */ +RAPIER_API struct R3Vector RAPIER_CALL r3SoftBody_CenterOfMass(struct R3SoftBodyHandle handle); + +/** + * Return the soft body root rigid-proxy handle. + * @ingroup soft_bodies + */ +RAPIER_API struct R3RigidBodyHandle RAPIER_CALL r3SoftBody_RootBody(struct R3SoftBodyHandle handle); + +/** + * Return whether the soft body is enabled. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3SoftBody_IsEnabled(struct R3SoftBodyHandle handle); + +/** + * Return whether the soft body is sleeping. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3SoftBody_IsSleeping(struct R3SoftBodyHandle handle); + +/** + * Copy world-space particle velocities. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_ParticleVelocities(struct R3SoftBodyHandle handle, + struct R3Vector *buffer, + size_t capacity); + +/** + * Copy flattened edge vertex indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_Edges(struct R3SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Copy flattened cell vertex indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_Cells(struct R3SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Copy flattened boundary element indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_Boundary(struct R3SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Copy piece identifiers. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_Pieces(struct R3SoftBodyHandle handle, + struct R3SoftBodyHandle *buffer, + size_t capacity); + +/** + * Set the soft body particle world-space velocity. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetParticleVelocity(struct R3SoftBodyHandle handle, + size_t index, + struct R3Vector value); + +/** + * Set the next world-space target position of a pinned particle. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetParticleKinematicTarget(struct R3SoftBodyHandle handle, + size_t index, + struct R3Vector value); + +/** + * Enable or disable pinning the particle for the soft body. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetParticlePinned(struct R3SoftBodyHandle handle, + size_t index, + R3Bool value); + +/** + * Apply a world-space impulse to one particle. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_ApplyParticleImpulse(struct R3SoftBodyHandle handle, + size_t index, + struct R3Vector value, + R3Bool wake_up); + +/** + * Accumulate a world-space force; it persists until reset. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_AddForce(struct R3SoftBodyHandle handle, + struct R3Vector value, + R3Bool wake_up); + +/** + * Apply a world-space linear impulse. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_ApplyImpulse(struct R3SoftBodyHandle handle, + struct R3Vector value, + R3Bool wake_up); + +/** + * Clear accumulated user forces. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBody_ResetForces(struct R3SoftBodyHandle handle, R3Bool wake_up); + +/** + * Enable or disable the soft body. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBody_SetEnabled(struct R3SoftBodyHandle handle, R3Bool value); + +/** + * Set the soft body target volume multiplier. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetVolumeFactor(struct R3SoftBodyHandle handle, + R3Real value); + +/** + * Attach a particle to a rigid body at the supplied body-local anchor. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_AttachParticle(struct R3SoftBodyHandle handle, + size_t index, + struct R3RigidBodyHandle rigid_body); + +/** + * Remove a particle attachment to a rigid body. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBody_DetachParticle(struct R3SoftBodyHandle handle, size_t index); + +/** + * Copy cluster indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_Clusters(struct R3SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Return the rigid proxy for the selected cluster. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3RigidBodyHandle RAPIER_CALL r3SoftBody_ClusterProxy(struct R3SoftBodyHandle handle, + uint32_t cluster); + +/** + * Copy particle indices for a cluster. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_ClusterParticles(struct R3SoftBodyHandle handle, + uint32_t cluster, + uint32_t *buffer, + size_t capacity); + +/** + * Enable or disable pinning the cluster for the soft body. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetClusterPinned(struct R3SoftBodyHandle handle, + uint32_t cluster, + R3Bool value); + +/** + * Set the next world-space target pose of a pinned cluster. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetClusterKinematicTarget(struct R3SoftBodyHandle handle, + uint32_t cluster, + struct R3Pose value); + +/** + * Enable or disable using cluster shape matching for the soft body. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetClusterShapeMatchingEnabled(struct R3SoftBodyHandle handle, + uint32_t cluster, + R3Bool value); + +/** + * Set the soft body cluster shape-matching stiffness multiplier. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetClusterStiffnessScale(struct R3SoftBodyHandle handle, + uint32_t cluster, + R3Real value); + +/** + * Set the soft body cluster tear-resistance multiplier. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetClusterTearResistance(struct R3SoftBodyHandle handle, + uint32_t cluster, + R3Real value); + +/** + * Copy collision mesh metadata. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_Meshes(struct R3SoftBodyHandle handle, + struct R3SoftMeshInfo *buffer, + size_t capacity); + +/** + * Copy world-space vertices for a mesh ID. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_MeshVerticesById(struct R3SoftBodyHandle handle, + struct R3SoftMeshId id, + struct R3Vector *buffer, + size_t capacity); + +/** + * Copy flattened indices for a mesh ID. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_MeshIndicesById(struct R3SoftBodyHandle handle, + struct R3SoftMeshId id, + uint32_t *buffer, + size_t capacity); + +/** + * Copy collision mesh collider handles. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_MeshColliders(struct R3SoftBodyHandle handle, + struct R3ColliderHandle *buffer, + size_t capacity); + +/** + * Copy world-space collision mesh vertices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_MeshVertices(struct R3SoftBodyHandle handle, + struct R3ColliderHandle collider, + struct R3Vector *buffer, + size_t capacity); + +/** + * Copy flattened collision mesh indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_MeshIndices(struct R3SoftBodyHandle handle, + struct R3ColliderHandle collider, + uint32_t *buffer, + size_t capacity); + +/** + * Return indices per collision-mesh element (2 for segments, 3 for triangles). + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBody_MeshArity(struct R3SoftBodyHandle handle, + struct R3ColliderHandle collider); + +/** + * Return the selected collision mesh topology revision for cache invalidation. + * @ingroup soft_bodies + */ +RAPIER_API +uint32_t RAPIER_CALL r3SoftBody_MeshTopologyVersion(struct R3SoftBodyHandle handle, + struct R3ColliderHandle collider); + +#if defined(RAPIER_FEM) +/** + * Set the soft body soft solver kind (R3_SOFT_SOLVER_*). + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBody_SetSolver(struct R3SoftBodyHandle handle, uint32_t solver); +#endif + +/** + * Set the soft body cluster shape-matching target pose. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetClusterShapeMatchingTarget(struct R3SoftBodyHandle handle, + uint32_t cluster, + const struct R3Pose *target); + +/** + * Set the soft body edge tear-resistance multiplier. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetEdgeTearResistance(struct R3SoftBodyHandle handle, + size_t index, + R3Real resistance); + +/** + * Return whether the selected collision mesh is closed. + * @ingroup soft_bodies + */ +RAPIER_API +R3Bool RAPIER_CALL r3SoftBody_MeshIsClosed(struct R3SoftBodyHandle handle, + struct R3ColliderHandle collider); + +/** + * Set the rigid body local mass properties added to collider contributions. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetAdditionalMassProperties(struct R3RigidBodyHandle handle, + struct R3MassProperties properties, + R3Bool wake_up); + +/** + * Recompute body mass and inertia from attached colliders and additional mass properties. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_RecomputeMassPropertiesFromColliders(struct R3RigidBodyHandle handle); + +/** + * Set the collider local mass properties. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetMassProperties(struct R3ColliderHandle handle, + struct R3MassProperties properties); + +/** + * Return the collider local mass properties. + * @ingroup colliders + */ +RAPIER_API struct R3MassProperties RAPIER_CALL r3Collider_MassProperties(struct R3ColliderHandle handle); + +/** + * Set the rigid body translation/rotation lock bitmask. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetLockedAxes(struct R3RigidBodyHandle handle, + uint8_t axes, + R3Bool wake_up); + +/** + * Return the rigid body translation/rotation lock bitmask. + * @ingroup rigid_bodies + */ +RAPIER_API uint8_t RAPIER_CALL r3RigidBody_LockedAxes(struct R3RigidBodyHandle handle); + +/** + * Return whether the collider is a voxel shape. + * @ingroup colliders + */ +RAPIER_API R3Bool RAPIER_CALL r3Collider_IsVoxels(struct R3ColliderHandle handle); + +/** + * Return voxel information at a flat index; found = 0 if absent. + * @ingroup colliders + */ +RAPIER_API +struct R3VoxelQuery RAPIER_CALL r3Collider_VoxelAtFlatId(struct R3ColliderHandle handle, + uint32_t id); + +/** + * Fill or clear the voxel at key; the collider must have a voxel shape. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetVoxel(struct R3ColliderHandle handle, + struct R3VoxelKey key, + R3Bool filled); + +/** + * Return the rigid body next kinematic world-space pose. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3Pose RAPIER_CALL r3RigidBody_NextPosition(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body world-space rotation. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3Rotation RAPIER_CALL r3RigidBody_Rotation(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body world-space center of mass. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3Vector RAPIER_CALL r3RigidBody_CenterOfMass(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body body-local center of mass. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3Vector RAPIER_CALL r3RigidBody_LocalCenterOfMass(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body accumulated user-applied world-space force. + * @ingroup rigid_bodies + */ +RAPIER_API struct R3Vector RAPIER_CALL r3RigidBody_UserForce(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body accumulated user-applied world-space torque. + * @ingroup rigid_bodies + */ +RAPIER_API R3AngVector RAPIER_CALL r3RigidBody_UserTorque(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body body type (R3_DYNAMIC, R3_FIXED, or a kinematic kind). + * @ingroup rigid_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r3RigidBody_BodyType(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body mass. + * @ingroup rigid_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3RigidBody_Mass(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body gravity multiplier. + * @ingroup rigid_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3RigidBody_GravityScale(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body linear damping coefficient. + * @ingroup rigid_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3RigidBody_LinearDamping(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body angular damping coefficient. + * @ingroup rigid_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3RigidBody_AngularDamping(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body kinetic energy. + * @ingroup rigid_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3RigidBody_KineticEnergy(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body soft-CCD prediction distance. + * @ingroup soft_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3RigidBody_SoftCcdPrediction(struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is using continuous collision detection. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_IsCcdEnabled(struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is dynamic. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_IsDynamic(struct R3RigidBodyHandle handle); + +/** + * Return the associated soft-body handle, or an invalid handle if this is not a soft proxy. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SoftBodyHandle RAPIER_CALL r3RigidBody_SoftBody(struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is a soft-body proxy. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_IsSoftFrame(struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is fixed. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_IsFixed(struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is kinematic. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_IsKinematic(struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is moving. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_IsMoving(struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is currently using CCD for its motion. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_IsCcdActive(struct R3RigidBodyHandle handle); + +/** + * Set the rigid body world-space rotation. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetRotation(struct R3RigidBodyHandle handle, + struct R3Rotation value, + R3Bool wake_up); + +/** + * Set the rigid body body type (R3_DYNAMIC, R3_FIXED, or a kinematic kind). + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetBodyType(struct R3RigidBodyHandle handle, + uint32_t value, + R3Bool wake_up); + +/** + * Set the rigid body next kinematic world-space rotation. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetNextKinematicRotation(struct R3RigidBodyHandle handle, + struct R3Rotation value); + +/** + * Set the rigid body mass added to collider contributions. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetAdditionalMass(struct R3RigidBodyHandle handle, + R3Real value, + R3Bool wake_up); + +/** + * Set the rigid body soft-CCD prediction distance. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetSoftCcdPrediction(struct R3RigidBodyHandle handle, + R3Real value); + +/** + * Enable or disable using continuous collision detection for the rigid body. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetCcdEnabled(struct R3RigidBodyHandle handle, + R3Bool value); + +/** + * Enable or disable locking translation for the rigid body. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetTranslationsLocked(struct R3RigidBodyHandle handle, + R3Bool value, + R3Bool wake_up); + +/** + * Enable or disable locking rotation for the rigid body. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetRotationsLocked(struct R3RigidBodyHandle handle, + R3Bool value, + R3Bool wake_up); + +/** + * Set the rigid body signed dominance group. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetDominanceGroup(struct R3RigidBodyHandle handle, + int8_t value); + +/** + * Set the rigid body additional solver iterations for connected bodies. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetAdditionalSolverIterations(struct R3RigidBodyHandle handle, + size_t value); + +/** + * Set the rigid body additional PGS iterations. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetAdditionalPgsIterations(struct R3RigidBodyHandle handle, + size_t value); + +/** + * Accumulate a world-space torque; it persists until reset. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_AddTorque(struct R3RigidBodyHandle handle, + R3AngVector value, + R3Bool wake_up); + +/** + * Apply a world-space angular impulse. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_ApplyTorqueImpulse(struct R3RigidBodyHandle handle, + R3AngVector value, + R3Bool wake_up); + +/** + * Accumulate a world-space force applied at a world-space point. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_AddForceAtPoint(struct R3RigidBodyHandle handle, + struct R3Vector value, + struct R3Vector point, + R3Bool wake_up); + +/** + * Clear accumulated user torques. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_ResetTorques(struct R3RigidBodyHandle handle, + R3Bool wake_up); + +/** + * Return world-space velocity at a world-space point, including angular motion. + * @ingroup rigid_bodies + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3RigidBody_VelocityAtPoint(struct R3RigidBodyHandle handle, + struct R3Vector point); + +/** + * Copy attached collider handles. + * @see @ref output_buffers + * @ingroup rigid_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3RigidBody_Colliders(struct R3RigidBodyHandle handle, + struct R3ColliderHandle *buffer, + size_t capacity); + +#if defined(RAPIER_DIM3) +/** + * Return whether the rigid body is using gyroscopic forces. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_GyroscopicForcesEnabled(struct R3RigidBodyHandle handle); +#endif + +#if defined(RAPIER_DIM3) +/** + * Enable or disable using gyroscopic forces for the rigid body. + * @ingroup rigid_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RigidBody_SetGyroscopicForcesEnabled(struct R3RigidBodyHandle handle, + R3Bool enabled); +#endif + +/** + * Set the collider mass per unit volume. + * @ingroup colliders + */ +RAPIER_API R3Status RAPIER_CALL r3Collider_SetDensity(struct R3ColliderHandle handle, R3Real value); + +/** + * Set the collider mass. + * @ingroup colliders + */ +RAPIER_API R3Status RAPIER_CALL r3Collider_SetMass(struct R3ColliderHandle handle, R3Real value); + +/** + * Enable or disable the collider. + * @ingroup colliders + */ +RAPIER_API R3Status RAPIER_CALL r3Collider_SetEnabled(struct R3ColliderHandle handle, R3Bool value); + +/** + * Set the collider contact-force filtering groups. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetSolverGroups(struct R3ColliderHandle handle, + struct R3InteractionGroups value); + +/** + * Set the collider friction combination rule (R3_COMBINE_*). + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetFrictionCombineRule(struct R3ColliderHandle handle, + uint32_t value); + +/** + * Set the collider restitution combination rule (R3_COMBINE_*). + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetRestitutionCombineRule(struct R3ColliderHandle handle, + uint32_t value); + +/** + * Set the collider extra separation skin around the shape. + * @ingroup colliders + */ +RAPIER_API R3Status RAPIER_CALL r3Collider_SetContactSkin(struct R3ColliderHandle handle, R3Real value); + +/** + * Set the collider force threshold for contact-force events. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetContactForceEventThreshold(struct R3ColliderHandle handle, + R3Real value); + +/** + * Set the collider event-generation bitmask (R3_COLLISION_EVENTS and R3_CONTACT_FORCE_EVENTS). + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetActiveEvents(struct R3ColliderHandle handle, + uint32_t value); + +/** + * Set the collider physics-hook activation bitmask. + * @ingroup colliders + */ +RAPIER_API R3Status RAPIER_CALL r3Collider_SetActiveHooks(struct R3ColliderHandle handle, uint32_t value); + +/** + * Set the collider body-type collision activation bitmask. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetActiveCollisionTypes(struct R3ColliderHandle handle, + uint16_t value); + +/** + * Return the collider world-space rotation. + * @ingroup colliders + */ +RAPIER_API struct R3Rotation RAPIER_CALL r3Collider_Rotation(struct R3ColliderHandle handle); + +/** + * Return the collider collision filtering groups. + * @ingroup colliders + */ +RAPIER_API +struct R3InteractionGroups RAPIER_CALL r3Collider_CollisionGroups(struct R3ColliderHandle handle); + +/** + * Return the collider contact-force filtering groups. + * @ingroup colliders + */ +RAPIER_API struct R3InteractionGroups RAPIER_CALL r3Collider_SolverGroups(struct R3ColliderHandle handle); + +/** + * Return the collider application-owned 128-bit user value. + * @ingroup colliders + */ +RAPIER_API struct R3UserData RAPIER_CALL r3Collider_UserData(struct R3ColliderHandle handle); + +/** + * Return the collider event-generation bitmask (R3_COLLISION_EVENTS and + * R3_CONTACT_FORCE_EVENTS). + * @ingroup colliders + */ +RAPIER_API uint32_t RAPIER_CALL r3Collider_ActiveEvents(struct R3ColliderHandle handle); + +/** + * Return the collider mass. + * @ingroup colliders + */ +RAPIER_API R3Real RAPIER_CALL r3Collider_Mass(struct R3ColliderHandle handle); + +/** + * Return the collider mass per unit volume. + * @ingroup colliders + */ +RAPIER_API R3Real RAPIER_CALL r3Collider_Density(struct R3ColliderHandle handle); + +/** + * Return the collider current volume. + * @ingroup colliders + */ +RAPIER_API R3Real RAPIER_CALL r3Collider_Volume(struct R3ColliderHandle handle); + +/** + * Return the collider extra separation skin around the shape. + * @ingroup colliders + */ +RAPIER_API R3Real RAPIER_CALL r3Collider_ContactSkin(struct R3ColliderHandle handle); + +/** + * Return the collider force threshold for contact-force events. + * @ingroup colliders + */ +RAPIER_API R3Real RAPIER_CALL r3Collider_ContactForceEventThreshold(struct R3ColliderHandle handle); + +/** + * Return whether the collider is enabled. + * @ingroup colliders + */ +RAPIER_API R3Bool RAPIER_CALL r3Collider_IsEnabled(struct R3ColliderHandle handle); + +/** + * Return the current world-space axis-aligned bounds. + * @ingroup colliders + */ +RAPIER_API struct R3Aabb RAPIER_CALL r3Collider_ComputeAabb(struct R3ColliderHandle handle); + +/** + * Return an owned wrapper sharing the collider geometry. Release with r3FreeSharedShape. + * Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3Collider_CloneShape(struct R3ColliderHandle handle); + +/** + * Replace collider geometry by sharing shape; the supplied wrapper is not consumed. + * @ingroup shapes + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetShape(struct R3ColliderHandle handle, + const R3SharedShape *shape); + +/** + * Set the collider pose relative to the parent rigid body. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetPositionWrtParent(struct R3ColliderHandle handle, + struct R3Pose value); + +/** + * Validate the index and generation in the live owning world. Cannot detect a world pointer that + * has already been freed. + * @ingroup rigid_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RigidBody_ValidateHandle(struct R3RigidBodyHandle handle); + +/** + * Validate the index and generation in the live owning world. Cannot detect a world pointer that + * has already been freed. + * @ingroup colliders + */ +RAPIER_API R3Status RAPIER_CALL r3Collider_ValidateHandle(struct R3ColliderHandle handle); + +/** + * Validate the index and generation in the live owning world. Cannot detect a world pointer that + * has already been freed. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBody_ValidateHandle(struct R3SoftBodyHandle handle); + +/** + * Set the joint desc joint frame relative to body 1. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetLocalFrame1(struct R3JointDesc *desc, + struct R3Pose value); + +/** + * Set the impulse joint joint frame relative to body 1. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetLocalFrame1(struct R3ImpulseJointHandle handle, + struct R3Pose value, + R3Bool wake_up); + +/** + * Set the joint desc joint frame relative to body 2. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetLocalFrame2(struct R3JointDesc *desc, + struct R3Pose value); + +/** + * Set the impulse joint joint frame relative to body 2. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetLocalFrame2(struct R3ImpulseJointHandle handle, + struct R3Pose value, + R3Bool wake_up); + +/** + * Set the joint desc joint anchor relative to body 1. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetLocalAnchor1(struct R3JointDesc *desc, + struct R3Vector value); + +/** + * Set the impulse joint joint anchor relative to body 1. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetLocalAnchor1(struct R3ImpulseJointHandle handle, + struct R3Vector value, + R3Bool wake_up); + +/** + * Set the joint desc joint anchor relative to body 2. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetLocalAnchor2(struct R3JointDesc *desc, + struct R3Vector value); + +/** + * Set the impulse joint joint anchor relative to body 2. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetLocalAnchor2(struct R3ImpulseJointHandle handle, + struct R3Vector value, + R3Bool wake_up); + +/** + * Enable or disable allowing contacts between connected bodies for the joint desc. + * @ingroup joints + */ +RAPIER_API R3Status RAPIER_CALL r3JointDesc_SetContactsEnabled(struct R3JointDesc *desc, R3Bool value); + +/** + * Enable or disable allowing contacts between connected bodies for the impulse joint. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetContactsEnabled(struct R3ImpulseJointHandle handle, + R3Bool value, + R3Bool wake_up); + +/** + * Enable or disable the joint desc. + * @ingroup joints + */ +RAPIER_API R3Status RAPIER_CALL r3JointDesc_SetEnabled(struct R3JointDesc *desc, R3Bool value); + +/** + * Enable or disable the impulse joint. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetEnabled(struct R3ImpulseJointHandle handle, + R3Bool value, + R3Bool wake_up); + +/** + * Set the joint desc joint spring coefficients. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetSoftness(struct R3JointDesc *desc, + struct R3SpringCoefficients value); + +/** + * Set the impulse joint joint spring coefficients. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetSoftness(struct R3ImpulseJointHandle handle, + struct R3SpringCoefficients value, + R3Bool wake_up); + +/** + * Set the joint desc translation/rotation lock bitmask. + * @ingroup joints + */ +RAPIER_API R3Status RAPIER_CALL r3JointDesc_SetLockedAxes(struct R3JointDesc *desc, uint8_t value); + +/** + * Set the impulse joint translation/rotation lock bitmask. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetLockedAxes(struct R3ImpulseJointHandle handle, + uint8_t value, + R3Bool wake_up); + +/** + * Set the joint desc joint axis mask with limits enabled. + * @ingroup joints + */ +RAPIER_API R3Status RAPIER_CALL r3JointDesc_SetLimitAxes(struct R3JointDesc *desc, uint8_t value); + +/** + * Set the impulse joint joint axis mask with limits enabled. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetLimitAxes(struct R3ImpulseJointHandle handle, + uint8_t value, + R3Bool wake_up); + +/** + * Set the joint desc joint axis mask with motors enabled. + * @ingroup joints + */ +RAPIER_API R3Status RAPIER_CALL r3JointDesc_SetMotorAxes(struct R3JointDesc *desc, uint8_t value); + +/** + * Set the impulse joint joint axis mask with motors enabled. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetMotorAxes(struct R3ImpulseJointHandle handle, + uint8_t value, + R3Bool wake_up); + +/** + * Set the joint desc coupled joint axis mask. + * @ingroup joints + */ +RAPIER_API R3Status RAPIER_CALL r3JointDesc_SetCoupledAxes(struct R3JointDesc *desc, uint8_t value); + +/** + * Set the impulse joint coupled joint axis mask. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetCoupledAxes(struct R3ImpulseJointHandle handle, + uint8_t value, + R3Bool wake_up); + +/** + * Set the joint desc joint principal axis in body 1 local coordinates. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetLocalAxis1(struct R3JointDesc *desc, + struct R3Vector value); + +/** + * Set the impulse joint joint principal axis in body 1 local coordinates. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetLocalAxis1(struct R3ImpulseJointHandle handle, + struct R3Vector value, + R3Bool wake_up); + +/** + * Set the joint desc joint principal axis in body 2 local coordinates. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetLocalAxis2(struct R3JointDesc *desc, + struct R3Vector value); + +/** + * Set the impulse joint joint principal axis in body 2 local coordinates. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetLocalAxis2(struct R3ImpulseJointHandle handle, + struct R3Vector value, + R3Bool wake_up); + +/** + * Set the joint desc minimum and maximum limits on an axis (linear distance or angular radians). + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetLimits(struct R3JointDesc *desc, + uint32_t joint_axis, + R3Real min, + R3Real max); + +/** + * Set the impulse joint minimum and maximum limits on an axis (linear distance or angular + * radians). + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetLimits(struct R3ImpulseJointHandle handle, + uint32_t joint_axis, + R3Real min, + R3Real max, + R3Bool wake_up); + +/** + * Set the joint desc motor position/velocity targets and spring coefficients on an axis. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetMotor(struct R3JointDesc *desc, + uint32_t joint_axis, + R3Real target_position, + R3Real target_velocity, + R3Real stiffness, + R3Real damping); + +/** + * Set the impulse joint motor position/velocity targets and spring coefficients on an axis. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetMotor(struct R3ImpulseJointHandle handle, + uint32_t joint_axis, + R3Real target_position, + R3Real target_velocity, + R3Real stiffness, + R3Real damping, + R3Bool wake_up); + +/** + * Set the joint desc maximum motor force or torque on an axis. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetMotorMaxForce(struct R3JointDesc *desc, + uint32_t joint_axis, + R3Real max_force); + +/** + * Set the impulse joint maximum motor force or torque on an axis. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetMotorMaxForce(struct R3ImpulseJointHandle handle, + uint32_t joint_axis, + R3Real max_force, + R3Bool wake_up); + +/** + * Set the joint desc motor model on an axis (0 = acceleration-based, 1 = force-based). + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetMotorModel(struct R3JointDesc *desc, + uint32_t joint_axis, + uint32_t model); + +/** + * Set the impulse joint motor model on an axis (0 = acceleration-based, 1 = force-based). + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetMotorModel(struct R3ImpulseJointHandle handle, + uint32_t joint_axis, + uint32_t model, + R3Bool wake_up); + +/** + * Set the joint desc application-owned 128-bit user value. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetUserData(struct R3JointDesc *desc, + struct R3UserData value); + +/** + * Set the impulse joint application-owned 128-bit user value. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetUserData(struct R3ImpulseJointHandle handle, + struct R3UserData value, + R3Bool wake_up); + +/** + * Set the joint desc motor position target and spring coefficients on an axis. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetMotorPosition(struct R3JointDesc *desc, + uint32_t joint_axis, + R3Real target_position, + R3Real stiffness, + R3Real damping); + +/** + * Set the impulse joint motor position target and spring coefficients on an axis. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetMotorPosition(struct R3ImpulseJointHandle handle, + uint32_t joint_axis, + R3Real target_position, + R3Real stiffness, + R3Real damping, + R3Bool wake_up); + +/** + * Set the joint desc motor velocity target and damping factor on an axis. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3JointDesc_SetMotorVelocity(struct R3JointDesc *desc, + uint32_t joint_axis, + R3Real target_velocity, + R3Real factor); + +/** + * Set the impulse joint motor velocity target and damping factor on an axis. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetMotorVelocity(struct R3ImpulseJointHandle handle, + uint32_t joint_axis, + R3Real target_velocity, + R3Real factor, + R3Bool wake_up); + +/** + * Create an owned compound shape by convex decomposition of the input surface. Release it with + * r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3ConvexDecompositionSharedShape(struct R3VectorView vertices, + R3SurfaceElementView indices); + +/** + * Create an owned voxel shape by quantizing points with the supplied per-axis voxel size. Release + * it with r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3VoxelsSharedShapeFromPoints(struct R3Vector voxel_size, + struct R3VectorView points); + +/** + * Create an owned voxel shape from a surface mesh with the supplied uniform voxel size. Release it + * with r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3VoxelizedMeshSharedShape(struct R3VectorView vertices, + R3SurfaceElementView indices, + R3Real voxel_size); + +/** + * Create an owned convex hull of the supplied vertices. Release it with r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3ConvexHullSharedShape(struct R3VectorView vertices); + +/** + * Create an owned triangle mesh from vertices and triangle indices. Release it with + * r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3TrimeshSharedShape(struct R3VectorView vertices, + struct R3TriangleView indices); + +/** + * Create an owned polyline from vertices and edge indices. Release it with r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3PolylineSharedShape(struct R3VectorView vertices, + struct R3EdgeView indices); + +#if defined(RAPIER_DIM2) +/** + * Create an owned oriented 2D polyline from vertices and edge indices. Release it with + * r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3OrientedPolylineSharedShape(struct R3VectorView vertices, + struct R3EdgeView indices); +#endif + +#if defined(RAPIER_DIM2) +/** + * Create an owned convex polygon from vertices already ordered along its boundary. Release it with + * r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API R3SharedShape *RAPIER_CALL r3ConvexPolylineSharedShape(struct R3VectorView vertices); +#endif + +/** + * Create an owned round convex hull shape. Release it with r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3RoundConvexHullSharedShape(struct R3VectorView vertices, + R3Real border_radius); + +/** + * Create an owned triangle mesh with the supplied TRIMESH_* processing flags. Release it with + * r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3TrimeshSharedShapeWithFlags(struct R3VectorView vertices, + struct R3TriangleView indices, + uint32_t flags); + +/** + * Create an owned world. Release it with FreeWorld. + * @ingroup worlds + */ +RAPIER_API struct R3World *RAPIER_CALL r3NewWorld(void); + +/** + * Free a world. NULL is allowed. Rejects destruction from an active callback. + * The caller must prevent other threads from starting calls during destruction. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3FreeWorld(struct R3World *world); + +/** + * Compute a velocity correction from callback-visible body state, updating the PID controller + * history. The context is valid only during its callback. + * @ingroup callbacks + */ +RAPIER_API +struct R3VelocityCorrection RAPIER_CALL r3ReadPidController_RigidBodyCorrection(const struct R3ReadContext *context, + struct R3PidController *controller, + R3Real dt, + struct R3RigidBodyHandle body, + struct R3Pose target_pose, + struct R3Vector target_linvel, + R3AngVector target_angvel); + +/** + * Return the number of rigid body objects in the world. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API size_t RAPIER_CALL r3ReadRigidBodyCount(const struct R3ReadContext *context); + +/** + * Copy entity handles. Uses only the callback-scoped read context; never retain the context. + * @see @ref output_buffers + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r3ReadRigidBodyHandles(const struct R3ReadContext *context, + struct R3RigidBodyHandle *buffer, + size_t capacity); + +/** + * Test whether the live world contains this rigid body handle. A removed/stale handle returns + * false. Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_Contains(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the number of collider objects in the world. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API size_t RAPIER_CALL r3ReadColliderCount(const struct R3ReadContext *context); + +/** + * Copy entity handles. Uses only the callback-scoped read context; never retain the context. + * @see @ref output_buffers + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r3ReadColliderHandles(const struct R3ReadContext *context, + struct R3ColliderHandle *buffer, + size_t capacity); + +/** + * Test whether the live world contains this collider handle. A removed/stale handle returns false. + * Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadCollider_Contains(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return a process-local geometry identity for caching, not a serializable ID. Keep a shared-shape + * clone alive while using it as a cache key. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r3ReadCollider_ShapeIdentity(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider local mass properties. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3MassProperties RAPIER_CALL r3ReadCollider_MassProperties(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the rigid body translation/rotation lock bitmask. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint8_t RAPIER_CALL r3ReadRigidBody_LockedAxes(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the collider is a voxel shape. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadCollider_IsVoxels(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return voxel information at a flat index; found = 0 if absent. Uses only the callback-scoped + * read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3VoxelQuery RAPIER_CALL r3ReadCollider_VoxelAtFlatId(const struct R3ReadContext *context, + struct R3ColliderHandle handle, + uint32_t id); + +/** + * Return the rigid body next kinematic world-space pose. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Pose RAPIER_CALL r3ReadRigidBody_NextPosition(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body world-space rotation. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Rotation RAPIER_CALL r3ReadRigidBody_Rotation(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body world-space center of mass. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3ReadRigidBody_CenterOfMass(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body body-local center of mass. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3ReadRigidBody_LocalCenterOfMass(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body accumulated user-applied world-space force. Uses only the callback-scoped + * read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3ReadRigidBody_UserForce(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body accumulated user-applied world-space torque. Uses only the callback-scoped + * read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3AngVector RAPIER_CALL r3ReadRigidBody_UserTorque(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body body type (R3_DYNAMIC, R3_FIXED, or a kinematic kind). Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint32_t RAPIER_CALL r3ReadRigidBody_BodyType(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body mass. Uses only the callback-scoped read context; never retain the + * context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadRigidBody_Mass(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body gravity multiplier. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadRigidBody_GravityScale(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body linear damping coefficient. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadRigidBody_LinearDamping(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body angular damping coefficient. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadRigidBody_AngularDamping(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body kinetic energy. Uses only the callback-scoped read context; never retain + * the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadRigidBody_KineticEnergy(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body soft-CCD prediction distance. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadRigidBody_SoftCcdPrediction(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is using continuous collision detection. Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_IsCcdEnabled(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is dynamic. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_IsDynamic(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the associated soft-body handle, or an invalid handle if this is not a soft proxy. Uses + * only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3SoftBodyHandle RAPIER_CALL r3ReadRigidBody_SoftBody(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is a soft-body proxy. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_IsSoftFrame(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is fixed. Uses only the callback-scoped read context; never retain + * the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_IsFixed(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is kinematic. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_IsKinematic(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is moving. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_IsMoving(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is currently using CCD for its motion. Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_IsCcdActive(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return world-space velocity at a world-space point, including angular motion. Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3ReadRigidBody_VelocityAtPoint(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle, + struct R3Vector point); + +/** + * Copy attached collider handles. Uses only the callback-scoped read context; never retain the + * context. + * @see @ref output_buffers + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r3ReadRigidBody_Colliders(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle, + struct R3ColliderHandle *buffer, + size_t capacity); + +#if defined(RAPIER_DIM3) +/** + * Return whether the rigid body is using gyroscopic forces. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_GyroscopicForcesEnabled(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); +#endif + +/** + * Return the collider world-space rotation. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Rotation RAPIER_CALL r3ReadCollider_Rotation(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider collision filtering groups. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3InteractionGroups RAPIER_CALL r3ReadCollider_CollisionGroups(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider contact-force filtering groups. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3InteractionGroups RAPIER_CALL r3ReadCollider_SolverGroups(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider application-owned 128-bit user value. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3UserData RAPIER_CALL r3ReadCollider_UserData(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider event-generation bitmask (R3_COLLISION_EVENTS and + * R3_CONTACT_FORCE_EVENTS). Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint32_t RAPIER_CALL r3ReadCollider_ActiveEvents(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider mass. Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadCollider_Mass(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider mass per unit volume. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadCollider_Density(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider current volume. Uses only the callback-scoped read context; never retain the + * context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadCollider_Volume(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider extra separation skin around the shape. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadCollider_ContactSkin(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider force threshold for contact-force events. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadCollider_ContactForceEventThreshold(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return whether the collider is enabled. Uses only the callback-scoped read context; never retain + * the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadCollider_IsEnabled(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the current world-space axis-aligned bounds. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Aabb RAPIER_CALL r3ReadCollider_ComputeAabb(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return an owned wrapper sharing the collider geometry. Release with r3FreeSharedShape. Uses + * only the callback-scoped read context; never retain the context. + * Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. + * @ingroup callbacks + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3ReadCollider_CloneShape(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Validate the index and generation in the live owning world. Cannot detect a world pointer that + * has already been freed. Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Status RAPIER_CALL r3ReadRigidBody_ValidateHandle(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Validate the index and generation in the live owning world. Cannot detect a world pointer that + * has already been freed. Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Status RAPIER_CALL r3ReadCollider_ValidateHandle(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the rigid body world-space pose. Uses only the callback-scoped read context; never retain + * the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Pose RAPIER_CALL r3ReadRigidBody_Position(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body world-space translation. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3ReadRigidBody_Translation(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body world-space linear velocity. Uses only the callback-scoped read context; + * never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3ReadRigidBody_Linvel(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body world-space angular velocity (radians per second). Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3AngVector RAPIER_CALL r3ReadRigidBody_Angvel(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is sleeping. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_IsSleeping(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body is enabled. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_IsEnabled(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body application-owned 128-bit user value. Uses only the callback-scoped read + * context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3UserData RAPIER_CALL r3ReadRigidBody_UserData(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the collider world-space pose. Uses only the callback-scoped read context; never retain + * the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Pose RAPIER_CALL r3ReadCollider_Position(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider world-space translation. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3ReadCollider_Translation(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider friction coefficient. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadCollider_Friction(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider restitution coefficient. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Real RAPIER_CALL r3ReadCollider_Restitution(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return whether the collider is a sensor (detects overlaps without contact forces). Uses only the + * callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadCollider_IsSensor(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Read the parent body handle during a callback; a standalone collider returns an invalid handle + * with OK status. + * @ingroup callbacks + */ +RAPIER_API +struct R3RigidBodyHandle RAPIER_CALL r3ReadCollider_Parent(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Copy callback-visible body states in the supplied handle order. All handles must belong to the + * context world. + * @see @ref output_buffers + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r3ReadRigidBodyReadStates(const struct R3ReadContext *context, + const struct R3RigidBodyHandle *handles, + size_t handle_count, + struct R3RigidBodyState *states, + size_t capacity); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif + +#endif /* RAPIER_H */ diff --git a/c/include/rapier.hpp b/c/include/rapier.hpp new file mode 100644 index 000000000..002ddc350 --- /dev/null +++ b/c/include/rapier.hpp @@ -0,0 +1,114 @@ +/** @file + * Optional C++ RAII wrappers. + * @ingroup cpp + */ +#ifndef RAPIER_HPP +#define RAPIER_HPP +#include "rapier_helpers.h" +#include +#include +#include + +/** Optional C++ ownership helpers. @ingroup cpp */ +namespace rapier { +/** Throw std::runtime_error with LastError when status is not OK. */ +inline void check(RAPIER_TYPE(Status) status) { + if (status != RAPIER_CONST(OK)) { + throw std::runtime_error(std::string(RAPIER_FN(LastError)())); + } +} + +// Use Owner only for owned pointers, never for a borrowed callback context. +/** Deleter for an owned pointer; Free must succeed at destruction time. */ +template struct Deleter { + /** Release the pointer through its matching C API function. */ + void operator()(T *value) const noexcept { + (void)Free(value); + } +}; +/** Unique owner of a native pointer; never wrap a borrowed pointer. */ +template +using Owner = std::unique_ptr>; +/** Own a native World using its matching Free function. */ +using World = Owner; +/** Own a native Shape using its matching Free function. */ +using Shape = Owner; +/** Own a native EventCollector using its matching Free function. */ +using EventCollector = Owner; +/** Own a native Snapshot using its matching Free function. */ +using Snapshot = Owner; + +/** Own a native SoftBodyTearEvent using its matching Free function. */ +using SoftBodyTearEvent = Owner; +/** Own a native ShapeMesh using its matching Free function. */ +using ShapeMesh = Owner; +/** Own a native KinematicCharacterController using its matching Free function. */ +using KinematicCharacterController = + Owner; +/** Own a native PidController using its matching Free function. */ +using PidController = Owner; +#if defined(RAPIER_DIM3) +/** Own a native DynamicRayCastVehicleController using its matching Free function. */ +using DynamicRayCastVehicleController = + Owner; +/** Own a native TriMeshData using its matching Free function. */ +using TriMeshData = Owner; +#endif +#if defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32) +/** Own a native UrdfRobot using its matching Free function. */ +using UrdfRobot = Owner; +/** Own a native UrdfRobotHandles using its matching Free function. */ +using UrdfRobotHandles = Owner; +/** Own a native MjcfRobot using its matching Free function. */ +using MjcfRobot = Owner; +/** Own a native MjcfRobotHandles using its matching Free function. */ +using MjcfRobotHandles = Owner; +#endif + +// These factories return ordinary values. No allocation or deleter is needed. +/** Return a POD rigid-body description with the selected body type. */ +inline RAPIER_TYPE(RigidBodyDesc) rigid_body(uint32_t kind = RAPIER_CONST(DYNAMIC)) { + RAPIER_TYPE(RigidBodyDesc) value = RAPIER_FN(DynamicRigidBodyDesc)(); + value.bodyType = kind; + return value; +} + +/** Return a POD ball collider description. */ +inline RAPIER_TYPE(ColliderDesc) ball(RAPIER_TYPE(Real) radius) { + return RAPIER_FN(BallColliderDesc)(radius); +} + +/** Return a POD cuboid collider description. */ +inline RAPIER_TYPE(ColliderDesc) cuboid(RAPIER_TYPE(Vector) half_extents) { + return RAPIER_FN(CuboidColliderDesc)(half_extents); +} + +/** Return native soft-body description defaults. */ +inline RAPIER_TYPE(SoftBodyDesc) soft_body() { + return RAPIER_FN(DefaultSoftBodyDesc)(); +} + +/** Return a joint description with the selected locked-axis mask. */ +inline RAPIER_TYPE(JointDesc) joint(uint8_t locked_axes = 0) { + RAPIER_TYPE(JointDesc) value = RAPIER_FN(DefaultJointDesc)(); + value.lockedAxes = locked_axes; + return value; +} + +/** Return query defaults with no callback or exclusions. */ +inline RAPIER_TYPE(QueryOptions) queryOptions() { + return RAPIER_FN(DefaultQueryOptions)(); +} + +/** Check the ABI and create an owned world; throw on failure. */ +inline World make_world() { + check(RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), RAPIER_CONST(DIMENSION), + sizeof(RAPIER_TYPE(Real)), sizeof(RAPIER_TYPE(Vector)), + sizeof(RAPIER_TYPE(Pose)))); + RAPIER_TYPE(World) *value = RAPIER_FN(NewWorld)(); + check(RAPIER_FN(LastStatus)()); + return World(value); +} +} // namespace rapier +#endif diff --git a/c/include/rapier_helpers.h b/c/include/rapier_helpers.h new file mode 100644 index 000000000..129663033 --- /dev/null +++ b/c/include/rapier_helpers.h @@ -0,0 +1,24 @@ +/** @file + * Explicit invalid handle values; no owned resources. + * @ingroup math + */ +#ifndef RAPIER_HELPERS_H +#define RAPIER_HELPERS_H +#include "rapier.h" + +/* Value initialization functions are exported by rapier.h. */ + +/* Explicit invalid values; zero-initialized Rapier handles are not invalid. + * These constants have internal linkage and own no resources. */ +/** Invalid rigid body handle. */ +static const RAPIER_TYPE(RigidBodyHandle) RAPIER_CONST(INVALID_RIGID_BODY_HANDLE) = {NULL, UINT32_MAX, UINT32_MAX}; +/** Invalid collider handle. */ +static const RAPIER_TYPE(ColliderHandle) RAPIER_CONST(INVALID_COLLIDER_HANDLE) = {NULL, UINT32_MAX, UINT32_MAX}; +/** Invalid impulse joint handle. */ +static const RAPIER_TYPE(ImpulseJointHandle) RAPIER_CONST(INVALID_IMPULSE_JOINT_HANDLE) = {NULL, UINT32_MAX, UINT32_MAX}; +/** Invalid multibody joint handle. */ +static const RAPIER_TYPE(MultibodyJointHandle) RAPIER_CONST(INVALID_MULTIBODY_JOINT_HANDLE) = {NULL, UINT32_MAX, UINT32_MAX}; +/** Invalid soft body handle. */ +static const RAPIER_TYPE(SoftBodyHandle) RAPIER_CONST(INVALID_SOFT_BODY_HANDLE) = {NULL, UINT32_MAX, UINT32_MAX}; + +#endif diff --git a/c/include/rapier_math.h b/c/include/rapier_math.h new file mode 100644 index 000000000..fb2eebfea --- /dev/null +++ b/c/include/rapier_math.h @@ -0,0 +1,183 @@ +/** @file + * Inline value constructors and arithmetic; no allocation or error-state changes. + * @defgroup inline_math Inline math + * @ingroup math + * @{ + */ +#ifndef RAPIER_MATH_H +#define RAPIER_MATH_H +#include "rapier.h" +#include + +#if defined(RAPIER_DIM2) +/** Pi in the selected scalar precision. */ +#define R2_PI ((R2Real)3.14159265358979323846) +#else +/** Pi in the selected scalar precision. */ +#define R3_PI ((R3Real)3.14159265358979323846) +#endif + +/* Value constructors and arithmetic for Rapier's public C math types. */ +#if defined(RAPIER_DIM2) +/** Construct a vector from its components. */ +static inline RAPIER_TYPE(Vector) RAPIER_FN(Vector)(RAPIER_TYPE(Real) x, RAPIER_TYPE(Real) y) { + RAPIER_TYPE(Vector) result = {x, y}; + return result; +} + +/** Construct a 2D rotation from an angle in radians. */ +static inline RAPIER_TYPE(Rotation) RAPIER_FN(Rotation)(RAPIER_TYPE(Real) angle) { + RAPIER_TYPE(Rotation) result = {angle}; + return result; +} +#else +/** Construct a vector from its components. */ +static inline RAPIER_TYPE(Vector) RAPIER_FN(Vector)(RAPIER_TYPE(Real) x, RAPIER_TYPE(Real) y, RAPIER_TYPE(Real) z) { + RAPIER_TYPE(Vector) result = {x, y, z}; + return result; +} + +/** Construct a 3D unit quaternion; normalizes axis, and returns identity for a zero axis. Angle is in radians. */ +static inline RAPIER_TYPE(Rotation) RAPIER_FN(RotationFromAxisAngle)(RAPIER_TYPE(Vector) axis, + RAPIER_TYPE(Real) angle) { + RAPIER_TYPE(Real) length = + (RAPIER_TYPE(Real))sqrt(axis.x * axis.x + axis.y * axis.y + axis.z * axis.z); + if (length == 0) { + RAPIER_TYPE(Rotation) result = {0, 0, 0, 1}; + return result; + } + RAPIER_TYPE(Real) scale = (RAPIER_TYPE(Real))sin(angle / 2) / length; + RAPIER_TYPE(Rotation) result = {axis.x * scale, axis.y * scale, axis.z * scale, + (RAPIER_TYPE(Real))cos(angle / 2)}; + return result; +} +#endif + +/** Return a + b. */ +static inline RAPIER_TYPE(Vector) RAPIER_FN(VectorAdd)(RAPIER_TYPE(Vector) a, RAPIER_TYPE(Vector) b) { +#if defined(RAPIER_DIM2) + return RAPIER_FN(Vector)(a.x + b.x, a.y + b.y); +#else + return RAPIER_FN(Vector)(a.x + b.x, a.y + b.y, a.z + b.z); +#endif +} + +/** Return a - b. */ +static inline RAPIER_TYPE(Vector) RAPIER_FN(VectorSub)(RAPIER_TYPE(Vector) a, RAPIER_TYPE(Vector) b) { +#if defined(RAPIER_DIM2) + return RAPIER_FN(Vector)(a.x - b.x, a.y - b.y); +#else + return RAPIER_FN(Vector)(a.x - b.x, a.y - b.y, a.z - b.z); +#endif +} + +/** Multiply each component by scale. */ +static inline RAPIER_TYPE(Vector) RAPIER_FN(VectorScale)(RAPIER_TYPE(Vector) vector, RAPIER_TYPE(Real) scale) { +#if defined(RAPIER_DIM2) + return RAPIER_FN(Vector)(vector.x * scale, vector.y * scale); +#else + return RAPIER_FN(Vector)(vector.x * scale, vector.y * scale, vector.z * scale); +#endif +} + +/** Return the dot product. */ +static inline RAPIER_TYPE(Real) RAPIER_FN(VectorDot)(RAPIER_TYPE(Vector) a, RAPIER_TYPE(Vector) b) { +#if defined(RAPIER_DIM2) + return a.x * b.x + a.y * b.y; +#else + return a.x * b.x + a.y * b.y + a.z * b.z; +#endif +} +/** Return the Euclidean length. */ +static inline RAPIER_TYPE(Real) RAPIER_FN(VectorLength)(RAPIER_TYPE(Vector) vector) { + return (RAPIER_TYPE(Real))sqrt(RAPIER_FN(VectorDot)(vector, vector)); +} +/** Normalize a nonzero vector; a zero vector is returned unchanged. */ +static inline RAPIER_TYPE(Vector) RAPIER_FN(VectorNormalize)(RAPIER_TYPE(Vector) vector) { + RAPIER_TYPE(Real) length = RAPIER_FN(VectorLength)(vector); + return length > 0 ? RAPIER_FN(VectorScale)(vector, 1 / length) : vector; +} +#if defined(RAPIER_DIM3) +/** Return the 3D cross product a x b. */ +static inline RAPIER_TYPE(Vector) RAPIER_FN(VectorCross)(RAPIER_TYPE(Vector) a, RAPIER_TYPE(Vector) b) { + return RAPIER_FN(Vector)(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x); +} +#endif + +/** Compose rotations, applying b then a. Inputs must be normalized. */ +static inline RAPIER_TYPE(Rotation) RAPIER_FN(RotationMul)(RAPIER_TYPE(Rotation) a, RAPIER_TYPE(Rotation) b) { +#if defined(RAPIER_DIM2) + RAPIER_TYPE(Rotation) result = {a.angle + b.angle}; + return result; +#else + RAPIER_TYPE(Rotation) result = {a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, + a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, + a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, + a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z}; + return result; +#endif +} + +/* Rotate a vector without changing its length. The rotation must be normalized. + */ +/** Rotate a vector; rotation must be normalized. */ +static inline RAPIER_TYPE(Vector) RAPIER_FN(RotationTransformVector)(RAPIER_TYPE(Rotation) rotation, + RAPIER_TYPE(Vector) vector) { +#if defined(RAPIER_DIM2) + const RAPIER_TYPE(Real) c = (RAPIER_TYPE(Real))cos(rotation.angle), + s = (RAPIER_TYPE(Real))sin(rotation.angle); + return RAPIER_FN(Vector)(c * vector.x - s * vector.y, + s * vector.x + c * vector.y); +#else + const RAPIER_TYPE(Vector) t = {2 * (rotation.y * vector.z - rotation.z * vector.y), + 2 * (rotation.z * vector.x - rotation.x * vector.z), + 2 * (rotation.x * vector.y - rotation.y * vector.x)}; + return RAPIER_FN(Vector)( + vector.x + rotation.w * t.x + rotation.y * t.z - rotation.z * t.y, + vector.y + rotation.w * t.y + rotation.z * t.x - rotation.x * t.z, + vector.z + rotation.w * t.z + rotation.x * t.y - rotation.y * t.x); +#endif +} + +/** Construct a pose from translation and rotation without validation. */ +static inline RAPIER_TYPE(Pose) RAPIER_FN(Pose)(RAPIER_TYPE(Vector) translation, + RAPIER_TYPE(Rotation) rotation) { + RAPIER_TYPE(Pose) result = {translation, rotation}; + return result; +} + +/** Construct a pose with the supplied translation and identity rotation. */ +static inline RAPIER_TYPE(Pose) RAPIER_FN(TranslationPose)(RAPIER_TYPE(Vector) translation) { +#if defined(RAPIER_DIM2) + RAPIER_TYPE(Rotation) rotation = {0}; +#else + RAPIER_TYPE(Rotation) rotation = {0, 0, 0, 1}; +#endif + return RAPIER_FN(Pose)(translation, rotation); +} +/** Return the inverse of a normalized rotation. */ +static inline RAPIER_TYPE(Rotation) RAPIER_FN(RotationInverse)(RAPIER_TYPE(Rotation) rotation) { +#if defined(RAPIER_DIM2) + return RAPIER_FN(Rotation)(-rotation.angle); +#else + RAPIER_TYPE(Rotation) result = {-rotation.x, -rotation.y, -rotation.z, rotation.w}; + return result; +#endif +} +/** Transform a point by rotation then translation. Rotation must be normalized. */ +static inline RAPIER_TYPE(Vector) RAPIER_FN(PoseTransformPoint)(RAPIER_TYPE(Pose) pose, + RAPIER_TYPE(Vector) point) { + return RAPIER_FN(VectorAdd)( + pose.translation, RAPIER_FN(RotationTransformVector)(pose.rotation, point)); +} +/** Return the inverse rigid transform. Rotation must be normalized. */ +static inline RAPIER_TYPE(Pose) RAPIER_FN(PoseInverse)(RAPIER_TYPE(Pose) pose) { + RAPIER_TYPE(Rotation) rotation = RAPIER_FN(RotationInverse)(pose.rotation); + return RAPIER_FN(Pose)(RAPIER_FN(RotationTransformVector)( + rotation, RAPIER_FN(VectorScale)(pose.translation, -1)), + rotation); +} +#endif + +/** @} */ diff --git a/c/rapier-c-macros/Cargo.toml b/c/rapier-c-macros/Cargo.toml new file mode 100644 index 000000000..4ef395320 --- /dev/null +++ b/c/rapier-c-macros/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rapier-c-macros" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +publish = false + +[lib] +proc-macro = true diff --git a/c/rapier-c-macros/src/lib.rs b/c/rapier-c-macros/src/lib.rs new file mode 100644 index 000000000..cf024cf07 --- /dev/null +++ b/c/rapier-c-macros/src/lib.rs @@ -0,0 +1,157 @@ +//! Export naming for the dimension-independent Rapier C implementation. +use proc_macro::{TokenStream, TokenTree}; + +fn pascal_case(name: &str) -> Result { + let mut result = String::new(); + for word in name.split('_') { + if word.is_empty() + || !word + .bytes() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) + { + return Err("expected a nonempty snake_case function name after rpr_"); + } + let mut chars = word.chars(); + result.push(chars.next().unwrap().to_ascii_uppercase()); + result.extend(chars); + } + Ok(result) +} + +fn name_suffix(name: &str, receiver: Option<&str>) -> Result { + let suffix = name + .strip_prefix("rpr_") + .ok_or("expected an rpr_ function name")?; + match receiver { + Some(receiver) => { + let method = suffix + .strip_prefix(receiver) + .and_then(|method| method.strip_prefix('_')) + .ok_or( + "method name must start with the receiver prefix followed by an underscore", + )?; + Ok(format!( + "{}_{}", + pascal_case(receiver)?, + pascal_case(method)? + )) + } + None => pascal_case(suffix), + } +} + +/// Export a dimension-specific C symbol, keeping the Rust identifier unchanged. +/// +/// `#[rapier_export]` exports `rpr_new_world` as `r2NewWorld` / `r3NewWorld`. +/// Instance methods specify their receiver prefix: `#[rapier_export(rigid_body)]` +/// exports `rpr_rigid_body_position` as `r2RigidBody_Position` / `r3RigidBody_Position`. +/// Constructors, destructors, static helpers, and short world functions omit the receiver. +#[proc_macro_attribute] +pub fn rapier_export(args: TokenStream, item: TokenStream) -> TokenStream { + fn expand(args: TokenStream, item: TokenStream) -> Result { + let mut args = args.into_iter(); + let receiver = match (args.next(), args.next()) { + (None, None) => None, + (Some(TokenTree::Ident(receiver)), None) => Some(receiver.to_string()), + _ => return Err("expected no arguments or one snake_case receiver prefix"), + }; + let mut tokens = item.clone().into_iter(); + // Groups are opaque here, so a `fn` token in a body or attribute cannot match. + let found = + tokens.any(|token| matches!(token, TokenTree::Ident(id) if id.to_string() == "fn")); + if !found { + return Err("rapier_export requires a function"); + } + let Some(TokenTree::Ident(name)) = tokens.next() else { + return Err("expected a function name"); + }; + let suffix = name_suffix(&name.to_string(), receiver.as_deref())?; + let attributes = format!( + r#"#[cfg_attr(feature = "dim2", unsafe(export_name = "r2{suffix}"))] + #[cfg_attr(feature = "dim3", unsafe(export_name = "r3{suffix}"))]"# + ); + let mut output: TokenStream = attributes.parse().unwrap(); + output.extend(item); + Ok(output) + } + expand(args, item) + .unwrap_or_else(|message| format!("compile_error!({message:?});").parse().unwrap()) +} + +#[cfg(test)] +mod tests { + use super::name_suffix; + + #[test] + fn lifecycle_functions_and_helpers_use_natural_word_order() { + for (rust, c) in [ + ("rpr_new_world", "NewWorld"), + ("rpr_free_world", "FreeWorld"), + ("rpr_cuboid_collider_desc", "CuboidColliderDesc"), + ("rpr_default_query_options", "DefaultQueryOptions"), + ("rpr_urdf_robot_from_file", "UrdfRobotFromFile"), + ("rpr_dynamic_rigid_body_desc", "DynamicRigidBodyDesc"), + ("rpr_ball_shared_shape", "BallSharedShape"), + ("rpr_step", "Step"), + ("rpr_insert_rigid_body", "InsertRigidBody"), + ("rpr_check_abi", "CheckAbi"), + ("rpr_matrix_3x3", "Matrix3x3"), + ] { + assert_eq!(name_suffix(rust, None), Ok(c.into())); + } + } + + #[test] + fn instance_methods_separate_the_receiver_from_the_method() { + for (rust, receiver, c) in [ + ( + "rpr_rigid_body_is_ccd_enabled", + "rigid_body", + "RigidBody_IsCcdEnabled", + ), + ( + "rpr_joint_desc_set_local_frame1", + "joint_desc", + "JointDesc_SetLocalFrame1", + ), + ( + "rpr_soft_body_tear_event_bodies", + "soft_body_tear_event", + "SoftBodyTearEvent_Bodies", + ), + ( + "rpr_read_rigid_body_position", + "read_rigid_body", + "ReadRigidBody_Position", + ), + ] { + assert_eq!(name_suffix(rust, Some(receiver)), Ok(c.into())); + } + } + + #[test] + fn reject_names_outside_the_export_convention() { + for name in [ + "physics_world_new", + "rpr_", + "rpr__new", + "rpr_world_", + "rpr_World", + ] { + assert!(name_suffix(name, None).is_err(), "{name}"); + } + for (name, receiver) in [ + ("rpr_free_world", "rigid_body"), + ("rpr_free_world", "wor"), + ("rpr_free_world", ""), + ("rpr_free_world", "World"), + ("rpr_world_", "world"), + ("rpr_world", "world"), + ] { + assert!( + name_suffix(name, Some(receiver)).is_err(), + "{name}: {receiver}" + ); + } + } +} diff --git a/c/rapier2d-f64-ffi/Cargo.toml b/c/rapier2d-f64-ffi/Cargo.toml new file mode 100644 index 000000000..cab6836d4 --- /dev/null +++ b/c/rapier2d-f64-ffi/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "rapier2d-f64-ffi" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +description = "C ABI for rapier2d-f64" +publish = false +build = "../build.rs" + +[lib] +name = "rapier2d_f64_ffi" +path = "../src/lib.rs" +crate-type = ["staticlib", "cdylib", "rlib"] + +[features] +default = ["dim2", "f64"] +dim2 = [] +f64 = [] +parallel = ["rapier/parallel"] +profiler = ["rapier/profiler"] +enhanced-determinism = ["rapier/enhanced-determinism"] +fem = ["rapier/fem"] + +[dependencies] +rapier-c-macros = { path = "../rapier-c-macros" } +rapier = { package = "rapier2d-f64", path = "../../crates/rapier2d-f64", features = ["serde-serialize", "debug-render"] } +bincode.workspace = true +serde.workspace = true + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("dim2", "dim3", "f32", "f64", "robotics"))'] } diff --git a/c/rapier2d-ffi/Cargo.toml b/c/rapier2d-ffi/Cargo.toml new file mode 100644 index 000000000..69fe0680e --- /dev/null +++ b/c/rapier2d-ffi/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "rapier2d-ffi" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +description = "C ABI for rapier2d" +publish = false +build = "../build.rs" + +[lib] +name = "rapier2d_ffi" +path = "../src/lib.rs" +crate-type = ["staticlib", "cdylib", "rlib"] + +[features] +default = ["dim2", "f32"] +dim2 = [] +f32 = [] +parallel = ["rapier/parallel"] +profiler = ["rapier/profiler"] +simd8 = ["rapier/simd8"] +enhanced-determinism = ["rapier/enhanced-determinism"] +fem = ["rapier/fem"] + +[dependencies] +rapier-c-macros = { path = "../rapier-c-macros" } +rapier = { package = "rapier2d", path = "../../crates/rapier2d", features = ["serde-serialize", "debug-render"] } +bincode.workspace = true +serde.workspace = true + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("dim2", "dim3", "f32", "f64", "robotics"))'] } + +[[example]] +name = "compare_steps" +path = "../examples/compare_steps.rs" diff --git a/c/rapier3d-f64-ffi/Cargo.toml b/c/rapier3d-f64-ffi/Cargo.toml new file mode 100644 index 000000000..1c8f4c0b5 --- /dev/null +++ b/c/rapier3d-f64-ffi/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "rapier3d-f64-ffi" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +description = "C ABI for rapier3d-f64" +publish = false +build = "../build.rs" + +[lib] +name = "rapier3d_f64_ffi" +path = "../src/lib.rs" +crate-type = ["staticlib", "cdylib", "rlib"] + +[features] +default = ["dim3", "f64"] +dim3 = [] +f64 = [] +parallel = ["rapier/parallel"] +profiler = ["rapier/profiler"] +enhanced-determinism = ["rapier/enhanced-determinism"] +fem = ["rapier/fem"] + +[dependencies] +rapier-c-macros = { path = "../rapier-c-macros" } +rapier = { package = "rapier3d-f64", path = "../../crates/rapier3d-f64", features = ["serde-serialize", "debug-render"] } +bincode.workspace = true +serde.workspace = true + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("dim2", "dim3", "f32", "f64", "robotics"))'] } diff --git a/c/rapier3d-ffi/Cargo.toml b/c/rapier3d-ffi/Cargo.toml new file mode 100644 index 000000000..94807de27 --- /dev/null +++ b/c/rapier3d-ffi/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "rapier3d-ffi" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +description = "C ABI for rapier3d" +publish = false +build = "../build.rs" + +[lib] +name = "rapier3d_ffi" +path = "../src/lib.rs" +crate-type = ["staticlib", "cdylib", "rlib"] + +[features] +default = ["dim3", "f32"] +dim3 = [] +f32 = [] +parallel = ["rapier/parallel"] +profiler = ["rapier/profiler"] +simd8 = ["rapier/simd8"] +enhanced-determinism = ["rapier/enhanced-determinism"] +fem = ["rapier/fem"] +robotics = ["dep:rapier3d-urdf", "dep:rapier3d-mjcf"] + +[dependencies] +rapier-c-macros = { path = "../rapier-c-macros" } +rapier = { package = "rapier3d", path = "../../crates/rapier3d", features = ["serde-serialize", "debug-render"] } +rapier3d-urdf = { workspace = true, optional = true, features = ["stl", "collada", "wavefront"] } +rapier3d-mjcf = { workspace = true, optional = true, features = ["stl", "wavefront", "msh"] } +bincode.workspace = true +serde.workspace = true + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("dim2", "dim3", "f32", "f64", "robotics"))'] } + +[[example]] +name = "compare_steps" +path = "../examples/compare_steps.rs" diff --git a/c/src/array_views.rs b/c/src/array_views.rs new file mode 100644 index 000000000..80a7cb1f2 --- /dev/null +++ b/c/src/array_views.rs @@ -0,0 +1,504 @@ +//! Borrowed, typed array inputs. Setters do not allocate or retain ownership. +#![allow(non_snake_case)] +use crate::*; + +/// Vertex indices for one edge; contiguous u32 fields with no padding. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprEdge { + /// First vertex index. + pub a: u32, + /// Second vertex index. + pub b: u32, +} +const _: () = assert!(size_of::() == 2 * size_of::()); +/// Vertex indices for one triangle; contiguous u32 fields with no padding. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprTriangle { + /// First vertex index. + pub a: u32, + /// Second vertex index. + pub b: u32, + /// Third vertex index. + pub c: u32, +} +const _: () = assert!(size_of::() == 3 * size_of::()); +/// Vertex indices for one tetrahedron; contiguous u32 fields with no padding. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprTetrahedron { + /// First vertex index. + pub a: u32, + /// Second vertex index. + pub b: u32, + /// Third vertex index. + pub c: u32, + /// Fourth vertex index. + pub d: u32, +} +const _: () = assert!(size_of::() == 4 * size_of::()); +/// Vertex indices for one dihedral; contiguous u32 fields with no padding. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprDihedral { + /// First vertex index. + pub a: u32, + /// Second vertex index. + pub b: u32, + /// Third vertex index. + pub c: u32, + /// Fourth vertex index. + pub d: u32, +} +const _: () = assert!(size_of::() == 4 * size_of::()); +/// Borrowed array of vector elements. count counts elements of the declared type. +/// Copying this view does not copy its data or extend its lifetime. No Free is needed. +/// Data must remain live through the build/insert call that reads the description. +/// NULL is permitted only when count is zero. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprVectorView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const RprVector, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} +/// Borrowed array of real elements. count counts elements of the declared type. +/// Copying this view does not copy its data or extend its lifetime. No Free is needed. +/// Data must remain live through the build/insert call that reads the description. +/// NULL is permitted only when count is zero. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprRealView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const RprReal, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} +/// Borrowed array of index elements. count counts elements of the declared type. +/// Copying this view does not copy its data or extend its lifetime. No Free is needed. +/// Data must remain live through the build/insert call that reads the description. +/// NULL is permitted only when count is zero. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprIndexView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const u32, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} +/// Borrowed array of edge elements. count counts elements of the declared type. +/// Copying this view does not copy its data or extend its lifetime. No Free is needed. +/// Data must remain live through the build/insert call that reads the description. +/// NULL is permitted only when count is zero. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprEdgeView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const RprEdge, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} +/// Borrowed array of triangle elements. count counts elements of the declared type. +/// Copying this view does not copy its data or extend its lifetime. No Free is needed. +/// Data must remain live through the build/insert call that reads the description. +/// NULL is permitted only when count is zero. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprTriangleView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const RprTriangle, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} +/// Borrowed array of tetrahedron elements. count counts elements of the declared type. +/// Copying this view does not copy its data or extend its lifetime. No Free is needed. +/// Data must remain live through the build/insert call that reads the description. +/// NULL is permitted only when count is zero. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprTetrahedronView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const RprTetrahedron, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} +/// Borrowed array of dihedral elements. count counts elements of the declared type. +/// Copying this view does not copy its data or extend its lifetime. No Free is needed. +/// Data must remain live through the build/insert call that reads the description. +/// NULL is permitted only when count is zero. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprDihedralView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const RprDihedral, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} +/// Borrowed cell array: triangles in 2D, tetrahedra in 3D. +/// @ingroup math +#[cfg(feature = "dim2")] +pub type RprCellView = RprTriangleView; +/// Borrowed cell array: triangles in 2D, tetrahedra in 3D. +/// @ingroup math +#[cfg(feature = "dim3")] +pub type RprCellView = RprTetrahedronView; +/// Borrowed surface array: edges in 2D, triangles in 3D. +/// @ingroup math +#[cfg(feature = "dim2")] +pub type RprSurfaceElementView = RprEdgeView; +/// Borrowed surface array: edges in 2D, triangles in 3D. +/// @ingroup math +#[cfg(feature = "dim3")] +pub type RprSurfaceElementView = RprTriangleView; + +// Validate metadata without reading elements or allocating. Numerical values and +// topology bounds are validated by the existing build/insert path. +pub(crate) fn validate_view(data: *const T, count: usize) -> Result { + if count == 0 { + return Ok(()); + } + ensure( + count <= isize::MAX as usize / size_of::(), + "array view is too large", + )?; + if data.is_null() { + return Err((RPR_NULL_POINTER, "nonempty array view has null data".into())); + } + ensure(data.is_aligned(), "misaligned array view") +} + +/// Replace the shape geometry with a borrowed tri mesh. Counts are elements. +/// Copies no arrays. Invalid view metadata leaves the description unchanged. +/// Geometry and flags are validated when the description is built or inserted. +/// @ingroup shapes +#[rapier_export(shape_desc)] +pub unsafe extern "C" fn rpr_shape_desc_set_trimesh( + desc: *mut RprShapeDesc, + vertices: RprVectorView, + indices: RprTriangleView, + flags: u32, +) -> RprStatus { + ffi(|| unsafe { + validate_view(vertices.data, vertices.count)?; + validate_view(indices.data, indices.count)?; + let shape = RprShapeDesc { + kind: RPR_SHAPE_DESC_TRIMESH, + vertices, + triangles: indices, + flags, + ..RprShapeDesc::default() + }; + output(desc, shape) + }) +} +/// Replace the shape geometry with a borrowed polyline. Counts are elements. +/// Copies no arrays. Invalid view metadata leaves the description unchanged. +/// Geometry and flags are validated when the description is built or inserted. +/// @ingroup shapes +#[rapier_export(shape_desc)] +pub unsafe extern "C" fn rpr_shape_desc_set_polyline( + desc: *mut RprShapeDesc, + vertices: RprVectorView, + indices: RprEdgeView, + flags: u32, +) -> RprStatus { + ffi(|| unsafe { + validate_view(vertices.data, vertices.count)?; + validate_view(indices.data, indices.count)?; + let shape = RprShapeDesc { + kind: RPR_SHAPE_DESC_POLYLINE, + vertices, + edges: indices, + flags, + ..RprShapeDesc::default() + }; + output(desc, shape) + }) +} +/// Replace the shape geometry with a borrowed convex hull point cloud. +/// @ingroup shapes +#[rapier_export(shape_desc)] +pub unsafe extern "C" fn rpr_shape_desc_set_convex_hull( + desc: *mut RprShapeDesc, + vertices: RprVectorView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(vertices.data, vertices.count)?; + output( + desc, + RprShapeDesc { + kind: RPR_SHAPE_DESC_CONVEX_HULL, + vertices, + ..RprShapeDesc::default() + }, + ) + }) +} +/// Select an explicit particle recipe and borrow its positions. Other fields are preserved. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_particles( + desc: *mut RprSoftBodyDesc, + positions: RprVectorView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(positions.data, positions.count)?; + let desc = get_mut(desc)?; + desc.kind = RPR_SOFT_DESC_PARTICLES; + desc.positions = positions; + Ok(()) + }) +} +/// Select a surface recipe and borrow its vertices and elements. Other fields are preserved. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_surface_mesh( + desc: *mut RprSoftBodyDesc, + vertices: RprVectorView, + elements: RprSurfaceElementView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(vertices.data, vertices.count)?; + validate_view(elements.data, elements.count)?; + let desc = get_mut(desc)?; + desc.kind = RPR_SOFT_DESC_SURFACE; + desc.positions = vertices; + desc.surface = elements; + Ok(()) + }) +} +/// Borrow skin geometry. Other fields, including skinCollision, are preserved. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_skin( + desc: *mut RprSoftBodyDesc, + vertices: RprVectorView, + elements: RprSurfaceElementView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(vertices.data, vertices.count)?; + validate_view(elements.data, elements.count)?; + let desc = get_mut(desc)?; + desc.skinVertices = vertices; + desc.skinIndices = elements; + Ok(()) + }) +} +/// Borrow masses; preserve all other fields. No allocation or element reads. +/// Zero counts retain the recipe's generated defaults at insertion, as with directly assigned +/// views. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_masses( + desc: *mut RprSoftBodyDesc, + view: RprRealView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.masses = view; + Ok(()) + }) +} +/// Borrow pinned particles; preserve all other fields. No allocation or element reads. +/// Zero counts retain the recipe's generated defaults at insertion, as with directly assigned +/// views. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_pinned_particles( + desc: *mut RprSoftBodyDesc, + view: RprIndexView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.pinned = view; + Ok(()) + }) +} +/// Borrow edges; preserve all other fields. No allocation or element reads. +/// Zero counts retain the recipe's generated defaults at insertion, as with directly assigned +/// views. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_edges( + desc: *mut RprSoftBodyDesc, + view: RprEdgeView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.edges = view; + Ok(()) + }) +} +/// Borrow bend edges; preserve all other fields. No allocation or element reads. +/// Zero counts retain the recipe's generated defaults at insertion, as with directly assigned +/// views. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_bend_edges( + desc: *mut RprSoftBodyDesc, + view: RprEdgeView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.bendEdges = view; + Ok(()) + }) +} +/// Borrow cells; preserve all other fields. No allocation or element reads. +/// Zero counts retain the recipe's generated defaults at insertion, as with directly assigned +/// views. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_cells( + desc: *mut RprSoftBodyDesc, + view: RprCellView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.cells = view; + Ok(()) + }) +} +/// Borrow surface; preserve all other fields. No allocation or element reads. +/// Zero counts retain the recipe's generated defaults at insertion, as with directly assigned +/// views. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_surface( + desc: *mut RprSoftBodyDesc, + view: RprSurfaceElementView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.surface = view; + Ok(()) + }) +} +/// Borrow tension only edges; preserve all other fields. No allocation or element reads. +/// Zero counts retain the recipe's generated defaults at insertion, as with directly assigned +/// views. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_tension_only_edges( + desc: *mut RprSoftBodyDesc, + view: RprIndexView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.tensionOnlyEdges = view; + Ok(()) + }) +} +/// Borrow dihedrals; preserve all other fields. No allocation or element reads. +/// Zero counts retain the recipe's generated defaults at insertion, as with directly assigned +/// views. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[cfg(feature = "dim3")] +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_dihedrals( + desc: *mut RprSoftBodyDesc, + view: RprDihedralView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.dihedrals = view; + Ok(()) + }) +} +/// Borrow wire; preserve all other fields. No allocation or element reads. +/// Zero counts retain the recipe's generated defaults at insertion, as with directly assigned +/// views. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[cfg(feature = "dim3")] +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_wire( + desc: *mut RprSoftBodyDesc, + view: RprEdgeView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.wire = view; + Ok(()) + }) +} + +/// Triangle in 2D or tetrahedron in 3D. +/// @ingroup math +#[cfg(feature = "dim2")] +pub type RprCell = RprTriangle; +/// Triangle in 2D or tetrahedron in 3D. +/// @ingroup math +#[cfg(feature = "dim3")] +pub type RprCell = RprTetrahedron; +/// Edge in 2D or triangle in 3D. +/// @ingroup math +#[cfg(feature = "dim2")] +pub type RprSurfaceElement = RprEdge; +/// Edge in 2D or triangle in 3D. +/// @ingroup math +#[cfg(feature = "dim3")] +pub type RprSurfaceElement = RprTriangle; + +/// Borrowed elements; count counts elements. Data must remain live through insertion. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftEdgeSoftnessView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const RprSoftEdgeSoftness, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} + +/// Borrowed elements; count counts elements. Data must remain live through insertion. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftEdgeTearView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const RprSoftEdgeTear, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} + +/// Borrowed elements; count counts elements. Data must remain live through insertion. +/// @ingroup shapes +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprCompoundShapeView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const RprCompoundShapeDesc, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} diff --git a/c/src/config_data.rs b/c/src/config_data.rs new file mode 100644 index 000000000..dfdae9f3b --- /dev/null +++ b/c/src/config_data.rs @@ -0,0 +1,603 @@ +//! Copyable configuration snapshots. Apply functions validate before replacing native state. +#![allow(non_snake_case)] +use crate::*; +#[cfg(feature = "dim3")] +use rapier::dynamics::FrictionModel; +#[cfg(feature = "fem")] +use rapier::dynamics::SoftFemParameters; +use rapier::dynamics::{ + SoftBodiesSettings, SoftEdgePlasticFlow, SoftPatchConstraints, SoftRecoverySettings, +}; + +/// Optional scalar override; enabled = 0 selects no override. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprOptionalReal { + /// Whether this setting/object is enabled (0 or 1). + pub enabled: RprBool, + /// Value used when enabled is 1. + pub value: RprReal, +} +/// Optional unsigned integer override; enabled = 0 selects no override. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprOptionalU32 { + /// Whether this setting/object is enabled (0 or 1). + pub enabled: RprBool, + /// Value used when enabled is 1. + pub value: u32, +} +/// Optional boolean override. When disabled, retain the recipe's native default. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprOptionalBool { + /// Whether this setting/object is enabled (0 or 1). + pub enabled: RprBool, + /// Value used when enabled is 1. + pub value: RprBool, +} +/// Plain configuration data; initialize defaults, edit, then apply. No destructor. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftBodyMaterial { + /// Spring coefficients for structural edge constraints. + pub edgeSoftness: RprSpringCoefficients, + /// Spring coefficients for bending edges and dihedrals. + pub bendSoftness: RprSpringCoefficients, + /// Spring coefficients for cell and global volume constraints. + pub volumeSoftness: RprSpringCoefficients, + /// Spring coefficients for shape-matching constraints. + pub shapeMatchingSoftness: RprSpringCoefficients, + /// Elastic modulus: force per area in 3D, force per length in 2D; nonnegative. + pub youngModulus: RprReal, + /// Poisson ratio for elastic cells, in [0, 0.5). + pub poissonRatio: RprReal, + /// Nonnegative damping ratio of elastic cells. + pub elasticDampingRatio: RprReal, + /// Cell strain threshold for plastic flow; zero disables plasticity. + pub plasticYield: RprReal, + /// Nonnegative rate per second at which excess cell strain becomes permanent. + pub plasticCreep: RprReal, + /// Maximum accumulated cell plastic stretch, measured by the norm of P - I. + pub plasticMax: RprReal, + /// Rate per second pulling particle velocities toward best-fit rigid motion; zero disables it. + pub deformationDamping: RprReal, + /// Edge strain threshold for plastic flow; zero disables plasticity. + pub edgePlasticYield: RprReal, + /// Nonnegative rate per second at which excess edge strain becomes permanent. + pub edgePlasticCreep: RprReal, + /// Maximum permanent edge-length change as a fraction of its initial length. + pub edgePlasticMax: RprReal, + /// Plastic flow direction: 0 both, 1 compression only, 2 tension only. + pub edgePlasticFlow: u32, + /// Optional strain threshold for tearing; disabled means no strain-based tearing. + pub tearStrain: RprOptionalReal, + /// Optional tensile edge-force threshold for tearing. + pub tearForce: RprOptionalReal, + /// Exponential load-smoothing time constant in seconds; zero disables smoothing. + pub tearSmoothing: RprReal, + /// Tear-threshold multiplier for undamaged interior elements. + pub interiorStrength: RprReal, + /// Maximum ordinary edge tears per step; edges above twice their threshold bypass the limit. + pub maxTearsPerStep: u32, + /// Optional minimum particle count of tear pieces. + pub minPiece: RprOptionalU32, +} +impl From for RprSoftBodyMaterial { + fn from(value: SoftBodyMaterial) -> Self { + Self { + edgeSoftness: value.edge_softness.into(), + bendSoftness: value.bend_softness.into(), + volumeSoftness: value.volume_softness.into(), + shapeMatchingSoftness: value.shape_matching_softness.into(), + youngModulus: value.young_modulus, + poissonRatio: value.poisson_ratio, + elasticDampingRatio: value.elastic_damping_ratio, + plasticYield: value.plastic_yield, + plasticCreep: value.plastic_creep, + plasticMax: value.plastic_max, + deformationDamping: value.deformation_damping, + edgePlasticYield: value.edge_plastic_yield, + edgePlasticCreep: value.edge_plastic_creep, + edgePlasticMax: value.edge_plastic_max, + edgePlasticFlow: match value.edge_plastic_flow { + SoftEdgePlasticFlow::Both => 0, + SoftEdgePlasticFlow::Compression => 1, + SoftEdgePlasticFlow::Tension => 2, + }, + tearStrain: RprOptionalReal { + enabled: value.tear_strain.is_some() as RprBool, + value: value.tear_strain.unwrap_or_default(), + }, + tearForce: RprOptionalReal { + enabled: value.tear_force.is_some() as RprBool, + value: value.tear_force.unwrap_or_default(), + }, + tearSmoothing: value.tear_smoothing, + interiorStrength: value.interior_strength, + maxTearsPerStep: value.max_tears_per_step, + minPiece: RprOptionalU32 { + enabled: value.min_piece.is_some() as RprBool, + value: value.min_piece.unwrap_or_default(), + }, + } + } +} +impl RprSoftBodyMaterial { + pub(crate) fn raw(&self) -> Result { + Ok(SoftBodyMaterial { + edge_softness: self.edgeSoftness.raw()?, + bend_softness: self.bendSoftness.raw()?, + volume_softness: self.volumeSoftness.raw()?, + shape_matching_softness: self.shapeMatchingSoftness.raw()?, + young_modulus: nonnegative(self.youngModulus)?, + poisson_ratio: { + ensure( + (0.0..0.5).contains(&self.poissonRatio), + "Poisson ratio must be in [0, 0.5)", + )?; + self.poissonRatio + }, + elastic_damping_ratio: nonnegative(self.elasticDampingRatio)?, + plastic_yield: nonnegative(self.plasticYield)?, + plastic_creep: nonnegative(self.plasticCreep)?, + plastic_max: nonnegative(self.plasticMax)?, + deformation_damping: nonnegative(self.deformationDamping)?, + edge_plastic_yield: nonnegative(self.edgePlasticYield)?, + edge_plastic_creep: nonnegative(self.edgePlasticCreep)?, + edge_plastic_max: nonnegative(self.edgePlasticMax)?, + edge_plastic_flow: match self.edgePlasticFlow { + 0 => SoftEdgePlasticFlow::Both, + 1 => SoftEdgePlasticFlow::Compression, + 2 => SoftEdgePlasticFlow::Tension, + _ => return Err(invalid("invalid edge_plastic_flow")), + }, + tear_strain: if boolean(self.tearStrain.enabled)? { + Some(nonnegative(self.tearStrain.value)?) + } else { + None + }, + tear_force: if boolean(self.tearForce.enabled)? { + Some(nonnegative(self.tearForce.value)?) + } else { + None + }, + tear_smoothing: nonnegative(self.tearSmoothing)?, + interior_strength: nonnegative(self.interiorStrength)?, + max_tears_per_step: self.maxTearsPerStep, + min_piece: if boolean(self.minPiece.enabled)? { + Some({ + ensure(self.minPiece.value > 0, "min piece must be positive")?; + self.minPiece.value + }) + } else { + None + }, + }) + } +} +/// Return native default soft body material. This POD value owns no resources. +/// @ingroup soft_bodies +#[rapier_export] +pub extern "C" fn rpr_default_soft_body_material() -> RprSoftBodyMaterial { + SoftBodyMaterial::default().into() +} +/// Plain configuration data; initialize defaults, edit, then apply. No destructor. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftRecoverySettings { + /// Expand speculative contact margins to cover particle velocities set between steps. + pub authoredVelocityMargin: RprBool, + /// Enable speculative edge-edge collision constraints. + pub edgeSpeculation: RprBool, + /// Detect inverted cells to support self-contact recovery. + pub invertedCellDetection: RprBool, + /// Detect surface self-crossings each step. + pub selfCrossingDetection: RprBool, + /// Skip self-crossing detection when accumulated motion cannot have created a crossing. + pub detectionMotionGating: RprBool, + /// Detect boundary crossings between soft bodies. + pub crossBodyDetection: RprBool, + /// Disable contacts on tangled features so elasticity can untangle them. + pub selfStandDown: RprBool, + /// Allow contacts at cross-body crossings to expel, but not hold, the intruder. + pub crossBodyExpelGate: RprBool, + /// Disable edge constraints touching cross-body crossings. + pub edgeStandDown: RprBool, + /// Repel crossing features toward their neighborhood's side of the surface. + pub crossingRepulsion: RprBool, + /// Guide cross-body repulsion by overlap-volume normals; closed meshes only. + pub crossingRepulsionGuide: RprBool, + /// Guide self-crossing repulsion by self-intersection-volume normals; closed meshes only. + pub crossingRepulsionSelfGuide: RprBool, + /// Maximum recovery rate in length units per second, scaled by lengthUnit. + pub recoveryPace: RprReal, + /// Enable intersection-volume constraints for overlapping closed surfaces. + pub overlapConstraints: RprBool, + /// Enable intersection-volume constraints against rigid colliders. + pub overlapRigid: RprBool, + /// Skip pair overlap constraints for self-crossed meshes. + pub overlapSkipSelfTangled: RprBool, + /// Disable 3D closed-surface edge constraints where overlap constraints take over. + pub overlapEdgeStandDown: RprBool, + /// Velocity-change limit per step, as a multiple of recoveryPace. + pub overlapConstraintPace: RprReal, + /// Per-point constraints inside overlap patches: 0 keep, 1 stand down, 2 align with overlap + /// normal. + pub overlapPatchConstraints: u32, + /// Measure overlap on contact-skin surfaces rather than bare geometry. + pub overlapSkinVolume: RprBool, + /// Overlap depth retained by recovery, as a fraction of the pair's contact skins. + pub overlapKeptDepth: RprReal, + /// Enable recovery of self-intersection regions. + pub overlapSelfRegions: RprBool, + /// Use the overlap normal for recovery pushes. + pub overlapNormalPush: RprBool, + /// Use spatially split overlap-volume constraints. + pub overlapMultiVolume: RprBool, + /// Cells per tangent axis of the multi-volume grid. + pub overlapSplit: u32, + /// Recovery progress patience in steps. + pub overlapPatience: u32, + /// Relative overlap-volume decrease that counts as recovery progress. + pub overlapProgressMargin: RprReal, +} +impl From for RprSoftRecoverySettings { + fn from(value: SoftRecoverySettings) -> Self { + Self { + authoredVelocityMargin: value.authored_velocity_margin as RprBool, + edgeSpeculation: value.edge_speculation as RprBool, + invertedCellDetection: value.inverted_cell_detection as RprBool, + selfCrossingDetection: value.self_crossing_detection as RprBool, + detectionMotionGating: value.detection_motion_gating as RprBool, + crossBodyDetection: value.cross_body_detection as RprBool, + selfStandDown: value.self_stand_down as RprBool, + crossBodyExpelGate: value.cross_body_expel_gate as RprBool, + edgeStandDown: value.edge_stand_down as RprBool, + crossingRepulsion: value.crossing_repulsion as RprBool, + crossingRepulsionGuide: value.crossing_repulsion_guide as RprBool, + crossingRepulsionSelfGuide: value.crossing_repulsion_self_guide as RprBool, + recoveryPace: value.recovery_pace, + overlapConstraints: value.overlap_constraints as RprBool, + overlapRigid: value.overlap_rigid as RprBool, + overlapSkipSelfTangled: value.overlap_skip_self_tangled as RprBool, + overlapEdgeStandDown: value.overlap_edge_stand_down as RprBool, + overlapConstraintPace: value.overlap_constraint_pace, + overlapPatchConstraints: match value.overlap_patch_constraints { + SoftPatchConstraints::Keep => 0, + SoftPatchConstraints::StandDown => 1, + SoftPatchConstraints::AlongNormal => 2, + }, + overlapSkinVolume: value.overlap_skin_volume as RprBool, + overlapKeptDepth: value.overlap_kept_depth, + overlapSelfRegions: value.overlap_self_regions as RprBool, + overlapNormalPush: value.overlap_normal_push as RprBool, + overlapMultiVolume: value.overlap_multi_volume as RprBool, + overlapSplit: value.overlap_split, + overlapPatience: value.overlap_patience, + overlapProgressMargin: value.overlap_progress_margin, + } + } +} +impl RprSoftRecoverySettings { + pub(crate) fn raw(&self) -> Result { + Ok(SoftRecoverySettings { + authored_velocity_margin: boolean(self.authoredVelocityMargin)?, + edge_speculation: boolean(self.edgeSpeculation)?, + inverted_cell_detection: boolean(self.invertedCellDetection)?, + self_crossing_detection: boolean(self.selfCrossingDetection)?, + detection_motion_gating: boolean(self.detectionMotionGating)?, + cross_body_detection: boolean(self.crossBodyDetection)?, + self_stand_down: boolean(self.selfStandDown)?, + cross_body_expel_gate: boolean(self.crossBodyExpelGate)?, + edge_stand_down: boolean(self.edgeStandDown)?, + crossing_repulsion: boolean(self.crossingRepulsion)?, + crossing_repulsion_guide: boolean(self.crossingRepulsionGuide)?, + crossing_repulsion_self_guide: boolean(self.crossingRepulsionSelfGuide)?, + recovery_pace: nonnegative(self.recoveryPace)?, + overlap_constraints: boolean(self.overlapConstraints)?, + overlap_rigid: boolean(self.overlapRigid)?, + overlap_skip_self_tangled: boolean(self.overlapSkipSelfTangled)?, + overlap_edge_stand_down: boolean(self.overlapEdgeStandDown)?, + overlap_constraint_pace: nonnegative(self.overlapConstraintPace)?, + overlap_patch_constraints: match self.overlapPatchConstraints { + 0 => SoftPatchConstraints::Keep, + 1 => SoftPatchConstraints::StandDown, + 2 => SoftPatchConstraints::AlongNormal, + _ => return Err(invalid("invalid overlap_patch_constraints")), + }, + overlap_skin_volume: boolean(self.overlapSkinVolume)?, + overlap_kept_depth: nonnegative(self.overlapKeptDepth)?, + overlap_self_regions: boolean(self.overlapSelfRegions)?, + overlap_normal_push: boolean(self.overlapNormalPush)?, + overlap_multi_volume: boolean(self.overlapMultiVolume)?, + overlap_split: self.overlapSplit, + overlap_patience: self.overlapPatience, + overlap_progress_margin: nonnegative(self.overlapProgressMargin)?, + }) + } +} +/// Return native default soft recovery settings. This POD value owns no resources. +/// @ingroup soft_bodies +#[rapier_export] +pub extern "C" fn rpr_default_soft_recovery_settings() -> RprSoftRecoverySettings { + SoftRecoverySettings::default().into() +} +#[cfg(feature = "fem")] +/// Plain configuration data; initialize defaults, edit, then apply. No destructor. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftFemParameters { + /// FEM iterative linear-solver tolerance. + pub linearTolerance: RprReal, + /// Maximum FEM linear-solver iterations. + pub maxLinearIterations: usize, + /// Maximum degrees of freedom solved by the dense FEM solver. + pub maxDenseDofs: usize, +} +#[cfg(feature = "fem")] +impl From for RprSoftFemParameters { + fn from(value: SoftFemParameters) -> Self { + Self { + linearTolerance: value.linear_tolerance, + maxLinearIterations: value.max_linear_iterations, + maxDenseDofs: value.max_dense_dofs, + } + } +} +#[cfg(feature = "fem")] +impl RprSoftFemParameters { + pub(crate) fn raw(&self) -> Result { + Ok(SoftFemParameters { + linear_tolerance: positive(self.linearTolerance)?, + max_linear_iterations: self.maxLinearIterations, + max_dense_dofs: self.maxDenseDofs, + }) + } +} +/// Return native default soft fem parameters. This POD value owns no resources. +/// @ingroup soft_bodies +#[cfg(feature = "fem")] +#[rapier_export] +pub extern "C" fn rpr_default_soft_fem_parameters() -> RprSoftFemParameters { + SoftFemParameters::default().into() +} +/// Plain configuration data; initialize defaults, edit, then apply. No destructor. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftBodiesSettings { + /// Soft-body crossing detection and recovery settings. + pub recovery: RprSoftRecoverySettings, + /// Strain threshold for re-solving soft constraints after contacts within a substep. + pub resweepStrain: RprReal, + /// Maximum additional substeps requested by soft-body motion. + pub maxExtraSubsteps: usize, + /// Multiplier on contact natural frequency for soft-body contacts. + pub contactStiffening: RprReal, + #[cfg(feature = "fem")] + /// FEM linear-solver settings, present only when RAPIER_FEM is enabled. + pub fem: RprSoftFemParameters, +} +impl From for RprSoftBodiesSettings { + fn from(value: SoftBodiesSettings) -> Self { + Self { + recovery: value.recovery.into(), + resweepStrain: value.resweep_strain, + maxExtraSubsteps: value.max_extra_substeps, + contactStiffening: value.contact_stiffening, + #[cfg(feature = "fem")] + fem: value.fem.into(), + } + } +} +impl RprSoftBodiesSettings { + pub(crate) fn raw(&self) -> Result { + Ok(SoftBodiesSettings { + recovery: self.recovery.raw()?, + resweep_strain: nonnegative(self.resweepStrain)?, + max_extra_substeps: self.maxExtraSubsteps, + contact_stiffening: nonnegative(self.contactStiffening)?, + #[cfg(feature = "fem")] + fem: self.fem.raw()?, + }) + } +} +/// Return native default soft bodies settings. This POD value owns no resources. +/// @ingroup soft_bodies +#[rapier_export] +pub extern "C" fn rpr_default_soft_bodies_settings() -> RprSoftBodiesSettings { + SoftBodiesSettings::default().into() +} +/// Plain configuration data; initialize defaults, edit, then apply. No destructor. +/// @ingroup worlds +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprIntegrationParameters { + /// Simulation step duration in seconds. + pub dt: RprReal, + /// Minimum CCD substep duration in seconds. + pub minCcdDt: RprReal, + /// Spring coefficients for dynamic contact constraints. + pub contactSoftness: RprSpringCoefficients, + /// Spring coefficients for contacts against fixed bodies. + pub staticContactSoftness: RprSpringCoefficients, + /// Scale applied to cached impulses when warmstarting. + pub warmstartCoefficient: RprReal, + /// Typical world-space length of one meter; scales solver tolerances, not geometry. + pub lengthUnit: RprReal, + /// Soft-body integration and recovery settings. + pub softBodies: RprSoftBodiesSettings, + /// Allowed penetration divided by lengthUnit. + pub normalizedAllowedLinearError: RprReal, + /// Maximum penetration-correction speed divided by lengthUnit. + pub normalizedMaxCorrectiveVelocity: RprReal, + /// Speculative-contact distance divided by lengthUnit. + pub normalizedPredictionDistance: RprReal, + /// Maximum linear speed divided by lengthUnit. + pub normalizedMaxLinearVelocity: RprReal, + /// Number of solver substeps/iterations; must be positive. + pub numSolverIterations: usize, + /// PGS iterations per solver substep. + pub numInternalPgsIterations: usize, + /// Stabilization iterations after velocity solving. + pub numInternalStabilizationIterations: usize, + /// Maximum CCD substeps; 0 disables all CCD for the world. + pub maxCcdSubsteps: usize, + /// Whether to cluster contacts for solving. + pub contactClustering: RprBool, + /// Whether to reuse nearby contacts between steps. + pub contactRecycling: RprBool, + /// Contact recycling distance divided by lengthUnit. + pub normalizedContactRecycleDistance: RprReal, + /// Whether to solve friction in the bias pass. + pub frictionInBiasPass: RprBool, + /// Whether to warmstart joint constraints. + pub warmstartJoints: RprBool, + #[cfg(feature = "dim3")] + /// Friction model: 0 simplified, 1 Coulomb (3D only). + pub frictionModel: u32, +} +impl From for RprIntegrationParameters { + fn from(value: IntegrationParameters) -> Self { + Self { + dt: value.dt, + minCcdDt: value.min_ccd_dt, + contactSoftness: value.contact_softness.into(), + staticContactSoftness: value.static_contact_softness.into(), + warmstartCoefficient: value.warmstart_coefficient, + lengthUnit: value.length_unit, + softBodies: value.soft_bodies.into(), + normalizedAllowedLinearError: value.normalized_allowed_linear_error, + normalizedMaxCorrectiveVelocity: value.normalized_max_corrective_velocity, + normalizedPredictionDistance: value.normalized_prediction_distance, + normalizedMaxLinearVelocity: value.normalized_max_linear_velocity, + numSolverIterations: value.num_solver_iterations, + numInternalPgsIterations: value.num_internal_pgs_iterations, + numInternalStabilizationIterations: value.num_internal_stabilization_iterations, + maxCcdSubsteps: value.max_ccd_substeps, + contactClustering: value.contact_clustering as RprBool, + contactRecycling: value.contact_recycling as RprBool, + normalizedContactRecycleDistance: value.normalized_contact_recycle_distance, + frictionInBiasPass: value.friction_in_bias_pass as RprBool, + warmstartJoints: value.warmstart_joints as RprBool, + #[cfg(feature = "dim3")] + frictionModel: match value.friction_model { + FrictionModel::Simplified => 0, + FrictionModel::Coulomb => 1, + }, + } + } +} +impl RprIntegrationParameters { + pub(crate) fn raw(&self) -> Result { + Ok(IntegrationParameters { + dt: positive(self.dt)?, + min_ccd_dt: nonnegative(self.minCcdDt)?, + contact_softness: self.contactSoftness.raw()?, + static_contact_softness: self.staticContactSoftness.raw()?, + warmstart_coefficient: nonnegative(self.warmstartCoefficient)?, + length_unit: positive(self.lengthUnit)?, + soft_bodies: self.softBodies.raw()?, + normalized_allowed_linear_error: nonnegative(self.normalizedAllowedLinearError)?, + normalized_max_corrective_velocity: nonnegative(self.normalizedMaxCorrectiveVelocity)?, + normalized_prediction_distance: nonnegative(self.normalizedPredictionDistance)?, + normalized_max_linear_velocity: nonnegative(self.normalizedMaxLinearVelocity)?, + num_solver_iterations: self.numSolverIterations, + num_internal_pgs_iterations: self.numInternalPgsIterations, + num_internal_stabilization_iterations: self.numInternalStabilizationIterations, + max_ccd_substeps: self.maxCcdSubsteps, + contact_clustering: boolean(self.contactClustering)?, + contact_recycling: boolean(self.contactRecycling)?, + normalized_contact_recycle_distance: nonnegative( + self.normalizedContactRecycleDistance, + )?, + friction_in_bias_pass: boolean(self.frictionInBiasPass)?, + warmstart_joints: boolean(self.warmstartJoints)?, + #[cfg(feature = "dim3")] + friction_model: match self.frictionModel { + 0 => FrictionModel::Simplified, + 1 => FrictionModel::Coulomb, + _ => return Err(invalid("invalid friction_model")), + }, + }) + } +} +/// Return native default integration parameters. This POD value owns no resources. +/// @ingroup worlds +#[rapier_export] +pub extern "C" fn rpr_default_integration_parameters() -> RprIntegrationParameters { + IntegrationParameters::default().into() +} + +/// Return a copy of all world integration settings. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_integration_parameters( + world: *const RprWorld, +) -> RprIntegrationParameters { + ffi_value(|out: *mut RprIntegrationParameters| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + output(out, get(object)?.0.into()) + }) + }) +} + +/// Copies validated values; does not expose a writable alias to Rust memory. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_integration_parameters( + world: *mut RprWorld, + data: *const RprIntegrationParameters, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + crate::handle_access::forward(native_integration_parameters_set_data( + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(), + data, + )) + }) +} + +pub(crate) unsafe fn native_integration_parameters_set_data( + object: *mut NativeIntegrationParameters, + data: *const RprIntegrationParameters, +) -> RprStatus { + ffi(|| unsafe { + let value = get(data)?.raw()?; + get_mut(object)?.0 = value; + Ok(()) + }) +} + +/// Copies the live soft body's material into caller-owned data. +pub(crate) unsafe fn native_soft_body_read_material( + body: *const RprSoftBody, + out: *mut RprSoftBodyMaterial, +) -> RprStatus { + ffi(|| unsafe { output(out, (*get(body)?.0.material()).into()) }) +} +/// Validates all fields before applying the material to a live soft body. +pub(crate) unsafe fn native_soft_body_set_material_data( + body: *mut RprSoftBody, + data: *const RprSoftBodyMaterial, +) -> RprStatus { + ffi(|| unsafe { + let material = get(data)?.raw()?; + get_mut(body)?.0.set_material(material); + Ok(()) + }) +} diff --git a/c/src/control.rs b/c/src/control.rs new file mode 100644 index 000000000..96a8a9149 --- /dev/null +++ b/c/src/control.rs @@ -0,0 +1,867 @@ +use crate::*; +use rapier::control::{ + CharacterAutostep, CharacterCollision, CharacterLength, KinematicCharacterController, +}; +/// Controller plus reusable collision output from the last move_shape call. +/// Kinematic character controller and its last collision list. Release with the matching Free +/// function. +/// @ingroup controllers +pub struct RprKinematicCharacterController { + world: *mut RprWorld, + inner: KinematicCharacterController, + collisions: Vec, +} +/// CharacterLength counterpart: relative=1 scales with character height, relative=0 uses world +/// units. +/// @ingroup controllers +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprCharacterLength { + /// Value used when enabled is 1. + pub value: RprReal, + /// 1 scales value by the character shape size; 0 uses an absolute length. + pub relative: RprBool, +} +impl RprCharacterLength { + fn raw(self) -> Result { + nonnegative(self.value)?; + Ok(if boolean(self.relative)? { + CharacterLength::Relative(self.value) + } else { + CharacterLength::Absolute(self.value) + }) + } +} +/// Allowed character motion and ground-contact state. +/// @ingroup controllers +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprCharacterMovement { + /// Allowed world-space displacement; not applied automatically. + pub translation: RprVector, + /// Whether the character touches the ground after movement. + pub grounded: RprBool, + /// Whether motion includes sliding down a non-climbable slope. + pub is_sliding_down_slope: RprBool, +} +/// Collision recorded during character movement. +/// @ingroup controllers +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprCharacterCollision { + /// World-bound collider handle. + pub collider: RprColliderHandle, + /// World-space character pose at collision. + pub character_pos: RprPose, + /// World-space translation already applied before collision. + pub translation_applied: RprVector, + /// World-space translation remaining at collision. + pub translation_remaining: RprVector, + /// Shape/ray impact details. + pub hit: RprShapeCastHit, +} +/// Allocate a character controller with native defaults; release it with +/// rpr_free_kinematic_character_controller. +/// @ingroup controllers +#[rapier_export] +pub unsafe extern "C" fn rpr_new_kinematic_character_controller() +-> *mut RprKinematicCharacterController { + ffi_value(|out: *mut *mut RprKinematicCharacterController| { + ffi(|| unsafe { + out_ptr(out)?; + output( + out, + Box::into_raw(Box::new(RprKinematicCharacterController { + world: std::ptr::null_mut(), + inner: Default::default(), + collisions: Vec::new(), + })), + ) + }) + }) +} +/// Release an owned kinematic character controller. NULL is allowed. Do not pass borrowed pointers +/// or free the object twice. +/// @ingroup controllers +#[rapier_export] +pub unsafe extern "C" fn rpr_free_kinematic_character_controller( + controller: *mut RprKinematicCharacterController, +) -> RprStatus { + ffi(|| unsafe { + if !controller.is_null() { + get(controller)?; + drop(Box::from_raw(controller)); + } + Ok(()) + }) +} +/// Set the up direction; it must be finite and nonzero and is normalized on input. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_set_up( + controller: *mut RprKinematicCharacterController, + up: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let v = up.raw()?; + positive(v.length())?; + get_mut(controller)?.inner.up = v.normalize(); + Ok(()) + }) +} +/// Set the collision separation margin; use a positive absolute or relative character length. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_set_offset( + controller: *mut RprKinematicCharacterController, + offset: RprCharacterLength, +) -> RprStatus { + ffi(|| unsafe { + positive(offset.value)?; + let offset = offset.raw()?; + get_mut(controller)?.inner.offset = offset; + Ok(()) + }) +} +/// Enable or disable sliding along obstacles. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_set_slide( + controller: *mut RprKinematicCharacterController, + enabled: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let v = boolean(enabled)?; + get_mut(controller)?.inner.slide = v; + Ok(()) + }) +} +/// Set the maximum climb angle and minimum slide angle, in radians. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_set_slopes( + controller: *mut RprKinematicCharacterController, + max_climb_angle: RprReal, + min_slide_angle: RprReal, +) -> RprStatus { + ffi(|| unsafe { + for a in [max_climb_angle, min_slide_angle] { + nonnegative(a)?; + ensure(a <= 2.0 * real_pi(), "angle must be <= 2*pi")?; + } + let c = get_mut(controller)?; + c.inner.max_slope_climb_angle = max_climb_angle; + c.inner.min_slope_slide_angle = min_slide_angle; + Ok(()) + }) +} +/// Configure automatic stepping over obstacles. enabled = 0 disables it. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_set_autostep( + controller: *mut RprKinematicCharacterController, + enabled: RprBool, + max_height: RprCharacterLength, + min_width: RprCharacterLength, + include_dynamic_bodies: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let step = CharacterAutostep { + max_height: max_height.raw()?, + min_width: min_width.raw()?, + include_dynamic_bodies: boolean(include_dynamic_bodies)?, + }; + let value = boolean(enabled)?.then_some(step); + get_mut(controller)?.inner.autostep = value; + Ok(()) + }) +} +/// Configure downward ground snapping. enabled = 0 disables it. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_set_snap_to_ground( + controller: *mut RprKinematicCharacterController, + enabled: RprBool, + distance: RprCharacterLength, +) -> RprStatus { + ffi(|| unsafe { + let d = distance.raw()?; + let value = boolean(enabled)?.then_some(d); + get_mut(controller)?.inner.snap_to_ground = value; + Ok(()) + }) +} +/// Computes movement without moving any collider. Use the returned translation to set the character +/// target. +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_move_shape( + world: *const RprWorld, + options: *const RprQueryOptions, + controller: *mut RprKinematicCharacterController, + dt: RprReal, + shape: *const RprSharedShape, + pose: RprPose, + desired_translation: RprVector, +) -> RprCharacterMovement { + ffi_value(|out: *mut RprCharacterMovement| { + ffi(|| unsafe { + if !options.is_null() { + get(options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, options)?; + let query: *const QueryAccess = &query; + + out_ptr(out)?; + positive(dt)?; + let pose = pose.raw()?; + let translation = desired_translation.raw()?; + let shape = &*get(shape)?.0; + let c = get_mut(controller)?; + c.world = world as *mut RprWorld; + c.collisions.clear(); + let result = get(query)?.with_raw(|q| { + Ok(c.inner + .move_shape(dt, &q, shape, &pose, translation, |hit| { + c.collisions.push(hit) + })) + })?; + output( + out, + RprCharacterMovement { + translation: result.translation.into(), + grounded: result.grounded as u32, + is_sliding_down_slope: result.is_sliding_down_slope as u32, + }, + ) + }) + }) +} + +/// Copy collisions recorded by the most recent MoveShape call. +/// @see @ref output_buffers +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_collisions( + controller: *const RprKinematicCharacterController, + buffer: *mut RprCharacterCollision, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array( + get(controller).map_or(std::ptr::null_mut(), |c| c.world), + buffer, + capacity, + |count: *mut usize| { + ffi(|| { + let values: Vec<_> = get(controller)? + .collisions + .iter() + .map(|c| RprCharacterCollision { + collider: c.handle.into(), + character_pos: c.character_pos.into(), + translation_applied: c.translation_applied.into(), + translation_remaining: c.translation_remaining.into(), + hit: RprShapeCastHit { + collider: c.handle.into(), + time_of_impact: c.hit.time_of_impact, + witness1: c.hit.witness1.into(), + witness2: c.hit.witness2.into(), + normal1: c.hit.normal1.into(), + normal2: c.hit.normal2.into(), + status: c.hit.status as u32, + }, + }) + .collect(); + copy_out(&values, buffer, capacity, count) + }) + }, + ) + } +} +/// Applies impulses for the most recent move_shape collisions. Use the same world, shape, dt and +/// filter. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_solve_character_collision_impulses( + controller: *const RprKinematicCharacterController, + shape: *const RprSharedShape, + dt: RprReal, + mass: RprReal, + filter: *const RprQueryFilter, +) -> RprStatus { + ffi(|| unsafe { + let world = get(controller)?.world; + if !filter.is_null() { + get(filter)?.check_world(world)?; + } + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + positive(dt)?; + positive(mass)?; + let f = if filter.is_null() { + RprQueryFilter::default() + } else { + *get(filter)? + } + .raw()?; + let c = get(controller)?; + let shape = &*get(shape)?.0; + let w = &mut get_mut(world)?.0; + let mut q = w.broad_phase.as_query_pipeline_mut( + w.narrow_phase.query_dispatcher(), + &mut w.bodies, + &mut w.colliders, + f, + ); + c.inner + .solve_character_collision_impulses(dt, &mut q, shape, mass, &c.collisions); + Ok(()) + }) +} + +#[cfg(feature = "dim3")] +mod vehicle { + use super::*; + use rapier::control::{DynamicRayCastVehicleController, WheelTuning}; + /// Vehicle controller borrowing its chassis world. Release with the matching Free function. + /// @ingroup controllers + pub struct RprDynamicRayCastVehicleController(DynamicRayCastVehicleController, *mut RprWorld); + #[repr(C)] + #[derive(Copy, Clone, Default)] + /// Wheel suspension/friction parameters. Initialize with rpr_default_wheel_tuning. + /// @ingroup controllers + pub struct RprWheelTuning { + /// Nonnegative suspension spring stiffness. + pub suspension_stiffness: RprReal, + /// Nonnegative damping coefficient during suspension compression. + pub suspension_compression: RprReal, + /// Nonnegative suspension relaxation damping. + pub suspension_damping: RprReal, + /// Maximum suspension travel in length units. + pub max_suspension_travel: RprReal, + /// Maximum tire friction/slip coefficient. + pub friction_slip: RprReal, + /// Maximum force exerted by suspension. + pub max_suspension_force: RprReal, + /// Sideways tire friction stiffness. + pub side_friction_stiffness: RprReal, + } + impl RprWheelTuning { + fn raw(self) -> Result { + Ok(WheelTuning { + suspension_stiffness: nonnegative(self.suspension_stiffness)?, + suspension_compression: nonnegative(self.suspension_compression)?, + suspension_damping: nonnegative(self.suspension_damping)?, + max_suspension_travel: nonnegative(self.max_suspension_travel)?, + friction_slip: nonnegative(self.friction_slip)?, + max_suspension_force: nonnegative(self.max_suspension_force)?, + side_friction_stiffness: nonnegative(self.side_friction_stiffness)?, + }) + } + } + #[repr(C)] + #[derive(Copy, Clone, Default)] + /// Copy of the current wheel pose, suspension, and contact state. + /// @ingroup controllers + pub struct RprWheelState { + /// World-space wheel center. + pub center: RprVector, + /// World-space suspension direction. + pub suspension: RprVector, + /// World-space axle direction. + pub axle: RprVector, + /// Wheel rotation angle in radians. + pub rotation: RprReal, + /// Current suspension force. + pub suspension_force: RprReal, + /// Current suspension length. + pub suspension_length: RprReal, + /// Whether the wheel has ground contact. + pub is_in_contact: RprBool, + /// Ground collider handle, invalid when there is no contact. + pub ground_object: RprColliderHandle, + /// World-space ground contact point. + pub contact_point: RprVector, + /// World-space ground contact normal. + pub contact_normal: RprVector, + } + /// Return native default wheel tuning. This POD value owns no resources. + /// @ingroup controllers + #[rapier_export] + pub extern "C" fn rpr_default_wheel_tuning() -> RprWheelTuning { + let t = WheelTuning::default(); + RprWheelTuning { + suspension_stiffness: t.suspension_stiffness, + suspension_compression: t.suspension_compression, + suspension_damping: t.suspension_damping, + max_suspension_travel: t.max_suspension_travel, + friction_slip: t.friction_slip, + max_suspension_force: t.max_suspension_force, + side_friction_stiffness: t.side_friction_stiffness, + } + } + /// Allocate a vehicle controller bound to its chassis body. The chassis world must outlive the + /// controller. Release with rpr_free_dynamic_ray_cast_vehicle_controller. + /// @ingroup controllers + #[rapier_export] + pub unsafe extern "C" fn rpr_new_dynamic_ray_cast_vehicle_controller( + chassis: RprRigidBodyHandle, + ) -> *mut RprDynamicRayCastVehicleController { + let world = chassis.world; + ffi_value(|out: *mut *mut RprDynamicRayCastVehicleController| { + ffi(|| unsafe { + chassis.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let bodies: *const RprRigidBodySet = std::ptr::addr_of!((*raw).0.bodies).cast(); + + out_ptr(out)?; + let b = get(bodies)?.0.get(chassis.raw()).ok_or_else(missing)?; + ensure( + b.is_dynamic() && b.soft_body().is_none(), + "vehicle chassis must be an ordinary dynamic body", + )?; + output( + out, + Box::into_raw(Box::new(RprDynamicRayCastVehicleController( + DynamicRayCastVehicleController::new(chassis.raw()), + world, + ))), + ) + }) + }) + } + + /// Release an owned dynamic ray cast vehicle controller. NULL is allowed. Do not pass borrowed + /// pointers or free the object twice. + /// @ingroup controllers + #[rapier_export] + pub unsafe extern "C" fn rpr_free_dynamic_ray_cast_vehicle_controller( + controller: *mut RprDynamicRayCastVehicleController, + ) -> RprStatus { + ffi(|| unsafe { + if !controller.is_null() { + get(controller)?; + drop(Box::from_raw(controller)); + } + Ok(()) + }) + } + /// Append a wheel and return its zero-based index. Connection, suspension direction, and axle + /// are in chassis-local coordinates. + /// @ingroup controllers + #[rapier_export(dynamic_ray_cast_vehicle_controller)] + pub unsafe extern "C" fn rpr_dynamic_ray_cast_vehicle_controller_add_wheel( + controller: *mut RprDynamicRayCastVehicleController, + connection: RprVector, + direction: RprVector, + axle: RprVector, + rest_length: RprReal, + radius: RprReal, + tuning: *const RprWheelTuning, + ) -> usize { + ffi_value(|out_index: *mut usize| { + ffi(|| unsafe { + out_ptr(out_index)?; + let p = connection.raw()?; + let d = direction.raw()?; + let a = axle.raw()?; + positive(d.length())?; + positive(a.length())?; + ensure( + d.cross(a).length_squared() > 1.0e-10, + "wheel direction and axle must not be parallel", + )?; + nonnegative(rest_length)?; + positive(radius)?; + let t = get(tuning)?.raw()?; + let c = &mut get_mut(controller)?.0; + let index = c.wheels().len(); + c.add_wheel(p, d.normalize(), a.normalize(), rest_length, radius, &t); + output(out_index, index) + }) + }) + } + /// Set the chassis up/forward axis indices (0 = X, 1 = Y, 2 = Z). + /// @ingroup controllers + #[rapier_export(dynamic_ray_cast_vehicle_controller)] + pub unsafe extern "C" fn rpr_dynamic_ray_cast_vehicle_controller_set_axes( + controller: *mut RprDynamicRayCastVehicleController, + up: usize, + forward: usize, + ) -> RprStatus { + ffi(|| unsafe { + ensure( + up < 3 && forward < 3 && up != forward, + "invalid vehicle axes", + )?; + let c = &mut get_mut(controller)?.0; + c.index_up_axis = up; + c.index_forward_axis = forward; + Ok(()) + }) + } + /// Set a wheel engine force, brake force, and steering angle in radians. + /// @ingroup controllers + #[rapier_export(dynamic_ray_cast_vehicle_controller)] + pub unsafe extern "C" fn rpr_dynamic_ray_cast_vehicle_controller_set_wheel_controls( + controller: *mut RprDynamicRayCastVehicleController, + index: usize, + steering: RprReal, + engine_force: RprReal, + brake: RprReal, + ) -> RprStatus { + ffi(|| unsafe { + finite(steering)?; + finite(engine_force)?; + nonnegative(brake)?; + let w = get_mut(controller)? + .0 + .wheels_mut() + .get_mut(index) + .ok_or_else(|| invalid("wheel index out of range"))?; + w.steering = steering; + w.engine_force = engine_force; + w.brake = brake; + Ok(()) + }) + } + /// Ray-cast wheel contacts and apply vehicle forces for dt seconds. Does not step the world. + /// @ingroup controllers + #[rapier_export(dynamic_ray_cast_vehicle_controller)] + pub unsafe extern "C" fn rpr_dynamic_ray_cast_vehicle_controller_update_vehicle( + controller: *mut RprDynamicRayCastVehicleController, + dt: RprReal, + filter: *const RprQueryFilter, + ) -> RprStatus { + ffi(|| unsafe { + let world = get(controller)?.1; + if !filter.is_null() { + get(filter)?.check_world(world)?; + } + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + positive(dt)?; + let c = &mut get_mut(controller)?.0; + let mut f = if filter.is_null() { + RprQueryFilter::default() + } else { + *get(filter)? + } + .raw()?; + f.exclude_rigid_body = Some(c.chassis); + let w = &mut get_mut(world)?.0; + let chassis = w.bodies.get(c.chassis).ok_or_else(missing)?; + ensure( + chassis.is_dynamic() && chassis.soft_body().is_none(), + "vehicle chassis must be an ordinary dynamic body", + )?; + let q = w.broad_phase.as_query_pipeline_mut( + w.narrow_phase.query_dispatcher(), + &mut w.bodies, + &mut w.colliders, + f, + ); + c.update_vehicle(dt, q); + Ok(()) + }) + } + + /// Return signed chassis speed along its forward direction. + /// @ingroup controllers + #[rapier_export(dynamic_ray_cast_vehicle_controller)] + pub unsafe extern "C" fn rpr_dynamic_ray_cast_vehicle_controller_current_vehicle_speed( + controller: *const RprDynamicRayCastVehicleController, + ) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { output(out, get(controller)?.0.current_vehicle_speed) }) + }) + } + /// Copy current wheel state in wheel insertion order. + /// @see @ref output_buffers + /// @ingroup controllers + #[rapier_export(dynamic_ray_cast_vehicle_controller)] + pub unsafe extern "C" fn rpr_dynamic_ray_cast_vehicle_controller_wheels( + controller: *const RprDynamicRayCastVehicleController, + buffer: *mut RprWheelState, + capacity: usize, + ) -> usize { + unsafe { + ffi_world_array( + get(controller).map_or(std::ptr::null_mut(), |c| c.1), + buffer, + capacity, + |count: *mut usize| { + ffi(|| { + let v: Vec<_> = get(controller)? + .0 + .wheels() + .iter() + .map(|w| { + let r = w.raycast_info(); + RprWheelState { + center: w.center().into(), + suspension: w.suspension().into(), + axle: w.axle().into(), + rotation: w.rotation, + suspension_force: w.wheel_suspension_force, + suspension_length: r.suspension_length, + is_in_contact: r.is_in_contact as u32, + ground_object: r + .ground_object + .map(Into::into) + .unwrap_or_default(), + contact_point: r.contact_point_ws.into(), + contact_normal: r.contact_normal_ws.into(), + } + }) + .collect(); + copy_out(&v, buffer, capacity, count) + }) + }, + ) + } + } +} +#[cfg(feature = "dim3")] +pub use vehicle::*; + +/// PID controller with persistent integral state. +/// Stateful proportional-integral-derivative controller. Release with the matching Free function. +/// @ingroup controllers +pub struct RprPidController(rapier::control::PidController); + +/// Per-axis proportional, integral, and derivative controller gains. +/// @ingroup controllers +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprPidGains { + /// Linear proportional gain per axis. + pub lin_kp: RprVector, + /// Linear integral gain per axis. + pub lin_ki: RprVector, + /// Linear derivative gain per axis. + pub lin_kd: RprVector, + /// Angular proportional gain per axis. + pub ang_kp: RprAngVector, + /// Angular integral gain per axis. + pub ang_ki: RprAngVector, + /// Angular derivative gain per axis. + pub ang_kd: RprAngVector, +} + +/// Allocate a PID controller with supplied gains and controlled axes. Release with +/// rpr_free_pid_controller. +/// @ingroup controllers +#[rapier_export] +pub unsafe extern "C" fn rpr_new_pid_controller() -> *mut RprPidController { + ffi_value(|out: *mut *mut RprPidController| { + ffi(|| unsafe { + out_ptr(out)?; + output( + out, + Box::into_raw(Box::new(RprPidController(Default::default()))), + ) + }) + }) +} +/// Release an owned pid controller. NULL is allowed. Do not pass borrowed pointers or free the +/// object twice. +/// @ingroup controllers +#[rapier_export] +pub unsafe extern "C" fn rpr_free_pid_controller(controller: *mut RprPidController) -> RprStatus { + ffi(|| unsafe { + if !controller.is_null() { + drop(Box::from_raw(controller)); + } + Ok(()) + }) +} +/// Return a copy of the proportional, integral, and derivative gains. +/// @ingroup controllers +#[rapier_export(pid_controller)] +pub unsafe extern "C" fn rpr_pid_controller_gains( + controller: *const RprPidController, +) -> RprPidGains { + ffi_value(|out: *mut RprPidGains| { + ffi(|| unsafe { + let c = &get(controller)?.0; + output( + out, + RprPidGains { + lin_kp: c.pd.lin_kp.into(), + lin_ki: c.lin_ki.into(), + lin_kd: c.pd.lin_kd.into(), + ang_kp: angular_out(c.pd.ang_kp), + ang_ki: angular_out(c.ang_ki), + ang_kd: angular_out(c.pd.ang_kd), + }, + ) + }) + }) +} +/// Replace the proportional, integral, and derivative gains. +/// @ingroup controllers +#[rapier_export(pid_controller)] +pub unsafe extern "C" fn rpr_pid_controller_set_gains( + controller: *mut RprPidController, + gains: RprPidGains, +) -> RprStatus { + ffi(|| unsafe { + let lin_kp = gains.lin_kp.raw()?; + let lin_ki = gains.lin_ki.raw()?; + let lin_kd = gains.lin_kd.raw()?; + let ang_kp = angular(gains.ang_kp)?; + let ang_ki = angular(gains.ang_ki)?; + let ang_kd = angular(gains.ang_kd)?; + let c = &mut get_mut(controller)?.0; + c.pd.lin_kp = lin_kp; + c.lin_ki = lin_ki; + c.pd.lin_kd = lin_kd; + c.pd.ang_kp = ang_kp; + c.ang_ki = ang_ki; + c.pd.ang_kd = ang_kd; + Ok(()) + }) +} +/// AxesMask bits match Rapier: linear X/Y/Z are 1/2/4, angular X/Y/Z are 8/16/32. +/// @ingroup controllers +#[rapier_export(pid_controller)] +pub unsafe extern "C" fn rpr_pid_controller_set_axes( + controller: *mut RprPidController, + axes: u32, +) -> RprStatus { + ffi(|| unsafe { + let axes = u8::try_from(axes) + .ok() + .and_then(AxesMask::from_bits) + .ok_or_else(|| invalid("unknown PID axes"))?; + get_mut(controller)?.0.set_axes(axes); + Ok(()) + }) +} +/// Compute a velocity correction, preserving the body's state and updating PID integrals. +/// @ingroup controllers +#[rapier_export(pid_controller)] +pub unsafe extern "C" fn rpr_pid_controller_rigid_body_correction( + controller: *mut RprPidController, + dt: RprReal, + body: RprRigidBodyHandle, + target_pose: RprPose, + target_linvel: RprVector, + target_angvel: RprAngVector, +) -> RprVelocityCorrection { + let world = body.world; + ffi_value(|result: *mut RprVelocityCorrection| { + let linear = unsafe { std::ptr::addr_of_mut!((*result).linear) }; + let angular_velocity = unsafe { std::ptr::addr_of_mut!((*result).angularVelocity) }; + + ffi(|| unsafe { + body.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_pid_controller_rigid_body_correction( + controller, + dt, + std::ptr::addr_of!((*raw).0.bodies).cast(), + body, + target_pose, + target_linvel, + target_angvel, + linear, + angular_velocity, + )) + }) + }) +} + +pub(crate) unsafe fn native_pid_controller_rigid_body_correction( + controller: *mut RprPidController, + dt: RprReal, + bodies: *const RprRigidBodySet, + body: RprRigidBodyHandle, + target_pose: RprPose, + target_linvel: RprVector, + target_angvel: RprAngVector, + linear: *mut RprVector, + angular_velocity: *mut RprAngVector, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(linear)?; + out_ptr(angular_velocity)?; + let dt = positive(dt)?; + let pose = target_pose.raw()?; + let velocity = RigidBodyVelocity { + linvel: target_linvel.raw()?, + angvel: angular(target_angvel)?, + }; + let correction = get_mut(controller)?.0.rigid_body_correction( + dt, + get(bodies)?.0.get(body.raw()).ok_or_else(missing)?, + pose, + velocity, + ); + output(linear, correction.linvel.into())?; + output(angular_velocity, angular_out(correction.angvel)) + }) +} + +/// Copy of character sliding, slope, and snapping settings. +/// @ingroup controllers +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprCharacterControllerSettings { + /// Whether obstacle sliding is enabled. + pub slide: RprBool, + /// Maximum climbable slope angle in radians. + pub max_slope_climb_angle: RprReal, + /// Minimum slope angle for sliding, in radians. + pub min_slope_slide_angle: RprReal, + /// Whether downward ground snapping is enabled. + pub snap_to_ground: RprBool, + /// Maximum downward snapping distance. + pub snap_distance: RprCharacterLength, +} +/// Return a copy of slide, slope, and ground-snap settings. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_settings( + controller: *const RprKinematicCharacterController, +) -> RprCharacterControllerSettings { + ffi_value(|out: *mut RprCharacterControllerSettings| { + ffi(|| unsafe { + let c = &get(controller)?.inner; + let snap_distance = match c.snap_to_ground { + Some(CharacterLength::Relative(value)) => RprCharacterLength { value, relative: 1 }, + Some(CharacterLength::Absolute(value)) => RprCharacterLength { value, relative: 0 }, + None => RprCharacterLength { + value: 0.1, + relative: 1, + }, + }; + output( + out, + RprCharacterControllerSettings { + slide: c.slide as u32, + max_slope_climb_angle: c.max_slope_climb_angle, + min_slope_slide_angle: c.min_slope_slide_angle, + snap_to_ground: c.snap_to_ground.is_some() as u32, + snap_distance, + }, + ) + }) + }) +} diff --git a/c/src/descriptors.rs b/c/src/descriptors.rs new file mode 100644 index 000000000..4346d0b1a --- /dev/null +++ b/c/src/descriptors.rs @@ -0,0 +1,741 @@ +//! Caller-owned construction descriptions. Pointers borrow input only until build/insert returns. +#![allow(non_snake_case)] +use crate::*; + +/// Stack-allocated rigid-body construction data. Initialize with rpr_dynamic_rigid_body_desc, +/// rpr_fixed_rigid_body_desc, or a kinematic description constructor. +/// Copying this value is safe; it owns no resources and must never be freed by Rapier. +/// @ingroup rigid_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprRigidBodyDesc { + /// World-space pose. + pub position: RprPose, + /// World-space linear velocity. + pub linvel: RprVector, + /// World-space angular velocity in radians per second. + pub angvel: RprAngVector, + /// RPR_DYNAMIC, RPR_FIXED, RPR_KINEMATIC_POSITION_BASED, or RPR_KINEMATIC_VELOCITY_BASED. + pub bodyType: u32, + /// Multiplier applied to world gravity. + pub gravityScale: RprReal, + /// Nonnegative linear damping coefficient. + pub linearDamping: RprReal, + /// Nonnegative angular damping coefficient. + pub angularDamping: RprReal, + /// Nonnegative mass added to attached collider contributions. + pub additionalMass: RprReal, + /// 1 uses additionalMassProperties; 0 uses additionalMass. + pub useAdditionalMassProperties: RprBool, + /// Additional body-local mass and inertia when enabled. + pub additionalMassProperties: RprMassProperties, + /// Locked-axis bitmask; translations precede rotations. + pub lockedAxes: u8, + /// Whether automatic sleeping is allowed. + pub canSleep: RprBool, + /// Whether the body starts/is asleep. + pub sleeping: RprBool, + /// Whether continuous collision detection is enabled. + pub ccdEnabled: RprBool, + /// Nonnegative prediction distance for soft CCD. + pub softCcdPrediction: RprReal, + /// Whether to allow fast rotations without the native angular-motion clamp. + pub allowFastRotation: RprBool, + /// Whether this setting/object is enabled (0 or 1). + pub enabled: RprBool, + /// Signed dominance group; larger groups dominate smaller groups. + pub dominanceGroup: i8, + /// Extra solver iterations for this body and connected bodies. + pub additionalSolverIterations: usize, + /// Extra PGS iterations for this body. + pub additionalPgsIterations: usize, + /// Whether to include gyroscopic forces (3D). + pub gyroscopicForcesEnabled: RprBool, + /// Application data; Rapier does not own pointers encoded in it. + pub userData: RprUserData, +} +impl RprRigidBodyDesc { + fn new(kind: u32) -> Result { + let b = RigidBodyBuilder::new(body_type(kind)?); + Ok(Self { + position: b.position.into(), + linvel: b.linvel.into(), + angvel: angular_out(b.angvel), + bodyType: kind, + gravityScale: b.gravity_scale, + linearDamping: b.linear_damping, + angularDamping: b.angular_damping, + additionalMass: 0.0, + useAdditionalMassProperties: 0, + additionalMassProperties: MassProperties::default().into(), + lockedAxes: 0, + canSleep: b.can_sleep as _, + sleeping: b.sleeping as _, + ccdEnabled: b.ccd_enabled as _, + softCcdPrediction: b.soft_ccd_prediction, + allowFastRotation: b.allow_fast_rotation as _, + enabled: b.enabled as _, + dominanceGroup: b.dominance_group, + additionalSolverIterations: b.additional_solver_iterations, + additionalPgsIterations: b.additional_pgs_iterations, + gyroscopicForcesEnabled: b.gyroscopic_forces_enabled as _, + userData: b.user_data.into(), + }) + } + pub(crate) fn raw(&self) -> Result { + let axes = + LockedAxes::from_bits(self.lockedAxes).ok_or_else(|| invalid("unknown locked axes"))?; + let mut b = RigidBodyBuilder::new(body_type(self.bodyType)?) + .pose(self.position.raw()?) + .linvel(self.linvel.raw()?) + .angvel(angular(self.angvel)?) + .gravity_scale(finite(self.gravityScale)?) + .linear_damping(nonnegative(self.linearDamping)?) + .angular_damping(nonnegative(self.angularDamping)?) + .locked_axes(axes) + .can_sleep(boolean(self.canSleep)?) + .sleeping(boolean(self.sleeping)?) + .ccd_enabled(boolean(self.ccdEnabled)?) + .soft_ccd_prediction(nonnegative(self.softCcdPrediction)?) + .allow_fast_rotation(boolean(self.allowFastRotation)?) + .enabled(boolean(self.enabled)?) + .dominance_group(self.dominanceGroup) + .additional_solver_iterations(self.additionalSolverIterations) + .additional_pgs_iterations(self.additionalPgsIterations) + .user_data(self.userData.raw()); + b = if boolean(self.useAdditionalMassProperties)? { + b.additional_mass_properties(self.additionalMassProperties.raw()?) + } else { + b.additional_mass(nonnegative(self.additionalMass)?) + }; + let gyro = boolean(self.gyroscopicForcesEnabled)?; + #[cfg(feature = "dim3")] + { + b = b.gyroscopic_forces_enabled(gyro); + } + #[cfg(feature = "dim2")] + let _ = gyro; + Ok(b) + } +} +/// Return a dynamic rigid-body description with native defaults; no allocation. +/// @ingroup rigid_bodies +#[rapier_export] +pub extern "C" fn rpr_dynamic_rigid_body_desc() -> RprRigidBodyDesc { + RprRigidBodyDesc::new(RPR_DYNAMIC).expect("valid body kind") +} + +/// Return a fixed rigid-body description with native defaults; no allocation. +/// @ingroup rigid_bodies +#[rapier_export] +pub extern "C" fn rpr_fixed_rigid_body_desc() -> RprRigidBodyDesc { + RprRigidBodyDesc::new(RPR_FIXED).expect("valid body kind") +} + +/// Return a kinematic position based rigid-body description with native defaults; no allocation. +/// @ingroup rigid_bodies +#[rapier_export] +pub extern "C" fn rpr_kinematic_position_based_rigid_body_desc() -> RprRigidBodyDesc { + RprRigidBodyDesc::new(RPR_KINEMATIC_POSITION_BASED).expect("valid body kind") +} + +/// Return a kinematic velocity based rigid-body description with native defaults; no allocation. +/// @ingroup rigid_bodies +#[rapier_export] +pub extern "C" fn rpr_kinematic_velocity_based_rigid_body_desc() -> RprRigidBodyDesc { + RprRigidBodyDesc::new(RPR_KINEMATIC_VELOCITY_BASED).expect("valid body kind") +} + +/// @ingroup shapes +/// Treat the 2D polyline as oriented when generating contact normals. +#[cfg(feature = "dim2")] +pub const RPR_POLYLINE_ORIENTED: u32 = 1; +/// @ingroup shapes +/// Prepare polyline acceleration data for deformation. +pub const RPR_POLYLINE_DEFORMABLE: u32 = 2; + +/// @ingroup shapes +/// ShapeDesc kind selecting a ball. +pub const RPR_SHAPE_DESC_BALL: u32 = 0; +/// @ingroup shapes +/// ShapeDesc kind selecting a cuboid. +pub const RPR_SHAPE_DESC_CUBOID: u32 = 1; +/// @ingroup shapes +/// ShapeDesc kind selecting a round cuboid. +pub const RPR_SHAPE_DESC_ROUND_CUBOID: u32 = 2; +/// @ingroup shapes +/// ShapeDesc kind selecting a capsule. +pub const RPR_SHAPE_DESC_CAPSULE: u32 = 3; +/// @ingroup shapes +/// ShapeDesc kind selecting a segment. +pub const RPR_SHAPE_DESC_SEGMENT: u32 = 4; +/// @ingroup shapes +/// ShapeDesc kind selecting a triangle. +pub const RPR_SHAPE_DESC_TRIANGLE: u32 = 5; +/// @ingroup shapes +/// ShapeDesc kind selecting a half-space. +pub const RPR_SHAPE_DESC_HALFSPACE: u32 = 6; +/// @ingroup shapes +/// ShapeDesc kind selecting a convex hull. +pub const RPR_SHAPE_DESC_CONVEX_HULL: u32 = 7; +/// @ingroup shapes +/// ShapeDesc kind selecting a triangle mesh. +pub const RPR_SHAPE_DESC_TRIMESH: u32 = 8; +/// @ingroup shapes +/// ShapeDesc kind selecting a polyline. +pub const RPR_SHAPE_DESC_POLYLINE: u32 = 9; +/// @ingroup shapes +/// ShapeDesc kind selecting borrowed shared geometry. +pub const RPR_SHAPE_DESC_SHARED: u32 = 10; +/// @ingroup shapes +/// ShapeDesc kind selecting a heightfield. +pub const RPR_SHAPE_DESC_HEIGHTFIELD: u32 = 11; +/// @ingroup shapes +/// ShapeDesc kind selecting a cylinder. +pub const RPR_SHAPE_DESC_CYLINDER: u32 = 12; +/// @ingroup shapes +/// ShapeDesc kind selecting a cone. +pub const RPR_SHAPE_DESC_CONE: u32 = 13; +/// @ingroup shapes +/// ShapeDesc kind selecting compound child shapes. +pub const RPR_SHAPE_DESC_COMPOUND: u32 = 14; +/// @ingroup shapes +/// ShapeDesc kind selecting a round cylinder. +pub const RPR_SHAPE_DESC_ROUND_CYLINDER: u32 = 15; + +/// Non-owning shape description. Only fields selected by kind are read. +/// a = cuboid half extents, capsule/segment endpoint, triangle vertex, or halfspace normal. +/// b/c = remaining endpoints/vertices. radius is also the rounded-cuboid border radius. +/// Mesh views count edges or triangles; heightfields are column-major. +/// Arrays, compound children, and sharedShape remain borrowed until build/insert returns. +/// @ingroup shapes +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RprShapeDesc { + /// Discriminant selecting which description fields are read. + pub kind: u32, + /// Half extents, endpoint/vertex, or halfspace normal, selected by kind. + pub a: RprVector, + /// Second endpoint or triangle vertex, selected by kind. + pub b: RprVector, + /// Third triangle vertex. + pub c: RprVector, + /// Radius for the selected primitive or recipe. + pub radius: RprReal, + /// Half the height of a cylinder or cone. + pub halfHeight: RprReal, + /// Rounding radius for a rounded shape. + pub borderRadius: RprReal, + /// Borrowed vertex positions. + pub vertices: RprVectorView, + /// Borrowed triangle topology; count is triangles. + pub triangles: RprTriangleView, + /// Borrowed edge topology; count is edges. + pub edges: RprEdgeView, + /// TRIMESH_*, POLYLINE_*, or HEIGHTFIELD_* bitmask selected by kind. + pub flags: u32, + /// Borrowed height samples; 3D uses column-major rows * columns samples. + pub heights: RprRealView, + /// Number of heightfield rows. + pub rows: usize, + /// Number of heightfield columns. + pub columns: usize, + /// Shape scale along each axis. + pub scale: RprVector, + /// Borrowed shared geometry; keep its wrapper alive through build/insert. + pub sharedShape: *const RprSharedShape, + /// Borrowed compound children and their nested geometry views. + pub children: RprCompoundShapeView, +} +/// One compound child with a local pose and borrowed geometry description. +/// @ingroup shapes +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprCompoundShapeDesc { + /// Pose relative to the compound parent. + pub pose: RprPose, + /// Non-owning shape description; build/insert consumes its views synchronously. + pub shape: RprShapeDesc, +} +impl Default for RprShapeDesc { + fn default() -> Self { + Self { + kind: RPR_SHAPE_DESC_BALL, + a: Vector::ZERO.into(), + b: Vector::ZERO.into(), + c: Vector::ZERO.into(), + radius: 0.5, + halfHeight: 0.5, + borderRadius: 0.0, + vertices: RprVectorView::default(), + triangles: RprTriangleView::default(), + edges: RprEdgeView::default(), + flags: 0, + heights: RprRealView::default(), + rows: 0, + columns: 0, + scale: Vector::ONE.into(), + sharedShape: std::ptr::null(), + children: RprCompoundShapeView::default(), + } + } +} +impl RprShapeDesc { + pub(crate) unsafe fn raw(&self) -> Result { + unsafe { self.raw_depth(0) } + } + unsafe fn raw_depth(&self, depth: usize) -> Result { + ensure(depth < 32, "compound shape nesting exceeds 32 levels")?; + Ok(match self.kind { + RPR_SHAPE_DESC_BALL => SharedShape::ball(positive(self.radius)?), + RPR_SHAPE_DESC_CUBOID | RPR_SHAPE_DESC_ROUND_CUBOID => { + let v = self.a.raw()?; + ensure(v.min_element() > 0.0, "half extents must be positive")?; + if self.kind == RPR_SHAPE_DESC_CUBOID { + #[cfg(feature = "dim2")] + { + SharedShape::cuboid(v.x, v.y) + } + #[cfg(feature = "dim3")] + { + SharedShape::cuboid(v.x, v.y, v.z) + } + } else { + let r = nonnegative(self.radius)?; + #[cfg(feature = "dim2")] + { + SharedShape::round_cuboid(v.x, v.y, r) + } + #[cfg(feature = "dim3")] + { + SharedShape::round_cuboid(v.x, v.y, v.z, r) + } + } + } + RPR_SHAPE_DESC_CAPSULE => { + SharedShape::capsule(self.a.raw()?, self.b.raw()?, positive(self.radius)?) + } + RPR_SHAPE_DESC_SEGMENT => SharedShape::segment(self.a.raw()?, self.b.raw()?), + RPR_SHAPE_DESC_TRIANGLE => { + SharedShape::triangle(self.a.raw()?, self.b.raw()?, self.c.raw()?) + } + RPR_SHAPE_DESC_HALFSPACE => { + let n = self.a.raw()?; + positive(n.length())?; + SharedShape::halfspace(n.normalize()) + } + #[cfg(feature = "dim3")] + RPR_SHAPE_DESC_ROUND_CYLINDER => SharedShape::round_cylinder( + positive(self.halfHeight)?, + positive(self.radius)?, + nonnegative(self.borderRadius)?, + ), + RPR_SHAPE_DESC_SHARED => unsafe { get(self.sharedShape)?.0.clone() }, + RPR_SHAPE_DESC_COMPOUND => { + let mut shapes = Vec::new(); + for child in unsafe { input(self.children.data, self.children.count)? } { + shapes.push((child.pose.raw()?, unsafe { + child.shape.raw_depth(depth + 1)? + })); + } + ensure(!shapes.is_empty(), "empty compound shape")?; + SharedShape::compound(shapes) + } + RPR_SHAPE_DESC_CONVEX_HULL | RPR_SHAPE_DESC_TRIMESH | RPR_SHAPE_DESC_POLYLINE => { + let vertices: Vec<_> = unsafe { input(self.vertices.data, self.vertices.count)? } + .iter() + .map(|v| v.raw()) + .collect::>()?; + ensure(!vertices.is_empty(), "empty vertices")?; + if self.kind == RPR_SHAPE_DESC_CONVEX_HULL { + SharedShape::convex_hull(&vertices) + .ok_or_else(|| invalid("degenerate convex hull"))? + } else { + let (data, count, arity) = if self.kind == RPR_SHAPE_DESC_TRIMESH { + (self.triangles.data.cast::(), self.triangles.count, 3) + } else { + (self.edges.data.cast::(), self.edges.count, 2) + }; + let count = count + .checked_mul(arity) + .ok_or_else(|| invalid("index count overflow"))?; + let indices = unsafe { input(data, count)? }; + ensure( + indices.iter().all(|i| (*i as usize) < vertices.len()), + "mesh index out of bounds", + )?; + if self.kind == RPR_SHAPE_DESC_TRIMESH { + let flags = rapier::parry::shape::TriMeshFlags::from_bits( + u16::try_from(self.flags) + .map_err(|_| invalid("unknown trimesh flags"))?, + ) + .ok_or_else(|| invalid("unknown trimesh flags"))?; + SharedShape::trimesh_with_flags( + vertices, + indices + .chunks_exact(3) + .map(|v| [v[0], v[1], v[2]]) + .collect(), + flags, + ) + .map_err(|e| invalid(e.to_string()))? + } else { + let flags = rapier::parry::shape::PolylineFlags::from_bits( + u8::try_from(self.flags) + .map_err(|_| invalid("unknown polyline flags"))?, + ) + .ok_or_else(|| invalid("unknown polyline flags"))?; + SharedShape::new(rapier::parry::shape::Polyline::with_flags( + vertices, + if indices.is_empty() { + None + } else { + Some(indices.chunks_exact(2).map(|v| [v[0], v[1]]).collect()) + }, + flags, + )) + } + } + } + #[cfg(feature = "dim3")] + RPR_SHAPE_DESC_CYLINDER => { + SharedShape::cylinder(positive(self.halfHeight)?, positive(self.radius)?) + } + #[cfg(feature = "dim3")] + RPR_SHAPE_DESC_CONE => { + SharedShape::cone(positive(self.halfHeight)?, positive(self.radius)?) + } + RPR_SHAPE_DESC_HEIGHTFIELD => { + let count = self + .rows + .checked_mul(self.columns) + .ok_or_else(|| invalid("heightfield size overflow"))?; + ensure( + self.heights.count == count, + "heightfield data length does not match dimensions", + )?; + let heights = unsafe { input(self.heights.data, self.heights.count)? }; + for &value in heights { + finite(value)?; + } + let scale = self.scale.raw()?; + ensure( + scale.min_element() > 0.0, + "heightfield scale must be positive", + )?; + #[cfg(feature = "dim2")] + { + ensure( + self.rows >= 2 && self.columns == 1, + "invalid 2D heightfield dimensions", + )?; + SharedShape::heightfield(heights.to_vec(), scale) + } + #[cfg(feature = "dim3")] + { + ensure( + self.rows >= 2 && self.columns >= 2, + "invalid 3D heightfield dimensions", + )?; + SharedShape::heightfield_with_flags( + rapier::parry::utils::Array2::new( + self.rows, + self.columns, + heights.to_vec(), + ), + scale, + rapier::parry::shape::HeightFieldFlags::from_bits( + u8::try_from(self.flags) + .map_err(|_| invalid("unknown heightfield flags"))?, + ) + .ok_or_else(|| invalid("unknown heightfield flags"))?, + ) + } + } + _ => return Err(invalid("unknown or unsupported shape description")), + }) + } +} +/// Return native default shape desc. This POD value owns no resources. +/// @ingroup shapes +#[rapier_export] +pub extern "C" fn rpr_default_shape_desc() -> RprShapeDesc { + RprShapeDesc::default() +} +/// Build an owned shared shape from a description; release it with rpr_free_shared_shape. Borrowed +/// inputs may be released after this call. +/// @ingroup shapes +#[rapier_export(shape_desc)] +pub unsafe extern "C" fn rpr_shape_desc_build(desc: *const RprShapeDesc) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let shape = get(desc)?.raw()?; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} + +/// @ingroup colliders +/// Mass density. +pub const RPR_MASS_DENSITY: u32 = 0; +/// @ingroup colliders +/// Mass total. +pub const RPR_MASS_TOTAL: u32 = 1; +/// @ingroup colliders +/// Mass properties. +pub const RPR_MASS_PROPERTIES: u32 = 2; +/// Copyable collider construction data. Shape inputs are borrowed, never owned. +/// @ingroup colliders +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RprColliderDesc { + /// Non-owning shape description; build/insert consumes its views synchronously. + pub shape: RprShapeDesc, + /// Pose relative to the parent body; world-space for an unparented collider. + pub position: RprPose, + /// RPR_MASS_DENSITY, RPR_MASS_TOTAL, or RPR_MASS_PROPERTIES. + pub massMode: u32, + /// Nonnegative mass per unit volume. + pub density: RprReal, + /// Mass; nonnegative when supplied as input. + pub mass: RprReal, + /// Explicit local mass and inertia when massMode selects them. + pub massProperties: RprMassProperties, + /// Nonnegative friction coefficient. + pub friction: RprReal, + /// Nonnegative restitution coefficient. + pub restitution: RprReal, + /// RPR_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + pub frictionCombineRule: u32, + /// RPR_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + pub restitutionCombineRule: u32, + /// 1 detects intersections without generating contact forces. + pub isSensor: RprBool, + /// Whether this setting/object is enabled (0 or 1). + pub enabled: RprBool, + /// Groups controlling collision detection. + pub collisionGroups: RprInteractionGroups, + /// Groups controlling contact-force solving. + pub solverGroups: RprInteractionGroups, + /// Bitmask of body-type pairs allowed to collide. + pub activeCollisionTypes: u16, + /// Hook flags enabling pair filtering/contact modification. + pub activeHooks: u32, + /// RPR_COLLISION_EVENTS and/or RPR_CONTACT_FORCE_EVENTS. + pub activeEvents: u32, + /// Nonnegative force threshold for force events. + pub contactForceEventThreshold: RprReal, + /// Nonnegative extra separation distance around the collider. + pub contactSkin: RprReal, + /// Application data; Rapier does not own pointers encoded in it. + pub userData: RprUserData, +} +impl Default for RprColliderDesc { + fn default() -> Self { + Self { + shape: RprShapeDesc::default(), + position: Pose::IDENTITY.into(), + massMode: RPR_MASS_DENSITY, + density: 1.0, + mass: 0.0, + massProperties: MassProperties::default().into(), + friction: ColliderBuilder::default_friction(), + restitution: 0.0, + frictionCombineRule: 0, + restitutionCombineRule: 0, + isSensor: 0, + enabled: 1, + collisionGroups: InteractionGroups::all().into(), + solverGroups: InteractionGroups::all().into(), + activeCollisionTypes: ActiveCollisionTypes::default().bits(), + activeHooks: 0, + activeEvents: 0, + contactForceEventThreshold: 0.0, + contactSkin: 0.0, + userData: 0u128.into(), + } + } +} +impl RprColliderDesc { + pub(crate) unsafe fn raw(&self) -> Result { + let mut b = ColliderBuilder::new(unsafe { self.shape.raw()? }) + .position(self.position.raw()?) + .friction(nonnegative(self.friction)?) + .restitution(nonnegative(self.restitution)?) + .sensor(boolean(self.isSensor)?) + .enabled(boolean(self.enabled)?) + .collision_groups(self.collisionGroups.raw()?) + .solver_groups(self.solverGroups.raw()?) + .user_data(self.userData.raw()) + .friction_combine_rule(combine(self.frictionCombineRule)?) + .restitution_combine_rule(combine(self.restitutionCombineRule)?) + .active_collision_types( + ActiveCollisionTypes::from_bits(self.activeCollisionTypes) + .ok_or_else(|| invalid("unknown collision types"))?, + ) + .active_hooks( + ActiveHooks::from_bits(self.activeHooks).ok_or_else(|| invalid("unknown hooks"))?, + ) + .active_events( + ActiveEvents::from_bits(self.activeEvents) + .ok_or_else(|| invalid("unknown events"))?, + ) + .contact_force_event_threshold(nonnegative(self.contactForceEventThreshold)?) + .contact_skin(nonnegative(self.contactSkin)?); + b = match self.massMode { + RPR_MASS_DENSITY => b.density(nonnegative(self.density)?), + RPR_MASS_TOTAL => b.mass(nonnegative(self.mass)?), + RPR_MASS_PROPERTIES => b.mass_properties(self.massProperties.raw()?), + _ => return Err(invalid("unknown mass mode")), + }; + Ok(b) + } +} +/// Return native default collider desc. This POD value owns no resources. +/// @ingroup colliders +#[rapier_export] +pub extern "C" fn rpr_default_collider_desc() -> RprColliderDesc { + RprColliderDesc::default() +} +/// Return a ball description with the supplied radius. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[rapier_export] +pub extern "C" fn rpr_ball_collider_desc(radius: RprReal) -> RprColliderDesc { + let mut d = RprColliderDesc::default(); + d.shape.radius = radius; + d +} +/// Return an axis-aligned box description with the supplied half-extents. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[rapier_export] +pub extern "C" fn rpr_cuboid_collider_desc(half_extents: RprVector) -> RprColliderDesc { + let mut d = RprColliderDesc::default(); + d.shape.kind = RPR_SHAPE_DESC_CUBOID; + d.shape.a = half_extents; + d +} +/// Create a body from the description and return its world-bound handle. The world owns the body. +/// @ingroup rigid_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_insert_rigid_body( + world: *mut RprWorld, + desc: *const RprRigidBodyDesc, +) -> RprRigidBodyHandle { + ffi_world_value(world, |out: *mut RprRigidBodyHandle| { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + if !out.is_null() { + out_ptr(out)?; + } + let body = get(desc)?.raw()?.build(); + let h = get_mut(world)?.0.insert_body(body); + if !out.is_null() { + output(out, h.into())?; + } + Ok(()) + }) + }) +} + +/// Insert a collider attached to a rigid body, using the world stored in its handle. +/// The parent handle is copied by value. The description is borrowed through this call. +/// Invalid or removed parents fail without inserting a collider. +/// @ingroup colliders +#[rapier_export] +pub unsafe extern "C" fn rpr_insert_collider( + parent: RprRigidBodyHandle, + desc: *const RprColliderDesc, +) -> RprColliderHandle { + let world = parent.world; + ffi_world_value(world, |out| { + ffi(|| unsafe { + if world.is_null() { + return Err(missing()); + } + let access = get(world)?.write()?; + let world = &mut (*access.raw()).0; + let parent = parent.raw(); + world.bodies.get(parent).ok_or_else(missing)?; + let collider = get(desc)?.raw()?.build(); + output(out, world.insert_collider(collider, Some(parent)).into()) + }) + }) +} + +/// Insert a collider without a rigid-body parent. The world owns the collider. +/// The description is borrowed through this call. +/// @ingroup colliders +#[rapier_export] +pub unsafe extern "C" fn rpr_insert_collider_without_parent( + world: *mut RprWorld, + desc: *const RprColliderDesc, +) -> RprColliderHandle { + ffi_world_value(world, |out| { + ffi(|| unsafe { + let access = get(world)?.write()?; + let world = &mut (*access.raw()).0; + let collider = get(desc)?.raw()?.build(); + output(out, world.insert_collider(collider, None).into()) + }) + }) +} + +/// Sizes of the POD types in this library build, for foreign-language layout checks. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprPodLayout { + /// Size in bytes of the rigidBodyDesc structure; zero when unavailable. + pub rigidBodyDesc: usize, + /// Size in bytes of the colliderDesc structure; zero when unavailable. + pub colliderDesc: usize, + /// Size in bytes of the shapeDesc structure; zero when unavailable. + pub shapeDesc: usize, + /// Size in bytes of the jointDesc structure; zero when unavailable. + pub jointDesc: usize, + /// Size in bytes of the softBodyMaterial structure; zero when unavailable. + pub softBodyMaterial: usize, + /// Size in bytes of the integrationParameters structure; zero when unavailable. + pub integrationParameters: usize, + /// Size in bytes of the softBodyDesc structure; zero when unavailable. + pub softBodyDesc: usize, + /// Size in bytes of the softMeshBindingDesc structure; zero when unavailable. + pub softMeshBindingDesc: usize, + /// Size in bytes of the queryOptions structure; zero when unavailable. + pub queryOptions: usize, + /// Zero unless 3D f32 robotics is enabled. + pub urdfLoaderOptions: usize, + /// Zero unless 3D f32 robotics is enabled. + pub mjcfLoaderOptions: usize, +} +/// Return POD structure sizes for checking foreign-language layouts against this library. +/// @ingroup errors +#[rapier_export] +pub extern "C" fn rpr_pod_layout() -> RprPodLayout { + RprPodLayout { + rigidBodyDesc: size_of::(), + colliderDesc: size_of::(), + shapeDesc: size_of::(), + jointDesc: size_of::(), + softBodyMaterial: size_of::(), + integrationParameters: size_of::(), + softBodyDesc: size_of::(), + softMeshBindingDesc: size_of::(), + queryOptions: size_of::(), + #[cfg(all(feature = "robotics", feature = "dim3", feature = "f32"))] + urdfLoaderOptions: size_of::(), + #[cfg(not(all(feature = "robotics", feature = "dim3", feature = "f32")))] + urdfLoaderOptions: 0, + #[cfg(all(feature = "robotics", feature = "dim3", feature = "f32"))] + mjcfLoaderOptions: size_of::(), + #[cfg(not(all(feature = "robotics", feature = "dim3", feature = "f32")))] + mjcfLoaderOptions: 0, + } +} diff --git a/c/src/dynamics.rs b/c/src/dynamics.rs new file mode 100644 index 000000000..3770cd3cc --- /dev/null +++ b/c/src/dynamics.rs @@ -0,0 +1,798 @@ +use crate::*; + +pub(crate) unsafe fn native_rigid_body_position( + object: *const RprRigidBody, + out: *mut RprPose, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, (*object.0.position()).into()) + }) +} + +pub(crate) unsafe fn native_rigid_body_next_position( + object: *const RprRigidBody, + out: *mut RprPose, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, (*object.0.next_position()).into()) + }) +} + +pub(crate) unsafe fn native_rigid_body_translation( + object: *const RprRigidBody, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.translation().into()) + }) +} + +pub(crate) unsafe fn native_rigid_body_rotation( + object: *const RprRigidBody, + out: *mut RprRotation, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, (*object.0.rotation()).into()) + }) +} + +pub(crate) unsafe fn native_rigid_body_linvel( + object: *const RprRigidBody, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.linvel().into()) + }) +} + +pub(crate) unsafe fn native_rigid_body_angvel( + object: *const RprRigidBody, + out: *mut RprAngVector, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, angular_out(object.0.angvel())) + }) +} + +pub(crate) unsafe fn native_rigid_body_center_of_mass( + object: *const RprRigidBody, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.center_of_mass().into()) + }) +} + +pub(crate) unsafe fn native_rigid_body_local_center_of_mass( + object: *const RprRigidBody, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.local_center_of_mass().into()) + }) +} + +pub(crate) unsafe fn native_rigid_body_user_force( + object: *const RprRigidBody, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.user_force().into()) + }) +} + +pub(crate) unsafe fn native_rigid_body_user_torque( + object: *const RprRigidBody, + out: *mut RprAngVector, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, angular_out(object.0.user_torque())) + }) +} + +pub(crate) unsafe fn native_rigid_body_body_type( + object: *const RprRigidBody, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.body_type() as u32) + }) +} + +pub(crate) unsafe fn native_rigid_body_user_data( + object: *const RprRigidBody, + out: *mut RprUserData, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.user_data.into()) + }) +} + +pub(crate) unsafe fn native_rigid_body_mass( + object: *const RprRigidBody, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.mass()) + }) +} + +pub(crate) unsafe fn native_rigid_body_gravity_scale( + object: *const RprRigidBody, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.gravity_scale()) + }) +} + +pub(crate) unsafe fn native_rigid_body_linear_damping( + object: *const RprRigidBody, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.linear_damping()) + }) +} + +pub(crate) unsafe fn native_rigid_body_angular_damping( + object: *const RprRigidBody, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.angular_damping()) + }) +} + +pub(crate) unsafe fn native_rigid_body_kinetic_energy( + object: *const RprRigidBody, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.kinetic_energy()) + }) +} + +pub(crate) unsafe fn native_rigid_body_soft_ccd_prediction( + object: *const RprRigidBody, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.soft_ccd_prediction()) + }) +} + +pub(crate) unsafe fn native_rigid_body_is_sleeping( + object: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_sleeping() as u32) + }) +} + +pub(crate) unsafe fn native_rigid_body_is_enabled( + object: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_enabled() as u32) + }) +} + +pub(crate) unsafe fn native_rigid_body_is_ccd_enabled( + object: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_ccd_enabled() as u32) + }) +} + +pub(crate) unsafe fn native_rigid_body_is_dynamic( + object: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_dynamic() as u32) + }) +} + +/// The soft body owning this proxy, or an invalid handle for an ordinary rigid body. +pub(crate) unsafe fn native_rigid_body_soft_body( + object: *const RprRigidBody, + out: *mut RprSoftBodyHandle, +) -> RprStatus { + ffi(|| unsafe { + output( + out, + get(object)? + .0 + .soft_body() + .map(Into::into) + .unwrap_or_default(), + ) + }) +} + +pub(crate) unsafe fn native_rigid_body_is_soft_frame( + object: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { output(out, get(object)?.0.is_soft_frame() as RprBool) }) +} + +pub(crate) unsafe fn native_rigid_body_is_fixed( + object: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_fixed() as u32) + }) +} + +pub(crate) unsafe fn native_rigid_body_is_kinematic( + object: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_kinematic() as u32) + }) +} + +pub(crate) unsafe fn native_rigid_body_is_moving( + object: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_moving() as u32) + }) +} + +pub(crate) unsafe fn native_rigid_body_is_ccd_active( + object: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_ccd_active() as u32) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_position( + object: *mut RprRigidBody, + value: RprPose, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.set_position(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_translation( + object: *mut RprRigidBody, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.set_translation(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_rotation( + object: *mut RprRigidBody, + value: RprRotation, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.set_rotation(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_linvel( + object: *mut RprRigidBody, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.set_linvel(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_angvel( + object: *mut RprRigidBody, + value: RprAngVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = angular(value)?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.set_angvel(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_body_type( + object: *mut RprRigidBody, + value: u32, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = body_type(value)?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.set_body_type(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_next_kinematic_position( + object: *mut RprRigidBody, + value: RprPose, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let object = get_mut(object)?; + object.0.set_next_kinematic_position(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_next_kinematic_translation( + object: *mut RprRigidBody, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let object = get_mut(object)?; + object.0.set_next_kinematic_translation(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_next_kinematic_rotation( + object: *mut RprRigidBody, + value: RprRotation, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let object = get_mut(object)?; + object.0.set_next_kinematic_rotation(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_gravity_scale( + object: *mut RprRigidBody, + value: RprReal, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = finite(value)?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.set_gravity_scale(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_additional_mass( + object: *mut RprRigidBody, + value: RprReal, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.set_additional_mass(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_linear_damping( + object: *mut RprRigidBody, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let object = get_mut(object)?; + object.0.set_linear_damping(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_angular_damping( + object: *mut RprRigidBody, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let object = get_mut(object)?; + object.0.set_angular_damping(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_soft_ccd_prediction( + object: *mut RprRigidBody, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let object = get_mut(object)?; + object.0.set_soft_ccd_prediction(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_enabled( + object: *mut RprRigidBody, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + let object = get_mut(object)?; + object.0.set_enabled(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_ccd_enabled( + object: *mut RprRigidBody, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + let object = get_mut(object)?; + object.0.enable_ccd(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_translations_locked( + object: *mut RprRigidBody, + value: RprBool, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.lock_translations(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_rotations_locked( + object: *mut RprRigidBody, + value: RprBool, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.lock_rotations(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_dominance_group( + object: *mut RprRigidBody, + value: i8, +) -> RprStatus { + ffi(|| unsafe { + let object = get_mut(object)?; + object.0.set_dominance_group(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_additional_solver_iterations( + object: *mut RprRigidBody, + value: usize, +) -> RprStatus { + ffi(|| unsafe { + let object = get_mut(object)?; + object.0.set_additional_solver_iterations(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_additional_pgs_iterations( + object: *mut RprRigidBody, + value: usize, +) -> RprStatus { + ffi(|| unsafe { + let object = get_mut(object)?; + object.0.set_additional_pgs_iterations(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_add_force( + object: *mut RprRigidBody, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.add_force(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_apply_impulse( + object: *mut RprRigidBody, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.apply_impulse(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_add_torque( + object: *mut RprRigidBody, + value: RprAngVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = angular(value)?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.add_torque(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_apply_torque_impulse( + object: *mut RprRigidBody, + value: RprAngVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = angular(value)?; + let wake_up = boolean(wake_up)?; + let object = get_mut(object)?; + object.0.apply_torque_impulse(value, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_user_data( + object: *mut RprRigidBody, + value: RprUserData, +) -> RprStatus { + ffi(|| unsafe { + get_mut(object)?.0.user_data = value.raw(); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_add_force_at_point( + object: *mut RprRigidBody, + value: RprVector, + point: RprVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let point = point.raw()?; + let wake_up = boolean(wake_up)?; + get_mut(object)?.0.add_force_at_point(value, point, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_apply_impulse_at_point( + object: *mut RprRigidBody, + value: RprVector, + point: RprVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let point = point.raw()?; + let wake_up = boolean(wake_up)?; + get_mut(object)? + .0 + .apply_impulse_at_point(value, point, wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_reset_forces( + object: *mut RprRigidBody, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let wake_up = boolean(wake_up)?; + get_mut(object)?.0.reset_forces(wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_reset_torques( + object: *mut RprRigidBody, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let wake_up = boolean(wake_up)?; + get_mut(object)?.0.reset_torques(wake_up); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_sleep(object: *mut RprRigidBody) -> RprStatus { + ffi(|| unsafe { + get_mut(object)?.0.sleep(); + Ok(()) + }) +} + +pub(crate) unsafe fn native_rigid_body_velocity_at_point( + object: *const RprRigidBody, + point: RprVector, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { output(out, get(object)?.0.velocity_at_point(point.raw()?).into()) }) +} + +pub(crate) unsafe fn native_rigid_body_colliders( + object: *const RprRigidBody, + buffer: *mut RprColliderHandle, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let values: Vec<_> = get(object)? + .0 + .colliders() + .iter() + .copied() + .map(Into::into) + .collect(); + copy_out(&values, buffer, capacity, count) + }) +} + +/// Propagate all modified body poses to attached colliders. Run collision detection or step before +/// querying the broad phase. +/// @ingroup rigid_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_rigid_body_propagate_modified_body_positions_to_colliders( + world: *mut RprWorld, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *const RprRigidBodySet = std::ptr::addr_of!((*raw).0.bodies).cast(); + let colliders: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + get(set)? + .0 + .propagate_modified_body_positions_to_colliders(&mut get_mut(colliders)?.0); + Ok(()) + }) +} + +#[cfg(feature = "dim3")] +pub(crate) unsafe fn native_rigid_body_gyroscopic_forces_enabled( + body: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { output(out, get(body)?.0.gyroscopic_forces_enabled() as RprBool) }) +} + +#[cfg(feature = "dim3")] +pub(crate) unsafe fn native_rigid_body_set_gyroscopic_forces_enabled( + body: *mut RprRigidBody, + enabled: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let enabled = boolean(enabled)?; + get_mut(body)?.0.enable_gyroscopic_forces(enabled); + Ok(()) + }) +} + +/// Copies the island manager's active body handles. +/// @see @ref output_buffers +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_active_rigid_bodies( + world: *const RprWorld, + buffer: *mut RprRigidBodyHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + + let islands: *const RprIslandManager = std::ptr::addr_of!((*raw).0.islands).cast(); + + let handles: Vec = + get(islands)?.0.active_bodies().map(Into::into).collect(); + copy_out(&handles, buffer, capacity, count) + }) + }) + } +} + +/// Wake a body by handle, including a soft-body cluster proxy. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_wake_up( + handle: RprRigidBodyHandle, + strong: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let strong = boolean(strong)?; + get_mut(set)? + .0 + .get_mut(handle.raw()) + .ok_or_else(missing)? + .wake_up(strong); + Ok(()) + }) +} diff --git a/c/src/error.rs b/c/src/error.rs new file mode 100644 index 000000000..65437fcae --- /dev/null +++ b/c/src/error.rs @@ -0,0 +1,260 @@ +use crate::*; +use std::{ + cell::{Cell, RefCell}, + ffi::{CString, c_char, c_void}, + panic::{AssertUnwindSafe, catch_unwind}, +}; + +/// Status-returning operations use these integer codes. +/// @ingroup errors +pub type RprStatus = u32; +/// @ingroup errors +/// Operation succeeded. +pub const RPR_OK: RprStatus = 0; +/// @ingroup errors +/// A required pointer was NULL. +pub const RPR_NULL_POINTER: RprStatus = 1; +/// @ingroup errors +/// An argument failed validation. +pub const RPR_INVALID_ARGUMENT: RprStatus = 2; +/// @ingroup errors +/// The entity handle is stale, invalid, or belongs to another world. +pub const RPR_INVALID_HANDLE: RprStatus = 3; +/// @ingroup errors +/// Output capacity is insufficient; the returned count is the required capacity. +pub const RPR_BUFFER_TOO_SMALL: RprStatus = 4; +/// @ingroup errors +/// This build or object does not support the operation. +pub const RPR_UNSUPPORTED: RprStatus = 5; +/// @ingroup errors +/// Rust panicked; discard objects mutated by the call. +pub const RPR_PANIC: RprStatus = 6; +/// @ingroup errors +/// No matching query result or object was found. +pub const RPR_NOT_FOUND: RprStatus = 7; +/// @ingroup errors +/// Conflicting or reentrant access to simulation state. No mutation was performed. +pub const RPR_WORLD_BUSY: RprStatus = 8; +pub(crate) type Result = std::result::Result; +thread_local! { static LAST_STATUS: Cell = const { Cell::new(RPR_OK) }; } +thread_local! { static LAST_ERROR: RefCell = RefCell::new(CString::default()); } +/// Called synchronously on the calling thread when an operation reports an error. +/// The diagnostic is borrowed for the duration of the callback. The callback +/// must return normally or terminate the process: never throw or longjmp across +/// the Rust/C boundary. Nested failing calls do not invoke the handler recursively. +/// @ingroup errors +pub type RprErrorCallback = Option; + +/// An optional thread-local error handler. A null callback disables reporting. +/// Keep the callback and user_data alive until the handler is replaced. +/// @ingroup errors +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprErrorHandler { + /// Optional error callback; NULL disables notifications. + pub callback: RprErrorCallback, + /// Application data; Rapier does not own pointers encoded in it. + pub user_data: *mut c_void, +} + +thread_local! { + static ERROR_HANDLER: Cell = const { Cell::new(RprErrorHandler { + callback: None, + user_data: std::ptr::null_mut(), + }) }; + static FFI_DEPTH: Cell = const { Cell::new(0) }; +} + +/// Replace this thread's error handler and return the previous handler so it can +/// be restored at the end of a scope. Status returns are unchanged. A handler +/// that returns lets the caller recover by checking the status; a fail-fast +/// handler may terminate the process. Includes RPR_NOT_FOUND query misses. +/// @ingroup errors +#[rapier_export] +pub unsafe extern "C" fn rpr_set_error_handler(handler: RprErrorHandler) -> RprErrorHandler { + ERROR_HANDLER.with(|current| current.replace(handler)) +} + +pub(crate) fn invalid(message: impl Into) -> (RprStatus, String) { + (RPR_INVALID_ARGUMENT, message.into()) +} +pub(crate) fn missing() -> (RprStatus, String) { + (RPR_INVALID_HANDLE, "invalid or stale handle".into()) +} +pub(crate) fn ensure(condition: bool, message: &str) -> Result { + if condition { + Ok(()) + } else { + Err(invalid(message)) + } +} +pub(crate) fn finite(value: Real) -> Result { + ensure(value.is_finite(), "expected a finite number")?; + Ok(value) +} +pub(crate) fn nonnegative(value: Real) -> Result { + finite(value)?; + ensure(value >= 0.0, "expected a nonnegative number")?; + Ok(value) +} +pub(crate) fn positive(value: Real) -> Result { + finite(value)?; + ensure(value > 0.0, "expected a positive number")?; + Ok(value) +} +pub(crate) fn boolean(value: u32) -> Result { + ensure(value <= 1, "boolean must be 0 or 1")?; + Ok(value != 0) +} +struct FfiCall; +impl Drop for FfiCall { + fn drop(&mut self) { + FFI_DEPTH.with(|depth| depth.set(depth.get() - 1)); + } +} + +pub(crate) fn ffi(f: impl FnOnce() -> Result) -> RprStatus { + let outermost = FFI_DEPTH.with(|depth| { + let previous = depth.get(); + depth.set(previous + 1); + previous == 0 + }); + let _call = FfiCall; + let result = catch_unwind(AssertUnwindSafe(f)).unwrap_or_else(|payload| { + let msg = payload + .downcast_ref::<&str>() + .copied() + .or_else(|| payload.downcast_ref::().map(String::as_str)) + .unwrap_or("Rust panic"); + Err(( + RPR_PANIC, + format!("Rapier panic: {msg}; discard objects mutated by this call"), + )) + }); + match result { + Ok(()) => { + LAST_STATUS.with(|s| s.set(RPR_OK)); + LAST_ERROR.with(|e| *e.borrow_mut() = CString::default()); + RPR_OK + } + Err((status, message)) => { + LAST_STATUS.with(|s| s.set(status)); + let message = CString::new(message.replace('\0', "?")).unwrap(); + LAST_ERROR.with(|e| *e.borrow_mut() = message.clone()); + let handler = ERROR_HANDLER.with(Cell::get); + if let Some(callback) = handler.callback { + if outermost { + // All operation-local borrows and the panic boundary have ended. + // Keep the diagnostic alive even if the handler calls Rapier again. + unsafe { callback(status, message.as_ptr(), handler.user_data) }; + LAST_ERROR.with(|e| *e.borrow_mut() = message); + LAST_STATUS.with(|s| s.set(status)); + } + } + status + } + } +} +/// Status of the most recent fallible operation on this thread. Reading this or +/// LastError does not clear it. Infallible value constructors do not change it. +/// Check immediately after a fallible value-returning operation when recovering +/// from errors instead of using a fail-fast error callback. +/// @ingroup errors +#[rapier_export] +pub extern "C" fn rpr_last_status() -> RprStatus { + LAST_STATUS.with(Cell::get) +} + +/// Capture a newly produced value without exposing an output pointer in the ABI. +/// Errors return the type's default value. A short-buffer error preserves the +/// required length so callers can resize and retry. +pub(crate) fn ffi_value(f: impl FnOnce(*mut T) -> RprStatus) -> T { + let mut value = T::default(); + let status = f(&mut value); + if status != RPR_OK && status != RPR_BUFFER_TOO_SMALL { + return T::default(); + } + value +} + +/// Thread-local UTF-8 diagnostic, valid until the next fallible call on this thread. +/// @ingroup errors +#[rapier_export] +pub extern "C" fn rpr_last_error() -> *const c_char { + LAST_ERROR.with(|e| e.borrow().as_ptr()) +} +/// These helpers check null and alignment, not allocation validity or ownership. +pub(crate) unsafe fn get<'a, T>(p: *const T) -> Result<&'a T> { + if p.is_null() { + return Err((RPR_NULL_POINTER, "null pointer".into())); + } + ensure(p.is_aligned(), "misaligned pointer")?; + Ok(unsafe { &*p }) +} +pub(crate) unsafe fn get_mut<'a, T>(p: *mut T) -> Result<&'a mut T> { + if p.is_null() { + return Err((RPR_NULL_POINTER, "null pointer".into())); + } + ensure(p.is_aligned(), "misaligned pointer")?; + Ok(unsafe { &mut *p }) +} +pub(crate) unsafe fn input<'a, T>(p: *const T, count: usize) -> Result<&'a [T]> { + if count == 0 { + return Ok(&[]); + } + unsafe { + get(p)?; + } + ensure( + count <= isize::MAX as usize / std::mem::size_of::().max(1), + "array is too large", + )?; + Ok(unsafe { std::slice::from_raw_parts(p, count) }) +} +pub(crate) unsafe fn output(p: *mut T, value: T) -> Result { + if p.is_null() { + return Err((RPR_NULL_POINTER, "null output pointer".into())); + } + ensure(p.is_aligned(), "misaligned output pointer")?; + unsafe { + p.write(value); + } + Ok(()) +} +pub(crate) unsafe fn out_ptr(p: *mut T) -> Result { + if p.is_null() { + return Err((RPR_NULL_POINTER, "null output pointer".into())); + } + ensure(p.is_aligned(), "misaligned output pointer") +} +/// A null buffer with capacity zero is a successful size query. Otherwise no partial writes. +pub(crate) unsafe fn copy_out( + values: &[T], + buffer: *mut T, + capacity: usize, + count: *mut usize, +) -> Result { + unsafe { + out_ptr(count)?; + } + if buffer.is_null() && capacity == 0 { + return unsafe { output(count, values.len()) }; + } + ensure( + capacity <= isize::MAX as usize / std::mem::size_of::().max(1), + "buffer is too large", + )?; + unsafe { + out_ptr(buffer)?; + } + unsafe { + output(count, values.len())?; + } + if capacity < values.len() { + return Err((RPR_BUFFER_TOO_SMALL, "output buffer is too small".into())); + } + unsafe { + std::ptr::copy_nonoverlapping(values.as_ptr(), buffer, values.len()); + } + Ok(()) +} diff --git a/c/src/extra.rs b/c/src/extra.rs new file mode 100644 index 000000000..7505b75e5 --- /dev/null +++ b/c/src/extra.rs @@ -0,0 +1,671 @@ +use crate::*; +use rapier::geometry::ContactPair; +/// Explicit mass and principal inertia, matching MassProperties constructors. Zero mass/inertia +/// means infinite. +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprMassProperties { + /// Center of mass in local coordinates. + pub local_com: RprVector, + /// Mass; nonnegative when supplied as input. + pub mass: RprReal, + /// Principal angular inertia; scalar in 2D, three diagonal entries in 3D. + pub principal_inertia: RprAngVector, + #[cfg(feature = "dim3")] + /// Orientation of principal inertia axes in local coordinates (3D). + pub principal_inertia_local_frame: RprRotation, +} +impl RprMassProperties { + pub(crate) fn raw(self) -> Result { + let com = self.local_com.raw()?; + nonnegative(self.mass)?; + #[cfg(feature = "dim2")] + { + Ok(MassProperties::new( + com, + self.mass, + nonnegative(self.principal_inertia)?, + )) + } + #[cfg(feature = "dim3")] + { + let i = self.principal_inertia.raw()?; + ensure(i.min_element() >= 0.0, "negative inertia")?; + Ok(MassProperties::with_principal_inertia_frame( + com, + self.mass, + i, + self.principal_inertia_local_frame.raw()?, + )) + } + } +} +impl From for RprMassProperties { + fn from(m: MassProperties) -> Self { + Self { + local_com: m.local_com.into(), + mass: m.mass(), + principal_inertia: angular_out(m.principal_inertia()), + #[cfg(feature = "dim3")] + principal_inertia_local_frame: m.principal_inertia_local_frame.into(), + } + } +} +pub(crate) unsafe fn native_rigid_body_set_additional_mass_properties( + body: *mut RprRigidBody, + properties: RprMassProperties, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let p = properties.raw()?; + let wake_up = boolean(wake_up)?; + get_mut(body)?.0.set_additional_mass_properties(p, wake_up); + Ok(()) + }) +} +pub(crate) unsafe fn native_rigid_body_recompute_mass_properties_from_colliders( + body: *mut RprRigidBody, + colliders: *const RprColliderSet, +) -> RprStatus { + ffi(|| unsafe { + get_mut(body)? + .0 + .recompute_mass_properties_from_colliders(&get(colliders)?.0); + Ok(()) + }) +} +pub(crate) unsafe fn native_collider_set_mass_properties( + collider: *mut RprCollider, + properties: RprMassProperties, +) -> RprStatus { + ffi(|| unsafe { + let p = properties.raw()?; + get_mut(collider)?.0.set_mass_properties(p); + Ok(()) + }) +} +pub(crate) unsafe fn native_collider_mass_properties( + collider: *const RprCollider, + out: *mut RprMassProperties, +) -> RprStatus { + ffi(|| unsafe { output(out, get(collider)?.0.mass_properties().into()) }) +} +pub(crate) unsafe fn native_rigid_body_set_locked_axes( + body: *mut RprRigidBody, + axes: u8, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let a = LockedAxes::from_bits(axes).ok_or_else(|| invalid("unknown locked axes"))?; + let w = boolean(wake_up)?; + get_mut(body)?.0.set_locked_axes(a, w); + Ok(()) + }) +} +pub(crate) unsafe fn native_rigid_body_locked_axes( + body: *const RprRigidBody, + out: *mut u8, +) -> RprStatus { + ffi(|| unsafe { output(out, get(body)?.0.locked_axes().bits()) }) +} + +/// Create an owned heightfield shape from copied samples. 3D samples are column-major, with rows * +/// columns entries. Release with rpr_free_shared_shape. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_heightfield_shared_shape( + heights: RprRealView, + rows: usize, + columns: usize, + scale: RprVector, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let n = rows + .checked_mul(columns) + .ok_or_else(|| invalid("heightfield dimensions overflow"))?; + ensure( + heights.count == n, + "heightfield data length does not match dimensions", + )?; + let data = input(heights.data, heights.count)?; + for &h in data { + finite(h)?; + } + let scale = scale.raw()?; + ensure( + scale.min_element() > 0.0, + "heightfield scale must be positive", + )?; + #[cfg(feature = "dim2")] + let shape = { + ensure( + rows >= 2 && columns == 1, + "2D heightfield requires rows>=2 and columns=1", + )?; + SharedShape::heightfield(data.to_vec(), scale) + }; + #[cfg(feature = "dim3")] + let shape = { + ensure( + rows >= 2 && columns >= 2, + "heightfield dimensions must be >=2", + )?; + SharedShape::heightfield( + rapier::parry::utils::Array2::new(rows, columns, data.to_vec()), + scale, + ) + }; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} +/// Vertex indices have DIM entries per element. Uses Rapier's default decomposition parameters. +pub(crate) unsafe fn impl_rpr_shared_shape_convex_decomposition( + vertices: *const RprVector, + vertex_count: usize, + indices: *const u32, + element_count: usize, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let p = input(vertices, vertex_count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + let i = crate::geometry::indices_array::<{ rapier::math::DIM }>( + indices, + element_count, + vertex_count, + )?; + ensure(!i.is_empty(), "empty mesh")?; + let shape = SharedShape::convex_decomposition(&p, &i); + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) +} +pub(crate) unsafe fn impl_rpr_shared_shape_voxels_from_points( + voxel_size: RprVector, + points: *const RprVector, + count: usize, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let size = voxel_size.raw()?; + ensure(size.min_element() > 0.0, "voxel size must be positive")?; + let p = input(points, count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + ensure(!p.is_empty(), "no voxel points")?; + output( + out, + Box::into_raw(Box::new(RprSharedShape(SharedShape::voxels_from_points( + size, &p, + )))), + ) + }) +} +/// Compute the shape axis-aligned bounds at the supplied world-space pose. +/// @ingroup shapes +#[rapier_export(shared_shape)] +pub unsafe extern "C" fn rpr_shared_shape_compute_aabb( + shape: *const RprSharedShape, + pose: RprPose, +) -> RprAabb { + ffi_value(|out: *mut RprAabb| { + ffi(|| unsafe { + let a = get(shape)?.0.compute_aabb(&pose.raw()?); + output( + out, + RprAabb { + mins: a.mins.into(), + maxs: a.maxs.into(), + }, + ) + }) + }) +} +/// Compute local mass properties for the supplied nonnegative density. +/// @ingroup shapes +#[rapier_export(shared_shape)] +pub unsafe extern "C" fn rpr_shared_shape_mass_properties( + shape: *const RprSharedShape, + density: RprReal, +) -> RprMassProperties { + ffi_value(|out: *mut RprMassProperties| { + ffi(|| unsafe { + nonnegative(density)?; + output(out, get(shape)?.0.mass_properties(density).into()) + }) + }) +} +/// Test whether the world-space point lies inside the shape at pose. +/// @ingroup shapes +#[rapier_export(shared_shape)] +pub unsafe extern "C" fn rpr_shared_shape_contains_point( + shape: *const RprSharedShape, + pose: RprPose, + point: RprVector, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let p = pose.raw()?; + let point = point.raw()?; + output(out, get(shape)?.0.contains_point(&p, point) as u32) + }) + }) +} +/// Current narrow-phase pair and accumulated solver impulse summary. +/// @ingroup events +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprContactPair { + /// First collider in the pair. + pub collider1: RprColliderHandle, + /// Second collider in the pair. + pub collider2: RprColliderHandle, + /// Whether the pair has an active solver contact. + pub has_any_active_contact: RprBool, + /// Sum of world-space contact impulse vectors. + pub total_impulse: RprVector, + /// Sum of contact impulse magnitudes. + pub total_impulse_magnitude: RprReal, + /// Largest contact impulse magnitude. + pub max_impulse: RprReal, + /// World-space direction of the largest contact impulse. + pub max_impulse_direction: RprVector, +} +impl From<&ContactPair> for RprContactPair { + fn from(p: &ContactPair) -> Self { + let (max, dir) = p.max_impulse(); + Self { + collider1: p.collider1.into(), + collider2: p.collider2.into(), + has_any_active_contact: p.has_any_active_contact() as u32, + total_impulse: p.total_impulse().into(), + total_impulse_magnitude: p.total_impulse_magnitude(), + max_impulse: max, + max_impulse_direction: dir.into(), + } + } +} +/// Sensor intersection state for a collider pair. +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprIntersectionPair { + /// First collider in the pair. + pub collider1: RprColliderHandle, + /// Second collider in the pair. + pub collider2: RprColliderHandle, + /// Whether the two sensor/collider shapes intersect. + pub intersecting: RprBool, +} +/// Copy current narrow-phase contact pairs, including pairs without active solver contacts. +/// @see @ref output_buffers +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_contact_pairs( + world: *const RprWorld, + buffer: *mut RprContactPair, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + + let narrow: *const RprNarrowPhase = + std::ptr::addr_of!((*raw).0.narrow_phase).cast(); + + let v: Vec<_> = get(narrow)?.0.contact_pairs().map(Into::into).collect(); + copy_out(&v, buffer, capacity, count) + }) + }) + } +} + +/// Return the narrow-phase contact pair for two colliders, or report RPR_NOT_FOUND. +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_contact_pair( + collider1: RprColliderHandle, + collider2: RprColliderHandle, +) -> RprContactPair { + let world = collider1.world; + ffi_world_value(world, |out: *mut RprContactPair| { + ffi(|| unsafe { + collider1.check_world(world)?; + collider2.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let narrow: *const RprNarrowPhase = std::ptr::addr_of!((*raw).0.narrow_phase).cast(); + + let p = get(narrow)? + .0 + .contact_pair(collider1.raw(), collider2.raw()) + .ok_or((RPR_NOT_FOUND, "no contact pair".into()))?; + output(out, p.into()) + }) + }) +} + +/// Copy current sensor intersection pairs from the narrow phase. +/// @see @ref output_buffers +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_intersection_pairs( + world: *const RprWorld, + buffer: *mut RprIntersectionPair, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + + let narrow: *const RprNarrowPhase = + std::ptr::addr_of!((*raw).0.narrow_phase).cast(); + + let v: Vec<_> = get(narrow)? + .0 + .intersection_pairs() + .map(|(a, b, hit)| RprIntersectionPair { + collider1: a.into(), + collider2: b.into(), + intersecting: hit as u32, + }) + .collect(); + copy_out(&v, buffer, capacity, count) + }) + }) + } +} + +/// One manifold contact and its normal solver impulse. +/// @ingroup events +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprContactPoint { + /// Index of the contact manifold within its pair. + pub manifold_index: usize, + /// Contact point in collider 1 local coordinates. + pub local_p1: RprVector, + /// Contact point in collider 2 local coordinates. + pub local_p2: RprVector, + /// World-space contact or surface normal. + pub normal: RprVector, + /// Signed separation; negative means penetration. + pub distance: RprReal, + /// Normal impulse applied at this contact. + pub impulse: RprReal, +} +/// Contact points in collider-local space; normal in world space. Geometric manifolds may be +/// recycled. +/// For clustered solver impulses use contact pair totals. Soft pairs have no rigid manifolds. +/// @see @ref output_buffers +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_contact_points( + collider1: RprColliderHandle, + collider2: RprColliderHandle, + buffer: *mut RprContactPoint, + capacity: usize, +) -> usize { + let world = collider1.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + collider1.check_world(world)?; + collider2.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let narrow: *const RprNarrowPhase = std::ptr::addr_of!((*raw).0.narrow_phase).cast(); + + let p = get(narrow)? + .0 + .contact_pair(collider1.raw(), collider2.raw()) + .ok_or((RPR_NOT_FOUND, "no contact pair".into()))?; + let v: Vec<_> = p + .manifolds() + .iter() + .enumerate() + .flat_map(|(i, m)| { + m.points.iter().map(move |p| RprContactPoint { + manifold_index: i, + local_p1: p.local_p1.into(), + local_p2: p.local_p2.into(), + normal: m.data.normal.into(), + distance: p.dist, + impulse: p.data.impulse, + }) + }) + .collect(); + copy_out(&v, buffer, capacity, count) + }) + }) +} + +/// Copy the articulation generalized velocities in native degree-of-freedom order. +/// @see @ref output_buffers +/// @ingroup joints +#[rapier_export(multibody_joint)] +pub unsafe extern "C" fn rpr_multibody_joint_generalized_velocity( + handle: RprMultibodyJointHandle, + buffer: *mut RprReal, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprMultibodyJointSet = + std::ptr::addr_of!((*raw).0.multibody_joints).cast(); + + let (m, _) = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + copy_out(m.generalized_velocity().as_slice(), buffer, capacity, count) + }) + }) +} + +/// Replace articulation generalized velocities; the array length must match its degrees of freedom. +/// @ingroup joints +#[rapier_export(multibody_joint)] +pub unsafe extern "C" fn rpr_multibody_joint_set_generalized_velocity( + handle: RprMultibodyJointHandle, + values: *const RprReal, + count: usize, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprMultibodyJointSet = + std::ptr::addr_of_mut!((*raw).0.multibody_joints).cast(); + + let v = input(values, count)?; + for &x in v { + finite(x)?; + } + let (m, _) = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + m.ndofs() == count, + "velocity count must equal articulation dofs", + )?; + m.generalized_velocity_mut() + .as_mut_slice() + .copy_from_slice(v); + Ok(()) + }) +} + +/// Check this before passing any dimension/precision-dependent structs across the ABI. +/// @ingroup errors +#[rapier_export] +pub unsafe extern "C" fn rpr_check_abi( + version: u32, + dimension: u32, + real_size: usize, + vector_size: usize, + pose_size: usize, +) -> RprStatus { + ffi(|| { + ensure( + version == RPR_ABI_VERSION + && dimension == rapier::math::DIM as u32 + && real_size == std::mem::size_of::() + && vector_size == std::mem::size_of::() + && pose_size == std::mem::size_of::(), + "header/library ABI mismatch", + ) + }) +} + +/// Voxelize a boundary mesh with the native default solid-fill mode. +/// Indices contain DIM entries per boundary element. +pub(crate) unsafe fn impl_rpr_shared_shape_voxelized_mesh( + vertices: *const RprVector, + vertex_count: usize, + indices: *const u32, + element_count: usize, + voxel_size: RprReal, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + positive(voxel_size)?; + let points = input(vertices, vertex_count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + ensure(!points.is_empty(), "empty voxelization mesh")?; + let indices = crate::geometry::indices_array::<{ rapier::math::DIM }>( + indices, + element_count, + points.len(), + )?; + output( + out, + Box::into_raw(Box::new(RprSharedShape(SharedShape::voxelized_mesh( + &points, + &indices, + voxel_size, + Default::default(), + )))), + ) + }) +} + +pub(crate) unsafe fn native_collider_is_voxels( + collider: *const RprCollider, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + output( + out, + get(collider)?.0.shape().as_voxels().is_some() as RprBool, + ) + }) +} +/// Voxel coordinates have DIM signed integer components. +/// @ingroup math +#[cfg(feature = "f32")] +pub type RprVoxelCoord = i32; +/// Signed voxel coordinate integer. +/// @ingroup math +#[cfg(feature = "f64")] +pub type RprVoxelCoord = i64; +/// Integer coordinates of a voxel cell. +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprVoxelKey { + /// X component. + pub x: RprVoxelCoord, + /// Y component. + pub y: RprVoxelCoord, + #[cfg(feature = "dim3")] + /// Z component. + pub z: RprVoxelCoord, +} +impl RprVoxelKey { + fn raw(self) -> rapier::math::IVector { + #[cfg(feature = "dim2")] + { + rapier::math::IVector::new(self.x, self.y) + } + #[cfg(feature = "dim3")] + { + rapier::math::IVector::new(self.x, self.y, self.z) + } + } +} +pub(crate) unsafe fn native_collider_voxel_at_flat_id( + collider: *const RprCollider, + id: u32, + key: *mut RprVoxelKey, + center: *mut RprVector, + size: *mut RprVector, + found: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(key)?; + out_ptr(center)?; + out_ptr(size)?; + out_ptr(found)?; + let voxels = get(collider)? + .0 + .shape() + .as_voxels() + .ok_or_else(|| invalid("not a voxel collider"))?; + output(size, voxels.voxel_size().into())?; + if let Some(k) = voxels.voxel_at_flat_id(id) { + output( + key, + RprVoxelKey { + x: k.x, + y: k.y, + #[cfg(feature = "dim3")] + z: k.z, + }, + )?; + output(center, voxels.voxel_center(k).into())?; + output(found, 1) + } else { + output(key, Default::default())?; + output(center, Default::default())?; + output(found, 0) + } + }) +} +pub(crate) unsafe fn native_collider_set_voxel( + collider: *mut RprCollider, + key: RprVoxelKey, + filled: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let filled = boolean(filled)?; + get_mut(collider)? + .0 + .shape_mut() + .as_voxels_mut() + .ok_or_else(|| invalid("not a voxel collider"))? + .set_voxel(key.raw(), filled); + Ok(()) + }) +} diff --git a/c/src/geometry.rs b/c/src/geometry.rs new file mode 100644 index 000000000..0d49c9615 --- /dev/null +++ b/c/src/geometry.rs @@ -0,0 +1,822 @@ +use crate::*; +/// Create an owned ball shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_ball_shared_shape(radius: RprReal) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let shape = SharedShape::ball(positive(radius)?); + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} + +/// Create an owned cuboid shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_cuboid_shared_shape(half_extents: RprVector) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let v = half_extents.raw()?; + ensure(v.min_element() > 0.0, "half extents must be positive")?; + let shape = { + #[cfg(feature = "dim2")] + { + SharedShape::cuboid(v.x, v.y) + } + #[cfg(feature = "dim3")] + { + SharedShape::cuboid(v.x, v.y, v.z) + } + }; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} + +/// Create an owned round cuboid shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_round_cuboid_shared_shape( + half_extents: RprVector, + border_radius: RprReal, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let v = half_extents.raw()?; + ensure(v.min_element() > 0.0, "half extents must be positive")?; + let r = nonnegative(border_radius)?; + let shape = { + #[cfg(feature = "dim2")] + { + SharedShape::round_cuboid(v.x, v.y, r) + } + #[cfg(feature = "dim3")] + { + SharedShape::round_cuboid(v.x, v.y, v.z, r) + } + }; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} + +/// Create an owned capsule shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_capsule_shared_shape( + a: RprVector, + b: RprVector, + radius: RprReal, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let shape = SharedShape::capsule(a.raw()?, b.raw()?, positive(radius)?); + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} + +/// Create an owned segment shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_segment_shared_shape( + a: RprVector, + b: RprVector, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let shape = SharedShape::segment(a.raw()?, b.raw()?); + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} + +/// Create an owned triangle shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_triangle_shared_shape( + a: RprVector, + b: RprVector, + c: RprVector, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let shape = SharedShape::triangle(a.raw()?, b.raw()?, c.raw()?); + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} + +/// Create an owned halfspace shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_halfspace_shared_shape(normal: RprVector) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let v = normal.raw()?; + positive(v.length())?; + let shape = SharedShape::halfspace(v / v.length()); + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} + +/// Create an owned cylinder shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[cfg(feature = "dim3")] +#[rapier_export] +pub unsafe extern "C" fn rpr_cylinder_shared_shape( + half_height: RprReal, + radius: RprReal, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let shape = SharedShape::cylinder(positive(half_height)?, positive(radius)?); + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} + +/// Create an owned cone shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[cfg(feature = "dim3")] +#[rapier_export] +pub unsafe extern "C" fn rpr_cone_shared_shape( + half_height: RprReal, + radius: RprReal, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let shape = SharedShape::cone(positive(half_height)?, positive(radius)?); + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} + +pub(crate) unsafe fn impl_rpr_shared_shape_convex_hull( + vertices: *const RprVector, + count: usize, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let points = input(vertices, count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + ensure(points.len() > rapier::math::DIM, "not enough vertices")?; + let shape = + SharedShape::convex_hull(&points).ok_or_else(|| invalid("degenerate convex hull"))?; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) +} + +pub(crate) unsafe fn impl_rpr_shared_shape_trimesh( + vertices: *const RprVector, + vertex_count: usize, + indices: *const u32, + element_count: usize, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let points = input(vertices, vertex_count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + let indices = indices_array::<3>(indices, element_count, vertex_count)?; + ensure(!indices.is_empty(), "empty mesh")?; + let shape = SharedShape::trimesh(points, indices).map_err(|e| invalid(e.to_string()))?; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) +} + +pub(crate) unsafe fn impl_rpr_shared_shape_polyline( + vertices: *const RprVector, + vertex_count: usize, + indices: *const u32, + element_count: usize, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let points = input(vertices, vertex_count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + let indices = indices_array::<2>(indices, element_count, vertex_count)?; + ensure(!indices.is_empty(), "empty mesh")?; + let shape = SharedShape::polyline(points, Some(indices)); + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) +} + +pub(crate) unsafe fn indices_array( + indices: *const u32, + count: usize, + vertices: usize, +) -> Result> { + let len = count + .checked_mul(N) + .ok_or_else(|| invalid("index count overflow"))?; + let values = unsafe { input(indices, len)? }; + ensure( + values.iter().all(|&i| (i as usize) < vertices), + "vertex index out of range", + )?; + Ok(values + .chunks_exact(N) + .map(|c| c.try_into().unwrap()) + .collect()) +} +/// Create an owned compound shape; each child pose is relative to the compound. Child shapes are +/// shared, not consumed. Release with rpr_free_shared_shape. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_compound_shared_shape( + children: RprCompoundShapeView, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let desc = RprShapeDesc { + kind: RPR_SHAPE_DESC_COMPOUND, + children, + ..Default::default() + }; + output(out, Box::into_raw(Box::new(RprSharedShape(desc.raw()?)))) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_position( + object: *mut RprCollider, + value: RprPose, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let object = get_mut(object)?; + object.0.set_position(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_translation( + object: *mut RprCollider, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let object = get_mut(object)?; + object.0.set_translation(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_friction( + object: *mut RprCollider, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let object = get_mut(object)?; + object.0.set_friction(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_restitution( + object: *mut RprCollider, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let object = get_mut(object)?; + object.0.set_restitution(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_density( + object: *mut RprCollider, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let object = get_mut(object)?; + object.0.set_density(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_mass( + object: *mut RprCollider, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let object = get_mut(object)?; + object.0.set_mass(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_sensor( + object: *mut RprCollider, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + let object = get_mut(object)?; + object.0.set_sensor(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_enabled( + object: *mut RprCollider, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + let object = get_mut(object)?; + object.0.set_enabled(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_collision_groups( + object: *mut RprCollider, + value: RprInteractionGroups, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let object = get_mut(object)?; + object.0.set_collision_groups(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_solver_groups( + object: *mut RprCollider, + value: RprInteractionGroups, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let object = get_mut(object)?; + object.0.set_solver_groups(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_friction_combine_rule( + object: *mut RprCollider, + value: u32, +) -> RprStatus { + ffi(|| unsafe { + let value = combine(value)?; + let object = get_mut(object)?; + object.0.set_friction_combine_rule(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_restitution_combine_rule( + object: *mut RprCollider, + value: u32, +) -> RprStatus { + ffi(|| unsafe { + let value = combine(value)?; + let object = get_mut(object)?; + object.0.set_restitution_combine_rule(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_contact_skin( + object: *mut RprCollider, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let object = get_mut(object)?; + object.0.set_contact_skin(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_contact_force_event_threshold( + object: *mut RprCollider, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let object = get_mut(object)?; + object.0.set_contact_force_event_threshold(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_active_events( + object: *mut RprCollider, + value: u32, +) -> RprStatus { + ffi(|| unsafe { + let value = ActiveEvents::from_bits(value).ok_or_else(|| invalid("unknown event flags"))?; + let object = get_mut(object)?; + object.0.set_active_events(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_active_hooks( + object: *mut RprCollider, + value: u32, +) -> RprStatus { + ffi(|| unsafe { + let value = ActiveHooks::from_bits(value).ok_or_else(|| invalid("unknown hook flags"))?; + let object = get_mut(object)?; + object.0.set_active_hooks(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_active_collision_types( + object: *mut RprCollider, + value: u16, +) -> RprStatus { + ffi(|| unsafe { + let value = ActiveCollisionTypes::from_bits(value) + .ok_or_else(|| invalid("unknown collision type flags"))?; + let object = get_mut(object)?; + object.0.set_active_collision_types(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_user_data( + object: *mut RprCollider, + value: RprUserData, +) -> RprStatus { + ffi(|| unsafe { + get_mut(object)?.0.user_data = value.raw(); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_position( + object: *const RprCollider, + out: *mut RprPose, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, (*object.0.position()).into()) + }) +} + +pub(crate) unsafe fn native_collider_translation( + object: *const RprCollider, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.translation().into()) + }) +} + +pub(crate) unsafe fn native_collider_rotation( + object: *const RprCollider, + out: *mut RprRotation, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.rotation().into()) + }) +} + +pub(crate) unsafe fn native_collider_collision_groups( + object: *const RprCollider, + out: *mut RprInteractionGroups, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.collision_groups().into()) + }) +} + +pub(crate) unsafe fn native_collider_solver_groups( + object: *const RprCollider, + out: *mut RprInteractionGroups, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.solver_groups().into()) + }) +} + +pub(crate) unsafe fn native_collider_parent( + object: *const RprCollider, + out: *mut RprRigidBodyHandle, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.parent().map(Into::into).unwrap_or_default()) + }) +} + +pub(crate) unsafe fn native_collider_user_data( + object: *const RprCollider, + out: *mut RprUserData, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.user_data.into()) + }) +} + +pub(crate) unsafe fn native_collider_active_events( + object: *const RprCollider, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.active_events().bits()) + }) +} + +pub(crate) unsafe fn native_collider_friction( + object: *const RprCollider, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.friction()) + }) +} + +pub(crate) unsafe fn native_collider_restitution( + object: *const RprCollider, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.restitution()) + }) +} + +pub(crate) unsafe fn native_collider_mass( + object: *const RprCollider, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.mass()) + }) +} + +pub(crate) unsafe fn native_collider_density( + object: *const RprCollider, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.density()) + }) +} + +pub(crate) unsafe fn native_collider_volume( + object: *const RprCollider, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.volume()) + }) +} + +pub(crate) unsafe fn native_collider_contact_skin( + object: *const RprCollider, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.contact_skin()) + }) +} + +pub(crate) unsafe fn native_collider_contact_force_event_threshold( + object: *const RprCollider, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.contact_force_event_threshold()) + }) +} + +pub(crate) unsafe fn native_collider_is_sensor( + object: *const RprCollider, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_sensor() as u32) + }) +} + +pub(crate) unsafe fn native_collider_is_enabled( + object: *const RprCollider, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_enabled() as u32) + }) +} + +pub(crate) unsafe fn native_collider_compute_aabb( + object: *const RprCollider, + out: *mut RprAabb, +) -> RprStatus { + ffi(|| unsafe { + let a = get(object)?.0.compute_aabb(); + output( + out, + RprAabb { + mins: a.mins.into(), + maxs: a.maxs.into(), + }, + ) + }) +} + +/// Returns an owned shared reference, independently freeable. +pub(crate) unsafe fn native_collider_shared_shape( + object: *const RprCollider, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let shape = get(object)?.0.shared_shape().clone(); + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) +} + +pub(crate) unsafe fn native_collider_set_shape( + object: *mut RprCollider, + shape: *const RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + let shape = get(shape)?.0.clone(); + get_mut(object)?.0.set_shape(shape); + Ok(()) + }) +} + +pub(crate) unsafe fn native_collider_set_position_wrt_parent( + object: *mut RprCollider, + value: RprPose, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let object = get_mut(object)?; + object.0.set_position_wrt_parent(value); + Ok(()) + }) +} + +/// Remove the collider and update its parent body mass properties. wake_up wakes the parent. +/// @ingroup colliders +#[rapier_export] +pub unsafe extern "C" fn rpr_remove_collider( + handle: RprColliderHandle, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + let islands: *mut RprIslandManager = std::ptr::addr_of_mut!((*raw).0.islands).cast(); + let bodies: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + let soft_bodies: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let wake_up = boolean(wake_up)?; + get_mut(set)? + .0 + .remove( + handle.raw(), + &mut get_mut(islands)?.0, + &mut get_mut(bodies)?.0, + &mut get_mut(soft_bodies)?.0, + wake_up, + ) + .ok_or_else(missing)?; + Ok(()) + }) +} + +/// Build a one-sided 2D polyline. Indices are flat pairs, as for impl_rpr_shared_shape_polyline. +#[cfg(feature = "dim2")] +pub(crate) unsafe fn impl_rpr_shared_shape_oriented_polyline( + vertices: *const RprVector, + vertex_count: usize, + indices: *const u32, + element_count: usize, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let points = input(vertices, vertex_count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + let indices = indices_array::<2>(indices, element_count, vertex_count)?; + ensure(!indices.is_empty(), "empty mesh")?; + let shape = ColliderBuilder::oriented_polyline(points, Some(indices)).shape; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) +} +/// Convex polygon from an already ordered convex boundary. +#[cfg(feature = "dim2")] +pub(crate) unsafe fn impl_rpr_shared_shape_convex_polyline( + vertices: *const RprVector, + count: usize, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let points = input(vertices, count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + ensure(points.len() >= 3, "not enough vertices")?; + let shape = SharedShape::convex_polyline(points) + .ok_or_else(|| invalid("invalid convex polygon"))?; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) +} +/// Rounded convex hull of the supplied points. +pub(crate) unsafe fn impl_rpr_shared_shape_round_convex_hull( + vertices: *const RprVector, + count: usize, + border_radius: RprReal, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let points = input(vertices, count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + ensure(points.len() > rapier::math::DIM, "not enough vertices")?; + let shape = SharedShape::round_convex_hull(&points, nonnegative(border_radius)?) + .ok_or_else(|| invalid("degenerate convex hull"))?; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) +} + +/// Triangle mesh with Parry's TriMeshFlags bits. +pub(crate) unsafe fn impl_rpr_shared_shape_trimesh_with_flags( + vertices: *const RprVector, + vertex_count: usize, + indices: *const u32, + element_count: usize, + flags: u32, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let points = input(vertices, vertex_count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + let indices = indices_array::<3>(indices, element_count, vertex_count)?; + ensure(!indices.is_empty(), "empty mesh")?; + let flags = rapier::parry::shape::TriMeshFlags::from_bits(flags as u16) + .filter(|_| flags <= u16::MAX as u32) + .ok_or_else(|| invalid("unknown trimesh flags"))?; + let shape = SharedShape::trimesh_with_flags(points, indices, flags) + .map_err(|e| invalid(e.to_string()))?; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) +} diff --git a/c/src/geometry_views.rs b/c/src/geometry_views.rs new file mode 100644 index 000000000..9a08652f7 --- /dev/null +++ b/c/src/geometry_views.rs @@ -0,0 +1,224 @@ +//! Typed geometry input boundaries; view counts always count elements. +use crate::handle_access::forward; +use crate::*; +/// Create an owned compound shape by convex decomposition of the input surface. Release it with +/// rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_convex_decomposition_shared_shape( + vertices: RprVectorView, + indices: RprSurfaceElementView, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(vertices.data, vertices.count)?; + crate::array_views::validate_view(indices.data, indices.count)?; + forward(impl_rpr_shared_shape_convex_decomposition( + vertices.data, + vertices.count, + indices.data.cast(), + indices.count, + out, + )) + }) + }) +} +/// Create an owned voxel shape by quantizing points with the supplied per-axis voxel size. Release +/// it with rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_voxels_shared_shape_from_points( + voxel_size: RprVector, + points: RprVectorView, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(points.data, points.count)?; + forward(impl_rpr_shared_shape_voxels_from_points( + voxel_size, + points.data, + points.count, + out, + )) + }) + }) +} +/// Create an owned voxel shape from a surface mesh with the supplied uniform voxel size. Release it +/// with rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_voxelized_mesh_shared_shape( + vertices: RprVectorView, + indices: RprSurfaceElementView, + voxel_size: RprReal, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(vertices.data, vertices.count)?; + crate::array_views::validate_view(indices.data, indices.count)?; + forward(impl_rpr_shared_shape_voxelized_mesh( + vertices.data, + vertices.count, + indices.data.cast(), + indices.count, + voxel_size, + out, + )) + }) + }) +} +/// Create an owned convex hull of the supplied vertices. Release it with rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_convex_hull_shared_shape( + vertices: RprVectorView, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(vertices.data, vertices.count)?; + forward(impl_rpr_shared_shape_convex_hull( + vertices.data, + vertices.count, + out, + )) + }) + }) +} +/// Create an owned triangle mesh from vertices and triangle indices. Release it with +/// rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_trimesh_shared_shape( + vertices: RprVectorView, + indices: RprTriangleView, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(vertices.data, vertices.count)?; + crate::array_views::validate_view(indices.data, indices.count)?; + forward(impl_rpr_shared_shape_trimesh( + vertices.data, + vertices.count, + indices.data.cast(), + indices.count, + out, + )) + }) + }) +} +/// Create an owned polyline from vertices and edge indices. Release it with rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_polyline_shared_shape( + vertices: RprVectorView, + indices: RprEdgeView, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(vertices.data, vertices.count)?; + crate::array_views::validate_view(indices.data, indices.count)?; + forward(impl_rpr_shared_shape_polyline( + vertices.data, + vertices.count, + indices.data.cast(), + indices.count, + out, + )) + }) + }) +} +/// Create an owned oriented 2D polyline from vertices and edge indices. Release it with +/// rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[cfg(feature = "dim2")] +#[rapier_export] +pub unsafe extern "C" fn rpr_oriented_polyline_shared_shape( + vertices: RprVectorView, + indices: RprEdgeView, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(vertices.data, vertices.count)?; + crate::array_views::validate_view(indices.data, indices.count)?; + forward(impl_rpr_shared_shape_oriented_polyline( + vertices.data, + vertices.count, + indices.data.cast(), + indices.count, + out, + )) + }) + }) +} +/// Create an owned convex polygon from vertices already ordered along its boundary. Release it with +/// rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[cfg(feature = "dim2")] +#[rapier_export] +pub unsafe extern "C" fn rpr_convex_polyline_shared_shape( + vertices: RprVectorView, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(vertices.data, vertices.count)?; + forward(impl_rpr_shared_shape_convex_polyline( + vertices.data, + vertices.count, + out, + )) + }) + }) +} +/// Create an owned round convex hull shape. Release it with rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_round_convex_hull_shared_shape( + vertices: RprVectorView, + border_radius: RprReal, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(vertices.data, vertices.count)?; + forward(impl_rpr_shared_shape_round_convex_hull( + vertices.data, + vertices.count, + border_radius, + out, + )) + }) + }) +} +/// Create an owned triangle mesh with the supplied TRIMESH_* processing flags. Release it with +/// rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_trimesh_shared_shape_with_flags( + vertices: RprVectorView, + indices: RprTriangleView, + flags: u32, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(vertices.data, vertices.count)?; + crate::array_views::validate_view(indices.data, indices.count)?; + forward(impl_rpr_shared_shape_trimesh_with_flags( + vertices.data, + vertices.count, + indices.data.cast(), + indices.count, + flags, + out, + )) + }) + }) +} diff --git a/c/src/handle_access.rs b/c/src/handle_access.rs new file mode 100644 index 000000000..6e6b8ab78 --- /dev/null +++ b/c/src/handle_access.rs @@ -0,0 +1,1405 @@ +//! Short-lived element access through generational handles. No element pointer escapes. +//! Handles remain scoped to the original set; they do not keep the set/world alive. +use crate::*; + +// Delegate to the existing accessors so their validation and change semantics stay identical. +// ffi suppresses nested error callbacks; the outermost call reports each error once. +pub(crate) unsafe fn forward(status: RprStatus) -> Result { + if status == RPR_OK { + Ok(()) + } else { + Err(( + status, + unsafe { std::ffi::CStr::from_ptr(rpr_last_error()) } + .to_string_lossy() + .into_owned(), + )) + } +} + +/// Return the rigid body world-space pose. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_position(handle: RprRigidBodyHandle) -> RprPose { + let world = handle.world; + ffi_value(|out: *mut RprPose| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_position( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_position( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprPose, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_position( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Return the rigid body world-space translation. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_translation(handle: RprRigidBodyHandle) -> RprVector { + let world = handle.world; + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_translation( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_translation( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_translation( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Return the rigid body world-space linear velocity. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_linvel(handle: RprRigidBodyHandle) -> RprVector { + let world = handle.world; + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_linvel( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_linvel( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_linvel( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Return the rigid body world-space angular velocity (radians per second). +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_angvel(handle: RprRigidBodyHandle) -> RprAngVector { + let world = handle.world; + ffi_value(|out: *mut RprAngVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_angvel( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_angvel( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprAngVector, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_angvel( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Return whether the rigid body is sleeping. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_is_sleeping(handle: RprRigidBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_is_sleeping( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_is_sleeping( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_is_sleeping( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Return whether the rigid body is enabled. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_is_enabled(handle: RprRigidBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_is_enabled( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_is_enabled( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_is_enabled( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Return the rigid body application-owned 128-bit user value. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_user_data(handle: RprRigidBodyHandle) -> RprUserData { + let world = handle.world; + ffi_value(|out: *mut RprUserData| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_user_data( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_user_data( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprUserData, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_user_data( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Set the rigid body world-space pose. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_position( + handle: RprRigidBodyHandle, + value: RprPose, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_position( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Set the rigid body world-space translation. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_translation( + handle: RprRigidBodyHandle, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_translation( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Set the rigid body world-space linear velocity. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_linvel( + handle: RprRigidBodyHandle, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_linvel( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Set the rigid body world-space angular velocity (radians per second). +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_angvel( + handle: RprRigidBodyHandle, + value: RprAngVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_angvel( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Set the rigid body next kinematic world-space pose. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_next_kinematic_position( + handle: RprRigidBodyHandle, + value: RprPose, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_next_kinematic_position( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Set the rigid body next kinematic world-space translation. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_next_kinematic_translation( + handle: RprRigidBodyHandle, + value: RprVector, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_next_kinematic_translation( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Set the rigid body gravity multiplier. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_gravity_scale( + handle: RprRigidBodyHandle, + value: RprReal, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_gravity_scale( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Set the rigid body linear damping coefficient. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_linear_damping( + handle: RprRigidBodyHandle, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_set_linear_damping( + std::ptr::addr_of_mut!((*raw).0.bodies).cast(), + handle, + value, + )) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_set_linear_damping( + set: *mut RprRigidBodySet, + handle: RprRigidBodyHandle, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_linear_damping( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Set the rigid body angular damping coefficient. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_angular_damping( + handle: RprRigidBodyHandle, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_angular_damping( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Enable or disable the rigid body. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_enabled( + handle: RprRigidBodyHandle, + value: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_set_enabled( + std::ptr::addr_of_mut!((*raw).0.bodies).cast(), + handle, + value, + )) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_set_enabled( + set: *mut RprRigidBodySet, + handle: RprRigidBodyHandle, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_enabled( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Set the rigid body application-owned 128-bit user value. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_user_data( + handle: RprRigidBodyHandle, + value: RprUserData, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_user_data( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Apply a world-space linear impulse. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_apply_impulse( + handle: RprRigidBodyHandle, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_apply_impulse( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Apply a world-space impulse at a world-space point. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_apply_impulse_at_point( + handle: RprRigidBodyHandle, + value: RprVector, + point: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_apply_impulse_at_point( + (element as *mut RigidBody).cast(), + value, + point, + wake_up, + )) + }) +} + +/// Accumulate a world-space force; it persists until reset. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_add_force( + handle: RprRigidBodyHandle, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_add_force( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Clear accumulated user forces. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_reset_forces( + handle: RprRigidBodyHandle, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_reset_forces( + (element as *mut RigidBody).cast(), + wake_up, + )) + }) +} + +/// Put the body to sleep. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_sleep(handle: RprRigidBodyHandle) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_sleep((element as *mut RigidBody).cast())) + }) +} + +/// Return the collider world-space pose. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_position(handle: RprColliderHandle) -> RprPose { + let world = handle.world; + ffi_value(|out: *mut RprPose| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_position( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_position( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprPose, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_position( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Return the collider world-space translation. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_translation(handle: RprColliderHandle) -> RprVector { + let world = handle.world; + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_translation( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_translation( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_translation( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Return the collider friction coefficient. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_friction(handle: RprColliderHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_friction( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_friction( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_friction( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Return the collider restitution coefficient. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_restitution(handle: RprColliderHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_restitution( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_restitution( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_restitution( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Return whether the collider is a sensor (detects overlaps without contact forces). +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_is_sensor(handle: RprColliderHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_is_sensor( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_is_sensor( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_is_sensor( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Return the parent body handle, or an invalid handle with OK status for a standalone collider. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_parent(handle: RprColliderHandle) -> RprRigidBodyHandle { + let world = handle.world; + ffi_world_value(world, |out: *mut RprRigidBodyHandle| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_parent( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_parent( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprRigidBodyHandle, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_parent( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Set the collider world-space pose. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_position( + handle: RprColliderHandle, + value: RprPose, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_position( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider world-space translation. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_translation( + handle: RprColliderHandle, + value: RprVector, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_translation( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider friction coefficient. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_friction( + handle: RprColliderHandle, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_friction( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider restitution coefficient. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_restitution( + handle: RprColliderHandle, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_restitution( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Enable or disable a sensor (detects overlaps without contact forces) for the collider. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_sensor( + handle: RprColliderHandle, + value: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_sensor( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider collision filtering groups. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_collision_groups( + handle: RprColliderHandle, + value: RprInteractionGroups, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_collision_groups( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider application-owned 128-bit user value. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_user_data( + handle: RprColliderHandle, + value: RprUserData, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_user_data( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Return the world-space position of the indexed particle. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_particle_position( + handle: RprSoftBodyHandle, + index: usize, +) -> RprVector { + let world = handle.world; + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_particle_position( + (element as *const SoftBody).cast(), + index, + out, + )) + }) + }) +} + +/// Copy world-space particle positions. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_particle_positions( + handle: RprSoftBodyHandle, + buffer: *mut RprVector, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_particle_positions( + (element as *const SoftBody).cast(), + buffer, + capacity, + count, + )) + }) + }) +} + +/// Return a copy of the soft body material parameters. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_material(handle: RprSoftBodyHandle) -> RprSoftBodyMaterial { + let world = handle.world; + ffi_value(|out: *mut RprSoftBodyMaterial| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_read_material( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Set the world-space position of the indexed particle. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_particle_position( + handle: RprSoftBodyHandle, + index: usize, + value: RprVector, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_particle_position( + (element as *mut SoftBody).cast(), + index, + value, + )) + }) +} + +/// Copy material parameters into the soft body. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_material( + handle: RprSoftBodyHandle, + data: *const RprSoftBodyMaterial, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_material_data( + (element as *mut SoftBody).cast(), + data, + )) + }) +} + +/// Add a world-space force to the indexed particle. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_add_particle_force( + handle: RprSoftBodyHandle, + index: usize, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_add_particle_force( + (element as *mut SoftBody).cast(), + index, + value, + wake_up, + )) + }) +} + +/// A copied state snapshot, with no pointers or ownership obligations. +/// @ingroup rigid_bodies +#[repr(C)] +#[derive(Clone, Copy)] +#[allow(non_snake_case)] +pub struct RprRigidBodyState { + /// World-space pose. + pub position: RprPose, + /// World-space linear velocity. + pub linvel: RprVector, + /// World-space angular velocity in radians per second. + pub angvel: RprAngVector, + /// Whether the body starts/is asleep. + pub sleeping: RprBool, + /// Whether this setting/object is enabled (0 or 1). + pub enabled: RprBool, + /// Application data; Rapier does not own pointers encoded in it. + pub userData: RprUserData, +} +impl From<&RigidBody> for RprRigidBodyState { + fn from(body: &RigidBody) -> Self { + Self { + position: (*body.position()).into(), + linvel: body.linvel().into(), + angvel: angular_out(body.angvel()), + sleeping: body.is_sleeping() as _, + enabled: body.is_enabled() as _, + userData: body.user_data.into(), + } + } +} + +/// Copies states in the same order as handles, without allocating temporary storage. +/// All handles are validated before writing. On INVALID_HANDLE outputs are unchanged. +/// NULL/0 is a size query. BUFFER_TOO_SMALL returns the required count and leaves states untouched. +/// @ingroup rigid_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_rigid_body_read_states( + world: *const RprWorld, + handles: *const RprRigidBodyHandle, + handle_count: usize, + states: *mut RprRigidBodyState, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + for value in input(handles, handle_count)? { + value.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_read_states( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handles, + handle_count, + states, + capacity, + count, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_read_states( + set: *const RprRigidBodySet, + handles: *const RprRigidBodyHandle, + handle_count: usize, + states: *mut RprRigidBodyState, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let bodies = &get(set)?.0; + let handles = input(handles, handle_count)?; + out_ptr(count)?; + for handle in handles { + bodies.get(handle.raw()).ok_or_else(missing)?; + } + if states.is_null() && capacity == 0 { + return output(count, handles.len()); + } + out_ptr(states)?; + ensure( + capacity <= isize::MAX as usize / size_of::(), + "buffer is too large", + )?; + if capacity < handles.len() { + output(count, handles.len())?; + return Err((RPR_BUFFER_TOO_SMALL, "output buffer is too small".into())); + } + for (i, handle) in handles.iter().enumerate() { + states + .add(i) + .write(RprRigidBodyState::from(&bodies[handle.raw()])); + } + output(count, handles.len()) + }) +} + +/// Copies joint configuration without returning a borrowed joint pointer. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_desc(handle: RprImpulseJointHandle) -> RprJointDesc { + let world = handle.world; + ffi_value(|out: *mut RprJointDesc| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprImpulseJointSet = std::ptr::addr_of!((*raw).0.impulse_joints).cast(); + + output( + out, + get(set)? + .0 + .get(handle.raw()) + .ok_or_else(missing)? + .data + .into(), + ) + }) + }) +} + +/// Replaces configuration after validation, resetting cached limit/motor impulses. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_desc( + handle: RprImpulseJointHandle, + desc: *const RprJointDesc, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let desc = get(desc)?.raw()?; + let wake_up = boolean(wake_up)?; + get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)? + .data = desc; + Ok(()) + }) +} diff --git a/c/src/handle_world.rs b/c/src/handle_world.rs new file mode 100644 index 000000000..732a987aa --- /dev/null +++ b/c/src/handle_world.rs @@ -0,0 +1,96 @@ +//! World ownership on foreign entity handles. Internal native adapters may create +//! unbound handles; attach the owner before returning them across the C boundary. +use crate::*; + +pub(crate) trait WorldHandles { + fn attach_world(&mut self, world: *mut RprWorld); + fn check_world(&self, world: *const RprWorld) -> Result; + fn with_world(mut self, world: *mut RprWorld) -> Self + where + Self: Sized, + { + self.attach_world(world); + self + } +} +macro_rules! entity_world { + ($($ty:ty),* $(,)?) => {$ ( + // Handles carry an address only; dereferencing it requires an unsafe API + // call with external lifetime synchronization and the world's borrow gate. + unsafe impl Send for $ty {} + unsafe impl Sync for $ty {} + impl WorldHandles for $ty { + fn attach_world(&mut self, world: *mut RprWorld) { + if self.index != u32::MAX { self.world = world; } + } + fn check_world(&self, world: *const RprWorld) -> Result { + if self.index != u32::MAX && (self.world.is_null() || !std::ptr::eq(self.world, world)) { + return Err((RPR_INVALID_HANDLE, "handle belongs to a different world".into())); + } + Ok(()) + } + } + )*}; +} +entity_world!( + RprRigidBodyHandle, + RprColliderHandle, + RprImpulseJointHandle, + RprMultibodyJointHandle, + RprSoftBodyHandle +); +macro_rules! fields_world { + ($ty:ty, $($field:ident),+ $(,)?) => { + impl WorldHandles for $ty { + fn attach_world(&mut self, world: *mut RprWorld) { $(self.$field.attach_world(world);)+ } + fn check_world(&self, world: *const RprWorld) -> Result { + $(self.$field.check_world(world)?;)+ Ok(()) + } + } + }; +} +fields_world!(RprCharacterCollision, collider, hit); +fields_world!(RprCollisionEvent, collider1, collider2); +fields_world!(RprContactForceEvent, collider1, collider2); +fields_world!(RprContactPair, collider1, collider2); +fields_world!(RprIntersectionPair, collider1, collider2); +fields_world!(RprJointBodies, body1, body2); +fields_world!(RprOptionalParticleDestination, body); +fields_world!(RprOptionalRayHit, hit); +fields_world!(RprParticleDestination, body); +fields_world!(RprPointProjection, collider); +fields_world!(RprQueryFilter, exclude_collider, exclude_rigid_body); +fields_world!(RprQueryOptions, filter); +fields_world!(RprRayHit, collider); +fields_world!(RprRayToi, collider); +fields_world!(RprShapeCastHit, collider); +fields_world!(RprSoftClusterSplit, soft_body, proxy); +fields_world!(RprSoftJointMove, joint, from, to); +fields_world!(RprSoftMeshInfo, collider); +#[cfg(feature = "dim3")] +fields_world!(RprWheelState, ground_object); + +pub(crate) fn ffi_world_value( + world: *const RprWorld, + call: impl FnOnce(*mut T) -> RprStatus, +) -> T { + ffi_value(call).with_world(world.cast_mut()) +} +/// copy_out writes no elements on failure, including BUFFER_TOO_SMALL. +pub(crate) unsafe fn ffi_world_array( + world: *const RprWorld, + buffer: *mut T, + capacity: usize, + call: impl FnOnce(*mut usize) -> RprStatus, +) -> usize { + let count = ffi_value(call); + if rpr_last_status() == RPR_OK && !buffer.is_null() && count <= capacity { + for value in unsafe { std::slice::from_raw_parts_mut(buffer, count) } { + value.attach_world(world.cast_mut()); + } + } + count +} +pub(crate) unsafe fn read_context_world(context: *const RprReadContext) -> *mut RprWorld { + unsafe { get(context).map_or(std::ptr::null_mut(), |c| c.world) } +} diff --git a/c/src/joint_access.rs b/c/src/joint_access.rs new file mode 100644 index 000000000..416bce0ab --- /dev/null +++ b/c/src/joint_access.rs @@ -0,0 +1,906 @@ +//! Joint configuration setters and handle-scoped live-joint edits. +use crate::handle_access::forward; +use crate::*; +/// Set the joint desc joint frame relative to body 1. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_local_frame1( + desc: *mut RprJointDesc, + value: RprPose, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_local_frame1(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint joint frame relative to body 1. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_local_frame1( + handle: RprImpulseJointHandle, + value: RprPose, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_local_frame1( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc joint frame relative to body 2. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_local_frame2( + desc: *mut RprJointDesc, + value: RprPose, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_local_frame2(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint joint frame relative to body 2. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_local_frame2( + handle: RprImpulseJointHandle, + value: RprPose, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_local_frame2( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc joint anchor relative to body 1. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_local_anchor1( + desc: *mut RprJointDesc, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_local_anchor1(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint joint anchor relative to body 1. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_local_anchor1( + handle: RprImpulseJointHandle, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_local_anchor1( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc joint anchor relative to body 2. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_local_anchor2( + desc: *mut RprJointDesc, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_local_anchor2(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint joint anchor relative to body 2. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_local_anchor2( + handle: RprImpulseJointHandle, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_local_anchor2( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Enable or disable allowing contacts between connected bodies for the joint desc. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_contacts_enabled( + desc: *mut RprJointDesc, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_contacts_enabled(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Enable or disable allowing contacts between connected bodies for the impulse joint. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_contacts_enabled( + handle: RprImpulseJointHandle, + value: RprBool, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_contacts_enabled( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Enable or disable the joint desc. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_enabled( + desc: *mut RprJointDesc, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_enabled(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Enable or disable the impulse joint. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_enabled( + handle: RprImpulseJointHandle, + value: RprBool, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_enabled( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc joint spring coefficients. +/// @ingroup soft_bodies +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_softness( + desc: *mut RprJointDesc, + value: RprSpringCoefficients, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_softness(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint joint spring coefficients. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup soft_bodies +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_softness( + handle: RprImpulseJointHandle, + value: RprSpringCoefficients, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_softness( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc translation/rotation lock bitmask. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_locked_axes( + desc: *mut RprJointDesc, + value: u8, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_locked_axes(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint translation/rotation lock bitmask. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_locked_axes( + handle: RprImpulseJointHandle, + value: u8, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_locked_axes( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc joint axis mask with limits enabled. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_limit_axes( + desc: *mut RprJointDesc, + value: u8, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_limit_axes(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint joint axis mask with limits enabled. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_limit_axes( + handle: RprImpulseJointHandle, + value: u8, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_limit_axes( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc joint axis mask with motors enabled. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_motor_axes( + desc: *mut RprJointDesc, + value: u8, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_motor_axes(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint joint axis mask with motors enabled. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_motor_axes( + handle: RprImpulseJointHandle, + value: u8, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_motor_axes( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc coupled joint axis mask. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_coupled_axes( + desc: *mut RprJointDesc, + value: u8, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_coupled_axes(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint coupled joint axis mask. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_coupled_axes( + handle: RprImpulseJointHandle, + value: u8, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_coupled_axes( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc joint principal axis in body 1 local coordinates. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_local_axis1( + desc: *mut RprJointDesc, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_local_axis1(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint joint principal axis in body 1 local coordinates. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_local_axis1( + handle: RprImpulseJointHandle, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_local_axis1( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc joint principal axis in body 2 local coordinates. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_local_axis2( + desc: *mut RprJointDesc, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_local_axis2(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint joint principal axis in body 2 local coordinates. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_local_axis2( + handle: RprImpulseJointHandle, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_local_axis2( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc minimum and maximum limits on an axis (linear distance or angular radians). +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_limits( + desc: *mut RprJointDesc, + joint_axis: u32, + min: RprReal, + max: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_limits( + &mut joint, joint_axis, min, max, + ))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint minimum and maximum limits on an axis (linear distance or angular +/// radians). +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_limits( + handle: RprImpulseJointHandle, + joint_axis: u32, + min: RprReal, + max: RprReal, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_limits( + (&mut joint.data as *mut GenericJoint).cast(), + joint_axis, + min, + max, + )) + }) +} + +/// Set the joint desc motor position/velocity targets and spring coefficients on an axis. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_motor( + desc: *mut RprJointDesc, + joint_axis: u32, + target_position: RprReal, + target_velocity: RprReal, + stiffness: RprReal, + damping: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_motor( + &mut joint, + joint_axis, + target_position, + target_velocity, + stiffness, + damping, + ))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint motor position/velocity targets and spring coefficients on an axis. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_motor( + handle: RprImpulseJointHandle, + joint_axis: u32, + target_position: RprReal, + target_velocity: RprReal, + stiffness: RprReal, + damping: RprReal, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_motor( + (&mut joint.data as *mut GenericJoint).cast(), + joint_axis, + target_position, + target_velocity, + stiffness, + damping, + )) + }) +} + +/// Set the joint desc maximum motor force or torque on an axis. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_motor_max_force( + desc: *mut RprJointDesc, + joint_axis: u32, + max_force: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_motor_max_force( + &mut joint, joint_axis, max_force, + ))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint maximum motor force or torque on an axis. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_motor_max_force( + handle: RprImpulseJointHandle, + joint_axis: u32, + max_force: RprReal, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_motor_max_force( + (&mut joint.data as *mut GenericJoint).cast(), + joint_axis, + max_force, + )) + }) +} + +/// Set the joint desc motor model on an axis (0 = acceleration-based, 1 = force-based). +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_motor_model( + desc: *mut RprJointDesc, + joint_axis: u32, + model: u32, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_motor_model( + &mut joint, joint_axis, model, + ))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint motor model on an axis (0 = acceleration-based, 1 = force-based). +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_motor_model( + handle: RprImpulseJointHandle, + joint_axis: u32, + model: u32, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_motor_model( + (&mut joint.data as *mut GenericJoint).cast(), + joint_axis, + model, + )) + }) +} + +/// Set the joint desc application-owned 128-bit user value. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_user_data( + desc: *mut RprJointDesc, + value: RprUserData, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_user_data(&mut joint, value))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint application-owned 128-bit user value. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_user_data( + handle: RprImpulseJointHandle, + value: RprUserData, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_user_data( + (&mut joint.data as *mut GenericJoint).cast(), + value, + )) + }) +} + +/// Set the joint desc motor position target and spring coefficients on an axis. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_motor_position( + desc: *mut RprJointDesc, + joint_axis: u32, + target_position: RprReal, + stiffness: RprReal, + damping: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_motor_position( + &mut joint, + joint_axis, + target_position, + stiffness, + damping, + ))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint motor position target and spring coefficients on an axis. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_motor_position( + handle: RprImpulseJointHandle, + joint_axis: u32, + target_position: RprReal, + stiffness: RprReal, + damping: RprReal, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_motor_position( + (&mut joint.data as *mut GenericJoint).cast(), + joint_axis, + target_position, + stiffness, + damping, + )) + }) +} + +/// Set the joint desc motor velocity target and damping factor on an axis. +/// @ingroup joints +#[rapier_export(joint_desc)] +pub unsafe extern "C" fn rpr_joint_desc_set_motor_velocity( + desc: *mut RprJointDesc, + joint_axis: u32, + target_velocity: RprReal, + factor: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let mut joint = RprGenericJoint(get(desc)?.raw()?); + forward(rpr_generic_joint_set_motor_velocity( + &mut joint, + joint_axis, + target_velocity, + factor, + ))?; + output(desc, joint.0.into()) + }) +} +/// Set the impulse joint motor velocity target and damping factor on an axis. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_set_motor_velocity( + handle: RprImpulseJointHandle, + joint_axis: u32, + target_velocity: RprReal, + factor: RprReal, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let joint = get_mut(set)? + .0 + .get_mut(handle.raw(), wake_up) + .ok_or_else(missing)?; + forward(rpr_generic_joint_set_motor_velocity( + (&mut joint.data as *mut GenericJoint).cast(), + joint_axis, + target_velocity, + factor, + )) + }) +} diff --git a/c/src/joint_desc.rs b/c/src/joint_desc.rs new file mode 100644 index 000000000..60e858ebb --- /dev/null +++ b/c/src/joint_desc.rs @@ -0,0 +1,301 @@ +//! Plain joint configuration, distinct from a borrowed live joint. +#![allow(non_snake_case)] +use crate::*; +/// @ingroup joints +/// Number of translational and angular joint axes in this dimension. +#[cfg(feature = "dim2")] +pub const RPR_JOINT_DOF_COUNT: usize = 3; +/// @ingroup joints +/// Number of translational and angular joint axes in this dimension. +#[cfg(feature = "dim3")] +pub const RPR_JOINT_DOF_COUNT: usize = 6; +/// Lower and upper axis limits in length units or radians. +/// @ingroup joints +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprJointLimits { + /// Minimum allowed axis displacement (length or radians). + pub min: RprReal, + /// Maximum allowed axis displacement (length or radians). + pub max: RprReal, +} +/// Position/velocity motor settings for one joint axis. +/// @ingroup joints +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprJointMotor { + /// Motor target velocity (length per second or radians per second). + pub targetVel: RprReal, + /// Motor target position (length or radians). + pub targetPos: RprReal, + /// Nonnegative motor spring stiffness. + pub stiffness: RprReal, + /// Nonnegative motor damping. + pub damping: RprReal, + /// Nonnegative maximum force or torque. + pub maxForce: RprReal, + /// Motor model: 0 acceleration-based, 1 force-based. + pub model: u32, +} +/// Copyable joint configuration. Limits/motors take effect when their axis mask is enabled. +/// Solver impulses are deliberately excluded. Applying data resets cached limit and motor impulses. +/// @ingroup joints +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprJointDesc { + /// Joint frame in body 1 local coordinates. + pub localFrame1: RprPose, + /// Joint frame in body 2 local coordinates. + pub localFrame2: RprPose, + /// Locked joint degrees of freedom; translations precede rotations. + pub lockedAxes: u8, + /// Axis mask enabling corresponding limits entries. + pub limitAxes: u8, + /// Axis mask enabling corresponding motors entries. + pub motorAxes: u8, + /// Axis mask sharing a coupled constraint. + pub coupledAxes: u8, + /// Axis limits in translation-then-rotation order. + pub limits: [RprJointLimits; RPR_JOINT_DOF_COUNT], + /// Axis motors in translation-then-rotation order. + pub motors: [RprJointMotor; RPR_JOINT_DOF_COUNT], + /// Spring coefficients for constraint correction. + pub softness: RprSpringCoefficients, + /// Whether connected bodies may collide. + pub contactsEnabled: RprBool, + /// Whether this setting/object is enabled (0 or 1). + pub enabled: RprBool, + /// Application data; Rapier does not own pointers encoded in it. + pub userData: RprUserData, +} +impl From for RprJointDesc { + fn from(j: GenericJoint) -> Self { + Self { + localFrame1: j.local_frame1.into(), + localFrame2: j.local_frame2.into(), + lockedAxes: j.locked_axes.bits(), + limitAxes: j.limit_axes.bits(), + motorAxes: j.motor_axes.bits(), + coupledAxes: j.coupled_axes.bits(), + limits: j.limits.map(|v| RprJointLimits { + min: v.min, + max: v.max, + }), + motors: j.motors.map(|v| RprJointMotor { + targetVel: v.target_vel, + targetPos: v.target_pos, + stiffness: v.stiffness, + damping: v.damping, + maxForce: v.max_force, + model: match v.model { + MotorModel::AccelerationBased => 0, + MotorModel::ForceBased => 1, + }, + }), + softness: j.softness.into(), + contactsEnabled: j.contacts_enabled as _, + enabled: j.is_enabled() as _, + userData: j.user_data.into(), + } + } +} +impl RprJointDesc { + pub(crate) fn raw(&self) -> Result { + let mut j = GenericJoint::new(axes(self.lockedAxes)?); + j.local_frame1 = self.localFrame1.raw()?; + j.local_frame2 = self.localFrame2.raw()?; + j.limit_axes = axes(self.limitAxes)?; + j.motor_axes = axes(self.motorAxes)?; + j.coupled_axes = axes(self.coupledAxes)?; + j.softness = self.softness.raw()?; + j.contacts_enabled = boolean(self.contactsEnabled)?; + j.set_enabled(boolean(self.enabled)?); + j.user_data = self.userData.raw(); + for i in 0..RPR_JOINT_DOF_COUNT { + let l = self.limits[i]; + ensure( + !l.min.is_nan() && !l.max.is_nan(), + "joint limits must not be NaN", + )?; + ensure(l.min <= l.max, "reversed joint limits")?; + j.limits[i].min = l.min; + j.limits[i].max = l.max; + let m = self.motors[i]; + let dst = &mut j.motors[i]; + dst.target_vel = finite(m.targetVel)?; + dst.target_pos = finite(m.targetPos)?; + dst.stiffness = nonnegative(m.stiffness)?; + dst.damping = nonnegative(m.damping)?; + dst.max_force = nonnegative(m.maxForce)?; + dst.model = match m.model { + 0 => MotorModel::AccelerationBased, + 1 => MotorModel::ForceBased, + _ => return Err(invalid("invalid motor model")), + }; + } + Ok(j) + } +} +/// Return native default joint desc. This POD value owns no resources. +/// @ingroup joints +#[rapier_export] +pub extern "C" fn rpr_default_joint_desc() -> RprJointDesc { + GenericJoint::new(JointAxesMask::empty()).into() +} + +// Axis normalization cannot fail across the C boundary. Invalid directions produce +// nonfinite frames, which the normal description validation rejects at insertion. +fn joint_desc_with_axis(locked_axes: JointAxesMask, axis: RprVector) -> RprJointDesc { + let mut desc = RprJointDesc::from(GenericJoint::new(locked_axes)); + let rotation = match axis.raw().ok().and_then(|v| v.try_normalize()) { + Some(axis) => GenericJoint::complete_ang_frame(axis).into(), + None => { + #[cfg(feature = "dim2")] + { + RprRotation { angle: Real::NAN } + } + #[cfg(feature = "dim3")] + { + RprRotation { + x: Real::NAN, + y: Real::NAN, + z: Real::NAN, + w: Real::NAN, + } + } + } + }; + desc.localFrame1.rotation = rotation; + desc.localFrame2.rotation = rotation; + desc +} + +/// Return a fixed joint description with native defaults; no allocation. +/// @ingroup joints +#[rapier_export] +pub extern "C" fn rpr_fixed_joint_desc() -> RprJointDesc { + GenericJoint::from(FixedJointBuilder::new().build()).into() +} +/// Return a revolute joint description with native defaults; no allocation. +/// @ingroup joints +#[cfg(feature = "dim2")] +#[rapier_export] +pub extern "C" fn rpr_revolute_joint_desc() -> RprJointDesc { + GenericJoint::from(RevoluteJointBuilder::new().build()).into() +} +/// Returns a joint description. Invalid axes produce nonfinite frames, rejected on insertion. +/// @ingroup joints +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_revolute_joint_desc(axis_vector: RprVector) -> RprJointDesc { + joint_desc_with_axis(JointAxesMask::LOCKED_REVOLUTE_AXES, axis_vector) +} +/// Returns a joint description. Invalid axes produce nonfinite frames, rejected on insertion. +/// @ingroup joints +#[rapier_export] +pub extern "C" fn rpr_prismatic_joint_desc(axis_vector: RprVector) -> RprJointDesc { + joint_desc_with_axis(JointAxesMask::LOCKED_PRISMATIC_AXES, axis_vector) +} +/// Return a rope joint description with native defaults; no allocation. +/// @ingroup joints +#[rapier_export] +pub extern "C" fn rpr_rope_joint_desc(length: RprReal) -> RprJointDesc { + GenericJoint::from(RopeJointBuilder::new(length).build()).into() +} +/// Return a spring joint description with native defaults; no allocation. +/// @ingroup joints +#[rapier_export] +pub extern "C" fn rpr_spring_joint_desc( + length: RprReal, + stiffness: RprReal, + damping: RprReal, +) -> RprJointDesc { + GenericJoint::from(SpringJointBuilder::new(length, stiffness, damping).build()).into() +} +/// Return a spherical joint description with native defaults; no allocation. +/// @ingroup joints +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_spherical_joint_desc() -> RprJointDesc { + GenericJoint::from(SphericalJointBuilder::new().build()).into() +} +/// Returns a joint description. Invalid axes produce nonfinite frames, rejected on insertion. +/// @ingroup joints +#[cfg(feature = "dim2")] +#[rapier_export] +pub extern "C" fn rpr_pin_slot_joint_desc(axis_vector: RprVector) -> RprJointDesc { + joint_desc_with_axis(JointAxesMask::LOCKED_PIN_SLOT_AXES, axis_vector) +} +/// Create an impulse joint connecting two bodies in the same world. The world owns the joint; +/// wake_up wakes the connected bodies. +/// @ingroup joints +#[rapier_export] +pub unsafe extern "C" fn rpr_insert_impulse_joint( + body1: RprRigidBodyHandle, + body2: RprRigidBodyHandle, + joint: *const RprJointDesc, +) -> RprImpulseJointHandle { + let world = body1.world; + ffi_world_value(world, |out: *mut RprImpulseJointHandle| { + ffi(|| unsafe { + body1.check_world(world)?; + body2.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + if !out.is_null() { + out_ptr(out)?; + } + let joint = get(joint)?.raw()?; + let world = &mut get_mut(world)?.0; + world.bodies.get(body1.raw()).ok_or_else(missing)?; + world.bodies.get(body2.raw()).ok_or_else(missing)?; + ensure(body1 != body2, "joint endpoints must differ")?; + let handle = world.insert_impulse_joint(body1.raw(), body2.raw(), joint); + if !out.is_null() { + output(out, handle.into())?; + } + Ok(()) + }) + }) +} + +/// Create an articulation joint between bodies in the same world. Returns an invalid handle on +/// failure; check rpr_last_status. +/// @ingroup joints +#[rapier_export] +pub unsafe extern "C" fn rpr_insert_multibody_joint( + body1: RprRigidBodyHandle, + body2: RprRigidBodyHandle, + joint: *const RprJointDesc, +) -> RprMultibodyJointHandle { + let world = body1.world; + ffi_world_value(world, |out: *mut RprMultibodyJointHandle| { + ffi(|| unsafe { + body1.check_world(world)?; + body2.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + if !out.is_null() { + out_ptr(out)?; + } + let joint = get(joint)?.raw()?; + let world = &mut get_mut(world)?.0; + world.bodies.get(body1.raw()).ok_or_else(missing)?; + world.bodies.get(body2.raw()).ok_or_else(missing)?; + ensure(body1 != body2, "joint endpoints must differ")?; + let handle = world + .insert_multibody_joint(body1.raw(), body2.raw(), joint) + .ok_or_else(|| invalid("multibody loop or duplicate link"))?; + if !out.is_null() { + output(out, handle.into())?; + } + Ok(()) + }) + }) +} diff --git a/c/src/joints.rs b/c/src/joints.rs new file mode 100644 index 000000000..c3f86c3f1 --- /dev/null +++ b/c/src/joints.rs @@ -0,0 +1,539 @@ +use crate::*; +pub(crate) fn axis(value: u32) -> Result { + match value { + 0 => Ok(JointAxis::LinX), + 1 => Ok(JointAxis::LinY), + #[cfg(feature = "dim3")] + 2 => Ok(JointAxis::LinZ), + #[cfg(feature = "dim3")] + 3 => Ok(JointAxis::AngX), + #[cfg(feature = "dim3")] + 4 => Ok(JointAxis::AngY), + #[cfg(feature = "dim3")] + 5 => Ok(JointAxis::AngZ), + #[cfg(feature = "dim2")] + 2 => Ok(JointAxis::AngX), + _ => Err(invalid("joint axis out of range")), + } +} +pub(crate) fn axes(value: u8) -> Result { + JointAxesMask::from_bits(value).ok_or_else(|| invalid("invalid joint axes")) +} + +pub(crate) unsafe fn rpr_generic_joint_set_local_frame1( + joint: *mut RprGenericJoint, + value: RprPose, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + get_mut(joint)?.0.set_local_frame1(value); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_local_frame2( + joint: *mut RprGenericJoint, + value: RprPose, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + get_mut(joint)?.0.set_local_frame2(value); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_local_anchor1( + joint: *mut RprGenericJoint, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + get_mut(joint)?.0.set_local_anchor1(value); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_local_anchor2( + joint: *mut RprGenericJoint, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + get_mut(joint)?.0.set_local_anchor2(value); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_contacts_enabled( + joint: *mut RprGenericJoint, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + get_mut(joint)?.0.set_contacts_enabled(value); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_enabled( + joint: *mut RprGenericJoint, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + get_mut(joint)?.0.set_enabled(value); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_softness( + joint: *mut RprGenericJoint, + value: RprSpringCoefficients, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let _ = get_mut(joint)?.0.set_softness(value); + Ok(()) + }) +} + +pub(crate) unsafe fn rpr_generic_joint_set_locked_axes( + joint: *mut RprGenericJoint, + value: u8, +) -> RprStatus { + ffi(|| unsafe { + let value = axes(value)?; + get_mut(joint)?.0.locked_axes = value; + Ok(()) + }) +} + +pub(crate) unsafe fn rpr_generic_joint_set_limit_axes( + joint: *mut RprGenericJoint, + value: u8, +) -> RprStatus { + ffi(|| unsafe { + let value = axes(value)?; + get_mut(joint)?.0.limit_axes = value; + Ok(()) + }) +} + +pub(crate) unsafe fn rpr_generic_joint_set_motor_axes( + joint: *mut RprGenericJoint, + value: u8, +) -> RprStatus { + ffi(|| unsafe { + let value = axes(value)?; + get_mut(joint)?.0.motor_axes = value; + Ok(()) + }) +} + +pub(crate) unsafe fn rpr_generic_joint_set_coupled_axes( + joint: *mut RprGenericJoint, + value: u8, +) -> RprStatus { + ffi(|| unsafe { + let value = axes(value)?; + get_mut(joint)?.0.coupled_axes = value; + Ok(()) + }) +} + +pub(crate) unsafe fn rpr_generic_joint_set_local_axis1( + joint: *mut RprGenericJoint, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let v = value.raw()?; + positive(v.length())?; + get_mut(joint)?.0.set_local_axis1(v.normalize()); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_local_axis2( + joint: *mut RprGenericJoint, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let v = value.raw()?; + positive(v.length())?; + get_mut(joint)?.0.set_local_axis2(v.normalize()); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_limits( + joint: *mut RprGenericJoint, + joint_axis: u32, + min: RprReal, + max: RprReal, +) -> RprStatus { + ffi(|| unsafe { + ensure( + !min.is_nan() && !max.is_nan() && min <= max, + "limits must be ordered and not NaN", + )?; + let a = axis(joint_axis)?; + get_mut(joint)?.0.set_limits(a, [min, max]); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_motor( + joint: *mut RprGenericJoint, + joint_axis: u32, + target_position: RprReal, + target_velocity: RprReal, + stiffness: RprReal, + damping: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let a = axis(joint_axis)?; + finite(target_position)?; + finite(target_velocity)?; + nonnegative(stiffness)?; + nonnegative(damping)?; + get_mut(joint)? + .0 + .set_motor(a, target_position, target_velocity, stiffness, damping); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_motor_max_force( + joint: *mut RprGenericJoint, + joint_axis: u32, + max_force: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let a = axis(joint_axis)?; + nonnegative(max_force)?; + get_mut(joint)?.0.set_motor_max_force(a, max_force); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_motor_model( + joint: *mut RprGenericJoint, + joint_axis: u32, + model: u32, +) -> RprStatus { + ffi(|| unsafe { + let a = axis(joint_axis)?; + let model = match model { + 0 => MotorModel::AccelerationBased, + 1 => MotorModel::ForceBased, + _ => return Err(invalid("unknown motor model")), + }; + get_mut(joint)?.0.set_motor_model(a, model); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_user_data( + joint: *mut RprGenericJoint, + value: RprUserData, +) -> RprStatus { + ffi(|| unsafe { + get_mut(joint)?.0.user_data = value.raw(); + Ok(()) + }) +} + +/// Remove an impulse joint. wake_up wakes its connected bodies. +/// @ingroup joints +#[rapier_export] +pub unsafe extern "C" fn rpr_remove_impulse_joint( + handle: RprImpulseJointHandle, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprImpulseJointSet = std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + + let wake_up = boolean(wake_up)?; + let set = get_mut(set)?; + set.0.get(handle.raw()).ok_or_else(missing)?; + set.0.remove(handle.raw(), wake_up); + Ok(()) + }) +} + +/// Copy entity handles. +/// @see @ref output_buffers +/// @ingroup joints +#[rapier_export] +pub unsafe extern "C" fn rpr_impulse_joint_handles( + world: *const RprWorld, + buffer: *mut RprImpulseJointHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprImpulseJointSet = + std::ptr::addr_of!((*raw).0.impulse_joints).cast(); + + let values: Vec<_> = get(set)?.0.iter().map(|(h, _)| h.into()).collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + } +} + +/// Remove an articulation joint. wake_up wakes affected bodies. +/// @ingroup joints +#[rapier_export] +pub unsafe extern "C" fn rpr_remove_multibody_joint( + handle: RprMultibodyJointHandle, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprMultibodyJointSet = + std::ptr::addr_of_mut!((*raw).0.multibody_joints).cast(); + + let wake_up = boolean(wake_up)?; + let set = get_mut(set)?; + set.0.get(handle.raw()).ok_or_else(missing)?; + set.0.remove(handle.raw(), wake_up); + Ok(()) + }) +} + +/// Copy entity handles. +/// @see @ref output_buffers +/// @ingroup joints +#[rapier_export] +pub unsafe extern "C" fn rpr_multibody_joint_handles( + world: *const RprWorld, + buffer: *mut RprMultibodyJointHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprMultibodyJointSet = + std::ptr::addr_of!((*raw).0.multibody_joints).cast(); + + let values: Vec<_> = get(set)?.0.iter().map(|(h, _, _, _)| h.into()).collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + } +} + +/// Return the two bodies connected by an impulse joint. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_bodies(handle: RprImpulseJointHandle) -> RprJointBodies { + let world = handle.world; + ffi_world_value(world, |result: *mut RprJointBodies| { + let body1 = unsafe { std::ptr::addr_of_mut!((*result).body1) }; + let body2 = unsafe { std::ptr::addr_of_mut!((*result).body2) }; + + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprImpulseJointSet = std::ptr::addr_of!((*raw).0.impulse_joints).cast(); + + out_ptr(body1)?; + out_ptr(body2)?; + let j = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + output(body1, j.body1().into())?; + output(body2, j.body2().into()) + }) + }) +} + +/// Damped least-squares inverse-kinematics parameters. +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprInverseKinematicsOptions { + /// Nonnegative motor damping. + pub damping: RprReal, + /// Maximum inverse-kinematics iterations. + pub max_iters: usize, + /// Controlled-axis bitmask; translations precede rotations. + pub constrained_axes: u8, + /// Linear convergence tolerance. + pub epsilon_linear: RprReal, + /// Angular convergence tolerance in radians. + pub epsilon_angular: RprReal, +} +/// Return native default inverse kinematics options. This POD value owns no resources. +/// @ingroup joints +#[rapier_export] +pub extern "C" fn rpr_default_inverse_kinematics_options() -> RprInverseKinematicsOptions { + let options = InverseKinematicsOption::default(); + RprInverseKinematicsOptions { + damping: options.damping, + max_iters: options.max_iters, + constrained_axes: options.constrained_axes.bits(), + epsilon_linear: options.epsilon_linear, + epsilon_angular: options.epsilon_angular, + } +} + +/// Return the articulation degrees of freedom associated with the joint. +/// @ingroup joints +#[rapier_export(multibody_joint)] +pub unsafe extern "C" fn rpr_multibody_joint_ndofs(handle: RprMultibodyJointHandle) -> usize { + let world = handle.world; + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprMultibodyJointSet = + std::ptr::addr_of!((*raw).0.multibody_joints).cast(); + + output( + out, + get(set)?.0.get(handle.raw()).ok_or_else(missing)?.0.ndofs(), + ) + }) + }) +} + +/// Optional per-link filter, called synchronously. Must not reenter or retain physics objects. +/// @ingroup joints +pub type RprIkJointCanMove = + Option RprBool>; +/// Read/write displacement buffer must contain exactly ndofs entries; zero it for a fresh solve. +/// @ingroup joints +#[rapier_export(multibody_joint)] +pub unsafe extern "C" fn rpr_multibody_joint_inverse_kinematics( + handle: RprMultibodyJointHandle, + options: *const RprInverseKinematicsOptions, + target: RprPose, + can_move: RprIkJointCanMove, + user_data: *mut std::ffi::c_void, + displacements: *mut RprReal, + count: usize, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprMultibodyJointSet = std::ptr::addr_of!((*raw).0.multibody_joints).cast(); + let bodies: *const RprRigidBodySet = std::ptr::addr_of!((*raw).0.bodies).cast(); + + let (multibody, link_id) = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + ensure(multibody.link(link_id).is_some(), "invalid multibody link")?; + ensure( + count == multibody.ndofs(), + "displacement count must equal articulation dofs", + )?; + let values = input(displacements, count)?; + for &value in values { + finite(value)?; + } + let mut values = rapier::math::DVector::from_column_slice(values); + let options = get(options)?; + let options = InverseKinematicsOption { + damping: nonnegative(options.damping)?, + max_iters: options.max_iters, + constrained_axes: axes(options.constrained_axes)?, + epsilon_linear: nonnegative(options.epsilon_linear)?, + epsilon_angular: nonnegative(options.epsilon_angular)?, + }; + let target = target.raw()?; + multibody.inverse_kinematics( + &get(bodies)?.0, + link_id, + &options, + &target, + |link| { + can_move.is_none_or(|callback| { + callback( + user_data, + RprRigidBodyHandle::from(link.rigid_body_handle()).with_world(world), + ) != 0 + }) + }, + &mut values, + ); + if count != 0 { + std::ptr::copy_nonoverlapping(values.as_ptr(), displacements, count); + } + Ok(()) + }) +} + +/// Apply generalized articulation displacements in native degree-of-freedom order. +/// @ingroup joints +#[rapier_export(multibody_joint)] +pub unsafe extern "C" fn rpr_multibody_joint_apply_displacements( + handle: RprMultibodyJointHandle, + displacements: *const RprReal, + count: usize, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprMultibodyJointSet = + std::ptr::addr_of_mut!((*raw).0.multibody_joints).cast(); + + let values = input(displacements, count)?; + for &value in values { + finite(value)?; + } + let (multibody, _) = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + count == multibody.ndofs(), + "displacement count must equal articulation dofs", + )?; + multibody.apply_displacements(values); + Ok(()) + }) +} + +pub(crate) unsafe fn rpr_generic_joint_set_motor_position( + joint: *mut RprGenericJoint, + joint_axis: u32, + target_position: RprReal, + stiffness: RprReal, + damping: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let a = axis(joint_axis)?; + finite(target_position)?; + nonnegative(stiffness)?; + nonnegative(damping)?; + get_mut(joint)? + .0 + .set_motor_position(a, target_position, stiffness, damping); + Ok(()) + }) +} +pub(crate) unsafe fn rpr_generic_joint_set_motor_velocity( + joint: *mut RprGenericJoint, + joint_axis: u32, + target_velocity: RprReal, + factor: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let a = axis(joint_axis)?; + finite(target_velocity)?; + nonnegative(factor)?; + get_mut(joint)? + .0 + .set_motor_velocity(a, target_velocity, factor); + Ok(()) + }) +} diff --git a/c/src/lib.rs b/c/src/lib.rs new file mode 100644 index 000000000..bf5a0d6c5 --- /dev/null +++ b/c/src/lib.rs @@ -0,0 +1,91 @@ +//! C ABI shared by the four Rapier dimensions/precisions. See c/README.md. +#![deny(unsafe_op_in_unsafe_fn)] +#![allow(clippy::missing_safety_doc, clippy::too_many_arguments)] +use rapier::math::{AngVector, Pose, Real, Rotation, Vector}; +use rapier::prelude::*; +use rapier_c_macros::rapier_export; +mod config_data; +pub use config_data::*; +mod joint_desc; +pub use joint_desc::*; +mod soft_desc; +pub use soft_desc::*; +mod world_queries; +pub use world_queries::*; +mod descriptors; +pub use descriptors::*; +mod control; +mod dynamics; +mod error; +mod geometry; +mod joints; +mod objects; +mod pipeline; +mod queries; +mod soft_body; +mod types; +pub use control::*; +pub use dynamics::*; +pub use error::*; +pub use geometry::*; +pub use joints::*; +pub use objects::*; +pub use pipeline::*; +pub use queries::*; +pub use soft_body::*; +pub use types::*; + +mod extra; +pub use extra::*; + +#[cfg(test)] +mod tests; + +mod render; +pub use render::*; + +#[cfg(all(feature = "robotics", feature = "dim3", feature = "f32"))] +mod robotics; +#[cfg(all(feature = "robotics", feature = "dim3", feature = "f32"))] +pub use robotics::*; + +#[cfg(test)] +mod pod_tests; + +mod handle_access; +pub use handle_access::*; + +mod array_views; +pub use array_views::*; + +mod shape_desc; +pub use shape_desc::*; + +mod soft_recipes; +pub use soft_recipes::*; + +mod scoped_access; +pub use scoped_access::*; + +mod joint_access; +pub use joint_access::*; + +mod geometry_views; +pub use geometry_views::*; + +mod world; +pub use world::*; +mod read_access; +pub use read_access::*; + +#[cfg(test)] +mod world_tests; + +mod return_values; +pub use return_values::*; + +mod handle_world; +use handle_world::*; + +#[cfg(test)] +mod owner_handle_tests; diff --git a/c/src/objects.rs b/c/src/objects.rs new file mode 100644 index 000000000..948f477a6 --- /dev/null +++ b/c/src/objects.rs @@ -0,0 +1,374 @@ +use crate::*; +// Internal transparent adapter for native PhysicsWorld operations. +#[repr(transparent)] +pub(crate) struct RprPhysicsWorld(pub(crate) PhysicsWorld); + +// Internal transparent adapter for native PhysicsPipeline operations. +#[repr(transparent)] +pub(crate) struct RprPhysicsPipeline(pub(crate) PhysicsPipeline); + +// Internal transparent adapter for native IslandManager operations. +#[repr(transparent)] +pub(crate) struct RprIslandManager(pub(crate) IslandManager); + +// Internal transparent adapter for native BroadPhaseBvh operations. +#[repr(transparent)] +pub(crate) struct RprBroadPhaseBvh(pub(crate) BroadPhaseBvh); + +// Internal transparent adapter for native NarrowPhase operations. +#[repr(transparent)] +pub(crate) struct RprNarrowPhase(pub(crate) NarrowPhase); + +// Internal transparent adapter for native RigidBodySet operations. +#[repr(transparent)] +pub(crate) struct RprRigidBodySet(pub(crate) RigidBodySet); + +// Internal transparent adapter for native ColliderSet operations. +#[repr(transparent)] +pub(crate) struct RprColliderSet(pub(crate) ColliderSet); + +// Internal transparent adapter for native ImpulseJointSet operations. +#[repr(transparent)] +pub(crate) struct RprImpulseJointSet(pub(crate) ImpulseJointSet); + +// Internal transparent adapter for native MultibodyJointSet operations. +#[repr(transparent)] +pub(crate) struct RprMultibodyJointSet(pub(crate) MultibodyJointSet); + +// Internal transparent adapter for native SoftBodySet operations. +#[repr(transparent)] +pub(crate) struct RprSoftBodySet(pub(crate) SoftBodySet); + +// Internal transparent adapter for native IntegrationParameters operations. +#[repr(transparent)] +pub(crate) struct NativeIntegrationParameters(pub(crate) IntegrationParameters); + +// Internal transparent adapter for native RigidBody operations. +#[repr(transparent)] +pub(crate) struct RprRigidBody(pub(crate) RigidBody); + +// Internal transparent adapter for native Collider operations. +#[repr(transparent)] +pub(crate) struct RprCollider(pub(crate) Collider); + +// Internal transparent adapter for native GenericJoint operations. +#[repr(transparent)] +pub(crate) struct RprGenericJoint(pub(crate) GenericJoint); + +/// Opaque SharedShape. See the ownership and borrowing contract in README.md. +/// @ingroup shapes +#[repr(transparent)] +pub struct RprSharedShape(pub(crate) SharedShape); +/// Frees an owned object; NULL is allowed. Never free a borrowed pointer. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_free_shared_shape(object: *mut RprSharedShape) -> RprStatus { + ffi(|| unsafe { + if !object.is_null() { + get(object)?; + drop(Box::from_raw(object)); + } + Ok(()) + }) +} + +/// Create an owned wrapper sharing the same immutable geometry. Release it with +/// rpr_free_shared_shape. +/// @ingroup shapes +#[rapier_export(shared_shape)] +pub unsafe extern "C" fn rpr_shared_shape_clone( + object: *const RprSharedShape, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let value = get(object)?.0.clone(); + output(out, Box::into_raw(Box::new(RprSharedShape(value)))) + }) + }) +} + +// Internal transparent adapter for native SoftBody operations. +#[repr(transparent)] +pub(crate) struct RprSoftBody(pub(crate) SoftBody); + +/// Return the number of rigid body objects in the world. +/// @ingroup rigid_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_rigid_body_count(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_len( + std::ptr::addr_of!((*raw).0.bodies).cast(), + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_len( + object: *const RprRigidBodySet, + out: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.len()) + }) +} + +/// Copy entity handles. +/// @see @ref output_buffers +/// @ingroup rigid_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_rigid_body_handles( + world: *const RprWorld, + buffer: *mut RprRigidBodyHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_handles( + std::ptr::addr_of!((*raw).0.bodies).cast(), + buffer, + capacity, + count, + )) + }) + }) + } +} + +pub(crate) unsafe fn native_rigid_body_set_handles( + set: *const RprRigidBodySet, + buffer: *mut RprRigidBodyHandle, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let values: Vec<_> = get(set)?.0.iter().map(|(h, _)| h.into()).collect(); + copy_out(&values, buffer, capacity, count) + }) +} + +/// Test whether the live world contains this rigid body handle. A removed/stale handle returns +/// false. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_contains(handle: RprRigidBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_contains( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_contains( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { output(out, get(set)?.0.get(handle.raw()).is_some() as u32) }) +} + +/// Return the number of collider objects in the world. +/// @ingroup colliders +#[rapier_export] +pub unsafe extern "C" fn rpr_collider_count(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_len( + std::ptr::addr_of!((*raw).0.colliders).cast(), + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_len( + object: *const RprColliderSet, + out: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.len()) + }) +} + +/// Copy entity handles. +/// @see @ref output_buffers +/// @ingroup colliders +#[rapier_export] +pub unsafe extern "C" fn rpr_collider_handles( + world: *const RprWorld, + buffer: *mut RprColliderHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_handles( + std::ptr::addr_of!((*raw).0.colliders).cast(), + buffer, + capacity, + count, + )) + }) + }) + } +} + +pub(crate) unsafe fn native_collider_set_handles( + set: *const RprColliderSet, + buffer: *mut RprColliderHandle, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let values: Vec<_> = get(set)?.0.iter().map(|(h, _)| h.into()).collect(); + copy_out(&values, buffer, capacity, count) + }) +} + +/// Test whether the live world contains this collider handle. A removed/stale handle returns false. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_contains(handle: RprColliderHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_contains( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_contains( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { output(out, get(set)?.0.get(handle.raw()).is_some() as u32) }) +} + +/// Return the number of soft body objects in the world. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_soft_body_count(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let object = get(object)?; + output(out, object.0.len()) + }) + }) +} + +/// Copy entity handles. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_soft_body_handles( + world: *const RprWorld, + buffer: *mut RprSoftBodyHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let values: Vec<_> = get(set)?.0.iter().map(|(h, _)| h.into()).collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + } +} + +/// Test whether the live world contains this soft body handle. A removed/stale handle returns +/// false. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_contains(handle: RprSoftBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + output(out, get(set)?.0.get(handle.raw()).is_some() as u32) + }) + }) +} + +/// Remove a body and its joints, optionally keeping colliders as standalone objects. +/// Returns whether a body was removed; a stale handle returns false without error. +/// @ingroup rigid_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_remove_rigid_body( + handle: RprRigidBodyHandle, + remove_attached_colliders: RprBool, +) -> RprBool { + let world = handle.world; + ffi_value(|removed: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let remove = boolean(remove_attached_colliders)?; + if !removed.is_null() { + out_ptr(removed)?; + } + let access = get(world)?.write()?; + let world = &mut (*access.raw()).0; + if let Some(body) = world.bodies.get(handle.raw()) { + ensure( + body.soft_body().is_none() || body.is_soft_frame(), + "remove a soft-body root through RemoveSoftBody", + )?; + } + let did_remove = world + .remove_body_with_colliders(handle.raw(), remove) + .is_some(); + if !removed.is_null() { + output(removed, did_remove as RprBool)?; + } + Ok(()) + }) + }) +} diff --git a/c/src/owner_handle_tests.rs b/c/src/owner_handle_tests.rs new file mode 100644 index 000000000..db8cea10c --- /dev/null +++ b/c/src/owner_handle_tests.rs @@ -0,0 +1,263 @@ +use crate::*; +use std::ptr; + +fn ok() { + assert_eq!(rpr_last_status(), RPR_OK); +} + +#[test] +fn collider_insertion_checks_parent_and_preserves_world_on_failure() { + unsafe { + let world = rpr_new_world(); + ok(); + let parent = rpr_insert_rigid_body(world, &rpr_dynamic_rigid_body_desc()); + ok(); + let desc = rpr_ball_collider_desc(0.5); + let attached = rpr_insert_collider(parent, &desc); + ok(); + assert_eq!(rpr_collider_parent(attached), parent); + ok(); + let standalone = rpr_insert_collider_without_parent(world, &desc); + ok(); + assert_eq!(standalone.world, world); + assert_eq!( + rpr_collider_parent(standalone), + RprRigidBodyHandle::default() + ); + ok(); + assert_eq!(rpr_remove_rigid_body(parent, 1), 1); + ok(); + assert_eq!(rpr_collider_count(world), 1); + ok(); + let failed = rpr_insert_collider(parent, &desc); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + assert_eq!(failed, RprColliderHandle::default()); + assert_eq!(rpr_collider_count(world), 1); + ok(); + let failed = rpr_insert_collider(RprRigidBodyHandle::default(), &desc); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + assert_eq!(failed, RprColliderHandle::default()); + let failed = rpr_insert_collider_without_parent(ptr::null_mut(), &desc); + assert_eq!(rpr_last_status(), RPR_NULL_POINTER); + assert_eq!(failed, RprColliderHandle::default()); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn equal_native_indices_in_different_worlds_remain_distinct() { + unsafe { + let a = rpr_new_world(); + let b = rpr_new_world(); + let mut desc = rpr_dynamic_rigid_body_desc(); + desc.position.translation.x = 1.0; + let first_body = rpr_insert_rigid_body(a, &desc); + ok(); + let first_collider = rpr_insert_collider(first_body, &rpr_ball_collider_desc(0.5)); + ok(); + desc.position.translation.x = 7.0; + let other_body = rpr_insert_rigid_body(b, &desc); + ok(); + let other_collider = rpr_insert_collider(other_body, &rpr_ball_collider_desc(0.5)); + ok(); + assert_eq!(first_body.world, a); + assert_eq!(first_collider.world, a); + assert_eq!(other_body.world, b); + assert_eq!(first_body.index, other_body.index); + assert_eq!(first_body.generation, other_body.generation); + assert_ne!(first_body, other_body); + assert_eq!(rpr_rigid_body_translation(first_body).x, 1.0); + ok(); + assert_eq!(rpr_rigid_body_translation(other_body).x, 7.0); + ok(); + assert_eq!(rpr_collider_parent(first_collider), first_body); + ok(); + let failed = rpr_insert_impulse_joint(first_body, other_body, &rpr_fixed_joint_desc()); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + assert_eq!(failed, RprImpulseJointHandle::default()); + assert_eq!(rpr_impulse_joint_handles(a, ptr::null_mut(), 0), 0); + ok(); + let attached = rpr_insert_collider(other_body, &rpr_ball_collider_desc(0.5)); + ok(); + assert_eq!(attached.world, b); + assert_eq!(rpr_collider_parent(attached), other_body); + ok(); + assert_eq!(rpr_collider_count(b), 2); + ok(); + assert_eq!(rpr_collider_count(a), 1); + ok(); + let mut options = rpr_default_query_options(); + options.filter.exclude_collider = other_collider; + rpr_cast_ray(a, &options, RprVector::default(), Vector::X.into(), 10.0, 1); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + assert_eq!(rpr_free_world(a), RPR_OK); + assert_eq!(rpr_free_world(b), RPR_OK); + } +} + +#[test] +fn returned_arrays_joints_and_query_hits_keep_the_owner() { + unsafe { + let world = rpr_new_world(); + let fixed = rpr_insert_rigid_body(world, &rpr_fixed_rigid_body_desc()); + ok(); + let mut desc = rpr_dynamic_rigid_body_desc(); + desc.position.translation.x = 3.0; + let item_body = rpr_insert_rigid_body(world, &desc); + ok(); + let item_collider = rpr_insert_collider(item_body, &rpr_ball_collider_desc(0.5)); + ok(); + let joint = rpr_insert_impulse_joint(fixed, item_body, &rpr_fixed_joint_desc()); + ok(); + assert_eq!(joint.world, world); + let ends = rpr_impulse_joint_bodies(joint); + ok(); + assert_eq!(ends.body1, fixed); + assert_eq!(ends.body2, item_body); + let mut list = [RprRigidBodyHandle::default(); 2]; + assert_eq!( + rpr_rigid_body_handles(world, list.as_mut_ptr(), list.len()), + 2 + ); + ok(); + assert!(list.iter().all(|h| h.world == world)); + let sentinel = list[0]; + assert_eq!(rpr_rigid_body_handles(world, list.as_mut_ptr(), 1), 2); + assert_eq!(rpr_last_status(), RPR_BUFFER_TOO_SMALL); + assert_eq!(list[0], sentinel); + let mut colliders = [RprColliderHandle::default(); 1]; + assert_eq!( + rpr_rigid_body_colliders(item_body, colliders.as_mut_ptr(), 1), + 1 + ); + ok(); + assert_eq!(colliders[0], item_collider); + assert_eq!( + rpr_detect_collisions(world, ptr::null(), ptr::null()), + RPR_OK + ); + let hit = rpr_cast_ray( + world, + ptr::null(), + Vector::ZERO.into(), + Vector::X.into(), + 10.0, + 1, + ); + ok(); + assert_eq!(hit.collider, item_collider); + assert_eq!(rpr_remove_rigid_body(item_body, 1), 1); + ok(); + rpr_rigid_body_translation(item_body); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn restored_world_returns_fresh_owners_and_survives_source_destruction() { + unsafe { + let original = rpr_new_world(); + let mut desc = rpr_dynamic_rigid_body_desc(); + desc.position.translation.x = 5.0; + let old = rpr_insert_rigid_body(original, &desc); + ok(); + let snapshot = rpr_serialize_world(original); + ok(); + let bytes = rpr_bytes_data(snapshot); + ok(); + let restored = rpr_deserialize_world(bytes.data, bytes.count); + ok(); + let mut fresh = RprRigidBodyHandle::default(); + assert_eq!(rpr_rigid_body_handles(restored, &mut fresh, 1), 1); + ok(); + assert_eq!(fresh.world, restored); + assert_eq!(fresh.index, old.index); + assert_eq!(fresh.generation, old.generation); + assert_ne!(fresh, old); + assert_eq!(rpr_free_world(original), RPR_OK); + assert_eq!(rpr_rigid_body_translation(fresh).x, 5.0); + ok(); + assert_eq!(rpr_free_bytes(snapshot), RPR_OK); + assert_eq!(rpr_free_world(restored), RPR_OK); + } +} + +#[test] +fn callback_and_accumulated_event_handles_keep_their_original_worlds() { + unsafe extern "C" fn filter( + data: *mut std::ffi::c_void, + read: *const RprReadContext, + a: RprColliderHandle, + b: RprColliderHandle, + body_a: RprRigidBodyHandle, + body_b: RprRigidBodyHandle, + ) -> i32 { + let world = data.cast::(); + assert_eq!(a.world, world); + assert_eq!(b.world, world); + assert_eq!(body_a.world, world); + assert_eq!(body_b.world, world); + unsafe { + rpr_read_collider_translation(read, a); + } + ok(); + unsafe { + rpr_collider_translation(a); + } + assert_eq!(rpr_last_status(), RPR_WORLD_BUSY); + 1 + } + unsafe { + let events = rpr_new_event_collector(); + ok(); + let worlds = [rpr_new_world(), rpr_new_world()]; + for &world in &worlds { + let mut shape = rpr_ball_collider_desc(1.0); + shape.activeEvents = RPR_COLLISION_EVENTS; + shape.activeHooks = RPR_FILTER_CONTACT_PAIRS; + let fixed = rpr_insert_rigid_body(world, &rpr_fixed_rigid_body_desc()); + ok(); + rpr_insert_collider(fixed, &shape); + ok(); + let dynamic = rpr_insert_rigid_body(world, &rpr_dynamic_rigid_body_desc()); + ok(); + rpr_insert_collider(dynamic, &shape); + ok(); + let hooks = RprPhysicsHooks { + user_data: world.cast(), + filter_contact_pair: Some(filter), + ..Default::default() + }; + assert_eq!(rpr_step(world, &hooks, events), RPR_OK); + } + let mut copied = [RprCollisionEvent::default(); 2]; + assert_eq!( + rpr_event_collector_collision_events(events, copied.as_mut_ptr(), 2), + 2 + ); + ok(); + for (e, &world) in copied.iter().zip(&worlds) { + assert_eq!(e.collider1.world, world); + assert_eq!(e.collider2.world, world); + assert_eq!(rpr_collider_contains(e.collider1), 1); + ok(); + } + assert_eq!(rpr_free_event_collector(events), RPR_OK); + for world in worlds { + assert_eq!(rpr_free_world(world), RPR_OK); + } + } +} + +#[test] +fn invalid_event_alignment_is_reported_before_reading_owner() { + unsafe { + let event = ptr::without_provenance::(1); + assert_eq!( + rpr_soft_body_tear_event_soft_body(event), + RprSoftBodyHandle::default() + ); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + } +} diff --git a/c/src/pipeline.rs b/c/src/pipeline.rs new file mode 100644 index 000000000..29b3ce3a7 --- /dev/null +++ b/c/src/pipeline.rs @@ -0,0 +1,2388 @@ +use crate::*; +/// Return the world setting documented by RprIntegrationParameters::dt. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_time_step(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.dt) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::dt. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_time_step(world: *mut RprWorld, value: RprReal) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + positive(value)?; + get_mut(object)?.0.dt = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::minCcdDt. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_min_ccd_dt(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.min_ccd_dt) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::minCcdDt. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_min_ccd_dt(world: *mut RprWorld, value: RprReal) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + nonnegative(value)?; + get_mut(object)?.0.min_ccd_dt = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::lengthUnit. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_length_unit(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.length_unit) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::lengthUnit. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_length_unit(world: *mut RprWorld, value: RprReal) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + positive(value)?; + get_mut(object)?.0.length_unit = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::warmstartCoefficient. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_warmstart_coefficient(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.warmstart_coefficient) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::warmstartCoefficient. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_warmstart_coefficient( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + nonnegative(value)?; + ensure(value <= 1.0, "warmstart coefficient must be <= 1")?; + get_mut(object)?.0.warmstart_coefficient = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::normalizedAllowedLinearError. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_normalized_allowed_linear_error(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.normalized_allowed_linear_error) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::normalizedAllowedLinearError. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_normalized_allowed_linear_error( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + nonnegative(value)?; + get_mut(object)?.0.normalized_allowed_linear_error = value; + Ok(()) + }) +} + +/// Return the world setting documented by +/// RprIntegrationParameters::normalizedMaxCorrectiveVelocity. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_normalized_max_corrective_velocity(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.normalized_max_corrective_velocity) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::normalizedMaxCorrectiveVelocity. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_normalized_max_corrective_velocity( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + nonnegative(value)?; + get_mut(object)?.0.normalized_max_corrective_velocity = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::normalizedPredictionDistance. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_normalized_prediction_distance(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.normalized_prediction_distance) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::normalizedPredictionDistance. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_normalized_prediction_distance( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + nonnegative(value)?; + get_mut(object)?.0.normalized_prediction_distance = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::normalizedMaxLinearVelocity. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_normalized_max_linear_velocity(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.normalized_max_linear_velocity) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::normalizedMaxLinearVelocity. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_normalized_max_linear_velocity( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + nonnegative(value)?; + get_mut(object)?.0.normalized_max_linear_velocity = value; + Ok(()) + }) +} + +/// Return the world setting documented by +/// RprIntegrationParameters::normalizedContactRecycleDistance. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_normalized_contact_recycle_distance( + world: *const RprWorld, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.normalized_contact_recycle_distance) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::normalizedContactRecycleDistance. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_normalized_contact_recycle_distance( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + nonnegative(value)?; + get_mut(object)?.0.normalized_contact_recycle_distance = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::numSolverIterations. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_num_solver_iterations(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.num_solver_iterations) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::numSolverIterations. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_num_solver_iterations( + world: *mut RprWorld, + value: usize, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + ensure(value > 0, "iteration count must be positive")?; + get_mut(object)?.0.num_solver_iterations = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::numInternalPgsIterations. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_num_internal_pgs_iterations(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.num_internal_pgs_iterations) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::numInternalPgsIterations. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_num_internal_pgs_iterations( + world: *mut RprWorld, + value: usize, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + ensure(value > 0, "iteration count must be positive")?; + get_mut(object)?.0.num_internal_pgs_iterations = value; + Ok(()) + }) +} + +/// Return the world setting documented by +/// RprIntegrationParameters::numInternalStabilizationIterations. +/// @ingroup errors +#[rapier_export] +pub unsafe extern "C" fn rpr_num_internal_stabilization_iterations( + world: *const RprWorld, +) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.num_internal_stabilization_iterations) + }) + }) +} + +/// Set the world setting documented by +/// RprIntegrationParameters::numInternalStabilizationIterations. +/// @ingroup errors +#[rapier_export] +pub unsafe extern "C" fn rpr_set_num_internal_stabilization_iterations( + world: *mut RprWorld, + value: usize, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + get_mut(object)?.0.num_internal_stabilization_iterations = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::maxCcdSubsteps. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_max_ccd_substeps(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.max_ccd_substeps) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::maxCcdSubsteps. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_max_ccd_substeps(world: *mut RprWorld, value: usize) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + get_mut(object)?.0.max_ccd_substeps = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::contactClustering. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_contact_clustering(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.contact_clustering as u32) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::contactClustering. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_contact_clustering( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(object)?.0.contact_clustering = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::contactRecycling. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_contact_recycling(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.contact_recycling as u32) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::contactRecycling. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_contact_recycling( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(object)?.0.contact_recycling = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::frictionInBiasPass. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_friction_in_bias_pass(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.friction_in_bias_pass as u32) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::frictionInBiasPass. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_friction_in_bias_pass( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(object)?.0.friction_in_bias_pass = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::warmstartJoints. +/// @ingroup joints +#[rapier_export] +pub unsafe extern "C" fn rpr_warmstart_joints(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.warmstart_joints as u32) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::warmstartJoints. +/// @ingroup joints +#[rapier_export] +pub unsafe extern "C" fn rpr_set_warmstart_joints( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(object)?.0.warmstart_joints = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::contactSoftness. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_contact_softness(world: *const RprWorld) -> RprSpringCoefficients { + ffi_value(|out: *mut RprSpringCoefficients| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.contact_softness.into()) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::contactSoftness. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_set_contact_softness( + world: *mut RprWorld, + value: RprSpringCoefficients, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = value.raw()?; + get_mut(object)?.0.contact_softness = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprIntegrationParameters::staticContactSoftness. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_static_contact_softness( + world: *const RprWorld, +) -> RprSpringCoefficients { + ffi_value(|out: *mut RprSpringCoefficients| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let object: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + + let object = get(object)?; + output(out, object.0.static_contact_softness.into()) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::staticContactSoftness. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_set_static_contact_softness( + world: *mut RprWorld, + value: RprSpringCoefficients, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let object: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = value.raw()?; + get_mut(object)?.0.static_contact_softness = value; + Ok(()) + }) +} + +use bincode::Options; +use rapier::geometry::{ContactForceEvent, ContactPair, SolverFlags}; +use rapier::pipeline::{ContactModificationContext, PairFilterContext}; +use std::{ffi::c_void, sync::Mutex}; + +/// Collision start/stop flags match Rapier CollisionEventFlags. +/// @ingroup events +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprCollisionEvent { + /// First collider in the pair. + pub collider1: RprColliderHandle, + /// Second collider in the pair. + pub collider2: RprColliderHandle, + /// 1 for a starting event, 0 for a stopping event. + pub started: RprBool, + /// Event flags: bit 0 sensor pair, bit 1 removed collider. + pub flags: u32, +} +/// Contact-force event, enabled by flags and the collider force threshold. +/// @ingroup events +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprContactForceEvent { + /// First collider in the pair. + pub collider1: RprColliderHandle, + /// Second collider in the pair. + pub collider2: RprColliderHandle, + /// Sum of world-space contact forces. + pub total_force: RprVector, + /// Sum of contact force magnitudes. + pub total_force_magnitude: RprReal, + /// World-space direction of the strongest contact force. + pub max_force_direction: RprVector, + /// Magnitude of the strongest contact force. + pub max_force_magnitude: RprReal, + /// 1 for a starting event, 0 for a stopping event. + pub started: RprBool, +} +/// Events accumulate until clear. Copying events never drains them, allowing two-call buffer +/// sizing. +/// @ingroup events +#[derive(Default)] +pub struct RprEventCollector { + collisions: Mutex>, + forces: Mutex>, + tears: Mutex>, +} +struct WorldEvents<'a> { + world: *mut RprWorld, + events: &'a RprEventCollector, +} +// The address is only copied into handles; the step holds the world write guard. +unsafe impl Sync for WorldEvents<'_> {} +impl EventHandler for WorldEvents<'_> { + fn handle_collision_event( + &self, + _: &RigidBodySet, + _: &ColliderSet, + event: CollisionEvent, + _: Option<&ContactPair>, + ) { + let (a, b, started, flags) = match event { + CollisionEvent::Started(a, b, f) => (a, b, 1, f.bits()), + CollisionEvent::Stopped(a, b, f) => (a, b, 0, f.bits()), + }; + self.events + .collisions + .lock() + .unwrap() + .push(RprCollisionEvent { + collider1: RprColliderHandle::from(a).with_world(self.world), + collider2: RprColliderHandle::from(b).with_world(self.world), + started, + flags, + }); + } + fn handle_contact_force_event( + &self, + dt: Real, + _: &RigidBodySet, + _: &ColliderSet, + pair: &ContactPair, + magnitude: Real, + ) { + let e = ContactForceEvent::from_contact_pair(dt, pair, magnitude); + self.events + .forces + .lock() + .unwrap() + .push(RprContactForceEvent { + collider1: RprColliderHandle::from(e.collider1).with_world(self.world), + collider2: RprColliderHandle::from(e.collider2).with_world(self.world), + total_force: e.total_force.into(), + total_force_magnitude: e.total_force_magnitude, + max_force_direction: e.max_force_direction.into(), + max_force_magnitude: e.max_force_magnitude, + started: e.started as u32, + }); + } + fn handle_soft_body_tear_event(&self, _: &SoftBodySet, event: &SoftBodyTearEvent) { + self.events + .tears + .lock() + .unwrap() + .push(RprSoftBodyTearEvent(event.clone(), self.world)); + } +} +/// Pair callback: -1 rejects a contact pair; 0 detects contacts without impulses; 1 computes +/// impulses. +/// For sensor intersections only, zero rejects and any positive value accepts. +/// @ingroup math +pub type RprPairFilter = Option< + unsafe extern "C" fn( + user_data: *mut c_void, + read: *const RprReadContext, + collider1: RprColliderHandle, + collider2: RprColliderHandle, + body1: RprRigidBodyHandle, + body2: RprRigidBodyHandle, + ) -> i32, +>; +/// Mutable per-manifold properties. Set enabled=0 to discard all its solver contacts. +/// @ingroup events +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprContactModification { + /// World-space contact or surface normal. + pub normal: RprVector, + /// Nonnegative friction coefficient. + pub friction: RprReal, + /// Nonnegative restitution coefficient. + pub restitution: RprReal, + /// Application data; Rapier does not own pointers encoded in it. + pub user_data: u32, + /// Whether this setting/object is enabled (0 or 1). + pub enabled: RprBool, +} +/// Modify aggregate contact properties for one pair; contact is mutable only during this callback. +/// @ingroup callbacks +pub type RprModifyContacts = Option< + unsafe extern "C" fn( + user_data: *mut c_void, + read: *const RprReadContext, + collider1: RprColliderHandle, + collider2: RprColliderHandle, + contact: *mut RprContactModification, + ), +>; +/// Borrowed native contact context. Valid only during its callback; never retain or free it. +/// @ingroup events +pub struct RprContactModificationContext { + raw: *mut c_void, +} +/// Modify individual solver contacts through a borrowed context, valid only during the callback. +/// @ingroup callbacks +pub type RprModifyContactContext = Option< + unsafe extern "C" fn( + user_data: *mut c_void, + read: *const RprReadContext, + collider1: RprColliderHandle, + collider2: RprColliderHandle, + context: *mut RprContactModificationContext, + ), +>; + +/// Callbacks must not unwind or retain arguments. Use their ReadContext to inspect bodies and +/// colliders; ordinary access to the stepping world returns WORLD_BUSY. Mutations must be +/// performed after stepping. With parallel builds +/// callbacks and their user_data must be safe for concurrent invocation. NULL callbacks use +/// defaults. +/// @ingroup callbacks +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprPhysicsHooks { + /// Application data; Rapier does not own pointers encoded in it. + pub user_data: *mut c_void, + /// Optional contact-pair filter, called only for colliders enabling the hook. + pub filter_contact_pair: RprPairFilter, + /// Optional sensor-pair filter, called only for colliders enabling the hook. + pub filter_intersection_pair: RprPairFilter, + /// Optional legacy aggregate contact-edit callback. + pub modify_solver_contacts: RprModifyContacts, + /// Runs after the legacy property callback. Context accessors may be called here. + pub modify_solver_contacts_context: RprModifyContactContext, +} +// SAFETY: The public callback contract requires thread-safe callbacks and user_data in parallel builds. +unsafe impl Sync for RprPhysicsHooks {} +struct WorldHooks { + world: *mut RprWorld, + hooks: RprPhysicsHooks, +} +// Callback data follows RprPhysicsHooks' thread-safety contract; the address is metadata. +unsafe impl Sync for WorldHooks {} +impl PhysicsHooks for WorldHooks { + fn filter_contact_pair(&self, c: &PairFilterContext) -> Option { + let Some(f) = self.hooks.filter_contact_pair else { + return Some(SolverFlags::COMPUTE_RIGID_IMPULSES); + }; + let result = unsafe { + f( + self.hooks.user_data, + &RprReadContext::new(self.world, c.bodies, c.colliders), + RprColliderHandle::from(c.collider1).with_world(self.world), + RprColliderHandle::from(c.collider2).with_world(self.world), + c.rigid_body1 + .map(|h| RprRigidBodyHandle::from(h).with_world(self.world)) + .unwrap_or_default(), + c.rigid_body2 + .map(|h| RprRigidBodyHandle::from(h).with_world(self.world)) + .unwrap_or_default(), + ) + }; + match result { + v if v < 0 => None, + 0 => Some(SolverFlags::empty()), + _ => Some(SolverFlags::COMPUTE_RIGID_IMPULSES), + } + } + fn filter_intersection_pair(&self, c: &PairFilterContext) -> bool { + self.hooks.filter_intersection_pair.is_none_or(|f| unsafe { + f( + self.hooks.user_data, + &RprReadContext::new(self.world, c.bodies, c.colliders), + RprColliderHandle::from(c.collider1).with_world(self.world), + RprColliderHandle::from(c.collider2).with_world(self.world), + c.rigid_body1 + .map(|h| RprRigidBodyHandle::from(h).with_world(self.world)) + .unwrap_or_default(), + c.rigid_body2 + .map(|h| RprRigidBodyHandle::from(h).with_world(self.world)) + .unwrap_or_default(), + ) > 0 + }) + } + fn modify_solver_contacts(&self, c: &mut ContactModificationContext) { + if let Some(f) = self.hooks.modify_solver_contacts { + let read = RprReadContext::new(self.world, c.bodies, c.colliders); + let a = RprColliderHandle::from(c.collider1).with_world(self.world); + let b = RprColliderHandle::from(c.collider2).with_world(self.world); + if let Some(m) = c.rigid_mut() { + let mut value = RprContactModification { + normal: (*m.normal).into(), + friction: *m.friction, + restitution: *m.restitution, + user_data: *m.user_data, + enabled: 1, + }; + unsafe { f(self.hooks.user_data, &read, a, b, &mut value) }; + // Invalid callback values leave the original manifold unchanged. + if let (Ok(n), Ok(fr), Ok(re), Ok(enabled)) = ( + value.normal.raw(), + nonnegative(value.friction), + nonnegative(value.restitution), + boolean(value.enabled), + ) { + if n.length_squared().is_finite() && n.length_squared() > 1.0e-20 { + *m.normal = n.normalize(); + *m.friction = fr; + *m.restitution = re; + *m.user_data = value.user_data; + if !enabled { + m.solver_contacts.clear(); + } + } + } + } + } + if let Some(callback) = self.hooks.modify_solver_contacts_context { + let mut context = RprContactModificationContext { + raw: (c as *mut ContactModificationContext<'_>).cast(), + }; + unsafe { + callback( + self.hooks.user_data, + &RprReadContext::new(self.world, c.bodies, c.colliders), + RprColliderHandle::from(c.collider1).with_world(self.world), + RprColliderHandle::from(c.collider2).with_world(self.world), + &mut context, + ) + }; + } + } +} + +/// Applies Rapier's persistent one-way platform logic to the borrowed manifold. +/// @ingroup worlds +#[rapier_export(contact_modification_context)] +pub unsafe extern "C" fn rpr_contact_modification_context_update_as_oneway_platform( + context: *mut RprContactModificationContext, + allowed_local_n1: RprVector, + allowed_angle: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let normal = allowed_local_n1.raw()?; + let angle = nonnegative(allowed_angle)?; + let raw = get_mut(context)? + .raw + .cast::>(); + (*raw).update_as_oneway_platform(normal, angle); + Ok(()) + }) +} + +/// Sets the tangent velocity of every rigid solver contact in this manifold. +/// @ingroup worlds +#[rapier_export(contact_modification_context)] +pub unsafe extern "C" fn rpr_contact_modification_context_set_tangent_velocity( + context: *mut RprContactModificationContext, + velocity: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let velocity = velocity.raw()?; + let raw = get_mut(context)? + .raw + .cast::>(); + if let Some(rigid) = (*raw).rigid_mut() { + for contact in rigid.solver_contacts.iter_mut() { + contact.tangent_velocity = velocity; + } + } + Ok(()) + }) +} + +/// Allocate an empty event collector; release it with rpr_free_event_collector. +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_new_event_collector() -> *mut RprEventCollector { + ffi_value(|out: *mut *mut RprEventCollector| { + ffi(|| unsafe { + out_ptr(out)?; + output(out, Box::into_raw(Box::new(RprEventCollector::default()))) + }) + }) +} +/// Release an owned event collector. NULL is allowed. Do not pass borrowed pointers or free the +/// object twice. +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_free_event_collector(events: *mut RprEventCollector) -> RprStatus { + ffi(|| unsafe { + if !events.is_null() { + get(events)?; + drop(Box::from_raw(events)); + } + Ok(()) + }) +} +/// Discard all collected events. Does not change the world. +/// @ingroup events +#[rapier_export(event_collector)] +pub unsafe extern "C" fn rpr_event_collector_clear(events: *mut RprEventCollector) -> RprStatus { + ffi(|| unsafe { + let e = get_mut(events)?; + e.collisions.get_mut().unwrap().clear(); + e.forces.get_mut().unwrap().clear(); + e.tears.get_mut().unwrap().clear(); + Ok(()) + }) +} +/// Copy the collected collision start/stop events without removing them. +/// @see @ref output_buffers +/// @ingroup events +#[rapier_export(event_collector)] +pub unsafe extern "C" fn rpr_event_collector_collision_events( + events: *const RprEventCollector, + buffer: *mut RprCollisionEvent, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + copy_out( + &get(events)?.collisions.lock().unwrap(), + buffer, + capacity, + count, + ) + }) + }) +} +/// Copy the collected contact-force events without removing them. +/// @see @ref output_buffers +/// @ingroup events +#[rapier_export(event_collector)] +pub unsafe extern "C" fn rpr_event_collector_contact_force_events( + events: *const RprEventCollector, + buffer: *mut RprContactForceEvent, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + copy_out( + &get(events)?.forces.lock().unwrap(), + buffer, + capacity, + count, + ) + }) + }) +} +/// Return the number of queued soft-body tear events. +/// @ingroup events +#[rapier_export(event_collector)] +pub unsafe extern "C" fn rpr_event_collector_tear_event_count( + events: *const RprEventCollector, +) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { output(out, get(events)?.tears.lock().unwrap().len()) }) + }) +} +/// Owned copy of a tear event. Read particle remapping before rebuilding render meshes. +/// @ingroup events +#[derive(Clone)] +pub struct RprSoftBodyTearEvent(pub(crate) SoftBodyTearEvent, pub(crate) *mut RprWorld); +// Owned event data plus a non-owning world address, never dereferenced by the event. +unsafe impl Send for RprSoftBodyTearEvent {} +unsafe impl Sync for RprSoftBodyTearEvent {} +/// Return an owned copy of a queued tear event; release with rpr_free_soft_body_tear_event. Does +/// not remove the queued event. +/// @ingroup events +#[rapier_export(event_collector)] +pub unsafe extern "C" fn rpr_event_collector_tear_event( + events: *const RprEventCollector, + index: usize, +) -> *mut RprSoftBodyTearEvent { + ffi_value(|out: *mut *mut RprSoftBodyTearEvent| { + ffi(|| unsafe { + out_ptr(out)?; + let e = get(events)? + .tears + .lock() + .unwrap() + .get(index) + .ok_or_else(|| invalid("event index out of range"))? + .clone(); + output(out, Box::into_raw(Box::new(e))) + }) + }) +} +/// Return the world-space gravitational acceleration. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_gravity(world: *const RprWorld) -> RprVector { + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let world: *const RprPhysicsWorld = raw; + output(out, get(world)?.0.gravity.into()) + }) + }) +} + +/// Set the world-space gravitational acceleration. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_gravity(world: *mut RprWorld, value: RprVector) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + let value = value.raw()?; + get_mut(world)?.0.gravity = value; + Ok(()) + }) +} + +/// Hooks and events may be NULL. This call invalidates all borrowed set-element pointers. +/// Advance simulation by one timestep. Hooks and events may be NULL. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_step( + world: *mut RprWorld, + hooks: *const RprPhysicsHooks, + events: *const RprEventCollector, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let hooks = if hooks.is_null() { + RprPhysicsHooks::default() + } else { + *get(hooks)? + }; + let hooks = WorldHooks { world, hooks }; + let events = if events.is_null() { + None + } else { + Some(WorldEvents { + world, + events: get(events)?, + }) + }; + let events: &dyn EventHandler = events.as_ref().map_or(&() as &dyn EventHandler, |e| e); + (*access.raw()).0.step_with_events(&hooks, events); + Ok(()) + }) +} + +/// Refresh collision detection without advancing simulation. Hooks and events may be NULL. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_detect_collisions( + world: *mut RprWorld, + hooks: *const RprPhysicsHooks, + events: *const RprEventCollector, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let hooks = if hooks.is_null() { + RprPhysicsHooks::default() + } else { + *get(hooks)? + }; + let hooks = WorldHooks { world, hooks }; + let events = if events.is_null() { + None + } else { + Some(WorldEvents { + world, + events: get(events)?, + }) + }; + let events: &dyn EventHandler = events.as_ref().map_or(&() as &dyn EventHandler, |e| e); + (*access.raw()).0.detect_collisions(&hooks, events); + Ok(()) + }) +} + +/// Immutable owned byte buffer. Release with the matching FreeBytes function. +/// @ingroup worlds +pub struct RprBytes(Vec); +/// Borrow snapshot bytes without copying; valid until rpr_free_bytes. Never free the returned data +/// pointer. +/// @ingroup worlds +#[rapier_export(bytes)] +pub unsafe extern "C" fn rpr_bytes_data(bytes: *const RprBytes) -> RprByteView { + ffi_value(|result: *mut RprByteView| { + let data = unsafe { std::ptr::addr_of_mut!((*result).data) }; + let count = unsafe { std::ptr::addr_of_mut!((*result).count) }; + + ffi(|| unsafe { + out_ptr(data)?; + out_ptr(count)?; + let b = get(bytes)?; + output(data, b.0.as_ptr())?; + output(count, b.0.len()) + }) + }) +} +/// Release an owned snapshot byte buffer. NULL is allowed. Do not pass borrowed pointers or free +/// the object twice. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_free_bytes(bytes: *mut RprBytes) -> RprStatus { + ffi(|| unsafe { + if !bytes.is_null() { + get(bytes)?; + drop(Box::from_raw(bytes)); + } + Ok(()) + }) +} +// Wire format: magic, then little-endian u32 ABI, dimension, and scalar size. +// The distinct magic prevents interpreting the old six-byte header as this format. +const SNAPSHOT_HEADER_LEN: usize = 16; + +fn snapshot_header(abi_version: u32) -> [u8; SNAPSHOT_HEADER_LEN] { + let build = rpr_build_info(); + let mut header = [0; SNAPSHOT_HEADER_LEN]; + header[..4].copy_from_slice(b"RPRS"); + header[4..8].copy_from_slice(&abi_version.to_le_bytes()); + header[8..12].copy_from_slice(&build.dimension.to_le_bytes()); + header[12..16].copy_from_slice(&build.real_size.to_le_bytes()); + header +} + +fn snapshot_options() -> impl Options { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .with_limit(256 * 1024 * 1024) + .reject_trailing_bytes() +} +/// Return owned snapshot bytes; release them with rpr_free_bytes. See @ref snapshots for +/// restoration and handle lifetimes. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_serialize_world(world: *const RprWorld) -> *mut RprBytes { + ffi_value(|out: *mut *mut RprBytes| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let world: *const RprPhysicsWorld = raw; + + out_ptr(out)?; + let mut bytes = snapshot_header(RPR_ABI_VERSION).to_vec(); + bytes.extend( + snapshot_options() + .serialize(&get(world)?.0) + .map_err(|e| invalid(e.to_string()))?, + ); + output(out, Box::into_raw(Box::new(RprBytes(bytes)))) + }) + }) +} + +/// Restore ONLY trusted snapshots produced by the identical Rapier build. Snapshots are not a +/// stable file format. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_deserialize_world(data: *const u8, count: usize) -> *mut RprWorld { + ffi_value(|out: *mut *mut RprWorld| { + ffi(|| unsafe { + out_ptr(out)?; + ensure(count <= 256 * 1024 * 1024, "snapshot exceeds 256 MiB")?; + let bytes = input(data, count)?; + ensure( + bytes.starts_with(&snapshot_header(RPR_ABI_VERSION)), + "incompatible snapshot", + )?; + let world = snapshot_options() + .deserialize(&bytes[SNAPSHOT_HEADER_LEN..]) + .map_err(|e| invalid(e.to_string()))?; + output( + out, + Box::into_raw(Box::new(RprWorld::new(RprPhysicsWorld(world)))), + ) + }) + }) +} + +/// World-space line segment produced by physics debug rendering. +/// @ingroup events +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprDebugLine { + /// World-space start point. + pub a: RprVector, + /// World-space end point. + pub b: RprVector, + /// RGBA color, four floats. + pub color: [f32; 4], +} +struct Lines(Vec); +impl rapier::pipeline::DebugRenderBackend for Lines { + fn draw_line( + &mut self, + _: rapier::pipeline::DebugRenderObject, + a: Vector, + b: Vector, + color: [f32; 4], + ) { + self.0.push(RprDebugLine { + a: a.into(), + b: b.into(), + color, + }); + } +} +/// Color is HSLA (hue in degrees), matching Rapier DebugColor. mode uses DebugRenderMode bits. +/// @see @ref output_buffers +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_debug_render( + world: *const RprWorld, + mode: u32, + buffer: *mut RprDebugLine, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let world: *const RprPhysicsWorld = raw; + + let mode = rapier::pipeline::DebugRenderMode::from_bits(mode) + .ok_or_else(|| invalid("unknown debug render flags"))?; + let mut pipeline = rapier::pipeline::DebugRenderPipeline::new(Default::default(), mode); + let mut lines = Lines(Vec::new()); + get(world)?.0.debug_render(&mut pipeline, &mut lines); + copy_out(&lines.0, buffer, capacity, count) + }) + }) +} + +/// Set the world setting documented by RprSoftBodiesSettings::resweepStrain. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_soft_bodies_set_resweep_strain( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + nonnegative(value)?; + get_mut(parameters)?.0.soft_bodies.resweep_strain = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprSoftBodiesSettings::resweepStrain. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_soft_bodies_resweep_strain(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + output(out, get(parameters)?.0.soft_bodies.resweep_strain) + }) + }) +} + +/// Set the world setting documented by RprSoftBodiesSettings::contactStiffening. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_soft_bodies_set_contact_stiffening( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + nonnegative(value)?; + get_mut(parameters)?.0.soft_bodies.contact_stiffening = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprSoftBodiesSettings::contactStiffening. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_soft_bodies_contact_stiffening(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + output(out, get(parameters)?.0.soft_bodies.contact_stiffening) + }) + }) +} + +/// Set the world setting documented by RprSoftBodiesSettings::maxExtraSubsteps. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_soft_bodies_set_max_extra_substeps( + world: *mut RprWorld, + value: usize, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + get_mut(parameters)?.0.soft_bodies.max_extra_substeps = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprSoftBodiesSettings::maxExtraSubsteps. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_soft_bodies_max_extra_substeps(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + output(out, get(parameters)?.0.soft_bodies.max_extra_substeps) + }) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::authoredVelocityMargin. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_authored_velocity_margin( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .authored_velocity_margin = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::edgeSpeculation. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_edge_speculation( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)?.0.soft_bodies.recovery.edge_speculation = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::invertedCellDetection. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_inverted_cell_detection( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .inverted_cell_detection = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::selfCrossingDetection. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_self_crossing_detection( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .self_crossing_detection = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::detectionMotionGating. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_detection_motion_gating( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .detection_motion_gating = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::crossBodyDetection. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_cross_body_detection( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .cross_body_detection = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::selfStandDown. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_self_stand_down( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)?.0.soft_bodies.recovery.self_stand_down = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::crossBodyExpelGate. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_cross_body_expel_gate( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .cross_body_expel_gate = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::edgeStandDown. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_edge_stand_down( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)?.0.soft_bodies.recovery.edge_stand_down = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::crossingRepulsion. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_crossing_repulsion( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .crossing_repulsion = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::crossingRepulsionGuide. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_crossing_repulsion_guide( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .crossing_repulsion_guide = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::crossingRepulsionSelfGuide. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_crossing_repulsion_self_guide( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .crossing_repulsion_self_guide = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::recoveryPace. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_recovery_pace( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = nonnegative(value)?; + get_mut(parameters)?.0.soft_bodies.recovery.recovery_pace = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapConstraints. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_constraints( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_constraints = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapRigid. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_rigid( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)?.0.soft_bodies.recovery.overlap_rigid = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapSkipSelfTangled. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_skip_self_tangled( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_skip_self_tangled = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapEdgeStandDown. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_edge_stand_down( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_edge_stand_down = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapConstraintPace. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_constraint_pace( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = nonnegative(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_constraint_pace = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapSkinVolume. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_skin_volume( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_skin_volume = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapKeptDepth. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_kept_depth( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = nonnegative(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_kept_depth = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapSelfRegions. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_self_regions( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_self_regions = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapNormalPush. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_normal_push( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_normal_push = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapMultiVolume. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_multi_volume( + world: *mut RprWorld, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = boolean(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_multi_volume = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapProgressMargin. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_progress_margin( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = nonnegative(value)?; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_progress_margin = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftFemParameters::linearTolerance. +/// @ingroup soft_bodies +#[cfg(feature = "fem")] +#[rapier_export] +pub unsafe extern "C" fn rpr_fem_set_linear_tolerance( + world: *mut RprWorld, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + positive(value)?; + get_mut(parameters)?.0.soft_bodies.fem.linear_tolerance = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftFemParameters::maxLinearIterations. +/// @ingroup soft_bodies +#[cfg(feature = "fem")] +#[rapier_export] +pub unsafe extern "C" fn rpr_fem_set_max_linear_iterations( + world: *mut RprWorld, + value: usize, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + ensure(value > 0, "iterations must be positive")?; + get_mut(parameters)?.0.soft_bodies.fem.max_linear_iterations = value; + Ok(()) + }) +} + +/// Set the world setting documented by RprSoftFemParameters::maxDenseDofs. +/// @ingroup soft_bodies +#[cfg(feature = "fem")] +#[rapier_export] +pub unsafe extern "C" fn rpr_fem_set_max_dense_dofs( + world: *mut RprWorld, + value: usize, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + get_mut(parameters)?.0.soft_bodies.fem.max_dense_dofs = value; + Ok(()) + }) +} + +/// Configures a dedicated pool for this world's parallel work. Zero selects Rayon's default. +/// Takes effect on the next step. Reconfiguration must not race with a step or callback. +/// Returns RPR_UNSUPPORTED in builds without the parallel feature; keeps the previous +/// pool when constructing the new one fails. The pool is not included in snapshots. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_num_threads( + world: *mut RprWorld, + num_threads: usize, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + let world = get_mut(world)?; + #[cfg(feature = "parallel")] + { + world + .0 + .configure_thread_pool(num_threads) + .map_err(|e| invalid(e.to_string())) + } + #[cfg(not(feature = "parallel"))] + { + let _ = (world, num_threads); + Err(( + RPR_UNSUPPORTED, + "thread pools require a Rapier library built with the parallel feature".into(), + )) + } + }) +} + +/// Removes the world's dedicated pool. A parallel build then uses the calling +/// context's Rayon pool (normally the global pool), not a single worker. +/// Returns RPR_UNSUPPORTED in a build without the parallel feature. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_clear_thread_pool(world: *mut RprWorld) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + let world = get_mut(world)?; + #[cfg(feature = "parallel")] + { + world.0.clear_thread_pool(); + Ok(()) + } + #[cfg(not(feature = "parallel"))] + { + let _ = world; + Err(( + RPR_UNSUPPORTED, + "thread pools require a Rapier library built with the parallel feature".into(), + )) + } + }) +} + +/// Size of the world's dedicated pool, or zero if a parallel build has no dedicated +/// pool configured. Returns one for a build without the parallel feature. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_num_threads(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let world: *const RprPhysicsWorld = raw; + + let world = get(world)?; + #[cfg(feature = "parallel")] + let count = world.0.num_threads().unwrap_or(0); + #[cfg(not(feature = "parallel"))] + let count = { + let _ = world; + 1 + }; + output(out, count) + }) + }) +} + +/// Enable or disable the native pipeline profiling counters. Enabling returns +/// RPR_UNSUPPORTED if the library was built without the profiler feature. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_set_counters_enabled( + world: *mut RprWorld, + enabled: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let pipeline: *mut RprPhysicsPipeline = + std::ptr::addr_of_mut!((*raw).0.physics_pipeline).cast(); + + let enabled = boolean(enabled)?; + let counters = &mut get_mut(pipeline)?.0.counters; + if enabled && !cfg!(feature = "profiler") { + return Err(( + RPR_UNSUPPORTED, + "profiling requires the profiler feature".into(), + )); + } + if enabled { + counters.enable(); + } else { + counters.disable(); + } + Ok(()) + }) +} + +/// Native engine time of the most recent step, in milliseconds, as in the Rust testbed. +/// Enable counters before stepping. Excludes C callbacks outside the step, rendering, +/// and dispatch into a dedicated thread pool; remains unchanged while paused. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_step_time_ms(world: *const RprWorld) -> f64 { + ffi_value(|out: *mut f64| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let pipeline: *const RprPhysicsPipeline = + std::ptr::addr_of!((*raw).0.physics_pipeline).cast(); + output(out, get(pipeline)?.0.counters.step_time_ms()) + }) + }) +} + +/// Read ONLY trusted legacy rigid-world snapshots from debug_deserialize3.rs, +/// produced by the identical Rapier build. This is not a stable interchange format. +/// +/// Import trusted legacy Rust testbed rigid-state bytes into a new owned world. Release with +/// rpr_free_world; see @ref snapshots. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_deserialize_rigid_state( + data: *const u8, + count: usize, +) -> *mut RprWorld { + ffi_value(|out: *mut *mut RprWorld| { + #[derive(serde::Deserialize)] + struct PhysicsState { + gravity: Vector, + integration_parameters: IntegrationParameters, + islands: IslandManager, + broad_phase: BroadPhaseBvh, + narrow_phase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + impulse_joints: ImpulseJointSet, + multibody_joints: MultibodyJointSet, + } + ffi(|| unsafe { + out_ptr(out)?; + ensure(count <= 256 * 1024 * 1024, "snapshot exceeds 256 MiB")?; + let bytes = input(data, count)?; + let state: PhysicsState = bincode::DefaultOptions::new() + .with_fixint_encoding() + .allow_trailing_bytes() + .with_limit(256 * 1024 * 1024) + .deserialize(bytes) + .map_err(|e| invalid(e.to_string()))?; + let mut world = PhysicsWorld::new(); + world.gravity = state.gravity; + world.integration_parameters = state.integration_parameters; + world.islands = state.islands; + world.broad_phase = state.broad_phase; + world.narrow_phase = state.narrow_phase; + world.bodies = state.bodies; + world.colliders = state.colliders; + world.impulse_joints = state.impulse_joints; + world.multibody_joints = state.multibody_joints; + output( + out, + Box::into_raw(Box::new(RprWorld::new(RprPhysicsWorld(world)))), + ) + }) + }) +} + +#[cfg(test)] +mod snapshot_header_tests { + use super::*; + + #[test] + fn snapshot_header_preserves_full_abi_version() { + for version in [1, 255, 256, 257, 0x0102_0304, u32::MAX] { + let header = snapshot_header(version); + assert_eq!(&header[..4], b"RPRS"); + assert_eq!( + u32::from_le_bytes(header[4..8].try_into().unwrap()), + version + ); + assert_eq!( + u32::from_le_bytes(header[8..12].try_into().unwrap()), + rpr_build_info().dimension + ); + assert_eq!( + u32::from_le_bytes(header[12..16].try_into().unwrap()), + rpr_build_info().real_size + ); + } + assert_eq!(&snapshot_header(0x0102_0304)[4..8], &[4, 3, 2, 1]); + assert_ne!(snapshot_header(1), snapshot_header(257)); + } + + #[test] + fn snapshots_reject_incompatible_and_truncated_headers() { + unsafe { + let world = rpr_new_world(); + let snapshot = rpr_serialize_world(world); + assert_eq!(rpr_last_status(), RPR_OK); + let data = &(*snapshot).0; + let header = snapshot_header(RPR_ABI_VERSION); + assert!(data.starts_with(&header)); + + let restored = rpr_deserialize_world(data.as_ptr(), data.len()); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_free_world(restored), RPR_OK); + + // Every header byte is checked, including the high ABI bytes that + // previously disappeared in the u8 cast (e.g. ABI 1 versus 257). + for index in 0..SNAPSHOT_HEADER_LEN { + let mut invalid = data.clone(); + invalid[index] ^= 1; + assert!(rpr_deserialize_world(invalid.as_ptr(), invalid.len()).is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + } + for len in 0..=SNAPSHOT_HEADER_LEN { + assert!(rpr_deserialize_world(data.as_ptr(), len).is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + } + + let mut legacy = vec![b'R', b'P', b'R', 1, header[8], header[12]]; + legacy.extend_from_slice(&data[SNAPSHOT_HEADER_LEN..]); + assert!(rpr_deserialize_world(legacy.as_ptr(), legacy.len()).is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_free_bytes(snapshot), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } +} diff --git a/c/src/pod_tests.rs b/c/src/pod_tests.rs new file mode 100644 index 000000000..5250f35fa --- /dev/null +++ b/c/src/pod_tests.rs @@ -0,0 +1,390 @@ +use crate::*; + +fn encoded(value: &T) -> Vec { + bincode::serialize(value).unwrap() +} + +#[test] +fn pod_defaults_match_native_configuration() { + unsafe { + for kind in [ + RPR_DYNAMIC, + RPR_FIXED, + RPR_KINEMATIC_POSITION_BASED, + RPR_KINEMATIC_VELOCITY_BASED, + ] { + let desc = match kind { + RPR_DYNAMIC => rpr_dynamic_rigid_body_desc(), + RPR_FIXED => rpr_fixed_rigid_body_desc(), + RPR_KINEMATIC_POSITION_BASED => rpr_kinematic_position_based_rigid_body_desc(), + _ => rpr_kinematic_velocity_based_rigid_body_desc(), + }; + let actual = desc.raw().unwrap().build(); + let expected = RigidBodyBuilder::new(body_type(kind).unwrap()).build(); + assert_eq!(encoded(&actual), encoded(&expected)); + } + let desc = RprColliderDesc::default(); + assert_eq!( + encoded(&desc.raw().unwrap().build()), + encoded(&ColliderBuilder::ball(0.5).build()) + ); + let native = IntegrationParameters::default(); + let desc = RprIntegrationParameters::from(native); + assert_eq!(encoded(&desc.raw().unwrap()), encoded(&native)); + let native = SoftBodyMaterial::default(); + let desc = RprSoftBodyMaterial::from(native); + assert_eq!(encoded(&desc.raw().unwrap()), encoded(&native)); + let native = GenericJoint::new(JointAxesMask::empty()); + assert_eq!( + encoded(&RprJointDesc::from(native).raw().unwrap()), + encoded(&native) + ); + } +} + +fn check_soft_recipe(desc: RprSoftBodyDesc, native: SoftBodyBuilder) { + let actual = unsafe { desc.raw().unwrap() }; + // Includes topology, generated radii, material and collider template defaults. + assert_eq!(format!("{actual:?}"), format!("{native:?}")); +} + +#[test] +fn pod_soft_recipes_preserve_generator_defaults() { + let positions: [RprVector; 2] = [Vector::ZERO.into(), Vector::X.into()]; + check_soft_recipe( + RprSoftBodyDesc { + positions: RprVectorView { + data: (positions.as_ptr()).cast(), + count: 2, + }, + ..Default::default() + }, + SoftBodyBuilder::new(vec![Vector::ZERO, Vector::X]), + ); + check_soft_recipe( + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_ROPE, + a: Vector::ZERO.into(), + b: Vector::X.into(), + nx: 5, + ..Default::default() + }, + SoftBodyBuilder::rope(Vector::ZERO, Vector::X, 5), + ); + #[cfg(feature = "dim2")] + check_soft_recipe( + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_GRID, + ..Default::default() + }, + SoftBodyBuilder::grid(Vector::ZERO, Vector::ONE, 2, 2), + ); + #[cfg(feature = "dim3")] + { + check_soft_recipe( + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_CLOTH, + ..Default::default() + }, + SoftBodyBuilder::cloth(Vector::ZERO, Vector::X, Vector::Y, 2, 2), + ); + check_soft_recipe( + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_CUBOID, + ..Default::default() + }, + SoftBodyBuilder::cuboid(Vector::ZERO, Vector::ONE, 2, 2, 2), + ); + } + let points: Vec = vec![Vector::ZERO.into(), Vector::X.into(), Vector::Y.into()]; + #[cfg(feature = "dim3")] + let indices = [0, 1, 2]; + #[cfg(feature = "dim2")] + let indices = [0, 1, 1, 2, 2, 0]; + let desc = RprSoftBodyDesc { + kind: RPR_SOFT_DESC_SURFACE, + positions: RprVectorView { + data: (points.as_ptr()).cast(), + count: 3, + }, + surface: RprSurfaceElementView { + data: (indices.as_ptr()).cast(), + count: indices.len() / rapier::math::DIM, + }, + ..Default::default() + }; + #[cfg(feature = "dim3")] + let native = + SoftBodyBuilder::trimesh(vec![Vector::ZERO, Vector::X, Vector::Y], vec![[0, 1, 2]]) + .unwrap(); + #[cfg(feature = "dim2")] + let native = SoftBodyBuilder::polyline( + vec![Vector::ZERO, Vector::X, Vector::Y], + Some(vec![[0, 1], [1, 2], [2, 0]]), + ) + .unwrap(); + check_soft_recipe(desc, native); +} + +#[test] +fn pod_insertion_copies_inputs_and_rejects_invalid_data_atomically() { + unsafe { + let mut world = RprWorld::new(RprPhysicsWorld(PhysicsWorld::default())); + let body = rpr_dynamic_rigid_body_desc(); + let mut collider = RprColliderDesc::default(); + collider.shape.radius = -1.0; + + let body_handle = rpr_insert_rigid_body(&mut world, &body); + assert_eq!(rpr_last_status(), RPR_OK); + let failed = rpr_insert_collider(body_handle, &collider); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + // Separate calls leave the successfully inserted body intact on collider failure. + assert_eq!((*world.read().unwrap().raw()).0.bodies.len(), 1); + assert_eq!((*world.read().unwrap().raw()).0.colliders.len(), 0); + assert_eq!(failed, RprColliderHandle::default()); + assert_eq!(rpr_rigid_body_validate_handle(body_handle), RPR_OK); + let mut points: [RprVector; 2] = [Vector::ZERO.into(), Vector::X.into()]; + collider.shape.kind = RPR_SHAPE_DESC_POLYLINE; + collider.shape.vertices = RprVectorView { + data: points.as_ptr(), + count: 2, + }; + let ch = rpr_insert_collider_without_parent(&mut world, &collider); + assert_eq!(rpr_last_status(), RPR_OK); + points[1] = (Vector::X * 100.0).into(); + assert_eq!( + (&(*world.read().unwrap().raw()).0.colliders)[ch.raw()] + .shape() + .as_polyline() + .unwrap() + .vertices()[1], + Vector::X + ); + let desc = RprSoftBodyDesc { + positions: RprVectorView { + data: (points.as_ptr()).cast(), + count: 2, + }, + ..Default::default() + }; + let copied = desc.raw().unwrap(); + points[1] = Vector::ZERO.into(); + assert_eq!(copied.positions[1], Vector::X * 100.0); + assert_eq!(points[1].x, 0.0); + let mut data = RprIntegrationParameters::from(IntegrationParameters::default()); + let mut params = NativeIntegrationParameters(IntegrationParameters::default()); + let before = encoded(¶ms.0); + data.dt = Real::NAN; + assert_eq!( + native_integration_parameters_set_data(&mut params, &data), + RPR_INVALID_ARGUMENT + ); + assert_eq!(encoded(¶ms.0), before); + } +} + +#[test] +fn pod_shape_recipes_match_native_shapes() { + unsafe { + let mut desc = RprShapeDesc::default(); + let check = |desc: &RprShapeDesc, expected: SharedShape| { + assert_eq!(encoded(&desc.raw().unwrap()), encoded(&expected)); + }; + check(&desc, SharedShape::ball(0.5)); + desc.kind = RPR_SHAPE_DESC_CUBOID; + desc.a = Vector::ONE.into(); + #[cfg(feature = "dim2")] + check(&desc, SharedShape::cuboid(1.0, 1.0)); + #[cfg(feature = "dim3")] + check(&desc, SharedShape::cuboid(1.0, 1.0, 1.0)); + desc.kind = RPR_SHAPE_DESC_ROUND_CUBOID; + #[cfg(feature = "dim2")] + check(&desc, SharedShape::round_cuboid(1.0, 1.0, 0.5)); + #[cfg(feature = "dim3")] + check(&desc, SharedShape::round_cuboid(1.0, 1.0, 1.0, 0.5)); + desc.a = Vector::ZERO.into(); + desc.b = Vector::X.into(); + desc.c = Vector::Y.into(); + desc.kind = RPR_SHAPE_DESC_CAPSULE; + check(&desc, SharedShape::capsule(Vector::ZERO, Vector::X, 0.5)); + desc.kind = RPR_SHAPE_DESC_SEGMENT; + check(&desc, SharedShape::segment(Vector::ZERO, Vector::X)); + desc.kind = RPR_SHAPE_DESC_TRIANGLE; + check( + &desc, + SharedShape::triangle(Vector::ZERO, Vector::X, Vector::Y), + ); + desc.kind = RPR_SHAPE_DESC_HALFSPACE; + desc.a = Vector::Y.into(); + check(&desc, SharedShape::halfspace(Vector::Y)); + #[cfg(feature = "dim3")] + { + desc.kind = RPR_SHAPE_DESC_CYLINDER; + check(&desc, SharedShape::cylinder(0.5, 0.5)); + desc.kind = RPR_SHAPE_DESC_CONE; + check(&desc, SharedShape::cone(0.5, 0.5)); + } + let child = RprCompoundShapeDesc { + pose: Pose::IDENTITY.into(), + shape: RprShapeDesc::default(), + }; + desc.kind = RPR_SHAPE_DESC_COMPOUND; + desc.children = RprCompoundShapeView { + data: &child, + count: 1, + }; + check( + &desc, + SharedShape::compound(vec![(Pose::IDENTITY, SharedShape::ball(0.5))]), + ); + // Cycles and unsupported tags are rejected before native construction. + let mut cycle = child; + cycle.shape.kind = RPR_SHAPE_DESC_COMPOUND; + cycle.shape.children = RprCompoundShapeView { + data: std::ptr::addr_of!(cycle), + count: 1, + }; + assert!(cycle.shape.raw().is_err()); + desc.kind = u32::MAX; + assert!(desc.raw().is_err()); + desc.kind = RPR_SHAPE_DESC_HEIGHTFIELD; + let heights = [0.0, 1.0, 0.5, 0.0]; + desc.heights = RprRealView { + data: heights.as_ptr(), + count: if cfg!(feature = "dim2") { 2 } else { 4 }, + }; + desc.rows = 2; + #[cfg(feature = "dim2")] + { + desc.columns = 1; + check( + &desc, + SharedShape::heightfield(heights[..2].to_vec(), Vector::ONE), + ); + } + #[cfg(feature = "dim3")] + { + desc.columns = 2; + check( + &desc, + SharedShape::heightfield( + rapier::parry::utils::Array2::new(2, 2, heights.to_vec()), + Vector::ONE, + ), + ); + } + } +} + +#[test] +fn procedural_description_constructors_match_rust() { + #[cfg(feature = "dim2")] + { + let desc = rpr_disk_soft_body_desc(Vector::Y.into(), 1.0, 12); + check_soft_recipe(desc, SoftBodyBuilder::disk(Vector::Y, 1.0, 12)); + } + #[cfg(feature = "dim3")] + { + let desc = rpr_sphere_soft_body_desc(Vector::Y.into(), 1.0, 1); + check_soft_recipe(desc, SoftBodyBuilder::sphere(Vector::Y, 1.0, 1)); + let desc = + rpr_cloth_tube_soft_body_desc(Vector::ZERO.into(), Vector::Y.into(), 1.0, 0.5, 8, 3); + check_soft_recipe( + desc, + SoftBodyBuilder::cloth_tube(Vector::ZERO, Vector::Y, 1.0, 0.5, 8, 3), + ); + } + let mut desc = rpr_rope_soft_body_desc(Vector::ZERO.into(), Vector::X.into(), 3); + desc.totalMass = RprOptionalReal { + enabled: 1, + value: 6.0, + }; + desc.translation = Vector::Y.into(); + check_soft_recipe( + desc, + SoftBodyBuilder::rope(Vector::ZERO, Vector::X, 3) + .mass(6.0) + .translated(Vector::Y), + ); +} +#[test] +fn joint_descriptions_allow_one_sided_limits() { + let mut desc = rpr_default_joint_desc(); + desc.limitAxes = 1; + desc.limits[0].min = -Real::INFINITY; + desc.limits[0].max = 2.0; + assert!(desc.raw().is_ok()); + desc.limits[0].min = Real::NAN; + assert!(desc.raw().is_err()); +} + +#[test] +fn value_constructors_match_native_values_and_defer_validation() { + unsafe { + let collider = rpr_cuboid_collider_desc(Vector::splat(2.0).into()); + #[cfg(feature = "dim2")] + let native = ColliderBuilder::cuboid(2.0, 2.0); + #[cfg(feature = "dim3")] + let native = ColliderBuilder::cuboid(2.0, 2.0, 2.0); + assert_eq!( + encoded(&collider.raw().unwrap().build()), + encoded(&native.build()) + ); + assert_eq!( + encoded(&rpr_fixed_joint_desc().raw().unwrap()), + encoded(&GenericJoint::from(FixedJointBuilder::new().build())) + ); + let axis = (Vector::X + Vector::Y).normalize(); + let mut actual = rpr_prismatic_joint_desc((axis * 3.0).into()).raw().unwrap(); + let native = GenericJoint::from(PrismaticJointBuilder::new(axis).build()); + // 2D frames round-trip through an angle; permit trigonometric rounding. + assert!((actual.local_axis1() - native.local_axis1()).length() < Real::EPSILON * 8.0); + assert!((actual.local_axis2() - native.local_axis2()).length() < Real::EPSILON * 8.0); + actual.local_frame1.rotation = native.local_frame1.rotation; + actual.local_frame2.rotation = native.local_frame2.rotation; + assert_eq!(encoded(&actual), encoded(&native)); + #[cfg(feature = "dim2")] + assert_eq!( + encoded(&rpr_revolute_joint_desc().raw().unwrap()), + encoded(&GenericJoint::from(RevoluteJointBuilder::new().build())) + ); + #[cfg(feature = "dim3")] + assert_eq!( + encoded(&rpr_revolute_joint_desc(Vector::X.into()).raw().unwrap()), + encoded(&GenericJoint::from( + RevoluteJointBuilder::new(Vector::X).build() + )) + ); + let softness = RprSpringCoefficients { + natural_frequency: 12.0, + damping_ratio: 0.3, + }; + assert_eq!( + encoded(&rpr_uniform_soft_body_material(softness).raw().unwrap()), + encoded(&SoftBodyMaterial::uniform(softness.raw().unwrap())) + ); + + // Invalid values can be edited before insertion; constructors do not report errors. + let mut invalid = rpr_ball_collider_desc(-1.0); + assert_eq!(invalid.shape.radius, -1.0); + assert!(invalid.raw().is_err()); + invalid.shape.radius = 1.0; + assert!(invalid.raw().is_ok()); + assert!( + rpr_cuboid_collider_desc(Vector::splat(-1.0).into()) + .raw() + .is_err() + ); + assert!(rpr_prismatic_joint_desc(Vector::ZERO.into()).raw().is_err()); + assert!( + rpr_prismatic_joint_desc(Vector::splat(Real::NAN).into()) + .raw() + .is_err() + ); + assert!( + rpr_rope_soft_body_desc(Vector::ZERO.into(), Vector::X.into(), 0) + .raw() + .is_err() + ); + } +} diff --git a/c/src/queries.rs b/c/src/queries.rs new file mode 100644 index 000000000..374a496ba --- /dev/null +++ b/c/src/queries.rs @@ -0,0 +1,170 @@ +use crate::*; +use rapier::parry::query::ShapeCastOptions; +/// Scene-query flags, groups, and excluded handles. Initialize with rpr_default_query_filter. +/// @ingroup queries +#[repr(C)] +#[derive(Copy, Clone)] +pub struct RprQueryFilter { + /// RPR_QUERY_EXCLUDE_* bitmask selecting body types and sensors/solids. + pub flags: u32, + /// Whether to apply the groups filter. + pub use_groups: RprBool, + /// Groups to test when use_groups is 1. + pub groups: RprInteractionGroups, + /// Collider to exclude; use the explicit invalid handle to exclude none. + pub exclude_collider: RprColliderHandle, + /// Body whose colliders are excluded; use the explicit invalid handle for none. + pub exclude_rigid_body: RprRigidBodyHandle, +} +impl Default for RprQueryFilter { + fn default() -> Self { + Self { + flags: 0, + use_groups: 0, + groups: RprInteractionGroups { + memberships: u32::MAX, + filter: u32::MAX, + test_mode: 0, + }, + exclude_collider: Default::default(), + exclude_rigid_body: Default::default(), + } + } +} +impl RprQueryFilter { + pub(crate) fn raw(self) -> Result> { + Ok(QueryFilter { + flags: QueryFilterFlags::from_bits(self.flags) + .ok_or_else(|| invalid("unknown query flags"))?, + groups: if boolean(self.use_groups)? { + Some(self.groups.raw()?) + } else { + None + }, + exclude_collider: (self.exclude_collider != RprColliderHandle::default()) + .then(|| self.exclude_collider.raw()), + exclude_rigid_body: (self.exclude_rigid_body != RprRigidBodyHandle::default()) + .then(|| self.exclude_rigid_body.raw()), + predicate: None, + }) + } +} +/// Return native default query filter. This POD value owns no resources. +/// @ingroup queries +#[rapier_export] +pub extern "C" fn rpr_default_query_filter() -> RprQueryFilter { + RprQueryFilter::default() +} + +/// Closest ray intersection, with a world-space normal. +/// @ingroup queries +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprRayHit { + /// World-bound collider handle. + pub collider: RprColliderHandle, + /// Ray/sweep parameter at first impact, bounded by the query options. + pub time_of_impact: RprReal, + /// World-space contact or surface normal. + pub normal: RprVector, + /// Shape feature kind: RPR_FEATURE_UNKNOWN, RPR_FEATURE_VERTEX, RPR_FEATURE_EDGE, or + /// RPR_FEATURE_FACE. + pub feature_type: u32, + /// Index within the feature kind; zero for unknown. + pub feature_id: u32, +} +pub(crate) fn feature(f: rapier::parry::shape::FeatureId) -> (u32, u32) { + use rapier::parry::shape::FeatureId; + match f { + FeatureId::Vertex(i) => (1, i), + #[cfg(feature = "dim3")] + FeatureId::Edge(i) => (2, i), + FeatureId::Face(i) => (3, i), + FeatureId::Unknown => (0, 0), + } +} + +/// Closest projected world-space point and its collider. +/// @ingroup queries +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprPointProjection { + /// World-bound collider handle. + pub collider: RprColliderHandle, + /// Projected world-space point. + pub point: RprVector, + /// Whether the original point was inside the collider. + pub is_inside: RprBool, +} + +/// Sweep termination settings. Initialize with rpr_default_shape_cast_options. +/// @ingroup queries +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprShapeCastOptions { + /// Maximum sweep parameter; movement is velocity multiplied by this time. + pub max_time_of_impact: RprReal, + /// Nonnegative separation at which a shape cast counts as a hit. + pub target_distance: RprReal, + /// Whether to stop at t = 0 for an initial overlap. + pub stop_at_penetration: RprBool, + /// Whether to compute witness points/normals for an initial overlap. + pub compute_impact_geometry_on_penetration: RprBool, +} +impl RprShapeCastOptions { + pub(crate) fn raw(self) -> Result { + Ok(ShapeCastOptions { + max_time_of_impact: nonnegative(self.max_time_of_impact)?, + target_distance: nonnegative(self.target_distance)?, + stop_at_penetration: boolean(self.stop_at_penetration)?, + compute_impact_geometry_on_penetration: boolean( + self.compute_impact_geometry_on_penetration, + )?, + }) + } +} +/// Shape-cast impact geometry; collider-side data is world-space, moving-shape data is local. +/// @ingroup queries +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprShapeCastHit { + /// World-bound collider handle. + pub collider: RprColliderHandle, + /// Ray/sweep parameter at first impact, bounded by the query options. + pub time_of_impact: RprReal, + /// Impact witness on the collider, in world coordinates. + pub witness1: RprVector, + /// Impact witness on the moving shape, in its local coordinates. + pub witness2: RprVector, + /// Impact normal on the collider, in world coordinates. + pub normal1: RprVector, + /// Impact normal on the moving shape, in its local coordinates. + pub normal2: RprVector, + /// Native result: 0 out of iterations, 1 converged, 2 failed, 3 penetrating or within target + /// distance. + pub status: u32, +} + +/// Return native default shape cast options. This POD value owns no resources. +/// @ingroup queries +#[rapier_export] +pub extern "C" fn rpr_default_shape_cast_options() -> RprShapeCastOptions { + let o = ShapeCastOptions::default(); + RprShapeCastOptions { + max_time_of_impact: o.max_time_of_impact, + target_distance: o.target_distance, + stop_at_penetration: o.stop_at_penetration as _, + compute_impact_geometry_on_penetration: o.compute_impact_geometry_on_penetration as _, + } +} + +/// Called with scoped read access and a collider handle. Shared queries may nest; +/// world mutations are rejected until the outer query returns. Never retain the context. +/// @ingroup queries +pub type RprQueryPredicate = Option< + unsafe extern "C" fn( + user_data: *mut std::ffi::c_void, + read: *const RprReadContext, + handle: RprColliderHandle, + ) -> RprBool, +>; diff --git a/c/src/read_access.rs b/c/src/read_access.rs new file mode 100644 index 000000000..bc34257f5 --- /dev/null +++ b/c/src/read_access.rs @@ -0,0 +1,1286 @@ +//! Scoped read access derived from the borrows Rapier supplies to callbacks. +use crate::*; +/// Compute a velocity correction from callback-visible body state, updating the PID controller +/// history. The context is valid only during its callback. +/// @ingroup callbacks +#[rapier_export(read_pid_controller)] +pub unsafe extern "C" fn rpr_read_pid_controller_rigid_body_correction( + context: *const RprReadContext, + controller: *mut RprPidController, + dt: RprReal, + body: RprRigidBodyHandle, + target_pose: RprPose, + target_linvel: RprVector, + target_angvel: RprAngVector, +) -> RprVelocityCorrection { + ffi_value(|result: *mut RprVelocityCorrection| { + let linear = unsafe { std::ptr::addr_of_mut!((*result).linear) }; + let angular_velocity = unsafe { std::ptr::addr_of_mut!((*result).angularVelocity) }; + + ffi(|| unsafe { + body.check_world(read_context_world(context))?; + crate::handle_access::forward(native_pid_controller_rigid_body_correction( + controller, + dt, + get(context)?.bodies, + body, + target_pose, + target_linvel, + target_angvel, + linear, + angular_velocity, + )) + }) + }) +} +/// Return the number of rigid body objects in the world. Uses only the callback-scoped read +/// context; never retain the context. +/// @ingroup callbacks +#[rapier_export] +pub unsafe extern "C" fn rpr_read_rigid_body_count(context: *const RprReadContext) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + crate::handle_access::forward(native_rigid_body_set_len(get(context)?.bodies, out)) + }) + }) +} +/// Copy entity handles. Uses only the callback-scoped read context; never retain the context. +/// @see @ref output_buffers +/// @ingroup callbacks +#[rapier_export] +pub unsafe extern "C" fn rpr_read_rigid_body_handles( + context: *const RprReadContext, + buffer: *mut RprRigidBodyHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array( + read_context_world(context), + buffer, + capacity, + |count: *mut usize| { + ffi(|| { + crate::handle_access::forward(native_rigid_body_set_handles( + get(context)?.bodies, + buffer, + capacity, + count, + )) + }) + }, + ) + } +} +/// Test whether the live world contains this rigid body handle. A removed/stale handle returns +/// false. Uses only the callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_contains( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_contains( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the number of collider objects in the world. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export] +pub unsafe extern "C" fn rpr_read_collider_count(context: *const RprReadContext) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + crate::handle_access::forward(native_collider_set_len(get(context)?.colliders, out)) + }) + }) +} +/// Copy entity handles. Uses only the callback-scoped read context; never retain the context. +/// @see @ref output_buffers +/// @ingroup callbacks +#[rapier_export] +pub unsafe extern "C" fn rpr_read_collider_handles( + context: *const RprReadContext, + buffer: *mut RprColliderHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array( + read_context_world(context), + buffer, + capacity, + |count: *mut usize| { + ffi(|| { + crate::handle_access::forward(native_collider_set_handles( + get(context)?.colliders, + buffer, + capacity, + count, + )) + }) + }, + ) + } +} +/// Test whether the live world contains this collider handle. A removed/stale handle returns false. +/// Uses only the callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_contains( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_contains( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return a process-local geometry identity for caching, not a serializable ID. Keep a shared-shape +/// clone alive while using it as a cache key. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_shape_identity( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_shape_identity( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider local mass properties. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_mass_properties( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprMassProperties { + ffi_value(|out: *mut RprMassProperties| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_mass_properties( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the rigid body translation/rotation lock bitmask. Uses only the callback-scoped read +/// context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_locked_axes( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> u8 { + ffi_value(|out: *mut u8| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_locked_axes( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return whether the collider is a voxel shape. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_is_voxels( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_is_voxels( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return voxel information at a flat index; found = 0 if absent. Uses only the callback-scoped +/// read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_voxel_at_flat_id( + context: *const RprReadContext, + handle: RprColliderHandle, + id: u32, +) -> RprVoxelQuery { + ffi_value(|result: *mut RprVoxelQuery| { + let key = unsafe { std::ptr::addr_of_mut!((*result).key) }; + let center = unsafe { std::ptr::addr_of_mut!((*result).center) }; + let size = unsafe { std::ptr::addr_of_mut!((*result).size) }; + let found = unsafe { std::ptr::addr_of_mut!((*result).found) }; + + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_voxel_at_flat_id( + get(context)?.colliders, + handle, + id, + key, + center, + size, + found, + )) + }) + }) +} +/// Return the rigid body next kinematic world-space pose. Uses only the callback-scoped read +/// context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_next_position( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprPose { + ffi_value(|out: *mut RprPose| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_next_position( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body world-space rotation. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_rotation( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprRotation { + ffi_value(|out: *mut RprRotation| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_rotation( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body world-space center of mass. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_center_of_mass( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprVector { + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_center_of_mass( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body body-local center of mass. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_local_center_of_mass( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprVector { + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_local_center_of_mass( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body accumulated user-applied world-space force. Uses only the callback-scoped +/// read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_user_force( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprVector { + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_user_force( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body accumulated user-applied world-space torque. Uses only the callback-scoped +/// read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_user_torque( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprAngVector { + ffi_value(|out: *mut RprAngVector| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_user_torque( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body body type (RPR_DYNAMIC, RPR_FIXED, or a kinematic kind). Uses only the +/// callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_body_type( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> u32 { + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_body_type( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body mass. Uses only the callback-scoped read context; never retain the +/// context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_mass( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_mass( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body gravity multiplier. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_gravity_scale( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_gravity_scale( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body linear damping coefficient. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_linear_damping( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_linear_damping( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body angular damping coefficient. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_angular_damping( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_angular_damping( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body kinetic energy. Uses only the callback-scoped read context; never retain +/// the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_kinetic_energy( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_kinetic_energy( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body soft-CCD prediction distance. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_soft_ccd_prediction( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_soft_ccd_prediction( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return whether the rigid body is using continuous collision detection. Uses only the +/// callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_is_ccd_enabled( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_is_ccd_enabled( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return whether the rigid body is dynamic. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_is_dynamic( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_is_dynamic( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the associated soft-body handle, or an invalid handle if this is not a soft proxy. Uses +/// only the callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_soft_body( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprSoftBodyHandle { + ffi_world_value( + unsafe { read_context_world(context) }, + |out: *mut RprSoftBodyHandle| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_soft_body( + get(context)?.bodies, + handle, + out, + )) + }) + }, + ) +} +/// Return whether the rigid body is a soft-body proxy. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_is_soft_frame( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_is_soft_frame( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return whether the rigid body is fixed. Uses only the callback-scoped read context; never retain +/// the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_is_fixed( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_is_fixed( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return whether the rigid body is kinematic. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_is_kinematic( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_is_kinematic( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return whether the rigid body is moving. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_is_moving( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_is_moving( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return whether the rigid body is currently using CCD for its motion. Uses only the +/// callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_is_ccd_active( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_is_ccd_active( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return world-space velocity at a world-space point, including angular motion. Uses only the +/// callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_velocity_at_point( + context: *const RprReadContext, + handle: RprRigidBodyHandle, + point: RprVector, +) -> RprVector { + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_velocity_at_point( + get(context)?.bodies, + handle, + point, + out, + )) + }) + }) +} +/// Copy attached collider handles. Uses only the callback-scoped read context; never retain the +/// context. +/// @see @ref output_buffers +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_colliders( + context: *const RprReadContext, + handle: RprRigidBodyHandle, + buffer: *mut RprColliderHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array( + read_context_world(context), + buffer, + capacity, + |count: *mut usize| { + ffi(|| { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_colliders( + get(context)?.bodies, + handle, + buffer, + capacity, + count, + )) + }) + }, + ) + } +} +/// Return whether the rigid body is using gyroscopic forces. Uses only the callback-scoped read +/// context; never retain the context. +/// @ingroup callbacks +#[cfg(feature = "dim3")] +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_gyroscopic_forces_enabled( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_gyroscopic_forces_enabled( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the collider world-space rotation. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_rotation( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprRotation { + ffi_value(|out: *mut RprRotation| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_rotation( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider collision filtering groups. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_collision_groups( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprInteractionGroups { + ffi_value(|out: *mut RprInteractionGroups| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_collision_groups( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider contact-force filtering groups. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_solver_groups( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprInteractionGroups { + ffi_value(|out: *mut RprInteractionGroups| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_solver_groups( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider application-owned 128-bit user value. Uses only the callback-scoped read +/// context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_user_data( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprUserData { + ffi_value(|out: *mut RprUserData| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_user_data( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider event-generation bitmask (RPR_COLLISION_EVENTS and +/// RPR_CONTACT_FORCE_EVENTS). Uses only the callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_active_events( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> u32 { + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_active_events( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider mass. Uses only the callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_mass( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_mass( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider mass per unit volume. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_density( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_density( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider current volume. Uses only the callback-scoped read context; never retain the +/// context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_volume( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_volume( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider extra separation skin around the shape. Uses only the callback-scoped read +/// context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_contact_skin( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_contact_skin( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider force threshold for contact-force events. Uses only the callback-scoped read +/// context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_contact_force_event_threshold( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_contact_force_event_threshold( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return whether the collider is enabled. Uses only the callback-scoped read context; never retain +/// the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_is_enabled( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_is_enabled( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the current world-space axis-aligned bounds. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_compute_aabb( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprAabb { + ffi_value(|out: *mut RprAabb| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_compute_aabb( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return an owned wrapper sharing the collider geometry. Release with rpr_free_shared_shape. Uses +/// only the callback-scoped read context; never retain the context. +/// Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_clone_shape( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_shared_shape( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Validate the index and generation in the live owning world. Cannot detect a world pointer that +/// has already been freed. Uses only the callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_validate_handle( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprStatus { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_validate_handle( + get(context)?.bodies, + handle, + )) + }) +} +/// Validate the index and generation in the live owning world. Cannot detect a world pointer that +/// has already been freed. Uses only the callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_validate_handle( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprStatus { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_validate_handle( + get(context)?.colliders, + handle, + )) + }) +} +/// Return the rigid body world-space pose. Uses only the callback-scoped read context; never retain +/// the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_position( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprPose { + ffi_value(|out: *mut RprPose| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_position( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body world-space translation. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_translation( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprVector { + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_translation( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body world-space linear velocity. Uses only the callback-scoped read context; +/// never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_linvel( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprVector { + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_linvel( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body world-space angular velocity (radians per second). Uses only the +/// callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_angvel( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprAngVector { + ffi_value(|out: *mut RprAngVector| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_angvel( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return whether the rigid body is sleeping. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_is_sleeping( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_is_sleeping( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return whether the rigid body is enabled. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_is_enabled( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_is_enabled( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body application-owned 128-bit user value. Uses only the callback-scoped read +/// context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_user_data( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprUserData { + ffi_value(|out: *mut RprUserData| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_user_data( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the collider world-space pose. Uses only the callback-scoped read context; never retain +/// the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_position( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprPose { + ffi_value(|out: *mut RprPose| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_position( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider world-space translation. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_translation( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprVector { + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_translation( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider friction coefficient. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_friction( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_friction( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider restitution coefficient. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_restitution( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_restitution( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return whether the collider is a sensor (detects overlaps without contact forces). Uses only the +/// callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_is_sensor( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_is_sensor( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Read the parent body handle during a callback; a standalone collider returns an invalid handle +/// with OK status. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_parent( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprRigidBodyHandle { + ffi_world_value( + unsafe { read_context_world(context) }, + |out: *mut RprRigidBodyHandle| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_parent( + get(context)?.colliders, + handle, + out, + )) + }) + }, + ) +} +/// Copy callback-visible body states in the supplied handle order. All handles must belong to the +/// context world. +/// @see @ref output_buffers +/// @ingroup callbacks +#[rapier_export] +pub unsafe extern "C" fn rpr_read_rigid_body_read_states( + context: *const RprReadContext, + handles: *const RprRigidBodyHandle, + handle_count: usize, + states: *mut RprRigidBodyState, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + for value in input(handles, handle_count)? { + value.check_world(read_context_world(context))?; + } + crate::handle_access::forward(native_rigid_body_set_read_states( + get(context)?.bodies, + handles, + handle_count, + states, + capacity, + count, + )) + }) + }) +} diff --git a/c/src/render.rs b/c/src/render.rs new file mode 100644 index 000000000..c7a5c2e8c --- /dev/null +++ b/c/src/render.rs @@ -0,0 +1,346 @@ +//! Shape geometry for foreign renderers. Tessellation is independent of graphics libraries. +use crate::*; +use rapier::parry::shape::{Shape, TypedShape}; + +/// Owned tessellated shape: flat triangle vertices and independent line segments, in local space. +/// Rounded 3D shapes use their inner surface (as in the Rust testbed). Halfspaces use a finite +/// patch. +/// @ingroup shapes +pub struct RprShapeMesh { + triangles: Vec, + lines: Vec, +} + +impl RprShapeMesh { + fn triangle_mesh(&mut self, pose: Pose, vertices: Vec, indices: Vec<[u32; 3]>) { + for tri in indices { + for i in tri { + self.triangles.push((pose * vertices[i as usize]).into()); + } + } + } + fn line_mesh(&mut self, pose: Pose, vertices: Vec, indices: Vec<[u32; 2]>) { + for segment in indices { + for i in segment { + self.lines.push((pose * vertices[i as usize]).into()); + } + } + } + #[cfg(feature = "dim2")] + fn polygon(&mut self, pose: Pose, vertices: Vec) { + for i in 1..vertices.len().saturating_sub(1) { + for j in [0, i, i + 1] { + self.triangles.push((pose * vertices[j]).into()); + } + } + } + fn append(&mut self, shape: &dyn Shape, pose: Pose, n: u32) -> Result<()> { + match shape.as_typed_shape() { + TypedShape::Compound(c) => { + for (p, s) in c.shapes() { + self.append(s.as_ref(), pose * *p, n)?; + } + } + TypedShape::Segment(s) => self.line_mesh(pose, vec![s.a, s.b], vec![[0, 1]]), + TypedShape::Polyline(p) => { + self.line_mesh(pose, p.vertices().to_vec(), p.indices().to_vec()) + } + TypedShape::Triangle(t) => { + self.triangle_mesh(pose, vec![t.a, t.b, t.c], vec![[0, 1, 2]]) + } + TypedShape::RoundTriangle(t) => self.append(&t.inner_shape, pose, n)?, + TypedShape::TriMesh(m) => { + self.triangle_mesh(pose, m.vertices().to_vec(), m.indices().to_vec()) + } + TypedShape::HalfSpace(h) => { + #[cfg(feature = "dim2")] + { + let tangent = Vector::new(-h.normal.y, h.normal.x) * 1000.0; + self.line_mesh(pose, vec![-tangent, tangent], vec![[0, 1]]); + } + #[cfg(feature = "dim3")] + { + let normal = h.normal; + let tangent = normal.any_orthonormal_vector() * 1000.0; + let bitangent = normal.cross(tangent); + self.triangle_mesh( + pose, + vec![ + -tangent - bitangent, + tangent - bitangent, + tangent + bitangent, + -tangent + bitangent, + ], + vec![[0, 1, 2], [0, 2, 3]], + ); + } + } + #[cfg(feature = "dim2")] + TypedShape::Ball(s) => self.polygon(pose, s.to_polyline(n)), + #[cfg(feature = "dim2")] + TypedShape::Cuboid(s) => self.polygon(pose, s.to_polyline()), + #[cfg(feature = "dim2")] + TypedShape::RoundCuboid(s) => self.polygon(pose, s.to_polyline(n / 4)), + #[cfg(feature = "dim2")] + TypedShape::Capsule(s) => self.polygon(pose, s.to_polyline(n)), + #[cfg(feature = "dim2")] + TypedShape::ConvexPolygon(s) => self.polygon(pose, s.points().to_vec()), + #[cfg(feature = "dim2")] + TypedShape::RoundConvexPolygon(s) => self.polygon(pose, s.to_polyline(n / 4)), + #[cfg(feature = "dim2")] + TypedShape::HeightField(s) => { + let (v, i) = s.to_polyline(); + self.line_mesh(pose, v, i); + } + #[cfg(feature = "dim2")] + TypedShape::Voxels(s) => { + let (v, i) = s.to_polyline(); + self.line_mesh(pose, v, i); + } + #[cfg(feature = "dim3")] + TypedShape::Ball(s) => { + let (v, i) = s.to_trimesh(n, n / 2); + self.triangle_mesh(pose, v, i); + } + #[cfg(feature = "dim3")] + TypedShape::Cuboid(s) => { + let (v, i) = s.to_trimesh(); + self.triangle_mesh(pose, v, i); + } + #[cfg(feature = "dim3")] + TypedShape::Capsule(s) => { + let (v, i) = s.to_trimesh(n, n / 2); + self.triangle_mesh(pose, v, i); + } + #[cfg(feature = "dim3")] + TypedShape::Cylinder(s) => { + let (v, i) = s.to_trimesh(n); + self.triangle_mesh(pose, v, i); + } + #[cfg(feature = "dim3")] + TypedShape::Cone(s) => { + let (v, i) = s.to_trimesh(n); + self.triangle_mesh(pose, v, i); + } + #[cfg(feature = "dim3")] + TypedShape::RoundCuboid(s) => self.append(&s.inner_shape, pose, n)?, + #[cfg(feature = "dim3")] + TypedShape::RoundCylinder(s) => self.append(&s.inner_shape, pose, n)?, + #[cfg(feature = "dim3")] + TypedShape::RoundCone(s) => self.append(&s.inner_shape, pose, n)?, + #[cfg(feature = "dim3")] + TypedShape::ConvexPolyhedron(s) => { + let (v, i) = s.to_trimesh(); + self.triangle_mesh(pose, v, i); + } + #[cfg(feature = "dim3")] + TypedShape::RoundConvexPolyhedron(s) => self.append(&s.inner_shape, pose, n)?, + #[cfg(feature = "dim3")] + TypedShape::HeightField(s) => { + let (v, i) = s.to_trimesh(); + self.triangle_mesh(pose, v, i); + } + #[cfg(feature = "dim3")] + TypedShape::Voxels(s) => { + let (v, i) = s.to_trimesh(); + self.triangle_mesh(pose, v, i); + } + TypedShape::Custom(_) => { + return Err(( + RPR_UNSUPPORTED, + "custom shapes cannot be tessellated".into(), + )); + } + } + Ok(()) + } +} +/// Process-local identity of the immutable shape allocation, for render caches. Keep an owned +/// SharedShape clone alive while caching this value. Not serializable; does not identify equal +/// geometry. +pub(crate) unsafe fn native_collider_shape_identity( + collider: *const RprCollider, + out: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + output( + out, + get(collider)?.0.shape() as *const dyn Shape as *const () as usize, + ) + }) +} +/// Return owned local-space rendering geometry; release it with rpr_free_shape_mesh. subdivisions +/// controls curved-shape resolution. +/// @ingroup shapes +#[rapier_export(shared_shape)] +pub unsafe extern "C" fn rpr_shared_shape_tessellate( + shape: *const RprSharedShape, + subdivisions: u32, +) -> *mut RprShapeMesh { + ffi_value(|out: *mut *mut RprShapeMesh| { + ffi(|| unsafe { + out_ptr(out)?; + ensure( + (8..=128).contains(&subdivisions), + "subdivisions must be between 8 and 128", + )?; + let mut mesh = RprShapeMesh { + triangles: Vec::new(), + lines: Vec::new(), + }; + mesh.append(get(shape)?.0.as_ref(), Pose::IDENTITY, subdivisions)?; + output(out, Box::into_raw(Box::new(mesh))) + }) + }) +} +/// Flat groups of three vertices. Standard output-buffer convention. +/// @see @ref output_buffers +/// @ingroup shapes +#[rapier_export(shape_mesh)] +pub unsafe extern "C" fn rpr_shape_mesh_triangles( + mesh: *const RprShapeMesh, + buffer: *mut RprVector, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { copy_out(&get(mesh)?.triangles, buffer, capacity, count) }) + }) +} +/// Flat groups of two vertices. Standard output-buffer convention. +/// @see @ref output_buffers +/// @ingroup shapes +#[rapier_export(shape_mesh)] +pub unsafe extern "C" fn rpr_shape_mesh_lines( + mesh: *const RprShapeMesh, + buffer: *mut RprVector, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { copy_out(&get(mesh)?.lines, buffer, capacity, count) }) + }) +} +/// Release an owned shape mesh. NULL is allowed. Do not pass borrowed pointers or free the object +/// twice. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_free_shape_mesh(mesh: *mut RprShapeMesh) -> RprStatus { + ffi(|| unsafe { + if !mesh.is_null() { + get(mesh)?; + drop(Box::from_raw(mesh)); + } + Ok(()) + }) +} +/// Create an owned round cylinder shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[cfg(feature = "dim3")] +#[rapier_export] +pub unsafe extern "C" fn rpr_round_cylinder_shared_shape( + half_height: RprReal, + radius: RprReal, + border_radius: RprReal, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + output( + out, + Box::into_raw(Box::new(RprSharedShape(SharedShape::round_cylinder( + positive(half_height)?, + positive(radius)?, + nonnegative(border_radius)?, + )))), + ) + }) + }) +} + +/// Owned indexed geometry from Parry's shape tessellation, preserving its vertex order. +/// @ingroup shapes +#[cfg(feature = "dim3")] +pub struct RprTriMeshData { + vertices: Vec, + indices: Vec, +} + +/// Tessellate a ball or capsule with independent longitude/latitude subdivision counts. +/// Cuboids, cones, cylinders, convex polyhedra, trimeshes, and heightfields are also supported. +/// @ingroup shapes +#[cfg(feature = "dim3")] +#[rapier_export(shared_shape)] +pub unsafe extern "C" fn rpr_shared_shape_to_trimesh( + shape: *const RprSharedShape, + ntheta: u32, + nphi: u32, +) -> *mut RprTriMeshData { + ffi_value(|out: *mut *mut RprTriMeshData| { + ffi(|| unsafe { + out_ptr(out)?; + ensure( + (3..=4096).contains(&ntheta) && (2..=4096).contains(&nphi), + "invalid tessellation subdivisions", + )?; + let (vertices, indices) = match get(shape)?.0.as_typed_shape() { + TypedShape::Ball(s) => s.to_trimesh(ntheta, nphi), + TypedShape::Capsule(s) => s.to_trimesh(ntheta, nphi), + TypedShape::Cuboid(s) => s.to_trimesh(), + TypedShape::Cone(s) => s.to_trimesh(ntheta), + TypedShape::Cylinder(s) => s.to_trimesh(ntheta), + TypedShape::ConvexPolyhedron(s) => s.to_trimesh(), + TypedShape::TriMesh(s) => (s.vertices().to_vec(), s.indices().to_vec()), + TypedShape::HeightField(s) => s.to_trimesh(), + _ => return Err((RPR_UNSUPPORTED, "shape has no indexed tessellation".into())), + }; + let mesh = RprTriMeshData { + vertices: vertices.into_iter().map(Into::into).collect(), + indices: indices.into_iter().flatten().collect(), + }; + output(out, Box::into_raw(Box::new(mesh))) + }) + }) +} + +/// Copy vertices. +/// @see @ref output_buffers +/// @ingroup shapes +#[cfg(feature = "dim3")] +#[rapier_export(tri_mesh_data)] +pub unsafe extern "C" fn rpr_tri_mesh_data_vertices( + mesh: *const RprTriMeshData, + buffer: *mut RprVector, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { copy_out(&get(mesh)?.vertices, buffer, capacity, count) }) + }) +} + +/// Flat triangle indices; count and capacity are numbers of u32 entries. +/// @see @ref output_buffers +/// @ingroup shapes +#[cfg(feature = "dim3")] +#[rapier_export(tri_mesh_data)] +pub unsafe extern "C" fn rpr_tri_mesh_data_indices( + mesh: *const RprTriMeshData, + buffer: *mut u32, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { copy_out(&get(mesh)?.indices, buffer, capacity, count) }) + }) +} + +/// Release an owned tri mesh data. NULL is allowed. Do not pass borrowed pointers or free the +/// object twice. +/// @ingroup shapes +#[cfg(feature = "dim3")] +#[rapier_export] +pub unsafe extern "C" fn rpr_free_tri_mesh_data(mesh: *mut RprTriMeshData) -> RprStatus { + ffi(|| unsafe { + if !mesh.is_null() { + drop(Box::from_raw(mesh)); + } + Ok(()) + }) +} diff --git a/c/src/return_values.rs b/c/src/return_values.rs new file mode 100644 index 000000000..af2f4cb1e --- /dev/null +++ b/c/src/return_values.rs @@ -0,0 +1,99 @@ +//! Values returned by operations that produce several related outputs. +#![allow(non_snake_case)] +use crate::*; + +/// Optional ray collider/time result. A miss is found = 0 with status OK. +/// @ingroup queries +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprRayToi { + /// World-bound collider handle. + pub collider: RprColliderHandle, + /// Ray parameter t at impact: origin + direction * t. + pub toi: RprReal, + /// Whether a result exists; other result fields are meaningful only when this is 1. + pub found: RprBool, +} + +/// Optional full ray result. A miss is found = 0 with status OK. +/// @ingroup queries +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprOptionalRayHit { + /// Shape/ray impact details. + pub hit: RprRayHit, + /// Whether a result exists; other result fields are meaningful only when this is 1. + pub found: RprBool, +} + +/// Linear and angular velocity correction computed by a controller. +/// @ingroup controllers +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprVelocityCorrection { + /// World-space linear velocity correction. + pub linear: RprVector, + /// World-space angular velocity correction, in radians per second. + pub angularVelocity: RprAngVector, +} + +/// Particle remapping after tearing. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprParticleDestination { + /// World-bound rigid/soft-body handle, as selected by the field type. + pub body: RprSoftBodyHandle, + /// Zero-based element index. + pub index: u32, +} + +/// Optional particle remapping; check found before reading the destination. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprOptionalParticleDestination { + /// World-bound rigid/soft-body handle, as selected by the field type. + pub body: RprSoftBodyHandle, + /// Zero-based element index. + pub index: u32, + /// Whether a result exists; other result fields are meaningful only when this is 1. + pub found: RprBool, +} + +/// Borrowed bytes. Valid while the source Bytes object remains alive; never free data. +/// @ingroup math +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprByteView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const u8, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} + +/// Optional voxel lookup result; check found before reading voxel data. +/// @ingroup queries +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprVoxelQuery { + /// Integer voxel coordinates. + pub key: RprVoxelKey, + /// World-space wheel center. + pub center: RprVector, + /// Voxel dimensions along each axis. + pub size: RprVector, + /// Whether a result exists; other result fields are meaningful only when this is 1. + pub found: RprBool, +} + +/// World-bound handles of the two connected bodies. +/// @ingroup joints +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprJointBodies { + /// First connected body. + pub body1: RprRigidBodyHandle, + /// Second connected body. + pub body2: RprRigidBodyHandle, +} diff --git a/c/src/robotics.rs b/c/src/robotics.rs new file mode 100644 index 000000000..7c795f50f --- /dev/null +++ b/c/src/robotics.rs @@ -0,0 +1,1181 @@ +//! Optional native URDF and MJCF importers (3D f32). +#![allow(non_snake_case)] +use crate::*; +use rapier3d_mjcf::MjcfVisualMesh; +use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot, MjcfRobotHandles}; +use rapier3d_urdf::{UrdfLoaderOptions, UrdfMultibodyOptions, UrdfRobot, UrdfRobotHandles}; +use std::ffi::{CStr, c_char}; + +unsafe fn path_string<'a>(path: *const c_char) -> Result<&'a str> { + ensure(!path.is_null(), "null path")?; + unsafe { CStr::from_ptr(path) } + .to_str() + .map_err(|_| invalid("path must be UTF-8")) +} +/// Loader configuration. Initialize with DefaultUrdfLoaderOptions; no destructor. +/// Blueprint array views and shared shapes are borrowed through the load call. +/// @ingroup robotics +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RprUrdfLoaderOptions { + /// Whether to build colliders from collision geometry. + pub createCollidersFromCollisionShapes: RprBool, + /// Whether to also build colliders from visual geometry. + pub createCollidersFromVisualShapes: RprBool, + /// Whether to use imported mass/inertia properties. + pub applyImportedMassProps: RprBool, + /// Whether bodies connected by imported joints may collide. + pub enableJointCollisions: RprBool, + /// Whether imported root bodies are fixed. + pub makeRootsFixed: RprBool, + /// Whether to merge empty fixed URDF links. + pub squeezeEmptyFixedLinks: RprBool, + /// Transform applied to the imported model. + pub shift: RprPose, + /// Shape scale along each axis. + pub scale: RprReal, + /// Default collider description; nested geometry resources are borrowed through loading. + pub colliderBlueprint: RprColliderDesc, + /// Default rigid-body description used by the importer. + pub rigidBodyBlueprint: RprRigidBodyDesc, +} +impl Default for RprUrdfLoaderOptions { + fn default() -> Self { + let defaults = UrdfLoaderOptions::default(); + Self { + createCollidersFromCollisionShapes: defaults.create_colliders_from_collision_shapes + as RprBool, + createCollidersFromVisualShapes: defaults.create_colliders_from_visual_shapes + as RprBool, + applyImportedMassProps: defaults.apply_imported_mass_props as RprBool, + enableJointCollisions: defaults.enable_joint_collisions as RprBool, + makeRootsFixed: defaults.make_roots_fixed as RprBool, + squeezeEmptyFixedLinks: defaults.squeeze_empty_fixed_links as RprBool, + shift: defaults.shift.into(), + scale: defaults.scale, + colliderBlueprint: RprColliderDesc { + density: 0.0, + ..Default::default() + }, + rigidBodyBlueprint: rpr_dynamic_rigid_body_desc(), + } + } +} +impl RprUrdfLoaderOptions { + unsafe fn raw(&self) -> Result { + Ok(UrdfLoaderOptions { + create_colliders_from_collision_shapes: boolean( + self.createCollidersFromCollisionShapes, + )?, + create_colliders_from_visual_shapes: boolean(self.createCollidersFromVisualShapes)?, + apply_imported_mass_props: boolean(self.applyImportedMassProps)?, + enable_joint_collisions: boolean(self.enableJointCollisions)?, + make_roots_fixed: boolean(self.makeRootsFixed)?, + squeeze_empty_fixed_links: boolean(self.squeezeEmptyFixedLinks)?, + shift: self.shift.raw()?, + scale: positive(self.scale)?, + collider_blueprint: unsafe { self.colliderBlueprint.raw()? }, + rigid_body_blueprint: self.rigidBodyBlueprint.raw()?, + ..Default::default() + }) + } +} +/// Return native default urdf loader options. This POD value owns no resources. +/// @ingroup robotics +#[rapier_export] +pub extern "C" fn rpr_default_urdf_loader_options() -> RprUrdfLoaderOptions { + RprUrdfLoaderOptions::default() +} +/// Loaded URDF robot; insertion clones its simulation objects. Release with the matching Free +/// function. +/// @ingroup robotics +pub struct RprUrdfRobot(pub(crate) UrdfRobot); +/// Release an owned urdf robot. NULL is allowed. Do not pass borrowed pointers or free the object +/// twice. +/// @ingroup robotics +#[rapier_export] +pub unsafe extern "C" fn rpr_free_urdf_robot(object: *mut RprUrdfRobot) -> RprStatus { + ffi(|| unsafe { + if !object.is_null() { + get(object)?; + drop(Box::from_raw(object)); + } + Ok(()) + }) +} +/// Load from a UTF-8 path. Validates options before reading the file. +/// Options and their blueprint resources are borrowed through this call; the robot is owned. +/// @ingroup robotics +#[rapier_export] +pub unsafe extern "C" fn rpr_urdf_robot_from_file( + path: *const c_char, + options: *const RprUrdfLoaderOptions, +) -> *mut RprUrdfRobot { + ffi_value(|out: *mut *mut RprUrdfRobot| { + ffi(|| unsafe { + out_ptr(out)?; + let path = path_string(path)?; + let (robot, _) = UrdfRobot::from_file(path, get(options)?.raw()?, None) + .map_err(|e| invalid(e.to_string()))?; + output(out, Box::into_raw(Box::new(RprUrdfRobot(robot)))) + }) + }) +} +/// Apply an additional transform to the loaded robot before insertion. +/// @ingroup robotics +#[rapier_export(urdf_robot)] +pub unsafe extern "C" fn rpr_urdf_robot_append_transform( + robot: *mut RprUrdfRobot, + transform: RprPose, +) -> RprStatus { + ffi(|| unsafe { + get_mut(robot)?.0.append_transform(&transform.raw()?); + Ok(()) + }) +} +/// Owned container of borrowed handles to an inserted URDF robot. Release with the matching Free +/// function. +/// @ingroup robotics +pub struct RprUrdfRobotHandles { + world: *mut RprWorld, + handles: UrdfHandles, +} +enum UrdfHandles { + Impulse(UrdfRobotHandles), + Multibody(UrdfRobotHandles>), +} +/// Release an owned urdf robot handles. NULL is allowed. Do not pass borrowed pointers or free the +/// object twice. +/// @ingroup robotics +#[rapier_export] +pub unsafe extern "C" fn rpr_free_urdf_robot_handles( + handles: *mut RprUrdfRobotHandles, +) -> RprStatus { + ffi(|| unsafe { + if !handles.is_null() { + get(handles)?; + drop(Box::from_raw(handles)); + } + Ok(()) + }) +} +/// Inserts a clone; the source robot remains owned by the caller. Returns owned handles. +/// @ingroup robotics +#[rapier_export(urdf_robot)] +pub unsafe extern "C" fn rpr_urdf_robot_insert_using_impulse_joints( + world: *mut RprWorld, + robot: *const RprUrdfRobot, +) -> *mut RprUrdfRobotHandles { + ffi_value(|out: *mut *mut RprUrdfRobotHandles| { + ffi(|| unsafe { + let owner = world; + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + out_ptr(out)?; + + let world = &mut get_mut(world)?.0; + let handles = get(robot)?.0.clone().insert_using_impulse_joints( + &mut world.bodies, + &mut world.colliders, + &mut world.impulse_joints, + ); + output( + out, + Box::into_raw(Box::new(RprUrdfRobotHandles { + world: owner, + handles: UrdfHandles::Impulse(handles), + })), + ) + }) + }) +} + +/// Inserts a clone; the source robot remains owned by the caller. Returns owned handles. +/// @ingroup robotics +#[rapier_export(urdf_robot)] +pub unsafe extern "C" fn rpr_urdf_robot_insert_using_multibody_joints( + world: *mut RprWorld, + robot: *const RprUrdfRobot, + options: u8, +) -> *mut RprUrdfRobotHandles { + ffi_value(|out: *mut *mut RprUrdfRobotHandles| { + ffi(|| unsafe { + let owner = world; + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + out_ptr(out)?; + let options = UrdfMultibodyOptions::from_bits(options) + .ok_or_else(|| invalid("unknown multibody options"))?; + let world = &mut get_mut(world)?.0; + let handles = get(robot)?.0.clone().insert_using_multibody_joints( + &mut world.bodies, + &mut world.colliders, + &mut world.multibody_joints, + options, + ); + output( + out, + Box::into_raw(Box::new(RprUrdfRobotHandles { + world: owner, + handles: UrdfHandles::Multibody(handles), + })), + ) + }) + }) +} + +/// Body handles in source order; absent MJCF bodies have invalid handles. +/// @see @ref output_buffers +/// @ingroup robotics +#[rapier_export(urdf_robot_handles)] +pub unsafe extern "C" fn rpr_urdf_robot_handles_bodies( + handles: *const RprUrdfRobotHandles, + buffer: *mut RprRigidBodyHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array( + get(handles).map_or(std::ptr::null_mut(), |h| h.world), + buffer, + capacity, + |count: *mut usize| { + ffi(|| { + let values: Vec = match &get(handles)?.handles { + UrdfHandles::Impulse(h) => h.links.iter().map(|b| b.body.into()).collect(), + UrdfHandles::Multibody(h) => { + h.links.iter().map(|b| b.body.into()).collect() + } + }; + copy_out(&values, buffer, capacity, count) + }) + }, + ) + } +} +/// Loader configuration. Initialize with DefaultMjcfLoaderOptions; no destructor. +/// Blueprint array views and shared shapes are borrowed through the load call. +/// @ingroup robotics +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RprMjcfLoaderOptions { + /// Whether to build colliders from collision geometry. + pub createCollidersFromCollisionShapes: RprBool, + /// Whether to also build colliders from visual geometry. + pub createCollidersFromVisualShapes: RprBool, + /// Whether to use imported mass/inertia properties. + pub applyImportedMassProps: RprBool, + /// Whether bodies connected by imported joints may collide. + pub enableJointCollisions: RprBool, + /// Whether imported root bodies are fixed. + pub makeRootsFixed: RprBool, + /// Whether to omit MJCF plane geometry. + pub skipPlaneGeoms: RprBool, + /// Whether imported joint motors are disabled. + pub disableJointMotors: RprBool, + /// Transform applied to the imported model. + pub shift: RprPose, + /// Shape scale along each axis. + pub scale: RprReal, + /// Default collider description; nested geometry resources are borrowed through loading. + pub colliderBlueprint: RprColliderDesc, + /// Default rigid-body description used by the importer. + pub rigidBodyBlueprint: RprRigidBodyDesc, +} +impl Default for RprMjcfLoaderOptions { + fn default() -> Self { + let defaults = MjcfLoaderOptions::default(); + Self { + createCollidersFromCollisionShapes: defaults.create_colliders_from_collision_shapes + as RprBool, + createCollidersFromVisualShapes: defaults.create_colliders_from_visual_shapes + as RprBool, + applyImportedMassProps: defaults.apply_imported_mass_props as RprBool, + enableJointCollisions: defaults.enable_joint_collisions as RprBool, + makeRootsFixed: defaults.make_roots_fixed as RprBool, + skipPlaneGeoms: defaults.skip_plane_geoms as RprBool, + disableJointMotors: defaults.disable_joint_motors as RprBool, + shift: defaults.shift.into(), + scale: defaults.scale, + colliderBlueprint: RprColliderDesc { + density: 0.0, + ..Default::default() + }, + rigidBodyBlueprint: rpr_dynamic_rigid_body_desc(), + } + } +} +impl RprMjcfLoaderOptions { + unsafe fn raw(&self) -> Result { + Ok(MjcfLoaderOptions { + create_colliders_from_collision_shapes: boolean( + self.createCollidersFromCollisionShapes, + )?, + create_colliders_from_visual_shapes: boolean(self.createCollidersFromVisualShapes)?, + apply_imported_mass_props: boolean(self.applyImportedMassProps)?, + enable_joint_collisions: boolean(self.enableJointCollisions)?, + make_roots_fixed: boolean(self.makeRootsFixed)?, + skip_plane_geoms: boolean(self.skipPlaneGeoms)?, + disable_joint_motors: boolean(self.disableJointMotors)?, + shift: self.shift.raw()?, + scale: positive(self.scale)?, + collider_blueprint: unsafe { self.colliderBlueprint.raw()? }, + rigid_body_blueprint: self.rigidBodyBlueprint.raw()?, + ..Default::default() + }) + } +} +/// Return native default mjcf loader options. This POD value owns no resources. +/// @ingroup robotics +#[rapier_export] +pub extern "C" fn rpr_default_mjcf_loader_options() -> RprMjcfLoaderOptions { + RprMjcfLoaderOptions::default() +} +/// Loaded MJCF robot and its visual/keyframe data. Release with the matching Free function. +/// @ingroup robotics +pub struct RprMjcfRobot(pub(crate) MjcfRobot); +/// Release an owned mjcf robot. NULL is allowed. Do not pass borrowed pointers or free the object +/// twice. +/// @ingroup robotics +#[rapier_export] +pub unsafe extern "C" fn rpr_free_mjcf_robot(object: *mut RprMjcfRobot) -> RprStatus { + ffi(|| unsafe { + if !object.is_null() { + get(object)?; + drop(Box::from_raw(object)); + } + Ok(()) + }) +} +/// Load from a UTF-8 path. Validates options before reading the file. +/// Options and their blueprint resources are borrowed through this call; the robot is owned. +/// @ingroup robotics +#[rapier_export] +pub unsafe extern "C" fn rpr_mjcf_robot_from_file( + path: *const c_char, + options: *const RprMjcfLoaderOptions, +) -> *mut RprMjcfRobot { + ffi_value(|out: *mut *mut RprMjcfRobot| { + ffi(|| unsafe { + out_ptr(out)?; + let path = path_string(path)?; + let (robot, _) = MjcfRobot::from_file(path, get(options)?.raw()?) + .map_err(|e| invalid(e.to_string()))?; + output(out, Box::into_raw(Box::new(RprMjcfRobot(robot)))) + }) + }) +} +/// Apply an additional transform to the loaded robot before insertion. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_append_transform( + robot: *mut RprMjcfRobot, + transform: RprPose, +) -> RprStatus { + ffi(|| unsafe { + get_mut(robot)?.0.append_transform(&transform.raw()?); + Ok(()) + }) +} +/// Owned container of borrowed handles and actuators of an inserted MJCF robot. Release with the +/// matching Free function. +/// @ingroup robotics +pub struct RprMjcfRobotHandles { + world: *mut RprWorld, + handles: MjcfHandles, +} +enum MjcfHandles { + Impulse(MjcfRobotHandles), + Multibody(MjcfRobotHandles>), +} +/// Release an owned mjcf robot handles. NULL is allowed. Do not pass borrowed pointers or free the +/// object twice. +/// @ingroup robotics +#[rapier_export] +pub unsafe extern "C" fn rpr_free_mjcf_robot_handles( + handles: *mut RprMjcfRobotHandles, +) -> RprStatus { + ffi(|| unsafe { + if !handles.is_null() { + get(handles)?; + drop(Box::from_raw(handles)); + } + Ok(()) + }) +} +/// Inserts a clone; the source robot remains owned by the caller. Returns owned handles. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_insert_using_impulse_joints( + world: *mut RprWorld, + robot: *const RprMjcfRobot, +) -> *mut RprMjcfRobotHandles { + ffi_value(|out: *mut *mut RprMjcfRobotHandles| { + ffi(|| unsafe { + let owner = world; + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + out_ptr(out)?; + + let world = &mut get_mut(world)?.0; + let handles = get(robot)?.0.clone().insert_using_impulse_joints( + &mut world.bodies, + &mut world.colliders, + &mut world.impulse_joints, + ); + output( + out, + Box::into_raw(Box::new(RprMjcfRobotHandles { + world: owner, + handles: MjcfHandles::Impulse(handles), + })), + ) + }) + }) +} + +/// Inserts a clone; the source robot remains owned by the caller. Returns owned handles. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_insert_using_multibody_joints( + world: *mut RprWorld, + robot: *const RprMjcfRobot, + options: u8, +) -> *mut RprMjcfRobotHandles { + ffi_value(|out: *mut *mut RprMjcfRobotHandles| { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + crate::handle_access::forward(native_mjcf_robot_insert_using_multibody_joints( + robot, raw, options, out, world, + )) + }) + }) +} + +pub(crate) unsafe fn native_mjcf_robot_insert_using_multibody_joints( + robot: *const RprMjcfRobot, + world: *mut RprPhysicsWorld, + options: u8, + out: *mut *mut RprMjcfRobotHandles, + owner: *mut RprWorld, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(out)?; + let options = MjcfMultibodyOptions::from_bits(options) + .ok_or_else(|| invalid("unknown multibody options"))?; + let world = &mut get_mut(world)?.0; + let handles = get(robot)?.0.clone().insert_using_multibody_joints( + &mut world.bodies, + &mut world.colliders, + &mut world.multibody_joints, + &mut world.impulse_joints, + options, + ); + output( + out, + Box::into_raw(Box::new(RprMjcfRobotHandles { + world: owner, + handles: MjcfHandles::Multibody(handles), + })), + ) + }) +} +/// Body handles in source order; absent MJCF bodies have invalid handles. +/// @see @ref output_buffers +/// @ingroup robotics +#[rapier_export(mjcf_robot_handles)] +pub unsafe extern "C" fn rpr_mjcf_robot_handles_bodies( + handles: *const RprMjcfRobotHandles, + buffer: *mut RprRigidBodyHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array( + get(handles).map_or(std::ptr::null_mut(), |h| h.world), + buffer, + capacity, + |count: *mut usize| { + ffi(|| { + let values: Vec = match &get(handles)?.handles { + MjcfHandles::Impulse(h) => h + .bodies + .iter() + .map(|b| b.as_ref().map(|b| b.body.into()).unwrap_or_default()) + .collect(), + MjcfHandles::Multibody(h) => h + .bodies + .iter() + .map(|b| b.as_ref().map(|b| b.body.into()).unwrap_or_default()) + .collect(), + }; + copy_out(&values, buffer, capacity, count) + }) + }, + ) + } +} +/// Resolved model gravity before the caller chooses a world convention. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_gravity(robot: *const RprMjcfRobot) -> RprVector { + ffi_value(|out: *mut RprVector| ffi(|| unsafe { output(out, get(robot)?.0.gravity.into()) })) +} +/// Return the number of source MJCF bodies. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_body_count(robot: *const RprMjcfRobot) -> usize { + ffi_value(|out: *mut usize| ffi(|| unsafe { output(out, get(robot)?.0.bodies.len()) })) +} +/// Return the collider count for a source body index. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_body_collider_count( + robot: *const RprMjcfRobot, + body: usize, +) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + output( + out, + get(robot)? + .0 + .bodies + .get(body) + .ok_or_else(|| invalid("body index out of range"))? + .colliders + .len(), + ) + }) + }) +} +/// Set collision groups on a collider in the loaded robot, before insertion. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_set_body_collider_collision_groups( + robot: *mut RprMjcfRobot, + body: usize, + collider: usize, + groups: RprInteractionGroups, +) -> RprStatus { + ffi(|| unsafe { + let groups = groups.raw()?; + let collider = get_mut(robot)? + .0 + .bodies + .get_mut(body) + .and_then(|b| b.colliders.get_mut(collider)) + .ok_or_else(|| invalid("collider index out of range"))?; + collider.set_collision_groups(groups); + Ok(()) + }) +} +/// Return the number of imported keyframes. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_keyframe_count(robot: *const RprMjcfRobot) -> usize { + ffi_value(|out: *mut usize| ffi(|| unsafe { output(out, get(robot)?.0.keyframes.len()) })) +} +/// Copies a NUL-terminated UTF-8 name. Count includes NUL; unnamed keys return an empty string. +/// @see @ref output_buffers +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_keyframe_name( + robot: *const RprMjcfRobot, + key: usize, + buffer: *mut c_char, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let key = get(robot)? + .0 + .keyframes + .get(key) + .ok_or_else(|| invalid("keyframe index out of range"))?; + let mut bytes = key.name.as_deref().unwrap_or("").as_bytes().to_vec(); + bytes.push(0); + copy_out(&bytes, buffer.cast(), capacity, count) + }) + }) +} +/// Append a keyframe from the source MJCF model to the loaded robot. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_append_keyframe( + robot: *mut RprMjcfRobot, + source: *const RprMjcfRobot, + key: usize, +) -> RprStatus { + ffi(|| unsafe { + let key = get(source)? + .0 + .keyframes + .get(key) + .ok_or_else(|| invalid("keyframe index out of range"))? + .clone(); + get_mut(robot)?.0.keyframes.push(key); + Ok(()) + }) +} +/// Copy actuator controls for the selected keyframe. +/// @see @ref output_buffers +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_keyframe_controls( + robot: *const RprMjcfRobot, + key: usize, + buffer: *mut RprReal, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let robot = &get(robot)?.0; + let key = robot + .keyframes + .get(key) + .ok_or_else(|| invalid("keyframe index out of range"))?; + copy_out(&robot.keyframe_controls(key), buffer, capacity, count) + }) + }) +} +/// Return the number of imported actuators. +/// @ingroup robotics +#[rapier_export(mjcf_robot_handles)] +pub unsafe extern "C" fn rpr_mjcf_robot_handles_actuator_count( + handles: *const RprMjcfRobotHandles, +) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + output( + out, + match &get(handles)?.handles { + MjcfHandles::Impulse(h) => h.actuators.len(), + MjcfHandles::Multibody(h) => h.actuators.len(), + }, + ) + }) + }) +} +/// Apply the selected keyframe to the inserted robot. +/// @ingroup robotics +#[rapier_export(mjcf_robot_handles)] +pub unsafe extern "C" fn rpr_mjcf_robot_handles_apply_keyframe( + handles: *const RprMjcfRobotHandles, + robot: *const RprMjcfRobot, + key: usize, +) -> RprStatus { + ffi(|| unsafe { + let world = get(handles)?.world; + let access = get(world)?.write()?; + let raw = access.raw(); + + crate::handle_access::forward(native_mjcf_robot_handles_apply_keyframe( + handles, raw, robot, key, + )) + }) +} + +pub(crate) unsafe fn native_mjcf_robot_handles_apply_keyframe( + handles: *const RprMjcfRobotHandles, + world: *mut RprPhysicsWorld, + robot: *const RprMjcfRobot, + key: usize, +) -> RprStatus { + ffi(|| unsafe { + let robot = &get(robot)?.0; + let key = robot + .keyframes + .get(key) + .ok_or_else(|| invalid("keyframe index out of range"))?; + let world = &mut get_mut(world)?.0; + match &get(handles)?.handles { + MjcfHandles::Impulse(h) => h.apply_keyframe(&mut world.bodies, robot, key), + MjcfHandles::Multibody(h) => { + h.apply_keyframe(&mut world.bodies, &mut world.multibody_joints, robot, key) + } + }; + Ok(()) + }) +} +/// Apply actuator controls with per-actuator scaling to the inserted robot. +/// @ingroup robotics +#[rapier_export(mjcf_robot_handles)] +pub unsafe extern "C" fn rpr_mjcf_robot_handles_apply_controls_scaled( + handles: *const RprMjcfRobotHandles, + controls: *const RprReal, + count: usize, + gain: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let world = get(handles)?.world; + let access = get(world)?.write()?; + let raw = access.raw(); + + crate::handle_access::forward(native_mjcf_robot_handles_apply_controls_scaled( + handles, raw, controls, count, gain, + )) + }) +} + +pub(crate) unsafe fn native_mjcf_robot_handles_apply_controls_scaled( + handles: *const RprMjcfRobotHandles, + world: *mut RprPhysicsWorld, + controls: *const RprReal, + count: usize, + gain: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let controls = input(controls, count)?; + for &value in controls { + finite(value)?; + } + let gain = nonnegative(gain)?; + let world = &mut get_mut(world)?.0; + match &get(handles)?.handles { + MjcfHandles::Impulse(h) => { + h.apply_controls_scaled(&mut world.impulse_joints, controls, gain) + } + MjcfHandles::Multibody(h) => h.apply_controls_multibody_scaled( + &mut world.bodies, + &mut world.multibody_joints, + controls, + gain, + ), + }; + Ok(()) + }) +} +/// A borrowed visual declaration, valid until its robot is freed or its body storage changes. +/// @ingroup robotics +#[repr(transparent)] +pub struct RprMjcfVisualMesh(pub(crate) MjcfVisualMesh); +/// Imported physically based visual material; no texture ownership. +/// @ingroup robotics +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprRenderMaterial { + /// Material metallic factor. + pub metallic: f32, + /// Material roughness factor. + pub roughness: f32, + /// Material reflectance factor. + pub reflectance: f32, + /// RGB emissive color. + pub emissive: [f32; 3], +} +/// Copied metadata for a borrowed MJCF visual mesh. +/// @ingroup robotics +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprMjcfVisualMeshInfo { + /// Visual pose relative to its source body. + pub local_pose: RprPose, + /// RGBA visual color. + pub rgba: [f32; 4], + /// Copied render material; meaningful when has_material is 1. + pub material: RprRenderMaterial, + /// Whether rgba contains an authored color. + pub has_color: RprBool, + /// Whether material contains authored material data. + pub has_material: RprBool, + /// Whether the visual geometry is a triangle mesh. + pub is_trimesh: RprBool, +} +/// Return the number of visual meshes for a source body. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_body_visual_count( + robot: *const RprMjcfRobot, + body: usize, +) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + output( + out, + get(robot)? + .0 + .bodies + .get(body) + .ok_or_else(|| invalid("body index out of range"))? + .visual_meshes + .len(), + ) + }) + }) +} +/// Borrow a visual mesh by body/visual index. Valid until the robot is freed or its storage +/// changes; never free this pointer. +/// @ingroup robotics +#[rapier_export(mjcf_robot)] +pub unsafe extern "C" fn rpr_mjcf_robot_body_visual( + robot: *const RprMjcfRobot, + body: usize, + visual: usize, +) -> *const RprMjcfVisualMesh { + ffi_value(|out: *mut *const RprMjcfVisualMesh| { + ffi(|| unsafe { + let visual = get(robot)? + .0 + .bodies + .get(body) + .and_then(|b| b.visual_meshes.get(visual)) + .ok_or_else(|| invalid("visual index out of range"))?; + output( + out, + visual as *const rapier3d_mjcf::MjcfVisualMesh as *const RprMjcfVisualMesh, + ) + }) + }) +} +/// Return a copy of visual pose, color, material, and geometry-kind flags. +/// @ingroup robotics +#[rapier_export(mjcf_visual_mesh)] +pub unsafe extern "C" fn rpr_mjcf_visual_mesh_info( + visual: *const RprMjcfVisualMesh, +) -> RprMjcfVisualMeshInfo { + ffi_value(|out: *mut RprMjcfVisualMeshInfo| { + ffi(|| unsafe { + let v = &get(visual)?.0; + let m = v.material; + output( + out, + RprMjcfVisualMeshInfo { + local_pose: v.local_pose.into(), + rgba: v.rgba.unwrap_or([0.7, 0.7, 0.75, 1.0]), + has_color: v.rgba.is_some() as RprBool, + has_material: m.is_some() as RprBool, + is_trimesh: v.shape.as_trimesh().is_some() as RprBool, + material: RprRenderMaterial { + metallic: m.map_or(0.0, |m| m.metallic), + roughness: m.map_or(1.0, |m| m.roughness), + reflectance: m.map_or(0.5, |m| m.reflectance), + emissive: m.map_or([0.0; 3], |m| m.emissive), + }, + }, + ) + }) + }) +} +/// Returns an owned shared shape reference. +/// Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. +/// @ingroup robotics +#[rapier_export(mjcf_visual_mesh)] +pub unsafe extern "C" fn rpr_mjcf_visual_mesh_clone_shape( + visual: *const RprMjcfVisualMesh, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + output( + out, + Box::into_raw(Box::new(RprSharedShape(get(visual)?.0.shape.clone()))), + ) + }) + }) +} +/// Copies flattened pairs of per-vertex UV coordinates. +/// @see @ref output_buffers +/// @ingroup robotics +#[rapier_export(mjcf_visual_mesh)] +pub unsafe extern "C" fn rpr_mjcf_visual_mesh_uvs( + visual: *const RprMjcfVisualMesh, + buffer: *mut f32, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + copy_out( + get(visual)?.0.uvs.as_deref().unwrap_or(&[]).as_flattened(), + buffer, + capacity, + count, + ) + }) + }) +} +/// Copies flattened triples of per-vertex normals. +/// @see @ref output_buffers +/// @ingroup robotics +#[rapier_export(mjcf_visual_mesh)] +pub unsafe extern "C" fn rpr_mjcf_visual_mesh_normals( + visual: *const RprMjcfVisualMesh, + buffer: *mut f32, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + copy_out( + get(visual)? + .0 + .normals + .as_deref() + .unwrap_or(&[]) + .as_flattened(), + buffer, + capacity, + count, + ) + }) + }) +} +/// Copies a NUL-terminated texture path, or an empty string for untextured meshes. +/// @see @ref output_buffers +/// @ingroup robotics +#[rapier_export(mjcf_visual_mesh)] +pub unsafe extern "C" fn rpr_mjcf_visual_mesh_texture( + visual: *const RprMjcfVisualMesh, + buffer: *mut c_char, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let v = &get(visual)?.0; + let mut bytes = v + .texture + .as_ref() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default() + .into_bytes(); + bytes.push(0); + copy_out(&bytes, buffer.cast(), capacity, count) + }) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn loader_values_preserve_defaults_and_validate_before_io() { + unsafe { + let urdf = rpr_default_urdf_loader_options(); + let mjcf = rpr_default_mjcf_loader_options(); + let native_urdf = urdf.raw().unwrap(); + let native_mjcf = mjcf.raw().unwrap(); + assert_eq!(native_urdf.scale, UrdfLoaderOptions::default().scale); + assert_eq!( + native_mjcf.skip_plane_geoms, + MjcfLoaderOptions::default().skip_plane_geoms + ); + assert!(native_urdf.squeeze_empty_fixed_links); + for body in [ + native_urdf.rigid_body_blueprint.build(), + native_mjcf.rigid_body_blueprint.build(), + ] { + assert!(body.is_dynamic() && body.is_enabled()); + assert_eq!(body.gravity_scale(), 1.0); + } + assert_eq!(native_urdf.collider_blueprint.build().density(), 0.0); + assert_eq!(native_mjcf.collider_blueprint.build().density(), 0.0); + + let mut copy = urdf; + copy.rigidBodyBlueprint.gravityScale = 2.0; + copy.colliderBlueprint.friction = 0.9; + let converted = copy.raw().unwrap(); + assert_eq!(converted.rigid_body_blueprint.build().gravity_scale(), 2.0); + assert_eq!(converted.collider_blueprint.build().friction(), 0.9); + assert_eq!(urdf.rigidBodyBlueprint.gravityScale, 1.0); + + let path = c""; // Invalid options must fail before attempting to open a file. + copy.scale = 0.0; + assert!(rpr_urdf_robot_from_file(path.as_ptr(), ©).is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + let mut copy = mjcf; + copy.skipPlaneGeoms = 2; + assert!(rpr_mjcf_robot_from_file(path.as_ptr(), ©).is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + copy = mjcf; + copy.rigidBodyBlueprint.bodyType = u32::MAX; + assert!(copy.raw().is_err()); + + let shape = rpr_ball_shared_shape(0.75); + let mut copy = urdf; + copy.colliderBlueprint.shape.kind = RPR_SHAPE_DESC_SHARED; + copy.colliderBlueprint.shape.sharedShape = shape; + let converted = copy.raw().unwrap(); + assert_eq!(rpr_free_shared_shape(shape), RPR_OK); + assert_eq!( + converted + .collider_blueprint + .build() + .shape() + .as_ball() + .unwrap() + .radius, + 0.75 + ); + } + } + + #[test] + fn urdf_load_uses_value_blueprints() { + let path = std::env::temp_dir().join(format!("rapier-c-pod-{}.urdf", std::process::id())); + std::fs::write( + &path, + r#" + + "#, + ) + .unwrap(); + let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap(); + unsafe { + let robot = { + let mut options = rpr_default_urdf_loader_options(); + options.makeRootsFixed = 1; + options.colliderBlueprint.friction = 0.8; + rpr_urdf_robot_from_file(cpath.as_ptr(), &options) + }; // No options allocation or destructor; the loaded robot is independent. + std::fs::remove_file(path).unwrap(); + assert_eq!(rpr_last_status(), RPR_OK); + assert!(!robot.is_null()); + let world = rpr_new_world(); + let handles = rpr_urdf_robot_insert_using_impulse_joints(world, robot); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_free_urdf_robot(robot), RPR_OK); + let mut bodies = [RprRigidBodyHandle::default(); 1]; + assert_eq!( + rpr_urdf_robot_handles_bodies(handles, bodies.as_mut_ptr(), 1), + 1 + ); + assert_eq!(rpr_rigid_body_is_fixed(bodies[0]), 1); + let mut colliders = [RprColliderHandle::default(); 1]; + assert_eq!( + rpr_rigid_body_colliders(bodies[0], colliders.as_mut_ptr(), 1), + 1 + ); + assert_eq!(rpr_collider_friction(colliders[0]), 0.8); + assert_eq!(rpr_free_urdf_robot_handles(handles), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[test] + fn imported_keyframes_and_actuators_are_accessible_through_c() { + let path = std::env::temp_dir().join(format!("rapier-c-robot-{}.xml", std::process::id())); + std::fs::write( + &path, + r#" + + + + + + + "#, + ) + .unwrap(); + let path_string = std::ffi::CString::new(path.to_str().unwrap()).unwrap(); + unsafe { + let options = rpr_default_mjcf_loader_options(); + let robot = rpr_mjcf_robot_from_file(path_string.as_ptr(), &options); + assert_eq!(rpr_last_status(), RPR_OK); + assert!(!robot.is_null()); + std::fs::remove_file(path).unwrap(); + let mut count = 0; + assert_eq!( + { + let value = rpr_mjcf_robot_keyframe_count(robot); + let status = rpr_last_status(); + let destination: *mut usize = &mut count; + if !destination.is_null() { + destination.write(value); + } + status + }, + RPR_OK + ); + assert_eq!(count, 1); + let mut name = [0i8; 5]; + assert_eq!( + { + let value = + rpr_mjcf_robot_keyframe_name(robot, 0, name.as_mut_ptr(), name.len()); + let status = rpr_last_status(); + let destination: *mut usize = &mut count; + if !destination.is_null() { + destination.write(value); + } + status + }, + RPR_OK + ); + assert_eq!(CStr::from_ptr(name.as_ptr()).to_bytes(), b"home"); + let mut ctrl = [0.0]; + assert_eq!( + { + let value = + rpr_mjcf_robot_keyframe_controls(robot, 0, ctrl.as_mut_ptr(), ctrl.len()); + let status = rpr_last_status(); + let destination: *mut usize = &mut count; + if !destination.is_null() { + destination.write(value); + } + status + }, + RPR_OK + ); + assert_eq!(ctrl, [0.5]); + let mut world = RprPhysicsWorld(PhysicsWorld::new()); + let mut handles = std::ptr::null_mut(); + assert_eq!( + native_mjcf_robot_insert_using_multibody_joints( + robot, + &mut world, + 0, + &mut handles, + std::ptr::null_mut() + ), + RPR_OK + ); + assert_eq!( + { + let value = rpr_mjcf_robot_handles_actuator_count(handles); + let status = rpr_last_status(); + let destination: *mut usize = &mut count; + if !destination.is_null() { + destination.write(value); + } + status + }, + RPR_OK + ); + assert_eq!(count, 1); + assert_eq!( + native_mjcf_robot_handles_apply_keyframe(handles, &mut world, robot, 0), + RPR_OK + ); + assert_eq!( + native_mjcf_robot_handles_apply_controls_scaled( + handles, + &mut world, + ctrl.as_ptr(), + 1, + 0.25 + ), + RPR_OK + ); + for _ in 0..30 { + world.0.step(); + } + for (_, body) in world.0.bodies.iter() { + assert!(body.translation().is_finite()); + } + assert_eq!( + native_mjcf_robot_handles_apply_keyframe(handles, &mut world, robot, 1), + RPR_INVALID_ARGUMENT + ); + assert_eq!(rpr_free_mjcf_robot_handles(handles), RPR_OK); + assert_eq!(rpr_free_mjcf_robot(robot), RPR_OK); + } + } +} diff --git a/c/src/scoped_access.rs b/c/src/scoped_access.rs new file mode 100644 index 000000000..10410c62c --- /dev/null +++ b/c/src/scoped_access.rs @@ -0,0 +1,3609 @@ +//! Complete set-and-handle element access. No borrowed element pointer escapes. +use crate::handle_access::forward; +use crate::*; +/// Return a process-local geometry identity for caching, not a serializable ID. Keep a shared-shape +/// clone alive while using it as a cache key. +/// @ingroup shapes +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_shape_identity(handle: RprColliderHandle) -> usize { + let world = handle.world; + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_shape_identity( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_shape_identity( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_shape_identity( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the soft body particle count. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_num_particles(handle: RprSoftBodyHandle) -> usize { + let world = handle.world; + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_num_particles( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Return a counter that changes when particle connectivity changes; use it to invalidate mesh +/// caches. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_topology_version(handle: RprSoftBodyHandle) -> u32 { + let world = handle.world; + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_topology_version( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Return the soft body mass. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_mass(handle: RprSoftBodyHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_mass( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Return the soft body current volume. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_volume(handle: RprSoftBodyHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_volume( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Return the soft body undeformed volume. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_rest_volume(handle: RprSoftBodyHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_rest_volume( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Return the soft body target volume multiplier. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_volume_factor(handle: RprSoftBodyHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_volume_factor( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Return the soft body world-space center of mass. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_center_of_mass(handle: RprSoftBodyHandle) -> RprVector { + let world = handle.world; + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_center_of_mass( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Return the soft body root rigid-proxy handle. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_root_body(handle: RprSoftBodyHandle) -> RprRigidBodyHandle { + let world = handle.world; + ffi_world_value(world, |out: *mut RprRigidBodyHandle| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_root_body( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Return whether the soft body is enabled. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_is_enabled(handle: RprSoftBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_is_enabled( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Return whether the soft body is sleeping. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_is_sleeping(handle: RprSoftBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_is_sleeping( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +/// Copy world-space particle velocities. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_particle_velocities( + handle: RprSoftBodyHandle, + buffer: *mut RprVector, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_particle_velocities( + (element as *const SoftBody).cast(), + buffer, + capacity, + count, + )) + }) + }) +} + +/// Copy flattened edge vertex indices. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_edges( + handle: RprSoftBodyHandle, + buffer: *mut u32, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_edges( + (element as *const SoftBody).cast(), + buffer, + capacity, + count, + )) + }) + }) +} + +/// Copy flattened cell vertex indices. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_cells( + handle: RprSoftBodyHandle, + buffer: *mut u32, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_cells( + (element as *const SoftBody).cast(), + buffer, + capacity, + count, + )) + }) + }) +} + +/// Copy flattened boundary element indices. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_boundary( + handle: RprSoftBodyHandle, + buffer: *mut u32, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_boundary( + (element as *const SoftBody).cast(), + buffer, + capacity, + count, + )) + }) + }) +} + +/// Copy piece identifiers. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_pieces( + handle: RprSoftBodyHandle, + buffer: *mut RprSoftBodyHandle, + capacity: usize, +) -> usize { + let world = handle.world; + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_pieces( + (element as *const SoftBody).cast(), + buffer, + capacity, + count, + )) + }) + }) + } +} + +/// Set the soft body particle world-space velocity. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_particle_velocity( + handle: RprSoftBodyHandle, + index: usize, + value: RprVector, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_particle_velocity( + (element as *mut SoftBody).cast(), + index, + value, + )) + }) +} + +/// Set the next world-space target position of a pinned particle. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_particle_kinematic_target( + handle: RprSoftBodyHandle, + index: usize, + value: RprVector, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_particle_kinematic_target( + (element as *mut SoftBody).cast(), + index, + value, + )) + }) +} + +/// Enable or disable pinning the particle for the soft body. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_particle_pinned( + handle: RprSoftBodyHandle, + index: usize, + value: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_particle_pinned( + (element as *mut SoftBody).cast(), + index, + value, + )) + }) +} + +/// Apply a world-space impulse to one particle. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_apply_particle_impulse( + handle: RprSoftBodyHandle, + index: usize, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_apply_particle_impulse( + (element as *mut SoftBody).cast(), + index, + value, + wake_up, + )) + }) +} + +/// Accumulate a world-space force; it persists until reset. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_add_force( + handle: RprSoftBodyHandle, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_add_force( + (element as *mut SoftBody).cast(), + value, + wake_up, + )) + }) +} + +/// Apply a world-space linear impulse. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_apply_impulse( + handle: RprSoftBodyHandle, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_apply_impulse( + (element as *mut SoftBody).cast(), + value, + wake_up, + )) + }) +} + +/// Clear accumulated user forces. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_reset_forces( + handle: RprSoftBodyHandle, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_reset_forces( + (element as *mut SoftBody).cast(), + wake_up, + )) + }) +} + +/// Enable or disable the soft body. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_enabled( + handle: RprSoftBodyHandle, + value: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_enabled( + (element as *mut SoftBody).cast(), + value, + )) + }) +} + +/// Set the soft body target volume multiplier. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_volume_factor( + handle: RprSoftBodyHandle, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_volume_factor( + (element as *mut SoftBody).cast(), + value, + )) + }) +} + +/// Attach a particle to a rigid body at the supplied body-local anchor. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_attach_particle( + handle: RprSoftBodyHandle, + index: usize, + rigid_body: RprRigidBodyHandle, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + rigid_body.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + let bodies: *const RprRigidBodySet = std::ptr::addr_of!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_attach_particle( + (element as *mut SoftBody).cast(), + index, + rigid_body, + bodies, + )) + }) +} + +/// Remove a particle attachment to a rigid body. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_detach_particle( + handle: RprSoftBodyHandle, + index: usize, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_detach_particle( + (element as *mut SoftBody).cast(), + index, + )) + }) +} + +/// Copy cluster indices. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_clusters( + handle: RprSoftBodyHandle, + buffer: *mut u32, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_clusters( + (element as *const SoftBody).cast(), + buffer, + capacity, + count, + )) + }) + }) +} + +/// Return the rigid proxy for the selected cluster. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_cluster_proxy( + handle: RprSoftBodyHandle, + cluster: u32, +) -> RprRigidBodyHandle { + let world = handle.world; + ffi_world_value(world, |out: *mut RprRigidBodyHandle| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_cluster_proxy( + (element as *const SoftBody).cast(), + cluster, + out, + )) + }) + }) +} + +/// Copy particle indices for a cluster. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_cluster_particles( + handle: RprSoftBodyHandle, + cluster: u32, + buffer: *mut u32, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_cluster_particles( + (element as *const SoftBody).cast(), + cluster, + buffer, + capacity, + count, + )) + }) + }) +} + +/// Enable or disable pinning the cluster for the soft body. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_cluster_pinned( + handle: RprSoftBodyHandle, + cluster: u32, + value: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_cluster_pinned( + (element as *mut SoftBody).cast(), + cluster, + value, + )) + }) +} + +/// Set the next world-space target pose of a pinned cluster. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_cluster_kinematic_target( + handle: RprSoftBodyHandle, + cluster: u32, + value: RprPose, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_cluster_kinematic_target( + (element as *mut SoftBody).cast(), + cluster, + value, + )) + }) +} + +/// Enable or disable using cluster shape matching for the soft body. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_cluster_shape_matching_enabled( + handle: RprSoftBodyHandle, + cluster: u32, + value: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_cluster_shape_matching_enabled( + (element as *mut SoftBody).cast(), + cluster, + value, + )) + }) +} + +/// Set the soft body cluster shape-matching stiffness multiplier. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_cluster_stiffness_scale( + handle: RprSoftBodyHandle, + cluster: u32, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_cluster_stiffness_scale( + (element as *mut SoftBody).cast(), + cluster, + value, + )) + }) +} + +/// Set the soft body cluster tear-resistance multiplier. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_cluster_tear_resistance( + handle: RprSoftBodyHandle, + cluster: u32, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_cluster_tear_resistance( + (element as *mut SoftBody).cast(), + cluster, + value, + )) + }) +} + +/// Copy collision mesh metadata. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_meshes( + handle: RprSoftBodyHandle, + buffer: *mut RprSoftMeshInfo, + capacity: usize, +) -> usize { + let world = handle.world; + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_meshes( + (element as *const SoftBody).cast(), + buffer, + capacity, + count, + )) + }) + }) + } +} + +/// Copy world-space vertices for a mesh ID. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_mesh_vertices_by_id( + handle: RprSoftBodyHandle, + id: RprSoftMeshId, + buffer: *mut RprVector, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_mesh_vertices_by_id( + (element as *const SoftBody).cast(), + id, + buffer, + capacity, + count, + )) + }) + }) +} + +/// Copy flattened indices for a mesh ID. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_mesh_indices_by_id( + handle: RprSoftBodyHandle, + id: RprSoftMeshId, + buffer: *mut u32, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_mesh_indices_by_id( + (element as *const SoftBody).cast(), + id, + buffer, + capacity, + count, + )) + }) + }) +} + +/// Copy collision mesh collider handles. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_mesh_colliders( + handle: RprSoftBodyHandle, + buffer: *mut RprColliderHandle, + capacity: usize, +) -> usize { + let world = handle.world; + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_mesh_colliders( + (element as *const SoftBody).cast(), + buffer, + capacity, + count, + )) + }) + }) + } +} + +/// Copy world-space collision mesh vertices. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_mesh_vertices( + handle: RprSoftBodyHandle, + collider: RprColliderHandle, + buffer: *mut RprVector, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + collider.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_mesh_vertices( + (element as *const SoftBody).cast(), + collider, + buffer, + capacity, + count, + )) + }) + }) +} + +/// Copy flattened collision mesh indices. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_mesh_indices( + handle: RprSoftBodyHandle, + collider: RprColliderHandle, + buffer: *mut u32, + capacity: usize, +) -> usize { + let world = handle.world; + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + collider.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_mesh_indices( + (element as *const SoftBody).cast(), + collider, + buffer, + capacity, + count, + )) + }) + }) +} + +/// Return indices per collision-mesh element (2 for segments, 3 for triangles). +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_mesh_arity( + handle: RprSoftBodyHandle, + collider: RprColliderHandle, +) -> usize { + let world = handle.world; + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + collider.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_mesh_arity( + (element as *const SoftBody).cast(), + collider, + out, + )) + }) + }) +} + +/// Return the selected collision mesh topology revision for cache invalidation. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_mesh_topology_version( + handle: RprSoftBodyHandle, + collider: RprColliderHandle, +) -> u32 { + let world = handle.world; + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(world)?; + collider.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_mesh_topology_version( + (element as *const SoftBody).cast(), + collider, + out, + )) + }) + }) +} + +/// Set the soft body soft solver kind (RPR_SOFT_SOLVER_*). +/// @ingroup soft_bodies +#[cfg(feature = "fem")] +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_solver( + handle: RprSoftBodyHandle, + solver: u32, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_solver( + (element as *mut SoftBody).cast(), + solver, + )) + }) +} + +/// Set the soft body cluster shape-matching target pose. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_cluster_shape_matching_target( + handle: RprSoftBodyHandle, + cluster: u32, + target: *const RprPose, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_cluster_shape_matching_target( + (element as *mut SoftBody).cast(), + cluster, + target, + )) + }) +} + +/// Set the soft body edge tear-resistance multiplier. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_edge_tear_resistance( + handle: RprSoftBodyHandle, + index: usize, + resistance: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_edge_tear_resistance( + (element as *mut SoftBody).cast(), + index, + resistance, + )) + }) +} + +/// Return whether the selected collision mesh is closed. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_mesh_is_closed( + handle: RprSoftBodyHandle, + collider: RprColliderHandle, +) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + collider.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_mesh_is_closed( + (element as *const SoftBody).cast(), + collider, + out, + )) + }) + }) +} + +/// Set the rigid body local mass properties added to collider contributions. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_additional_mass_properties( + handle: RprRigidBodyHandle, + properties: RprMassProperties, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_additional_mass_properties( + (element as *mut RigidBody).cast(), + properties, + wake_up, + )) + }) +} + +/// Recompute body mass and inertia from attached colliders and additional mass properties. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_recompute_mass_properties_from_colliders( + handle: RprRigidBodyHandle, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + let colliders: *const RprColliderSet = std::ptr::addr_of!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_recompute_mass_properties_from_colliders( + (element as *mut RigidBody).cast(), + colliders, + )) + }) +} + +/// Set the collider local mass properties. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_mass_properties( + handle: RprColliderHandle, + properties: RprMassProperties, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_mass_properties( + (element as *mut Collider).cast(), + properties, + )) + }) +} + +/// Return the collider local mass properties. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_mass_properties( + handle: RprColliderHandle, +) -> RprMassProperties { + let world = handle.world; + ffi_value(|out: *mut RprMassProperties| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_mass_properties( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_mass_properties( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprMassProperties, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_mass_properties( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Set the rigid body translation/rotation lock bitmask. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_locked_axes( + handle: RprRigidBodyHandle, + axes: u8, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_locked_axes( + (element as *mut RigidBody).cast(), + axes, + wake_up, + )) + }) +} + +/// Return the rigid body translation/rotation lock bitmask. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_locked_axes(handle: RprRigidBodyHandle) -> u8 { + let world = handle.world; + ffi_value(|out: *mut u8| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_locked_axes( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_locked_axes( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut u8, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_locked_axes( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return whether the collider is a voxel shape. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_is_voxels(handle: RprColliderHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_is_voxels( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_is_voxels( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_is_voxels( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return voxel information at a flat index; found = 0 if absent. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_voxel_at_flat_id( + handle: RprColliderHandle, + id: u32, +) -> RprVoxelQuery { + let world = handle.world; + ffi_value(|result: *mut RprVoxelQuery| { + let key = unsafe { std::ptr::addr_of_mut!((*result).key) }; + let center = unsafe { std::ptr::addr_of_mut!((*result).center) }; + let size = unsafe { std::ptr::addr_of_mut!((*result).size) }; + let found = unsafe { std::ptr::addr_of_mut!((*result).found) }; + + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_voxel_at_flat_id( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + id, + key, + center, + size, + found, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_voxel_at_flat_id( + set: *const RprColliderSet, + handle: RprColliderHandle, + id: u32, + key: *mut RprVoxelKey, + center: *mut RprVector, + size: *mut RprVector, + found: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_voxel_at_flat_id( + (element as *const Collider).cast(), + id, + key, + center, + size, + found, + )) + }) +} +/// Fill or clear the voxel at key; the collider must have a voxel shape. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_voxel( + handle: RprColliderHandle, + key: RprVoxelKey, + filled: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_voxel( + (element as *mut Collider).cast(), + key, + filled, + )) + }) +} + +/// Return the rigid body next kinematic world-space pose. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_next_position(handle: RprRigidBodyHandle) -> RprPose { + let world = handle.world; + ffi_value(|out: *mut RprPose| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_next_position( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_next_position( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprPose, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_next_position( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body world-space rotation. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_rotation(handle: RprRigidBodyHandle) -> RprRotation { + let world = handle.world; + ffi_value(|out: *mut RprRotation| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_rotation( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_rotation( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprRotation, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_rotation( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body world-space center of mass. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_center_of_mass(handle: RprRigidBodyHandle) -> RprVector { + let world = handle.world; + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_center_of_mass( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_center_of_mass( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_center_of_mass( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body body-local center of mass. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_local_center_of_mass( + handle: RprRigidBodyHandle, +) -> RprVector { + let world = handle.world; + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_local_center_of_mass( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_local_center_of_mass( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_local_center_of_mass( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body accumulated user-applied world-space force. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_user_force(handle: RprRigidBodyHandle) -> RprVector { + let world = handle.world; + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_user_force( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_user_force( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_user_force( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body accumulated user-applied world-space torque. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_user_torque(handle: RprRigidBodyHandle) -> RprAngVector { + let world = handle.world; + ffi_value(|out: *mut RprAngVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_user_torque( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_user_torque( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprAngVector, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_user_torque( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body body type (RPR_DYNAMIC, RPR_FIXED, or a kinematic kind). +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_body_type(handle: RprRigidBodyHandle) -> u32 { + let world = handle.world; + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_body_type( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_body_type( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_body_type( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body mass. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_mass(handle: RprRigidBodyHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_mass( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_mass( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_mass( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body gravity multiplier. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_gravity_scale(handle: RprRigidBodyHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_gravity_scale( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_gravity_scale( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_gravity_scale( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body linear damping coefficient. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_linear_damping(handle: RprRigidBodyHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_linear_damping( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_linear_damping( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_linear_damping( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body angular damping coefficient. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_angular_damping(handle: RprRigidBodyHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_angular_damping( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_angular_damping( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_angular_damping( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body kinetic energy. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_kinetic_energy(handle: RprRigidBodyHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_kinetic_energy( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_kinetic_energy( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_kinetic_energy( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the rigid body soft-CCD prediction distance. +/// @ingroup soft_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_soft_ccd_prediction(handle: RprRigidBodyHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_soft_ccd_prediction( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_soft_ccd_prediction( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_soft_ccd_prediction( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return whether the rigid body is using continuous collision detection. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_is_ccd_enabled(handle: RprRigidBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_is_ccd_enabled( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_is_ccd_enabled( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_is_ccd_enabled( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return whether the rigid body is dynamic. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_is_dynamic(handle: RprRigidBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_is_dynamic( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_is_dynamic( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_is_dynamic( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return the associated soft-body handle, or an invalid handle if this is not a soft proxy. +/// @ingroup soft_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_soft_body(handle: RprRigidBodyHandle) -> RprSoftBodyHandle { + let world = handle.world; + ffi_world_value(world, |out: *mut RprSoftBodyHandle| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_soft_body( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_soft_body( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprSoftBodyHandle, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_soft_body( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return whether the rigid body is a soft-body proxy. +/// @ingroup soft_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_is_soft_frame(handle: RprRigidBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_is_soft_frame( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_is_soft_frame( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_is_soft_frame( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return whether the rigid body is fixed. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_is_fixed(handle: RprRigidBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_is_fixed( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_is_fixed( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_is_fixed( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return whether the rigid body is kinematic. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_is_kinematic(handle: RprRigidBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_is_kinematic( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_is_kinematic( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_is_kinematic( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return whether the rigid body is moving. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_is_moving(handle: RprRigidBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_is_moving( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_is_moving( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_is_moving( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Return whether the rigid body is currently using CCD for its motion. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_is_ccd_active(handle: RprRigidBodyHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_is_ccd_active( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_is_ccd_active( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_is_ccd_active( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Set the rigid body world-space rotation. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_rotation( + handle: RprRigidBodyHandle, + value: RprRotation, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_rotation( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Set the rigid body body type (RPR_DYNAMIC, RPR_FIXED, or a kinematic kind). +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_body_type( + handle: RprRigidBodyHandle, + value: u32, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_body_type( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Set the rigid body next kinematic world-space rotation. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_next_kinematic_rotation( + handle: RprRigidBodyHandle, + value: RprRotation, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_next_kinematic_rotation( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Set the rigid body mass added to collider contributions. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_additional_mass( + handle: RprRigidBodyHandle, + value: RprReal, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_additional_mass( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Set the rigid body soft-CCD prediction distance. +/// @ingroup soft_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_soft_ccd_prediction( + handle: RprRigidBodyHandle, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_soft_ccd_prediction( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Enable or disable using continuous collision detection for the rigid body. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_ccd_enabled( + handle: RprRigidBodyHandle, + value: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_ccd_enabled( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Enable or disable locking translation for the rigid body. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_translations_locked( + handle: RprRigidBodyHandle, + value: RprBool, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_translations_locked( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Enable or disable locking rotation for the rigid body. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_rotations_locked( + handle: RprRigidBodyHandle, + value: RprBool, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_rotations_locked( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Set the rigid body signed dominance group. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_dominance_group( + handle: RprRigidBodyHandle, + value: i8, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_dominance_group( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Set the rigid body additional solver iterations for connected bodies. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_additional_solver_iterations( + handle: RprRigidBodyHandle, + value: usize, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_additional_solver_iterations( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Set the rigid body additional PGS iterations. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_additional_pgs_iterations( + handle: RprRigidBodyHandle, + value: usize, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_additional_pgs_iterations( + (element as *mut RigidBody).cast(), + value, + )) + }) +} + +/// Accumulate a world-space torque; it persists until reset. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_add_torque( + handle: RprRigidBodyHandle, + value: RprAngVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_add_torque( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Apply a world-space angular impulse. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_apply_torque_impulse( + handle: RprRigidBodyHandle, + value: RprAngVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_apply_torque_impulse( + (element as *mut RigidBody).cast(), + value, + wake_up, + )) + }) +} + +/// Accumulate a world-space force applied at a world-space point. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_add_force_at_point( + handle: RprRigidBodyHandle, + value: RprVector, + point: RprVector, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_add_force_at_point( + (element as *mut RigidBody).cast(), + value, + point, + wake_up, + )) + }) +} + +/// Clear accumulated user torques. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_reset_torques( + handle: RprRigidBodyHandle, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_reset_torques( + (element as *mut RigidBody).cast(), + wake_up, + )) + }) +} + +/// Return world-space velocity at a world-space point, including angular motion. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_velocity_at_point( + handle: RprRigidBodyHandle, + point: RprVector, +) -> RprVector { + let world = handle.world; + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_velocity_at_point( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + point, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_velocity_at_point( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + point: RprVector, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_velocity_at_point( + (element as *const RigidBody).cast(), + point, + out, + )) + }) +} +/// Copy attached collider handles. +/// @see @ref output_buffers +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_colliders( + handle: RprRigidBodyHandle, + buffer: *mut RprColliderHandle, + capacity: usize, +) -> usize { + let world = handle.world; + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_colliders( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + buffer, + capacity, + count, + )) + }) + }) + } +} + +pub(crate) unsafe fn native_rigid_body_set_get_colliders( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + buffer: *mut RprColliderHandle, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_colliders( + (element as *const RigidBody).cast(), + buffer, + capacity, + count, + )) + }) +} +/// Return whether the rigid body is using gyroscopic forces. +/// @ingroup rigid_bodies +#[cfg(feature = "dim3")] +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_gyroscopic_forces_enabled( + handle: RprRigidBodyHandle, +) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_gyroscopic_forces_enabled( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} +#[cfg(feature = "dim3")] +pub(crate) unsafe fn native_rigid_body_set_get_gyroscopic_forces_enabled( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_gyroscopic_forces_enabled( + (element as *const RigidBody).cast(), + out, + )) + }) +} +/// Enable or disable using gyroscopic forces for the rigid body. +/// @ingroup rigid_bodies +#[cfg(feature = "dim3")] +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_gyroscopic_forces_enabled( + handle: RprRigidBodyHandle, + enabled: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_gyroscopic_forces_enabled( + (element as *mut RigidBody).cast(), + enabled, + )) + }) +} + +/// Set the collider mass per unit volume. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_density( + handle: RprColliderHandle, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_density( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider mass. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_mass( + handle: RprColliderHandle, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_mass( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Enable or disable the collider. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_enabled( + handle: RprColliderHandle, + value: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_enabled( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider contact-force filtering groups. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_solver_groups( + handle: RprColliderHandle, + value: RprInteractionGroups, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_solver_groups( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider friction combination rule (RPR_COMBINE_*). +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_friction_combine_rule( + handle: RprColliderHandle, + value: u32, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_friction_combine_rule( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider restitution combination rule (RPR_COMBINE_*). +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_restitution_combine_rule( + handle: RprColliderHandle, + value: u32, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_restitution_combine_rule( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider extra separation skin around the shape. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_contact_skin( + handle: RprColliderHandle, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_contact_skin( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider force threshold for contact-force events. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_contact_force_event_threshold( + handle: RprColliderHandle, + value: RprReal, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_contact_force_event_threshold( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider event-generation bitmask (RPR_COLLISION_EVENTS and RPR_CONTACT_FORCE_EVENTS). +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_active_events( + handle: RprColliderHandle, + value: u32, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_active_events( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider physics-hook activation bitmask. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_active_hooks( + handle: RprColliderHandle, + value: u32, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_active_hooks( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Set the collider body-type collision activation bitmask. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_active_collision_types( + handle: RprColliderHandle, + value: u16, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_active_collision_types( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Return the collider world-space rotation. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_rotation(handle: RprColliderHandle) -> RprRotation { + let world = handle.world; + ffi_value(|out: *mut RprRotation| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_rotation( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_rotation( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprRotation, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_rotation( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the collider collision filtering groups. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_collision_groups( + handle: RprColliderHandle, +) -> RprInteractionGroups { + let world = handle.world; + ffi_value(|out: *mut RprInteractionGroups| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_collision_groups( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_collision_groups( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprInteractionGroups, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_collision_groups( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the collider contact-force filtering groups. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_solver_groups( + handle: RprColliderHandle, +) -> RprInteractionGroups { + let world = handle.world; + ffi_value(|out: *mut RprInteractionGroups| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_solver_groups( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_solver_groups( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprInteractionGroups, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_solver_groups( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the collider application-owned 128-bit user value. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_user_data(handle: RprColliderHandle) -> RprUserData { + let world = handle.world; + ffi_value(|out: *mut RprUserData| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_user_data( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_user_data( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprUserData, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_user_data( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the collider event-generation bitmask (RPR_COLLISION_EVENTS and +/// RPR_CONTACT_FORCE_EVENTS). +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_active_events(handle: RprColliderHandle) -> u32 { + let world = handle.world; + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_active_events( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_active_events( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_active_events( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the collider mass. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_mass(handle: RprColliderHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_mass( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_mass( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_mass( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the collider mass per unit volume. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_density(handle: RprColliderHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_density( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_density( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_density( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the collider current volume. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_volume(handle: RprColliderHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_volume( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_volume( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_volume( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the collider extra separation skin around the shape. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_contact_skin(handle: RprColliderHandle) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_contact_skin( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_contact_skin( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_contact_skin( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the collider force threshold for contact-force events. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_contact_force_event_threshold( + handle: RprColliderHandle, +) -> RprReal { + let world = handle.world; + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_contact_force_event_threshold( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_contact_force_event_threshold( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_contact_force_event_threshold( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return whether the collider is enabled. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_is_enabled(handle: RprColliderHandle) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_is_enabled( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_is_enabled( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_is_enabled( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the current world-space axis-aligned bounds. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_compute_aabb(handle: RprColliderHandle) -> RprAabb { + let world = handle.world; + ffi_value(|out: *mut RprAabb| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_compute_aabb( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_compute_aabb( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprAabb, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_compute_aabb( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return an owned wrapper sharing the collider geometry. Release with rpr_free_shared_shape. +/// Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. +/// @ingroup shapes +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_clone_shape( + handle: RprColliderHandle, +) -> *mut RprSharedShape { + let world = handle.world; + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_shared_shape( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_shared_shape( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut *mut RprSharedShape, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_shared_shape( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Replace collider geometry by sharing shape; the supplied wrapper is not consumed. +/// @ingroup shapes +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_shape( + handle: RprColliderHandle, + shape: *const RprSharedShape, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_shape( + (element as *mut Collider).cast(), + shape, + )) + }) +} + +/// Set the collider pose relative to the parent rigid body. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_position_wrt_parent( + handle: RprColliderHandle, + value: RprPose, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_position_wrt_parent( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Validate the index and generation in the live owning world. Cannot detect a world pointer that +/// has already been freed. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_validate_handle(handle: RprRigidBodyHandle) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_validate_handle( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + )) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_validate_handle( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, +) -> RprStatus { + ffi(|| unsafe { + get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + Ok(()) + }) +} +/// Validate the index and generation in the live owning world. Cannot detect a world pointer that +/// has already been freed. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_validate_handle(handle: RprColliderHandle) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_validate_handle( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + )) + }) +} + +pub(crate) unsafe fn native_collider_set_validate_handle( + set: *const RprColliderSet, + handle: RprColliderHandle, +) -> RprStatus { + ffi(|| unsafe { + get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + Ok(()) + }) +} +/// Validate the index and generation in the live owning world. Cannot detect a world pointer that +/// has already been freed. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_validate_handle(handle: RprSoftBodyHandle) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + Ok(()) + }) +} diff --git a/c/src/shape_desc.rs b/c/src/shape_desc.rs new file mode 100644 index 000000000..5081c1996 --- /dev/null +++ b/c/src/shape_desc.rs @@ -0,0 +1,206 @@ +//! POD shape constructors; input geometry is borrowed until insertion. +use crate::*; +/// Return a rounded box description; half_extents exclude the added border_radius. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[rapier_export] +pub extern "C" fn rpr_round_cuboid_collider_desc( + half_extents: RprVector, + border_radius: RprReal, +) -> RprColliderDesc { + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_ROUND_CUBOID, + a: half_extents, + radius: border_radius, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} +/// Return a capsule description with segment endpoints a/b and the supplied radius. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[rapier_export] +pub extern "C" fn rpr_capsule_collider_desc( + a: RprVector, + b: RprVector, + radius: RprReal, +) -> RprColliderDesc { + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_CAPSULE, + a, + b, + radius, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} +/// Return a segment description with endpoints a and b. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[rapier_export] +pub extern "C" fn rpr_segment_collider_desc(a: RprVector, b: RprVector) -> RprColliderDesc { + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_SEGMENT, + a, + b, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} +/// Return a triangle description with vertices a, b, and c. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[rapier_export] +pub extern "C" fn rpr_triangle_collider_desc( + a: RprVector, + b: RprVector, + c: RprVector, +) -> RprColliderDesc { + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_TRIANGLE, + a, + b, + c, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} +/// Return a half-space description bounded by a plane through the origin; normal points outward. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[rapier_export] +pub extern "C" fn rpr_halfspace_collider_desc(normal: RprVector) -> RprColliderDesc { + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_HALFSPACE, + a: normal, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} +/// Return a Y-aligned cylinder description with the supplied half-height and radius. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_cylinder_collider_desc( + half_height: RprReal, + radius: RprReal, +) -> RprColliderDesc { + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_CYLINDER, + halfHeight: half_height, + radius, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} +/// Return a Y-aligned cone description with the supplied half-height and base radius. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_cone_collider_desc(half_height: RprReal, radius: RprReal) -> RprColliderDesc { + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_CONE, + halfHeight: half_height, + radius, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} +/// Return a rounded Y-aligned cylinder description; dimensions exclude border_radius. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_round_cylinder_collider_desc( + half_height: RprReal, + radius: RprReal, + border_radius: RprReal, +) -> RprColliderDesc { + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_ROUND_CYLINDER, + halfHeight: half_height, + radius, + borderRadius: border_radius, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} +/// Return a X-aligned capsule description; half_height is half the segment length, excluding caps. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[rapier_export] +pub extern "C" fn rpr_capsule_x_collider_desc( + half_height: RprReal, + radius: RprReal, +) -> RprColliderDesc { + let axis = Vector::X * half_height; + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_CAPSULE, + a: (-axis).into(), + b: axis.into(), + radius, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} +/// Return a Y-aligned capsule description; half_height is half the segment length, excluding caps. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[rapier_export] +pub extern "C" fn rpr_capsule_y_collider_desc( + half_height: RprReal, + radius: RprReal, +) -> RprColliderDesc { + let axis = Vector::Y * half_height; + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_CAPSULE, + a: (-axis).into(), + b: axis.into(), + radius, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} +/// Return a Z-aligned capsule description; half_height is half the segment length, excluding caps. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_capsule_z_collider_desc( + half_height: RprReal, + radius: RprReal, +) -> RprColliderDesc { + let axis = Vector::Z * half_height; + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_CAPSULE, + a: (-axis).into(), + b: axis.into(), + radius, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} diff --git a/c/src/soft_body.rs b/c/src/soft_body.rs new file mode 100644 index 000000000..648f70b9a --- /dev/null +++ b/c/src/soft_body.rs @@ -0,0 +1,1295 @@ +use crate::*; + +/// Remove a soft body and its associated simulation objects. Invalidates its handle. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_remove_soft_body(handle: RprSoftBodyHandle) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + let islands: *mut RprIslandManager = std::ptr::addr_of_mut!((*raw).0.islands).cast(); + let bodies: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + let colliders: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + let impulse_joints: *mut RprImpulseJointSet = + std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + let multibody_joints: *mut RprMultibodyJointSet = + std::ptr::addr_of_mut!((*raw).0.multibody_joints).cast(); + + get_mut(set)? + .0 + .remove( + handle.raw(), + &mut get_mut(islands)?.0, + &mut get_mut(bodies)?.0, + &mut get_mut(colliders)?.0, + &mut get_mut(impulse_joints)?.0, + &mut get_mut(multibody_joints)?.0, + ) + .ok_or_else(missing)?; + Ok(()) + }) +} + +pub(crate) unsafe fn native_soft_body_num_particles( + body: *const RprSoftBody, + out: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + output(out, b.num_particles()) + }) +} +pub(crate) unsafe fn native_soft_body_topology_version( + body: *const RprSoftBody, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + output(out, b.topology_version()) + }) +} +pub(crate) unsafe fn native_soft_body_mass( + body: *const RprSoftBody, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + output(out, b.mass()) + }) +} +pub(crate) unsafe fn native_soft_body_volume( + body: *const RprSoftBody, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + output(out, b.volume()) + }) +} +pub(crate) unsafe fn native_soft_body_rest_volume( + body: *const RprSoftBody, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + output(out, b.rest_volume()) + }) +} +pub(crate) unsafe fn native_soft_body_volume_factor( + body: *const RprSoftBody, + out: *mut RprReal, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + output(out, b.volume_factor()) + }) +} +pub(crate) unsafe fn native_soft_body_center_of_mass( + body: *const RprSoftBody, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + output(out, b.center_of_mass().into()) + }) +} +pub(crate) unsafe fn native_soft_body_root_body( + body: *const RprSoftBody, + out: *mut RprRigidBodyHandle, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + output(out, b.root_body().into()) + }) +} +pub(crate) unsafe fn native_soft_body_is_enabled( + body: *const RprSoftBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + output(out, b.is_enabled() as u32) + }) +} +pub(crate) unsafe fn native_soft_body_is_sleeping( + body: *const RprSoftBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + output(out, b.is_sleeping() as u32) + }) +} +pub(crate) unsafe fn native_soft_body_particle_positions( + body: *const RprSoftBody, + buffer: *mut RprVector, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let values: Vec<_> = get(body)?.0.particle_positions().map(Into::into).collect(); + copy_out(&values, buffer, capacity, count) + }) +} +pub(crate) unsafe fn native_soft_body_particle_velocities( + body: *const RprSoftBody, + buffer: *mut RprVector, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let values: Vec<_> = get(body)?.0.particle_velocities().map(Into::into).collect(); + copy_out(&values, buffer, capacity, count) + }) +} +/// Flat particle indices; count and capacity are numbers of uint32_t elements. +pub(crate) unsafe fn native_soft_body_edges( + body: *const RprSoftBody, + buffer: *mut u32, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + let values: Vec<_> = b.edges().iter().flat_map(|v| v.vertices).collect(); + copy_out(&values, buffer, capacity, count) + }) +} +/// Flat particle indices; count and capacity are numbers of uint32_t elements. +pub(crate) unsafe fn native_soft_body_cells( + body: *const RprSoftBody, + buffer: *mut u32, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + let values: Vec<_> = b.cells().iter().flat_map(|v| v.vertices).collect(); + copy_out(&values, buffer, capacity, count) + }) +} +/// Flat particle indices; count and capacity are numbers of uint32_t elements. +pub(crate) unsafe fn native_soft_body_boundary( + body: *const RprSoftBody, + buffer: *mut u32, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let b = &get(body)?.0; + let values: Vec<_> = b.boundary().iter().flatten().copied().collect(); + copy_out(&values, buffer, capacity, count) + }) +} +pub(crate) unsafe fn native_soft_body_pieces( + body: *const RprSoftBody, + buffer: *mut RprSoftBodyHandle, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let values: Vec<_> = get(body)? + .0 + .pieces() + .iter() + .copied() + .map(Into::into) + .collect(); + copy_out(&values, buffer, capacity, count) + }) +} +pub(crate) unsafe fn native_soft_body_set_particle_position( + body: *mut RprSoftBody, + index: usize, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let b = &mut get_mut(body)?.0; + ensure(index < b.num_particles(), "particle index out of range")?; + b.set_particle_position(index, value); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_set_particle_velocity( + body: *mut RprSoftBody, + index: usize, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let b = &mut get_mut(body)?.0; + ensure(index < b.num_particles(), "particle index out of range")?; + b.set_particle_velocity(index, value); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_set_particle_kinematic_target( + body: *mut RprSoftBody, + index: usize, + value: RprVector, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let b = &mut get_mut(body)?.0; + ensure(index < b.num_particles(), "particle index out of range")?; + b.set_particle_kinematic_target(index, value); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_set_particle_pinned( + body: *mut RprSoftBody, + index: usize, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + let b = &mut get_mut(body)?.0; + ensure(index < b.num_particles(), "particle index out of range")?; + b.set_particle_pinned(index, value); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_add_particle_force( + body: *mut RprSoftBody, + index: usize, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let wake_up = boolean(wake_up)?; + let b = &mut get_mut(body)?.0; + ensure(index < b.num_particles(), "particle index out of range")?; + b.add_particle_force(index, value, wake_up); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_apply_particle_impulse( + body: *mut RprSoftBody, + index: usize, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let wake_up = boolean(wake_up)?; + let b = &mut get_mut(body)?.0; + ensure(index < b.num_particles(), "particle index out of range")?; + b.apply_particle_impulse(index, value, wake_up); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_add_force( + body: *mut RprSoftBody, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let wake_up = boolean(wake_up)?; + get_mut(body)?.0.add_force(value, wake_up); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_apply_impulse( + body: *mut RprSoftBody, + value: RprVector, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let wake_up = boolean(wake_up)?; + get_mut(body)?.0.apply_impulse(value, wake_up); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_reset_forces( + body: *mut RprSoftBody, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let wake_up = boolean(wake_up)?; + get_mut(body)?.0.reset_forces(wake_up); + Ok(()) + }) +} +/// Wake the soft body and its rigid proxies. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_wake_up(handle: RprSoftBodyHandle) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + get_mut(set)? + .0 + .get_mut(handle.raw()) + .ok_or_else(missing)? + .wake_up(); + Ok(()) + }) +} + +pub(crate) unsafe fn native_soft_body_set_enabled( + body: *mut RprSoftBody, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + get_mut(body)?.0.set_enabled(value); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_set_volume_factor( + body: *mut RprSoftBody, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + positive(value)?; + get_mut(body)?.0.set_volume_factor(value); + Ok(()) + }) +} + +pub(crate) unsafe fn native_soft_body_attach_particle( + body: *mut RprSoftBody, + index: usize, + rigid_body: RprRigidBodyHandle, + bodies: *const RprRigidBodySet, +) -> RprStatus { + ffi(|| unsafe { + let bodies = get(bodies)?; + let rb = bodies.0.get(rigid_body.raw()).ok_or_else(missing)?; + ensure( + rb.soft_body().is_none(), + "attachment target must be a rigid body", + )?; + let b = &mut get_mut(body)?.0; + ensure(index < b.num_particles(), "particle index out of range")?; + b.attach_particle(index, rigid_body.raw(), &bodies.0); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_detach_particle( + body: *mut RprSoftBody, + index: usize, +) -> RprStatus { + ffi(|| unsafe { + let b = &mut get_mut(body)?.0; + ensure(index < b.num_particles(), "particle index out of range")?; + b.detach_particle(index); + Ok(()) + }) +} + +/// Release an owned soft body tear event. NULL is allowed. Do not pass borrowed pointers or free +/// the object twice. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_free_soft_body_tear_event( + event: *mut RprSoftBodyTearEvent, +) -> RprStatus { + ffi(|| unsafe { + if !event.is_null() { + get(event)?; + drop(Box::from_raw(event)); + } + Ok(()) + }) +} +/// Return the source soft-body handle for this tear event. +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_soft_body( + event: *const RprSoftBodyTearEvent, +) -> RprSoftBodyHandle { + ffi_world_value( + unsafe { get(event).map_or(std::ptr::null_mut(), |e| e.1) }, + |out: *mut RprSoftBodyHandle| { + ffi(|| unsafe { output(out, get(event)?.0.soft_body.into()) }) + }, + ) +} +/// Copy the soft-body handles produced by the tear. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_bodies( + event: *const RprSoftBodyTearEvent, + buffer: *mut RprSoftBodyHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array( + get(event).map_or(std::ptr::null_mut(), |e| e.1), + buffer, + capacity, + |count: *mut usize| { + ffi(|| { + let v: Vec<_> = get(event)?.0.bodies().map(Into::into).collect(); + copy_out(&v, buffer, capacity, count) + }) + }, + ) + } +} +/// Return the destination body and particle index for an original particle. +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_particle_destination( + event: *const RprSoftBodyTearEvent, + particle: u32, +) -> RprParticleDestination { + ffi_world_value( + unsafe { get(event).map_or(std::ptr::null_mut(), |e| e.1) }, + |result: *mut RprParticleDestination| { + let body = unsafe { std::ptr::addr_of_mut!((*result).body) }; + let index = unsafe { std::ptr::addr_of_mut!((*result).index) }; + + ffi(|| unsafe { + out_ptr(body)?; + out_ptr(index)?; + let (b, i) = get(event)? + .0 + .particle_destination(particle) + .ok_or((RPR_NOT_FOUND, "particle has no destination".into()))?; + output(body, b.into())?; + output(index, i) + }) + }, + ) +} +/// Flat indices; element arity follows the corresponding Rust event field. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_torn_edges( + event: *const RprSoftBodyTearEvent, + buffer: *mut u32, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let e = &get(event)?.0; + let v: Vec<_> = e.torn_edges.iter().flatten().copied().collect(); + copy_out(&v, buffer, capacity, count) + }) + }) +} +/// Flat indices; element arity follows the corresponding Rust event field. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_torn_cells( + event: *const RprSoftBodyTearEvent, + buffer: *mut u32, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let e = &get(event)?.0; + let v: Vec<_> = e.torn_cells.iter().flatten().copied().collect(); + copy_out(&v, buffer, capacity, count) + }) + }) +} +/// Flat indices; element arity follows the corresponding Rust event field. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_removed_edges( + event: *const RprSoftBodyTearEvent, + buffer: *mut u32, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let e = &get(event)?.0; + let v: Vec<_> = e.removed_edges.iter().flatten().copied().collect(); + copy_out(&v, buffer, capacity, count) + }) + }) +} +/// Flat indices; element arity follows the corresponding Rust event field. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_split_particles( + event: *const RprSoftBodyTearEvent, + buffer: *mut u32, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let e = &get(event)?.0; + let v: Vec<_> = e + .split_particles + .iter() + .flat_map(|&(a, b)| [a, b]) + .collect(); + copy_out(&v, buffer, capacity, count) + }) + }) +} +/// Flat indices; element arity follows the corresponding Rust event field. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_inserted_particles( + event: *const RprSoftBodyTearEvent, + buffer: *mut u32, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let e = &get(event)?.0; + let v: Vec<_> = e.inserted_particles.clone(); + copy_out(&v, buffer, capacity, count) + }) + }) +} +/// Copy original particle indices belonging to a resulting piece. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_piece_particles( + event: *const RprSoftBodyTearEvent, + piece_index: usize, + buffer: *mut u32, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let p = get(event)? + .0 + .pieces + .get(piece_index) + .ok_or_else(|| invalid("piece index out of range"))?; + copy_out(&p.particles, buffer, capacity, count) + }) + }) +} +/// Cluster and proxy remapping after a soft-body split. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprSoftClusterSplit { + /// Original cluster index before splitting. + pub source_cluster: u32, + /// Resulting soft-body handle. + pub soft_body: RprSoftBodyHandle, + /// Cluster index within its soft body. + pub cluster: u32, + /// Cluster rigid-proxy body handle. + pub proxy: RprRigidBodyHandle, + /// Whether this split retains the original proxy. + pub keeps_proxy: RprBool, +} +/// Impulse-joint movement between rigid proxies after tearing. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprSoftJointMove { + /// Impulse joint that moved between proxies. + pub joint: RprImpulseJointHandle, + /// Original rigid-proxy body. + pub from: RprRigidBodyHandle, + /// Destination rigid-proxy body. + pub to: RprRigidBodyHandle, +} +/// Copy cluster-to-piece and rigid-proxy remapping records. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_clusters( + event: *const RprSoftBodyTearEvent, + buffer: *mut RprSoftClusterSplit, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array( + get(event).map_or(std::ptr::null_mut(), |e| e.1), + buffer, + capacity, + |count: *mut usize| { + ffi(|| { + let v: Vec<_> = get(event)? + .0 + .clusters + .iter() + .map(|e| RprSoftClusterSplit { + source_cluster: e.source_cluster, + soft_body: e.soft_body.into(), + cluster: e.cluster, + proxy: e.proxy.into(), + keeps_proxy: e.keeps_proxy as u32, + }) + .collect(); + copy_out(&v, buffer, capacity, count) + }) + }, + ) + } +} +/// Copy impulse-joint remapping records produced by the tear. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_moved_joints( + event: *const RprSoftBodyTearEvent, + buffer: *mut RprSoftJointMove, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array( + get(event).map_or(std::ptr::null_mut(), |e| e.1), + buffer, + capacity, + |count: *mut usize| { + ffi(|| { + let v: Vec<_> = get(event)? + .0 + .moved_joints + .iter() + .map(|e| RprSoftJointMove { + joint: e.joint.into(), + from: e.from.into(), + to: e.to.into(), + }) + .collect(); + copy_out(&v, buffer, capacity, count) + }) + }, + ) + } +} + +/// Tear the selected edges and return an owned remapping event. Release it with +/// rpr_free_soft_body_tear_event. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_tear( + handle: RprSoftBodyHandle, + edges: *const u32, + edge_count: usize, + cells: *const u32, + cell_count: usize, +) -> *mut RprSoftBodyTearEvent { + let world = handle.world; + ffi_value(|out: *mut *mut RprSoftBodyTearEvent| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + let islands: *mut RprIslandManager = std::ptr::addr_of_mut!((*raw).0.islands).cast(); + let bodies: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + let colliders: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + let impulse_joints: *mut RprImpulseJointSet = + std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + let multibody_joints: *mut RprMultibodyJointSet = + std::ptr::addr_of_mut!((*raw).0.multibody_joints).cast(); + + out_ptr(out)?; + let edges = input(edges, edge_count)?; + let cells = input(cells, cell_count)?; + let set = get_mut(set)?; + let b = set.0.get(handle.raw()).ok_or_else(missing)?; + ensure( + edges.iter().all(|&i| (i as usize) < b.edges().len()) + && cells.iter().all(|&i| (i as usize) < b.cells().len()), + "element index out of range", + )?; + let e = set.0.tear( + handle.raw(), + edges, + cells, + &mut get_mut(islands)?.0, + &mut get_mut(bodies)?.0, + &mut get_mut(colliders)?.0, + &mut get_mut(impulse_joints)?.0, + &mut get_mut(multibody_joints)?.0, + ); + output( + out, + e.map(|e| Box::into_raw(Box::new(RprSoftBodyTearEvent(e, handle.world)))) + .unwrap_or(std::ptr::null_mut()), + ) + }) + }) +} + +/// Create a rigid proxy cluster from the supplied particle indices and return its cluster index. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_add_cluster( + handle: RprSoftBodyHandle, + particles: *const u32, + count: usize, +) -> u32 { + let world = handle.world; + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + let bodies: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + let colliders: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + out_ptr(out)?; + let p = input(particles, count)?; + let s = get_mut(set)?; + let b = s.0.get(handle.raw()).ok_or_else(missing)?; + ensure( + !p.is_empty() && p.iter().all(|&i| (i as usize) < b.num_particles()), + "invalid cluster particles", + )?; + let id = + s.0.add_cluster( + handle.raw(), + p, + &mut get_mut(bodies)?.0, + &mut get_mut(colliders)?.0, + ) + .ok_or_else(|| invalid("cluster could not be created"))?; + output(out, id) + }) + }) +} + +/// Remove the selected cluster and its rigid proxy. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_remove_cluster( + handle: RprSoftBodyHandle, + cluster: u32, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + let islands: *mut RprIslandManager = std::ptr::addr_of_mut!((*raw).0.islands).cast(); + let bodies: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + let colliders: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + let impulse_joints: *mut RprImpulseJointSet = + std::ptr::addr_of_mut!((*raw).0.impulse_joints).cast(); + let multibody_joints: *mut RprMultibodyJointSet = + std::ptr::addr_of_mut!((*raw).0.multibody_joints).cast(); + + let s = get_mut(set)?; + s.0.get(handle.raw()).ok_or_else(missing)?; + s.0.remove_cluster( + handle.raw(), + cluster, + &mut get_mut(islands)?.0, + &mut get_mut(bodies)?.0, + &mut get_mut(colliders)?.0, + &mut get_mut(impulse_joints)?.0, + &mut get_mut(multibody_joints)?.0, + ) + .ok_or_else(|| invalid("invalid cluster"))?; + Ok(()) + }) +} + +pub(crate) unsafe fn native_soft_body_clusters( + body: *const RprSoftBody, + buffer: *mut u32, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let v: Vec<_> = get(body)?.0.live_clusters().map(|(i, _)| i).collect(); + copy_out(&v, buffer, capacity, count) + }) +} +pub(crate) unsafe fn native_soft_body_cluster_proxy( + body: *const RprSoftBody, + cluster: u32, + out: *mut RprRigidBodyHandle, +) -> RprStatus { + ffi(|| unsafe { + let h = get(body)? + .0 + .cluster_proxy(cluster) + .ok_or_else(|| invalid("invalid cluster"))?; + output(out, h.into()) + }) +} +pub(crate) unsafe fn native_soft_body_cluster_particles( + body: *const RprSoftBody, + cluster: u32, + buffer: *mut u32, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let c = get(body)? + .0 + .cluster(cluster) + .filter(|c| c.is_live()) + .ok_or_else(|| invalid("invalid cluster"))?; + copy_out(c.particles(), buffer, capacity, count) + }) +} +pub(crate) unsafe fn native_soft_body_set_cluster_pinned( + body: *mut RprSoftBody, + cluster: u32, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + let b = &mut get_mut(body)?.0; + b.cluster(cluster) + .filter(|c| c.is_live()) + .ok_or_else(|| invalid("invalid cluster"))?; + b.set_cluster_pinned(cluster, value); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_set_cluster_kinematic_target( + body: *mut RprSoftBody, + cluster: u32, + value: RprPose, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let b = &mut get_mut(body)?.0; + b.cluster(cluster) + .filter(|c| c.is_live()) + .ok_or_else(|| invalid("invalid cluster"))?; + b.set_cluster_kinematic_target(cluster, value); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_set_cluster_shape_matching_enabled( + body: *mut RprSoftBody, + cluster: u32, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + let b = &mut get_mut(body)?.0; + b.cluster(cluster) + .filter(|c| c.is_live()) + .ok_or_else(|| invalid("invalid cluster"))?; + b.enable_cluster_shape_matching(cluster, value); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_set_cluster_stiffness_scale( + body: *mut RprSoftBody, + cluster: u32, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let b = &mut get_mut(body)?.0; + b.cluster(cluster) + .filter(|c| c.is_live()) + .ok_or_else(|| invalid("invalid cluster"))?; + b.set_cluster_stiffness_scale(cluster, value); + Ok(()) + }) +} +pub(crate) unsafe fn native_soft_body_set_cluster_tear_resistance( + body: *mut RprSoftBody, + cluster: u32, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + let b = &mut get_mut(body)?.0; + b.cluster(cluster) + .filter(|c| c.is_live()) + .ok_or_else(|| invalid("invalid cluster"))?; + b.set_cluster_tear_resistance(cluster, value); + Ok(()) + }) +} + +/// Stable identity of a live mesh within one soft body; matches Rapier's SoftMeshId. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default, PartialEq, Eq)] +pub struct RprSoftMeshId { + /// Cluster index within its soft body. + pub cluster: u32, + /// Mesh index within the cluster. + pub mesh: u32, +} +impl RprSoftMeshId { + fn raw(self) -> SoftMeshId { + SoftMeshId { + cluster: self.cluster, + mesh: self.mesh, + } + } +} +/// Mesh identity and rendering metadata. A render-only mesh has an invalid collider handle. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftMeshInfo { + /// Cluster/mesh pair identifying this collision mesh. + pub id: RprSoftMeshId, + /// World-bound collider handle. + pub collider: RprColliderHandle, + /// Indices per mesh element: 2 for an edge or 3 for a triangle. + pub arity: usize, + /// Whether vertex positions are obtained by skinning. + pub is_skinned: RprBool, + /// Whether collision detection is enabled for this mesh. + pub collision_enabled: RprBool, +} +/// Enumerates all live meshes, including skins without a physics collider. +pub(crate) unsafe fn native_soft_body_meshes( + body: *const RprSoftBody, + buffer: *mut RprSoftMeshInfo, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let meshes: Vec<_> = get(body)? + .0 + .meshes() + .map(|m| RprSoftMeshInfo { + id: RprSoftMeshId { + cluster: m.id().cluster, + mesh: m.id().mesh, + }, + collider: m.collider().into(), + arity: m.arity(), + is_skinned: m.is_skinned() as RprBool, + collision_enabled: m.collision_enabled() as RprBool, + }) + .collect(); + copy_out(&meshes, buffer, capacity, count) + }) +} +/// Copies world-space vertices of a mesh, including render-only skins. +pub(crate) unsafe fn native_soft_body_mesh_vertices_by_id( + body: *const RprSoftBody, + id: RprSoftMeshId, + buffer: *mut RprVector, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let body = &get(body)?.0; + let mesh = body.mesh(id.raw()).ok_or_else(missing)?; + let vertices: Vec<_> = mesh.vertex_positions(body).map(Into::into).collect(); + copy_out(&vertices, buffer, capacity, count) + }) +} +/// Copies flattened vertex indices. Each element has RprSoftMeshInfo::arity entries. +pub(crate) unsafe fn native_soft_body_mesh_indices_by_id( + body: *const RprSoftBody, + id: RprSoftMeshId, + buffer: *mut u32, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let mesh = get(body)?.0.mesh(id.raw()).ok_or_else(missing)?; + let indices: Vec<_> = (0..mesh.indices().len()) + .flat_map(|i| mesh.element(i).iter().copied()) + .collect(); + copy_out(&indices, buffer, capacity, count) + }) +} +/// Enumerates actual collider handles only. Use SoftBodyMeshes for render-only meshes too. +pub(crate) unsafe fn native_soft_body_mesh_colliders( + body: *const RprSoftBody, + buffer: *mut RprColliderHandle, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let v: Vec<_> = get(body)? + .0 + .meshes() + .map(|m| m.collider()) + .filter(|h| *h != ColliderHandle::invalid()) + .map(Into::into) + .collect(); + copy_out(&v, buffer, capacity, count) + }) +} +pub(crate) unsafe fn native_soft_body_mesh_vertices( + body: *const RprSoftBody, + collider: RprColliderHandle, + buffer: *mut RprVector, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + ensure( + collider != RprColliderHandle::default(), + "mesh has no collider; use its mesh ID", + )?; + let b = &get(body)?.0; + let m = b.mesh_of(collider.raw()).ok_or_else(missing)?; + let v: Vec<_> = m.vertex_positions(b).map(Into::into).collect(); + copy_out(&v, buffer, capacity, count) + }) +} +/// Indices use mesh vertices, not particles. Arity is two for wire meshes, DIM otherwise. +pub(crate) unsafe fn native_soft_body_mesh_indices( + body: *const RprSoftBody, + collider: RprColliderHandle, + buffer: *mut u32, + capacity: usize, + count: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + ensure( + collider != RprColliderHandle::default(), + "mesh has no collider; use its mesh ID", + )?; + let b = &get(body)?.0; + let m = b.mesh_of(collider.raw()).ok_or_else(missing)?; + let v: Vec<_> = (0..m.indices().len()) + .flat_map(|i| m.element(i).iter().copied()) + .collect(); + copy_out(&v, buffer, capacity, count) + }) +} +pub(crate) unsafe fn native_soft_body_mesh_arity( + body: *const RprSoftBody, + collider: RprColliderHandle, + out: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + ensure( + collider != RprColliderHandle::default(), + "mesh has no collider; use its mesh ID", + )?; + let m = get(body)?.0.mesh_of(collider.raw()).ok_or_else(missing)?; + output(out, m.arity()) + }) +} +pub(crate) unsafe fn native_soft_body_mesh_topology_version( + body: *const RprSoftBody, + collider: RprColliderHandle, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + ensure( + collider != RprColliderHandle::default(), + "mesh has no collider; use its mesh ID", + )?; + let m = get(body)?.0.mesh_of(collider.raw()).ok_or_else(missing)?; + output(out, m.topology_version()) + }) +} + +#[cfg(feature = "fem")] +pub(crate) unsafe fn native_soft_body_set_solver(body: *mut RprSoftBody, solver: u32) -> RprStatus { + ffi(|| unsafe { + let v = match solver { + 0 => SoftBodySolver::Constraints, + 1 => SoftBodySolver::Fem, + _ => return Err(invalid("unknown soft body solver")), + }; + get_mut(body)?.0.set_solver(v); + Ok(()) + }) +} + +/// Set the optional shape-matching target of a live cluster. A null target clears it. +pub(crate) unsafe fn native_soft_body_set_cluster_shape_matching_target( + body: *mut RprSoftBody, + cluster: u32, + target: *const RprPose, +) -> RprStatus { + ffi(|| unsafe { + let target = if target.is_null() { + None + } else { + Some(get(target)?.raw()?) + }; + let b = &mut get_mut(body)?.0; + let c = b + .cluster_mut(cluster) + .filter(|c| c.is_live()) + .ok_or_else(|| invalid("invalid cluster"))?; + c.set_shape_matching_target(target); + Ok(()) + }) +} + +/// Corresponds to `SoftBody::particle_position`. Copies one particle position. +pub(crate) unsafe fn native_soft_body_particle_position( + body: *const RprSoftBody, + index: usize, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let body = &get(body)?.0; + ensure(index < body.num_particles(), "particle index out of bounds")?; + output(out, body.particle_position(index).into()) + }) +} + +/// Optional particle destination after a tear. Missing destinations are normal and set +/// found to false; body/index are only written when a destination exists. +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_try_particle_destination( + event: *const RprSoftBodyTearEvent, + particle: u32, +) -> RprOptionalParticleDestination { + ffi_world_value( + unsafe { get(event).map_or(std::ptr::null_mut(), |e| e.1) }, + |result: *mut RprOptionalParticleDestination| { + let body = unsafe { std::ptr::addr_of_mut!((*result).body) }; + let index = unsafe { std::ptr::addr_of_mut!((*result).index) }; + let found = unsafe { std::ptr::addr_of_mut!((*result).found) }; + + ffi(|| unsafe { + out_ptr(body)?; + out_ptr(index)?; + out_ptr(found)?; + if let Some((destination, new_index)) = get(event)?.0.particle_destination(particle) + { + output(body, destination.into())?; + output(index, new_index)?; + output(found, 1) + } else { + output(found, 0) + } + }) + }, + ) +} + +/// Changes the tear resistance multiplier for an individual edge. +pub(crate) unsafe fn native_soft_body_set_edge_tear_resistance( + body: *mut RprSoftBody, + index: usize, + resistance: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let resistance = nonnegative(resistance)?; + let body = &mut get_mut(body)?.0; + ensure(index < body.edges().len(), "edge index out of range")?; + body.set_edge_tear_resistance(index, resistance); + Ok(()) + }) +} + +/// Cut using DIM points (a segment in 2D, triangle in 3D). A no-op returns a null event. +/// The optional owned event must be freed with FreeSoftBodyTearEvent. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_cut_soft_body( + handle: RprSoftBodyHandle, + blade: *const RprVector, +) -> *mut RprSoftBodyTearEvent { + let world = handle.world; + ffi_value(|out: *mut *mut RprSoftBodyTearEvent| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + out_ptr(out)?; + let blade = input(blade, rapier::math::DIM)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + let blade: [Vector; rapier::math::DIM] = blade.try_into().unwrap(); + let event = get_mut(world)?.0.cut_soft_body(handle.raw(), &blade); + output( + out, + event + .map(|e| Box::into_raw(Box::new(RprSoftBodyTearEvent(e, handle.world)))) + .unwrap_or(std::ptr::null_mut()), + ) + }) + }) +} + +/// Parameters for the native volumetric mesher. Enclosure: 0 cover, 1 crust (3D). +/// @ingroup shapes +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprVolumeMeshParameters { + /// Positive target cell size for volume meshing. + pub cell_size: RprReal, + #[cfg(feature = "dim2")] + /// Minimum target element angle in radians; above pi/6, refinement may stop early. + pub min_angle: RprReal, + #[cfg(feature = "dim3")] + /// Enclosure strategy: 0 covers the whole shape, 1 encloses only its surface. + pub enclosure: u32, + #[cfg(feature = "dim3")] + /// Number of cover-smoothing passes. + pub cover_smoothing: u32, + #[cfg(feature = "dim3")] + /// Minimum smoothed-cover distance from the shape, as a fraction of local cell size. + pub cover_guard: RprReal, + #[cfg(feature = "dim3")] + /// Maximum boundary-cell halvings below cell_size before cover smoothing. + pub cover_subdivisions: u32, +} +/// Return volume-meshing settings for the supplied cell size; this POD value requires no +/// destructor. +/// @ingroup worlds +#[rapier_export] +pub extern "C" fn rpr_new_volume_mesh_parameters(cell_size: RprReal) -> RprVolumeMeshParameters { + let p = rapier::parry::transformation::VolumeMeshParameters::new(cell_size); + RprVolumeMeshParameters { + cell_size: p.cell_size, + #[cfg(feature = "dim2")] + min_angle: p.min_angle, + #[cfg(feature = "dim3")] + enclosure: 0, + #[cfg(feature = "dim3")] + cover_smoothing: p.cover_smoothing, + #[cfg(feature = "dim3")] + cover_guard: p.cover_guard, + #[cfg(feature = "dim3")] + cover_subdivisions: p.cover_subdivisions, + } +} + +/// Whether the collision mesh encloses an interior. Non-mesh colliders return false. +pub(crate) unsafe fn native_soft_body_mesh_is_closed( + body: *const RprSoftBody, + collider: RprColliderHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + ensure( + collider != RprColliderHandle::default(), + "expected a valid collider handle", + )?; + output( + out, + get(body)? + .0 + .mesh_of(collider.raw()) + .is_some_and(|m| m.is_closed()) as RprBool, + ) + }) +} diff --git a/c/src/soft_desc.rs b/c/src/soft_desc.rs new file mode 100644 index 000000000..242362f2c --- /dev/null +++ b/c/src/soft_desc.rs @@ -0,0 +1,643 @@ +//! Soft-body recipes and borrowed topology. All arrays are copied during insertion. +#![allow(non_snake_case)] +use crate::*; +/// @ingroup soft_bodies +/// Soft-body selector: desc particles. +pub const RPR_SOFT_DESC_PARTICLES: u32 = 0; +/// @ingroup soft_bodies +/// Soft-body selector: desc rope. +pub const RPR_SOFT_DESC_ROPE: u32 = 1; +/// @ingroup soft_bodies +/// Soft-body selector: desc grid. +pub const RPR_SOFT_DESC_GRID: u32 = 2; +/// @ingroup soft_bodies +/// Soft-body selector: desc cloth. +pub const RPR_SOFT_DESC_CLOTH: u32 = 3; +/// @ingroup soft_bodies +/// Soft-body selector: desc cuboid. +pub const RPR_SOFT_DESC_CUBOID: u32 = 4; +/// @ingroup soft_bodies +/// Soft-body selector: desc surface. +pub const RPR_SOFT_DESC_SURFACE: u32 = 5; +/// @ingroup soft_bodies +/// Soft-body selector: desc disk. +pub const RPR_SOFT_DESC_DISK: u32 = 6; +/// @ingroup soft_bodies +/// Soft-body selector: desc sphere. +pub const RPR_SOFT_DESC_SPHERE: u32 = 7; +/// @ingroup soft_bodies +/// Soft-body selector: desc cloth tube. +pub const RPR_SOFT_DESC_CLOTH_TUBE: u32 = 8; +/// @ingroup soft_bodies +/// Soft-body selector: desc volumetric. +pub const RPR_SOFT_DESC_VOLUMETRIC: u32 = 9; +/// Spring-coefficient override for one soft-body edge. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftEdgeSoftness { + /// Zero-based edge index. + pub edge: u32, + /// Spring coefficients for constraint correction. + pub softness: RprSpringCoefficients, +} +/// Tear-resistance override for one soft-body edge. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftEdgeTear { + /// Zero-based edge index. + pub edge: u32, + /// Nonnegative edge tear-resistance multiplier. + pub resistance: RprReal, +} +/// Copyable recipe, not an owned procedural builder. Initialize before editing. +/// All array views borrow caller data until build/insert returns; counts +/// for topology arrays are element counts (edges, triangles, or tetrahedra). +/// Nonempty topology overrides the generator's topology. Zero counts retain it. +/// Generator inputs: a/b are rope ends or center/half-extents; cloth uses a/du/dv. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RprSoftBodyDesc { + /// Discriminant selecting which description fields are read. + pub kind: u32, + /// Recipe origin/center or first rope endpoint; use a recipe constructor. + pub a: RprVector, + /// Recipe half extents, second rope endpoint, or tube axis; use a recipe constructor. + pub b: RprVector, + /// Cloth basis step along its first parameter axis. + pub du: RprVector, + /// Cloth basis step along its second parameter axis. + pub dv: RprVector, + /// First recipe resolution; interpretation depends on kind. + pub nx: usize, + /// Second recipe resolution; interpretation depends on kind. + pub ny: usize, + /// Third recipe resolution; interpretation depends on kind. + pub nz: usize, + /// Radius for the selected primitive or recipe. + pub radius: RprReal, + /// Radius at the second end of a tapered cloth tube. + pub radiusEnd: RprReal, + /// Translation vector. + pub translation: RprVector, + /// Optional total mass override for generated particles. + pub totalMass: RprOptionalReal, + /// Volume-meshing parameters for a volumetric recipe. + pub meshing: RprVolumeMeshParameters, + /// Borrowed initial particle positions. + pub positions: RprVectorView, + /// Borrowed per-particle masses; when empty, particleMass is used. + pub masses: RprRealView, + /// Borrowed indices of pinned particles. + pub pinned: RprIndexView, + /// Borrowed edge topology; count is edges. + pub edges: RprEdgeView, + /// Borrowed bending edge constraints. + pub bendEdges: RprEdgeView, + /// Borrowed indices of edges that resist tension only. + pub tensionOnlyEdges: RprIndexView, + /// Borrowed per-edge softness overrides. + pub edgeSoftness: RprSoftEdgeSoftnessView, + /// Borrowed per-edge tear-resistance overrides. + pub edgeTearResistance: RprSoftEdgeTearView, + /// Borrowed triangles in 2D or tetrahedra in 3D. + pub cells: RprCellView, + /// Borrowed boundary edges in 2D or triangles in 3D. + pub surface: RprSurfaceElementView, + #[cfg(feature = "dim3")] + /// Borrowed four-vertex bending constraints. + pub dihedrals: RprDihedralView, + #[cfg(feature = "dim3")] + /// Borrowed wire edges for a surface recipe. + pub wire: RprEdgeView, + /// Borrowed skin vertex positions. + pub skinVertices: RprVectorView, + /// Borrowed skin topology. + pub skinIndices: RprSurfaceElementView, + /// Soft-body material coefficients. + pub material: RprSoftBodyMaterial, + /// RPR_SOFT_CELL_VOLUME, RPR_SOFT_CELL_COROTATIONAL, or RPR_SOFT_CELL_NEO_HOOKEAN. + pub cellModel: u32, + /// 0 = constraints, 1 = FEM (requires a library built with FEM). + pub solver: u32, + /// Default nonnegative particle mass. + pub particleMass: RprReal, + /// Disabled by default: retain the radius computed by the generator. + pub particleRadius: RprOptionalReal, + /// Whether to preserve volume. + pub volumePreservation: RprBool, + /// Target volume multiplier. + pub volumeFactor: RprReal, + /// Optional shape-matching override; disabled retains recipe defaults. + pub shapeMatching: RprOptionalBool, + /// Whether self-collision is enabled. + pub selfContacts: RprBool, + /// Whether skin elements participate in collision detection. + pub skinCollision: RprBool, + /// Whether the generated collision geometry is enabled. + pub collisionEnabled: RprBool, + /// Collider configuration used by the recipe; shape comes from the generated geometry. + pub collider: RprColliderDesc, + /// Nonnegative linear damping coefficient. + pub linearDamping: RprReal, + /// Multiplier applied to world gravity. + pub gravityScale: RprReal, + /// Extra solver iterations for this body and connected bodies. + pub additionalSolverIterations: usize, + /// Extra PGS iterations for this body. + pub additionalPgsIterations: usize, + /// Whether automatic sleeping is allowed. + pub canSleep: RprBool, + /// Signed dominance group; larger groups dominate smaller groups. + pub dominanceGroup: i8, + /// Application data; Rapier does not own pointers encoded in it. + pub userData: RprUserData, +} +impl Default for RprSoftBodyDesc { + fn default() -> Self { + let settings = rapier::dynamics::SoftBodyParticleSettings::default(); + let mut collider = RprColliderDesc::default(); + collider.shape.radius = 0.05; + collider.density = 0.0; + let meshing = rapier::parry::transformation::VolumeMeshParameters::new(0.1); + Self { + kind: RPR_SOFT_DESC_PARTICLES, + radius: 0.5, + radiusEnd: 0.5, + translation: Vector::ZERO.into(), + totalMass: RprOptionalReal::default(), + meshing: RprVolumeMeshParameters { + cell_size: meshing.cell_size, + #[cfg(feature = "dim2")] + min_angle: meshing.min_angle, + #[cfg(feature = "dim3")] + enclosure: 0, + #[cfg(feature = "dim3")] + cover_smoothing: meshing.cover_smoothing, + #[cfg(feature = "dim3")] + cover_guard: meshing.cover_guard, + #[cfg(feature = "dim3")] + cover_subdivisions: meshing.cover_subdivisions, + }, + a: Vector::ZERO.into(), + b: Vector::ONE.into(), + du: Vector::X.into(), + dv: Vector::Y.into(), + nx: 2, + ny: 2, + nz: 2, + positions: RprVectorView::default(), + masses: RprRealView::default(), + pinned: RprIndexView::default(), + edges: RprEdgeView::default(), + bendEdges: RprEdgeView::default(), + tensionOnlyEdges: RprIndexView::default(), + edgeSoftness: RprSoftEdgeSoftnessView::default(), + edgeTearResistance: RprSoftEdgeTearView::default(), + cells: RprCellView::default(), + surface: RprSurfaceElementView::default(), + #[cfg(feature = "dim3")] + dihedrals: RprDihedralView::default(), + #[cfg(feature = "dim3")] + wire: RprEdgeView::default(), + skinVertices: RprVectorView::default(), + skinIndices: RprSurfaceElementView::default(), + material: SoftBodyMaterial::default().into(), + cellModel: match SoftBodyCellModel::default() { + SoftBodyCellModel::Volume => 0, + SoftBodyCellModel::Corotational => 1, + SoftBodyCellModel::NeoHookean => 2, + }, + solver: 0, + particleMass: 1.0, + particleRadius: RprOptionalReal::default(), + volumePreservation: 0, + volumeFactor: 1.0, + shapeMatching: RprOptionalBool::default(), + selfContacts: 0, + skinCollision: 0, + collisionEnabled: 1, + collider, + linearDamping: settings.linear_damping, + gravityScale: settings.gravity_scale, + additionalSolverIterations: settings.additional_solver_iterations, + additionalPgsIterations: settings.additional_pgs_iterations, + canSleep: settings.can_sleep as _, + dominanceGroup: settings.dominance_group, + userData: 0u128.into(), + } + } +} +impl RprSoftBodyDesc { + pub(crate) unsafe fn raw(&self) -> Result { + use crate::geometry::indices_array; + let points = || { + unsafe { input(self.positions.data, self.positions.count)? } + .iter() + .map(|v| v.raw()) + .collect::>>() + }; + let mut b = match self.kind { + RPR_SOFT_DESC_PARTICLES | RPR_SOFT_DESC_SURFACE => { + ensure( + self.positions.count > 0 && self.positions.count <= u32::MAX as usize, + "invalid particle count", + )?; + let vertices = points()?; + if self.kind == RPR_SOFT_DESC_PARTICLES { + SoftBodyBuilder::new(vertices) + } else { + let idx = unsafe { + indices_array::<{ rapier::math::DIM }>( + self.surface.data.cast(), + self.surface.count, + vertices.len(), + )? + }; + #[cfg(feature = "dim2")] + let b = SoftBodyBuilder::polyline(vertices, Some(idx)); + #[cfg(feature = "dim3")] + let b = SoftBodyBuilder::trimesh(vertices, idx); + b.ok_or_else(|| invalid("invalid soft surface"))? + } + } + #[cfg(feature = "dim2")] + RPR_SOFT_DESC_DISK => { + ensure( + (3..=1_000_000).contains(&self.nx), + "particle count out of range", + )?; + SoftBodyBuilder::disk(self.a.raw()?, positive(self.radius)?, self.nx) + } + #[cfg(feature = "dim3")] + RPR_SOFT_DESC_SPHERE => { + ensure(self.nx <= 6, "sphere subdivisions exceed 6")?; + SoftBodyBuilder::sphere(self.a.raw()?, positive(self.radius)?, self.nx) + } + #[cfg(feature = "dim3")] + RPR_SOFT_DESC_CLOTH_TUBE => { + ensure( + self.nx + .max(3) + .checked_mul(self.ny.max(2)) + .is_some_and(|n| n <= u32::MAX as usize), + "cloth tube too large", + )?; + SoftBodyBuilder::cloth_tube( + self.a.raw()?, + self.b.raw()?, + nonnegative(self.radius)?, + nonnegative(self.radiusEnd)?, + self.nx, + self.ny, + ) + } + RPR_SOFT_DESC_VOLUMETRIC => { + let points = points()?; + let indices = unsafe { + indices_array::<{ rapier::math::DIM }>( + self.surface.data.cast(), + self.surface.count, + points.len(), + )? + }; + let p = &self.meshing; + let mut params = rapier::parry::transformation::VolumeMeshParameters::new( + positive(p.cell_size)?, + ); + #[cfg(feature = "dim2")] + { + params.min_angle = nonnegative(p.min_angle)?; + } + #[cfg(feature = "dim3")] + { + use rapier::parry::transformation::MeshEnclosure; + params.enclosure = match p.enclosure { + 0 => MeshEnclosure::Cover, + 1 => MeshEnclosure::Crust, + _ => return Err(invalid("unknown mesh enclosure")), + }; + params.cover_smoothing = p.cover_smoothing; + params.cover_guard = nonnegative(p.cover_guard)?; + params.cover_subdivisions = p.cover_subdivisions; + } + SoftBodyBuilder::volumetric_with(&points, &indices, ¶ms) + .ok_or_else(|| invalid("volume meshing failed"))? + } + RPR_SOFT_DESC_ROPE => { + ensure( + (2..=u32::MAX as usize).contains(&self.nx), + "invalid rope count", + )?; + SoftBodyBuilder::rope(self.a.raw()?, self.b.raw()?, self.nx) + } + #[cfg(feature = "dim2")] + RPR_SOFT_DESC_GRID => { + self.check_grid(false)?; + SoftBodyBuilder::grid(self.a.raw()?, self.b.raw()?, self.nx, self.ny) + } + #[cfg(feature = "dim3")] + RPR_SOFT_DESC_CUBOID => { + self.check_grid(true)?; + SoftBodyBuilder::cuboid(self.a.raw()?, self.b.raw()?, self.nx, self.ny, self.nz) + } + #[cfg(feature = "dim3")] + RPR_SOFT_DESC_CLOTH => { + ensure( + self.nx >= 2 + && self.ny >= 2 + && self + .nx + .checked_mul(self.ny) + .is_some_and(|n| n <= u32::MAX as usize), + "invalid cloth size", + )?; + SoftBodyBuilder::cloth( + self.a.raw()?, + self.du.raw()?, + self.dv.raw()?, + self.nx, + self.ny, + ) + } + _ => return Err(invalid("unsupported soft-body recipe")), + }; + let n = b.positions.len(); + if self.masses.count != 0 { + ensure(self.masses.count == n, "particle masses length mismatch")?; + b.masses = unsafe { input(self.masses.data, n)? } + .iter() + .map(|v| positive(*v)) + .collect::>()?; + } + b.pinned = unsafe { input(self.pinned.data, self.pinned.count)? }.to_vec(); + ensure( + b.pinned.iter().all(|i| (*i as usize) < n), + "pinned particle out of bounds", + )?; + if self.edges.count != 0 { + b.edges = unsafe { indices_array::<2>(self.edges.data.cast(), self.edges.count, n)? }; + } + if self.bendEdges.count != 0 { + b.bend_edges = + unsafe { indices_array::<2>(self.bendEdges.data.cast(), self.bendEdges.count, n)? }; + } + if self.cells.count != 0 { + b.cells = unsafe { + indices_array::<{ rapier::math::DIM + 1 }>( + self.cells.data.cast(), + self.cells.count, + n, + )? + }; + } + if self.surface.count != 0 && self.kind != RPR_SOFT_DESC_VOLUMETRIC { + b.surface = unsafe { + indices_array::<{ rapier::math::DIM }>( + self.surface.data.cast(), + self.surface.count, + n, + )? + }; + } + #[cfg(feature = "dim3")] + { + if self.dihedrals.count != 0 { + b.dihedrals = unsafe { + indices_array::<4>(self.dihedrals.data.cast(), self.dihedrals.count, n)? + }; + } + if self.wire.count != 0 { + b.wire = unsafe { indices_array::<2>(self.wire.data.cast(), self.wire.count, n)? }; + } + } + let edge_count = b.edges.len() + b.bend_edges.len(); + b.tension_only_edges = + unsafe { input(self.tensionOnlyEdges.data, self.tensionOnlyEdges.count)? }.to_vec(); + ensure( + b.tension_only_edges + .iter() + .all(|i| (*i as usize) < edge_count), + "tension edge out of bounds", + )?; + b.edge_softness = unsafe { input(self.edgeSoftness.data, self.edgeSoftness.count)? } + .iter() + .map(|v| { + ensure( + (v.edge as usize) < edge_count, + "softness edge out of bounds", + )?; + Ok((v.edge, v.softness.raw()?)) + }) + .collect::>()?; + b.edge_tear_resistance = + unsafe { input(self.edgeTearResistance.data, self.edgeTearResistance.count)? } + .iter() + .map(|v| { + ensure((v.edge as usize) < edge_count, "tear edge out of bounds")?; + Ok((v.edge, nonnegative(v.resistance)?)) + }) + .collect::>()?; + if self.skinVertices.count != 0 { + let vertices = unsafe { input(self.skinVertices.data, self.skinVertices.count)? } + .iter() + .map(|v| v.raw()) + .collect::>>()?; + let indices = unsafe { + indices_array::<{ rapier::math::DIM }>( + self.skinIndices.data.cast(), + self.skinIndices.count, + vertices.len(), + )? + }; + b.skin = Some((vertices, indices)); + } else { + ensure(self.skinIndices.count == 0, "skin indices without vertices")?; + } + b.material = self.material.raw()?; + b.cell_model = match self.cellModel { + 0 => SoftBodyCellModel::Volume, + 1 => SoftBodyCellModel::Corotational, + 2 => SoftBodyCellModel::NeoHookean, + _ => return Err(invalid("invalid cell model")), + }; + #[cfg(feature = "fem")] + { + b.solver = match self.solver { + 0 => SoftBodySolver::Constraints, + 1 => SoftBodySolver::Fem, + _ => return Err(invalid("unknown soft solver")), + }; + } + #[cfg(not(feature = "fem"))] + ensure(self.solver == 0, "FEM support is not enabled")?; + b.particle_mass = positive(self.particleMass)?; + if boolean(self.particleRadius.enabled)? { + b.particle_radius = nonnegative(self.particleRadius.value)?; + } + b.volume_preservation = boolean(self.volumePreservation)?; + b.volume_factor = positive(self.volumeFactor)?; + if boolean(self.shapeMatching.enabled)? { + b.shape_matching = boolean(self.shapeMatching.value)?; + } + b.self_contacts = boolean(self.selfContacts)?; + b.skin_collision = boolean(self.skinCollision)?; + b.collider_template = if boolean(self.collisionEnabled)? { + Some(unsafe { self.collider.raw()? }) + } else { + None + }; + b.particle_settings.linear_damping = nonnegative(self.linearDamping)?; + b.particle_settings.gravity_scale = finite(self.gravityScale)?; + b.particle_settings.additional_solver_iterations = self.additionalSolverIterations; + b.particle_settings.additional_pgs_iterations = self.additionalPgsIterations; + b.particle_settings.can_sleep = boolean(self.canSleep)?; + b.particle_settings.dominance_group = self.dominanceGroup; + b.user_data = self.userData.raw(); + if boolean(self.totalMass.enabled)? { + b = b.mass(positive(self.totalMass.value)?); + } + b = b.translated(self.translation.raw()?); + Ok(b) + } + fn check_grid(&self, dim3: bool) -> Result<()> { + let count = self + .nx + .checked_add(1) + .and_then(|x| self.ny.checked_add(1).and_then(|y| x.checked_mul(y))) + .and_then(|n| { + if dim3 { + self.nz.checked_add(1).and_then(|z| n.checked_mul(z)) + } else { + Some(n) + } + }); + ensure( + self.nx > 0 + && self.ny > 0 + && (!dim3 || self.nz > 0) + && count.is_some_and(|n| n <= u32::MAX as usize) + && self.b.raw()?.min_element() > 0.0, + "invalid soft grid size", + ) + } +} +/// Return native default soft body desc. This POD value owns no resources. +/// @ingroup soft_bodies +#[rapier_export] +pub extern "C" fn rpr_default_soft_body_desc() -> RprSoftBodyDesc { + RprSoftBodyDesc::default() +} +/// Consumes no caller-owned resources. All borrowed arrays may be released on return. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_insert_soft_body( + world: *mut RprWorld, + desc: *const RprSoftBodyDesc, +) -> RprSoftBodyHandle { + ffi_world_value(world, |out: *mut RprSoftBodyHandle| { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + if !out.is_null() { + out_ptr(out)?; + } + let builder = get(desc)?.raw()?; + let h = get_mut(world)?.0.insert_soft_body(builder); + if !out.is_null() { + output(out, h.into())?; + } + Ok(()) + }) + }) +} + +/// @ingroup soft_bodies +/// Soft-body selector: binding skinned. +pub const RPR_SOFT_BINDING_SKINNED: u32 = 0; +/// @ingroup soft_bodies +/// Soft-body selector: binding direct. +pub const RPR_SOFT_BINDING_DIRECT: u32 = 1; +/// @ingroup soft_bodies +/// Soft-body selector: binding direct by position. +pub const RPR_SOFT_BINDING_DIRECT_BY_POSITION: u32 = 2; +/// Non-owning deformable binding description. Direct particle indices are borrowed. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftMeshBindingDesc { + /// Discriminant selecting which description fields are read. + pub kind: u32, + /// Borrowed particle indices used for a direct mesh binding. + pub particles: RprIndexView, + /// Nonnegative positional tolerance for direct-by-position binding. + pub epsilon: RprReal, + /// Whether self-collision is enabled. + pub selfContacts: RprBool, +} +impl RprSoftMeshBindingDesc { + unsafe fn raw(&self) -> Result { + let b = match self.kind { + RPR_SOFT_BINDING_SKINNED => SoftMeshBinding::skinned(), + RPR_SOFT_BINDING_DIRECT => SoftMeshBinding::direct( + unsafe { input(self.particles.data, self.particles.count)? }.to_vec(), + ), + RPR_SOFT_BINDING_DIRECT_BY_POSITION => { + SoftMeshBinding::direct_by_position(nonnegative(self.epsilon)?) + } + _ => return Err(invalid("unknown soft binding")), + }; + Ok(b.self_contacts(boolean(self.selfContacts)?)) + } +} +/// Return native default soft mesh binding desc. This POD value owns no resources. +/// @ingroup soft_bodies +#[rapier_export] +pub extern "C" fn rpr_default_soft_mesh_binding_desc() -> RprSoftMeshBindingDesc { + RprSoftMeshBindingDesc { + kind: 0, + particles: RprIndexView::default(), + epsilon: 0.0, + selfContacts: 0, + } +} +/// Create a deformable collider bound to a soft-body cluster. The world owns the collider; binding +/// arrays are borrowed only during insertion. +/// @ingroup colliders +#[rapier_export] +pub unsafe extern "C" fn rpr_insert_deformable_collider( + collider: *const RprColliderDesc, + binding: *const RprSoftMeshBindingDesc, + parent: RprRigidBodyHandle, +) -> RprColliderHandle { + let world = parent.world; + ffi_world_value(world, |out: *mut RprColliderHandle| { + ffi(|| unsafe { + parent.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let world: *mut RprPhysicsWorld = raw; + + if !out.is_null() { + out_ptr(out)?; + } + let collider = get(collider)?.raw()?; + let binding = get(binding)?.raw()?; + let w = &mut get_mut(world)?.0; + w.bodies.get(parent.raw()).ok_or_else(missing)?; + let handle = w + .insert_deformable(collider, binding, parent.raw()) + .map_err(|e| invalid(e.to_string()))?; + if !out.is_null() { + output(out, handle.into())?; + } + Ok(()) + }) + }) +} diff --git a/c/src/soft_recipes.rs b/c/src/soft_recipes.rs new file mode 100644 index 000000000..5c64519a1 --- /dev/null +++ b/c/src/soft_recipes.rs @@ -0,0 +1,210 @@ +//! POD soft-body recipes and copied previews of procedural geometry. +use crate::*; +/// Return a rope recipe with particles evenly spaced from a to b, including both endpoints. +/// Initializes a recipe without allocating. Geometry is validated during preview/insertion. +/// @ingroup soft_bodies +#[rapier_export] +pub extern "C" fn rpr_rope_soft_body_desc( + a: RprVector, + b: RprVector, + particles: usize, +) -> RprSoftBodyDesc { + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_ROPE, + a, + b, + nx: particles, + ..RprSoftBodyDesc::default() + } +} +/// Return a solid rectangle recipe on an nx by ny particle grid. +/// Initializes a recipe without allocating. Geometry is validated during preview/insertion. +/// @ingroup soft_bodies +#[cfg(feature = "dim2")] +#[rapier_export] +pub extern "C" fn rpr_grid_soft_body_desc( + center: RprVector, + half_extents: RprVector, + nx: usize, + ny: usize, +) -> RprSoftBodyDesc { + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_GRID, + a: center, + b: half_extents, + nx, + ny, + ..RprSoftBodyDesc::default() + } +} +/// Return a solid box recipe on an nx by ny by nz particle grid, subdivided into tetrahedra. +/// Initializes a recipe without allocating. Geometry is validated during preview/insertion. +/// @ingroup soft_bodies +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_cuboid_soft_body_desc( + center: RprVector, + half_extents: RprVector, + nx: usize, + ny: usize, + nz: usize, +) -> RprSoftBodyDesc { + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_CUBOID, + a: center, + b: half_extents, + nx, + ny, + nz, + ..RprSoftBodyDesc::default() + } +} +/// Return a cloth recipe with nu by nv particles at origin + i * du + j * dv. +/// Initializes a recipe without allocating. Geometry is validated during preview/insertion. +/// @ingroup soft_bodies +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_cloth_soft_body_desc( + origin: RprVector, + du: RprVector, + dv: RprVector, + nu: usize, + nv: usize, +) -> RprSoftBodyDesc { + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_CLOTH, + a: origin, + du, + dv, + nx: nu, + ny: nv, + ..RprSoftBodyDesc::default() + } +} +/// Return a closed regular polygon recipe with the specified boundary particle count and area +/// preservation. +/// Initializes a recipe without allocating. Geometry is validated during preview/insertion. +/// @ingroup soft_bodies +#[cfg(feature = "dim2")] +#[rapier_export] +pub extern "C" fn rpr_disk_soft_body_desc( + center: RprVector, + radius: RprReal, + particles: usize, +) -> RprSoftBodyDesc { + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_DISK, + volumePreservation: 1, + a: center, + radius, + nx: particles, + ..RprSoftBodyDesc::default() + } +} +/// Return a hollow icosphere recipe with the specified refinement levels and volume preservation. +/// Initializes a recipe without allocating. Geometry is validated during preview/insertion. +/// @ingroup soft_bodies +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_sphere_soft_body_desc( + center: RprVector, + radius: RprReal, + subdivisions: u32, +) -> RprSoftBodyDesc { + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_SPHERE, + a: center, + radius, + nx: subdivisions as usize, + volumePreservation: 1, + ..RprSoftBodyDesc::default() + } +} +/// Return a cloth tube recipe from origin to origin + axis with num_along rings of num_around +/// particles; radius varies linearly between the ends. +/// Initializes a recipe without allocating. Geometry is validated during preview/insertion. +/// @ingroup soft_bodies +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_cloth_tube_soft_body_desc( + origin: RprVector, + axis: RprVector, + radius_start: RprReal, + radius_end: RprReal, + num_around: usize, + num_along: usize, +) -> RprSoftBodyDesc { + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_CLOTH_TUBE, + a: origin, + b: axis, + radius: radius_start, + radiusEnd: radius_end, + nx: num_around, + ny: num_along, + ..RprSoftBodyDesc::default() + } +} +/// Initializes a borrowed meshing recipe. Mesh generation happens on preview/insertion. +/// @ingroup soft_bodies +#[rapier_export] +pub extern "C" fn rpr_volumetric_soft_body_desc( + vertices: RprVectorView, + surface: RprSurfaceElementView, + parameters: RprVolumeMeshParameters, +) -> RprSoftBodyDesc { + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_VOLUMETRIC, + positions: vertices, + surface, + meshing: parameters, + ..RprSoftBodyDesc::default() + } +} +/// Returns a material with the same softness for each constraint family. +/// @ingroup soft_bodies +#[rapier_export] +pub extern "C" fn rpr_uniform_soft_body_material( + value: RprSpringCoefficients, +) -> RprSoftBodyMaterial { + let mut material = RprSoftBodyMaterial::from(SoftBodyMaterial::default()); + material.edgeSoftness = value; + material.bendSoftness = value; + material.volumeSoftness = value; + material.shapeMatchingSoftness = value; + material +} +/// Copies generated particle positions into caller-owned storage; no persistent builder. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_particle_positions( + desc: *const RprSoftBodyDesc, + buffer: *mut RprVector, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let b = get(desc)?.raw()?; + let v: Vec = b.positions.iter().copied().map(Into::into).collect(); + copy_out(&v, buffer, capacity, count) + }) + }) +} +/// Copies generated cell indices into caller-owned storage. Counts scalar indices. +/// @see @ref output_buffers +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_cell_indices( + desc: *const RprSoftBodyDesc, + buffer: *mut u32, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let b = get(desc)?.raw()?; + let v: Vec = b.cells.iter().flatten().copied().collect(); + copy_out(&v, buffer, capacity, count) + }) + }) +} diff --git a/c/src/tests.rs b/c/src/tests.rs new file mode 100644 index 000000000..b52903776 --- /dev/null +++ b/c/src/tests.rs @@ -0,0 +1,614 @@ +use crate::*; +#[test] +fn panic_boundary_and_thread_local_errors() { + assert_eq!( + crate::error::ffi(|| panic!("intentional boundary test")), + RPR_PANIC + ); + let message = unsafe { std::ffi::CStr::from_ptr(rpr_last_error()) } + .to_str() + .unwrap() + .to_owned(); + assert!(message.contains("intentional boundary test")); + std::thread::spawn(|| { + assert_eq!(rpr_last_status(), RPR_OK); + assert!(unsafe { rpr_ball_shared_shape(-1.0) }.is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert!( + unsafe { std::ffi::CStr::from_ptr(rpr_last_error()) } + .to_str() + .unwrap() + .contains("positive") + ); + }) + .join() + .unwrap(); + assert_eq!( + unsafe { std::ffi::CStr::from_ptr(rpr_last_error()) } + .to_str() + .unwrap(), + message + ); + assert_eq!(unsafe { rpr_free_world(std::ptr::null_mut()) }, RPR_OK); + assert!( + unsafe { std::ffi::CStr::from_ptr(rpr_last_error()) } + .to_bytes() + .is_empty() + ); +} +#[test] +fn buffers_and_null_arrays() { + let mut count = 99; + let mut sentinel = [123u32, 456]; + let code = crate::error::ffi(|| unsafe { + crate::error::copy_out(&[1, 2, 3], sentinel.as_mut_ptr(), 2, &mut count) + }); + assert_eq!(code, RPR_BUFFER_TOO_SMALL); + assert_eq!(count, 3); + assert_eq!(sentinel, [123, 456]); + assert_eq!( + crate::error::ffi(|| unsafe { + crate::error::input::(std::ptr::null(), 0)?; + Ok(()) + }), + RPR_OK + ); + assert_eq!( + crate::error::ffi(|| unsafe { + crate::error::input::(std::ptr::null(), 1)?; + Ok(()) + }), + RPR_NULL_POINTER + ); + assert_eq!( + crate::error::ffi(|| unsafe { + crate::error::input::(sentinel.as_ptr(), usize::MAX)?; + Ok(()) + }), + RPR_INVALID_ARGUMENT + ); +} +#[test] +fn invalid_inputs_leave_objects_unchanged() { + unsafe { + let mut bodies = RprRigidBodySet(RigidBodySet::new()); + let handle = bodies.0.insert(RigidBodyBuilder::dynamic()).into(); + assert_eq!( + native_rigid_body_set_set_enabled(&mut bodies, handle, 2), + RPR_INVALID_ARGUMENT + ); + assert_eq!( + native_rigid_body_set_set_linear_damping(&mut bodies, handle, Real::NAN), + RPR_INVALID_ARGUMENT + ); + let mut enabled = 0; + assert_eq!( + native_rigid_body_set_get_is_enabled(&bodies, handle, &mut enabled), + RPR_OK + ); + assert_eq!(enabled, 1); + } +} + +#[test] +fn build_profile_is_reported_by_the_library() { + unsafe { + let profile = rpr_build_profile(); + assert_eq!( + std::ffi::CStr::from_ptr(profile).to_str().unwrap(), + env!("RAPIER_CARGO_PROFILE") + ); + // A later call must not invalidate the borrowed profile string. + assert_eq!(rpr_free_world(std::ptr::null_mut()), RPR_OK); + assert_eq!( + std::ffi::CStr::from_ptr(profile).to_str().unwrap(), + env!("RAPIER_CARGO_PROFILE") + ); + } +} + +#[test] +fn description_insertion_copies_values_and_preserves_ownership() { + unsafe { + let mut body = rpr_dynamic_rigid_body_desc(); + let world = rpr_new_world(); + assert_eq!(rpr_last_status(), RPR_OK); + let collider = rpr_ball_collider_desc(0.5); + + let body_handle = rpr_insert_rigid_body(world, &body); + assert_eq!(rpr_last_status(), RPR_OK); + let mut collider_handle = rpr_insert_collider(body_handle, &collider); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!( + (&(*(*world).read().unwrap().raw()).0.colliders)[collider_handle.raw()].parent(), + Some(body_handle.raw()) + ); + + // Reusing and changing a description does not change inserted objects. + body.position.translation = Vector::Y.into(); + let other = rpr_insert_rigid_body(world, &body); + assert_eq!(rpr_last_status(), RPR_OK); + rpr_insert_collider(other, &collider); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!((*(*world).read().unwrap().raw()).0.bodies.len(), 2); + assert_eq!( + (&(*(*world).read().unwrap().raw()).0.bodies)[body_handle.raw()].translation(), + Vector::ZERO + ); + + let count = (*(*world).read().unwrap().raw()).0.colliders.len(); + rpr_insert_collider(RprRigidBodyHandle::default(), &collider); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + assert_eq!((*(*world).read().unwrap().raw()).0.colliders.len(), count); + collider_handle = rpr_insert_collider_without_parent(world, &collider); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!( + (&(*(*world).read().unwrap().raw()).0.colliders)[collider_handle.raw()].parent(), + None + ); + rpr_insert_collider(body_handle, std::ptr::null()); + assert_eq!(rpr_last_status(), RPR_NULL_POINTER); + assert_eq!((*(*world).read().unwrap().raw()).0.bodies.len(), 2); + assert_eq!( + rpr_step(world, std::ptr::null(), std::ptr::null_mut()), + RPR_OK + ); + assert_eq!(rpr_free_world(world), RPR_OK); + + // Constructors return descriptions even for invalid geometry. Building validates it. + let collider = rpr_ball_collider_desc(-1.0); + assert_eq!(collider.shape.radius, -1.0); + let shape = rpr_shape_desc_build(&collider.shape); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert!(shape.is_null()); + } +} + +#[test] +fn world_soft_body_insertion_and_particle_access() { + unsafe { + let world = rpr_new_world(); + assert_eq!(rpr_last_status(), RPR_OK); + let desc = rpr_rope_soft_body_desc(Vector::ZERO.into(), Vector::X.into(), 3); + let handle = rpr_insert_soft_body(world, &desc); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!((*(*world).read().unwrap().raw()).0.soft_bodies.len(), 1); + let mut position: RprVector = rpr_soft_body_particle_position(handle, 2); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(position.raw().unwrap(), Vector::X); + position = rpr_soft_body_particle_position(handle, 3); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(position.raw().unwrap(), Vector::ZERO); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn error_handlers_are_scoped_thread_local_and_reentrant_safe() { + struct Report { + calls: usize, + status: RprStatus, + message: String, + nested_status: RprStatus, + } + unsafe extern "C" fn report( + status: RprStatus, + message: *const std::ffi::c_char, + user_data: *mut std::ffi::c_void, + ) { + let report = unsafe { &mut *user_data.cast::() }; + report.calls += 1; + report.status = status; + // Calling Rapier here must not recursively call the handler or invalidate message. + report.nested_status = + unsafe { rpr_set_gravity(std::ptr::null_mut(), RprVector::default()) }; + report.message = unsafe { std::ffi::CStr::from_ptr(message) } + .to_string_lossy() + .into_owned(); + } + unsafe { + let mut observed = Report { + calls: 0, + status: RPR_OK, + message: String::new(), + nested_status: RPR_OK, + }; + let previous = rpr_set_error_handler(RprErrorHandler { + callback: Some(report), + user_data: (&mut observed as *mut Report).cast(), + }); + assert!(previous.callback.is_none()); + let mut shape = rpr_ball_shared_shape(-1.0); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(observed.calls, 1); + assert_eq!(observed.status, RPR_INVALID_ARGUMENT); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert!(shape.is_null()); + assert_eq!(observed.nested_status, RPR_NULL_POINTER); + assert!(observed.message.contains("positive")); + assert_eq!( + std::ffi::CStr::from_ptr(rpr_last_error()).to_string_lossy(), + observed.message + ); + assert_eq!(rpr_free_world(std::ptr::null_mut()), RPR_OK); + assert_eq!(observed.calls, 1); + std::thread::spawn(|| { + assert_eq!( + rpr_set_gravity(std::ptr::null_mut(), RprVector::default()), + RPR_NULL_POINTER + ); + }) + .join() + .unwrap(); + assert_eq!(observed.calls, 1); + // Description validation reports exactly one error at the build boundary. + shape = rpr_shape_desc_build(&rpr_ball_collider_desc(-1.0).shape); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(observed.calls, 2); + assert!(shape.is_null()); + let installed = rpr_set_error_handler(previous); + assert_eq!(installed.user_data, (&mut observed as *mut Report).cast()); + assert_eq!( + rpr_set_gravity(std::ptr::null_mut(), RprVector::default()), + RPR_NULL_POINTER + ); + assert_eq!(observed.calls, 2); + } +} + +#[test] +fn query_predicates_filter_hits_and_misses_are_successful() { + unsafe extern "C" fn only_selected( + data: *mut std::ffi::c_void, + _read: *const RprReadContext, + handle: RprColliderHandle, + ) -> RprBool { + (handle == unsafe { *(data.cast::()) }) as RprBool + } + unsafe { + let mut world = RprPhysicsWorld(PhysicsWorld::new()); + world.0.insert_collider( + ColliderBuilder::ball(0.5).translation(Vector::X * 2.0), + None, + ); + let far = world.0.insert_collider( + ColliderBuilder::ball(0.5).translation(Vector::X * 5.0), + None, + ); + world.0.step(); + let world = RprWorld::new(world); + let mut query = rpr_default_query_options(); + let mut selected = + RprColliderHandle::from(far).with_world((&world as *const RprWorld).cast_mut()); + query.predicate = Some(only_selected); + query.userData = (&mut selected as *mut RprColliderHandle).cast(); + + let value = rpr_cast_ray_toi( + &world, + &query, + Vector::ZERO.into(), + Vector::X.into(), + 10.0, + 1, + ); + + let mut collider = value.collider; + let mut toi = value.toi; + let mut found = value.found; + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(found, 1); + assert!(collider == selected); + assert!((toi - 4.5).abs() < 1.0e-5); + let value = rpr_cast_ray_toi( + &world, + &query, + Vector::ZERO.into(), + (-Vector::X).into(), + 10.0, + 1, + ); + + collider = value.collider; + toi = value.toi; + found = value.found; + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(found, 0); + assert_eq!(toi, 0.0); + assert!(collider == RprColliderHandle::default()); + query.predicate = None; + let value = rpr_cast_ray_toi( + &world, + &query, + Vector::ZERO.into(), + Vector::X.into(), + 10.0, + 1, + ); + + collider = value.collider; + toi = value.toi; + found = value.found; + assert_eq!(rpr_last_status(), RPR_OK); + assert!((toi - 1.5).abs() < 1.0e-5); + assert_eq!(found, 1); + assert_ne!(collider, RprColliderHandle::default()); + } +} + +#[test] +fn pid_axes_preserve_uncontrolled_motion_and_body_state() { + unsafe { + let pid = rpr_new_pid_controller(); + assert_eq!(rpr_last_status(), RPR_OK); + let mut bodies = RprRigidBodySet(RigidBodySet::new()); + let handle = bodies + .0 + .insert(RigidBodyBuilder::dynamic().linvel(Vector::Y)); + let zero = angular_out(AngVector::default()); + let gains = RprPidGains { + lin_kp: Vector::splat(10.0).into(), + lin_ki: Vector::ZERO.into(), + lin_kd: Vector::ZERO.into(), + ang_kp: zero, + ang_ki: zero, + ang_kd: zero, + }; + assert_eq!(rpr_pid_controller_set_gains(pid, gains), RPR_OK); + assert_eq!(rpr_pid_controller_set_axes(pid, 1), RPR_OK); + let mut linear = RprVector::default(); + let mut angular = zero; + assert_eq!( + native_pid_controller_rigid_body_correction( + pid, + 0.01, + &bodies, + handle.into(), + Pose::from_translation(Vector::X + Vector::Y).into(), + Vector::ZERO.into(), + zero, + &mut linear, + &mut angular + ), + RPR_OK + ); + assert!(linear.x > 0.0); + assert_eq!(linear.y, 0.0); + assert_eq!(bodies.0[handle].translation(), Vector::ZERO); + assert_eq!(bodies.0[handle].linvel(), Vector::Y); + assert_eq!(rpr_free_pid_controller(pid), RPR_OK); + } +} + +#[test] +fn soft_proxy_can_be_woken_without_exposing_mutable_rigid_body() { + unsafe { + let world = rpr_new_world(); + assert_eq!(rpr_last_status(), RPR_OK); + let desc = rpr_rope_soft_body_desc(Vector::ZERO.into(), Vector::X.into(), 3); + let handle = rpr_insert_soft_body(world, &desc); + assert_eq!(rpr_last_status(), RPR_OK); + let proxy = rpr_soft_body_root_body(handle); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!( + rpr_rigid_body_set_translation(proxy, Vector::ZERO.into(), 1), + RPR_INVALID_ARGUMENT + ); + assert_eq!(rpr_rigid_body_wake_up(proxy, 1), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn legacy_rigid_snapshot_preserves_state() { + #[derive(serde::Serialize)] + struct State<'a> { + gravity: Vector, + integration_parameters: &'a IntegrationParameters, + islands: &'a IslandManager, + broad_phase: &'a BroadPhaseBvh, + narrow_phase: &'a NarrowPhase, + bodies: &'a RigidBodySet, + colliders: &'a ColliderSet, + impulse_joints: &'a ImpulseJointSet, + multibody_joints: &'a MultibodyJointSet, + } + let mut original = PhysicsWorld::new(); + original.insert( + RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0), + ColliderBuilder::ball(0.5), + ); + original.step(); + let bytes = bincode::serialize(&State { + gravity: original.gravity, + integration_parameters: &original.integration_parameters, + islands: &original.islands, + broad_phase: &original.broad_phase, + narrow_phase: &original.narrow_phase, + bodies: &original.bodies, + colliders: &original.colliders, + impulse_joints: &original.impulse_joints, + multibody_joints: &original.multibody_joints, + }) + .unwrap(); + unsafe { + let restored = rpr_deserialize_rigid_state(bytes.as_ptr(), bytes.len()); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!((*(*restored).read().unwrap().raw()).0.bodies.len(), 1); + original.step(); + assert_eq!( + rpr_step(restored, std::ptr::null(), std::ptr::null()), + RPR_OK + ); + let a = original.bodies.iter().next().unwrap().1; + let access = (*restored).read().unwrap(); + let b = (*access.raw()).0.bodies.iter().next().unwrap().1; + assert_eq!(a.position(), b.position()); + assert_eq!(a.linvel(), b.linvel()); + drop(access); + assert_eq!(rpr_free_world(restored), RPR_OK); + } +} + +#[cfg(feature = "dim3")] +#[test] +fn render_only_meshes_have_distinct_ids_and_no_collider_cache_key() { + let mut world = PhysicsWorld::new(); + let (vertices, indices) = rapier::parry::shape::Ball::new(0.75).to_trimesh(8, 8); + let handle = world.insert_soft_body( + SoftBodyBuilder::cuboid(Vector::ZERO, Vector::ONE, 3, 3, 3) + .skin(vertices, indices) + .skin_collision(false), + ); + let body = world.soft_bodies.get(handle).unwrap(); + let native: Vec<_> = body.meshes().collect(); + assert!( + native + .iter() + .any(|m| m.is_skinned() && m.collider() == ColliderHandle::invalid()) + ); + let body = (body as *const SoftBody).cast::(); + unsafe { + let mut count = 0; + assert_eq!( + native_soft_body_meshes(body, std::ptr::null_mut(), 0, &mut count), + RPR_OK + ); + assert_eq!(count, native.len()); + let mut meshes = Vec::::with_capacity(count); + assert_eq!( + native_soft_body_meshes(body, meshes.as_mut_ptr(), count, &mut count), + RPR_OK + ); + meshes.set_len(count); + for (i, mesh) in meshes.iter().enumerate() { + assert!(!meshes[..i].iter().any(|other| other.id == mesh.id)); + let mut nv = 0; + assert_eq!( + native_soft_body_mesh_vertices_by_id( + body, + mesh.id, + std::ptr::null_mut(), + 0, + &mut nv + ), + RPR_OK + ); + let mut vertices = vec![RprVector::default(); nv]; + assert_eq!( + native_soft_body_mesh_vertices_by_id( + body, + mesh.id, + vertices.as_mut_ptr(), + nv, + &mut nv + ), + RPR_OK + ); + assert_eq!( + vertices + .iter() + .map(|v| v.raw().unwrap()) + .collect::>(), + native[i] + .vertex_positions(&world.soft_bodies[handle]) + .collect::>() + ); + let mut ni = 0; + assert_eq!( + native_soft_body_mesh_indices_by_id( + body, + mesh.id, + std::ptr::null_mut(), + 0, + &mut ni + ), + RPR_OK + ); + let mut indices = vec![0; ni]; + assert_eq!( + native_soft_body_mesh_indices_by_id( + body, + mesh.id, + indices.as_mut_ptr(), + ni, + &mut ni + ), + RPR_OK + ); + assert_eq!(indices, native[i].indices().as_flattened()); + assert!(indices.iter().all(|&v| (v as usize) < nv)); + } + assert_eq!( + native_soft_body_mesh_vertices( + body, + RprColliderHandle::default(), + std::ptr::null_mut(), + 0, + &mut count + ), + RPR_INVALID_ARGUMENT + ); + assert_eq!( + native_soft_body_mesh_colliders(body, std::ptr::null_mut(), 0, &mut count), + RPR_OK + ); + let mut handles = vec![RprColliderHandle::default(); count]; + assert_eq!( + native_soft_body_mesh_colliders(body, handles.as_mut_ptr(), count, &mut count), + RPR_OK + ); + assert!(handles.iter().all(|&h| h != RprColliderHandle::default())); + assert_eq!( + native_soft_body_mesh_vertices_by_id( + body, + RprSoftMeshId { + cluster: u32::MAX, + mesh: 0 + }, + std::ptr::null_mut(), + 0, + &mut count + ), + RPR_INVALID_HANDLE + ); + } +} + +#[test] +fn value_returns_preserve_status_until_the_next_fallible_call() { + unsafe { + assert!(rpr_ball_shared_shape(-1.0).is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + let message = std::ffi::CStr::from_ptr(rpr_last_error()).to_owned(); + let _ = rpr_build_info(); + let _ = rpr_dynamic_rigid_body_desc(); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!( + std::ffi::CStr::from_ptr(rpr_last_error()), + message.as_c_str() + ); + let world = rpr_new_world(); + assert!(!world.is_null()); + assert_eq!(rpr_last_status(), RPR_OK); + assert!( + std::ffi::CStr::from_ptr(rpr_last_error()) + .to_bytes() + .is_empty() + ); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn value_returns_discard_partial_values_after_a_panic() { + let value: RprVector = ffi_value(|out| { + ffi(|| { + unsafe { + output(out, Vector::X.into())?; + } + panic!("value return boundary test"); + }) + }); + assert_eq!(value.raw().unwrap(), Vector::ZERO); + assert_eq!(rpr_last_status(), RPR_PANIC); +} diff --git a/c/src/types.rs b/c/src/types.rs new file mode 100644 index 000000000..c337f1fcd --- /dev/null +++ b/c/src/types.rs @@ -0,0 +1,817 @@ +use crate::*; + +/// Floating-point scalar: float for f32, double for f64. +/// @ingroup math +#[cfg(feature = "f32")] +pub type RprReal = f32; +/// Floating-point scalar: float for f32, double for f64. +/// @ingroup math +#[cfg(feature = "f64")] +pub type RprReal = f64; +/// ABI booleans are uint32_t: zero is false, one is true. +/// @ingroup math +pub type RprBool = u32; +/// @ingroup errors +/// C binary ABI revision expected by this header. +pub const RPR_ABI_VERSION: u32 = 1; +/// @ingroup rigid_bodies +/// Dynamic body affected by forces and contacts. +pub const RPR_DYNAMIC: u32 = 0; +/// @ingroup rigid_bodies +/// Immovable body. +pub const RPR_FIXED: u32 = 1; +/// @ingroup rigid_bodies +/// Kinematic body controlled by its next pose. +pub const RPR_KINEMATIC_POSITION_BASED: u32 = 2; +/// @ingroup rigid_bodies +/// Kinematic body controlled by its velocity. +pub const RPR_KINEMATIC_VELOCITY_BASED: u32 = 3; +/// @ingroup colliders +/// Enable collision-start and collision-stop events for this collider. +pub const RPR_COLLISION_EVENTS: u32 = 1; +/// @ingroup colliders +/// Enable contact-force events for this collider, subject to its force threshold. +pub const RPR_CONTACT_FORCE_EVENTS: u32 = 2; +/// @ingroup colliders +/// Combine the two material coefficients by their arithmetic mean. +pub const RPR_COMBINE_AVERAGE: u32 = 0; +/// @ingroup colliders +/// Use the smaller of the two material coefficients. +pub const RPR_COMBINE_MIN: u32 = 1; +/// @ingroup colliders +/// Multiply the two material coefficients. +pub const RPR_COMBINE_MULTIPLY: u32 = 2; +/// @ingroup colliders +/// Use the larger of the two material coefficients. +pub const RPR_COMBINE_MAX: u32 = 3; + +/// Cartesian vector with two or three components. +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprVector { + /// X component. + pub x: RprReal, + /// Y component. + pub y: RprReal, + #[cfg(feature = "dim3")] + /// Z component. + pub z: RprReal, +} +impl RprVector { + pub(crate) fn raw(self) -> Result { + finite(self.x)?; + finite(self.y)?; + #[cfg(feature = "dim2")] + { + Ok(Vector::new(self.x, self.y)) + } + #[cfg(feature = "dim3")] + { + finite(self.z)?; + Ok(Vector::new(self.x, self.y, self.z)) + } + } +} +impl From for RprVector { + fn from(v: Vector) -> Self { + Self { + x: v.x, + y: v.y, + #[cfg(feature = "dim3")] + z: v.z, + } + } +} +/// Angular scalar in 2D or vector in 3D, in radians for angular displacement. +/// @ingroup math +#[cfg(feature = "dim2")] +pub type RprAngVector = RprReal; +/// Angular scalar in 2D or vector in 3D, in radians for angular displacement. +/// @ingroup math +#[cfg(feature = "dim3")] +pub type RprAngVector = RprVector; +pub(crate) fn angular(v: RprAngVector) -> Result { + #[cfg(feature = "dim2")] + { + finite(v) + } + #[cfg(feature = "dim3")] + { + v.raw() + } +} +/// 2D: angle in radians. 3D: unit quaternion in x,y,z,w order (normalized on input). +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone)] +pub struct RprRotation { + #[cfg(feature = "dim2")] + /// Rotation angle in radians. + pub angle: RprReal, + #[cfg(feature = "dim3")] + /// X component. + pub x: RprReal, + #[cfg(feature = "dim3")] + /// Y component. + pub y: RprReal, + #[cfg(feature = "dim3")] + /// Z component. + pub z: RprReal, + #[cfg(feature = "dim3")] + /// Quaternion scalar component. + pub w: RprReal, +} +impl Default for RprRotation { + fn default() -> Self { + Rotation::IDENTITY.into() + } +} +impl RprRotation { + pub(crate) fn raw(self) -> Result { + #[cfg(feature = "dim2")] + { + Ok(Rotation::from_angle(finite(self.angle)?)) + } + #[cfg(feature = "dim3")] + { + finite(self.x)?; + finite(self.y)?; + finite(self.z)?; + finite(self.w)?; + let q = Rotation::from_xyzw(self.x, self.y, self.z, self.w); + ensure( + q.length_squared().is_finite() && q.length_squared() > 1.0e-20, + "quaternion must be nonzero and finite", + )?; + Ok(q.normalize()) + } + } +} +impl From for RprRotation { + fn from(q: Rotation) -> Self { + #[cfg(feature = "dim2")] + { + Self { angle: q.angle() } + } + #[cfg(feature = "dim3")] + { + Self { + x: q.x, + y: q.y, + z: q.z, + w: q.w, + } + } + } +} +/// Rigid transform combining translation and rotation. Use TranslationPose with a zero vector for +/// identity. +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprPose { + /// Translation vector. + pub translation: RprVector, + /// Rotation value. + pub rotation: RprRotation, +} +impl RprPose { + pub(crate) fn raw(self) -> Result { + Ok(Pose::from_parts( + self.translation.raw()?, + self.rotation.raw()?, + )) + } +} +impl From for RprPose { + fn from(p: Pose) -> Self { + Self { + translation: p.translation.into(), + rotation: p.rotation.into(), + } + } +} +/// Collision membership/filter masks and their pairwise combination rule. +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprInteractionGroups { + /// Groups this object belongs to, as a 32-bit mask. + pub memberships: u32, + /// Membership groups accepted by this object, as a 32-bit mask. + pub filter: u32, + /// 0 requires both membership/filter tests; 1 accepts either test. + pub test_mode: u32, +} +impl RprInteractionGroups { + pub(crate) fn raw(self) -> Result { + ensure(self.test_mode <= 1, "invalid group test mode")?; + Ok(InteractionGroups::new( + Group::from_bits_retain(self.memberships), + Group::from_bits_retain(self.filter), + if self.test_mode == 0 { + InteractionTestMode::And + } else { + InteractionTestMode::Or + }, + )) + } +} +impl From for RprInteractionGroups { + fn from(g: InteractionGroups) -> Self { + Self { + memberships: g.memberships.bits(), + filter: g.filter.bits(), + test_mode: g.test_mode as u32, + } + } +} +/// Application-defined 128-bit value. Contains no owned pointers. +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprUserData { + /// Low 64 bits of the application value. + pub low: u64, + /// High 64 bits of the application value. + pub high: u64, +} +impl RprUserData { + pub(crate) fn raw(self) -> u128 { + self.low as u128 | ((self.high as u128) << 64) + } +} +impl From for RprUserData { + fn from(v: u128) -> Self { + Self { + low: v as u64, + high: (v >> 64) as u64, + } + } +} +/// Axis-aligned bounding box. +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprAabb { + /// Minimum corner in each coordinate. + pub mins: RprVector, + /// Maximum corner in each coordinate. + pub maxs: RprVector, +} +/// Spring softness expressed as natural frequency and damping ratio. +/// @ingroup math +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprSpringCoefficients { + /// Nonnegative spring natural frequency in Hz. + pub natural_frequency: RprReal, + /// Nonnegative damping ratio; 1 is critical damping. + pub damping_ratio: RprReal, +} +impl RprSpringCoefficients { + pub(crate) fn raw(self) -> Result> { + Ok(SpringCoefficients::new( + nonnegative(self.natural_frequency)?, + nonnegative(self.damping_ratio)?, + )) + } +} +impl From> for RprSpringCoefficients { + fn from(v: SpringCoefficients) -> Self { + Self { + natural_frequency: v.natural_frequency, + damping_ratio: v.damping_ratio, + } + } +} +/// Runtime ABI identity and fundamental type sizes. +/// @ingroup errors +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprBuildInfo { + /// Binary ABI revision; compare with the header ABI_VERSION. + pub abi_version: u32, + /// Spatial dimension, 2 or 3. + pub dimension: u32, + /// Size of Real in bytes, 4 or 8. + pub real_size: u32, + /// Pointer size in bytes. + pub pointer_size: u32, +} +/// Return ABI version, dimension, scalar size, and pointer size of the linked library. +/// @ingroup errors +#[rapier_export] +pub extern "C" fn rpr_build_info() -> RprBuildInfo { + RprBuildInfo { + abi_version: RPR_ABI_VERSION, + dimension: rapier::math::DIM as u32, + real_size: std::mem::size_of::() as u32, + pointer_size: std::mem::size_of::() as u32, + } +} +/// Release version of the loaded C bindings, e.g. "0.35.3+c.2". +/// The suffix identifies the C bindings revision for the Rust crate version. +/// The UTF-8, NUL-terminated string is borrowed for the library's lifetime; do not free it. +/// This release identifier is independent of the ABI compatibility version. +/// @ingroup errors +#[rapier_export] +pub extern "C" fn rpr_version() -> *const std::ffi::c_char { + static VERSION: &[u8] = concat!(env!("RAPIER_C_VERSION"), "\0").as_bytes(); + VERSION.as_ptr().cast() +} + +/// Cargo profile of the loaded physics library: "debug" or "release". +/// Custom profiles report the corresponding inherited Cargo profile category. +/// The UTF-8, NUL-terminated string is borrowed for the library's lifetime; do not free it. +/// This is independent of the consumer's build mode and of per-package optimization overrides. +/// @ingroup errors +#[rapier_export] +pub extern "C" fn rpr_build_profile() -> *const std::ffi::c_char { + static PROFILE: &[u8] = concat!(env!("RAPIER_CARGO_PROFILE"), "\0").as_bytes(); + PROFILE.as_ptr().cast() +} + +/// Features available through the loaded C library, independent of consumer defines. +/// @ingroup errors +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprBuildFeatures { + /// Whether native profiling timers were compiled in. + pub profiling: RprBool, + /// Solver SIMD lane count. Hardware instruction width depends on the target CPU. + pub simd_lanes: u32, + /// Whether this library exposes Rapier's parallel execution and thread-pool APIs. + pub parallel: RprBool, +} +/// Return profiling, SIMD width, and parallelism of the linked library. +/// @ingroup errors +#[rapier_export] +pub extern "C" fn rpr_build_features() -> RprBuildFeatures { + RprBuildFeatures { + profiling: cfg!(feature = "profiler") as RprBool, + simd_lanes: rapier::math::SIMD_WIDTH as u32, + parallel: cfg!(feature = "parallel") as RprBool, + } +} + +pub(crate) fn body_type(value: u32) -> Result { + match value { + 0 => Ok(RigidBodyType::Dynamic), + 1 => Ok(RigidBodyType::Fixed), + 2 => Ok(RigidBodyType::KinematicPositionBased), + 3 => Ok(RigidBodyType::KinematicVelocityBased), + _ => Err(invalid("unknown rigid body type")), + } +} +pub(crate) fn combine(value: u32) -> Result { + match value { + 0 => Ok(CoefficientCombineRule::Average), + 1 => Ok(CoefficientCombineRule::Min), + 2 => Ok(CoefficientCombineRule::Multiply), + 3 => Ok(CoefficientCombineRule::Max), + _ => Err(invalid("unknown combine rule")), + } +} + +/// Copyable non-owning handle: world pointer plus entity index and generation. +/// The world must remain alive throughout every use. Copying does not retain it. +/// UINT32_MAX/UINT32_MAX with a NULL world is invalid. +/// @ingroup rigid_bodies +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RprRigidBodyHandle { + /// Borrowed owning world. Never use this handle after freeing that world. + pub world: *mut RprWorld, + /// Slot index; UINT32_MAX denotes the explicit invalid handle. + pub index: u32, + /// Slot generation used to reject stale handles. Do not modify it. + pub generation: u32, +} +impl Default for RprRigidBodyHandle { + fn default() -> Self { + Self { + world: std::ptr::null_mut(), + index: u32::MAX, + generation: u32::MAX, + } + } +} +impl RprRigidBodyHandle { + pub(crate) fn raw(self) -> RigidBodyHandle { + RigidBodyHandle::from_raw_parts(self.index, self.generation) + } +} +impl From for RprRigidBodyHandle { + fn from(h: RigidBodyHandle) -> Self { + let (index, generation) = h.into_raw_parts(); + Self { + world: std::ptr::null_mut(), + index, + generation, + } + } +} +/// Copyable non-owning handle: world pointer plus entity index and generation. +/// The world must remain alive throughout every use. Copying does not retain it. +/// UINT32_MAX/UINT32_MAX with a NULL world is invalid. +/// @ingroup colliders +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RprColliderHandle { + /// Borrowed owning world. Never use this handle after freeing that world. + pub world: *mut RprWorld, + /// Slot index; UINT32_MAX denotes the explicit invalid handle. + pub index: u32, + /// Slot generation used to reject stale handles. Do not modify it. + pub generation: u32, +} +impl Default for RprColliderHandle { + fn default() -> Self { + Self { + world: std::ptr::null_mut(), + index: u32::MAX, + generation: u32::MAX, + } + } +} +impl RprColliderHandle { + pub(crate) fn raw(self) -> ColliderHandle { + ColliderHandle::from_raw_parts(self.index, self.generation) + } +} +impl From for RprColliderHandle { + fn from(h: ColliderHandle) -> Self { + let (index, generation) = h.into_raw_parts(); + Self { + world: std::ptr::null_mut(), + index, + generation, + } + } +} +/// Copyable non-owning handle: world pointer plus entity index and generation. +/// The world must remain alive throughout every use. Copying does not retain it. +/// UINT32_MAX/UINT32_MAX with a NULL world is invalid. +/// @ingroup joints +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RprImpulseJointHandle { + /// Borrowed owning world. Never use this handle after freeing that world. + pub world: *mut RprWorld, + /// Zero-based element index. + pub index: u32, + /// Slot generation used to reject stale handles. Do not modify it. + pub generation: u32, +} +impl Default for RprImpulseJointHandle { + fn default() -> Self { + Self { + world: std::ptr::null_mut(), + index: u32::MAX, + generation: u32::MAX, + } + } +} +impl RprImpulseJointHandle { + pub(crate) fn raw(self) -> ImpulseJointHandle { + ImpulseJointHandle::from_raw_parts(self.index, self.generation) + } +} +impl From for RprImpulseJointHandle { + fn from(h: ImpulseJointHandle) -> Self { + let (index, generation) = h.into_raw_parts(); + Self { + world: std::ptr::null_mut(), + index, + generation, + } + } +} +/// Copyable non-owning handle: world pointer plus entity index and generation. +/// The world must remain alive throughout every use. Copying does not retain it. +/// UINT32_MAX/UINT32_MAX with a NULL world is invalid. +/// @ingroup joints +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RprMultibodyJointHandle { + /// Borrowed owning world. Never use this handle after freeing that world. + pub world: *mut RprWorld, + /// Zero-based element index. + pub index: u32, + /// Slot generation used to reject stale handles. Do not modify it. + pub generation: u32, +} +impl Default for RprMultibodyJointHandle { + fn default() -> Self { + Self { + world: std::ptr::null_mut(), + index: u32::MAX, + generation: u32::MAX, + } + } +} +impl RprMultibodyJointHandle { + pub(crate) fn raw(self) -> MultibodyJointHandle { + MultibodyJointHandle::from_raw_parts(self.index, self.generation) + } +} +impl From for RprMultibodyJointHandle { + fn from(h: MultibodyJointHandle) -> Self { + let (index, generation) = h.into_raw_parts(); + Self { + world: std::ptr::null_mut(), + index, + generation, + } + } +} +/// Copyable non-owning handle: world pointer plus entity index and generation. +/// The world must remain alive throughout every use. Copying does not retain it. +/// UINT32_MAX/UINT32_MAX with a NULL world is invalid. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RprSoftBodyHandle { + /// Borrowed owning world. Never use this handle after freeing that world. + pub world: *mut RprWorld, + /// Zero-based element index. + pub index: u32, + /// Slot generation used to reject stale handles. Do not modify it. + pub generation: u32, +} +impl Default for RprSoftBodyHandle { + fn default() -> Self { + Self { + world: std::ptr::null_mut(), + index: u32::MAX, + generation: u32::MAX, + } + } +} +impl RprSoftBodyHandle { + pub(crate) fn raw(self) -> SoftBodyHandle { + SoftBodyHandle::from_raw_parts(self.index, self.generation) + } +} +impl From for RprSoftBodyHandle { + fn from(h: SoftBodyHandle) -> Self { + let (index, generation) = h.into_raw_parts(); + Self { + world: std::ptr::null_mut(), + index, + generation, + } + } +} + +/// @ingroup colliders +/// Invoke the contact-pair filtering hook for this collider. +pub const RPR_FILTER_CONTACT_PAIRS: u32 = 1; +/// @ingroup colliders +/// Invoke the sensor-intersection filtering hook for this collider. +pub const RPR_FILTER_INTERSECTION_PAIR: u32 = 2; +/// @ingroup colliders +/// Invoke the solver-contact modification hook for this collider. +pub const RPR_MODIFY_SOLVER_CONTACTS: u32 = 4; +/// @ingroup queries +/// Query filter: exclude fixed. +pub const RPR_QUERY_EXCLUDE_FIXED: u32 = 1; +/// @ingroup queries +/// Query filter: exclude kinematic. +pub const RPR_QUERY_EXCLUDE_KINEMATIC: u32 = 2; +/// @ingroup queries +/// Query filter: exclude dynamic. +pub const RPR_QUERY_EXCLUDE_DYNAMIC: u32 = 4; +/// @ingroup queries +/// Query filter: exclude sensors. +pub const RPR_QUERY_EXCLUDE_SENSORS: u32 = 8; +/// @ingroup queries +/// Query filter: exclude solids. +pub const RPR_QUERY_EXCLUDE_SOLIDS: u32 = 16; +/// @ingroup queries +/// Query filter: only dynamic. +pub const RPR_QUERY_ONLY_DYNAMIC: u32 = 3; +/// @ingroup queries +/// Query filter: only kinematic. +pub const RPR_QUERY_ONLY_KINEMATIC: u32 = 5; +/// @ingroup queries +/// Query filter: only fixed. +pub const RPR_QUERY_ONLY_FIXED: u32 = 6; +/// @ingroup events +/// Debug-render flag: collider shapes. +pub const RPR_DEBUG_COLLIDER_SHAPES: u32 = 1; +/// @ingroup events +/// Debug-render flag: rigid body axes. +pub const RPR_DEBUG_RIGID_BODY_AXES: u32 = 2; +/// @ingroup events +/// Debug-render flag: multibody joints. +pub const RPR_DEBUG_MULTIBODY_JOINTS: u32 = 4; +/// @ingroup events +/// Debug-render flag: impulse joints. +pub const RPR_DEBUG_IMPULSE_JOINTS: u32 = 8; +/// @ingroup events +/// Debug-render flag: solver contacts. +pub const RPR_DEBUG_SOLVER_CONTACTS: u32 = 16; +/// @ingroup events +/// Debug-render flag: contacts. +pub const RPR_DEBUG_CONTACTS: u32 = 32; +/// @ingroup events +/// Debug-render flag: collider aabbs. +pub const RPR_DEBUG_COLLIDER_AABBS: u32 = 64; +/// @ingroup events +/// Debug-render flag: soft bodies. +pub const RPR_DEBUG_SOFT_BODIES: u32 = 128; +/// @ingroup events +/// Debug-render flag: pseudo normals. +pub const RPR_DEBUG_PSEUDO_NORMALS: u32 = 256; +/// @ingroup events +/// Debug-render flag: soft volume contacts. +pub const RPR_DEBUG_SOFT_VOLUME_CONTACTS: u32 = 512; +/// @ingroup events +/// Debug-render flag: soft body stress. +pub const RPR_DEBUG_SOFT_BODY_STRESS: u32 = 1024; +/// @ingroup colliders +/// Require both membership/filter intersections to be nonempty. +pub const RPR_GROUPS_AND: u32 = 0; +/// @ingroup colliders +/// Accept either membership/filter intersection if both participants select OR; otherwise use AND. +pub const RPR_GROUPS_OR: u32 = 1; +/// @ingroup joints +/// Motor stiffness and damping are acceleration-based, independent of mass. +pub const RPR_MOTOR_ACCELERATION_BASED: u32 = 0; +/// @ingroup joints +/// Motor stiffness and damping are force-based, so response depends on mass. +pub const RPR_MOTOR_FORCE_BASED: u32 = 1; +/// @ingroup soft_bodies +/// Cell model that constrains volume without elastic shear response. +pub const RPR_SOFT_CELL_VOLUME: u32 = 0; +/// @ingroup soft_bodies +/// Corotational elastic cell model. +pub const RPR_SOFT_CELL_COROTATIONAL: u32 = 1; +/// @ingroup soft_bodies +/// Neo-Hookean elastic cell model. +pub const RPR_SOFT_CELL_NEO_HOOKEAN: u32 = 2; +/// @ingroup soft_bodies +/// Use the constraint-based soft-body solver. +pub const RPR_SOFT_SOLVER_CONSTRAINTS: u32 = 0; +/// @ingroup soft_bodies +/// Use the finite-element solver; requires RAPIER_FEM. +pub const RPR_SOFT_SOLVER_FEM: u32 = 1; +/// @ingroup joints +/// Joint axis index for translation along local X. +pub const RPR_AXIS_LIN_X: u32 = 0; +/// @ingroup joints +/// Joint axis index for translation along local Y. +pub const RPR_AXIS_LIN_Y: u32 = 1; +/// @ingroup rigid_bodies +/// Rigid-body lock bit: translation x. +pub const RPR_LOCK_TRANSLATION_X: u32 = 1; +/// @ingroup rigid_bodies +/// Rigid-body lock bit: translation y. +pub const RPR_LOCK_TRANSLATION_Y: u32 = 2; +/// @ingroup rigid_bodies +/// Rigid-body lock bit: translation z. +pub const RPR_LOCK_TRANSLATION_Z: u32 = 4; +/// @ingroup rigid_bodies +/// Rigid-body lock bit: rotation x. +pub const RPR_LOCK_ROTATION_X: u32 = 8; +/// @ingroup rigid_bodies +/// Rigid-body lock bit: rotation y. +pub const RPR_LOCK_ROTATION_Y: u32 = 16; +/// @ingroup rigid_bodies +/// Rigid-body lock bit: rotation z. +pub const RPR_LOCK_ROTATION_Z: u32 = 32; +/// @ingroup joints +/// Joint axis index for the first rotation: Z in 2D, local X in 3D. +#[cfg(feature = "dim2")] +pub const RPR_AXIS_ANG_X: u32 = 2; +/// @ingroup joints +/// Locked-axis mask for a fixed joint: all translations and rotations. +#[cfg(feature = "dim2")] +pub const RPR_JOINT_FIXED_AXES: u32 = 7; +/// @ingroup joints +/// Locked-axis mask for a revolute joint: only the first angular axis is free. +#[cfg(feature = "dim2")] +pub const RPR_JOINT_REVOLUTE_AXES: u32 = 3; +/// @ingroup joints +/// Locked-axis mask for a prismatic joint: only translation along local X is free. +#[cfg(feature = "dim2")] +pub const RPR_JOINT_PRISMATIC_AXES: u32 = 6; +/// @ingroup joints +/// Joint axis index for translation along local Z. +#[cfg(feature = "dim3")] +pub const RPR_AXIS_LIN_Z: u32 = 2; +/// @ingroup joints +/// Joint axis index for the first rotation: Z in 2D, local X in 3D. +#[cfg(feature = "dim3")] +pub const RPR_AXIS_ANG_X: u32 = 3; +/// @ingroup joints +/// Joint axis index for rotation around local Y. +#[cfg(feature = "dim3")] +pub const RPR_AXIS_ANG_Y: u32 = 4; +/// @ingroup joints +/// Joint axis index for rotation around local Z. +#[cfg(feature = "dim3")] +pub const RPR_AXIS_ANG_Z: u32 = 5; +/// @ingroup joints +/// Locked-axis mask for a fixed joint: all translations and rotations. +#[cfg(feature = "dim3")] +pub const RPR_JOINT_FIXED_AXES: u32 = 63; +/// @ingroup joints +/// Locked-axis mask for a revolute joint: only the first angular axis is free. +#[cfg(feature = "dim3")] +pub const RPR_JOINT_REVOLUTE_AXES: u32 = 55; +/// @ingroup joints +/// Locked-axis mask for a prismatic joint: only translation along local X is free. +#[cfg(feature = "dim3")] +pub const RPR_JOINT_PRISMATIC_AXES: u32 = 62; +/// @ingroup joints +/// Locked-axis mask for a spherical joint: translations locked, rotations free. +#[cfg(feature = "dim3")] +pub const RPR_JOINT_SPHERICAL_AXES: u32 = 7; + +pub(crate) fn angular_out(v: AngVector) -> RprAngVector { + #[cfg(feature = "dim2")] + { + v + } + #[cfg(feature = "dim3")] + { + v.into() + } +} +pub(crate) fn real_pi() -> Real { + #[cfg(feature = "f32")] + { + std::f32::consts::PI + } + #[cfg(feature = "f64")] + { + std::f64::consts::PI + } +} + +/// @ingroup soft_bodies +/// Read-only body type of soft-body cluster proxies; cannot be used to construct a rigid body. +pub const RPR_SOFT_FRAME: u32 = 4; +/// @ingroup queries +/// Shape-cast iteration limit reached before convergence. +pub const RPR_SHAPE_CAST_OUT_OF_ITERATIONS: u32 = 0; +/// @ingroup queries +/// Shape cast converged to the reported impact. +pub const RPR_SHAPE_CAST_CONVERGED: u32 = 1; +/// @ingroup queries +/// Shape-cast numerical solver failed to converge. +pub const RPR_SHAPE_CAST_FAILED: u32 = 2; +/// @ingroup queries +/// Shapes overlap or are within the target distance at the start of the cast. +pub const RPR_SHAPE_CAST_PENETRATING: u32 = 3; +/// @ingroup queries +/// The hit feature is unspecified. +pub const RPR_FEATURE_UNKNOWN: u32 = 0; +/// @ingroup queries +/// The feature ID denotes a vertex. +pub const RPR_FEATURE_VERTEX: u32 = 1; +/// @ingroup queries +/// The feature ID denotes an edge. +pub const RPR_FEATURE_EDGE: u32 = 2; +/// @ingroup queries +/// The feature ID denotes a face. +pub const RPR_FEATURE_FACE: u32 = 3; + +/// @ingroup joints +/// Insert reduced-coordinate joints as kinematic articulations. +pub const RPR_MULTIBODY_JOINTS_ARE_KINEMATIC: u8 = 1; +/// @ingroup joints +/// Disable contacts between colliders of the inserted articulation. +pub const RPR_MULTIBODY_DISABLE_SELF_CONTACTS: u8 = 2; +/// @ingroup joints +/// Skip joints that would close a loop in the articulation. +pub const RPR_MULTIBODY_SKIP_LOOP_CLOSURES: u8 = 4; +/// @ingroup joints +/// Do not import joint motors into the articulation. +pub const RPR_MULTIBODY_SKIP_JOINT_MOTORS: u8 = 8; +/// @ingroup joints +/// Do not import joint limits into the articulation. +pub const RPR_MULTIBODY_SKIP_JOINT_LIMITS: u8 = 16; +/// @ingroup joints +/// Do not import joint springs into the articulation. +pub const RPR_MULTIBODY_SKIP_JOINT_SPRINGS: u8 = 32; + +/// @ingroup shapes +/// Merge triangle-mesh vertices with identical positions. +pub const RPR_TRIMESH_MERGE_DUPLICATE_VERTICES: u32 = 16; +/// @ingroup shapes +/// Correct contact normals at internal mesh edges; includes duplicate-vertex merging. +pub const RPR_TRIMESH_FIX_INTERNAL_EDGES: u32 = 144; +/// @ingroup shapes +/// Prepare triangle-mesh acceleration data for deformation. +pub const RPR_TRIMESH_DEFORMABLE: u32 = 256; +/// @ingroup shapes +/// Correct internal-edge contacts on both sides of the triangle mesh. +pub const RPR_TRIMESH_FIX_INTERNAL_EDGES_TWO_SIDED: u32 = 656; +/// @ingroup shapes +/// Correct contact normals at internal heightfield edges. +pub const RPR_HEIGHTFIELD_FIX_INTERNAL_EDGES: u32 = 1; diff --git a/c/src/world.rs b/c/src/world.rs new file mode 100644 index 000000000..e029c3821 --- /dev/null +++ b/c/src/world.rs @@ -0,0 +1,120 @@ +//! Owning C world and dynamic borrow checks at the foreign-language boundary. +use crate::*; +use std::{ + cell::UnsafeCell, + sync::atomic::{AtomicUsize, Ordering}, +}; + +const WRITER: usize = usize::MAX; +/// Sole owner of simulation state. Handles belong to the world that created them. +/// Ordinary reads may overlap. A mutation or step requires exclusive access. +/// Destruction must be externally synchronized with all users of this pointer. +/// @ingroup worlds +pub struct RprWorld { + access: AtomicUsize, + data: UnsafeCell, +} +// Access to simulation data is protected by the atomic shared/exclusive gate. +unsafe impl Send for RprWorld {} +unsafe impl Sync for RprWorld {} +impl RprWorld { + pub(crate) fn new(data: RprPhysicsWorld) -> Self { + Self { + access: AtomicUsize::new(0), + data: UnsafeCell::new(data), + } + } + pub(crate) fn read(&self) -> Result> { + self.access + .fetch_update(Ordering::Acquire, Ordering::Relaxed, |n| { + (n < WRITER - 1).then(|| n + 1) + }) + .map_err(|_| busy())?; + Ok(WorldRead(self)) + } + pub(crate) fn write(&self) -> Result> { + self.access + .compare_exchange(0, WRITER, Ordering::Acquire, Ordering::Relaxed) + .map_err(|_| busy())?; + Ok(WorldWrite(self)) + } +} +fn busy() -> (RprStatus, String) { + (RPR_WORLD_BUSY, "world is already borrowed; use callback read access or defer the mutation until the operation returns".into()) +} +pub(crate) struct WorldRead<'a>(&'a RprWorld); +impl WorldRead<'_> { + pub(crate) fn raw(&self) -> *const RprPhysicsWorld { + self.0.data.get() + } +} +impl Drop for WorldRead<'_> { + fn drop(&mut self) { + self.0.access.fetch_sub(1, Ordering::Release); + } +} +pub(crate) struct WorldWrite<'a>(&'a RprWorld); +impl WorldWrite<'_> { + pub(crate) fn raw(&self) -> *mut RprPhysicsWorld { + self.0.data.get() + } +} +impl Drop for WorldWrite<'_> { + fn drop(&mut self) { + self.0.access.store(0, Ordering::Release); + } +} +/// Create an owned world. Release it with FreeWorld. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_new_world() -> *mut RprWorld { + ffi_value(|out: *mut *mut RprWorld| { + ffi(|| unsafe { + out_ptr(out)?; + output( + out, + Box::into_raw(Box::new(RprWorld::new( + RprPhysicsWorld(PhysicsWorld::new()), + ))), + ) + }) + }) +} +/// Free a world. NULL is allowed. Rejects destruction from an active callback. +/// The caller must prevent other threads from starting calls during destruction. +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_free_world(world: *mut RprWorld) -> RprStatus { + ffi(|| unsafe { + if world.is_null() { + return Ok(()); + } + let access = get(world)?.write()?; + // End the shared reference to the wrapper, keeping the gate closed until destruction. + std::mem::forget(access); + drop(Box::from_raw(world)); + Ok(()) + }) +} + +/// Callback-scoped read access to bodies and colliders. Never retain or free it. +/// Only the Read* functions accept this context; it cannot mutate the world. +/// @ingroup callbacks +pub struct RprReadContext { + pub(crate) world: *mut RprWorld, + pub(crate) bodies: *const RprRigidBodySet, + pub(crate) colliders: *const RprColliderSet, +} +impl RprReadContext { + pub(crate) fn new( + world: *mut RprWorld, + bodies: &RigidBodySet, + colliders: &ColliderSet, + ) -> Self { + Self { + world, + bodies: (bodies as *const RigidBodySet).cast(), + colliders: (colliders as *const ColliderSet).cast(), + } + } +} diff --git a/c/src/world_queries.rs b/c/src/world_queries.rs new file mode 100644 index 000000000..896d8d7ef --- /dev/null +++ b/c/src/world_queries.rs @@ -0,0 +1,466 @@ +//! World queries with operation-scoped native access. +#![allow(non_snake_case)] +use crate::*; +use rapier::parry::bounding_volume::Aabb; +#[derive(Clone, Copy)] +pub(crate) struct QueryAccess { + pub world: *mut RprWorld, + pub broadPhase: *const RprBroadPhaseBvh, + pub narrowPhase: *const RprNarrowPhase, + pub bodies: *const RprRigidBodySet, + pub colliders: *const RprColliderSet, + pub filter: RprQueryFilter, + pub predicate: RprQueryPredicate, + pub userData: *mut std::ffi::c_void, +} +/// Copyable query settings. They borrow callback data, never world components. +/// @ingroup queries +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RprQueryOptions { + /// Query selection settings. + pub filter: RprQueryFilter, + /// Optional additional query filter; nonzero accepts a collider. + pub predicate: RprQueryPredicate, + /// Application data; Rapier does not own pointers encoded in it. + pub userData: *mut std::ffi::c_void, +} +impl Default for RprQueryOptions { + fn default() -> Self { + Self { + filter: RprQueryFilter::default(), + predicate: None, + userData: std::ptr::null_mut(), + } + } +} +/// Return native default query options. This POD value owns no resources. +/// @ingroup queries +#[rapier_export] +pub extern "C" fn rpr_default_query_options() -> RprQueryOptions { + RprQueryOptions::default() +} +impl QueryAccess { + pub(crate) unsafe fn from_world( + owner: *mut RprWorld, + world: *const RprPhysicsWorld, + query_options: *const RprQueryOptions, + ) -> Result { + unsafe { + let options = if query_options.is_null() { + RprQueryOptions::default() + } else { + *get(query_options)? + }; + let w = &get(world)?.0; + options.filter.check_world(owner)?; + options.filter.raw()?; + Ok(Self { + world: owner, + broadPhase: std::ptr::addr_of!(w.broad_phase).cast(), + narrowPhase: std::ptr::addr_of!(w.narrow_phase).cast(), + bodies: std::ptr::addr_of!(w.bodies).cast(), + colliders: std::ptr::addr_of!(w.colliders).cast(), + filter: options.filter, + predicate: options.predicate, + userData: options.userData, + }) + } + } + + pub(crate) unsafe fn with_raw( + &self, + f: impl FnOnce(QueryPipeline<'_>) -> Result, + ) -> Result { + unsafe { + let read = + RprReadContext::new(self.world, &get(self.bodies)?.0, &get(self.colliders)?.0); + let callback = |handle: ColliderHandle, _collider: &Collider| { + self.predicate.is_none_or(|p| { + p( + self.userData, + &read, + RprColliderHandle::from(handle).with_world(self.world), + ) != 0 + }) + }; + let mut filter = self.filter.raw()?; + if self.predicate.is_some() { + filter.predicate = Some(&callback); + } + let query = get(self.broadPhase)?.0.as_query_pipeline( + get(self.narrowPhase)?.0.query_dispatcher(), + &get(self.bodies)?.0, + &get(self.colliders)?.0, + filter, + ); + f(query) + } + } +} + +/// Return the closest ray hit, or report RPR_NOT_FOUND on a miss. The ray is origin + direction * t +/// for 0 <= t <= max_toi; direction need not be normalized. solid treats an interior origin as a +/// hit at t = 0. +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup queries +#[rapier_export] +pub unsafe extern "C" fn rpr_cast_ray( + world: *const RprWorld, + query_options: *const RprQueryOptions, + origin: RprVector, + direction: RprVector, + max_toi: RprReal, + solid: RprBool, +) -> RprRayHit { + ffi_world_value(world, |out: *mut RprRayHit| { + ffi(|| unsafe { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + let query: *const QueryAccess = &query; + + let origin = origin.raw()?; + let direction = direction.raw()?; + positive(direction.length())?; + nonnegative(max_toi)?; + let solid = boolean(solid)?; + get(query)?.with_raw(|q| { + let (h, hit) = q + .cast_ray_and_get_normal(&Ray::new(origin, direction), max_toi, solid) + .ok_or((RPR_NOT_FOUND, "ray missed".into()))?; + let (feature_type, feature_id) = feature(hit.feature); + output( + out, + RprRayHit { + collider: h.into(), + time_of_impact: hit.time_of_impact, + normal: hit.normal.into(), + feature_type, + feature_id, + }, + ) + }) + }) + }) +} + +/// Return the closest surface projection within max_distance, or report RPR_NOT_FOUND. With solid = +/// 1, an interior point projects to itself. +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup queries +#[rapier_export] +pub unsafe extern "C" fn rpr_project_point( + world: *const RprWorld, + query_options: *const RprQueryOptions, + point: RprVector, + max_distance: RprReal, + solid: RprBool, +) -> RprPointProjection { + ffi_world_value(world, |out: *mut RprPointProjection| { + ffi(|| unsafe { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + let query: *const QueryAccess = &query; + + let p = point.raw()?; + nonnegative(max_distance)?; + let solid = boolean(solid)?; + get(query)?.with_raw(|q| { + let (h, p) = q + .project_point(p, max_distance, solid) + .ok_or((RPR_NOT_FOUND, "no projection".into()))?; + output( + out, + RprPointProjection { + collider: h.into(), + point: p.point.into(), + is_inside: p.is_inside as u32, + }, + ) + }) + }) + }) +} + +/// Sweep shape from pose along velocity and return the first hit; report RPR_NOT_FOUND on a miss. +/// Time is bounded by options.max_time_of_impact. +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_cast_shape( + world: *const RprWorld, + query_options: *const RprQueryOptions, + pose: RprPose, + velocity: RprVector, + shape: *const RprSharedShape, + options: RprShapeCastOptions, +) -> RprShapeCastHit { + ffi_world_value(world, |out: *mut RprShapeCastHit| { + ffi(|| unsafe { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + let query: *const QueryAccess = &query; + + let p = pose.raw()?; + let v = velocity.raw()?; + let o = options.raw()?; + get(query)?.with_raw(|q| { + let (h, r) = q + .cast_shape(&p, v, &*get(shape)?.0, o) + .ok_or((RPR_NOT_FOUND, "shape missed".into()))?; + output( + out, + RprShapeCastHit { + collider: h.into(), + time_of_impact: r.time_of_impact, + witness1: r.witness1.into(), + witness2: r.witness2.into(), + normal1: r.normal1.into(), + normal2: r.normal2.into(), + status: r.status as u32, + }, + ) + }) + }) + }) +} + +/// Copy handles of colliders containing the world-space point. +/// @see @ref output_buffers +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup queries +#[rapier_export] +pub unsafe extern "C" fn rpr_intersect_point( + world: *const RprWorld, + query_options: *const RprQueryOptions, + point: RprVector, + buffer: *mut RprColliderHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + let query: *const QueryAccess = &query; + + let point = point.raw()?; + get(query)?.with_raw(|q| { + let values: Vec<_> = q.intersect_point(point).map(|(h, _)| h.into()).collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + }) + } +} + +/// Copy handles of colliders intersecting the shape at its world-space pose. The shape is borrowed +/// for this call. +/// @see @ref output_buffers +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup shapes +#[rapier_export] +pub unsafe extern "C" fn rpr_intersect_shape( + world: *const RprWorld, + query_options: *const RprQueryOptions, + pose: RprPose, + shape: *const RprSharedShape, + buffer: *mut RprColliderHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + let query: *const QueryAccess = &query; + + let pose = pose.raw()?; + get(query)?.with_raw(|q| { + let values: Vec<_> = q + .intersect_shape(pose, &*get(shape)?.0) + .map(|(h, _)| h.into()) + .collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + }) + } +} + +/// Copy broad-phase candidates whose bounding boxes overlap the world-space AABB. Results may +/// include false positives. +/// @see @ref output_buffers +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup queries +#[rapier_export] +pub unsafe extern "C" fn rpr_intersect_aabb_conservative( + world: *const RprWorld, + query_options: *const RprQueryOptions, + aabb: RprAabb, + buffer: *mut RprColliderHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + let query: *const QueryAccess = &query; + + let mins = aabb.mins.raw()?; + let maxs = aabb.maxs.raw()?; + ensure(mins.cmple(maxs).all(), "invalid AABB")?; + get(query)?.with_raw(|q| { + let values: Vec<_> = q + .intersect_aabb_conservative(Aabb::new(mins, maxs)) + .map(|(h, _)| h.into()) + .collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + }) + } +} + +/// Return the closest ray collider and time, with found = 0 on a miss (RPR_OK). The ray is origin + +/// direction * t; max_toi bounds t. +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup queries +#[rapier_export] +pub unsafe extern "C" fn rpr_cast_ray_toi( + world: *const RprWorld, + query_options: *const RprQueryOptions, + origin: RprVector, + direction: RprVector, + max_toi: RprReal, + solid: RprBool, +) -> RprRayToi { + ffi_world_value(world, |result: *mut RprRayToi| { + let collider = unsafe { std::ptr::addr_of_mut!((*result).collider) }; + let toi = unsafe { std::ptr::addr_of_mut!((*result).toi) }; + let found = unsafe { std::ptr::addr_of_mut!((*result).found) }; + + ffi(|| unsafe { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + let query: *const QueryAccess = &query; + + out_ptr(collider)?; + out_ptr(toi)?; + out_ptr(found)?; + let origin = origin.raw()?; + let direction = direction.raw()?; + positive(direction.length())?; + nonnegative(max_toi)?; + get(query)?.with_raw(|q| { + if let Some((handle, time)) = + q.cast_ray(&Ray::new(origin, direction), max_toi, boolean(solid)?) + { + output(collider, handle.into())?; + output(toi, time)?; + output(found, 1) + } else { + output(collider, Default::default())?; + output(toi, 0.0)?; + output(found, 0) + } + }) + }) + }) +} + +/// Return the closest ray hit with found = 0 on a miss (RPR_OK). The ray is origin + direction * t; +/// solid treats an interior origin as a hit at t = 0. +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup queries +#[rapier_export] +pub unsafe extern "C" fn rpr_try_cast_ray( + world: *const RprWorld, + query_options: *const RprQueryOptions, + origin: RprVector, + direction: RprVector, + max_toi: RprReal, + solid: RprBool, +) -> RprOptionalRayHit { + ffi_world_value(world, |result: *mut RprOptionalRayHit| { + let out = unsafe { std::ptr::addr_of_mut!((*result).hit) }; + let found = unsafe { std::ptr::addr_of_mut!((*result).found) }; + + ffi(|| unsafe { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + let query: *const QueryAccess = &query; + + out_ptr(out)?; + out_ptr(found)?; + let origin = origin.raw()?; + let direction = direction.raw()?; + positive(direction.length())?; + nonnegative(max_toi)?; + get(query)?.with_raw(|q| { + if let Some((h, hit)) = q.cast_ray_and_get_normal( + &Ray::new(origin, direction), + max_toi, + boolean(solid)?, + ) { + let (feature_type, feature_id) = feature(hit.feature); + output( + out, + RprRayHit { + collider: h.into(), + time_of_impact: hit.time_of_impact, + normal: hit.normal.into(), + feature_type, + feature_id, + }, + )?; + output(found, 1) + } else { + output(out, RprRayHit::default())?; + output(found, 0) + } + }) + }) + }) +} diff --git a/c/src/world_tests.rs b/c/src/world_tests.rs new file mode 100644 index 000000000..e47851474 --- /dev/null +++ b/c/src/world_tests.rs @@ -0,0 +1,223 @@ +use crate::*; +use std::{ + ffi::c_void, + ptr, + sync::atomic::{AtomicUsize, Ordering}, +}; + +struct CallbackState { + world: *mut RprWorld, + calls: AtomicUsize, +} +unsafe fn check_hook_access( + data: *mut c_void, + read: *const RprReadContext, + collider: RprColliderHandle, +) { + unsafe { + let state = &*data.cast::(); + let _position = rpr_read_collider_translation(read, collider); + assert_eq!(rpr_last_status(), RPR_OK); + let _count = rpr_collider_count(state.world); + assert_eq!(rpr_last_status(), RPR_WORLD_BUSY); + assert_eq!( + rpr_set_gravity(state.world, Vector::ZERO.into()), + RPR_WORLD_BUSY + ); + assert_eq!( + rpr_step(state.world, ptr::null(), ptr::null()), + RPR_WORLD_BUSY + ); + assert_eq!(rpr_free_world(state.world), RPR_WORLD_BUSY); + state.calls.fetch_add(1, Ordering::Relaxed); + } +} +unsafe extern "C" fn filter_pair( + data: *mut c_void, + read: *const RprReadContext, + a: RprColliderHandle, + _b: RprColliderHandle, + _ba: RprRigidBodyHandle, + _bb: RprRigidBodyHandle, +) -> i32 { + unsafe { check_hook_access(data, read, a) }; + 1 +} +unsafe extern "C" fn modify_contacts( + data: *mut c_void, + read: *const RprReadContext, + a: RprColliderHandle, + _b: RprColliderHandle, + context: *mut RprContactModificationContext, +) { + unsafe { + check_hook_access(data, read, a); + assert_eq!( + rpr_contact_modification_context_set_tangent_velocity(context, Vector::ZERO.into()), + RPR_OK + ); + } +} + +#[test] +fn hooks_read_scoped_state_and_reject_world_reentry() { + unsafe { + let world = rpr_new_world(); + assert_eq!(rpr_last_status(), RPR_OK); + let mut collider = rpr_ball_collider_desc(1.0); + collider.activeHooks = 1 | 4; + let fixed = rpr_fixed_rigid_body_desc(); + let mut dynamic = rpr_dynamic_rigid_body_desc(); + dynamic.position.translation = Vector::Y.into(); + let fixed_handle = rpr_insert_rigid_body(world, &fixed); + assert_eq!(rpr_last_status(), RPR_OK); + rpr_insert_collider(fixed_handle, &collider); + assert_eq!(rpr_last_status(), RPR_OK); + let dynamic_handle = rpr_insert_rigid_body(world, &dynamic); + assert_eq!(rpr_last_status(), RPR_OK); + rpr_insert_collider(dynamic_handle, &collider); + assert_eq!(rpr_last_status(), RPR_OK); + let mut state = CallbackState { + world, + calls: AtomicUsize::new(0), + }; + let hooks = RprPhysicsHooks { + user_data: (&mut state as *mut CallbackState).cast(), + filter_contact_pair: Some(filter_pair), + modify_solver_contacts_context: Some(modify_contacts), + ..Default::default() + }; + assert_eq!(rpr_step(world, &hooks, ptr::null()), RPR_OK); + assert!(state.calls.load(Ordering::Relaxed) >= 2); + // The step released its exclusive access, so the deferred change now succeeds. + assert_eq!(rpr_set_gravity(world, Vector::ZERO.into()), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +unsafe extern "C" fn query_predicate( + data: *mut c_void, + read: *const RprReadContext, + collider: RprColliderHandle, +) -> RprBool { + unsafe { + let state = &*data.cast::(); + let _position = rpr_read_collider_translation(read, collider); + assert_eq!(rpr_last_status(), RPR_OK); + let count = rpr_collider_count(state.world); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(count, 1); + assert_eq!( + rpr_set_gravity(state.world, Vector::ZERO.into()), + RPR_WORLD_BUSY + ); + assert_eq!(rpr_free_world(state.world), RPR_WORLD_BUSY); + + // Read-only queries can reenter, provided the nested query doesn't recurse indefinitely. + let hit = rpr_cast_ray( + state.world, + ptr::null(), + Vector::ZERO.into(), + Vector::X.into(), + 10.0, + 1, + ); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(hit.collider, collider); + state.calls.fetch_add(1, Ordering::Relaxed); + 1 + } +} + +#[test] +fn query_options_are_owner_independent_and_nested_reads_are_allowed() { + unsafe { + let world = rpr_new_world(); + assert_eq!(rpr_last_status(), RPR_OK); + let mut desc = rpr_ball_collider_desc(0.5); + desc.position.translation = (Vector::X * 2.0).into(); + rpr_insert_collider_without_parent(world, &desc); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!( + rpr_detect_collisions(world, ptr::null(), ptr::null()), + RPR_OK + ); + let mut state = CallbackState { + world, + calls: AtomicUsize::new(0), + }; + let mut options = rpr_default_query_options(); + options.predicate = Some(query_predicate); + options.userData = (&mut state as *mut CallbackState).cast(); + let _hit = rpr_cast_ray( + world, + &options, + Vector::ZERO.into(), + Vector::X.into(), + 10.0, + 1, + ); + assert_eq!(rpr_last_status(), RPR_OK); + assert!(state.calls.load(Ordering::Relaxed) > 0); + assert_eq!(rpr_set_gravity(world, Vector::ZERO.into()), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn world_gate_checks_conflicts_across_threads_and_releases_after_errors() { + fn thread_safe() {} + thread_safe::(); + let world = RprWorld::new(RprPhysicsWorld(PhysicsWorld::new())); + let read = world.read().unwrap(); + std::thread::scope(|scope| { + scope + .spawn(|| { + let count = unsafe { rpr_rigid_body_count(&world) }; + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(count, 0); + assert!(world.write().is_err()); + }) + .join() + .unwrap(); + }); + drop(read); + let write = world.write().unwrap(); + assert!(world.read().is_err()); + assert!(world.write().is_err()); + drop(write); + assert_eq!( + ffi(|| { + let _write = world.write()?; + panic!("release the guard"); + }), + RPR_PANIC + ); + assert!(world.write().is_ok()); +} + +#[test] +fn removing_a_body_can_preserve_its_colliders() { + unsafe { + let world = rpr_new_world(); + assert_eq!(rpr_last_status(), RPR_OK); + + let desc = rpr_dynamic_rigid_body_desc(); + let shape = rpr_ball_collider_desc(0.5); + let body = rpr_insert_rigid_body(world, &desc); + assert_eq!(rpr_last_status(), RPR_OK); + let collider = rpr_insert_collider(body, &shape); + assert_eq!(rpr_last_status(), RPR_OK); + let mut removed = rpr_remove_rigid_body(body, 0); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(removed, 1); + let parent = rpr_collider_parent(collider); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(parent, RprRigidBodyHandle::default()); + assert_eq!(rpr_rigid_body_validate_handle(body), RPR_INVALID_HANDLE); + removed = rpr_remove_rigid_body(body, 1); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(removed, 0); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} diff --git a/c/testbed/.clang-format b/c/testbed/.clang-format new file mode 100644 index 000000000..2ba613a80 --- /dev/null +++ b/c/testbed/.clang-format @@ -0,0 +1,16 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Right +AllowShortBlocksOnASingleLine: Never +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +InsertBraces: true +BreakBeforeBraces: Attach +KeepEmptyLinesAtTheStartOfBlocks: false +SeparateDefinitionBlocks: Always +SortIncludes: Never + +TypenameMacros: [RAPIER_TYPE] diff --git a/c/testbed/CMakeLists.txt b/c/testbed/CMakeLists.txt new file mode 100644 index 000000000..ed44ef4ca --- /dev/null +++ b/c/testbed/CMakeLists.txt @@ -0,0 +1,140 @@ +# The headless runner shares exactly the same C scene sources and callbacks. +file(GLOB_RECURSE TB_SCENES CONFIGURE_DEPENDS "examples${RAPIER_DIMENSION}d/*.c") +add_library(rapier_testbed_core STATIC testbed.c headless.c grab.c "registry${RAPIER_DIMENSION}.c" ${TB_SCENES}) +target_link_libraries(rapier_testbed_core PUBLIC Rapier::rapier) +target_include_directories(rapier_testbed_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") +target_compile_features(rapier_testbed_core PUBLIC c_std_11) +target_compile_definitions(rapier_testbed_core PUBLIC TB_ASSET_ROOT="${RAPIER_ROOT}/assets") +if(NOT MSVC) + target_link_libraries(rapier_testbed_core PUBLIC m) + target_compile_options(rapier_testbed_core PRIVATE -Wall -Wextra) +endif() +rapier_executable(rapier_testbed_headless headless_main.c) +target_link_libraries(rapier_testbed_headless PRIVATE rapier_testbed_core) +# Explicit opt-in target; performance checks are not pass/fail timing tests. +rapier_executable(rapier_testbed_step_benchmark tools/step_benchmark.c) +set_target_properties(rapier_testbed_step_benchmark PROPERTIES EXCLUDE_FROM_ALL TRUE) +target_link_libraries(rapier_testbed_step_benchmark PRIVATE rapier_testbed_core) +if(RAPIER_TESTBED_GRAPHICS) + if(DEFINED PLATFORM AND NOT PLATFORM STREQUAL "Desktop") + message(FATAL_ERROR "The testbed renderer supports PLATFORM=Desktop only (macOS, Windows, Linux/X11). Use RAPIER_TESTBED_GRAPHICS=OFF for other targets.") + endif() + include(cmake/Dependencies.cmake) + # Normal variables stay in this directory: don't alter the parent project's options. + set(BUILD_EXAMPLES OFF) + set(BUILD_SHARED_LIBS OFF) + # raylib 6.0's custom CMake flag parser treats even `#define FLAG 0` as ON. + # Use config.h defaults and explicit numeric overrides to retain automatic + # buffer swaps, input polling, and frame timing in EndDrawing(). + set(CUSTOMIZE_BUILD OFF) + set(SUPPORT_MODULE_RAUDIO OFF CACHE BOOL "" FORCE) + set(USE_AUDIO OFF CACHE BOOL "" FORCE) + set(USE_EXTERNAL_GLFW OFF CACHE STRING "" FORCE) + set(GLFW_BUILD_WAYLAND OFF CACHE BOOL "" FORCE) + set(GLFW_BUILD_X11 ON CACHE BOOL "" FORCE) + set(OPENGL_VERSION "3.3" CACHE STRING "" FORCE) + add_subdirectory("${raylib_SOURCE_DIR}" "${raylib_BINARY_DIR}" EXCLUDE_FROM_ALL) + target_compile_definitions(raylib PRIVATE + SUPPORT_MODULE_RAUDIO=0 SUPPORT_CUSTOM_FRAME_CONTROL=0 SUPPORT_BUSY_WAIT_LOOP=0 + # Rapier supplies meshes; Dear ImGui supplies font loading. Keep raylib's + # default font/white texture, image decoders, screenshots, and mesh drawing. + SUPPORT_FILEFORMAT_OBJ=0 SUPPORT_FILEFORMAT_MTL=0 SUPPORT_FILEFORMAT_IQM=0 + SUPPORT_FILEFORMAT_GLTF=0 SUPPORT_FILEFORMAT_GLTF_WRITE=0 + SUPPORT_FILEFORMAT_VOX=0 SUPPORT_FILEFORMAT_M3D=0 SUPPORT_MESH_GENERATION=0 + SUPPORT_FILEFORMAT_TTF=0 SUPPORT_FILEFORMAT_FNT=0 + SUPPORT_COMPRESSION_API=0 SUPPORT_GESTURES_SYSTEM=0 SUPPORT_MOUSE_GESTURES=0) + # Version-matched C wrappers and renderer; the testbed itself stays C11. + set(TB_IMGUI_DIR "${imgui_SOURCE_DIR}") + add_library(rapier_testbed_imgui STATIC + "${cimgui_SOURCE_DIR}/cimgui.cpp" "${rlimgui_SOURCE_DIR}/rlImGui.cpp" + "${TB_IMGUI_DIR}/imgui.cpp" "${TB_IMGUI_DIR}/imgui_draw.cpp" + "${TB_IMGUI_DIR}/imgui_widgets.cpp" "${TB_IMGUI_DIR}/imgui_tables.cpp" + "${TB_IMGUI_DIR}/imgui_demo.cpp") + target_compile_features(rapier_testbed_imgui PRIVATE cxx_std_17) + target_compile_definitions(rapier_testbed_imgui PUBLIC NO_FONT_AWESOME CIMGUI_NO_EXPORT) + target_include_directories(rapier_testbed_imgui SYSTEM PUBLIC + "${cimgui_SOURCE_DIR}" + "${rlimgui_SOURCE_DIR}" "${TB_IMGUI_DIR}") + target_include_directories(rapier_testbed_imgui SYSTEM PRIVATE "${TB_CIMGUI_INCLUDE_DIR}") + target_link_libraries(rapier_testbed_imgui PUBLIC raylib) + # Embed the font with CMake alone: no runtime asset path, font installation, + # generator dependency, or runtime network access is required. + set(TB_FONT_FILE "${fira_font_SOURCE_DIR}/FiraSans-Regular.ttf") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${TB_FONT_FILE}") + file(READ "${TB_FONT_FILE}" TB_FONT_HEX HEX) + string(REGEX REPLACE "(..)" "0x\\1," TB_FONT_BYTES "${TB_FONT_HEX}") + # Bound source line length for compilers such as MSVC. + string(REPEAT "0x[0-9a-f][0-9a-f]," 16 TB_FONT_ROW) + string(REGEX REPLACE "(${TB_FONT_ROW})" "\\1\n" TB_FONT_BYTES "${TB_FONT_BYTES}") + configure_file(font_data.h.in "${CMAKE_CURRENT_BINARY_DIR}/font_data.h" @ONLY) + unset(TB_FONT_HEX) + unset(TB_FONT_BYTES) + unset(TB_FONT_ROW) + rapier_executable(rapier_testbed gui.c) + target_include_directories(rapier_testbed PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + set_property(TARGET rapier_testbed APPEND PROPERTY LINK_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/THIRD_PARTY_NOTICES.txt") + add_custom_command(TARGET rapier_testbed POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/THIRD_PARTY_NOTICES.txt" + "$/THIRD_PARTY_NOTICES.txt" + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/licenses" + "$/licenses" + COMMAND "${CMAKE_COMMAND}" -E make_directory + "$/licenses/glfw-mingw" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${raylib_SOURCE_DIR}/src/external/glfw/deps/mingw/dinput.h" + "${raylib_SOURCE_DIR}/src/external/glfw/deps/mingw/xinput.h" + "${raylib_SOURCE_DIR}/src/external/glfw/deps/mingw/_mingw_dxhelper.h" + "$/licenses/glfw-mingw" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${raylib_SOURCE_DIR}/LICENSE" + "$/raylib-LICENSE.txt" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${raylib_SOURCE_DIR}/src/external/glfw/LICENSE.md" + "$/GLFW-LICENSE.txt" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/licenses/FiraSans-LICENSE.txt" + "$/FiraSans-LICENSE.txt" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${cimgui_SOURCE_DIR}/LICENSE" + "$/cimgui-LICENSE.txt" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${imgui_SOURCE_DIR}/LICENSE.txt" + "$/DearImGui-LICENSE.txt" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${rlimgui_SOURCE_DIR}/LICENSE" + "$/rlImGui-LICENSE.txt" + VERBATIM) + target_sources(rapier_testbed PRIVATE graphics.c) + target_link_libraries(rapier_testbed PRIVATE rapier_testbed_core rapier_testbed_imgui) + if(RAPIER_BUILD_TESTS) + rapier_executable(rapier_testbed_ui_test tests/ui.c) + target_sources(rapier_testbed_ui_test PRIVATE graphics.c) + target_include_directories(rapier_testbed_ui_test PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + target_link_libraries(rapier_testbed_ui_test PRIVATE rapier_testbed_core rapier_testbed_imgui) + add_test(NAME testbed_ui_input COMMAND rapier_testbed_ui_test) + rapier_executable(rapier_testbed_soft_render_test tests/soft_render.c) + target_link_libraries(rapier_testbed_soft_render_test PRIVATE rapier_testbed_core raylib) + add_test(NAME testbed_soft_render COMMAND rapier_testbed_soft_render_test) + set_tests_properties(testbed_soft_render PROPERTIES TIMEOUT 30) + endif() +endif() +if(RAPIER_BUILD_TESTS) + rapier_executable(rapier_testbed_grab_test tests/grab.c) + target_link_libraries(rapier_testbed_grab_test PRIVATE rapier_testbed_core) + add_test(NAME testbed_mouse_grab COMMAND rapier_testbed_grab_test) + rapier_executable(rapier_testbed_interactive_test tests/interactive.c) + target_link_libraries(rapier_testbed_interactive_test PRIVATE rapier_testbed_core) + add_test(NAME testbed_interactions COMMAND rapier_testbed_interactive_test) + rapier_executable(rapier_testbed_threading_test tests/threading.c) + target_link_libraries(rapier_testbed_threading_test PRIVATE rapier_testbed_core) + add_test(NAME testbed_threading COMMAND rapier_testbed_threading_test) + rapier_executable(rapier_testbed_error_test tests/errors.c) + target_link_libraries(rapier_testbed_error_test PRIVATE rapier_testbed_core) + add_test(NAME testbed_error_reporting COMMAND "${CMAKE_COMMAND}" + "-DEXECUTABLE=$" -P "${CMAKE_CURRENT_SOURCE_DIR}/tests/errors.cmake") + add_test(NAME testbed_rigid COMMAND rapier_testbed_headless --example "restitution${RAPIER_DIMENSION}" --steps 120 --no-sleep) + add_test(NAME testbed_dynamic COMMAND rapier_testbed_headless --example $,add_remove2,fountain3> --steps 120 --no-sleep) +endif() diff --git a/c/testbed/README.md b/c/testbed/README.md new file mode 100644 index 000000000..554a5dabb --- /dev/null +++ b/c/testbed/README.md @@ -0,0 +1,149 @@ +# Rapier C testbed + +The viewer uses raylib for graphics and Dear ImGui through cimgui for its C UI. +CMake downloads pinned sources and Fira Sans Regular during the first configuration. +A C/C++ compiler, Cargo, CMake 3.25+, and an internet connection for that initial +download are required. Subsequent builds reuse the downloaded sources. +macOS uses system frameworks. Linux requires the usual X11/OpenGL development +packages needed to compile raylib's bundled GLFW. See [dependencies.md](dependencies.md) for versions, offline builds, and licenses. + +From the repository root: + +```sh +cmake -S c -B build/c3 -DRAPIER_BUILD_TESTBED=ON -DRAPIER_DIMENSION=3 \ + -DRAPIER_PROFILE=release -DCMAKE_BUILD_TYPE=Release +cmake --build build/c3 --config Release --parallel +build/c3/testbed/rapier_testbed --example restitution3 +``` + +With a multi-configuration generator, the executable is under `testbed/Release/`. +Use a separate build directory and `-DRAPIER_DIMENSION=2` for 2D; use +`-DRAPIER_PRECISION=64` for double precision. The visible **Physics: Release/Debug** +label is read from the loaded Rapier library's `r3BuildProfile` function. +`RAPIER_PROFILE` selects Cargo's profile; `CMAKE_BUILD_TYPE` / `--config` selects +the C/C++ build mode. They are independent. The workspace development profile +currently has `opt-level=1`; the label still correctly identifies it as Debug. + +The header also reports **SIMD lanes**, **parallel support**, and the actual worker +count from the loaded library. Build options are `-DRAPIER_ENABLE_PARALLEL=ON|OFF` +(default ON for the testbed) and `-DRAPIER_SIMD_LANES=4|8` (default 4). This branch +always uses SIMD; eight lanes require f32 and cannot be combined with +`enhanced-determinism`. See [binding build options](../README.md#build-the-library). + +Choose workers under **Settings > Execution**, then Apply, or pass `--threads N` +to either executable. `0` selects automatic sizing, `1` uses one worker, and the +viewer accepts explicit counts through 256. The UI shows the resolved worker +count. Changes preserve the running scene and persist through restart, scene +switches, and snapshot restore. Builds without parallel support run serially and +explain how to enable the control. + +## Controls + +- Examples: grouped selection and search. Prev/Next cycle through matching available + demos, wrapping at the ends. Scenes requiring disabled build features are hidden. +- Settings: worker count, timestep, gravity, solver iterations, CCD substeps, and scene settings. + Scene-specific settings and the sleeping option apply on Restart. +- Performance: native physics time per step (the same counter as the Rust UI), + simulation time per rendered frame with its step count, and draw CPU time. + The viewer advances one step per rendered frame, like the Rust viewer. + Simulation includes the example's per-frame logic; compare the **ms/step** value with Rust. Draw CPU excludes the UI and GPU execution. +- Debug: surfaces, wireframes, body axes, contacts, joints, soft constraints/stress. +- T: play/pause. S: one step. R: restart. F: frame the simulation. +- Left drag: pull a dynamic object with a spring joint, including articulated links + and individual soft-body particles. Release removes the temporary joint and anchor. + Dragging needs the simulation running (or single-stepping) to move the object. +- Right drag: arc-ball orbit about the camera target in 3D, or pan in 2D. + Shift + right drag or middle drag: pan in 3D. Mouse wheel: zoom. +- Deformable polylines use three-pixel ribbons. Sensor colliders are translucent, + sorted behind-to-front with depth writes disabled during their render pass. +- Arrows and Enter: example controls. Space/Right Ctrl: character up/down; Shift: slow movement. + Hold C: cut in the cutting demo. Space adds a voxel in the 3D voxel demo; Left Shift + Space removes it. Text entry and focused UI widgets capture + keyboard input, and UI interactions do not control the scene camera. +- Save/Restore: in-memory physics snapshots. Disabled for scenes with local animation state + because physics snapshots do not serialize those C variables. + +The UI uses automatic layout and scrollable panels, keyboard navigation, and a +TrueType font rasterized at the display's framebuffer density. `--ui-tab Settings` +(or `Examples`, `Performance`, `Debug`) chooses the initial tab. + +## Headless and smoke runs + +```sh +build/c3/testbed/rapier_testbed_headless --list +build/c3/testbed/rapier_testbed_headless --example restitution3 --steps 120 --no-sleep --threads 1 +build/c3/testbed/rapier_testbed --example restitution3 --frames 90 --screenshot preview.png +ctest --test-dir build/c3 -C Release --output-on-failure +``` + +The `testbed_ui_input` CTest case exercises real ImGui text-input capture and +simulation shortcut routing, example navigation, and orbit-camera geometry using a +null renderer; it needs no display or GPU. `testbed_mouse_grab` checks rigid and soft +spring dragging, fixed/sensor exclusion, temporary-cluster cleanup, and deletion of +a grabbed object. +The C ABI test checks the library-reported profile and execution features against +CMake's options. `testbed_threading` verifies live worker changes and persistence +across restart, scene switches, and snapshot restore, including serial builds. +[Manual C-versus-Rust timing checks](tools/README.md) replay identical no-sleep +snapshots with both APIs and compare the final poses and velocities exactly. + +Configure `-DRAPIER_TESTBED_GRAPHICS=OFF` to build only the headless runner without +raylib, ImGui, or display requirements. Viewer and headless runner use the same C +scene sources. `--assets PATH` overrides the repository asset directory. + +## Example source + +Each scene uses the Rapier C API directly, following its matching Rust example. +Each example owns its render loop, physics stepping, events, and local animation +state. The viewer renders one frame and processes input when the example calls +`tbRenderFrame`; it does not wrap physics construction or stepping. + +## Port coverage + +All **202 Rust catalog entries** have matching C ports: **88 in 2D and 114 in 3D**. +`coverage.json` records the correspondence; `update_catalog.py` regenerates the +registries from the Rust catalog and C sources. Compile-time feature requirements +remain explicit in the example picker. +`--all` reports passed, failed, and unavailable counts separately; unavailable +scenes are not validation passes. The UI covers the controls described above; +advanced Rust profiler and internal physics-counter panels are not yet ported. + + +## Optional examples + +Enable the native FEM solver with `-DRAPIER_FEATURES=fem`. For 3D/f32, use +`-DRAPIER_FEATURES=fem,robotics` to include the URDF, MJCF, and MuJoCo Menagerie +examples too. Robotics is limited to 3D/f32 because the Rust loader crates have +that same restriction. These features add Rust dependencies through Cargo, not +extra C dependency installation steps. + +The URDF and Cassie MJCF examples use the repository's `assets/3d` files. +Menagerie discovers `/scene*.xml` in `../mujoco_menagerie`; set +`RAPIER_MENAGERIE_DIR` to use another checkout. It exposes model and keyframe +pickers, both joint representations, collision and spring switches, and live +actuator strength. Render meshes retain their colors, UVs, smooth normals, +textures, and material parameters; the lightweight raylib lighting differs +from the Rust viewer's rendering. + +The deserialization debug example requires `snapshotN.bincode` files in +`RAPIER_SNAPSHOT_DIR` (the Rust example's original directory is the fallback). +These are the legacy rigid-state files from the identical Rust build, not the +C binding's tagged whole-world snapshots. Missing or incompatible files report +an error; they do not count as a validated scene. + +OBJ models are read by a small example utility. The 2D logo's checked-in mesh +uses the Rust example's SVG tessellator, so there is no SVG dependency at runtime. +To regenerate it after editing the source logo: + +```sh +cargo run -p rapier-c-logo-mesh > c/testbed/examples2d/utils/logo_mesh.h +``` + +`testbed_interactions` drives the actual C example loops with synthetic input: +IK target convergence, switching from kinematic to PID control, and 3D voxel +addition/removal. It runs without a graphics context. + +The `testbed_soft_render` regression test traverses and draws the soft-mesh +geometry with recorded draw calls, without a window or GPU. It covers the +cluster/skin demos, bounds cache allocations, and checks that back-face culling +stays disabled until raylib flushes queued mesh triangles. All mesh surfaces are +rendered two-sided. diff --git a/c/testbed/THIRD_PARTY_NOTICES.txt b/c/testbed/THIRD_PARTY_NOTICES.txt new file mode 100644 index 000000000..92d358ed0 --- /dev/null +++ b/c/testbed/THIRD_PARTY_NOTICES.txt @@ -0,0 +1,1912 @@ +Third-party notices for the Rapier C testbed + +These notices cover the testbed renderer and its desktop backends. Component names +and source paths identify the applicable notices; they do not change their terms. +Complete supplemental licenses are in the adjacent licenses directory. +GLAD-generated loaders use the upstream licensing terms reproduced in GLAD.txt; +Khronos Apache-2.0 material additionally uses Apache-2.0.txt. QOI and the +embedded Proggy fonts use the MIT terms in MIT.txt with the copyrights below. +The GLFW fallback dinput.h and xinput.h headers remain LGPL-2.1-or-later; complete +source copies accompany the viewer under licenses/glfw-mingw. + +============================================================================== +raylib/src/config.h +============================================================================== + +LICENSE: zlib/libpng + + Copyright (c) 2018-2026 Ahmad Fatoum and Ramon Santamaria (@raysan5) + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +******************************************************************************************** + +============================================================================== +raylib/src/external/fix_win32_compatibility.h +============================================================================== + +LICENSE: MIT + + Copyright (c) 2025 Jeffery Myers + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +******************************************************************************************** + +============================================================================== +raylib/src/external/glad.h +============================================================================== + +* Copyright (c) 2008-2018 The Khronos Group Inc. +* +* Permission is hereby granted, free of charge, to any person obtaining a +* copy of this software and/or associated documentation files (the +* "Materials"), to deal in the Materials without restriction, including +* without limitation the rights to use, copy, modify, merge, publish, +* distribute, sublicense, and/or sell copies of the Materials, and to +* permit persons to whom the Materials are furnished to do so, subject to +* the following conditions: +* +* The above copyright notice and this permission notice shall be included +* in all copies or substantial portions of the Materials. +* +* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +/ + +============================================================================== +raylib/src/external/glfw/deps/mingw/_mingw_dxhelper.h +============================================================================== + +This file has no copyright assigned and is placed in the Public Domain. +This file is part of the mingw-w64 runtime package. +No warranty is given; refer to the file DISCLAIMER within this package. +/ + +============================================================================== +raylib/src/external/glfw/deps/mingw/dinput.h +============================================================================== + +Copyright (C) the Wine project + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA +/ + +============================================================================== +raylib/src/external/glfw/deps/mingw/xinput.h +============================================================================== + +The Wine project - Xinput Joystick Library +Copyright 2008 Andrew Fenn + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA +/ + +============================================================================== +raylib/src/external/glfw/include/GLFW/glfw3.h +============================================================================== + +GLFW 3.4 - www.glfw.org +A library for OpenGL, window and input +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +*********************************************************************** + +============================================================================== +raylib/src/external/glfw/include/GLFW/glfw3native.h +============================================================================== + +GLFW 3.4 - www.glfw.org +A library for OpenGL, window and input +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2018 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +*********************************************************************** + +============================================================================== +raylib/src/external/glfw/src/cocoa_init.m +============================================================================== + +======================================================================== +GLFW 3.4 macOS (modified for raylib) - www.glfw.org; www.raylib.com +------------------------------------------------------------------------ +Copyright (c) 2009-2019 Camilla Löwy +Copyright (c) 2024 M374LX + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/cocoa_joystick.h +============================================================================== + +======================================================================== +GLFW 3.4 Cocoa - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2006-2017 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/cocoa_joystick.m +============================================================================== + +======================================================================== +GLFW 3.4 Cocoa - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2009-2019 Camilla Löwy +Copyright (c) 2012 Torsten Walluhn + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/cocoa_monitor.m +============================================================================== + +======================================================================== +GLFW 3.4 macOS (modified for raylib) - www.glfw.org; www.raylib.com +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy +Copyright (c) 2024 M374LX + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/cocoa_platform.h +raylib/src/external/glfw/src/cocoa_window.m +raylib/src/external/glfw/src/nsgl_context.m +============================================================================== + +======================================================================== +GLFW 3.4 macOS - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2009-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/cocoa_time.c +============================================================================== + +======================================================================== +GLFW 3.4 macOS - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2009-2016 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/cocoa_time.h +============================================================================== + +======================================================================== +GLFW 3.4 macOS - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2009-2021 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/context.c +============================================================================== + +======================================================================== +GLFW 3.4 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2016 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/egl_context.c +============================================================================== + +======================================================================== +GLFW 3.4 EGL - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/glx_context.c +============================================================================== + +======================================================================== +GLFW 3.4 GLX - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/init.c +raylib/src/external/glfw/src/platform.h +raylib/src/external/glfw/src/vulkan.c +============================================================================== + +======================================================================== +GLFW 3.4 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2018 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/input.c +raylib/src/external/glfw/src/internal.h +raylib/src/external/glfw/src/monitor.c +============================================================================== + +======================================================================== +GLFW 3.4 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/linux_joystick.c +============================================================================== + +======================================================================== +GLFW 3.4 Linux - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2017 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/linux_joystick.h +raylib/src/external/glfw/src/xkb_unicode.h +============================================================================== + +======================================================================== +GLFW 3.4 Linux - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2014 Jonas Ådahl + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/mappings.h +raylib/src/external/glfw/src/mappings.h.in +============================================================================== + +======================================================================== +GLFW 3.4 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2006-2018 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== +As mappings.h.in, this file is used by CMake to produce the mappings.h +header file. If you are adding a GLFW specific gamepad mapping, this is +where to put it. +======================================================================== +As mappings.h, this provides all pre-defined gamepad mappings, including +all available in SDL_GameControllerDB. Do not edit this file. Any gamepad +mappings not specific to GLFW should be submitted to SDL_GameControllerDB. +This file can be re-generated from mappings.h.in and the upstream +gamecontrollerdb.txt with the 'update_mappings' CMake target. +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/mappings.h +raylib/src/external/glfw/src/mappings.h.in +============================================================================== + +All gamepad mappings not labeled GLFW are copied from the +SDL_GameControllerDB project under the following license: + +Simple DirectMedia Layer +Copyright (C) 1997-2013 Sam Lantinga + +This software is provided 'as-is', without any express or implied warranty. +In no event will the authors be held liable for any damages arising from the +use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +============================================================================== +raylib/src/external/glfw/src/null_init.c +raylib/src/external/glfw/src/null_platform.h +============================================================================== + +======================================================================== +GLFW 3.4 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2016 Google Inc. +Copyright (c) 2016-2017 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/null_joystick.c +============================================================================== + +======================================================================== +GLFW 3.4 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2016-2017 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/null_joystick.h +============================================================================== + +======================================================================== +GLFW 3.4 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2006-2017 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/null_monitor.c +============================================================================== + +======================================================================== +GLFW 3.4 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2016 Google Inc. +Copyright (c) 2016-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/null_window.c +============================================================================== + +======================================================================== +GLFW 3.4 (modified for raylib) - www.glfw.org; www.raylib.com +------------------------------------------------------------------------ +Copyright (c) 2016 Google Inc. +Copyright (c) 2016-2019 Camilla Löwy +Copyright (c) 2024 M374LX + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/osmesa_context.c +============================================================================== + +======================================================================== +GLFW 3.4 OSMesa - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2016 Google Inc. +Copyright (c) 2016-2017 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/platform.c +============================================================================== + +======================================================================== +GLFW 3.4 (modified for raylib) - www.glfw.org; www.raylib.com +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2018 Camilla Löwy +Copyright (c) 2024 M374LX + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/posix_module.c +============================================================================== + +======================================================================== +GLFW 3.4 POSIX - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2021 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/posix_poll.c +raylib/src/external/glfw/src/posix_poll.h +============================================================================== + +======================================================================== +GLFW 3.4 POSIX - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2022 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/posix_thread.c +raylib/src/external/glfw/src/posix_thread.h +raylib/src/external/glfw/src/posix_time.c +raylib/src/external/glfw/src/posix_time.h +============================================================================== + +======================================================================== +GLFW 3.4 POSIX - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2017 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/wgl_context.c +============================================================================== + +======================================================================== +GLFW 3.4 WGL - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/win32_init.c +raylib/src/external/glfw/src/win32_window.c +============================================================================== + +======================================================================== +GLFW 3.4 Win32 (modified for raylib) - www.glfw.org; www.raylib.com +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy +Copyright (c) 2024 M374LX + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/win32_joystick.c +raylib/src/external/glfw/src/win32_monitor.c +raylib/src/external/glfw/src/win32_platform.h +============================================================================== + +======================================================================== +GLFW 3.4 Win32 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/win32_joystick.h +============================================================================== + +======================================================================== +GLFW 3.4 Win32 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2006-2017 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/win32_module.c +============================================================================== + +======================================================================== +GLFW 3.4 Win32 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2021 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/win32_thread.c +raylib/src/external/glfw/src/win32_thread.h +raylib/src/external/glfw/src/win32_time.c +raylib/src/external/glfw/src/win32_time.h +============================================================================== + +======================================================================== +GLFW 3.4 Win32 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2017 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/window.c +============================================================================== + +======================================================================== +GLFW 3.4 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy +Copyright (c) 2012 Torsten Walluhn + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/x11_init.c +raylib/src/external/glfw/src/x11_window.c +============================================================================== + +======================================================================== +GLFW 3.4 X11 (modified for raylib) - www.glfw.org; www.raylib.com +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy +Copyright (c) 2024 M374LX + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/x11_monitor.c +raylib/src/external/glfw/src/x11_platform.h +============================================================================== + +======================================================================== +GLFW 3.4 X11 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/glfw/src/xkb_unicode.c +============================================================================== + +======================================================================== +GLFW 3.4 X11 - www.glfw.org +------------------------------------------------------------------------ +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2017 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +======================================================================== + +============================================================================== +raylib/src/external/qoi.h +============================================================================== + +Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org +SPDX-License-Identifier: MIT + +============================================================================== +raylib/src/external/rltexgpu.h +raylib/src/rmodels.c +raylib/src/rshapes.c +raylib/src/rtext.c +raylib/src/rtextures.c +============================================================================== + +LICENSE: zlib/libpng + + Copyright (c) 2013-2026 Ramon Santamaria (@raysan5) + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +******************************************************************************************** + +============================================================================== +raylib/src/external/rprand.h +============================================================================== + +LICENSE: zlib/libpng + + Copyright (c) 2023 Ramon Santamaria (@raysan5) + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +******************************************************************************************** + +============================================================================== +raylib/src/external/stb_image.h +raylib/src/external/stb_image_resize2.h +raylib/src/external/stb_image_write.h +raylib/src/external/stb_perlin.h +imgui/imstb_rectpack.h +imgui/imstb_textedit.h +imgui/imstb_truetype.h +============================================================================== + +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +/ + +============================================================================== +raylib/src/platforms/rcore_desktop_glfw.c +raylib/src/rcore.c +============================================================================== + +LICENSE: zlib/libpng + + Copyright (c) 2013-2026 Ramon Santamaria (@raysan5) and contributors + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +******************************************************************************************** + +============================================================================== +raylib/src/raylib.h +============================================================================== + +LICENSE: zlib/libpng + + raylib is licensed under an unmodified zlib/libpng license, which is an OSI-certified, + BSD-like license that allows static linking with closed source software: + + Copyright (c) 2013-2026 Ramon Santamaria (@raysan5) + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +******************************************************************************************** + +============================================================================== +raylib/src/raymath.h +============================================================================== + +LICENSE: zlib/libpng + + Copyright (c) 2015-2026 Ramon Santamaria (@raysan5) + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +******************************************************************************************** + +============================================================================== +raylib/src/rcamera.h +============================================================================== + +LICENSE: zlib/libpng + + Copyright (c) 2022-2026 Christoph Wagner (@Crydsch) and Ramon Santamaria (@raysan5) + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +******************************************************************************************** + +============================================================================== +raylib/src/rlgl.h +============================================================================== + +LICENSE: zlib/libpng + + Copyright (c) 2014-2026 Ramon Santamaria (@raysan5) + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +******************************************************************************************** + +============================================================================== +imgui/imgui_draw.cpp +============================================================================== + +----------------------------------------------------------------------------- +[SECTION] Default font data (ProggyClean.ttf) +----------------------------------------------------------------------------- +MIT License / Copyright (c) 2004, 2005 Tristan Grimmer +Download and more information at https://github.com/bluescan/proggyfonts +----------------------------------------------------------------------------- + +============================================================================== +imgui/imgui_draw.cpp +============================================================================== + +----------------------------------------------------------------------------- +[SECTION] Default font data (ProggyForever-Regular-minimal.ttf) +----------------------------------------------------------------------------- +Based on ProggyForever: https://github.com/ocornut/proggyforever +MIT license / Copyright (c) 2026 Disco Hello, Copyright (c) 2019,2023 Tristan Grimmer +----------------------------------------------------------------------------- + +============================================================================== +rlImGui/imgui_impl_raylib.h +============================================================================== + +raylibExtras * Utilities and Shared Components for Raylib + + rlImGui * basic ImGui integration + + LICENSE: ZLIB + Copyright (c) 2020-2021 Jeffery Myers + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + + +******************************************************************************************** + +============================================================================== +rlImGui/rlImGui.cpp +============================================================================== + +raylibExtras * Utilities and Shared Components for Raylib + + rlImGui * basic ImGui integration + + LICENSE: ZLIB + Copyright (c) 2020-2021 Jeffery Myers + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +******************************************************************************************** + +============================================================================== +rlImGui/rlImGui.h +============================================================================== + +raylibExtras * Utilities and Shared Components for Raylib + + rlImGui * basic ImGui integration + + LICENSE: ZLIB + Copyright (c) 2020-2021 Jeffery Myers + + This software is provided "as-is", without any express or implied warranty. In no event + will the authors be held liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + + + +******************************************************************************************** + +============================================================================== +raylib/LICENSE +============================================================================== + +Copyright (c) 2013-2026 Ramon Santamaria (@raysan5) + +This software is provided "as-is", without any express or implied warranty. In no event +will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial +applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +============================================================================== +cimgui/LICENSE +============================================================================== + +The MIT License (MIT) + +Copyright (c) 2015 Stephan Dilly + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +============================================================================== +imgui/LICENSE.txt +============================================================================== + +The MIT License (MIT) + +Copyright (c) 2014-2026 Omar Cornut + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +============================================================================== +rlImGui/LICENSE +============================================================================== + +Copyright (c) 2020-2021 Jeffery Myers + +This software is provided "as-is", without any express or implied warranty. In no event +will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial +applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + +============================================================================== +fira/LICENSE +============================================================================== + +Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +============================================================================== +raylib/src/external/glfw/LICENSE.md +============================================================================== + +Copyright (c) 2002-2006 Marcus Geelnard + +Copyright (c) 2006-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + diff --git a/c/testbed/cmake/Dependencies.cmake b/c/testbed/cmake/Dependencies.cmake new file mode 100644 index 000000000..9ae0081ee --- /dev/null +++ b/c/testbed/cmake/Dependencies.cmake @@ -0,0 +1,43 @@ +# Fetch sources only: the testbed controls which modules and targets are built. +# SOURCE_SUBDIR deliberately names a nonexistent directory to avoid configuring +# the upstream examples, generators, and independent CMake projects. +if(CMAKE_VERSION VERSION_LESS 3.25) + message(FATAL_ERROR "The graphical testbed requires CMake 3.25 or newer (raylib 6.0).") +endif() +include(FetchContent) +FetchContent_Declare(raylib + URL https://codeload.github.com/raysan5/raylib/tar.gz/dbc56a87da87d973a9c5baa4e7438a9d20121d28 + URL_HASH SHA256=81b06ce7c19cf3b634b0271c23c361ba6ad8bf45fb8b036abbfeb4260ec1e126 + DOWNLOAD_EXTRACT_TIMESTAMP FALSE TLS_VERIFY TRUE + SOURCE_SUBDIR rapier-no-cmake) +FetchContent_Declare(cimgui + URL https://codeload.github.com/cimgui/cimgui/tar.gz/d3f0c2f4a7d4d116ef908295b971a36bdfdafe27 + URL_HASH SHA256=39bae7634da15173b6dd9b9aea6719148d9509c0cd625b6f81cab77720656ca9 + DOWNLOAD_EXTRACT_TIMESTAMP FALSE TLS_VERIFY TRUE + SOURCE_SUBDIR rapier-no-cmake) +# This is the imgui submodule revision recorded by the pinned cimgui commit. +FetchContent_Declare(imgui + URL https://codeload.github.com/ocornut/imgui/tar.gz/dac07199cfd761113d966eb8ad739254e10df2fe + URL_HASH SHA256=d95368fadc5a1665fc1b3f198d4c9ff026e8bd8edcc7bad5d82fa12cf379c5f3 + DOWNLOAD_EXTRACT_TIMESTAMP FALSE TLS_VERIFY TRUE + SOURCE_SUBDIR rapier-no-cmake) +FetchContent_Declare(rlimgui + URL https://codeload.github.com/raylib-extras/rlImGui/tar.gz/3bc5731c4216bb8caa67fbea24aa85ce80d57ccb + URL_HASH SHA256=2198c4eb4c0a2b8efc2204da046ef014a2c7b736e1483039badee6d19a7ed35a + DOWNLOAD_EXTRACT_TIMESTAMP FALSE TLS_VERIFY TRUE + SOURCE_SUBDIR rapier-no-cmake) +# Download just the UI font, not Mozilla's complete font collection. +FetchContent_Declare(fira_font + URL https://raw.githubusercontent.com/mozilla/Fira/fd8c8c0a3d353cd99e8ca1662942d165e6961407/ttf/FiraSans-Regular.ttf + URL_HASH SHA256=a389cef71891df1232370fcebd7cfde5f74e741967070399adc91fd069b2094b + DOWNLOAD_NO_EXTRACT TRUE TLS_VERIFY TRUE) +FetchContent_MakeAvailable(raylib cimgui imgui rlimgui fira_font) + +# cimgui uses "./imgui/..." includes, but GitHub archives omit its submodule. +# Reproduce that header layout in the build directory without modifying either +# downloaded sources or user-supplied FETCHCONTENT_SOURCE_DIR_* checkouts. +set(TB_CIMGUI_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/cimgui-include") +foreach(header imgui.h imgui_internal.h imconfig.h imstb_rectpack.h imstb_textedit.h imstb_truetype.h) + configure_file("${imgui_SOURCE_DIR}/${header}" + "${TB_CIMGUI_INCLUDE_DIR}/imgui/${header}" COPYONLY) +endforeach() diff --git a/c/testbed/coverage.json b/c/testbed/coverage.json new file mode 100644 index 000000000..da33efcf5 --- /dev/null +++ b/c/testbed/coverage.json @@ -0,0 +1,1820 @@ +[ + { + "id": "add_remove2", + "dimension": 2, + "group": "Collisions", + "name": "Add remove", + "rust": "examples2d/add_remove2.rs", + "c": "examples2d/add_remove2.c", + "feature": null + }, + { + "id": "drum2", + "dimension": 2, + "group": "Collisions", + "name": "Drum", + "rust": "examples2d/drum2.rs", + "c": "examples2d/drum2.c", + "feature": null + }, + { + "id": "inv_pyramid2", + "dimension": 2, + "group": "Collisions", + "name": "Inv pyramid", + "rust": "examples2d/inv_pyramid2.rs", + "c": "examples2d/inv_pyramid2.c", + "feature": null + }, + { + "id": "platform2", + "dimension": 2, + "group": "Collisions", + "name": "Platform", + "rust": "examples2d/platform2.rs", + "c": "examples2d/platform2.c", + "feature": null + }, + { + "id": "pyramid2", + "dimension": 2, + "group": "Collisions", + "name": "Pyramid", + "rust": "examples2d/pyramid2.rs", + "c": "examples2d/pyramid2.c", + "feature": null + }, + { + "id": "sensor2", + "dimension": 2, + "group": "Collisions", + "name": "Sensor", + "rust": "examples2d/sensor2.rs", + "c": "examples2d/sensor2.c", + "feature": null + }, + { + "id": "convex_polygons2", + "dimension": 2, + "group": "Collisions", + "name": "Convex polygons", + "rust": "examples2d/convex_polygons2.rs", + "c": "examples2d/convex_polygons2.c", + "feature": null + }, + { + "id": "heightfield2", + "dimension": 2, + "group": "Collisions", + "name": "Heightfield", + "rust": "examples2d/heightfield2.rs", + "c": "examples2d/heightfield2.c", + "feature": null + }, + { + "id": "polyline2", + "dimension": 2, + "group": "Collisions", + "name": "Polyline", + "rust": "examples2d/polyline2.rs", + "c": "examples2d/polyline2.c", + "feature": null + }, + { + "id": "trimesh2", + "dimension": 2, + "group": "Collisions", + "name": "Trimesh", + "rust": "examples2d/trimesh2.rs", + "c": "examples2d/trimesh2.c", + "feature": null + }, + { + "id": "voxels2", + "dimension": 2, + "group": "Collisions", + "name": "Voxels", + "rust": "examples2d/voxels2.rs", + "c": "examples2d/voxels2.c", + "feature": null + }, + { + "id": "collision_groups2", + "dimension": 2, + "group": "Collisions", + "name": "Collision groups", + "rust": "examples2d/collision_groups2.rs", + "c": "examples2d/collision_groups2.c", + "feature": null + }, + { + "id": "one_way_platforms2", + "dimension": 2, + "group": "Collisions", + "name": "One-way platforms", + "rust": "examples2d/one_way_platforms2.rs", + "c": "examples2d/one_way_platforms2.c", + "feature": null + }, + { + "id": "locked_rotations2", + "dimension": 2, + "group": "Dynamics", + "name": "Locked rotations", + "rust": "examples2d/locked_rotations2.rs", + "c": "examples2d/locked_rotations2.c", + "feature": null + }, + { + "id": "restitution2", + "dimension": 2, + "group": "Dynamics", + "name": "Restitution", + "rust": "examples2d/restitution2.rs", + "c": "examples2d/restitution2.c", + "feature": null + }, + { + "id": "damping2", + "dimension": 2, + "group": "Dynamics", + "name": "Damping", + "rust": "examples2d/damping2.rs", + "c": "examples2d/damping2.c", + "feature": null + }, + { + "id": "ccd2", + "dimension": 2, + "group": "Dynamics", + "name": "CCD", + "rust": "examples2d/ccd2.rs", + "c": "examples2d/ccd2.c", + "feature": null + }, + { + "id": "joints2", + "dimension": 2, + "group": "Joints", + "name": "Joints", + "rust": "examples2d/joints2.rs", + "c": "examples2d/joints2.c", + "feature": null + }, + { + "id": "rope_joints2", + "dimension": 2, + "group": "Joints", + "name": "Rope Joints", + "rust": "examples2d/rope_joints2.rs", + "c": "examples2d/rope_joints2.c", + "feature": null + }, + { + "id": "pin_slot_joint2", + "dimension": 2, + "group": "Joints", + "name": "Pin Slot Joint", + "rust": "examples2d/pin_slot_joint2.rs", + "c": "examples2d/pin_slot_joint2.c", + "feature": null + }, + { + "id": "joint_motor_position2", + "dimension": 2, + "group": "Joints", + "name": "Joint motor position", + "rust": "examples2d/joint_motor_position2.rs", + "c": "examples2d/joint_motor_position2.c", + "feature": null + }, + { + "id": "inverse_kinematics2", + "dimension": 2, + "group": "Joints", + "name": "Inverse kinematics", + "rust": "examples2d/inverse_kinematics2.rs", + "c": "examples2d/inverse_kinematics2.c", + "feature": null + }, + { + "id": "multi_pendulum2", + "dimension": 2, + "group": "Joints", + "name": "Multi Pendulum", + "rust": "examples2d/multi_pendulum2.rs", + "c": "examples2d/multi_pendulum2.c", + "feature": null + }, + { + "id": "soft_bodies2", + "dimension": 2, + "group": "Soft bodies", + "name": "Soft bodies", + "rust": "examples2d/soft_bodies2.rs", + "c": "examples2d/soft_bodies2.c", + "feature": null + }, + { + "id": "soft_blobs2", + "dimension": 2, + "group": "Soft bodies", + "name": "Blobs", + "rust": "examples2d/soft_blobs2.rs", + "c": "examples2d/soft_blobs2.c", + "feature": null + }, + { + "id": "soft_jelly2", + "dimension": 2, + "group": "Soft bodies", + "name": "Jelly", + "rust": "examples2d/soft_jelly2.rs", + "c": "examples2d/soft_jelly2.c", + "feature": null + }, + { + "id": "soft_surface2", + "dimension": 2, + "group": "Soft bodies", + "name": "Deformable polylines", + "rust": "examples2d/soft_surface2.rs", + "c": "examples2d/soft_surface2.c", + "feature": null + }, + { + "id": "soft_pile2", + "dimension": 2, + "group": "Soft bodies", + "name": "Soft pile", + "rust": "examples2d/soft_pile2.rs", + "c": "examples2d/soft_pile2.c", + "feature": null + }, + { + "id": "soft_thin_features2", + "dimension": 2, + "group": "Soft bodies", + "name": "Thin features", + "rust": "examples2d/soft_thin_features2.rs", + "c": "examples2d/soft_thin_features2.c", + "feature": null + }, + { + "id": "soft_letters2", + "dimension": 2, + "group": "Soft bodies", + "name": "Soft letters", + "rust": "examples2d/soft_letters2.rs", + "c": "examples2d/soft_letters2.c", + "feature": null + }, + { + "id": "soft_plasticity2", + "dimension": 2, + "group": "Soft bodies", + "name": "Plasticity", + "rust": "examples2d/soft_plasticity2.rs", + "c": "examples2d/soft_plasticity2.c", + "feature": null + }, + { + "id": "soft_tearing2", + "dimension": 2, + "group": "Soft bodies", + "name": "Tearing", + "rust": "examples2d/soft_tearing2.rs", + "c": "examples2d/soft_tearing2.c", + "feature": null + }, + { + "id": "soft_force_tearing2", + "dimension": 2, + "group": "Soft bodies", + "name": "Force tearing", + "rust": "examples2d/soft_force_tearing2.rs", + "c": "examples2d/soft_force_tearing2.c", + "feature": null + }, + { + "id": "soft_cutting2", + "dimension": 2, + "group": "Soft bodies", + "name": "Cutting", + "rust": "examples2d/soft_cutting2.rs", + "c": "examples2d/soft_cutting2.c", + "feature": null + }, + { + "id": "soft_stress2", + "dimension": 2, + "group": "Soft bodies", + "name": "Stress coloring", + "rust": "examples2d/soft_stress2.rs", + "c": "examples2d/soft_stress2.c", + "feature": null + }, + { + "id": "soft_fem2", + "dimension": 2, + "group": "Soft bodies", + "name": "Soft FEM", + "rust": "examples2d/soft_fem2.rs", + "c": "examples2d/soft_fem2.c", + "feature": "fem" + }, + { + "id": "soft_joints2", + "dimension": 2, + "group": "Soft bodies", + "name": "Soft joints", + "rust": "examples2d/soft_joints2.rs", + "c": "examples2d/soft_joints2.c", + "feature": null + }, + { + "id": "character_controller2", + "dimension": 2, + "group": "Controls", + "name": "Character controller", + "rust": "examples2d/character_controller2.rs", + "c": "examples2d/character_controller2.c", + "feature": null + }, + { + "id": "debug_angular_limits2", + "dimension": 2, + "group": "Debug", + "name": "Angular limits", + "rust": "examples2d/debug_angular_limits2.rs", + "c": "examples2d/debug_angular_limits2.c", + "feature": null + }, + { + "id": "debug_box_ball2", + "dimension": 2, + "group": "Debug", + "name": "Box ball", + "rust": "examples2d/debug_box_ball2.rs", + "c": "examples2d/debug_box_ball2.c", + "feature": null + }, + { + "id": "debug_compression2", + "dimension": 2, + "group": "Debug", + "name": "Compression", + "rust": "examples2d/debug_compression2.rs", + "c": "examples2d/debug_compression2.c", + "feature": null + }, + { + "id": "debug_intersection2", + "dimension": 2, + "group": "Debug", + "name": "Intersection", + "rust": "examples2d/debug_intersection2.rs", + "c": "examples2d/debug_intersection2.c", + "feature": null + }, + { + "id": "debug_many_colliders2", + "dimension": 2, + "group": "Debug", + "name": "Many colliders", + "rust": "examples2d/debug_many_colliders2.rs", + "c": "examples2d/debug_many_colliders2.c", + "feature": null + }, + { + "id": "debug_total_overlap2", + "dimension": 2, + "group": "Debug", + "name": "Total overlap", + "rust": "examples2d/debug_total_overlap2.rs", + "c": "examples2d/debug_total_overlap2.c", + "feature": null + }, + { + "id": "debug_vertical_column2", + "dimension": 2, + "group": "Debug", + "name": "Vertical column", + "rust": "examples2d/debug_vertical_column2.rs", + "c": "examples2d/debug_vertical_column2.c", + "feature": null + }, + { + "id": "debug_self_intersect2", + "dimension": 2, + "group": "Debug", + "name": "Self intersect", + "rust": "examples2d/debug_self_intersect2.rs", + "c": "examples2d/debug_self_intersect2.c", + "feature": null + }, + { + "id": "s2d_high_mass_ratio_1", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "High mass ratio 1", + "rust": "examples2d/s2d_high_mass_ratio_1.rs", + "c": "examples2d/s2d_high_mass_ratio_1.c", + "feature": null + }, + { + "id": "s2d_high_mass_ratio_2", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "High mass ratio 2", + "rust": "examples2d/s2d_high_mass_ratio_2.rs", + "c": "examples2d/s2d_high_mass_ratio_2.c", + "feature": null + }, + { + "id": "s2d_high_mass_ratio_3", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "High mass ratio 3", + "rust": "examples2d/s2d_high_mass_ratio_3.rs", + "c": "examples2d/s2d_high_mass_ratio_3.c", + "feature": null + }, + { + "id": "s2d_confined", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "Confined", + "rust": "examples2d/s2d_confined.rs", + "c": "examples2d/s2d_confined.c", + "feature": null + }, + { + "id": "s2d_pyramid", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "Pyramid", + "rust": "examples2d/s2d_pyramid.rs", + "c": "examples2d/s2d_pyramid.c", + "feature": null + }, + { + "id": "s2d_card_house", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "Card house", + "rust": "examples2d/s2d_card_house.rs", + "c": "examples2d/s2d_card_house.c", + "feature": null + }, + { + "id": "s2d_arch", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "Arch", + "rust": "examples2d/s2d_arch.rs", + "c": "examples2d/s2d_arch.c", + "feature": null + }, + { + "id": "s2d_bridge", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "Bridge", + "rust": "examples2d/s2d_bridge.rs", + "c": "examples2d/s2d_bridge.c", + "feature": null + }, + { + "id": "s2d_ball_and_chain", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "Ball and chain", + "rust": "examples2d/s2d_ball_and_chain.rs", + "c": "examples2d/s2d_ball_and_chain.c", + "feature": null + }, + { + "id": "s2d_joint_grid", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "Joint grid", + "rust": "examples2d/s2d_joint_grid.rs", + "c": "examples2d/s2d_joint_grid.c", + "feature": null + }, + { + "id": "s2d_far_pyramid", + "dimension": 2, + "group": "Inspired by Solver 2D", + "name": "Far pyramid", + "rust": "examples2d/s2d_far_pyramid.rs", + "c": "examples2d/s2d_far_pyramid.c", + "feature": null + }, + { + "id": "b2d_compounds", + "dimension": 2, + "group": "Third-party benchmarks", + "name": "Compounds", + "rust": "examples2d/b2d_compounds.rs", + "c": "examples2d/b2d_compounds.c", + "feature": null + }, + { + "id": "b2d_joint_grid", + "dimension": 2, + "group": "Third-party benchmarks", + "name": "Joint grid", + "rust": "examples2d/b2d_joint_grid.rs", + "c": "examples2d/b2d_joint_grid.c", + "feature": null + }, + { + "id": "b2d_junkyard", + "dimension": 2, + "group": "Third-party benchmarks", + "name": "Junkyard", + "rust": "examples2d/b2d_junkyard.rs", + "c": "examples2d/b2d_junkyard.c", + "feature": null + }, + { + "id": "b2d_large_pyramid", + "dimension": 2, + "group": "Third-party benchmarks", + "name": "Large pyramid", + "rust": "examples2d/b2d_large_pyramid.rs", + "c": "examples2d/b2d_large_pyramid.c", + "feature": null + }, + { + "id": "b2d_many_pyramids", + "dimension": 2, + "group": "Third-party benchmarks", + "name": "Many pyramids", + "rust": "examples2d/b2d_many_pyramids.rs", + "c": "examples2d/b2d_many_pyramids.c", + "feature": null + }, + { + "id": "b2d_rain", + "dimension": 2, + "group": "Third-party benchmarks", + "name": "Rain", + "rust": "examples2d/b2d_rain.rs", + "c": "examples2d/b2d_rain.c", + "feature": null + }, + { + "id": "b2d_smash", + "dimension": 2, + "group": "Third-party benchmarks", + "name": "Smash", + "rust": "examples2d/b2d_smash.rs", + "c": "examples2d/b2d_smash.c", + "feature": null + }, + { + "id": "b2d_spinner", + "dimension": 2, + "group": "Third-party benchmarks", + "name": "Spinner", + "rust": "examples2d/b2d_spinner.rs", + "c": "examples2d/b2d_spinner.c", + "feature": null + }, + { + "id": "b2d_tumbler", + "dimension": 2, + "group": "Third-party benchmarks", + "name": "Tumbler", + "rust": "examples2d/b2d_tumbler.rs", + "c": "examples2d/b2d_tumbler.c", + "feature": null + }, + { + "id": "b2d_washer", + "dimension": 2, + "group": "Third-party benchmarks", + "name": "Washer", + "rust": "examples2d/b2d_washer.rs", + "c": "examples2d/b2d_washer.c", + "feature": null + }, + { + "id": "stress_tests_balls2", + "dimension": 2, + "group": "Stress tests", + "name": "Balls", + "rust": "examples2d/stress_tests/balls2.rs", + "c": "examples2d/stress_tests/balls2.c", + "feature": null + }, + { + "id": "stress_tests_boxes2", + "dimension": 2, + "group": "Stress tests", + "name": "Boxes", + "rust": "examples2d/stress_tests/boxes2.rs", + "c": "examples2d/stress_tests/boxes2.c", + "feature": null + }, + { + "id": "stress_tests_capsules2", + "dimension": 2, + "group": "Stress tests", + "name": "Capsules", + "rust": "examples2d/stress_tests/capsules2.rs", + "c": "examples2d/stress_tests/capsules2.c", + "feature": null + }, + { + "id": "stress_tests_convex_polygons2", + "dimension": 2, + "group": "Stress tests", + "name": "Convex polygons", + "rust": "examples2d/stress_tests/convex_polygons2.rs", + "c": "examples2d/stress_tests/convex_polygons2.c", + "feature": null + }, + { + "id": "stress_tests_heightfield2", + "dimension": 2, + "group": "Stress tests", + "name": "Heightfield", + "rust": "examples2d/stress_tests/heightfield2.rs", + "c": "examples2d/stress_tests/heightfield2.c", + "feature": null + }, + { + "id": "stress_tests_large_pyramids2", + "dimension": 2, + "group": "Stress tests", + "name": "Large pyramids", + "rust": "examples2d/stress_tests/large_pyramids2.rs", + "c": "examples2d/stress_tests/large_pyramids2.c", + "feature": null + }, + { + "id": "stress_tests_many_pyramids2", + "dimension": 2, + "group": "Stress tests", + "name": "Many pyramids", + "rust": "examples2d/stress_tests/many_pyramids2.rs", + "c": "examples2d/stress_tests/many_pyramids2.c", + "feature": null + }, + { + "id": "stress_tests_pyramid2", + "dimension": 2, + "group": "Stress tests", + "name": "Pyramid", + "rust": "examples2d/stress_tests/pyramid2.rs", + "c": "examples2d/stress_tests/pyramid2.c", + "feature": null + }, + { + "id": "stress_tests_ragdolls2", + "dimension": 2, + "group": "Stress tests", + "name": "Ragdoll piles", + "rust": "examples2d/stress_tests/ragdolls2.rs", + "c": "examples2d/stress_tests/ragdolls2.c", + "feature": null + }, + { + "id": "stress_tests_ropes2", + "dimension": 2, + "group": "Stress tests", + "name": "Ropes", + "rust": "examples2d/stress_tests/ropes2.rs", + "c": "examples2d/stress_tests/ropes2.c", + "feature": null + }, + { + "id": "stress_tests_vertical_stacks2", + "dimension": 2, + "group": "Stress tests", + "name": "Verticals stacks", + "rust": "examples2d/stress_tests/vertical_stacks2.rs", + "c": "examples2d/stress_tests/vertical_stacks2.c", + "feature": null + }, + { + "id": "stress_tests_joint_ball2", + "dimension": 2, + "group": "Stress tests", + "name": "(Stress test) joint ball", + "rust": "examples2d/stress_tests/joint_ball2.rs", + "c": "examples2d/stress_tests/joint_ball2.c", + "feature": null + }, + { + "id": "stress_tests_joint_fixed2", + "dimension": 2, + "group": "Stress tests", + "name": "(Stress test) joint fixed", + "rust": "examples2d/stress_tests/joint_fixed2.rs", + "c": "examples2d/stress_tests/joint_fixed2.c", + "feature": null + }, + { + "id": "stress_tests_joint_prismatic2", + "dimension": 2, + "group": "Stress tests", + "name": "(Stress test) joint prismatic", + "rust": "examples2d/stress_tests/joint_prismatic2.rs", + "c": "examples2d/stress_tests/joint_prismatic2.c", + "feature": null + }, + { + "id": "stress_tests_soft_blobs2", + "dimension": 2, + "group": "Stress tests", + "name": "Soft blobs", + "rust": "examples2d/stress_tests/soft_blobs2.rs", + "c": "examples2d/stress_tests/soft_blobs2.c", + "feature": null + }, + { + "id": "stress_tests_soft_jellies2", + "dimension": 2, + "group": "Stress tests", + "name": "Soft jellies", + "rust": "examples2d/stress_tests/soft_jellies2.rs", + "c": "examples2d/stress_tests/soft_jellies2.c", + "feature": null + }, + { + "id": "stress_tests_soft_ropes2", + "dimension": 2, + "group": "Stress tests", + "name": "Soft ropes", + "rust": "examples2d/stress_tests/soft_ropes2.rs", + "c": "examples2d/stress_tests/soft_ropes2.c", + "feature": null + }, + { + "id": "stress_tests_soft_strips2", + "dimension": 2, + "group": "Stress tests", + "name": "Soft strips", + "rust": "examples2d/stress_tests/soft_strips2.rs", + "c": "examples2d/stress_tests/soft_strips2.c", + "feature": null + }, + { + "id": "stress_tests_soft_cloth_keva2", + "dimension": 2, + "group": "Stress tests", + "name": "Soft cloth on Keva tower", + "rust": "examples2d/stress_tests/soft_cloth_keva2.rs", + "c": "examples2d/stress_tests/soft_cloth_keva2.c", + "feature": null + }, + { + "id": "stress_tests_soft_slab2", + "dimension": 2, + "group": "Stress tests", + "name": "Soft slab shower", + "rust": "examples2d/stress_tests/soft_slab2.rs", + "c": "examples2d/stress_tests/soft_slab2.c", + "feature": null + }, + { + "id": "stress_tests_soft_fem_beams2", + "dimension": 2, + "group": "Stress tests", + "name": "Soft FEM beams", + "rust": "examples2d/stress_tests/soft_fem_beams2.rs", + "c": "examples2d/stress_tests/soft_fem_beams2.c", + "feature": "fem" + }, + { + "id": "fountain3", + "dimension": 3, + "group": "Collisions", + "name": "Fountain", + "rust": "examples3d/fountain3.rs", + "c": "examples3d/fountain3.c", + "feature": null + }, + { + "id": "primitives3", + "dimension": 3, + "group": "Collisions", + "name": "Primitives", + "rust": "examples3d/primitives3.rs", + "c": "examples3d/primitives3.c", + "feature": null + }, + { + "id": "keva3", + "dimension": 3, + "group": "Collisions", + "name": "Keva tower", + "rust": "examples3d/keva3.rs", + "c": "examples3d/keva3.c", + "feature": null + }, + { + "id": "newton_cradle3", + "dimension": 3, + "group": "Collisions", + "name": "Newton cradle", + "rust": "examples3d/newton_cradle3.rs", + "c": "examples3d/newton_cradle3.c", + "feature": null + }, + { + "id": "domino3", + "dimension": 3, + "group": "Collisions", + "name": "Domino", + "rust": "examples3d/domino3.rs", + "c": "examples3d/domino3.c", + "feature": null + }, + { + "id": "platform3", + "dimension": 3, + "group": "Collisions", + "name": "Platform", + "rust": "examples3d/platform3.rs", + "c": "examples3d/platform3.c", + "feature": null + }, + { + "id": "sensor3", + "dimension": 3, + "group": "Collisions", + "name": "Sensor", + "rust": "examples3d/sensor3.rs", + "c": "examples3d/sensor3.c", + "feature": null + }, + { + "id": "compound3", + "dimension": 3, + "group": "Collisions", + "name": "Compound", + "rust": "examples3d/compound3.rs", + "c": "examples3d/compound3.c", + "feature": null + }, + { + "id": "convex_decomposition3", + "dimension": 3, + "group": "Collisions", + "name": "Convex decomposition", + "rust": "examples3d/convex_decomposition3.rs", + "c": "examples3d/convex_decomposition3.c", + "feature": null + }, + { + "id": "convex_polyhedron3", + "dimension": 3, + "group": "Collisions", + "name": "Convex polyhedron", + "rust": "examples3d/convex_polyhedron3.rs", + "c": "examples3d/convex_polyhedron3.c", + "feature": null + }, + { + "id": "trimesh3", + "dimension": 3, + "group": "Collisions", + "name": "TriMesh", + "rust": "examples3d/trimesh3.rs", + "c": "examples3d/trimesh3.c", + "feature": null + }, + { + "id": "dynamic_trimesh3", + "dimension": 3, + "group": "Collisions", + "name": "Dynamic trimeshes", + "rust": "examples3d/dynamic_trimesh3.rs", + "c": "examples3d/dynamic_trimesh3.c", + "feature": null + }, + { + "id": "heightfield3", + "dimension": 3, + "group": "Collisions", + "name": "Heightfield", + "rust": "examples3d/heightfield3.rs", + "c": "examples3d/heightfield3.c", + "feature": null + }, + { + "id": "voxels3", + "dimension": 3, + "group": "Collisions", + "name": "Voxels", + "rust": "examples3d/voxels3.rs", + "c": "examples3d/voxels3.c", + "feature": null + }, + { + "id": "collision_groups3", + "dimension": 3, + "group": "Collisions", + "name": "Collision groups", + "rust": "examples3d/collision_groups3.rs", + "c": "examples3d/collision_groups3.c", + "feature": null + }, + { + "id": "one_way_platforms3", + "dimension": 3, + "group": "Collisions", + "name": "One-way platforms", + "rust": "examples3d/one_way_platforms3.rs", + "c": "examples3d/one_way_platforms3.c", + "feature": null + }, + { + "id": "locked_rotations3", + "dimension": 3, + "group": "Dynamics", + "name": "Locked rotations", + "rust": "examples3d/locked_rotations3.rs", + "c": "examples3d/locked_rotations3.c", + "feature": null + }, + { + "id": "restitution3", + "dimension": 3, + "group": "Dynamics", + "name": "Restitution", + "rust": "examples3d/restitution3.rs", + "c": "examples3d/restitution3.c", + "feature": null + }, + { + "id": "damping3", + "dimension": 3, + "group": "Dynamics", + "name": "Damping", + "rust": "examples3d/damping3.rs", + "c": "examples3d/damping3.c", + "feature": null + }, + { + "id": "gyroscopic3", + "dimension": 3, + "group": "Dynamics", + "name": "Gyroscopic", + "rust": "examples3d/gyroscopic3.rs", + "c": "examples3d/gyroscopic3.c", + "feature": null + }, + { + "id": "ccd3", + "dimension": 3, + "group": "Dynamics", + "name": "CCD", + "rust": "examples3d/ccd3.rs", + "c": "examples3d/ccd3.c", + "feature": null + }, + { + "id": "joints3_run_impulse_joints", + "dimension": 3, + "group": "Joints", + "name": "Impulse Joints", + "rust": "examples3d/joints3.rs", + "c": "examples3d/joints3_run_impulse_joints.c", + "feature": null + }, + { + "id": "joints3_run_multibody_joints", + "dimension": 3, + "group": "Joints", + "name": "Multibody Joints", + "rust": "examples3d/joints3.rs", + "c": "examples3d/joints3_run_multibody_joints.c", + "feature": null + }, + { + "id": "rope_joints3", + "dimension": 3, + "group": "Joints", + "name": "Rope Joints", + "rust": "examples3d/rope_joints3.rs", + "c": "examples3d/rope_joints3.c", + "feature": null + }, + { + "id": "spring_joints3", + "dimension": 3, + "group": "Joints", + "name": "Spring Joints", + "rust": "examples3d/spring_joints3.rs", + "c": "examples3d/spring_joints3.c", + "feature": null + }, + { + "id": "joint_motor_position3", + "dimension": 3, + "group": "Joints", + "name": "Joint Motor Position", + "rust": "examples3d/joint_motor_position3.rs", + "c": "examples3d/joint_motor_position3.c", + "feature": null + }, + { + "id": "inverse_kinematics3", + "dimension": 3, + "group": "Joints", + "name": "Inverse kinematics", + "rust": "examples3d/inverse_kinematics3.rs", + "c": "examples3d/inverse_kinematics3.c", + "feature": null + }, + { + "id": "soft_bodies3", + "dimension": 3, + "group": "Soft bodies", + "name": "Soft bodies", + "rust": "examples3d/soft_bodies3.rs", + "c": "examples3d/soft_bodies3.c", + "feature": null + }, + { + "id": "soft_cloth3", + "dimension": 3, + "group": "Soft bodies", + "name": "Cloth", + "rust": "examples3d/soft_cloth3.rs", + "c": "examples3d/soft_cloth3.c", + "feature": null + }, + { + "id": "soft_jelly3", + "dimension": 3, + "group": "Soft bodies", + "name": "Jelly", + "rust": "examples3d/soft_jelly3.rs", + "c": "examples3d/soft_jelly3.c", + "feature": null + }, + { + "id": "soft_joints3", + "dimension": 3, + "group": "Soft bodies", + "name": "Soft joints", + "rust": "examples3d/soft_joints3.rs", + "c": "examples3d/soft_joints3.c", + "feature": null + }, + { + "id": "soft_meshes3", + "dimension": 3, + "group": "Soft bodies", + "name": "Cluster meshes", + "rust": "examples3d/soft_meshes3.rs", + "c": "examples3d/soft_meshes3.c", + "feature": null + }, + { + "id": "soft_surface3", + "dimension": 3, + "group": "Soft bodies", + "name": "Deformable trimeshes", + "rust": "examples3d/soft_surface3.rs", + "c": "examples3d/soft_surface3.c", + "feature": null + }, + { + "id": "soft_pile3", + "dimension": 3, + "group": "Soft bodies", + "name": "Soft pile", + "rust": "examples3d/soft_pile3.rs", + "c": "examples3d/soft_pile3.c", + "feature": null + }, + { + "id": "soft_thin_features3", + "dimension": 3, + "group": "Soft bodies", + "name": "Thin features", + "rust": "examples3d/soft_thin_features3.rs", + "c": "examples3d/soft_thin_features3.c", + "feature": null + }, + { + "id": "soft_cloth_stress3", + "dimension": 3, + "group": "Soft bodies", + "name": "Cloth stress", + "rust": "examples3d/soft_cloth_stress3.rs", + "c": "examples3d/soft_cloth_stress3.c", + "feature": null + }, + { + "id": "soft_plasticity3", + "dimension": 3, + "group": "Soft bodies", + "name": "Plasticity", + "rust": "examples3d/soft_plasticity3.rs", + "c": "examples3d/soft_plasticity3.c", + "feature": null + }, + { + "id": "soft_tearing3", + "dimension": 3, + "group": "Soft bodies", + "name": "Tearing", + "rust": "examples3d/soft_tearing3.rs", + "c": "examples3d/soft_tearing3.c", + "feature": null + }, + { + "id": "soft_dress3", + "dimension": 3, + "group": "Soft bodies", + "name": "Dancing dress", + "rust": "examples3d/soft_dress3.rs", + "c": "examples3d/soft_dress3.c", + "feature": null + }, + { + "id": "soft_fem3", + "dimension": 3, + "group": "Soft bodies", + "name": "Soft FEM", + "rust": "examples3d/soft_fem3.rs", + "c": "examples3d/soft_fem3.c", + "feature": "fem" + }, + { + "id": "soft_trimesh3", + "dimension": 3, + "group": "Soft bodies", + "name": "Soft trimeshes", + "rust": "examples3d/soft_trimesh3.rs", + "c": "examples3d/soft_trimesh3.c", + "feature": null + }, + { + "id": "character_controller3", + "dimension": 3, + "group": "Controls", + "name": "Character controller", + "rust": "examples3d/character_controller3.rs", + "c": "examples3d/character_controller3.c", + "feature": null + }, + { + "id": "vehicle_controller3", + "dimension": 3, + "group": "Controls", + "name": "Vehicle controller", + "rust": "examples3d/vehicle_controller3.rs", + "c": "examples3d/vehicle_controller3.c", + "feature": null + }, + { + "id": "vehicle_joints3", + "dimension": 3, + "group": "Controls", + "name": "Vehicle joints", + "rust": "examples3d/vehicle_joints3.rs", + "c": "examples3d/vehicle_joints3.c", + "feature": null + }, + { + "id": "urdf3", + "dimension": 3, + "group": "Robotics", + "name": "URDF", + "rust": "examples3d/urdf3.rs", + "c": "examples3d/urdf3.c", + "feature": "robotics" + }, + { + "id": "mjcf3", + "dimension": 3, + "group": "Robotics", + "name": "MJCF", + "rust": "examples3d/mjcf3.rs", + "c": "examples3d/mjcf3.c", + "feature": "robotics" + }, + { + "id": "mujoco_menagerie3", + "dimension": 3, + "group": "Robotics", + "name": "Mujoco Menagerie", + "rust": "examples3d/mujoco_menagerie3.rs", + "c": "examples3d/mujoco_menagerie3.c", + "feature": "robotics" + }, + { + "id": "debug_angular_limits3", + "dimension": 3, + "group": "Debug", + "name": "Angular limits", + "rust": "examples3d/debug_angular_limits3.rs", + "c": "examples3d/debug_angular_limits3.c", + "feature": null + }, + { + "id": "debug_articulations3", + "dimension": 3, + "group": "Debug", + "name": "Multibody joints", + "rust": "examples3d/debug_articulations3.rs", + "c": "examples3d/debug_articulations3.c", + "feature": null + }, + { + "id": "debug_add_remove_collider3", + "dimension": 3, + "group": "Debug", + "name": "Add/rm collider", + "rust": "examples3d/debug_add_remove_collider3.rs", + "c": "examples3d/debug_add_remove_collider3.c", + "feature": null + }, + { + "id": "debug_multi_collider_body3", + "dimension": 3, + "group": "Debug", + "name": "Multi-collider body", + "rust": "examples3d/debug_multi_collider_body3.rs", + "c": "examples3d/debug_multi_collider_body3.c", + "feature": null + }, + { + "id": "debug_big_colliders3", + "dimension": 3, + "group": "Debug", + "name": "Big colliders", + "rust": "examples3d/debug_big_colliders3.rs", + "c": "examples3d/debug_big_colliders3.c", + "feature": null + }, + { + "id": "debug_boxes3", + "dimension": 3, + "group": "Debug", + "name": "Boxes", + "rust": "examples3d/debug_boxes3.rs", + "c": "examples3d/debug_boxes3.c", + "feature": null + }, + { + "id": "debug_balls3", + "dimension": 3, + "group": "Debug", + "name": "Balls", + "rust": "examples3d/debug_balls3.rs", + "c": "examples3d/debug_balls3.c", + "feature": null + }, + { + "id": "debug_disabled3", + "dimension": 3, + "group": "Debug", + "name": "Disabled", + "rust": "examples3d/debug_disabled3.rs", + "c": "examples3d/debug_disabled3.c", + "feature": null + }, + { + "id": "debug_two_cubes3", + "dimension": 3, + "group": "Debug", + "name": "Two cubes", + "rust": "examples3d/debug_two_cubes3.rs", + "c": "examples3d/debug_two_cubes3.c", + "feature": null + }, + { + "id": "debug_pop3", + "dimension": 3, + "group": "Debug", + "name": "Pop", + "rust": "examples3d/debug_pop3.rs", + "c": "examples3d/debug_pop3.c", + "feature": null + }, + { + "id": "debug_dynamic_collider_add3", + "dimension": 3, + "group": "Debug", + "name": "Dyn. collider add", + "rust": "examples3d/debug_dynamic_collider_add3.rs", + "c": "examples3d/debug_dynamic_collider_add3.c", + "feature": null + }, + { + "id": "debug_friction3", + "dimension": 3, + "group": "Debug", + "name": "Friction", + "rust": "examples3d/debug_friction3.rs", + "c": "examples3d/debug_friction3.c", + "feature": null + }, + { + "id": "debug_internal_edges3", + "dimension": 3, + "group": "Debug", + "name": "Internal edges", + "rust": "examples3d/debug_internal_edges3.rs", + "c": "examples3d/debug_internal_edges3.c", + "feature": null + }, + { + "id": "debug_self_intersect3", + "dimension": 3, + "group": "Debug", + "name": "Self intersect", + "rust": "examples3d/debug_self_intersect3.rs", + "c": "examples3d/debug_self_intersect3.c", + "feature": null + }, + { + "id": "debug_long_chain3", + "dimension": 3, + "group": "Debug", + "name": "Long chain", + "rust": "examples3d/debug_long_chain3.rs", + "c": "examples3d/debug_long_chain3.c", + "feature": null + }, + { + "id": "debug_chain_high_mass_ratio3", + "dimension": 3, + "group": "Debug", + "name": "High mass ratio: chain", + "rust": "examples3d/debug_chain_high_mass_ratio3.rs", + "c": "examples3d/debug_chain_high_mass_ratio3.c", + "feature": null + }, + { + "id": "debug_cube_high_mass_ratio3", + "dimension": 3, + "group": "Debug", + "name": "High mass ratio: cube", + "rust": "examples3d/debug_cube_high_mass_ratio3.rs", + "c": "examples3d/debug_cube_high_mass_ratio3.c", + "feature": null + }, + { + "id": "debug_triangle3", + "dimension": 3, + "group": "Debug", + "name": "Triangle", + "rust": "examples3d/debug_triangle3.rs", + "c": "examples3d/debug_triangle3.c", + "feature": null + }, + { + "id": "debug_trimesh3", + "dimension": 3, + "group": "Debug", + "name": "Trimesh", + "rust": "examples3d/debug_trimesh3.rs", + "c": "examples3d/debug_trimesh3.c", + "feature": null + }, + { + "id": "debug_thin_cube_on_mesh3", + "dimension": 3, + "group": "Debug", + "name": "Thin cube", + "rust": "examples3d/debug_thin_cube_on_mesh3.rs", + "c": "examples3d/debug_thin_cube_on_mesh3.c", + "feature": null + }, + { + "id": "debug_cylinder3", + "dimension": 3, + "group": "Debug", + "name": "Cylinder", + "rust": "examples3d/debug_cylinder3.rs", + "c": "examples3d/debug_cylinder3.c", + "feature": null + }, + { + "id": "debug_infinite_fall3", + "dimension": 3, + "group": "Debug", + "name": "Infinite fall", + "rust": "examples3d/debug_infinite_fall3.rs", + "c": "examples3d/debug_infinite_fall3.c", + "feature": null + }, + { + "id": "debug_prismatic3", + "dimension": 3, + "group": "Debug", + "name": "Prismatic", + "rust": "examples3d/debug_prismatic3.rs", + "c": "examples3d/debug_prismatic3.c", + "feature": null + }, + { + "id": "debug_rollback3", + "dimension": 3, + "group": "Debug", + "name": "Rollback", + "rust": "examples3d/debug_rollback3.rs", + "c": "examples3d/debug_rollback3.c", + "feature": null + }, + { + "id": "debug_shape_modification3", + "dimension": 3, + "group": "Debug", + "name": "Shape modification", + "rust": "examples3d/debug_shape_modification3.rs", + "c": "examples3d/debug_shape_modification3.c", + "feature": null + }, + { + "id": "debug_sleeping_kinematic3", + "dimension": 3, + "group": "Debug", + "name": "Sleeping kinematics", + "rust": "examples3d/debug_sleeping_kinematic3.rs", + "c": "examples3d/debug_sleeping_kinematic3.c", + "feature": null + }, + { + "id": "debug_deserialize3", + "dimension": 3, + "group": "Debug", + "name": "Deserialize", + "rust": "examples3d/debug_deserialize3.rs", + "c": "examples3d/debug_deserialize3.c", + "feature": null + }, + { + "id": "debug_multibody_ang_motor_pos3", + "dimension": 3, + "group": "Debug", + "name": "Multibody ang. motor pos.", + "rust": "examples3d/debug_multibody_ang_motor_pos3.rs", + "c": "examples3d/debug_multibody_ang_motor_pos3.c", + "feature": null + }, + { + "id": "stress_tests_balls3", + "dimension": 3, + "group": "Stress tests", + "name": "Balls", + "rust": "examples3d/stress_tests/balls3.rs", + "c": "examples3d/stress_tests/balls3.c", + "feature": null + }, + { + "id": "stress_tests_boxes3", + "dimension": 3, + "group": "Stress tests", + "name": "Boxes", + "rust": "examples3d/stress_tests/boxes3.rs", + "c": "examples3d/stress_tests/boxes3.c", + "feature": null + }, + { + "id": "stress_tests_capsules3", + "dimension": 3, + "group": "Stress tests", + "name": "Capsules", + "rust": "examples3d/stress_tests/capsules3.rs", + "c": "examples3d/stress_tests/capsules3.c", + "feature": null + }, + { + "id": "stress_tests_ccd3", + "dimension": 3, + "group": "Stress tests", + "name": "CCD", + "rust": "examples3d/stress_tests/ccd3.rs", + "c": "examples3d/stress_tests/ccd3.c", + "feature": null + }, + { + "id": "stress_tests_compound3", + "dimension": 3, + "group": "Stress tests", + "name": "Compound", + "rust": "examples3d/stress_tests/compound3.rs", + "c": "examples3d/stress_tests/compound3.c", + "feature": null + }, + { + "id": "stress_tests_convex_polyhedron3", + "dimension": 3, + "group": "Stress tests", + "name": "Convex polyhedron", + "rust": "examples3d/stress_tests/convex_polyhedron3.rs", + "c": "examples3d/stress_tests/convex_polyhedron3.c", + "feature": null + }, + { + "id": "stress_tests_many_kinematics3", + "dimension": 3, + "group": "Stress tests", + "name": "Many kinematics", + "rust": "examples3d/stress_tests/many_kinematics3.rs", + "c": "examples3d/stress_tests/many_kinematics3.c", + "feature": null + }, + { + "id": "stress_tests_many_static3", + "dimension": 3, + "group": "Stress tests", + "name": "Many static", + "rust": "examples3d/stress_tests/many_static3.rs", + "c": "examples3d/stress_tests/many_static3.c", + "feature": null + }, + { + "id": "stress_tests_many_sleep3", + "dimension": 3, + "group": "Stress tests", + "name": "Many sleep", + "rust": "examples3d/stress_tests/many_sleep3.rs", + "c": "examples3d/stress_tests/many_sleep3.c", + "feature": null + }, + { + "id": "stress_tests_heightfield3", + "dimension": 3, + "group": "Stress tests", + "name": "Heightfield", + "rust": "examples3d/stress_tests/heightfield3.rs", + "c": "examples3d/stress_tests/heightfield3.c", + "feature": null + }, + { + "id": "stress_tests_stacks3", + "dimension": 3, + "group": "Stress tests", + "name": "Stacks", + "rust": "examples3d/stress_tests/stacks3.rs", + "c": "examples3d/stress_tests/stacks3.c", + "feature": null + }, + { + "id": "stress_tests_pyramid3", + "dimension": 3, + "group": "Stress tests", + "name": "Pyramid", + "rust": "examples3d/stress_tests/pyramid3.rs", + "c": "examples3d/stress_tests/pyramid3.c", + "feature": null + }, + { + "id": "stress_tests_trimesh3", + "dimension": 3, + "group": "Stress tests", + "name": "Trimesh", + "rust": "examples3d/stress_tests/trimesh3.rs", + "c": "examples3d/stress_tests/trimesh3.c", + "feature": null + }, + { + "id": "stress_tests_joint_ball3", + "dimension": 3, + "group": "Stress tests", + "name": "ImpulseJoint ball", + "rust": "examples3d/stress_tests/joint_ball3.rs", + "c": "examples3d/stress_tests/joint_ball3.c", + "feature": null + }, + { + "id": "stress_tests_joint_fixed3", + "dimension": 3, + "group": "Stress tests", + "name": "ImpulseJoint fixed", + "rust": "examples3d/stress_tests/joint_fixed3.rs", + "c": "examples3d/stress_tests/joint_fixed3.c", + "feature": null + }, + { + "id": "stress_tests_joint_revolute3", + "dimension": 3, + "group": "Stress tests", + "name": "ImpulseJoint revolute", + "rust": "examples3d/stress_tests/joint_revolute3.rs", + "c": "examples3d/stress_tests/joint_revolute3.c", + "feature": null + }, + { + "id": "stress_tests_joint_prismatic3", + "dimension": 3, + "group": "Stress tests", + "name": "ImpulseJoint prismatic", + "rust": "examples3d/stress_tests/joint_prismatic3.rs", + "c": "examples3d/stress_tests/joint_prismatic3.c", + "feature": null + }, + { + "id": "stress_tests_ragdolls3", + "dimension": 3, + "group": "Stress tests", + "name": "Ragdoll piles", + "rust": "examples3d/stress_tests/ragdolls3.rs", + "c": "examples3d/stress_tests/ragdolls3.c", + "feature": null + }, + { + "id": "stress_tests_ropes3", + "dimension": 3, + "group": "Stress tests", + "name": "Ropes", + "rust": "examples3d/stress_tests/ropes3.rs", + "c": "examples3d/stress_tests/ropes3.c", + "feature": null + }, + { + "id": "stress_tests_many_pyramids3", + "dimension": 3, + "group": "Stress tests", + "name": "Many pyramids", + "rust": "examples3d/stress_tests/many_pyramids3.rs", + "c": "examples3d/stress_tests/many_pyramids3.c", + "feature": null + }, + { + "id": "stress_tests_keva3", + "dimension": 3, + "group": "Stress tests", + "name": "Keva tower", + "rust": "examples3d/stress_tests/keva3.rs", + "c": "examples3d/stress_tests/keva3.c", + "feature": null + }, + { + "id": "stress_tests_ray_cast3", + "dimension": 3, + "group": "Stress tests", + "name": "Ray cast", + "rust": "examples3d/stress_tests/ray_cast3.rs", + "c": "examples3d/stress_tests/ray_cast3.c", + "feature": null + }, + { + "id": "stress_tests_soft_blobs3", + "dimension": 3, + "group": "Stress tests", + "name": "Soft blobs", + "rust": "examples3d/stress_tests/soft_blobs3.rs", + "c": "examples3d/stress_tests/soft_blobs3.c", + "feature": null + }, + { + "id": "stress_tests_soft_jellies3", + "dimension": 3, + "group": "Stress tests", + "name": "Soft jellies", + "rust": "examples3d/stress_tests/soft_jellies3.rs", + "c": "examples3d/stress_tests/soft_jellies3.c", + "feature": null + }, + { + "id": "stress_tests_soft_ropes3", + "dimension": 3, + "group": "Stress tests", + "name": "Soft ropes", + "rust": "examples3d/stress_tests/soft_ropes3.rs", + "c": "examples3d/stress_tests/soft_ropes3.c", + "feature": null + }, + { + "id": "stress_tests_soft_cloth_drape3", + "dimension": 3, + "group": "Stress tests", + "name": "Soft cloth drape", + "rust": "examples3d/stress_tests/soft_cloth_drape3.rs", + "c": "examples3d/stress_tests/soft_cloth_drape3.c", + "feature": null + }, + { + "id": "stress_tests_soft_cloth_keva3", + "dimension": 3, + "group": "Stress tests", + "name": "Soft cloth on Keva tower", + "rust": "examples3d/stress_tests/soft_cloth_keva3.rs", + "c": "examples3d/stress_tests/soft_cloth_keva3.c", + "feature": null + }, + { + "id": "stress_tests_soft_slab3", + "dimension": 3, + "group": "Stress tests", + "name": "Soft slab shower", + "rust": "examples3d/stress_tests/soft_slab3.rs", + "c": "examples3d/stress_tests/soft_slab3.c", + "feature": null + }, + { + "id": "stress_tests_soft_fem_beams3", + "dimension": 3, + "group": "Stress tests", + "name": "Soft FEM beams", + "rust": "examples3d/stress_tests/soft_fem_beams3.rs", + "c": "examples3d/stress_tests/soft_fem_beams3.c", + "feature": "fem" + }, + { + "id": "b3d_large_pyramid", + "dimension": 3, + "group": "Third-party benchmarks", + "name": "Large pyramid", + "rust": "examples3d/b3d_large_pyramid.rs", + "c": "examples3d/b3d_large_pyramid.c", + "feature": null + }, + { + "id": "b3d_many_pyramids", + "dimension": 3, + "group": "Third-party benchmarks", + "name": "Many pyramids", + "rust": "examples3d/b3d_many_pyramids.rs", + "c": "examples3d/b3d_many_pyramids.c", + "feature": null + }, + { + "id": "b3d_joint_grid", + "dimension": 3, + "group": "Third-party benchmarks", + "name": "Joint grid", + "rust": "examples3d/b3d_joint_grid.rs", + "c": "examples3d/b3d_joint_grid.c", + "feature": null + }, + { + "id": "b3d_junkyard", + "dimension": 3, + "group": "Third-party benchmarks", + "name": "Junkyard", + "rust": "examples3d/b3d_junkyard.rs", + "c": "examples3d/b3d_junkyard.c", + "feature": null + }, + { + "id": "b3d_washer", + "dimension": 3, + "group": "Third-party benchmarks", + "name": "Washer", + "rust": "examples3d/b3d_washer.rs", + "c": "examples3d/b3d_washer.c", + "feature": null + }, + { + "id": "b3d_trees_run100", + "dimension": 3, + "group": "Third-party benchmarks", + "name": "Trees 100", + "rust": "examples3d/b3d_trees.rs", + "c": "examples3d/b3d_trees_run100.c", + "feature": null + }, + { + "id": "b3d_trees_run50", + "dimension": 3, + "group": "Third-party benchmarks", + "name": "Trees 50", + "rust": "examples3d/b3d_trees.rs", + "c": "examples3d/b3d_trees_run50.c", + "feature": null + }, + { + "id": "b3d_trees_run25", + "dimension": 3, + "group": "Third-party benchmarks", + "name": "Trees 25", + "rust": "examples3d/b3d_trees.rs", + "c": "examples3d/b3d_trees_run25.c", + "feature": null + }, + { + "id": "b3d_rain", + "dimension": 3, + "group": "Third-party benchmarks", + "name": "Rain", + "rust": "examples3d/b3d_rain.rs", + "c": "examples3d/b3d_rain.c", + "feature": null + }, + { + "id": "b3d_large_world", + "dimension": 3, + "group": "Third-party benchmarks", + "name": "Large world", + "rust": "examples3d/b3d_large_world.rs", + "c": "examples3d/b3d_large_world.c", + "feature": null + } +] diff --git a/c/testbed/dependencies.md b/c/testbed/dependencies.md new file mode 100644 index 000000000..b87e1fa2d --- /dev/null +++ b/c/testbed/dependencies.md @@ -0,0 +1,73 @@ +# Testbed dependencies + +The graphical viewer uses CMake `FetchContent` to download these exact revisions +at **configure time**, then compiles them with the testbed. Archive/file SHA-256 +hashes are pinned in [cmake/Dependencies.cmake](cmake/Dependencies.cmake). +Sources are stored under the build directory's `_deps/`; nothing is downloaded +into the repository. No Git checkout, package installation, or binding generator +is needed for these dependencies. + +| Dependency | Revision | License | +| --- | --- | --- | +| [raylib 6.0](https://github.com/raysan5/raylib) | `dbc56a87da87d973a9c5baa4e7438a9d20121d28` | zlib | +| [Dear ImGui 1.92.7](https://github.com/ocornut/imgui) | `dac07199cfd761113d966eb8ad739254e10df2fe` | MIT | +| [cimgui 1.92.7](https://github.com/cimgui/cimgui) | `d3f0c2f4a7d4d116ef908295b971a36bdfdafe27` | MIT | +| [rlImGui Raylib_6_0](https://github.com/raylib-extras/rlImGui) | `3bc5731c4216bb8caa67fbea24aa85ce80d57ccb` | zlib | +| [Fira Sans Regular](https://github.com/mozilla/Fira) | `fd8c8c0a3d353cd99e8ca1662942d165e6961407` | SIL OFL 1.1 | + +Dear ImGui matches cimgui's submodule revision. Only the regular Fira font is +downloaded, then embedded in the executable. There is no runtime network or font +installation requirement. The UI remains C11; ImGui and its wrappers use C++17. + +Raylib uses desktop GLFW and OpenGL 3.3 (macOS, Windows, Linux/X11). Audio, model +importers, mesh generators, raylib font-file loaders, compression helpers, and +gesture detection are disabled. Full upstream archives are downloaded, but their +examples, generators, and unused backends are not built. Texture loading, +screenshot export, mesh instancing, and the default font/white texture remain. + +## Offline builds + +An existing populated build directory can be reconfigured with +`-DFETCHCONTENT_FULLY_DISCONNECTED=ON`. This requires the sources to have already +been downloaded; it does not bootstrap an empty build directory. + +For a new offline build, provide extracted sources at the pinned revisions: + +```sh +cmake -S c -B build/c3 -DRAPIER_BUILD_TESTBED=ON \ + -DFETCHCONTENT_SOURCE_DIR_RAYLIB=/path/to/raylib \ + -DFETCHCONTENT_SOURCE_DIR_CIMGUI=/path/to/cimgui \ + -DFETCHCONTENT_SOURCE_DIR_IMGUI=/path/to/imgui \ + -DFETCHCONTENT_SOURCE_DIR_RLIMGUI=/path/to/rlImGui \ + -DFETCHCONTENT_SOURCE_DIR_FIRA_FONT=/path/to/font-directory +``` + +The font directory must contain `FiraSans-Regular.ttf`. Source overrides are +used as-is; archive hashes are checked only for downloaded dependencies. CMake +does not modify the supplied source trees. Cargo dependencies must also be +available separately for an offline physics build. + +`RAPIER_BUILD_TESTBED=OFF` or `RAPIER_TESTBED_GRAPHICS=OFF` skips all five downloads. +See the [FetchContent documentation](https://cmake.org/cmake/help/latest/module/FetchContent.html) +for its cache and source override options. + +## Redistribution + +Ship the license files, `THIRD_PARTY_NOTICES.txt`, and `licenses/` directory copied +beside the viewer. Downloading instead of vendoring does not change the licenses. +The collected notices cover the configured renderer; if redistributing complete +upstream archives, preserve their additional per-file notices too. + +Raylib includes separately licensed components: GLFW/rprand/rltexgpu use zlib-style +terms; stb uses MIT/public-domain alternatives; GLAD includes Khronos notices; +QOI and ImGui's embedded Proggy fonts use MIT terms; dirent has its own permissive +notice. The supplied notices and supplemental texts retain these terms. + +GLFW's Windows fallback `dinput.h` and `xinput.h` headers are LGPL-2.1-or-later. +They contain declarations and small macros rather than a linked LGPL library; +[LGPL 2.1 section 5](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) +addresses this use. Their complete sources and LGPL license are copied beside the +viewer. The testbed and bindings retain their own license. + +When updating dependency pins or enabling additional modules, update the hashes, +collected notices, and supplemental licenses together. diff --git a/c/testbed/example_math.h b/c/testbed/example_math.h new file mode 100644 index 000000000..48fbe243f --- /dev/null +++ b/c/testbed/example_math.h @@ -0,0 +1,15 @@ +#ifndef RAPIER_EXAMPLE_MATH_H +#define RAPIER_EXAMPLE_MATH_H +#include "rapier_math.h" +#include + +/* A fixed PCG stream makes the C examples reproducible across libc versions. */ +static inline RAPIER_TYPE(Real) exampleRandom(uint64_t *state) { + uint64_t old = *state; + *state = old * UINT64_C(6364136223846793005) + UINT64_C(1442695040888963407); + uint32_t value = (uint32_t)(((old >> 18) ^ old) >> 27); + uint32_t rotation = (uint32_t)(old >> 59); + return (RAPIER_TYPE(Real))(((value >> rotation) | (value << ((-rotation) & 31))) >> 8) / + (RAPIER_TYPE(Real))16777216; +} +#endif diff --git a/c/testbed/examples2d/add_remove2.c b/c/testbed/examples2d/add_remove2.c new file mode 100644 index 000000000..5c787d23e --- /dev/null +++ b/c/testbed/examples2d/add_remove2.c @@ -0,0 +1,73 @@ +/* Port of examples2d/add_remove2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "example_math.h" + +void tbAddRemove2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + const R2Real rad = 0.5; + const R2Vector positions[] = {r2Vector(5.0, -1.0), r2Vector(-5.0, -1.0)}; + + R2RigidBodyHandle platformHandles[2] = {0}; + + for (size_t i = 0; i < TB_COUNT(positions); i++) { + R2RigidBodyDesc rigidBody = r2KinematicPositionBasedRigidBodyDesc(); + rigidBody.position.translation = positions[i]; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(rad * 10.0, rad)); + platformHandles[i] = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(platformHandles[i], &collider); + } + + /* Set up the viewer. */ + tbCamera2(testbed, 0.0, 0.0, 20.0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + + const uint64_t stepId = testbed->step + 1; + R2Real dt = r2TimeStep(world); + + const R2Real rot = -(R2Real)stepId * dt; + for (size_t i = 0; i < TB_COUNT(platformHandles); i++) { + r2RigidBody_SetNextKinematicRotation(platformHandles[i], r2Rotation(rot)); + } + + if (stepId % 10 == 0) { + const R2Real rad = 0.5; + const R2Real x = exampleRandom(&testbed->randomState) * 10.0 - 5.0; + const R2Real y = exampleRandom(&testbed->randomState) * 10.0 + 10.0; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, y); + rigidBody.canSleep = !testbed->noSleep; + + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(rad, rad)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + + /* Copy the handles before removing bodies from their set. */ + size_t numBodies = r2RigidBodyHandles(world, NULL, 0); + R2RigidBodyHandle *handles = malloc(numBodies * sizeof(*handles)); + if (!handles && numBodies != 0) { + abort(); + } + numBodies = r2RigidBodyHandles(world, handles, numBodies); + + for (size_t i = 0; i < numBodies; i++) { + R2Vector position = r2RigidBody_Translation(handles[i]); + if (position.y < -10.0) { + r2RemoveRigidBody(handles[i], 1); + } + } + free(handles); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/b2d_compounds.c b/c/testbed/examples2d/b2d_compounds.c new file mode 100644 index 000000000..ace65262f --- /dev/null +++ b/c/testbed/examples2d/b2d_compounds.c @@ -0,0 +1,68 @@ +/* Port of examples2d/b2d_compounds.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB2dCompounds(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, -10)); + R2RigidBodyHandle ground; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + rigidBody.canSleep = !testbed->noSleep; + ground = r2InsertRigidBody(world, &rigidBody); + + for (int i = 0; i <= 80; i++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.55, 0.5)); + collider.position.translation = r2Vector(-40 + i, 0); + r2InsertCollider(ground, &collider); + } + for (int side = -1; side <= 1; side += 2) { + for (int i = 0; i < 100; i++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.55)); + collider.position.translation = r2Vector(side * 40, i + 1); + r2InsertCollider(ground, &collider); + } + } + R2SharedShape *segment = r2SegmentSharedShape(r2Vector(-800, -80), r2Vector(800, -80)); + R2ColliderDesc shapeCollider = r2DefaultColliderDesc(); + shapeCollider.shape.kind = R2_SHAPE_DESC_SHARED; + shapeCollider.shape.sharedShape = segment; + r2InsertCollider(ground, &shapeCollider); + + const R2Vector left[] = {r2Vector(-1, 0), r2Vector(0.5, 1), r2Vector(0, 2)}; + const R2Vector right[] = {r2Vector(1, 0), r2Vector(-0.5, 1), r2Vector(0, 2)}; + R2Real side = 0.25; + for (int i = 0; i < 20; i++) { + for (int j = 0; j < 150; j++) { + R2RigidBodyHandle handle; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 2 - 19 + side, j * 2.25 + 5.575); + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + + side = -side; + for (int p = 0; p < 2; p++) { + R2ColliderDesc collider = r2DefaultColliderDesc(); + r2ShapeDesc_SetConvexHull(&collider.shape, (R2VectorView){p ? right : left, 3}); + collider.friction = 0.5; + r2InsertCollider(handle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 120, 2); + r2FreeSharedShape(segment); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/b2d_joint_grid.c b/c/testbed/examples2d/b2d_joint_grid.c new file mode 100644 index 000000000..e4303f4a0 --- /dev/null +++ b/c/testbed/examples2d/b2d_joint_grid.c @@ -0,0 +1,60 @@ +/* Port of examples2d/b2d_joint_grid.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB2dJointGrid(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, -10)); + R2RigidBodyHandle *handles = calloc(1, 10000 * sizeof(*handles)); + if (!handles) { + abort(); + } + for (int k = 0; k < 100; k++) { + for (int i = 0; i < 100; i++) { + int fixed = k >= 47 && k <= 53 && i == 0; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.bodyType = fixed ? R2_FIXED : R2_DYNAMIC; + rigidBody.position.translation = r2Vector(k, -i); + if (!fixed) { + rigidBody.canSleep = 0; + } + R2ColliderDesc collider = r2BallColliderDesc(0.4); + collider.collisionGroups = (R2InteractionGroups){2, UINT32_MAX ^ 2, R2_GROUPS_AND}; + R2RigidBodyHandle handle; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + for (int dir = 0; dir < 2; dir++) { + if (dir ? k > 0 : i > 0) { + R2JointDesc joint = r2DefaultJointDesc(); + joint.lockedAxes = 3; + joint.localFrame1.translation = dir ? r2Vector(0.5, 0) : r2Vector(0, -0.5); + joint.localFrame2.translation = dir ? r2Vector(-0.5, 0) : r2Vector(0, 0.5); + joint.contactsEnabled = 0; + r2InsertImpulseJoint(handles[k * 100 + i - (dir ? 100 : 1)], handle, + &joint); + } + } + handles[k * 100 + i] = handle; + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 50, -50, 4); + free(handles); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/b2d_junkyard.c b/c/testbed/examples2d/b2d_junkyard.c new file mode 100644 index 000000000..510592699 --- /dev/null +++ b/c/testbed/examples2d/b2d_junkyard.c @@ -0,0 +1,73 @@ +/* Port of examples2d/b2d_junkyard.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB2dJunkyard(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, -10)); + R2RigidBodyHandle ground; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + rigidBody.canSleep = !testbed->noSleep; + ground = r2InsertRigidBody(world, &rigidBody); + + for (int i = 0; i <= 160; i++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.55, 0.5)); + collider.position.translation = r2Vector(-80 + i, 0); + r2InsertCollider(ground, &collider); + } + for (int side = -1; side <= 1; side += 2) { + for (int i = 0; i < 50; i++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.55)); + collider.position.translation = r2Vector(side * 80, i + 1); + r2InsertCollider(ground, &collider); + } + } + R2Vector points[5]; + R2Real phi = R2_PI * ((R2Real)sqrt(5) - 1); + for (int i = 0; i < 5; i++) { + points[i] = r2Vector(0.25 * cos(phi * i), 0.25 * sin(phi * i)); + } + R2Real side = -0.1; + for (int i = 0; i < 200; i++) { + for (int j = 0; j < 40; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(1.5 * (2 * i - 200) * 0.25 + side, j + 15); + R2ColliderDesc collider = r2DefaultColliderDesc(); + r2ShapeDesc_SetConvexHull(&collider.shape, (R2VectorView){points, 5}); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + side = -side; + } + } + R2RigidBodyHandle pusher = {0}; + R2RigidBodyDesc platformBody = r2KinematicPositionBasedRigidBodyDesc(); + platformBody.position.translation = r2Vector(0, 0); + platformBody.canSleep = !testbed->noSleep; + pusher = r2InsertRigidBody(world, &platformBody); + + R2ColliderDesc boxCollider = r2CuboidColliderDesc(r2Vector(2, 4)); + boxCollider.position.translation = r2Vector(0, 4); + r2InsertCollider(pusher, &boxCollider); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 20, 4); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2RigidBody_SetNextKinematicTranslation( + pusher, r2Vector(60 * sin(0.2 * testbed->step / 60.0), 0)); + + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/b2d_large_pyramid.c b/c/testbed/examples2d/b2d_large_pyramid.c new file mode 100644 index 000000000..e3617e7c4 --- /dev/null +++ b/c/testbed/examples2d/b2d_large_pyramid.c @@ -0,0 +1,45 @@ +/* Port of examples2d/b2d_large_pyramid.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB2dLargePyramid(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, -10)); + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, -1); + R2ColliderDesc boxCollider = r2CuboidColliderDesc(r2Vector(120, 1)); + groundBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle groundBodyHandle = r2InsertRigidBody(world, &groundBody); + r2InsertCollider(groundBodyHandle, &boxCollider); + for (int i = 0; i < 200; i++) { + for (int j = i; j < 200; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = + r2Vector((i + 1) * 0.5 + j - i - 100, (2 * i + 1) * 0.5); + rigidBody.canSleep = 0; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + collider.density = 1; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 50, 2.5); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/b2d_many_pyramids.c b/c/testbed/examples2d/b2d_many_pyramids.c new file mode 100644 index 000000000..114a84a63 --- /dev/null +++ b/c/testbed/examples2d/b2d_many_pyramids.c @@ -0,0 +1,58 @@ +/* Port of examples2d/b2d_many_pyramids.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB2dManyPyramids(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, -10)); + R2RigidBodyHandle floor; + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + groundBody.canSleep = !testbed->noSleep; + floor = r2InsertRigidBody(world, &groundBody); + + for (int i = 0; i < 20; i++) { + R2SharedShape *shape = r2SegmentSharedShape(r2Vector(-110, i * 11), r2Vector(110, i * 11)); + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + r2InsertCollider(floor, &collider); + + r2FreeSharedShape(shape); + } + for (int row = 0; row < 20; row++) { + for (int col = 0; col < 20; col++) { + for (int i = 0; i < 10; i++) { + for (int j = i; j < 10; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = + r2Vector((i + 1) * 0.5 + (j - i) - 110 + col * 11 + 1 - 0.5, + (2 * i + 1) * 0.5 + row * 11); + rigidBody.canSleep = 0; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 60, 2); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/b2d_rain.c b/c/testbed/examples2d/b2d_rain.c new file mode 100644 index 000000000..b2363a5ec --- /dev/null +++ b/c/testbed/examples2d/b2d_rain.c @@ -0,0 +1,292 @@ +/* Port of examples2d/b2d_rain.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +enum { + ROW_COUNT = 5, + COLUMN_COUNT = 40, + GROUP_SIZE = 5, + GRID_COUNT = 500, + BONE_COUNT = 11, + HIP = 0, + TORSO = 1, + HEAD = 2, + UPPER_LEFT_LEG = 3, + LOWER_LEFT_LEG = 4, + UPPER_RIGHT_LEG = 5, + LOWER_RIGHT_LEG = 6, + UPPER_LEFT_ARM = 7, + LOWER_LEFT_ARM = 8, + UPPER_RIGHT_ARM = 9, + LOWER_RIGHT_ARM = 10 +}; + +const R2Real GRID_SIZE = .5; + +typedef struct BoneDef { + int parent; + R2Real posY; + R2Vector capA, capB; + R2Real capR; + int hasFoot; + R2Real pivotY, limits[2], frameAAngle; +} BoneDef; + +static const BoneDef boneDefs[BONE_COUNT] = { + // hip (root, no joint) + {.parent = -1, + .posY = 0.95, + .capA = {0.0, -0.02}, + .capB = {0.0, 0.02}, + .capR = 0.095, + .hasFoot = 0, + .pivotY = 0.0, + .limits = {0.0, 0.0}, + .frameAAngle = 0.0}, + // torso + {.parent = HIP, + .posY = 1.2, + .capA = {0.0, -0.135}, + .capB = {0.0, 0.135}, + .capR = 0.09, + .hasFoot = 0, + .pivotY = 1.0, + .limits = {-0.25 * R2_PI, 0.0}, + .frameAAngle = 0.0}, + // head + {.parent = TORSO, + .posY = 1.475, + .capA = {0.0, -0.038}, + .capB = {0.0, 0.039}, + .capR = 0.075, + .hasFoot = 0, + .pivotY = 1.4, + .limits = {-0.3 * R2_PI, 0.1 * R2_PI}, + .frameAAngle = 0.0}, + // upper left leg + {.parent = HIP, + .posY = 0.775, + .capA = {0.0, -0.125}, + .capB = {0.0, 0.125}, + .capR = 0.06, + .hasFoot = 0, + .pivotY = 0.9, + .limits = {-0.05 * R2_PI, 0.4 * R2_PI}, + .frameAAngle = 0.0}, + // lower left leg + {.parent = UPPER_LEFT_LEG, + .posY = 0.475, + .capA = {0.0, -0.155}, + .capB = {0.0, 0.125}, + .capR = 0.045, + .hasFoot = 1, + .pivotY = 0.625, + .limits = {-0.5 * R2_PI, -0.02 * R2_PI}, + .frameAAngle = 0.0}, + // upper right leg + {.parent = HIP, + .posY = 0.775, + .capA = {0.0, -0.125}, + .capB = {0.0, 0.125}, + .capR = 0.06, + .hasFoot = 0, + .pivotY = 0.9, + .limits = {-0.05 * R2_PI, 0.4 * R2_PI}, + .frameAAngle = 0.0}, + // lower right leg + {.parent = UPPER_RIGHT_LEG, + .posY = 0.475, + .capA = {0.0, -0.155}, + .capB = {0.0, 0.125}, + .capR = 0.045, + .hasFoot = 1, + .pivotY = 0.625, + .limits = {-0.5 * R2_PI, -0.02 * R2_PI}, + .frameAAngle = 0.0}, + // upper left arm + {.parent = TORSO, + .posY = 1.225, + .capA = {0.0, -0.125}, + .capB = {0.0, 0.125}, + .capR = 0.035, + .hasFoot = 0, + .pivotY = 1.35, + .limits = {-0.1 * R2_PI, 0.8 * R2_PI}, + .frameAAngle = 0.0}, + // lower left arm + {.parent = UPPER_LEFT_ARM, + .posY = 0.975, + .capA = {0.0, -0.125}, + .capB = {0.0, 0.125}, + .capR = 0.03, + .hasFoot = 0, + .pivotY = 1.1, + .limits = {-0.2 * R2_PI, 0.3 * R2_PI}, + .frameAAngle = 0.25 * R2_PI}, + // upper right arm + {.parent = TORSO, + .posY = 1.225, + .capA = {0.0, -0.125}, + .capB = {0.0, 0.125}, + .capR = 0.035, + .hasFoot = 0, + .pivotY = 1.35, + .limits = {-0.1 * R2_PI, 0.8 * R2_PI}, + .frameAAngle = 0.0}, + // lower right arm + {.parent = UPPER_RIGHT_ARM, + .posY = 0.975, + .capA = {0.0, -0.125}, + .capB = {0.0, 0.125}, + .capR = 0.03, + .hasFoot = 0, + .pivotY = 1.1, + .limits = {-0.2 * R2_PI, 0.3 * R2_PI}, + .frameAAngle = 0.25 * R2_PI}, +}; + +typedef struct HumanHandles { + R2RigidBodyHandle bones[BONE_COUNT]; +} HumanHandles; + +static HumanHandles createHuman(Testbed *testbed, R2World *world, R2Vector position, R2Real scale, + R2Real hertz, R2Real damping, uint32_t groupBit) { + HumanHandles human; + const uint32_t bit = UINT32_C(1) << (groupBit % 24); + const R2InteractionGroups groups = {bit, ~bit, 0}; + const R2Vector footPoints[] = {{-.03 * scale, -.185 * scale}, + {.11 * scale, -.185 * scale}, + {.11 * scale, -.16 * scale}, + {-.03 * scale, -.14 * scale}}; + for (size_t i = 0; i < BONE_COUNT; ++i) { + const BoneDef *def = &boneDefs[i]; + R2RigidBodyDesc body = r2DynamicRigidBodyDesc(); + body.canSleep = !testbed->noSleep; + body.position.translation = r2VectorAdd(position, r2Vector(0, def->posY * scale)); + human.bones[i] = r2InsertRigidBody(world, &body); + + R2ColliderDesc collider = r2CapsuleColliderDesc( + r2VectorScale(def->capA, scale), r2VectorScale(def->capB, scale), def->capR * scale); + collider.friction = .2; + collider.collisionGroups = groups; + r2InsertCollider(human.bones[i], &collider); + + if (def->hasFoot) { + R2SharedShape *shape = + r2RoundConvexHullSharedShape((R2VectorView){footPoints, 4}, .015 * scale); + collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + + collider.friction = .05; + collider.collisionGroups = groups; + r2InsertCollider(human.bones[i], &collider); + r2FreeSharedShape(shape); + } + } + const R2Real omega = 2 * R2_PI * hertz, stiffness = omega * omega, + motorDamping = 2 * damping * omega; + for (size_t i = 0; i < BONE_COUNT; ++i) { + const BoneDef *def = &boneDefs[i]; + if (def->parent < 0) { + continue; + } + R2JointDesc joint = r2DefaultJointDesc(); + joint.lockedAxes = 3; + const R2Vector anchorA = r2Vector(0, (def->pivotY - boneDefs[def->parent].posY) * scale); + const R2Vector anchorB = r2Vector(0, (def->pivotY - def->posY) * scale); + joint.localFrame1 = r2Pose(anchorA, r2Rotation(def->frameAAngle)); + joint.localFrame2 = r2TranslationPose(anchorB); + r2JointDesc_SetLimits(&joint, R2_AXIS_ANG_X, def->limits[0], def->limits[1]); + joint.contactsEnabled = 0; + r2JointDesc_SetMotorModel(&joint, R2_AXIS_ANG_X, 0); + r2JointDesc_SetMotorPosition(&joint, R2_AXIS_ANG_X, 0, stiffness, motorDamping); + r2InsertImpulseJoint(human.bones[def->parent], human.bones[i], &joint); + } + return human; +} + +typedef struct RainState { + HumanHandles groups[ROW_COUNT * COLUMN_COUNT][GROUP_SIZE]; + size_t columnCount, columnIndex; +} RainState; + +static void createGroup(Testbed *testbed, R2World *world, RainState *state, size_t row, + size_t col) { + const size_t groupIndex = row * COLUMN_COUNT + col; + const R2Real span = GRID_COUNT * GRID_SIZE, groupDistance = span / COLUMN_COUNT; + R2Real x = -.5 * span + groupDistance * (col + .5); + const R2Real y = 40 + 45 * row; + for (size_t i = 0; i < GROUP_SIZE; ++i) { + state->groups[groupIndex][i] = + createHuman(testbed, world, r2Vector(x, y), 1, 5, .5, (uint32_t)(i + 1)); + x += .5; + } +} + +static void destroyGroup(R2World *world, RainState *state, size_t row, size_t col) { + for (size_t i = 0; i < GROUP_SIZE; ++i) { + for (size_t j = 0; j < BONE_COUNT; ++j) { + r2RemoveRigidBody(state->groups[row * COLUMN_COUNT + col][i].bones[j], 1); + } + } +} + +static void stepRain(Testbed *testbed, R2World *world, RainState *state, size_t stepCount) { + if (stepCount & 0x7) { + return; + } + if (state->columnCount < COLUMN_COUNT) { + const size_t col = state->columnCount; + for (size_t row = 0; row < ROW_COUNT; ++row) { + createGroup(testbed, world, state, row, col); + } + ++state->columnCount; + } else { + const size_t col = state->columnIndex; + for (size_t row = 0; row < ROW_COUNT; ++row) { + destroyGroup(world, state, row, col); + createGroup(testbed, world, state, row, col); + } + state->columnIndex = (state->columnIndex + 1) % COLUMN_COUNT; + } +} + +void tbB2dRain(Testbed *testbed) { + R2World *world = r2NewWorld(); + r2SetGravity(world, r2Vector(0, -10)); + R2RigidBodyDesc builder = r2FixedRigidBodyDesc(); + R2RigidBodyHandle ground = r2InsertRigidBody(world, &builder); + + R2Real y = 0; + for (size_t row = 0; row < ROW_COUNT; ++row) { + R2Real x = -.5 * GRID_COUNT * GRID_SIZE; + for (size_t i = 0; i <= GRID_COUNT; ++i) { + R2ColliderDesc collider = + r2CuboidColliderDesc(r2Vector(.5 * GRID_SIZE, .5 * GRID_SIZE)); + collider.position.translation = r2Vector(x, y); + r2InsertCollider(ground, &collider); + + x += GRID_SIZE; + } + y += 45; + } + RainState *state = calloc(1, sizeof(*state)); + if (!state) { + abort(); + } + tbCamera2(testbed, 0, 110, 2); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + size_t stepCount = 0; + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + stepRain(testbed, world, state, stepCount); + ++stepCount; + r2Step(world, NULL, NULL); + } + } + free(state); + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/b2d_smash.c b/c/testbed/examples2d/b2d_smash.c new file mode 100644 index 000000000..ac146693c --- /dev/null +++ b/c/testbed/examples2d/b2d_smash.c @@ -0,0 +1,45 @@ +/* Port of examples2d/b2d_smash.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB2dSmash(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, 0)); + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-20, 0); + rigidBody.linvel = r2Vector(40, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(4, 4)); + collider.density = 8; + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + for (int i = 0; i < 120; i++) { + for (int j = 0; j < 80; j++) { + rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 0.4 + 30, (j - 40) * 0.4); + rigidBody.sleeping = 1; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.2, 0.2)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 20, 0, 8); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/b2d_spinner.c b/c/testbed/examples2d/b2d_spinner.c new file mode 100644 index 000000000..6ce27f936 --- /dev/null +++ b/c/testbed/examples2d/b2d_spinner.c @@ -0,0 +1,94 @@ +/* Port of examples2d/b2d_spinner.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB2dSpinner(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, -10)); + R2RigidBodyHandle ground; + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + groundBody.canSleep = !testbed->noSleep; + ground = r2InsertRigidBody(world, &groundBody); + + R2Vector points[360]; + R2Vector position = r2Vector(40, 0); + uint32_t indices[720]; + R2Real angle = -2 * R2_PI / 360; + for (uint32_t i = 0; i < 360; i++) { + points[i] = r2VectorAdd(position, r2Vector(0, 32)); + position = r2Vector(cos(angle) * position.x - sin(angle) * position.y, + sin(angle) * position.x + cos(angle) * position.y); + indices[2 * i] = i; + indices[2 * i + 1] = (i + 1) % 360; + } + R2SharedShape *shape = r2OrientedPolylineSharedShape( + (R2VectorView){points, 360}, (R2EdgeView){(const R2Edge *)indices, 360}); + R2ColliderDesc wall = r2DefaultColliderDesc(); + wall.shape.kind = R2_SHAPE_DESC_SHARED; + wall.shape.sharedShape = shape; + wall.friction = 0.1; + r2InsertCollider(ground, &wall); + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 12); + rigidBody.canSleep = 0; + R2ColliderDesc collider = r2RoundCuboidColliderDesc(r2Vector(0.4, 20), 0.2); + collider.friction = 0; + R2RigidBodyHandle spinner; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + spinner = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(spinner, &collider); + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2Vector(0, 12); + joint.localFrame2.translation = r2Vector(0, 0); + r2JointDesc_SetMotor(&joint, R2_AXIS_ANG_X, 0, 5, 0, 1.0e5); + r2JointDesc_SetMotorMaxForce(&joint, R2_AXIS_ANG_X, 1.0e9); + R2ImpulseJointHandle jointHandle = r2InsertImpulseJoint(ground, spinner, &joint); + R2Real x = -23; + R2Real y = 2; + for (int i = 0; i < 6076; i++) { + R2ColliderDesc particleCollider; + if (i % 3 == 0) { + particleCollider = + r2CapsuleColliderDesc(r2Vector(-0.25, 0.0), r2Vector(0.25, 0.0), 0.25); + } else if (i % 3 == 1) { + particleCollider = r2BallColliderDesc(0.35); + } else { + particleCollider = r2CuboidColliderDesc(r2Vector(0.35, 0.35)); + } + particleCollider.density = 0.25; + particleCollider.friction = 0.1; + particleCollider.restitution = 0.1; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, y); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &particleCollider); + + x += 0.5; + if (x >= 23) { + x = -23; + y += 0.5; + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 32, 6); + + r2FreeSharedShape(shape); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/b2d_tumbler.c b/c/testbed/examples2d/b2d_tumbler.c new file mode 100644 index 000000000..e165c4412 --- /dev/null +++ b/c/testbed/examples2d/b2d_tumbler.c @@ -0,0 +1,62 @@ +/* Port of examples2d/b2d_tumbler.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB2dTumbler(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, -10)); + R2RigidBodyHandle ground; + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + groundBody.canSleep = !testbed->noSleep; + ground = r2InsertRigidBody(world, &groundBody); + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 10); + rigidBody.canSleep = 0; + R2RigidBodyHandle drum; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + drum = r2InsertRigidBody(world, &rigidBody); + const R2Vector half[] = {r2Vector(0.5, 10), r2Vector(0.5, 10), r2Vector(10, 0.5), + r2Vector(10, 0.5)}; + const R2Vector off[] = {r2Vector(10, 0), r2Vector(-10, 0), r2Vector(0, 10), r2Vector(0, -10)}; + for (int i = 0; i < 4; i++) { + R2ColliderDesc collider = r2CuboidColliderDesc(half[i]); + collider.position.translation = off[i]; + collider.density = 50; + r2InsertCollider(drum, &collider); + } + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2Vector(0, 10); + joint.localFrame2.translation = r2Vector(0, 0); + r2JointDesc_SetMotor(&joint, R2_AXIS_ANG_X, 0, R2_PI / 180 * 25, 0, 1.0e5); + r2JointDesc_SetMotorMaxForce(&joint, R2_AXIS_ANG_X, 1.0e8); + r2InsertImpulseJoint(ground, drum, &joint); + for (int i = 0; i < 45; i++) { + for (int j = 0; j < 45; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-9 + j * 0.4, 1 + i * 0.4); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.125, 0.125)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 10, 12); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/b2d_washer.c b/c/testbed/examples2d/b2d_washer.c new file mode 100644 index 000000000..5297ad456 --- /dev/null +++ b/c/testbed/examples2d/b2d_washer.c @@ -0,0 +1,71 @@ +/* Port of examples2d/b2d_washer.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB2dWasher(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, -10)); + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + groundBody.canSleep = !testbed->noSleep; + r2InsertRigidBody(world, &groundBody); + + R2RigidBodyDesc rigidBody = r2KinematicVelocityBasedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 10); + rigidBody.angvel = R2_PI / 180 * 25; + rigidBody.linvel = r2Vector(0.001, -0.002); + R2RigidBodyHandle washer; + rigidBody.canSleep = !testbed->noSleep; + washer = r2InsertRigidBody(world, &rigidBody); + R2Real angle = R2_PI / 18; + R2Vector u1 = r2Vector(1, 0); + for (int i = 0; i < 36; i++) { + R2Vector u2 = i == 35 ? r2Vector(1, 0) + : r2Vector(cos(angle) * u1.x - sin(angle) * u1.y, + sin(angle) * u1.x + cos(angle) * u1.y); + R2Vector a1 = r2Vector(cos(angle * 0.1) * u1.x + sin(angle * 0.1) * u1.y, + -sin(angle * 0.1) * u1.x + cos(angle * 0.1) * u1.y); + R2Vector a2 = r2Vector(cos(angle * 0.1) * u2.x - sin(angle * 0.1) * u2.y, + sin(angle * 0.1) * u2.x + cos(angle * 0.1) * u2.y); + for (int part = 0; part < (i % 9 == 0 ? 2 : 1); part++) { + R2Vector vertices[4]; + R2Vector left = part ? u1 : a1; + R2Vector right = part ? u2 : a2; + R2Real rmin = part ? 14 : 16; + R2Real rmax = part ? 16 : 18; + vertices[0] = r2VectorScale(left, rmin); + vertices[1] = r2VectorScale(left, rmax); + vertices[2] = r2VectorScale(right, rmin); + vertices[3] = r2VectorScale(right, rmax); + R2ColliderDesc collider = r2DefaultColliderDesc(); + r2ShapeDesc_SetConvexHull(&collider.shape, (R2VectorView){vertices, TB_COUNT(vertices)}); + r2InsertCollider(washer, &collider); + } + u1 = u2; + } + for (int i = 0; i < 90; i++) { + for (int j = 0; j < 90; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-9.9 + j * 0.21, 0.1 + i * 0.21); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.1, 0.1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 10, 12); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/ccd2.c b/c/testbed/examples2d/ccd2.c new file mode 100644 index 000000000..7e5617029 --- /dev/null +++ b/c/testbed/examples2d/ccd2.c @@ -0,0 +1,102 @@ +/* Port of examples2d/ccd2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static int same(R2RigidBodyHandle handle, R2RigidBodyHandle handleB) { + return handle.world == handleB.world && handle.index == handleB.index && + handle.generation == handleB.generation; +} + +void tbCcd2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyHandle ground = {0}; + R2RigidBodyHandle sensor = {0}; + R2RigidBodyDesc floor = r2FixedRigidBodyDesc(); + floor.position.translation = r2Vector(0, 0); + floor.ccdEnabled = 1; + R2ColliderDesc boxCollider = r2CuboidColliderDesc(r2Vector(25, 0.1)); + floor.canSleep = !testbed->noSleep; + ground = r2InsertRigidBody(world, &floor); + r2InsertCollider(ground, &boxCollider); + + sensor = ground; + const R2Real x[] = {-3, 6, 2.5}; + for (int i = 0; i < 3; i++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.1, 25)); + collider.position.translation = r2Vector(x[i], 0); + if (i == 2) { + collider.isSensor = 1; + collider.activeEvents = R2_COLLISION_EVENTS; + } + r2InsertCollider(ground, &collider); + } + R2Pose poses[] = {r2TranslationPose(r2Vector(0, 0.35)), r2TranslationPose(r2Vector(-0.35, 0)), + r2TranslationPose(r2Vector(0.35, 0))}; + R2SharedShape *shapes[3] = {NULL}; + shapes[0] = r2CuboidSharedShape(r2Vector(0.4, 0.05)); + shapes[1] = r2CuboidSharedShape(r2Vector(0.05, 0.4)); + shapes[2] = r2CuboidSharedShape(r2Vector(0.05, 0.4)); + R2ColliderDesc shape; + R2CompoundShapeDesc shapeParts[3]; + for (size_t part = 0; part < 3; ++part) { + shapeParts[part].pose = poses[part]; + shapeParts[part].shape = r2DefaultShapeDesc(); + shapeParts[part].shape.kind = R2_SHAPE_DESC_SHARED; + shapeParts[part].shape.sharedShape = ((const R2SharedShape *const *)shapes)[part]; + } + shape = r2DefaultColliderDesc(); + shape.shape.kind = R2_SHAPE_DESC_COMPOUND; + shape.shape.children = (R2CompoundShapeView){shapeParts, 3}; + + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 6; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 0.82 - 1.96, j * 0.82 + 4.41); + rigidBody.linvel = r2Vector(100, -10); + rigidBody.ccdEnabled = 1; + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &shape); + } + } + for (size_t part = 0; part < 3; part++) { + r2FreeSharedShape(shapes[part]); + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + R2EventCollector *eventHandler = r2NewEventCollector(); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2EventCollector_Clear(eventHandler); + r2Step(world, NULL, eventHandler); + + size_t n = r2EventCollector_CollisionEvents(eventHandler, NULL, 0); + R2CollisionEvent *events = calloc(n ? n : 1, sizeof(*events)); + if (!events) { + abort(); + } + n = r2EventCollector_CollisionEvents(eventHandler, events, n); + for (size_t i = 0; i < n; i++) { + R2ColliderHandle colliderHandles[] = {events[i].collider1, events[i].collider2}; + for (size_t j = 0; j < 2; j++) { + R2RigidBodyHandle handle = r2Collider_Parent(colliderHandles[j]); + if (!same(handle, ground) && !same(handle, sensor)) { + tbBodyColor(testbed, handle, events[i].started ? 1 : 0.5f, + events[i].started ? 1 : 0.5f, events[i].started ? 0 : 1, 1); + } + } + } + free(events); + } + } + r2FreeEventCollector(eventHandler); + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/character_controller2.c b/c/testbed/examples2d/character_controller2.c new file mode 100644 index 000000000..6789640ae --- /dev/null +++ b/c/testbed/examples2d/character_controller2.c @@ -0,0 +1,164 @@ +/* Port of examples2d/character_controller2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include "utils/character.h" + +void tbCharacterController2(Testbed *testbed) { + R2World *world = r2NewWorld(); + + const R2Real groundSize = 5, groundHeight = .1; + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -groundHeight); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(groundSize, groundHeight)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Character with stronger gravity and predictive CCD for PID control. */ + R2RigidBodyHandle characterHandle; + { + R2RigidBodyDesc rigidBody = r2KinematicPositionBasedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-3, 5); + rigidBody.gravityScale = 10; + rigidBody.softCcdPrediction = 10; + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CapsuleYColliderDesc(.3, .15); + characterHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(characterHandle, &collider); + } + tbBodyColor(testbed, characterHandle, .8f, .1f, .1f, 1); + /* Cubes. */ + const int num = 8; + const R2Real rad = .1, shift = rad * 2, centerx = shift * (num / 2), centery = rad; + for (int j = 0; j < 4; ++j) { + for (int i = 0; i < num; ++i) { + const R2Real x = i * shift - centerx, y = j * shift + centery; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, y); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(rad, rad)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Stairs. */ + const R2Real stairWidth = 1, stairHeight = .1; + for (int i = 0; i < 10; ++i) { + const R2Real x = i * stairWidth / 2, y = i * stairHeight * 1.5 + 3; + { + R2ColliderDesc collider = + r2CuboidColliderDesc(r2Vector(stairWidth / 2, stairHeight / 2)); + collider.position.translation = r2Vector(x, y); + r2InsertColliderWithoutParent(world, &collider); + } + } + /* Climbable and unclimbable slopes. */ + const R2Real slopeAngle = .2, slopeSize = 2, impossibleSlopeSize = 2; + const R2Real impossibleSlopeAngle = .9; + { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(slopeSize, groundHeight)); + collider.position.translation = r2Vector(groundSize + slopeSize, -groundHeight + .4); + collider.position.rotation = r2Rotation(slopeAngle); + r2InsertColliderWithoutParent(world, &collider); + } + { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(slopeSize, groundHeight)); + collider.position.translation = + r2Vector(groundSize + slopeSize * 2 + impossibleSlopeSize - .9, -groundHeight + 2.3); + collider.position.rotation = r2Rotation(impossibleSlopeAngle); + r2InsertColliderWithoutParent(world, &collider); + } + /* Wall and horizontal ledge. */ + const R2Vector wallPos = + r2Vector(groundSize + slopeSize * 2 + impossibleSlopeSize + .35, -groundHeight + 2.5 * 2.3); + { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(2, groundHeight)); + collider.position.translation = wallPos; + collider.position.rotation = r2Rotation(R2_PI / 2); + r2InsertColliderWithoutParent(world, &collider); + } + { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(2, groundHeight)); + collider.position.translation = wallPos; + r2InsertColliderWithoutParent(world, &collider); + } + /* Moving platform. */ + R2RigidBodyHandle platformHandle; + { + R2RigidBodyDesc rigidBody = r2KinematicVelocityBasedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-8, 0); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(2, groundHeight)); + platformHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(platformHandle, &collider); + } + /* Wavy heightfield. */ + const int nsubdivs = 20; + R2Real heights[21]; + for (int i = 0; i <= nsubdivs; ++i) { + heights[i] = cos(i * 10.0 / nsubdivs / 2) * 1.5; + } + { + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_HEIGHTFIELD; + collider.shape.heights = (R2RealView){heights, (21) * (1)}; + collider.shape.rows = 21; + collider.shape.columns = 1; + collider.shape.scale = r2Vector(10, 1); + collider.shape.flags = 0; + collider.position.translation = r2Vector(-8, 5); + r2InsertColliderWithoutParent(world, &collider); + } + /* Tilting dynamic body with a limited joint. */ + R2RigidBodyDesc ground = r2FixedRigidBodyDesc(); + ground.position.translation = r2Vector(0, 5); + R2RigidBodyHandle groundHandle, handle; + groundHandle = r2InsertRigidBody(world, &ground); + + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(1, 0.1)); + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + } + { + R2JointDesc joint = r2RevoluteJointDesc(); + r2JointDesc_SetLimits(&joint, R2_AXIS_ANG_X, -.3, .3); + r2InsertImpulseJoint(groundHandle, handle, &joint); + } + CharacterControlMode controlMode = CHARACTER_KINEMATIC; + R2KinematicCharacterController *controller = NULL; + R2PidController *pid = NULL; + controller = r2NewKinematicCharacterController(); + pid = r2NewPidController(); + r2KinematicCharacterController_SetSlopes(controller, impossibleSlopeAngle - .02, + impossibleSlopeAngle - .02); + tbCamera2(testbed, 0, 1, 100); + + tbSetWorld(testbed, world); + size_t stepId = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + ++stepId; + + R2Real dt = r2TimeStep(world); + const R2Vector linvel = r2Vector(sin(stepId * dt * 2) * 2, sin(stepId * dt * 5) * 1.5); + + r2RigidBody_SetLinvel(platformHandle, linvel, 1); + updateCharacter(testbed, world, &controlMode, controller, pid, characterHandle); + } + } + r2FreePidController(pid); + r2FreeKinematicCharacterController(controller); + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/collision_groups2.c b/c/testbed/examples2d/collision_groups2.c new file mode 100644 index 000000000..2c7099bda --- /dev/null +++ b/c/testbed/examples2d/collision_groups2.c @@ -0,0 +1,50 @@ +/* Port of examples2d/collision_groups2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbCollisionGroups2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyHandle floor; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.1); + R2ColliderDesc boxCollider = r2CuboidColliderDesc(r2Vector(5, 0.1)); + rigidBody.canSleep = !testbed->noSleep; + floor = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(floor, &boxCollider); + for (int i = 1; i <= 2; i++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(1, 0.1)); + collider.position.translation = r2Vector(0, i); + collider.collisionGroups = (R2InteractionGroups){(uint32_t)i, (uint32_t)i, R2_GROUPS_AND}; + R2ColliderHandle colliderHandle = r2InsertCollider(floor, &collider); + tbColliderColor(testbed, colliderHandle, 0, i == 1, i == 2, 1); + } + for (int j = 0; j < 4; j++) { + for (int i = 0; i < 8; i++) { + uint32_t group = i % 2 ? 2 : 1; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.1, 0.1)); + collider.collisionGroups = (R2InteractionGroups){group, group, R2_GROUPS_AND}; + R2RigidBodyHandle handle; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 0.2 - 0.8, j * 0.2 + 2.5); + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + tbBodyColor(testbed, handle, 0, group == 1, group == 2, 1); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 1, 100); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/convex_polygons2.c b/c/testbed/examples2d/convex_polygons2.c new file mode 100644 index 000000000..faa8d3b66 --- /dev/null +++ b/c/testbed/examples2d/convex_polygons2.c @@ -0,0 +1,55 @@ +/* Port of examples2d/convex_polygons2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "example_math.h" + +void tbConvexPolygons2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(30, 1.2)); + groundBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle groundBodyHandle = r2InsertRigidBody(world, &groundBody); + r2InsertCollider(groundBodyHandle, &collider); + + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 30, 60); + rigidBody.position = r2Pose(r2Vector(side * 30, 60), r2Rotation(R2_PI / 2)); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(60, 1.2)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int i = 0; i < 14; i++) { + for (int j = 0; j < 56; j++) { + R2Vector points[10]; + for (size_t k = 0; k < 10; k++) { + R2Real x = exampleRandom(&testbed->randomState) * 4; + R2Real y = exampleRandom(&testbed->randomState) * 4; + points[k] = r2Vector(x, y); + } + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 4 - 28, j * 8 + 4); + R2ColliderDesc collider = r2DefaultColliderDesc(); + r2ShapeDesc_SetConvexHull(&collider.shape, (R2VectorView){points, 10}); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 50, 10); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/damping2.c b/c/testbed/examples2d/damping2.c new file mode 100644 index 000000000..cd6042c4a --- /dev/null +++ b/c/testbed/examples2d/damping2.c @@ -0,0 +1,39 @@ +/* Port of examples2d/damping2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDamping2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, 0)); + const int num = 10; + const R2Real subdiv = (R2Real)1 / num; + for (int i = 0; i < num; i++) { + R2Real x = (R2Real)sin(i * subdiv * R2_PI * 2); + R2Real y = (R2Real)cos(i * subdiv * R2_PI * 2); + R2RigidBodyDesc body = r2DynamicRigidBodyDesc(); + body.position.translation = r2Vector(x, y); + body.linvel = r2Vector(x * 10, y * 10); + body.angvel = 100; + body.linearDamping = (i + 1) * subdiv * 10; + body.angularDamping = (num - i) * subdiv * 10; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.2, 0.2)); + body.canSleep = !testbed->noSleep; + R2RigidBodyHandle bodyHandle = r2InsertRigidBody(world, &body); + r2InsertCollider(bodyHandle, &collider); + } + /* Set up the viewer. */ + tbCamera2(testbed, 3, 2, 50); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/debug_angular_limits2.c b/c/testbed/examples2d/debug_angular_limits2.c new file mode 100644 index 000000000..bc0b958f5 --- /dev/null +++ b/c/testbed/examples2d/debug_angular_limits2.c @@ -0,0 +1,60 @@ +/* Port of examples2d/debug_angular_limits2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugAngularLimits2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, 0)); + int multi = (int)tbSetting(testbed, "Multibody joints", 0, 0, 1, 1); + const R2Real limits[][2] = {{-45, 45}, {0, 270}, {135, 225}, {-350, 0}, {-200, 200}}; + for (int i = 0; i < 5; i++) { + for (int dir = 1; dir >= -1; dir -= 2) { + R2Vector position = r2Vector(i * 4, dir > 0 ? 0 : -4); + R2RigidBodyHandle anchor; + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = position; + R2ColliderDesc collider = r2BallColliderDesc(0.2); + groundBody.canSleep = !testbed->noSleep; + anchor = r2InsertRigidBody(world, &groundBody); + r2InsertCollider(anchor, &collider); + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2VectorAdd(position, r2Vector(1, 0)); + rigidBody.angularDamping = 3; + rigidBody.canSleep = 0; + R2RigidBodyHandle handle; + R2ColliderDesc boxCollider = r2CuboidColliderDesc(r2Vector(0.5, 0.1)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &boxCollider); + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2Vector(0, 0); + joint.localFrame2.translation = r2Vector(-1, 0); + r2JointDesc_SetLimits(&joint, R2_AXIS_ANG_X, limits[i][0] * R2_PI / 180, + limits[i][1] * R2_PI / 180); + r2JointDesc_SetMotor(&joint, R2_AXIS_ANG_X, 0, dir * 5, 0, 20); + if (multi) { + r2InsertMultibodyJoint(anchor, handle, &joint); + } else { + r2InsertImpulseJoint(anchor, handle, &joint); + } + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 8, -2, 40); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/debug_box_ball2.c b/c/testbed/examples2d/debug_box_ball2.c new file mode 100644 index 000000000..dfe7ca5b7 --- /dev/null +++ b/c/testbed/examples2d/debug_box_ball2.c @@ -0,0 +1,39 @@ +/* Port of examples2d/debug_box_ball2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugBoxBall2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc floor = r2FixedRigidBodyDesc(); + floor.position.translation = r2Vector(0, -1); + floor.position = r2Pose(r2Vector(0, -1), r2Rotation(R2_PI / 4)); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(1, 1)); + floor.canSleep = !testbed->noSleep; + R2RigidBodyHandle floorHandle = r2InsertRigidBody(world, &floor); + r2InsertCollider(floorHandle, &collider); + R2RigidBodyDesc ball = r2DynamicRigidBodyDesc(); + ball.position.translation = r2Vector(0, 3); + ball.canSleep = 0; + R2ColliderDesc ballCollider = r2BallColliderDesc(1); + if (testbed->noSleep) { + ball.canSleep = 0; + ball.sleeping = 0; + } + floorHandle = r2InsertRigidBody(world, &ball); + r2InsertCollider(floorHandle, &ballCollider); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 0, 50); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/debug_compression2.c b/c/testbed/examples2d/debug_compression2.c new file mode 100644 index 000000000..72eb9aab8 --- /dev/null +++ b/c/testbed/examples2d/debug_compression2.c @@ -0,0 +1,56 @@ +/* Port of examples2d/debug_compression2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugCompression2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, side * 32); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(75, 2)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + R2RigidBodyHandle handle[2] = {0}; + for (int i = 0; i < 2; i++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i ? 73 : -73, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(2, 30)); + rigidBody.canSleep = !testbed->noSleep; + handle[i] = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle[i], &collider); + } + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 8; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 7.5 - 30, j * 7.5 - 26.25); + R2ColliderDesc collider = r2BallColliderDesc(3.75); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 0, 50); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + + for (int i = 0; i < 2; i++) { + r2RigidBody_ResetForces(handle[i], 1); + r2RigidBody_AddForce(handle[i], + r2Vector((i ? -1 : 1) * (R2Real)testbed->step * 10000, 0), 1); + } + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/debug_intersection2.c b/c/testbed/examples2d/debug_intersection2.c new file mode 100644 index 000000000..cd5b724e4 --- /dev/null +++ b/c/testbed/examples2d/debug_intersection2.c @@ -0,0 +1,58 @@ +/* Port of examples2d/debug_intersection2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugIntersection2(Testbed *testbed) { + R2World *world = r2NewWorld(); + const R2Real rad = 1; + R2ColliderDesc collider = r2BallColliderDesc(rad); + const int count = 100; + R2RigidBodyHandle handles[100 * 100]; + for (int x = 0; x < count; ++x) { + for (int y = 0; y < count; ++y) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = + r2Vector((x - count / 2.0) * rad * 3, (y - count / 2.0) * rad * 3); + R2RigidBodyHandle handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + + handles[x * count + y] = handle; + tbBodyColor(testbed, handle, x / (float)count, (count - y) / (float)count, .5, 1); + } + } + + tbCamera2(testbed, 0, 0, 50); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + size_t stepId = 0; + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + ++stepId; + const R2Real slowTime = stepId / 3.0; + + R2SharedShape *ball = r2BallSharedShape(rad / 2); + const R2Pose pose = r2TranslationPose(r2Vector(cos(slowTime) * 10, sin(slowTime) * 10)); + size_t intersectionCount = r2IntersectShape(world, NULL, pose, ball, NULL, 0); + R2ColliderHandle *intersections = malloc(intersectionCount * sizeof(*intersections)); + if (intersectionCount && !intersections) { + abort(); + } + intersectionCount = + r2IntersectShape(world, NULL, pose, ball, intersections, intersectionCount); + r2FreeSharedShape(ball); + + for (size_t i = 0; i < intersectionCount; ++i) { + for (size_t j = 0; j < TB_COUNT(handles); ++j) { + tbBodyColor(testbed, handles[j], .5, .5, .5, 1); + } + + R2RigidBodyHandle bodyHandle = r2Collider_Parent(intersections[i]); + tbBodyColor(testbed, bodyHandle, 1, 0, 0, 1); + } + free(intersections); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/debug_many_colliders2.c b/c/testbed/examples2d/debug_many_colliders2.c new file mode 100644 index 000000000..0d110cb3e --- /dev/null +++ b/c/testbed/examples2d/debug_many_colliders2.c @@ -0,0 +1,206 @@ +/* Port of examples2d/debug_many_colliders2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static const R2Vector part0[] = { + {(R2Real)(525.0 * 0.01), (R2Real)(104.0 * 0.01)}, + {(R2Real)(540.0 * 0.01), (R2Real)(104.0 * 0.01)}, + {(R2Real)(419.0 * 0.01), (R2Real)(119.0 * 0.01)}, +}; + +static const R2Vector part1[] = { + {(R2Real)(419.0 * 0.01), (R2Real)(119.0 * 0.01)}, + {(R2Real)(449.0 * 0.01), (R2Real)(74.0 * 0.01)}, + {(R2Real)(510.0 * 0.01), (R2Real)(59.0 * 0.01)}, + {(R2Real)(525.0 * 0.01), (R2Real)(104.0 * 0.01)}, +}; + +static const R2Vector part2[] = { + {(R2Real)(299.0 * 0.01), (R2Real)(134.0 * 0.01)}, + {(R2Real)(419.0 * 0.01), (R2Real)(119.0 * 0.01)}, + {(R2Real)(540.0 * 0.01), (R2Real)(104.0 * 0.01)}, + {(R2Real)(540.0 * 0.01), (R2Real)(134.0 * 0.01)}, +}; + +static const R2Vector part3[] = { + {(R2Real)(315.0 * 0.01), (R2Real)(450.0 * 0.01)}, + {(R2Real)(179.0 * 0.01), (R2Real)(284.0 * 0.01)}, + {(R2Real)(179.0 * 0.01), (R2Real)(254.0 * 0.01)}, + {(R2Real)(224.0 * 0.01), (R2Real)(224.0 * 0.01)}, +}; + +static const R2Vector part4[] = { + {(R2Real)(224.0 * 0.01), (R2Real)(224.0 * 0.01)}, + {(R2Real)(224.0 * 0.01), (R2Real)(223.0 * 0.01)}, + {(R2Real)(299.0 * 0.01), (R2Real)(134.0 * 0.01)}, + {(R2Real)(540.0 * 0.01), (R2Real)(134.0 * 0.01)}, + {(R2Real)(555.0 * 0.01), (R2Real)(134.0 * 0.01)}, + {(R2Real)(555.0 * 0.01), (R2Real)(209.0 * 0.01)}, + {(R2Real)(359.0 * 0.01), (R2Real)(465.0 * 0.01)}, + {(R2Real)(315.0 * 0.01), (R2Real)(450.0 * 0.01)}, +}; + +static const R2Vector part5[] = { + {(R2Real)(119.0 * 0.01), (R2Real)(359.0 * 0.01)}, + {(R2Real)(134.0 * 0.01), (R2Real)(314.0 * 0.01)}, + {(R2Real)(179.0 * 0.01), (R2Real)(284.0 * 0.01)}, + {(R2Real)(315.0 * 0.01), (R2Real)(450.0 * 0.01)}, + {(R2Real)(315.0 * 0.01), (R2Real)(465.0 * 0.01)}, + {(R2Real)(300.0 * 0.01), (R2Real)(465.0 * 0.01)}, +}; + +static const R2Vector part6[] = { + {(R2Real)(164.0 * 0.01), (R2Real)(510.0 * 0.01)}, + {(R2Real)(134.0 * 0.01), (R2Real)(495.0 * 0.01)}, + {(R2Real)(240.0 * 0.01), (R2Real)(510.0 * 0.01)}, +}; + +static const R2Vector part7[] = { + {(R2Real)(240.0 * 0.01), (R2Real)(510.0 * 0.01)}, + {(R2Real)(240.0 * 0.01), (R2Real)(525.0 * 0.01)}, + {(R2Real)(164.0 * 0.01), (R2Real)(525.0 * 0.01)}, + {(R2Real)(164.0 * 0.01), (R2Real)(510.0 * 0.01)}, +}; + +static const R2Vector part8[] = { + {(R2Real)(134.0 * 0.01), (R2Real)(495.0 * 0.01)}, + {(R2Real)(104.0 * 0.01), (R2Real)(359.0 * 0.01)}, + {(R2Real)(119.0 * 0.01), (R2Real)(359.0 * 0.01)}, + {(R2Real)(300.0 * 0.01), (R2Real)(465.0 * 0.01)}, + {(R2Real)(270.0 * 0.01), (R2Real)(510.0 * 0.01)}, + {(R2Real)(240.0 * 0.01), (R2Real)(510.0 * 0.01)}, +}; + +static const R2Vector part9[] = { + {(R2Real)(615.0 * 0.01), (R2Real)(269.0 * 0.01)}, + {(R2Real)(660.0 * 0.01), (R2Real)(284.0 * 0.01)}, + {(R2Real)(660.0 * 0.01), (R2Real)(359.0 * 0.01)}, +}; + +static const R2Vector part10[] = { + {(R2Real)(673.6813186813187 * 0.01), (R2Real)(390.010989010989 * 0.01)}, + {(R2Real)(660.0 * 0.01), (R2Real)(359.0 * 0.01)}, + {(R2Real)(675.0 * 0.01), (R2Real)(359.0 * 0.01)}, + {(R2Real)(675.0 * 0.01), (R2Real)(389.0 * 0.01)}, +}; + +static const R2Vector part11[] = { + {(R2Real)(675.0 * 0.01), (R2Real)(389.0 * 0.01)}, + {(R2Real)(735.0 * 0.01), (R2Real)(434.0 * 0.01)}, + {(R2Real)(735.0 * 0.01), (R2Real)(495.0 * 0.01)}, + {(R2Real)(720.0 * 0.01), (R2Real)(495.0 * 0.01)}, + {(R2Real)(673.6813186813187 * 0.01), (R2Real)(390.010989010989 * 0.01)}, +}; + +static const R2Vector part12[] = { + {(R2Real)(645.0 * 0.01), (R2Real)(540.0 * 0.01)}, + {(R2Real)(645.0 * 0.01), (R2Real)(555.0 * 0.01)}, + {(R2Real)(494.0 * 0.01), (R2Real)(555.0 * 0.01)}, + {(R2Real)(494.0 * 0.01), (R2Real)(540.0 * 0.01)}, +}; + +static const R2Vector part13[] = { + {(R2Real)(705.0 * 0.01), (R2Real)(525.0 * 0.01)}, + {(R2Real)(645.0 * 0.01), (R2Real)(540.0 * 0.01)}, + {(R2Real)(494.0 * 0.01), (R2Real)(540.0 * 0.01)}, + {(R2Real)(464.0 * 0.01), (R2Real)(540.0 * 0.01)}, + {(R2Real)(464.0 * 0.01), (R2Real)(525.0 * 0.01)}, +}; + +static const R2Vector part14[] = { + {(R2Real)(660.0 * 0.01), (R2Real)(359.0 * 0.01)}, + {(R2Real)(720.0 * 0.01), (R2Real)(495.0 * 0.01)}, + {(R2Real)(705.0 * 0.01), (R2Real)(525.0 * 0.01)}, + {(R2Real)(464.0 * 0.01), (R2Real)(525.0 * 0.01)}, + {(R2Real)(434.0 * 0.01), (R2Real)(525.0 * 0.01)}, + {(R2Real)(434.0 * 0.01), (R2Real)(510.0 * 0.01)}, +}; + +static const R2Vector part15[] = { + {(R2Real)(660.0 * 0.01), (R2Real)(359.0 * 0.01)}, + {(R2Real)(434.0 * 0.01), (R2Real)(510.0 * 0.01)}, + {(R2Real)(404.0 * 0.01), (R2Real)(510.0 * 0.01)}, + {(R2Real)(404.0 * 0.01), (R2Real)(495.0 * 0.01)}, +}; + +static const R2Vector part16[] = { + {(R2Real)(404.0 * 0.01), (R2Real)(495.0 * 0.01)}, + {(R2Real)(359.0 * 0.01), (R2Real)(495.0 * 0.01)}, + {(R2Real)(359.0 * 0.01), (R2Real)(465.0 * 0.01)}, + {(R2Real)(555.0 * 0.01), (R2Real)(209.0 * 0.01)}, + {(R2Real)(570.0 * 0.01), (R2Real)(209.0 * 0.01)}, + {(R2Real)(615.0 * 0.01), (R2Real)(269.0 * 0.01)}, + {(R2Real)(660.0 * 0.01), (R2Real)(359.0 * 0.01)}, +}; + +static R2ColliderDesc polygon(const R2Vector *position, size_t n) { + R2ColliderDesc collider = r2DefaultColliderDesc(); + r2ShapeDesc_SetConvexHull(&collider.shape, (R2VectorView){position, n}); + return collider; +} + +void tbDebugManyColliders2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, 0)); + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + rigidBody.canSleep = 0; + rigidBody.angvel = 1; + R2RigidBodyHandle handle; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + handle = r2InsertRigidBody(world, &rigidBody); + for (int i = 0; i < 130; i++) { + R2ColliderDesc collider = polygon(part0, TB_COUNT(part0)); + r2InsertCollider(handle, &collider); + collider = polygon(part1, TB_COUNT(part1)); + r2InsertCollider(handle, &collider); + collider = polygon(part2, TB_COUNT(part2)); + r2InsertCollider(handle, &collider); + collider = polygon(part3, TB_COUNT(part3)); + r2InsertCollider(handle, &collider); + collider = polygon(part4, TB_COUNT(part4)); + r2InsertCollider(handle, &collider); + collider = polygon(part5, TB_COUNT(part5)); + r2InsertCollider(handle, &collider); + collider = polygon(part6, TB_COUNT(part6)); + r2InsertCollider(handle, &collider); + collider = polygon(part7, TB_COUNT(part7)); + r2InsertCollider(handle, &collider); + collider = polygon(part8, TB_COUNT(part8)); + r2InsertCollider(handle, &collider); + collider = polygon(part9, TB_COUNT(part9)); + r2InsertCollider(handle, &collider); + collider = polygon(part10, TB_COUNT(part10)); + r2InsertCollider(handle, &collider); + collider = polygon(part11, TB_COUNT(part11)); + r2InsertCollider(handle, &collider); + collider = polygon(part12, TB_COUNT(part12)); + r2InsertCollider(handle, &collider); + collider = polygon(part13, TB_COUNT(part13)); + r2InsertCollider(handle, &collider); + collider = polygon(part14, TB_COUNT(part14)); + r2InsertCollider(handle, &collider); + collider = polygon(part15, TB_COUNT(part15)); + r2InsertCollider(handle, &collider); + collider = polygon(part16, TB_COUNT(part16)); + r2InsertCollider(handle, &collider); + } + /* Set up the viewer. */ + tbCamera2(testbed, 5, 3, 40); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/debug_self_intersect2.c b/c/testbed/examples2d/debug_self_intersect2.c new file mode 100644 index 000000000..bd78767c5 --- /dev/null +++ b/c/testbed/examples2d/debug_self_intersect2.c @@ -0,0 +1,228 @@ +/* Port of examples2d/debug_self_intersect2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static R2SoftBodyDesc strip(R2Vector center) { + R2SoftBodyDesc builder = r2GridSoftBodyDesc(center, r2Vector(3, .15), 3, 2); + static const uint32_t pinned[] = {0, 1, 4, 5}; + r2SoftBodyDesc_SetPinnedParticles(&builder, (R2IndexView){(const uint32_t *)pinned, 4}); + builder.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){3, 1}); + builder.selfContacts = 1; + return builder; +} + +static void reload(R2SoftBodyHandle bullet, R2Vector cannonOrigin) { + size_t n = r2SoftBody_NumParticles(bullet); + for (size_t i = 0; i < n; ++i) { + const R2Real angle = 2 * R2_PI * i / n; + const R2Vector p = + r2VectorAdd(cannonOrigin, r2Vector(.3 * cos(angle), 2.5 + .3 * sin(angle))); + r2SoftBody_SetParticlePosition(bullet, i, p); + r2SoftBody_SetParticleVelocity(bullet, i, r2Vector(0, -150)); + } +} + +static R2SoftBodyHandle eightBlob(Testbed *testbed, R2World *world, R2Vector center, R2Real radius, + size_t n) { + R2SoftBodyDesc builder = r2DiskSoftBodyDesc(center, radius, n); + builder.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){4, 1}); + builder.selfContacts = 1; + builder.particleMass = .05; + builder.canSleep = !testbed->noSleep; + R2SoftBodyHandle handle = r2InsertSoftBody(world, &builder); + + return handle; +} + +static void geronoEight(R2SoftBodyHandle h, R2Vector center, R2Real r) { + size_t n = r2SoftBody_NumParticles(h); + for (size_t i = 0; i < n; ++i) { + const R2Real t = 2 * R2_PI * i / n; + r2SoftBody_SetParticlePosition( + h, i, r2VectorAdd(center, r2Vector(r * cos(t), r * sin(t) * cos(t)))); + } +} + +static void asymEight(R2SoftBodyHandle h, R2Vector center, R2Real rb, R2Real rs) { + size_t n = r2SoftBody_NumParticles(h); + const size_t nBig = (size_t)(n * rb / (rb + rs)); + for (size_t i = 0; i < n; ++i) { + R2Vector p; + if (i < nBig) { + const R2Real t = 2 * R2_PI * i / nBig; + p = r2VectorAdd(center, r2Vector(-rb + rb * cos(t), rb * sin(t))); + } else { + const R2Real t = 2 * R2_PI * (i - nBig) / (n - nBig); + p = r2VectorAdd(center, r2Vector(rs - rs * cos(t), -rs * sin(t))); + } + r2SoftBody_SetParticlePosition(h, i, p); + } +} + +static void eightFloor(Testbed *testbed, R2World *world, R2Real x) { + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, -6.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(3, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } +} + +void tbDebugSelfIntersect2(Testbed *testbed) { + R2World *world = r2NewWorld(); + + R2SoftBodyHandle captured; + { + R2SoftBodyDesc builder = strip(r2Vector(-16, 0)); + builder.canSleep = !testbed->noSleep; + captured = r2InsertSoftBody(world, &builder); + } + + r2SoftBody_SetParticlePosition(captured, 3, r2Vector(-15, -.4)); + R2SoftBodyHandle loaded; + { + R2SoftBodyDesc builder = strip(r2Vector(-6, 0)); + builder.canSleep = !testbed->noSleep; + loaded = r2InsertSoftBody(world, &builder); + } + + r2SoftBody_SetParticlePosition(loaded, 3, r2Vector(-5, -.4)); + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-5.7, 0.8); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2BallColliderDesc(.45); + collider.density = 20; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(2, -1); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(2.5, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + R2SoftBodyHandle blob; + R2SoftBodyDesc blobBuilder = r2DiskSoftBodyDesc(r2Vector(2, 0), .5, 16); + blobBuilder.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){4, 1}); + blobBuilder.selfContacts = 1; + blobBuilder.particleMass = .05; + blobBuilder.canSleep = !testbed->noSleep; + blob = r2InsertSoftBody(world, &blobBuilder); + + r2SoftBody_SetParticlePosition(blob, 4, r2Vector(2, -.8)); + const R2Vector grabOrigin = {12, 0}; + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(12, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(2.5, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + uint32_t bottomRow[7]; + for (uint32_t i = 0; i < 7; ++i) { + bottomRow[i] = i * 3; + } + R2SoftBodyHandle groundStrip; + R2SoftBodyDesc groundBuilder = + r2GridSoftBodyDesc(r2Vector(12, 0.15), r2Vector(1.5, 0.15), 7, 3); + r2SoftBodyDesc_SetPinnedParticles(&groundBuilder, (R2IndexView){(const uint32_t *)bottomRow, 7}); + groundBuilder.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){5, 1}); + groundBuilder.selfContacts = 1; + groundBuilder.canSleep = !testbed->noSleep; + groundStrip = r2InsertSoftBody(world, &groundBuilder); + + const uint32_t grabbed = 11; + + R2Vector anchor = r2SoftBody_ParticlePosition(groundStrip, grabbed); + uint32_t cluster = r2SoftBody_AddCluster(groundStrip, &grabbed, 1); + + R2RigidBodyHandle proxy = r2SoftBody_ClusterProxy(groundStrip, cluster); + R2RigidBodyDesc mouseBuilder = r2KinematicPositionBasedRigidBodyDesc(); + mouseBuilder.position.translation = anchor; + R2RigidBodyHandle mouse = r2InsertRigidBody(world, &mouseBuilder); + R2JointDesc joint = r2DefaultJointDesc(); + joint.lockedAxes = 0; + r2JointDesc_SetMotorPosition(&joint, R2_AXIS_LIN_X, 0, 1000, 50); + r2JointDesc_SetMotorPosition(&joint, R2_AXIS_LIN_Y, 0, 1000, 50); + r2InsertImpulseJoint(mouse, proxy, &joint); + + const R2Vector cannonOrigin = {20.5, 0}; + const uint32_t cannonPins[] = {0, 1, 16, 17}; + R2SoftBodyDesc cannonStrip = r2GridSoftBodyDesc(cannonOrigin, r2Vector(2, 0.1), 9, 2); + r2SoftBodyDesc_SetPinnedParticles(&cannonStrip, (R2IndexView){(const uint32_t *)cannonPins, 4}); + cannonStrip.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){10, 1}); + cannonStrip.selfContacts = 1; + cannonStrip.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &cannonStrip); + + R2SoftBodyHandle bullet; + R2SoftBodyDesc bulletBuilder = r2DiskSoftBodyDesc(r2Vector(20.5, 2.5), .3, 12); + bulletBuilder.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){20, 1}); + bulletBuilder.particleMass = .2; + bulletBuilder.selfContacts = 1; + bulletBuilder.canSleep = !testbed->noSleep; + bullet = r2InsertSoftBody(world, &bulletBuilder); + + reload(bullet, cannonOrigin); + eightFloor(testbed, world, -14); + R2SoftBodyHandle asym = eightBlob(testbed, world, r2Vector(-14, -5.35), .6, 24); + asymEight(asym, r2Vector(-13.9, -5.35), .55, .3); + eightFloor(testbed, world, -7); + R2SoftBodyHandle sym = eightBlob(testbed, world, r2Vector(-7, -5.35), .6, 24); + geronoEight(sym, r2Vector(-7, -5.35), .6); + eightFloor(testbed, world, 1); + R2SoftBodyHandle e1 = eightBlob(testbed, world, r2Vector(0.65, -5.35), .6, 24); + geronoEight(e1, r2Vector(0.65, -5.35), .6); + R2SoftBodyHandle e2 = eightBlob(testbed, world, r2Vector(1.35, -5.05), .6, 24); + geronoEight(e2, r2Vector(1.35, -5.05), .6); + eightFloor(testbed, world, 8); + R2SoftBodyHandle swallowed = eightBlob(testbed, world, r2Vector(8, -5.35), .6, 24); + asymEight(swallowed, r2Vector(7.8, -5.35), .55, .3); + eightBlob(testbed, world, r2Vector(8.65, -5.35), 0.55, 20); + eightFloor(testbed, world, 15); + R2SoftBodyHandle overlapped = eightBlob(testbed, world, r2Vector(15, -5.35), .6, 24); + asymEight(overlapped, r2Vector(15.1, -5.35), .55, .3); + eightBlob(testbed, world, r2Vector(14.15, -5.35), 0.5, 20); + eightFloor(testbed, world, 22); + R2SoftBodyHandle threaded = eightBlob(testbed, world, r2Vector(22, -5.35), .6, 24); + asymEight(threaded, r2Vector(21.8, -5.35), .55, .3); + eightBlob(testbed, world, r2Vector(22.1, -5.35), 0.22, 14); + tbCamera2(testbed, 0, -2.5, 38); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + R2Real t = 0; + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + const R2Real cycle = fmod(t, 6); + R2Vector target; + if (cycle < 1) { + target = r2Vector(0, .5 - .48 * cycle); + } else if (cycle < 4) { + const R2Real w = 2 * R2_PI * (cycle - 1); + target = r2Vector(.3 * sin(w), .02 + .03 * (1 - cos(2 * w))); + } else if (cycle < 5) { + target = r2Vector(0, .02 + .48 * (cycle - 4)); + } else { + target = r2Vector(0, .5); + } + + r2RigidBody_SetNextKinematicTranslation(mouse, r2VectorAdd(grabOrigin, target)); + r2RigidBody_WakeUp(proxy, 1); + + R2Real dt = r2TimeStep(world); + if (fmod(t, 4) < dt && t > 0) { + reload(bullet, cannonOrigin); + } + r2Step(world, NULL, NULL); + t += dt; + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/debug_total_overlap2.c b/c/testbed/examples2d/debug_total_overlap2.c new file mode 100644 index 000000000..dcfce6994 --- /dev/null +++ b/c/testbed/examples2d/debug_total_overlap2.c @@ -0,0 +1,30 @@ +/* Port of examples2d/debug_total_overlap2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugTotalOverlap2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + for (int i = 0; i < 100; i++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 0, 50); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/debug_vertical_column2.c b/c/testbed/examples2d/debug_vertical_column2.c new file mode 100644 index 000000000..fb075b9e6 --- /dev/null +++ b/c/testbed/examples2d/debug_vertical_column2.c @@ -0,0 +1,38 @@ +/* Port of examples2d/debug_vertical_column2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugVerticalColumn2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2ColliderDesc floor = r2CuboidColliderDesc(r2Vector(1, 1)); + floor.friction = 0.3; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &floor); + for (int i = 0; i < 80; i++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + collider.friction = 0.3; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, i + 1.5); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 5); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/drum2.c b/c/testbed/examples2d/drum2.c new file mode 100644 index 000000000..d953d6246 --- /dev/null +++ b/c/testbed/examples2d/drum2.c @@ -0,0 +1,55 @@ +/* Port of examples2d/drum2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDrum2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + for (int i = 0; i < 30; i++) { + for (int j = 0; j < 30; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 0.4 - 6, j * 0.4 - 6); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.2, 0.2)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + R2RigidBodyHandle drum = {0}; + R2RigidBodyDesc platformBody = r2KinematicVelocityBasedRigidBodyDesc(); + platformBody.position.translation = r2Vector(0, 0); + platformBody.canSleep = !testbed->noSleep; + drum = r2InsertRigidBody(world, &platformBody); + + for (int side = -1; side <= 1; side += 2) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(10, 0.25)); + collider.position.translation = r2Vector(0, side * 10); + r2InsertCollider(drum, &collider); + + collider = r2CuboidColliderDesc(r2Vector(0.25, 10)); + collider.position.translation = r2Vector(side * 10, 0); + r2InsertCollider(drum, &collider); + for (int x = -1; x <= 1; x += 2) { + collider = r2BallColliderDesc(1.25); + collider.position.translation = r2Vector(x * 6, side * 6); + r2InsertCollider(drum, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 1, 40); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + + r2RigidBody_SetAngvel(drum, -0.15, 1); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/heightfield2.c b/c/testbed/examples2d/heightfield2.c new file mode 100644 index 000000000..2cb6cfe08 --- /dev/null +++ b/c/testbed/examples2d/heightfield2.c @@ -0,0 +1,54 @@ +/* Port of examples2d/heightfield2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbHeightfield2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2Real heights[2001]; + for (int i = 0; i <= 2000; i++) { + heights[i] = i == 0 || i == 2000 ? 8 : (R2Real)cos(i * 50.0 / 2000) * 2; + } + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_HEIGHTFIELD; + collider.shape.heights = (R2RealView){heights, (2001) * (1)}; + collider.shape.rows = 2001; + collider.shape.columns = 1; + collider.shape.scale = r2Vector(50, 1); + collider.shape.flags = 0; + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + for (int i = 0; i < 20; i++) { + for (int j = 0; j < 20; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i - 10, j + 3.5); + R2ColliderDesc collider; + if (j % 2) { + collider = r2BallColliderDesc(0.5); + } else { + collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + } + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 0, 10); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/inv_pyramid2.c b/c/testbed/examples2d/inv_pyramid2.c new file mode 100644 index 000000000..aba3d65d2 --- /dev/null +++ b/c/testbed/examples2d/inv_pyramid2.c @@ -0,0 +1,40 @@ +/* Port of examples2d/inv_pyramid2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbInvPyramid2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(10, 1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + R2Real rad = 0.5; + R2Real y = rad; + for (int i = 0; i < 6; i++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, y + 1); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(rad, rad)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + y += rad + rad * 2; + rad *= 2; + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/inverse_kinematics2.c b/c/testbed/examples2d/inverse_kinematics2.c new file mode 100644 index 000000000..69c4b81cc --- /dev/null +++ b/c/testbed/examples2d/inverse_kinematics2.c @@ -0,0 +1,75 @@ +/* Port of examples2d/inverse_kinematics2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbInverseKinematics2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.01); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(1, 0.01)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + + const int numSegments = 10; + R2RigidBodyDesc body = r2FixedRigidBodyDesc(); + R2RigidBodyHandle lastBody = r2InsertRigidBody(world, &body); + R2MultibodyJointHandle lastLink = {NULL, UINT32_MAX, UINT32_MAX}; + for (int i = 0; i < numSegments; ++i) { + const R2Real size = 1.0 / numSegments; + R2RigidBodyHandle newBody; + /* Sensors draw the links; IK does not require colliders. */ + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + rigidBody.canSleep = 0; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(size / 8, size / 2)); + collider.density = 0; + collider.isSensor = 1; + newBody = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(newBody, &collider); + } + R2JointDesc linkAb = r2RevoluteJointDesc(); + linkAb.localFrame1.translation = r2Vector(0, size / 2 * (i != 0)); + linkAb.localFrame2.translation = r2Vector(0, -size / 2); + lastLink = r2InsertMultibodyJoint(lastBody, newBody, &linkAb); + + lastBody = newBody; + } + tbCamera2(testbed, 0, 0, 300); + + tbSetWorld(testbed, world); + R2Real *displacements = NULL; + size_t capacity = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + if (!testbed->cursorValid) { + continue; + } + size_t ndofs = r2MultibodyJoint_Ndofs(lastLink); + if (capacity < ndofs) { + R2Real *resized = realloc(displacements, ndofs * sizeof(*resized)); + if (!resized) { + abort(); + } + displacements = resized; + capacity = ndofs; + } + memset(displacements, 0, ndofs * sizeof(*displacements)); + R2InverseKinematicsOptions options = r2DefaultInverseKinematicsOptions(); + options.constrained_axes = 3; /* Linear axes only. */ + const R2Pose target = r2TranslationPose(testbed->cursor); + r2MultibodyJoint_InverseKinematics(lastLink, &options, target, NULL, NULL, + displacements, ndofs); + r2MultibodyJoint_ApplyDisplacements(lastLink, displacements, ndofs); + } + } + free(displacements); + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/joint_motor_position2.c b/c/testbed/examples2d/joint_motor_position2.c new file mode 100644 index 000000000..195d1d305 --- /dev/null +++ b/c/testbed/examples2d/joint_motor_position2.c @@ -0,0 +1,59 @@ +/* Port of examples2d/joint_motor_position2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbJointMotorPosition2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyHandle ground; + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + groundBody.canSleep = !testbed->noSleep; + ground = r2InsertRigidBody(world, &groundBody); + for (int row = 0; row < 2; row++) { + for (int num = 0; num < (row ? 8 : 9); num++) { + R2Real x = -6 + 1.5 * num; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, row ? 4.5 : 2); + rigidBody.canSleep = 0; + if (row) { + rigidBody.position = r2Pose(r2Vector(x, 4.5), r2Rotation(R2_PI)); + } + R2RigidBodyHandle handle; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.1, 0.5)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2Vector(x, row ? 5 : 1.5); + joint.localFrame2.translation = r2Vector(0, -0.5); + R2Real angle = -R2_PI + R2_PI / 4 * num; + if (row) { + r2JointDesc_SetMotor(&joint, R2_AXIS_ANG_X, 0, 1.5, 0, 30); + r2JointDesc_SetMotorMaxForce(&joint, R2_AXIS_ANG_X, 100); + r2JointDesc_SetLimits(&joint, R2_AXIS_ANG_X, -R2_PI, angle); + } else { + r2JointDesc_SetMotor(&joint, R2_AXIS_ANG_X, angle, 0, 1000, 150); + } + r2InsertImpulseJoint(ground, handle, &joint); + } + } + r2SetGravity(world, r2Vector(0, 0)); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 0, 40); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/joints2.c b/c/testbed/examples2d/joints2.c new file mode 100644 index 000000000..9bb0e33e9 --- /dev/null +++ b/c/testbed/examples2d/joints2.c @@ -0,0 +1,55 @@ +/* Port of examples2d/joints2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbJoints2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + int variable = (int)tbSetting(testbed, "Variable softness", 0, 0, 1, 1); + R2RigidBodyHandle *handles = calloc(1, 100 * sizeof(*handles)); + if (!handles) { + abort(); + } + for (int k = 0; k < 10; k++) { + for (int i = 0; i < 10; i++) { + int fixed = i == 0 && k == 0; + R2RigidBodyHandle handle; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.bodyType = fixed ? R2_FIXED : R2_DYNAMIC; + rigidBody.position.translation = r2Vector(k, -i); + R2ColliderDesc collider = r2BallColliderDesc(0.4); + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + for (int dir = 0; dir < 2; dir++) { + if (dir ? k > 0 : i > 0) { + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2Vector(0, 0); + joint.localFrame2.translation = dir ? r2Vector(-1, 0) : r2Vector(0, 1); + if (variable) { + R2Real scale = (i > k ? i : k) + 1; + joint.softness = (R2SpringCoefficients){5 * scale, 0.1 * scale}; + } + r2InsertImpulseJoint(handles[k * 10 + i - (dir ? 10 : 1)], handle, + &joint); + } + } + handles[k * 10 + i] = handle; + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 4.0, -4.0, 20); + free(handles); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/locked_rotations2.c b/c/testbed/examples2d/locked_rotations2.c new file mode 100644 index 000000000..d313db88c --- /dev/null +++ b/c/testbed/examples2d/locked_rotations2.c @@ -0,0 +1,46 @@ +/* Port of examples2d/locked_rotations2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbLockedRotations2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.1); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(5, 0.1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + R2RigidBodyDesc rectangle = r2DynamicRigidBodyDesc(); + rectangle.position.translation = r2Vector(0, 3); + rectangle.lockedAxes = R2_LOCK_TRANSLATION_X | R2_LOCK_TRANSLATION_Y; + R2ColliderDesc boxCollider = r2CuboidColliderDesc(r2Vector(2, 0.6)); + rectangle.canSleep = !testbed->noSleep; + rigidBodyHandle = r2InsertRigidBody(world, &rectangle); + r2InsertCollider(rigidBodyHandle, &boxCollider); + + R2RigidBodyDesc capsule = r2DynamicRigidBodyDesc(); + capsule.position.translation = r2Vector(0, 5); + capsule.position = r2Pose(r2Vector(0, 5), r2Rotation(1)); + capsule.lockedAxes = R2_LOCK_ROTATION_Z; + R2ColliderDesc capsuleCollider = r2CapsuleYColliderDesc(0.6, 0.4); + capsule.canSleep = !testbed->noSleep; + rigidBodyHandle = r2InsertRigidBody(world, &capsule); + r2InsertCollider(rigidBodyHandle, &capsuleCollider); + + /* Set up the viewer. */ + tbCamera2(testbed, 0, 0, 40); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/multi_pendulum2.c b/c/testbed/examples2d/multi_pendulum2.c new file mode 100644 index 000000000..82f22cf0e --- /dev/null +++ b/c/testbed/examples2d/multi_pendulum2.c @@ -0,0 +1,69 @@ +/* Port of examples2d/multi_pendulum2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "example_math.h" + +void tbMultiPendulum2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + r2SetGravity(world, r2Vector(0, -9.81)); + int randomize = (int)tbSetting(testbed, "Randomize", 0, 0, 1, 1); + int count = (int)tbSetting(testbed, "Pendulum Count", 32, 1, 64, 1); + int segments = (int)tbSetting(testbed, "Pendulum Segments", 4, 1, 40, 1); + R2Real spacing = 2 * segments + 4; + int cols = (int)ceil(sqrt(count)); + int rows = (count + cols - 1) / cols; + for (int i = 0; i < count; i++) { + R2Vector end = r2Vector((i % cols - (cols - 1) * 0.5) * spacing, + ((rows - 1) * 0.5 - i / cols) * spacing); + R2Vector local = r2Vector(0, 0); + R2RigidBodyHandle parent; + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = end; + groundBody.canSleep = !testbed->noSleep; + parent = r2InsertRigidBody(world, &groundBody); + + for (int n = 0; n < segments; n++) { + R2Real angle = randomize ? (exampleRandom(&testbed->randomState) - 0.5) * R2_PI : 0; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = end; + rigidBody.position = r2Pose(end, r2Rotation(angle)); + rigidBody.canSleep = 0; + R2SharedShape *shape = r2CapsuleSharedShape(r2Vector(-1, 0), r2Vector(1, 0), 0.2); + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + collider.position.translation = r2Vector(1, 0); + R2RigidBodyHandle handle; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = local; + joint.localFrame2.translation = r2Vector(0, 0); + joint.contactsEnabled = 0; + r2InsertImpulseJoint(parent, handle, &joint); + parent = handle; + local = r2Vector(2, 0); + end = r2VectorAdd(end, r2Vector(2 * cos(angle), 2 * sin(angle))); + + r2FreeSharedShape(shape); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 0, (float)fmin(1000 / ((cols > rows ? cols : rows) * spacing), 20)); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/one_way_platforms2.c b/c/testbed/examples2d/one_way_platforms2.c new file mode 100644 index 000000000..ce3b97a64 --- /dev/null +++ b/c/testbed/examples2d/one_way_platforms2.c @@ -0,0 +1,96 @@ +/* Port of examples2d/one_way_platforms2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +typedef struct OneWayPlatformHook { + R2ColliderHandle platform1, platform2; +} OneWayPlatformHook; + +static int sameCollider(R2ColliderHandle a, R2ColliderHandle b) { + return a.world == b.world && a.index == b.index && a.generation == b.generation; +} + +static void RAPIER_CALL modifySolverContacts(void *userData, const R2ReadContext *read, + R2ColliderHandle collider1, R2ColliderHandle collider2, + R2ContactModificationContext *context) { + (void)read; + const OneWayPlatformHook *hook = userData; + R2Vector allowedLocalN1 = r2Vector(0, 0); + /* Flip the allowed normal when the platform is collider2. */ + if (sameCollider(collider1, hook->platform1)) { + allowedLocalN1 = r2Vector(0, 1); + } else if (sameCollider(collider2, hook->platform1)) { + allowedLocalN1 = r2Vector(0, -1); + } + if (sameCollider(collider1, hook->platform2)) { + allowedLocalN1 = r2Vector(0, -1); + } else if (sameCollider(collider2, hook->platform2)) { + allowedLocalN1 = r2Vector(0, 1); + } + r2ContactModificationContext_UpdateAsOnewayPlatform(context, allowedLocalN1, .1); + const R2Real tangentVelocity = + sameCollider(collider1, hook->platform1) || sameCollider(collider2, hook->platform2) ? -12 + : 12; + r2ContactModificationContext_SetTangentVelocity(context, r2Vector(tangentVelocity, 0)); +} + +void tbOneWayPlatforms2(Testbed *testbed) { + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(25, 0.5)); + collider.position.translation = r2Vector(30, 2); + collider.activeHooks = R2_MODIFY_SOLVER_CONTACTS; + R2RigidBodyHandle handle; + OneWayPlatformHook platformHook; + handle = r2InsertRigidBody(world, &rigidBody); + platformHook.platform1 = r2InsertCollider(handle, &collider); + collider = r2CuboidColliderDesc(r2Vector(25, 0.5)); + collider.position.translation = r2Vector(-30, -2); + collider.activeHooks = R2_MODIFY_SOLVER_CONTACTS; + platformHook.platform2 = r2InsertCollider(handle, &collider); + R2PhysicsHooks physicsHooks = {0}; + physicsHooks.user_data = &platformHook; + physicsHooks.modify_solver_contacts_context = modifySolverContacts; + tbCamera2(testbed, 0, 0, 20); + + tbSetWorld(testbed, world); + size_t stepId = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, &physicsHooks, NULL); + ++stepId; + size_t bodyCount = r2RigidBodyCount(world); + /* Spawn cubes periodically and reverse gravity below the lower platform. */ + if (stepId % 200 == 0 && bodyCount <= 7) { + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(20, 10); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(1.5, 2)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + size_t activeCount = r2ActiveRigidBodies(world, NULL, 0); + R2RigidBodyHandle *active = malloc(activeCount * sizeof(*active)); + if (activeCount && !active) { + abort(); + } + activeCount = r2ActiveRigidBodies(world, active, activeCount); + for (size_t i = 0; i < activeCount; ++i) { + R2Vector position = r2RigidBody_Translation(active[i]); + if (position.y > 1) { + r2RigidBody_SetGravityScale(active[i], 1, 0); + } else if (position.y < -1) { + r2RigidBody_SetGravityScale(active[i], -1, 0); + } + } + free(active); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/pin_slot_joint2.c b/c/testbed/examples2d/pin_slot_joint2.c new file mode 100644 index 000000000..b8efd4def --- /dev/null +++ b/c/testbed/examples2d/pin_slot_joint2.c @@ -0,0 +1,76 @@ +/* Port of examples2d/pin_slot_joint2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include "utils/character.h" + +void tbPinSlotJoint2(Testbed *testbed) { + R2World *world = r2NewWorld(); + + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.1); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(3, 0.1)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + R2RigidBodyHandle characterHandle, cubeHandle, ballHandle; + { + R2RigidBodyDesc rigidBody = r2KinematicPositionBasedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0.3); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.15, 0.3)); + characterHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(characterHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(1, 1); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.4, 0.4)); + cubeHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(cubeHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(1, 1); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2BallColliderDesc(.1); + ballHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(ballHandle, &collider); + } + { + R2JointDesc fixedJoint = r2FixedJointDesc(); + fixedJoint.localFrame1.translation = r2Vector(0, 0); + fixedJoint.localFrame2.translation = r2Vector(0, -0.4); + r2InsertImpulseJoint(cubeHandle, ballHandle, &fixedJoint); + } + { + R2JointDesc pinSlotJoint = r2PinSlotJointDesc(r2Vector(1 / sqrt(2), 1 / sqrt(2))); + pinSlotJoint.localFrame1.translation = r2Vector(2, 2); + pinSlotJoint.localFrame2.translation = r2Vector(0, 0.4); + r2JointDesc_SetLimits(&pinSlotJoint, R2_AXIS_LIN_X, -1, INFINITY); + r2InsertImpulseJoint(characterHandle, cubeHandle, &pinSlotJoint); + } + CharacterControlMode controlMode = CHARACTER_KINEMATIC; + R2KinematicCharacterController *controller = NULL; + R2PidController *pid = NULL; + controller = r2NewKinematicCharacterController(); + pid = r2NewPidController(); + tbCamera2(testbed, 0, 1, 100); + + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + updateCharacter(testbed, world, &controlMode, controller, pid, characterHandle); + } + } + r2FreePidController(pid); + r2FreeKinematicCharacterController(controller); + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/platform2.c b/c/testbed/examples2d/platform2.c new file mode 100644 index 000000000..66320ee50 --- /dev/null +++ b/c/testbed/examples2d/platform2.c @@ -0,0 +1,64 @@ +/* Port of examples2d/platform2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbPlatform2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.1); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(10, 0.1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 6; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 0.4 - 1.2, j * 0.4 + 3.24); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.2, 0.2)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + R2RigidBodyHandle velocityBasedPlatformHandle = {0}; + R2RigidBodyHandle positionBasedPlatformHandle = {0}; + R2RigidBodyDesc platformBody = r2KinematicVelocityBasedRigidBodyDesc(); + platformBody.position.translation = r2Vector(-2, 2.3); + R2ColliderDesc boxCollider = r2CuboidColliderDesc(r2Vector(2, 0.2)); + platformBody.canSleep = !testbed->noSleep; + velocityBasedPlatformHandle = r2InsertRigidBody(world, &platformBody); + r2InsertCollider(velocityBasedPlatformHandle, &boxCollider); + R2RigidBodyDesc positionBasedPlatform = r2KinematicPositionBasedRigidBodyDesc(); + positionBasedPlatform.position.translation = r2Vector(-2, 4.3); + R2ColliderDesc positionBasedCollider = r2CuboidColliderDesc(r2Vector(2, 0.2)); + positionBasedPlatform.canSleep = !testbed->noSleep; + positionBasedPlatformHandle = r2InsertRigidBody(world, &positionBasedPlatform); + r2InsertCollider(positionBasedPlatformHandle, &positionBasedCollider); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 1, 40); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + + R2Real dt = r2TimeStep(world); + R2Real time = (R2Real)(testbed->step + 1) * dt; + R2Vector velocity = r2Vector(sin(time) * 5, sin(time * 5)); + + r2RigidBody_SetLinvel(velocityBasedPlatformHandle, velocity, 1); + + R2Vector position = r2RigidBody_Translation(positionBasedPlatformHandle); + r2RigidBody_SetNextKinematicTranslation( + positionBasedPlatformHandle, + r2VectorAdd(position, r2VectorScale(velocity, dt))); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/polyline2.c b/c/testbed/examples2d/polyline2.c new file mode 100644 index 000000000..6afcfe55a --- /dev/null +++ b/c/testbed/examples2d/polyline2.c @@ -0,0 +1,54 @@ +/* Port of examples2d/polyline2.rs. */ +#include "testbed.h" +#include "rapier_math.h" +#include "rapier_helpers.h" + +void tbPolyline2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2Vector points[2000]; + R2Edge edges[1999]; + points[0] = r2Vector(-25, 40); + points[1999] = r2Vector(25, 40); + for (int i = 1; i < 1999; i++) { + points[i] = r2Vector(-25 + i * 0.025, cos(i * 0.025) * 2); + } + for (uint32_t i = 0; i < 1999; i++) { + edges[i] = (R2Edge){i, i + 1}; + } + R2RigidBodyDesc ground = r2FixedRigidBodyDesc(); + ground.canSleep = !testbed->noSleep; + R2ColliderDesc groundCollider = r2DefaultColliderDesc(); + r2ShapeDesc_SetPolyline(&groundCollider.shape, (R2VectorView){points, TB_COUNT(points)}, + (R2EdgeView){edges, TB_COUNT(edges)}, 0); + R2RigidBodyHandle groundHandle = r2InsertRigidBody(world, &ground); + r2InsertCollider(groundHandle, &groundCollider); + for (int i = 0; i < 20; i++) { + for (int j = 0; j < 20; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i - 10, j + 3.5); + R2ColliderDesc collider; + if (j % 2) { + collider = r2BallColliderDesc(0.5); + } else { + collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + } + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 0, 10); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/pyramid2.c b/c/testbed/examples2d/pyramid2.c new file mode 100644 index 000000000..96c952611 --- /dev/null +++ b/c/testbed/examples2d/pyramid2.c @@ -0,0 +1,46 @@ +/* Port of examples2d/pyramid2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbPyramid2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + const R2Real groundThickness = 1; + const R2Real rad = 0.5; + const R2Real shift = rad * 2; + const int num = 10; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(10, groundThickness)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + const R2Real centerx = shift * num / 2; + const R2Real centery = shift / 2 + groundThickness + rad * 1.5; + for (int i = 0; i < num; i++) { + for (int j = i; j < num; j++) { + R2Real x = i * shift / 2 + (j - i) * shift - centerx; + R2Real y = i * shift + centery; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, y); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(rad, rad)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/restitution2.c b/c/testbed/examples2d/restitution2.c new file mode 100644 index 000000000..3fb6559fe --- /dev/null +++ b/c/testbed/examples2d/restitution2.c @@ -0,0 +1,41 @@ +/* Port of examples2d/restitution2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbRestitution2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2ColliderDesc floor = r2CuboidColliderDesc(r2Vector(20, 1)); + floor.restitution = 1; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -1); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &floor); + const int num = 10; + for (int j = 0; j < 2; j++) { + for (int i = 0; i <= num; i++) { + R2ColliderDesc collider = r2BallColliderDesc(0.5); + collider.restitution = (R2Real)i / num; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector((i - num / 2.0) * 2, 10 * (j + 1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 1, 25); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/rope_joints2.c b/c/testbed/examples2d/rope_joints2.c new file mode 100644 index 000000000..0af20a320 --- /dev/null +++ b/c/testbed/examples2d/rope_joints2.c @@ -0,0 +1,77 @@ +/* Port of examples2d/rope_joints2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include "utils/character.h" + +void tbRopeJoints2(Testbed *testbed) { + R2World *world = r2NewWorld(); + + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.1); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.75, 0.1)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-0.85, 0.75); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.1, 0.75)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0.85, 0.75); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.1, 0.75)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Manually controlled character, tethered to a ball. */ + R2RigidBodyHandle characterHandle; + { + R2RigidBodyDesc rigidBody = r2KinematicPositionBasedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0.3); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.15, 0.3)); + characterHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(characterHandle, &collider); + } + R2RigidBodyHandle childHandle; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(1, 1); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2BallColliderDesc(.04); + childHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(childHandle, &collider); + } + { + R2JointDesc joint = r2RopeJointDesc(2); + r2InsertImpulseJoint(characterHandle, childHandle, &joint); + } + CharacterControlMode controlMode = CHARACTER_KINEMATIC; + R2KinematicCharacterController *controller = NULL; + R2PidController *pid = NULL; + controller = r2NewKinematicCharacterController(); + pid = r2NewPidController(); + tbCamera2(testbed, 0, 1, 100); + + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + updateCharacter(testbed, world, &controlMode, controller, pid, characterHandle); + } + } + r2FreePidController(pid); + r2FreeKinematicCharacterController(controller); + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_arch.c b/c/testbed/examples2d/s2d_arch.c new file mode 100644 index 000000000..d6f8fa850 --- /dev/null +++ b/c/testbed/examples2d/s2d_arch.c @@ -0,0 +1,92 @@ +/* Port of examples2d/s2d_arch.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbS2dArch(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + const R2Vector ps1[] = {r2Vector(16.0 * 0.25, 0.0 * 0.25), + r2Vector(14.93803712795643 * 0.25, 5.133601056842984 * 0.25), + r2Vector(13.79871746027416 * 0.25, 10.24928069555078 * 0.25), + r2Vector(12.56252963284711 * 0.25, 15.34107019122473 * 0.25), + r2Vector(11.20040987372525 * 0.25, 20.39856541571217 * 0.25), + r2Vector(9.66521217819836 * 0.25, 25.40369899225096 * 0.25), + r2Vector(7.87179930638133 * 0.25, 30.3179337000085 * 0.25), + r2Vector(5.635199558196225 * 0.25, 35.03820717801641 * 0.25), + r2Vector(2.405937953536585 * 0.25, 39.09554102558315 * 0.25)}; + const R2Vector ps2[] = {r2Vector(24.0 * 0.25, 0.0 * 0.25), + r2Vector(22.33619528222415 * 0.25, 6.02299846205841 * 0.25), + r2Vector(20.54936888969905 * 0.25, 12.00964361211476 * 0.25), + r2Vector(18.60854610798073 * 0.25, 17.9470321677465 * 0.25), + r2Vector(16.46769273811807 * 0.25, 23.81367936585418 * 0.25), + r2Vector(14.05325025774858 * 0.25, 29.57079353071012 * 0.25), + r2Vector(11.23551045834022 * 0.25, 35.13775818285372 * 0.25), + r2Vector(7.752568160730571 * 0.25, 40.30450679009583 * 0.25), + r2Vector(3.016931552701656 * 0.25, 44.28891593799322 * 0.25)}; + R2SharedShape *shape = r2SegmentSharedShape(r2Vector(-100, 0), r2Vector(100, 0)); + R2ColliderDesc floor = r2DefaultColliderDesc(); + floor.shape.kind = R2_SHAPE_DESC_SHARED; + floor.shape.sharedShape = shape; + floor.friction = 0.6; + r2InsertColliderWithoutParent(world, &floor); + for (int side = 0; side < 2; side++) { + for (int i = 0; i < 8; i++) { + R2Vector vertices[4]; + if (!side) { + vertices[0] = ps1[i]; + vertices[1] = ps2[i]; + vertices[2] = ps2[i + 1]; + vertices[3] = ps1[i + 1]; + } else { + vertices[0] = r2Vector(-ps2[i].x, ps2[i].y); + vertices[1] = r2Vector(-ps1[i].x, ps1[i].y); + vertices[2] = r2Vector(-ps1[i + 1].x, ps1[i + 1].y); + vertices[3] = r2Vector(-ps2[i + 1].x, ps2[i + 1].y); + } + R2ColliderDesc collider = r2DefaultColliderDesc(); + r2ShapeDesc_SetConvexHull(&collider.shape, (R2VectorView){vertices, 4}); + collider.friction = 0.6; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + R2Vector position[] = {ps1[8], ps2[8], r2Vector(-ps1[8].x, ps1[8].y), + r2Vector(-ps2[8].x, ps2[8].y)}; + R2ColliderDesc objectCollider = r2DefaultColliderDesc(); + r2ShapeDesc_SetConvexHull(&objectCollider.shape, (R2VectorView){position, 4}); + objectCollider.friction = 0.6; + R2RigidBodyDesc dynamicBody = r2DynamicRigidBodyDesc(); + dynamicBody.position.translation = r2Vector(0, 0); + dynamicBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle dynamicBodyHandle = r2InsertRigidBody(world, &dynamicBody); + r2InsertCollider(dynamicBodyHandle, &objectCollider); + + for (int i = 0; i < 4; i++) { + objectCollider = r2CuboidColliderDesc(r2Vector(2, 0.5)); + objectCollider.friction = 0.6; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0.5 + ps2[8].y + i); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &objectCollider); + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + r2FreeSharedShape(shape); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_ball_and_chain.c b/c/testbed/examples2d/s2d_ball_and_chain.c new file mode 100644 index 000000000..b507e2310 --- /dev/null +++ b/c/testbed/examples2d/s2d_ball_and_chain.c @@ -0,0 +1,69 @@ +/* Port of examples2d/s2d_ball_and_chain.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbS2dBallAndChain(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyHandle ground; + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + groundBody.canSleep = !testbed->noSleep; + ground = r2InsertRigidBody(world, &groundBody); + + R2RigidBodyHandle prev = ground; + for (int i = 0; i < 40; i++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0 + 0.5 + i, 20); + rigidBody.linearDamping = 0.1; + rigidBody.angularDamping = 0.1; + R2SharedShape *shape = r2CapsuleSharedShape(r2Vector(-0.5, 0), r2Vector(0.5, 0), 0.125); + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + collider.friction = 0.6; + collider.density = 20; + R2RigidBodyHandle handle; + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = i ? r2Vector(0.5, 0) : r2Vector(0, 20); + joint.localFrame2.translation = r2Vector(-0.5, 0); + joint.contactsEnabled = 0; + r2InsertImpulseJoint(prev, handle, &joint); + prev = handle; + + r2FreeSharedShape(shape); + } + R2RigidBodyDesc dynamicBody = r2DynamicRigidBodyDesc(); + dynamicBody.position.translation = r2Vector(48, 20); + dynamicBody.linearDamping = 0.1; + dynamicBody.angularDamping = 0.1; + R2ColliderDesc ballCollider = r2BallColliderDesc(8); + ballCollider.density = 20; + ballCollider.friction = 0.6; + R2RigidBodyHandle handleH; + dynamicBody.canSleep = !testbed->noSleep; + handleH = r2InsertRigidBody(world, &dynamicBody); + r2InsertCollider(handleH, &ballCollider); + R2JointDesc jointValue = r2RevoluteJointDesc(); + jointValue.localFrame1.translation = r2Vector(0.5, 0); + jointValue.localFrame2.translation = r2Vector(-8, 0); + jointValue.contactsEnabled = 0; + r2InsertImpulseJoint(prev, handleH, &jointValue); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_bridge.c b/c/testbed/examples2d/s2d_bridge.c new file mode 100644 index 000000000..a040cdb57 --- /dev/null +++ b/c/testbed/examples2d/s2d_bridge.c @@ -0,0 +1,51 @@ +/* Port of examples2d/s2d_bridge.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbS2dBridge(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyHandle ground; + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + groundBody.canSleep = !testbed->noSleep; + ground = r2InsertRigidBody(world, &groundBody); + R2RigidBodyHandle prev = ground; + for (int i = 0; i < 160; i++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-80 + 0.5 + i, 20); + rigidBody.linearDamping = 0.1; + rigidBody.angularDamping = 0.1; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.125)); + collider.density = 20; + R2RigidBodyHandle handle; + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = i ? r2Vector(0.5, 0) : r2Vector(-80, 20); + joint.localFrame2.translation = r2Vector(-0.5, 0); + joint.contactsEnabled = 0; + r2InsertImpulseJoint(prev, handle, &joint); + prev = handle; + } + R2JointDesc jointValue = r2RevoluteJointDesc(); + jointValue.localFrame1.translation = r2Vector(0.5, 0); + jointValue.localFrame2.translation = r2Vector(80, 20); + jointValue.contactsEnabled = 0; + r2InsertImpulseJoint(prev, ground, &jointValue); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_card_house.c b/c/testbed/examples2d/s2d_card_house.c new file mode 100644 index 000000000..8342ce80b --- /dev/null +++ b/c/testbed/examples2d/s2d_card_house.c @@ -0,0 +1,56 @@ +/* Port of examples2d/s2d_card_house.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static void card(Testbed *testbed, R2World *world, R2Vector position, R2Real angle) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = position; + rigidBody.position = r2Pose(position, r2Rotation(angle)); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.01, 2)); + collider.friction = 0.7; + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); +} + +void tbS2dCardHouse(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2ColliderDesc floor = r2CuboidColliderDesc(r2Vector(40, 2)); + floor.friction = 0.7; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -2); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &floor); + R2Real z0 = 0; + R2Real y = 1.8; + for (int nb = 5; nb; nb--) { + R2Real z = z0; + for (int i = 0; i < nb; i++) { + if (i != nb - 1) { + card(testbed, world, r2Vector(z + 2.5, y + 1.85), R2_PI / 2); + } + card(testbed, world, r2Vector(z, y), -25 * R2_PI / 180); + z += 1.75; + card(testbed, world, r2Vector(z, y), 25 * R2_PI / 180); + z += 1.75; + } + y += 3.7; + z0 += 1.75; + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_confined.c b/c/testbed/examples2d/s2d_confined.c new file mode 100644 index 000000000..00b54453c --- /dev/null +++ b/c/testbed/examples2d/s2d_confined.c @@ -0,0 +1,49 @@ +/* Port of examples2d/s2d_confined.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbS2dConfined(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + const R2Vector ends[][2] = {{r2Vector(-10.5, 0), r2Vector(10.5, 0)}, + {r2Vector(-10.5, 0), r2Vector(-10.5, 20.5)}, + {r2Vector(10.5, 0), r2Vector(10.5, 20.5)}, + {r2Vector(-10.5, 20.5), r2Vector(10.5, 20.5)}}; + for (int i = 0; i < 4; i++) { + R2SharedShape *shape = r2CapsuleSharedShape(ends[i][0], ends[i][1], 0.5); + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + collider.friction = 0.6; + r2InsertColliderWithoutParent(world, &collider); + + r2FreeSharedShape(shape); + } + for (int col = 0; col < 25; col++) { + for (int row = 0; row < 25; row++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = + r2Vector(-8.75 + col * 18.0 / 25, 1.5 + row * 18.0 / 25); + rigidBody.gravityScale = 0; + R2ColliderDesc collider = r2BallColliderDesc(0.5); + collider.friction = 0.6; + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_far_pyramid.c b/c/testbed/examples2d/s2d_far_pyramid.c new file mode 100644 index 000000000..8a7cd9f2a --- /dev/null +++ b/c/testbed/examples2d/s2d_far_pyramid.c @@ -0,0 +1,45 @@ +/* Port of examples2d/s2d_far_pyramid.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbS2dFarPyramid(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2Vector origin = r2Vector(100000, -80000); + R2ColliderDesc floor = r2CuboidColliderDesc(r2Vector(100, 1)); + floor.friction = 0.6; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2VectorAdd(origin, r2Vector(0, -1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &floor); + int base = 10; + R2Real shift = 0.625; + for (int i = 0; i < base; i++) { + for (int j = i; j < base; j++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + collider.friction = 0.6; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = + r2VectorAdd(origin, r2Vector((i + 1) * shift + 2 * (j - i) * shift - 0.5 * base, + (2 * i + 1) * shift + 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, (float)origin.x, (float)origin.y + 2.5f, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_high_mass_ratio_1.c b/c/testbed/examples2d/s2d_high_mass_ratio_1.c new file mode 100644 index 000000000..499021aef --- /dev/null +++ b/c/testbed/examples2d/s2d_high_mass_ratio_1.c @@ -0,0 +1,51 @@ +/* Port of examples2d/s2d_high_mass_ratio_1.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbS2dHighMassRatio1(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2SharedShape *shape = r2SegmentSharedShape(r2Vector(-66, 0), r2Vector(66, 0)); + R2ColliderDesc floor = r2DefaultColliderDesc(); + floor.shape.kind = R2_SHAPE_DESC_SHARED; + floor.shape.sharedShape = shape; + floor.friction = 0.5; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &floor); + + for (int j = 0; j < 3; j++) { + for (int count = 10; count > 0; count--) { + for (int i = 0; i < count; i++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(1, 1)); + collider.density = count == 1 ? (j + 1) * 100 : 1; + collider.friction = 0.5; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = + r2Vector(2 * (i - count * 0.5) - 20 + 22 * j, + 1 + (10 - count) * 2 + (count == 1 ? 2 : 0)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + r2FreeSharedShape(shape); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_high_mass_ratio_2.c b/c/testbed/examples2d/s2d_high_mass_ratio_2.c new file mode 100644 index 000000000..9853d96a2 --- /dev/null +++ b/c/testbed/examples2d/s2d_high_mass_ratio_2.c @@ -0,0 +1,45 @@ +/* Port of examples2d/s2d_high_mass_ratio_2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbS2dHighMassRatio2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2SharedShape *shape = r2SegmentSharedShape(r2Vector(-66, 0), r2Vector(66, 0)); + R2ColliderDesc floor = r2DefaultColliderDesc(); + floor.shape.kind = R2_SHAPE_DESC_SHARED; + floor.shape.sharedShape = shape; + floor.friction = 0.6; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &floor); + + for (int i = 0; i < 3; i++) { + R2ColliderDesc collider = + r2CuboidColliderDesc(i == 2 ? r2Vector(10, 10) : r2Vector(0.5, 0.5)); + collider.friction = 0.6; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i == 2 ? 0 : i ? 9 : -9, i == 2 ? 26 : 0.5); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + r2FreeSharedShape(shape); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_high_mass_ratio_3.c b/c/testbed/examples2d/s2d_high_mass_ratio_3.c new file mode 100644 index 000000000..6e49cb524 --- /dev/null +++ b/c/testbed/examples2d/s2d_high_mass_ratio_3.c @@ -0,0 +1,39 @@ +/* Port of examples2d/s2d_high_mass_ratio_3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbS2dHighMassRatio3(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2ColliderDesc floor = r2CuboidColliderDesc(r2Vector(40, 2)); + floor.friction = 0.6; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -2); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &floor); + for (int i = 0; i < 3; i++) { + R2ColliderDesc collider = + r2CuboidColliderDesc(i == 2 ? r2Vector(10, 10) : r2Vector(0.5, 0.5)); + collider.friction = 0.6; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i == 2 ? 0 : i ? 9 : -9, i == 2 ? 26 : 0.5); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_joint_grid.c b/c/testbed/examples2d/s2d_joint_grid.c new file mode 100644 index 000000000..c3fbb8175 --- /dev/null +++ b/c/testbed/examples2d/s2d_joint_grid.c @@ -0,0 +1,51 @@ +/* Port of examples2d/s2d_joint_grid.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbS2dJointGrid(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyHandle *handles = calloc(1, 10000 * sizeof(*handles)); + if (!handles) { + abort(); + } + for (int k = 0; k < 100; k++) { + for (int i = 0; i < 100; i++) { + int fixed = k >= 47 && k <= 53 && i == 0; + R2RigidBodyHandle handle; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.bodyType = fixed ? R2_FIXED : R2_DYNAMIC; + rigidBody.position.translation = r2Vector(k, -i); + R2ColliderDesc collider = r2BallColliderDesc(0.4); + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + for (int dir = 0; dir < 2; dir++) { + if (dir ? k > 0 : i > 0) { + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = dir ? r2Vector(0.5, 0) : r2Vector(0, -0.5); + joint.localFrame2.translation = dir ? r2Vector(-0.5, 0) : r2Vector(0, 0.5); + joint.contactsEnabled = 0; + r2InsertImpulseJoint(handles[k * 100 + i - (dir ? 100 : 1)], handle, + &joint); + } + } + handles[k * 100 + i] = handle; + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 20); + free(handles); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/s2d_pyramid.c b/c/testbed/examples2d/s2d_pyramid.c new file mode 100644 index 000000000..0dfa6b820 --- /dev/null +++ b/c/testbed/examples2d/s2d_pyramid.c @@ -0,0 +1,45 @@ +/* Port of examples2d/s2d_pyramid.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbS2dPyramid(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2Vector origin = r2Vector(0, 0); + R2ColliderDesc floor = r2CuboidColliderDesc(r2Vector(100, 1)); + floor.friction = 0.6; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2VectorAdd(origin, r2Vector(0, -1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &floor); + int base = (int)tbSetting(testbed, "# of basis cubes", 100, 2, 200, 1); + R2Real shift = 0.5; + for (int i = 0; i < base; i++) { + for (int j = i; j < base; j++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + collider.friction = 0.6; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = + r2VectorAdd(origin, r2Vector((i + 1) * shift + 2 * (j - i) * shift - 0.5 * base, + (2 * i + 1) * shift + 0)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, (float)origin.x, (float)origin.y + 2.5f, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/sensor2.c b/c/testbed/examples2d/sensor2.c new file mode 100644 index 000000000..d3d5eb47b --- /dev/null +++ b/c/testbed/examples2d/sensor2.c @@ -0,0 +1,82 @@ +/* Port of examples2d/sensor2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static int same(R2RigidBodyHandle handle, R2RigidBodyHandle handleB) { + return handle.world == handleB.world && handle.index == handleB.index && + handle.generation == handleB.generation; +} + +void tbSensor2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyHandle ground = {0}; + R2RigidBodyHandle sensor = {0}; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.1); + R2ColliderDesc boxCollider = r2CuboidColliderDesc(r2Vector(200.1, 0.1)); + rigidBody.canSleep = !testbed->noSleep; + ground = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(ground, &boxCollider); + + for (int i = 0; i < 10; i++) { + R2RigidBodyHandle handle; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 0.4 - 2, 3); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.2, 0.2)); + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + + tbBodyColor(testbed, handle, 0.5, 0.5, 1, 1); + } + R2RigidBodyDesc dynamicBody = r2DynamicRigidBodyDesc(); + dynamicBody.position.translation = r2Vector(0, 10); + R2ColliderDesc sensorBodyCollider = r2CuboidColliderDesc(r2Vector(0.2, 0.2)); + dynamicBody.canSleep = !testbed->noSleep; + sensor = r2InsertRigidBody(world, &dynamicBody); + r2InsertCollider(sensor, &sensorBodyCollider); + + R2ColliderDesc collider = r2BallColliderDesc(1); + collider.density = 0; + collider.isSensor = 1; + collider.activeEvents = R2_COLLISION_EVENTS; + r2InsertCollider(sensor, &collider); + tbBodyColor(testbed, sensor, 0.5, 1, 1, 1); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 1, 100); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + R2EventCollector *eventHandler = r2NewEventCollector(); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2EventCollector_Clear(eventHandler); + r2Step(world, NULL, eventHandler); + + size_t n = r2EventCollector_CollisionEvents(eventHandler, NULL, 0); + R2CollisionEvent *events = calloc(n ? n : 1, sizeof(*events)); + if (!events) { + abort(); + } + n = r2EventCollector_CollisionEvents(eventHandler, events, n); + for (size_t i = 0; i < n; i++) { + R2ColliderHandle colliderHandles[] = {events[i].collider1, events[i].collider2}; + for (size_t j = 0; j < 2; j++) { + R2RigidBodyHandle handle = r2Collider_Parent(colliderHandles[j]); + if (!same(handle, ground) && !same(handle, sensor)) { + tbBodyColor(testbed, handle, events[i].started ? 1 : 0.5f, + events[i].started ? 1 : 0.5f, events[i].started ? 0 : 1, 1); + } + } + } + free(events); + } + } + r2FreeEventCollector(eventHandler); + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_blobs2.c b/c/testbed/examples2d/soft_blobs2.c new file mode 100644 index 000000000..55c34f32d --- /dev/null +++ b/c/testbed/examples2d/soft_blobs2.c @@ -0,0 +1,58 @@ +/* Port of examples2d/soft_blobs2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftBlobs2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(6, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 6, 6); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 6)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int j = 0; j < 15; j++) { + for (int i = 0; i < 5; i++) { + R2SoftBodyDesc softBody = r2DiskSoftBodyDesc( + r2Vector(-4 + i * 2 + j % 2 * 0.5, 3 + j * 2), 0.45 + 0.1 * ((i + j) % 3), 20); + softBody.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){20, 1}); + softBody.volumeFactor = 1.05; + softBody.selfContacts = 1; + softBody.particleMass = 0.05; + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + } + } + R2SoftBodyDesc strip = r2GridSoftBodyDesc(r2Vector(0, 16), r2Vector(3, 0.15), 40, 3); + strip.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){30, 1}); + strip.selfContacts = 1; + strip.particleMass = 0.02; + if (testbed->noSleep) { + strip.canSleep = 0; + } + r2InsertSoftBody(world, &strip); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 6, 30); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_bodies2.c b/c/testbed/examples2d/soft_bodies2.c new file mode 100644 index 000000000..5271ad398 --- /dev/null +++ b/c/testbed/examples2d/soft_bodies2.c @@ -0,0 +1,136 @@ +/* Port of examples2d/soft_bodies2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftBodies2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + /* Ground and walls. */ + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(15, 0.5)); + + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-15, 5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 5)); + + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(15, 5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 5)); + + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + + /* Pressurized blobs of various sizes. */ + for (int i = 0; i < 5; i++) { + const R2Real radius = 0.6 + 0.15 * i; + R2SoftBodyDesc blob = r2DiskSoftBodyDesc(r2Vector(-10.0 + i * 2.5, 2.0 + i), radius, 24); + blob.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){20.0, 1.0}); + blob.volumeFactor = 1.1; + blob.selfContacts = 1; + blob.particleMass = 0.05; + blob.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &blob); + } + + /* Jelly bodies: corotational, Neo-Hookean, and per-cell volume constraints. */ + { + R2SoftBodyDesc jelly = r2GridSoftBodyDesc(r2Vector(2, 1.2), r2Vector(1, 1), 6, 6); + jelly.cellModel = R2_SOFT_CELL_COROTATIONAL; + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 3.0e3; + material.poissonRatio = 0.35; + material.elasticDampingRatio = 0.5; + jelly.material = material; + jelly.particleMass = 0.2; + jelly.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &jelly); + } + { + R2SoftBodyDesc jelly = r2GridSoftBodyDesc(r2Vector(8, 1.2), r2Vector(1, 1), 6, 6); + jelly.cellModel = R2_SOFT_CELL_NEO_HOOKEAN; + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 3.0e3; + material.poissonRatio = 0.35; + material.elasticDampingRatio = 0.5; + jelly.material = material; + jelly.particleMass = 0.2; + jelly.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &jelly); + } + { + R2SoftBodyDesc jelly = r2GridSoftBodyDesc(r2Vector(5, 1.2), r2Vector(1, 1), 6, 6); + jelly.cellModel = R2_SOFT_CELL_VOLUME; + jelly.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){20.0, 1.0}); + jelly.particleMass = 0.2; + jelly.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &jelly); + } + + /* A rope hanging from a fixed anchor, holding a rigid weight. */ + const uint32_t pinnedParticle = 0; + R2SoftBodyDesc rope = r2DefaultSoftBodyDesc(); + rope.kind = R2_SOFT_DESC_ROPE; + rope.a = r2Vector(8, 9); + rope.b = r2Vector(12, 9); + rope.nx = 25; + rope.pinned = (R2IndexView){&pinnedParticle, 1}; + rope.material.edgeSoftness = rope.material.bendSoftness = rope.material.volumeSoftness = + rope.material.shapeMatchingSoftness = (R2SpringCoefficients){40.0, 1.0}; + rope.particleMass = 0.05; + rope.canSleep = !testbed->noSleep; + R2SoftBodyHandle ropeHandle = r2InsertSoftBody(world, &rope); + + R2Vector lastPos = r2SoftBody_ParticlePosition(ropeHandle, 24); + R2RigidBodyHandle weight; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2VectorAdd(lastPos, r2Vector(0, -0.4)); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.3, 0.3)); + collider.density = 2.0; + weight = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(weight, &collider); + } + + r2SoftBody_AttachParticle(ropeHandle, 24, weight); + + /* A stack of rigid boxes for the blobs to knock down. */ + for (int i = 0; i < 6; i++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-4.0, 0.3 + 0.6 * i); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.3, 0.3)); + + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + + /* Set up the viewer. */ + tbCamera2(testbed, 0.0, 4.0, 30.0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_cutting2.c b/c/testbed/examples2d/soft_cutting2.c new file mode 100644 index 000000000..fb2342fcb --- /dev/null +++ b/c/testbed/examples2d/soft_cutting2.c @@ -0,0 +1,178 @@ +/* Port of examples2d/soft_cutting2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftCutting2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(30, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Jelly block to slice by hand. */ + R2SoftBodyDesc block = r2GridSoftBodyDesc(r2Vector(-8, 2), r2Vector(2, 2), 13, 13); + block.cellModel = R2_SOFT_CELL_COROTATIONAL; + block.particleMass = .05; + block.particleRadius = (R2OptionalReal){1, .15}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 2.0e4; + material.poissonRatio = .35; + material.elasticDampingRatio = 1; + block.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.15); + surface.friction = .8; + block.collider = surface; + } + block.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &block); + + /* Pressurized ring: a cut opens it and it falls limp. */ + R2SoftBodyDesc blob = r2DiskSoftBodyDesc(r2Vector(-2, 2), 1.6, 40); + blob.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){30, 1}); + blob.particleMass = .05; + { + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = .8; + blob.collider = surface; + } + blob.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &blob); + + /* Curtain. */ + { + uint32_t pinned[279]; + size_t pinnedCount = 0; + for (uint32_t i = 0; i < 9; ++i) { + for (uint32_t j = 0; j < 31; ++j) { + if (j == 30) { + pinned[pinnedCount++] = i * 31 + j; + } + } + } + R2SoftBodyDesc curtain = r2GridSoftBodyDesc(r2Vector(3, 4.5), r2Vector(0.6, 3), 9, 31); + curtain.cellModel = R2_SOFT_CELL_COROTATIONAL; + r2SoftBodyDesc_SetPinnedParticles(&curtain, + (R2IndexView){(const uint32_t *)pinned, pinnedCount}); + curtain.particleMass = .05; + curtain.particleRadius = (R2OptionalReal){1, .1}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 3.0e4; + material.poissonRatio = .35; + material.elasticDampingRatio = 1; + curtain.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = .8; + curtain.collider = surface; + } + curtain.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &curtain); + } + /* Slab. */ + { + uint32_t pinned[125]; + size_t pinnedCount = 0; + for (uint32_t i = 0; i < 25; ++i) { + for (uint32_t j = 0; j < 5; ++j) { + if (i == 0 || i == 24) { + pinned[pinnedCount++] = i * 5 + j; + } + } + } + R2SoftBodyDesc slab = r2GridSoftBodyDesc(r2Vector(10, 4), r2Vector(3, 0.5), 25, 5); + slab.cellModel = R2_SOFT_CELL_COROTATIONAL; + r2SoftBodyDesc_SetPinnedParticles(&slab, + (R2IndexView){(const uint32_t *)pinned, pinnedCount}); + slab.particleMass = .05; + slab.particleRadius = (R2OptionalReal){1, .1}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 5.0e4; + material.poissonRatio = .35; + material.elasticDampingRatio = 1; + slab.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = .8; + slab.collider = surface; + } + slab.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &slab); + } + /* Sensor blade: the cut does the work, without pushing the slab. */ + const R2Vector sawStart = r2Vector(10, 1.5); + R2RigidBodyHandle saw; + { + R2RigidBodyDesc rigidBody = r2KinematicPositionBasedRigidBodyDesc(); + rigidBody.position.translation = sawStart; + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.05, 1)); + collider.isSensor = 1; + saw = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(saw, &collider); + } + + size_t handleCount = r2SoftBodyCount(world); + R2SoftBodyHandle *handles = malloc(handleCount * sizeof(*handles)); + if (!handles) { + abort(); + } + handleCount = r2SoftBodyHandles(world, handles, handleCount); + tbCamera2(testbed, 1, 3.5, 40); + + tbSetWorld(testbed, world); + R2Real t = 0; + R2Vector bladeStart = {0}; + int bladeActive = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + /* Hold C to position a hand blade, then release C to cut, even while paused. */ + if (testbed->cutting) { + if (!bladeActive && testbed->cursorValid) { + bladeStart = testbed->cursor; + bladeActive = 1; + } + if (bladeActive && testbed->cursorValid) { + tbLine(testbed, bladeStart, testbed->cursor, 1, .35f, .25f, 1); + } + } else if (bladeActive) { + bladeActive = 0; + if (testbed->cursorValid) { + const R2Vector edge[] = {bladeStart, testbed->cursor}; + for (size_t i = 0; i < handleCount; ++i) { + R2SoftBodyTearEvent *event = r2CutSoftBody(handles[i], edge); + r2FreeSoftBodyTearEvent(event); + } + } + } + + if (tbSimulating(testbed)) { + R2Real dt = r2TimeStep(world); + t += dt; + const R2Real rise = fmin(fmax(t - 1, 0) * .2, 4.5); + const R2Vector position = r2VectorAdd(sawStart, r2Vector(0, rise)); + + r2RigidBody_SetNextKinematicTranslation(saw, position); + const R2Vector edge[] = {r2VectorSub(position, r2Vector(0, 1)), + r2VectorAdd(position, r2Vector(0, 1))}; + for (size_t i = 0; i < handleCount; ++i) { + R2SoftBodyTearEvent *event = r2CutSoftBody(handles[i], edge); + r2FreeSoftBodyTearEvent(event); + } + r2Step(world, NULL, NULL); + } + } + free(handles); + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_fem2.c b/c/testbed/examples2d/soft_fem2.c new file mode 100644 index 000000000..d39252b0e --- /dev/null +++ b/c/testbed/examples2d/soft_fem2.c @@ -0,0 +1,129 @@ +/* Port of examples2d/soft_fem2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#ifdef RAPIER_FEM + +void tbSoftFem2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(30, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Compare FEM (1) with the constraint solver (0). */ + const uint32_t solvers[] = {R2_SOFT_SOLVER_FEM, R2_SOFT_SOLVER_CONSTRAINTS}; + for (size_t row = 0; row < TB_COUNT(solvers); ++row) { + const R2Real y = 2.0 + row * 6.0; + /* Cantilever bolted to a wall. */ + R2SoftBodyDesc beam = r2GridSoftBodyDesc(r2Vector(-6, y), r2Vector(2, 0.2), 17, 3); + beam.cellModel = R2_SOFT_CELL_COROTATIONAL; + beam.totalMass = (R2OptionalReal){1, 8.0}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 2.0e6; + material.poissonRatio = .3; + material.elasticDampingRatio = 1; + beam.material = material; + } + beam.canSleep = !testbed->noSleep; + uint32_t *beamPins = NULL; + { + size_t count = r2SoftBodyDesc_ParticlePositions(&beam, NULL, 0); + R2Vector *positions = malloc(count * sizeof(*positions)); + beamPins = malloc(count * sizeof(*beamPins)); + if (!positions || !beamPins) { + abort(); + } + count = r2SoftBodyDesc_ParticlePositions(&beam, positions, count); + size_t beamPinsCount = 0; + for (size_t i = 0; i < count; ++i) { + if (positions[i].x < -8.0 + 1.0e-4) { + beamPins[beamPinsCount++] = (uint32_t)i; + } + } + r2SoftBodyDesc_SetPinnedParticles( + &beam, (R2IndexView){(const uint32_t *)beamPins, beamPinsCount}); + + free(positions); + } + beam.solver = solvers[row]; + r2InsertSoftBody(world, &beam); + free(beamPins); + + /* Plank pinned at both ends. */ + R2SoftBodyDesc plank = r2GridSoftBodyDesc(r2Vector(2, y), r2Vector(3, 0.25), 21, 3); + plank.cellModel = R2_SOFT_CELL_COROTATIONAL; + plank.totalMass = (R2OptionalReal){1, 20.0}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 4.0e6; + material.poissonRatio = .3; + material.elasticDampingRatio = 1; + plank.material = material; + } + plank.canSleep = !testbed->noSleep; + uint32_t *plankPins = NULL; + { + size_t count = r2SoftBodyDesc_ParticlePositions(&plank, NULL, 0); + R2Vector *positions = malloc(count * sizeof(*positions)); + plankPins = malloc(count * sizeof(*plankPins)); + if (!positions || !plankPins) { + abort(); + } + count = r2SoftBodyDesc_ParticlePositions(&plank, positions, count); + size_t plankPinsCount = 0; + for (size_t i = 0; i < count; ++i) { + if (fabs(positions[i].x - 2.0) > 2.9) { + plankPins[plankPinsCount++] = (uint32_t)i; + } + } + r2SoftBodyDesc_SetPinnedParticles( + &plank, (R2IndexView){(const uint32_t *)plankPins, plankPinsCount}); + + free(positions); + } + plank.solver = solvers[row]; + r2InsertSoftBody(world, &plank); + free(plankPins); + + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(2, y + 2.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + collider.density = 20; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Neo-Hookean jelly. */ + R2SoftBodyDesc jelly = r2GridSoftBodyDesc(r2Vector(9, y + 1), r2Vector(0.8, 0.8), 5, 5); + jelly.cellModel = R2_SOFT_CELL_NEO_HOOKEAN; + jelly.particleMass = .1; + jelly.solver = solvers[row]; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 2.0e4; + material.poissonRatio = .4; + material.elasticDampingRatio = .5; + jelly.material = material; + } + jelly.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &jelly); + } + tbCamera2(testbed, 1, 5, 25); + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} +#endif diff --git a/c/testbed/examples2d/soft_force_tearing2.c b/c/testbed/examples2d/soft_force_tearing2.c new file mode 100644 index 000000000..573566b14 --- /dev/null +++ b/c/testbed/examples2d/soft_force_tearing2.c @@ -0,0 +1,228 @@ +/* Port of examples2d/soft_force_tearing2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +typedef struct DrivenParticle { + R2SoftBodyHandle body; + uint32_t index; + R2Vector rest; + int right; +} DrivenParticle; + +/* Follow driven particle indices through compaction and newly split bodies. */ +static void followTears(R2EventCollector *events, DrivenParticle *ends, size_t endCount) { + size_t count = r2EventCollector_TearEventCount(events); + for (size_t i = 0; i < count; ++i) { + R2SoftBodyTearEvent *event = r2EventCollector_TearEvent(events, i); + R2SoftBodyHandle origin = r2SoftBodyTearEvent_SoftBody(event); + for (size_t j = 0; j < endCount; ++j) { + if (ends[j].body.index == origin.index && + ends[j].body.generation == origin.generation) { + R2Bool found; + R2SoftBodyHandle destination; + uint32_t index; + R2OptionalParticleDestination softBodyTearEventTryParticleDestinationResult = + r2SoftBodyTearEvent_TryParticleDestination(event, ends[j].index); + destination = softBodyTearEventTryParticleDestinationResult.body; + index = softBodyTearEventTryParticleDestinationResult.index; + found = softBodyTearEventTryParticleDestinationResult.found; + if (found) { + ends[j].body = destination; + ends[j].index = index; + } + } + } + r2FreeSoftBodyTearEvent(event); + } +} + +static R2SoftBodyDesc hangingBar(R2Real x, R2SoftBodyMaterial *material) { + R2SoftBodyDesc bar = r2GridSoftBodyDesc(r2Vector(x, 7.5), r2Vector(.3, 1.5), 3, 13); + static const uint32_t pinned[] = {12, 25, 38}; + r2SoftBodyDesc_SetPinnedParticles(&bar, + (R2IndexView){(const uint32_t *)pinned, TB_COUNT(pinned)}); + bar.material = *material; + bar.particleMass = .05; + bar.particleRadius = (R2OptionalReal){1, .1}; + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = .8; + bar.collider = surface; + + return bar; +} + +static void stiff(R2SoftBodyMaterial *material) { + material->edgeSoftness = (R2SpringCoefficients){150, 1}; + material->volumeSoftness = (R2SpringCoefficients){150, 1}; +} + +void tbSoftForceTearing2(Testbed *testbed) { + R2World *world = r2NewWorld(); + const R2Real tearForce = tbSetting(testbed, "Tear force (crate bar)", 8, 2, 30, 0); + const R2Real interiorStrength = tbSetting(testbed, "Interior strength (slab)", 4, 1, 8, 0); + const R2Real smoothing = tbSetting(testbed, "Tear smoothing (right bar, s)", 1, 0, 3, 0); + + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(30, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 9.3); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(30, 0.3)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int column = 0; column < 2; ++column) { + const R2Real x = column == 0 ? -13 : -10; + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + stiff(&material); + if (column == 0) { + material.tearStrain = (R2OptionalReal){1, .5}; + } else { + material.tearForce = (R2OptionalReal){1, tearForce}; + material.tearSmoothing = .5; + } + R2SoftBodyDesc builder = hangingBar(x, &material); + builder.canSleep = !testbed->noSleep; + R2SoftBodyHandle bar = r2InsertSoftBody(world, &builder); + + R2RigidBodyHandle crateBody; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, 5.4); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.6, 0.5)); + collider.density = 1.2; + crateBody = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(crateBody, &collider); + } + + for (size_t i = 0; i < 3; ++i) { + r2SoftBody_AttachParticle(bar, i * 13, crateBody); + } + } + const size_t sx = 25, sy = 7; + uint32_t pinned[28]; + for (size_t j = 0; j < sy; ++j) { + pinned[4 * j] = j; + pinned[4 * j + 1] = sy + j; + pinned[4 * j + 2] = (sx - 2) * sy + j; + pinned[4 * j + 3] = (sx - 1) * sy + j; + } + R2SoftBodyDesc builder = r2GridSoftBodyDesc(r2Vector(-1, 2), r2Vector(3, 0.75), sx, sy); + r2SoftBodyDesc_SetPinnedParticles(&builder, + (R2IndexView){(const uint32_t *)pinned, TB_COUNT(pinned)}); + builder.particleMass = .05; + builder.particleRadius = (R2OptionalReal){1, .1}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.edgeSoftness = (R2SpringCoefficients){60, 1}; + material.volumeSoftness = (R2SpringCoefficients){60, 1}; + material.tearForce = (R2OptionalReal){1, 60}; + material.tearSmoothing = .05; + material.interiorStrength = interiorStrength; + builder.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = .8; + builder.collider = surface; + } + builder.canSleep = !testbed->noSleep; + R2SoftBodyHandle slab = r2InsertSoftBody(world, &builder); + + size_t indexCount = r2SoftBody_Edges(slab, NULL, 0); + uint32_t *edges = malloc(indexCount * sizeof(*edges)); + if (!edges) { + abort(); + } + indexCount = r2SoftBody_Edges(slab, edges, indexCount); + for (size_t i = 0; i < indexCount / 2; ++i) { + const uint32_t a = edges[2 * i], b = edges[2 * i + 1]; + if (a % sy == sy - 1 && b % sy == sy - 1 && a / sy >= 11 && a / sy <= 13 && b / sy >= 11 && + b / sy <= 13) { + r2SoftBody_SetEdgeTearResistance(slab, i, .4); + } + } + free(edges); + DrivenParticle rightEnd[14]; + for (size_t j = 0; j < sy; ++j) { + for (size_t side = 0; side < 2; ++side) { + const uint32_t index = (sx - 2 + side) * sy + j; + R2Vector rest = r2SoftBody_ParticlePosition(slab, index); + rightEnd[j * 2 + side] = (DrivenParticle){slab, index, rest, 1}; + } + } + R2RigidBodyHandle disks[2]; + for (int column = 0; column < 2; ++column) { + const R2Real x = column == 0 ? 8 : 13; + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.tearForce = (R2OptionalReal){1, 25}; + material.tearSmoothing = column == 0 ? 0 : smoothing; + stiff(&material); + R2SoftBodyDesc builder = hangingBar(x, &material); + builder.canSleep = !testbed->noSleep; + R2SoftBodyHandle bar = r2InsertSoftBody(world, &builder); + + R2RigidBodyHandle crateBody; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, 5.4); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(1.2, 0.4)); + collider.density = .75; + crateBody = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(crateBody, &collider); + } + + for (size_t i = 0; i < 3; ++i) { + r2SoftBody_AttachParticle(bar, i * 13, crateBody); + } + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x + .8, 8.6); + rigidBody.enabled = 0; + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2BallColliderDesc(.2); + collider.density = 3; + disks[column] = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(disks[column], &collider); + } + } + R2EventCollector *events = r2NewEventCollector(); + tbCamera2(testbed, 0, 4.5, 40); + testbed->snapshotSupported = 0; + testbed->initialDebug = R2_DEBUG_SOFT_BODIES | R2_DEBUG_SOFT_BODY_STRESS; + tbSetWorld(testbed, world); + R2Real t = 0; + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R2Real dt = r2TimeStep(world); + if (t < 1 && t + dt >= 1) { + for (size_t i = 0; i < TB_COUNT(disks); ++i) { + r2RigidBody_SetEnabled(disks[i], 1); + r2RigidBody_SetLinvel(disks[i], r2Vector(0, -15), 1); + } + } + t += dt; + const R2Real shift = fmin(fmax(t - 2, 0) * .25, 3); + for (size_t i = 0; i < TB_COUNT(rightEnd); ++i) { + r2SoftBody_SetParticleKinematicTarget( + rightEnd[i].body, rightEnd[i].index, + r2VectorAdd(rightEnd[i].rest, r2Vector(shift, 0))); + } + r2EventCollector_Clear(events); + r2Step(world, NULL, events); + followTears(events, rightEnd, TB_COUNT(rightEnd)); + } + } + r2FreeEventCollector(events); + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_jelly2.c b/c/testbed/examples2d/soft_jelly2.c new file mode 100644 index 000000000..03f69af5e --- /dev/null +++ b/c/testbed/examples2d/soft_jelly2.c @@ -0,0 +1,92 @@ +/* Port of examples2d/soft_jelly2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftJelly2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(20, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + for (int level = 0; level < 4; level++) { + for (int i = 0; i < 4 - level; i++) { + R2SoftBodyDesc softBody = r2GridSoftBodyDesc( + r2Vector(-8 + (i - (4 - level) * 0.5 + 0.5) * 1.6, 0.75 + level * 1.5), + r2Vector(0.75, 0.75), 5, 5); + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + softBody.cellModel = R2_SOFT_CELL_COROTATIONAL; + material.youngModulus = 2.0e4 / (1 + level * 1.5); + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.particleMass = 0.1; + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + } + } + R2SoftBodyDesc bridge = r2GridSoftBodyDesc(r2Vector(3, 3), r2Vector(4, 0.2), 41, 3); + bridge.cellModel = R2_SOFT_CELL_VOLUME; + bridge.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){30, 1}); + uint32_t pins[] = {0, 1, 2, 120, 121, 122}; + r2SoftBodyDesc_SetPinnedParticles(&bridge, (R2IndexView){(const uint32_t *)pins, 6}); + bridge.particleMass = 0.05; + if (testbed->noSleep) { + bridge.canSleep = 0; + } + r2InsertSoftBody(world, &bridge); + for (int i = 0; i < 6; i++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0.5 + i, 5 + i); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.25, 0.25)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + R2SoftBodyDesc driven = r2DiskSoftBodyDesc(r2Vector(8, 4), 0.8, 24); + driven.shapeMatching = (R2OptionalBool){1, 1}; + driven.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){15, 1}); + driven.volumePreservation = 0; + driven.gravityScale = 0; + driven.particleMass = 0.2; + driven.canSleep = 0; + R2SoftBodyHandle softBodyHandle = {0}; + if (testbed->noSleep) { + driven.canSleep = 0; + } + softBodyHandle = r2InsertSoftBody(world, &driven); + for (int i = 0; i < 8; i++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(6.5 + 0.5 * i, 0.25); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.2, 0.2)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 3, 30); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R2Real dt = r2TimeStep(world); + R2Real time = (R2Real)testbed->time + dt; + R2Pose target = r2Pose(r2Vector(8 + 2.5 * cos(time), 1 + 1.5 * fabs(sin(2 * time))), + r2Rotation(time)); + + r2SoftBody_SetClusterShapeMatchingTarget(softBodyHandle, 0, &target); + + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_joints2.c b/c/testbed/examples2d/soft_joints2.c new file mode 100644 index 000000000..25261dfec --- /dev/null +++ b/c/testbed/examples2d/soft_joints2.c @@ -0,0 +1,365 @@ +/* Port of examples2d/soft_joints2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static R2SoftBodyDesc jelly(R2Vector center, R2Vector half, R2Real young) { + R2SoftBodyDesc builder = r2GridSoftBodyDesc(center, half, 5, 5); + builder.cellModel = R2_SOFT_CELL_COROTATIONAL; + builder.particleMass = .08; + builder.particleRadius = (R2OptionalReal){1, .06}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = young; + material.poissonRatio = .35; + material.elasticDampingRatio = .8; + builder.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.06); + surface.friction = .6; + builder.collider = surface; + } + return builder; +} + +void tbSoftJoints2(Testbed *testbed) { + R2World *world = r2NewWorld(); + + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(30, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Revolute joint and velocity motor. */ + R2SoftBodyHandle spinner; + { + R2SoftBodyDesc builder = jelly(r2Vector(-12, 2), r2Vector(0.6, 0.6), 8e3); + builder.canSleep = !testbed->noSleep; + spinner = r2InsertSoftBody(world, &builder); + } + R2RigidBodyHandle spinnerRoot = r2SoftBody_RootBody(spinner); + R2Vector com = r2SoftBody_CenterOfMass(spinner); + R2RigidBodyHandle pivot; + { + R2RigidBodyDesc builder = r2FixedRigidBodyDesc(); + builder.position.translation = com; + pivot = r2InsertRigidBody(world, &builder); + } + { + R2JointDesc joint = r2RevoluteJointDesc(); + r2JointDesc_SetMotorVelocity(&joint, R2_AXIS_ANG_X, 1.5, 60); + r2InsertImpulseJoint(pivot, spinnerRoot, &joint); + } + /* Weld a rigid plate to the jelly's top cluster. */ + R2SoftBodyHandle wobbler; + { + R2SoftBodyDesc builder = jelly(r2Vector(-8, 0.61), r2Vector(0.6, 0.6), 2.5e3); + builder.canSleep = !testbed->noSleep; + wobbler = r2InsertSoftBody(world, &builder); + } + + R2Vector positions[64]; + size_t particleCount = + r2SoftBody_ParticlePositions(wobbler, positions, TB_COUNT(positions)); + R2Real maxY = 0; + for (size_t i = 0; i < particleCount; ++i) { + maxY = fmax(maxY, positions[i].y); + } + uint32_t top[5]; + size_t topCount = 0; + for (size_t i = 0; i < particleCount; ++i) { + if (fabs(positions[i].y - maxY) < 1e-3) { + top[topCount++] = (uint32_t)i; + } + } + uint32_t topCluster = r2SoftBody_AddCluster(wobbler, top, topCount); + R2RigidBodyHandle topProxy = r2SoftBody_ClusterProxy(wobbler, topCluster); + R2Vector topPos; + { topPos = r2RigidBody_Translation(topProxy); } + R2RigidBodyHandle plate; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2VectorAdd(topPos, r2Vector(0, 0.12)); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.7, 0.06)); + collider.density = .4; + plate = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(plate, &collider); + } + { + R2JointDesc joint = r2FixedJointDesc(); + joint.localFrame1.translation = r2Vector(0, -0.12); + r2InsertImpulseJoint(plate, topProxy, &joint); + } + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2VectorAdd(topPos, r2Vector(0.3, 1.4)); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.15, 0.15)); + collider.density = 1.5; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Prismatic motor with two stops. */ + R2SoftBodyHandle shuttle; + { + R2SoftBodyDesc builder = jelly(r2Vector(-3, 0.85), r2Vector(0.4, 0.4), 6e3); + builder.canSleep = !testbed->noSleep; + shuttle = r2InsertSoftBody(world, &builder); + } + R2RigidBodyHandle shuttleRoot = r2SoftBody_RootBody(shuttle); + R2Vector shuttleCom = r2SoftBody_CenterOfMass(shuttle); + R2RigidBodyHandle rail; + { + R2RigidBodyDesc builder = r2FixedRigidBodyDesc(); + builder.position.translation = shuttleCom; + rail = r2InsertRigidBody(world, &builder); + } + R2ImpulseJointHandle railJoint; + { + R2JointDesc joint = r2PrismaticJointDesc(r2Vector(1, 0)); + r2JointDesc_SetLimits(&joint, R2_AXIS_LIN_X, -1.8, 1.8); + r2JointDesc_SetMotorPosition(&joint, R2_AXIS_LIN_X, 0, 40, 8); + railJoint = r2InsertImpulseJoint(rail, shuttleRoot, &joint); + } + /* Rope over a ledge. */ + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(1.5, 1); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(1.2, 1)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + R2SoftBodyHandle anchorJelly; + { + R2SoftBodyDesc builder = jelly(r2Vector(1.5, 2.6), r2Vector(0.5, 0.5), 1.2e4); + builder.canSleep = !testbed->noSleep; + anchorJelly = r2InsertSoftBody(world, &builder); + } + R2SoftBodyHandle hangingJelly; + { + R2SoftBodyDesc builder = jelly(r2Vector(3.8, 2.6), r2Vector(0.5, 0.5), 1.2e4); + builder.canSleep = !testbed->noSleep; + hangingJelly = r2InsertSoftBody(world, &builder); + } + R2RigidBodyHandle anchorRoot = r2SoftBody_RootBody(anchorJelly); + R2RigidBodyHandle hangingRoot = r2SoftBody_RootBody(hangingJelly); + { + R2JointDesc joint = r2RopeJointDesc(2.2); + r2InsertImpulseJoint(anchorRoot, hangingRoot, &joint); + } + /* Spring bungee under a gantry. */ + R2SoftBodyHandle bungee; + { + R2SoftBodyDesc builder = jelly(r2Vector(6.5, 3.2), r2Vector(0.45, 0.45), 6e3); + builder.canSleep = !testbed->noSleep; + bungee = r2InsertSoftBody(world, &builder); + } + R2RigidBodyHandle bungeeRoot = r2SoftBody_RootBody(bungee); + R2RigidBodyHandle gantry; + { + R2RigidBodyDesc builder = r2FixedRigidBodyDesc(); + builder.position.translation = r2Vector(6.5, 5.5); + gantry = r2InsertRigidBody(world, &builder); + } + { + R2JointDesc joint = r2SpringJointDesc(1.2, 25, 1.5); + r2InsertImpulseJoint(gantry, bungeeRoot, &joint); + } + /* Bead on a visual-only pole. */ + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(9.5, 2.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.05, 2.5)); + collider.collisionGroups = (R2InteractionGroups){0, 0, 0}; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + R2SoftBodyHandle bead; + { + R2SoftBodyDesc builder = jelly(r2Vector(9.5, 4.2), r2Vector(0.35, 0.35), 8e3); + builder.canSleep = !testbed->noSleep; + bead = r2InsertSoftBody(world, &builder); + } + R2RigidBodyHandle beadRoot = r2SoftBody_RootBody(bead); + R2RigidBodyHandle pole; + { + R2RigidBodyDesc builder = r2FixedRigidBodyDesc(); + builder.position.translation = r2Vector(9.5, 2.5); + pole = r2InsertRigidBody(world, &builder); + } + { + R2JointDesc joint = r2PinSlotJointDesc(r2Vector(0, 1)); + r2JointDesc_SetLimits(&joint, R2_AXIS_LIN_X, -1.8, 1.8); + r2InsertImpulseJoint(pole, beadRoot, &joint); + } + /* Hinge two disjoint clusters of one soft bar. */ + R2SoftBodyHandle bar; + R2SoftBodyDesc barBuilder = r2GridSoftBodyDesc(r2Vector(13.5, 3), r2Vector(1, 0.22), 9, 3); + barBuilder.cellModel = R2_SOFT_CELL_COROTATIONAL; + barBuilder.particleMass = .08; + barBuilder.particleRadius = (R2OptionalReal){1, .06}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 2e4; + material.poissonRatio = .35; + material.elasticDampingRatio = 1; + barBuilder.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.06); + surface.friction = .5; + barBuilder.collider = surface; + } + barBuilder.canSleep = !testbed->noSleep; + bar = r2InsertSoftBody(world, &barBuilder); + + R2Vector barCom = r2SoftBody_CenterOfMass(bar); + uint32_t leftHalf[64], rightHalf[64]; + size_t leftCount = 0, rightCount = 0; + particleCount = r2SoftBody_ParticlePositions(bar, positions, TB_COUNT(positions)); + for (size_t i = 0; i < particleCount; ++i) { + if (positions[i].x < barCom.x - 1e-3) { + leftHalf[leftCount++] = i; + } else if (positions[i].x > barCom.x + 1e-3) { + rightHalf[rightCount++] = i; + } + } + uint32_t leftCluster = r2SoftBody_AddCluster(bar, leftHalf, leftCount); + uint32_t rightCluster = r2SoftBody_AddCluster(bar, rightHalf, rightCount); + R2RigidBodyHandle leftProxy = r2SoftBody_ClusterProxy(bar, leftCluster); + R2RigidBodyHandle rightProxy = r2SoftBody_ClusterProxy(bar, rightCluster); + R2Vector leftPos; + { leftPos = r2RigidBody_Translation(leftProxy); } + R2Vector rightPos; + { rightPos = r2RigidBody_Translation(rightProxy); } + R2RigidBodyHandle barAnchor; + { + R2RigidBodyDesc builder = r2FixedRigidBodyDesc(); + builder.position.translation = leftPos; + barAnchor = r2InsertRigidBody(world, &builder); + } + { + R2JointDesc joint = r2FixedJointDesc(); + r2InsertImpulseJoint(barAnchor, leftProxy, &joint); + } + R2ImpulseJointHandle flapJoint; + { + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2VectorSub(barCom, leftPos); + joint.localFrame2.translation = r2VectorSub(barCom, rightPos); + r2JointDesc_SetMotorPosition(&joint, R2_AXIS_ANG_X, 0, 80, 10); + flapJoint = r2InsertImpulseJoint(leftProxy, rightProxy, &joint); + } + /* Multibody arm and rope attached to one jelly. */ + R2RigidBodyHandle armRoot; + { + R2RigidBodyDesc builder = r2FixedRigidBodyDesc(); + builder.position.translation = r2Vector(17, 6); + armRoot = r2InsertRigidBody(world, &builder); + } + R2RigidBodyHandle link1; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(18.2, 6); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CapsuleXColliderDesc(.5, .08); + collider.density = 2; + link1 = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(link1, &collider); + } + { + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2Vector(0, 0); + joint.localFrame2.translation = r2Vector(-1.2, 0); + r2InsertMultibodyJoint(armRoot, link1, &joint); + } + R2RigidBodyHandle link2; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(19.4, 6); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CapsuleXColliderDesc(.5, .08); + collider.density = 2; + link2 = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(link2, &collider); + } + { + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2Vector(0.6, 0); + joint.localFrame2.translation = r2Vector(-0.6, 0); + r2InsertMultibodyJoint(link1, link2, &joint); + } + R2SoftBodyHandle pendulum; + { + R2SoftBodyDesc builder = jelly(r2Vector(20.2, 5.2), r2Vector(0.5, 0.5), 5e3); + builder.canSleep = !testbed->noSleep; + pendulum = r2InsertSoftBody(world, &builder); + } + R2RigidBodyHandle pendulumRoot = r2SoftBody_RootBody(pendulum); + { + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2Vector(0.7, 0); + joint.localFrame2.translation = r2Vector(0, 0.6); + r2InsertImpulseJoint(link2, pendulumRoot, &joint); + } + R2RigidBodyHandle crateBody; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(20.2, 0.3); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.3, 0.3)); + collider.density = .5; + crateBody = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(crateBody, &collider); + } + { + R2JointDesc joint = r2RopeJointDesc(4.2); + r2InsertImpulseJoint(pendulumRoot, crateBody, &joint); + } + /* Wave a pinned cluster without a joint. */ + R2SoftBodyHandle strand; + R2SoftBodyDesc gripBuilder = r2RopeSoftBodyDesc(r2Vector(-16, 5.5), r2Vector(-16, 1.5), 20); + gripBuilder.particleMass = .05; + gripBuilder.canSleep = !testbed->noSleep; + strand = r2InsertSoftBody(world, &gripBuilder); + + uint32_t gripParticles[2]; + for (uint32_t i = 0; i < 2; ++i) { + gripParticles[i] = i; + } + uint32_t grip = r2SoftBody_AddCluster(strand, gripParticles, 2); + R2RigidBodyHandle gripProxy = r2SoftBody_ClusterProxy(strand, grip); + r2SoftBody_SetClusterPinned(strand, grip, 1); + R2Vector gripHome; + { gripHome = r2RigidBody_Translation(gripProxy); } + tbCamera2(testbed, 2, 3, 30); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + R2Real t = 0; + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R2Real dt = r2TimeStep(world); + t += dt; + + r2ImpulseJoint_SetMotorPosition(railJoint, R2_AXIS_LIN_X, 1.5 * sin(.6 * t), 40, + 8, 1); + + r2ImpulseJoint_SetMotorPosition(flapJoint, R2_AXIS_ANG_X, .8 * sin(1.4 * t), 80, + 10, 1); + + r2SoftBody_SetClusterKinematicTarget( + strand, grip, + r2Pose(r2VectorAdd(gripHome, r2Vector(1.2 * sin(.7 * t), .15 * sin(1.9 * t))), + r2Rotation(.5 * sin(1.1 * t)))); + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_letters2.c b/c/testbed/examples2d/soft_letters2.c new file mode 100644 index 000000000..bbd876e79 --- /dev/null +++ b/c/testbed/examples2d/soft_letters2.c @@ -0,0 +1,112 @@ +/* Port of examples2d/soft_letters2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include "utils/logo_mesh.h" + +void tbSoftLetters2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(25, 1.2)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(25, 25); + rigidBody.position.rotation = r2Rotation(R2_PI / 2); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(25, 1.2)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-25, 25); + rigidBody.position.rotation = r2Rotation(R2_PI / 2); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(25, 1.2)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + const R2Real cellSize = 2.4; + + struct Letter { + R2Vector *positions; + uint32_t *cells; + size_t particleCount, indexCount; + } letters[TB_COUNT(logoMeshes)]; + + size_t letterCount = 0; + for (size_t i = 0; i < TB_COUNT(logoMeshes); ++i) { + const LogoMesh *mesh = &logoMeshes[i]; + + R2VolumeMeshParameters letterMeshing = r2NewVolumeMeshParameters(cellSize); + R2SoftBodyDesc letter = r2VolumetricSoftBodyDesc( + (R2VectorView){mesh->vertices, mesh->vertexCount}, + (R2SurfaceElementView){(const R2Edge *)mesh->outline, mesh->edgeCount}, letterMeshing); + if (letter.positions.count == 0) { + continue; + } + struct Letter *data = &letters[letterCount++]; + data->particleCount = r2SoftBodyDesc_ParticlePositions(&letter, NULL, 0); + data->indexCount = r2SoftBodyDesc_CellIndices(&letter, NULL, 0); + data->positions = malloc(data->particleCount * sizeof(*data->positions)); + data->cells = malloc(data->indexCount * sizeof(*data->cells)); + if (!data->positions || !data->cells) { + abort(); + } + data->particleCount = + r2SoftBodyDesc_ParticlePositions(&letter, data->positions, data->particleCount); + data->indexCount = r2SoftBodyDesc_CellIndices(&letter, data->cells, data->indexCount); + } + const R2Real stiffnesses[] = {1e3, 5e3, 1e4, 5e4, 1e5, 5e5, 1e6, 5e6}; + for (size_t row = 0; row < TB_COUNT(stiffnesses); ++row) { + const R2Real young = stiffnesses[TB_COUNT(stiffnesses) - 1 - row]; + for (size_t ith = 0; ith < letterCount; ++ith) { + const struct Letter *data = &letters[ith]; + const R2Vector offset = r2Vector(ith * 8.0 - 22, 12 + row * 11); + R2Vector *positions = malloc(data->particleCount * sizeof(*positions)); + if (!positions) { + abort(); + } + for (size_t i = 0; i < data->particleCount; ++i) { + positions[i] = r2VectorAdd(data->positions[i], offset); + } + R2SoftBodyDesc letter = r2DefaultSoftBodyDesc(); + r2SoftBodyDesc_SetParticles(&letter, (R2VectorView){positions, data->particleCount}); + r2SoftBodyDesc_SetCells( + &letter, (R2CellView){(const R2Triangle *)data->cells, data->indexCount / 3}); + letter.cellModel = R2_SOFT_CELL_COROTATIONAL; + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = young; + material.poissonRatio = .4; + material.elasticDampingRatio = .5; + material.deformationDamping = fmin(young / 4e4, 50); + letter.material = material; + letter.particleMass = .1; + letter.particleRadius = (R2OptionalReal){1, .15}; + letter.selfContacts = 1; + letter.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &letter); + free(positions); + } + } + for (size_t i = 0; i < letterCount; ++i) { + free(letters[i].positions); + free(letters[i].cells); + } + tbCamera2(testbed, 0, 20, 17); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_pile2.c b/c/testbed/examples2d/soft_pile2.c new file mode 100644 index 000000000..fc1515c3d --- /dev/null +++ b/c/testbed/examples2d/soft_pile2.c @@ -0,0 +1,122 @@ +/* Port of examples2d/soft_pile2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftPile2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + R2ColliderDesc boxCollider = r2CuboidColliderDesc(r2Vector(9, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &boxCollider); + + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 9, 12); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 12)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int i = 0; i < 5; i++) { + R2SharedShape *shape = r2CapsuleSharedShape(r2Vector(-0.6, 0), r2Vector(0.6, 0), 0.1); + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-6 + i * 3, 6); + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + r2FreeSharedShape(shape); + } + int k = 0; + for (int layer = 0; layer < 40; layer++) { + for (int i = 0; i < 6; i++, k++) { + R2Vector position = r2Vector(-7 + i * 2.8 + layer % 2, 9 + layer * 2.6); + R2SoftBodyDesc softBody; + if (k % 5 == 0 || k % 5 == 3) { + softBody = r2GridSoftBodyDesc(position, r2Vector(0.6, 0.6), 4, 4); + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + softBody.cellModel = R2_SOFT_CELL_COROTATIONAL; + material.youngModulus = 3.0e3 * (1 + k % 7 * 4); + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.particleMass = 0.1; + softBody.particleRadius = (R2OptionalReal){1, 0.08}; + R2ColliderDesc surfaceCollider = r2BallColliderDesc(0.08); + surfaceCollider.friction = 0.7; + softBody.collider = surfaceCollider; + } else if (k % 5 == 1) { + softBody = r2DiskSoftBodyDesc(position, 0.6, 20); + softBody.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){20, 1}); + softBody.volumeFactor = 1.1; + softBody.selfContacts = 1; + softBody.particleMass = 0.05; + softBody.particleRadius = (R2OptionalReal){1, 0.06}; + R2ColliderDesc surfaceCollider2 = r2BallColliderDesc(0.06); + surfaceCollider2.friction = 0.6; + softBody.collider = surfaceCollider2; + } else if (k % 5 == 2) { + softBody = r2GridSoftBodyDesc(position, r2Vector(1.2, 0.12), 13, 2); + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + softBody.cellModel = R2_SOFT_CELL_COROTATIONAL; + material.youngModulus = 2.0e4; + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.particleMass = 0.1; + softBody.particleRadius = (R2OptionalReal){1, 0.06}; + R2ColliderDesc surfaceCollider3 = r2BallColliderDesc(0.06); + surfaceCollider3.friction = 0.6; + softBody.collider = surfaceCollider3; + } else { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.4, 0.4)); + collider.density = 0.5; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = position; + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + } + } + } + for (int i = 0; i < 3; i++) { + R2SoftBodyDesc softBody = + r2RopeSoftBodyDesc(r2Vector(-6 + i, 36 + i), r2Vector(4 + i, 37 + i), 30); + softBody.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){30, 1}); + softBody.particleMass = 0.03; + R2ColliderDesc surfaceCollider4 = r2BallColliderDesc(0.08); + surfaceCollider4.friction = 0.6; + softBody.collider = surfaceCollider4; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 10, 20); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_plasticity2.c b/c/testbed/examples2d/soft_plasticity2.c new file mode 100644 index 000000000..1f7814099 --- /dev/null +++ b/c/testbed/examples2d/soft_plasticity2.c @@ -0,0 +1,175 @@ +/* Port of examples2d/soft_plasticity2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static R2SoftBodyMaterial clay(R2Real young, R2Real plasticYield, R2Real plasticCreep) { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = young; + material.poissonRatio = .35; + material.elasticDampingRatio = 1; + material.plasticYield = plasticYield; + material.plasticCreep = plasticCreep; + material.deformationDamping = 4; + return material; +} + +void tbSoftPlasticity2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(16, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(13, 2); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.2, 2)); + collider.friction = .8; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Yield ladder: elastic to increasingly plastic, hit by identical balls. */ + const R2Real yields[] = {0, .2, .08, .02}; + for (size_t i = 0; i < TB_COUNT(yields); ++i) { + const R2Real x = -13 + i * 2.4; + R2SoftBodyDesc square = r2GridSoftBodyDesc(r2Vector(x, 0.75), r2Vector(0.75, 0.75), 6, 6); + square.cellModel = R2_SOFT_CELL_COROTATIONAL; + square.particleMass = .1; + square.canSleep = !testbed->noSleep; + { + R2SoftBodyMaterial material = clay(1.0e4, yields[i], 20); + square.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = .8; + square.collider = surface; + } + r2InsertSoftBody(world, &square); + + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, 6); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2BallColliderDesc(.4); + collider.density = 5; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Clay slab stamped by a kinematic press. */ + R2SoftBodyDesc slab = r2GridSoftBodyDesc(r2Vector(0, 0.5), r2Vector(3, 0.5), 25, 5); + slab.cellModel = R2_SOFT_CELL_COROTATIONAL; + slab.particleMass = .1; + slab.canSleep = !testbed->noSleep; + { + R2SoftBodyMaterial material = clay(3.0e4, .02, 50); + slab.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.12); + surface.friction = .8; + slab.collider = surface; + } + r2InsertSoftBody(world, &slab); + + const R2Vector pressRest = r2Vector(-2, 2.4); + R2RigidBodyHandle press; + { + R2RigidBodyDesc rigidBody = r2KinematicPositionBasedRigidBodyDesc(); + rigidBody.position.translation = pressRest; + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.4, 0.4)); + collider.position.rotation = r2Rotation(R2_PI / 4); + collider.friction = .5; + press = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(press, &collider); + } + /* Elastic and creeping columns under their own weight. */ + for (int i = 0; i < 2; ++i) { + const R2Real x = 5.5 + i * 1.5; + R2SoftBodyDesc column = r2GridSoftBodyDesc(r2Vector(x, 1.2), r2Vector(0.3, 1.2), 3, 12); + column.cellModel = R2_SOFT_CELL_COROTATIONAL; + column.particleMass = .2; + column.canSleep = !testbed->noSleep; + { + R2SoftBodyMaterial material = clay(2.0e3, i == 0 ? 0 : .04, .5); + column.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = 1; + column.collider = surface; + } + r2InsertSoftBody(world, &column); + } + /* Volumetric clay disks thrown at the wall. */ + const size_t n = 24; + uint32_t indices[24][2]; + for (uint32_t i = 0; i < n; ++i) { + indices[i][0] = i; + indices[i][1] = (i + 1) % n; + } + for (int i = 0; i < 3; ++i) { + const R2Vector center = r2Vector(11.5 - i * 1.5, 1 + i * .5); + R2Vector vertices[24]; + for (size_t k = 0; k < n; ++k) { + const R2Real a = (R2Real)k / n * 2 * R2_PI; + vertices[k] = r2VectorAdd(center, r2Vector(.5 * cos(a), .5 * sin(a))); + } + + R2VolumeMeshParameters ballMeshing = r2NewVolumeMeshParameters(.15); + R2SoftBodyDesc ball = r2VolumetricSoftBodyDesc( + (R2VectorView){vertices, n}, (R2SurfaceElementView){(const R2Edge *)&indices[0][0], n}, + ballMeshing); + { + ball.cellModel = R2_SOFT_CELL_COROTATIONAL; + R2SoftBodyMaterial material = clay(1.0e4, .03, 60); + ball.material = material; + ball.particleMass = .05; + ball.canSleep = !testbed->noSleep; + { + R2ColliderDesc surface = r2BallColliderDesc(.075); + surface.friction = .8; + ball.collider = surface; + } + R2SoftBodyHandle handle = r2InsertSoftBody(world, &ball); + + size_t count = r2SoftBody_NumParticles(handle); + for (size_t k = 0; k < count; ++k) { + r2SoftBody_SetParticleVelocity(handle, k, r2Vector(10, 1)); + } + } + } + + tbCamera2(testbed, 0, 3, 30); + + tbSetWorld(testbed, world); + R2Real t = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R2Real dt = r2TimeStep(world); + t += dt; + /* One second down, one second up, then move to the next stamp. */ + const R2Real period = 3; + const R2Real cycle = floor(t / period); + const R2Real phase = t - cycle * period; + const R2Real x = pressRest.x + fmod(cycle, 5); + const R2Real depth = 1; + const R2Real y = pressRest.y - (phase < 1 ? depth * phase + : phase < 2 ? depth * (2 - phase) + : 0); + + r2RigidBody_SetNextKinematicTranslation(press, r2Vector(x, y)); + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_stress2.c b/c/testbed/examples2d/soft_stress2.c new file mode 100644 index 000000000..7fe6447b0 --- /dev/null +++ b/c/testbed/examples2d/soft_stress2.c @@ -0,0 +1,173 @@ +/* Port of examples2d/soft_stress2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftStress2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(30, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(8, 9.3); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(3, 0.3)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Bridge under a crate. */ + { + uint32_t pinned[155]; + size_t pinnedCount = 0; + for (uint32_t i = 0; i < 31; ++i) { + for (uint32_t j = 0; j < 5; ++j) { + if (i == 0 || i == 30) { + pinned[pinnedCount++] = i * 5 + j; + } + } + } + R2SoftBodyDesc bridge = r2GridSoftBodyDesc(r2Vector(-7, 3), r2Vector(4.5, 0.6), 31, 5); + r2SoftBodyDesc_SetPinnedParticles(&bridge, + (R2IndexView){(const uint32_t *)pinned, pinnedCount}); + bridge.particleMass = .05; + bridge.particleRadius = (R2OptionalReal){1, .15}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.edgeSoftness = (R2SpringCoefficients){100, 1}; + material.volumeSoftness = (R2SpringCoefficients){100, 1}; + material.tearForce = (R2OptionalReal){1, 65}; + material.tearSmoothing = .5; + bridge.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.15); + surface.friction = .8; + bridge.collider = surface; + } + bridge.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &bridge); + } + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-7, 4.4); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.6, 0.6)); + collider.density = 1.5; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Bar hanging a crate. */ + R2SoftBodyHandle bar; + { + uint32_t pinned[51]; + size_t pinnedCount = 0; + for (uint32_t i = 0; i < 3; ++i) { + for (uint32_t j = 0; j < 17; ++j) { + if (j == 16) { + pinned[pinnedCount++] = i * 17 + j; + } + } + } + R2SoftBodyDesc builder = r2GridSoftBodyDesc(r2Vector(8, 6.8), r2Vector(0.3, 2.2), 3, 17); + r2SoftBodyDesc_SetPinnedParticles(&builder, + (R2IndexView){(const uint32_t *)pinned, pinnedCount}); + builder.particleMass = .05; + builder.particleRadius = (R2OptionalReal){1, .1}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.edgeSoftness = (R2SpringCoefficients){100, 1}; + material.volumeSoftness = (R2SpringCoefficients){100, 1}; + material.tearForce = (R2OptionalReal){1, 16}; + material.tearSmoothing = .5; + builder.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = .8; + builder.collider = surface; + } + builder.canSleep = !testbed->noSleep; + bar = r2InsertSoftBody(world, &builder); + } + R2RigidBodyHandle crateBody; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(8, 4); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.6, 0.5)); + collider.density = 1; + crateBody = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(crateBody, &collider); + } + + for (size_t i = 0; i < 3; ++i) { + r2SoftBody_AttachParticle(bar, i * 17, crateBody); + } + /* Block under a crate. */ + R2SoftBodyDesc block = r2GridSoftBodyDesc(r2Vector(2, 1.2), r2Vector(1.2, 1.2), 9, 9); + block.particleMass = .05; + block.particleRadius = (R2OptionalReal){1, .15}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.edgeSoftness = (R2SpringCoefficients){100, 1}; + material.volumeSoftness = (R2SpringCoefficients){100, 1}; + material.tearForce = (R2OptionalReal){1, 10}; + material.tearSmoothing = .5; + block.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.15); + surface.friction = .8; + block.collider = surface; + } + block.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &block); + + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(2, 3.2); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.8, 0.6)); + collider.density = 2; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Blob colored by stretch instead of a tear threshold. */ + R2SoftBodyDesc blob = r2DiskSoftBodyDesc(r2Vector(13, 1.6), 1.5, 36); + blob.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){8, 1}); + blob.particleMass = .05; + { + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = .8; + blob.collider = surface; + } + blob.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &blob); + + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(13, 4); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.6, 0.4)); + collider.density = 1; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + tbCamera2(testbed, 2, 4, 40); + testbed->initialDebug = R2_DEBUG_SOFT_BODIES | R2_DEBUG_SOFT_BODY_STRESS; + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_surface2.c b/c/testbed/examples2d/soft_surface2.c new file mode 100644 index 000000000..7da687bbc --- /dev/null +++ b/c/testbed/examples2d/soft_surface2.c @@ -0,0 +1,141 @@ +/* Port of examples2d/soft_surface2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftSurface2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + R2ColliderDesc boxCollider = r2CuboidColliderDesc(r2Vector(22, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &boxCollider); + + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 22, 2); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 3)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + R2SoftBodyDesc hammock = r2GridSoftBodyDesc(r2Vector(-9, 5), r2Vector(3, 0.15), 21, 2); + hammock.cellModel = R2_SOFT_CELL_VOLUME; + uint32_t hp[] = {0, 1, 40, 41}; + r2SoftBodyDesc_SetPinnedParticles(&hammock, (R2IndexView){(const uint32_t *)hp, 4}); + hammock.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){30, 1}); + hammock.particleMass = 0.05; + hammock.particleRadius = (R2OptionalReal){1, 0.06}; + R2ColliderDesc surfaceCollider = r2BallColliderDesc(0.06); + surfaceCollider.friction = 0.6; + hammock.collider = surfaceCollider; + + if (testbed->noSleep) { + hammock.canSleep = 0; + } + r2InsertSoftBody(world, &hammock); + for (int i = 0; i < 12; i++) { + for (int h = 0; h < 4; h++) { + R2ColliderDesc collider = r2BallColliderDesc(0.08); + collider.density = 2; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-11.2 + i * 0.4 + h % 2 * 0.15, 7 + h * 0.5); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + R2SoftBodyDesc bridge = r2GridSoftBodyDesc(r2Vector(2, 3), r2Vector(3, 0.2), 31, 3); + bridge.cellModel = R2_SOFT_CELL_VOLUME; + uint32_t bp[] = {0, 1, 2, 90, 91, 92}; + r2SoftBodyDesc_SetPinnedParticles(&bridge, (R2IndexView){(const uint32_t *)bp, 6}); + bridge.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){30, 1}); + bridge.particleMass = 0.05; + bridge.particleRadius = (R2OptionalReal){1, 0.05}; + R2ColliderDesc clothSurfaceCollider = r2BallColliderDesc(0.05); + clothSurfaceCollider.friction = 0.8; + bridge.collider = clothSurfaceCollider; + + if (testbed->noSleep) { + bridge.canSleep = 0; + } + r2InsertSoftBody(world, &bridge); + for (int i = 0; i < 8; i++) { + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.03, 0.4)); + collider.density = 3; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-0.4 + i * 0.65, 4.5); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int i = 0; i < 3; i++) { + R2SoftBodyDesc softBody = + r2GridSoftBodyDesc(r2Vector(8, 0.75 + i * 1.6), r2Vector(0.7, 0.7), 5, 5); + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + softBody.cellModel = R2_SOFT_CELL_COROTATIONAL; + material.youngModulus = 1.0e4; + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.particleMass = 0.1; + softBody.particleRadius = (R2OptionalReal){1, 0.05}; + R2ColliderDesc jellySurfaceCollider = r2BallColliderDesc(0.05); + jellySurfaceCollider.friction = 0.8; + softBody.collider = jellySurfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + } + R2SoftBodyDesc blob = r2DiskSoftBodyDesc(r2Vector(8, 5), 0.8, 24); + blob.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){20, 1}); + blob.volumeFactor = 1.1; + blob.particleMass = 0.05; + blob.particleRadius = (R2OptionalReal){1, 0.05}; + R2ColliderDesc blobSurfaceCollider = r2BallColliderDesc(0.05); + blobSurfaceCollider.friction = 0.6; + blob.collider = blobSurfaceCollider; + + if (testbed->noSleep) { + blob.canSleep = 0; + } + r2InsertSoftBody(world, &blob); + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(14, 1); + R2ColliderDesc supportCollider = r2CuboidColliderDesc(r2Vector(0.15, 1)); + groundBody.canSleep = !testbed->noSleep; + rigidBodyHandle = r2InsertRigidBody(world, &groundBody); + r2InsertCollider(rigidBodyHandle, &supportCollider); + + R2SoftBodyDesc strip = r2GridSoftBodyDesc(r2Vector(14, 9), r2Vector(4, 0.1), 60, 2); + strip.cellModel = R2_SOFT_CELL_VOLUME; + strip.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){30, 1}); + strip.selfContacts = 1; + strip.particleMass = 0.02; + strip.particleRadius = (R2OptionalReal){1, 0.05}; + R2ColliderDesc stripSurfaceCollider = r2BallColliderDesc(0.05); + stripSurfaceCollider.friction = 0.6; + strip.collider = stripSurfaceCollider; + + if (testbed->noSleep) { + strip.canSleep = 0; + } + r2InsertSoftBody(world, &strip); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 4, 18); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_tearing2.c b/c/testbed/examples2d/soft_tearing2.c new file mode 100644 index 000000000..1cd1cd67e --- /dev/null +++ b/c/testbed/examples2d/soft_tearing2.c @@ -0,0 +1,228 @@ +/* Port of examples2d/soft_tearing2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +typedef struct DrivenParticle { + R2SoftBodyHandle body; + uint32_t index; + R2Vector rest; + int right; +} DrivenParticle; + +/* Follow driven particle indices through compaction and newly split bodies. */ +static void followTears(R2EventCollector *events, DrivenParticle *ends, size_t endCount) { + size_t count = r2EventCollector_TearEventCount(events); + for (size_t i = 0; i < count; ++i) { + R2SoftBodyTearEvent *event = r2EventCollector_TearEvent(events, i); + R2SoftBodyHandle origin = r2SoftBodyTearEvent_SoftBody(event); + for (size_t j = 0; j < endCount; ++j) { + if (ends[j].body.index == origin.index && + ends[j].body.generation == origin.generation) { + R2Bool found; + R2SoftBodyHandle destination; + uint32_t index; + R2OptionalParticleDestination softBodyTearEventTryParticleDestinationResult = + r2SoftBodyTearEvent_TryParticleDestination(event, ends[j].index); + destination = softBodyTearEventTryParticleDestinationResult.body; + index = softBodyTearEventTryParticleDestinationResult.index; + found = softBodyTearEventTryParticleDestinationResult.found; + if (found) { + ends[j].body = destination; + ends[j].index = index; + } + } + } + r2FreeSoftBodyTearEvent(event); + } +} + +void tbSoftTearing2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(30, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(16, 3); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.3, 3)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Bridge. */ + { + const size_t nx = 31, ny = 6; + uint32_t pinned[186]; + size_t pinnedCount = 0; + for (size_t i = 0; i < nx; ++i) { + for (size_t j = 0; j < ny; ++j) { + if (i == 0 || i == nx - 1) { + pinned[pinnedCount++] = (uint32_t)(i * ny + j); + } + } + } + R2SoftBodyDesc bridge = r2GridSoftBodyDesc(r2Vector(-8, 4), r2Vector(3, 0.5), nx, ny); + bridge.cellModel = R2_SOFT_CELL_COROTATIONAL; + r2SoftBodyDesc_SetPinnedParticles(&bridge, + (R2IndexView){(const uint32_t *)pinned, pinnedCount}); + bridge.particleMass = .05; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 1.0e6; + material.poissonRatio = .3; + material.elasticDampingRatio = 1; + material.tearStrain = (R2OptionalReal){1, .35}; + bridge.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = .8; + bridge.collider = surface; + } + bridge.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &bridge); + } + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-8, 9); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2BallColliderDesc(.6); + collider.density = 20; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Curtain. */ + { + const size_t nx = 7, ny = 41; + uint32_t pinned[287]; + size_t pinnedCount = 0; + for (size_t i = 0; i < nx; ++i) { + for (size_t j = 0; j < ny; ++j) { + if (j == ny - 1) { + pinned[pinnedCount++] = (uint32_t)(i * ny + j); + } + } + } + R2SoftBodyDesc curtain = r2GridSoftBodyDesc(r2Vector(2, 5), r2Vector(0.45, 3), nx, ny); + curtain.cellModel = R2_SOFT_CELL_COROTATIONAL; + r2SoftBodyDesc_SetPinnedParticles(&curtain, + (R2IndexView){(const uint32_t *)pinned, pinnedCount}); + curtain.particleMass = .05; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 3.0e4; + material.poissonRatio = .3; + material.elasticDampingRatio = 1; + material.tearStrain = (R2OptionalReal){1, .35}; + curtain.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.08); + surface.friction = .8; + curtain.collider = surface; + } + curtain.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &curtain); + } + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-4, 4.5); + rigidBody.linvel = r2Vector(20, 0); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2BallColliderDesc(.4); + collider.density = 10; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Jelly bar pulled apart by its two pinned ends. */ + R2SoftBodyHandle bar; + R2SoftBodyDesc builder = r2GridSoftBodyDesc(r2Vector(-3, 1), r2Vector(2, 0.4), 21, 5); + builder.cellModel = R2_SOFT_CELL_COROTATIONAL; + builder.particleMass = .05; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 5.0e4; + material.poissonRatio = .3; + material.elasticDampingRatio = 1; + material.tearStrain = (R2OptionalReal){1, .4}; + builder.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.1); + surface.friction = .8; + builder.collider = surface; + } + builder.canSleep = !testbed->noSleep; + bar = r2InsertSoftBody(world, &builder); + + size_t count = r2SoftBody_NumParticles(bar); + DrivenParticle *ends = malloc(count * sizeof(*ends)); + if (!ends) { + abort(); + } + size_t endCount = 0; + for (size_t i = 0; i < count; ++i) { + R2Vector position = r2SoftBody_ParticlePosition(bar, i); + int left = position.x < -4.99; + int right = position.x > -1.01; + if (left || right) { + ends[endCount++] = (DrivenParticle){bar, (uint32_t)i, position, right}; + r2SoftBody_SetParticlePinned(bar, i, 1); + } + } + R2EventCollector *events = r2NewEventCollector(); + + tbCamera2(testbed, 0, 4, 40); + + tbSetWorld(testbed, world); + R2Real t = 0; + uint32_t previousMinPiece = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + const int useDefault = (int)tbLiveSetting(testbed, "Default minimum piece", 1, 0, 1, 1); + const uint32_t selected = + (uint32_t)tbLiveSetting(testbed, "Minimum piece (elements)", 3, 1, 40, 1); + const uint32_t minPiece = useDefault ? 0 : selected; + if (minPiece != previousMinPiece) { + size_t bodyCount = r2SoftBodyCount(world); + R2SoftBodyHandle *handles = malloc(bodyCount * sizeof(*handles)); + if (bodyCount && !handles) { + abort(); + } + bodyCount = r2SoftBodyHandles(world, handles, bodyCount); + for (size_t i = 0; i < bodyCount; ++i) { + R2SoftBodyMaterial material = r2SoftBody_Material(handles[i]); + material.minPiece = (R2OptionalU32){minPiece != 0, minPiece}; + r2SoftBody_SetMaterial(handles[i], &material); + } + free(handles); + previousMinPiece = minPiece; + } + if (tbSimulating(testbed)) { + R2Real dt = r2TimeStep(world); + t += dt; + /* Move the right clamp after one second, stopping at twice the bar's length. */ + const R2Real shift = fmin(fmax(t - 1, 0) * .5, 4); + for (size_t i = 0; i < endCount; ++i) { + if (ends[i].right) { + r2SoftBody_SetParticleKinematicTarget( + ends[i].body, ends[i].index, + r2VectorAdd(ends[i].rest, r2Vector(shift, 0))); + } + } + r2EventCollector_Clear(events); + r2Step(world, NULL, events); + followTears(events, ends, endCount); + } + } + free(ends); + r2FreeEventCollector(events); + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/soft_thin_features2.c b/c/testbed/examples2d/soft_thin_features2.c new file mode 100644 index 000000000..5d173bfae --- /dev/null +++ b/c/testbed/examples2d/soft_thin_features2.c @@ -0,0 +1,187 @@ +/* Port of examples2d/soft_thin_features2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static R2SoftBodyDesc jelly(R2Vector center, R2Real half, size_t n, R2Real young) { + R2SoftBodyDesc builder = r2GridSoftBodyDesc(center, r2Vector(half, half), n, n); + builder.cellModel = R2_SOFT_CELL_COROTATIONAL; + builder.particleMass = .1; + builder.particleRadius = (R2OptionalReal){1, .06}; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = young; + material.poissonRatio = .4; + material.elasticDampingRatio = .5; + builder.material = material; + } + { + R2ColliderDesc surface = r2BallColliderDesc(.06); + surface.friction = .7; + builder.collider = surface; + } + return builder; +} + +static R2SoftBodyDesc strip(R2Vector center, R2Vector half, size_t nx) { + R2SoftBodyDesc builder = r2GridSoftBodyDesc(center, half, nx, 2); + builder.cellModel = R2_SOFT_CELL_VOLUME; + builder.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){30, 1}); + builder.particleMass = .03; + builder.particleRadius = (R2OptionalReal){1, .05}; + { + R2ColliderDesc surface = r2BallColliderDesc(.05); + surface.friction = .6; + builder.collider = surface; + } + return builder; +} + +static R2SoftBodyDesc blob(R2Vector center, R2Real radius) { + R2SoftBodyDesc builder = r2DiskSoftBodyDesc(center, radius, 24); + builder.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){20, 1}); + builder.volumeFactor = 1.2; + builder.particleMass = .05; + builder.particleRadius = (R2OptionalReal){1, .06}; + { + R2ColliderDesc surface = r2BallColliderDesc(.06); + surface.friction = .6; + builder.collider = surface; + } + return builder; +} + +void tbSoftThinFeatures2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(40, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Bed of nails. */ + for (int i = 0; i < 24; ++i) { + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-16 + i * .5, 0.6); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CapsuleYColliderDesc(.6, .03); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + { + R2SoftBodyDesc body = jelly(r2Vector(-14.5, 3.5), .75, 5, 3.0e3); + body.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &body); + } + { + R2SoftBodyDesc body = jelly(r2Vector(-11.5, 3.5), .75, 5, 5.0e4); + body.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &body); + } + { + R2SoftBodyDesc body = blob(r2Vector(-8.5, 3.5), .8); + body.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &body); + } + { + R2SoftBodyDesc body = strip(r2Vector(-6, 6), r2Vector(2.5, .1), 31); + body.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &body); + } + /* Needle rain on a hammock and a jelly block. */ + const uint32_t pinned[] = {0, 1, 60, 61}; + { + R2SoftBodyDesc hammock = strip(r2Vector(0, 4), r2Vector(3, .1), 31); + r2SoftBodyDesc_SetPinnedParticles(&hammock, + (R2IndexView){(const uint32_t *)pinned, TB_COUNT(pinned)}); + hammock.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &hammock); + } + { + R2SoftBodyDesc body = jelly(r2Vector(6.5, .9), .9, 6, 2.0e4); + body.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &body); + } + const R2Real centers[] = {0, 6.5}; + for (int i = 0; i < 10; ++i) { + for (int j = 0; j < 3; ++j) { + for (size_t k = 0; k < TB_COUNT(centers); ++k) { + const R2Real cx = centers[k]; + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(cx - 2 + i * .45, 8 + j * 1.2); + rigidBody.position.rotation = r2Rotation(.4 * (i + j)); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CapsuleYColliderDesc(.5, .02); + collider.density = 3; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + } + } + /* Thin plates falling on a blob. */ + { + R2SoftBodyDesc body = blob(r2Vector(11, .9), .9); + body.canSleep = !testbed->noSleep; + r2InsertSoftBody(world, &body); + } + for (int i = 0; i < 4; ++i) { + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(11, 3.5 + i * .5); + rigidBody.position.rotation = r2Rotation(.15 * i); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.9, 0.015)); + collider.density = 1; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* A strip stretched at its right end with a rod resting on it. */ + R2SoftBodyHandle stretched; + { + R2SoftBodyDesc body = strip(r2Vector(16, 3), r2Vector(2.5, .1), 31); + r2SoftBodyDesc_SetPinnedParticles(&body, + (R2IndexView){(const uint32_t *)pinned, TB_COUNT(pinned)}); + body.canSleep = !testbed->noSleep; + stretched = r2InsertSoftBody(world, &body); + } + + R2Vector rightRest[2]; + rightRest[0] = r2SoftBody_ParticlePosition(stretched, 60); + rightRest[1] = r2SoftBody_ParticlePosition(stretched, 61); + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(16, 4); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CapsuleXColliderDesc(.8, .03); + collider.density = 2; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + tbCamera2(testbed, 0, 4, 22); + + tbSetWorld(testbed, world); + R2Real t = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R2Real dt = r2TimeStep(world); + t += dt; + const R2Real stretch = 2.5 * (1 - cos(.5 * t)); + + for (size_t i = 0; i < 2; ++i) { + r2SoftBody_SetParticleKinematicTarget( + stretched, 60 + i, r2VectorAdd(rightRest[i], r2Vector(stretch, 0))); + } + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/balls2.c b/c/testbed/examples2d/stress_tests/balls2.c new file mode 100644 index 000000000..379fd2b42 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/balls2.c @@ -0,0 +1,32 @@ +/* Port of examples2d/stress_tests/balls2.rs. */ +#include "testbed.h" +#include "rapier_math.h" + +void tbStressTestsBalls2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + for (int i = 0; i < 50; i++) { + for (int j = 0; j < 250; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.bodyType = j ? R2_DYNAMIC : R2_FIXED; + rigidBody.position.translation = r2Vector(i * 2.5 - 62.5, j * 2 + 1); + R2ColliderDesc collider = r2BallColliderDesc(1); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 5); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/boxes2.c b/c/testbed/examples2d/stress_tests/boxes2.c new file mode 100644 index 000000000..62294a79d --- /dev/null +++ b/c/testbed/examples2d/stress_tests/boxes2.c @@ -0,0 +1,47 @@ +/* Port of examples2d/stress_tests/boxes2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsBoxes2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(25, 1.2)); + groundBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle groundBodyHandle = r2InsertRigidBody(world, &groundBody); + r2InsertCollider(groundBodyHandle, &collider); + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 25, 50); + rigidBody.position = r2Pose(r2Vector(side * 25, 50), r2Rotation(R2_PI / 2)); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(50, 1.2)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int i = 0; i < 26; i++) { + for (int j = 0; j < 130; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i - 13, j * 1 + 2.5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 50, 10); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/capsules2.c b/c/testbed/examples2d/stress_tests/capsules2.c new file mode 100644 index 000000000..e349667fb --- /dev/null +++ b/c/testbed/examples2d/stress_tests/capsules2.c @@ -0,0 +1,48 @@ +/* Port of examples2d/stress_tests/capsules2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsCapsules2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(25, 1.2)); + groundBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle groundBodyHandle = r2InsertRigidBody(world, &groundBody); + r2InsertCollider(groundBodyHandle, &collider); + + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 25, 100); + rigidBody.position = r2Pose(r2Vector(side * 25, 100), r2Rotation(R2_PI / 2)); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(100, 1.2)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int i = 0; i < 26; i++) { + for (int j = 0; j < 130; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i - 13, j * 2.5 + 3.5); + R2ColliderDesc collider = r2CapsuleYColliderDesc(0.75, 0.5); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 50, 10); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/convex_polygons2.c b/c/testbed/examples2d/stress_tests/convex_polygons2.c new file mode 100644 index 000000000..75f114186 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/convex_polygons2.c @@ -0,0 +1,63 @@ +/* Port of examples2d/stress_tests/convex_polygons2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "example_math.h" + +void tbStressTestsConvexPolygons2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(30, 1.2)); + groundBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle groundBodyHandle = r2InsertRigidBody(world, &groundBody); + r2InsertCollider(groundBodyHandle, &collider); + + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 30, 60); + rigidBody.position = r2Pose(r2Vector(side * 30, 60), r2Rotation(R2_PI / 2)); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(60, 1.2)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + R2SharedShape *shapes[5]; + for (size_t i = 0; i < 5; i++) { + R2Vector points[10]; + for (size_t k = 0; k < 10; k++) { + R2Real x = exampleRandom(&testbed->randomState) * 2; + R2Real y = exampleRandom(&testbed->randomState) * 2; + points[k] = r2Vector(x, y); + } + shapes[i] = r2ConvexHullSharedShape((R2VectorView){points, 10}); + } + for (int i = 0; i < 26; i++) { + for (int j = 0; j < 130; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 2 - 26, j * 4 + 3); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shapes[i % 5]; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + for (size_t i = 0; i < TB_COUNT(shapes); ++i) { + r2FreeSharedShape(shapes[i]); + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 50, 10); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/heightfield2.c b/c/testbed/examples2d/stress_tests/heightfield2.c new file mode 100644 index 000000000..ab001c8aa --- /dev/null +++ b/c/testbed/examples2d/stress_tests/heightfield2.c @@ -0,0 +1,54 @@ +/* Port of examples2d/stress_tests/heightfield2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsHeightfield2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2Real heights[2001]; + for (int i = 0; i <= 2000; i++) { + heights[i] = i == 0 || i == 2000 ? 80 : (R2Real)cos(i * 50.0 / 2000) * 2; + } + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_HEIGHTFIELD; + collider.shape.heights = (R2RealView){heights, (2001) * (1)}; + collider.shape.rows = 2001; + collider.shape.columns = 1; + collider.shape.scale = r2Vector(50, 1); + collider.shape.flags = 0; + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + for (int i = 0; i < 26; i++) { + for (int j = 0; j < 130; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i - 13, j + 3.5); + R2ColliderDesc collider; + if (j % 2) { + collider = r2BallColliderDesc(0.5); + } else { + collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + } + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 50, 10); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/joint_ball2.c b/c/testbed/examples2d/stress_tests/joint_ball2.c new file mode 100644 index 000000000..51116dd3b --- /dev/null +++ b/c/testbed/examples2d/stress_tests/joint_ball2.c @@ -0,0 +1,50 @@ +/* Port of examples2d/stress_tests/joint_ball2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsJointBall2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyHandle *handles = calloc(1, 10000 * sizeof(*handles)); + if (!handles) { + abort(); + } + for (int k = 0; k < 100; k++) { + for (int i = 0; i < 100; i++) { + int fixed = k >= 47 && k <= 53 && i == 0; + R2RigidBodyHandle handle; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.bodyType = fixed ? R2_FIXED : R2_DYNAMIC; + rigidBody.position.translation = r2Vector(k, -i); + R2ColliderDesc collider = r2BallColliderDesc(0.4); + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + for (int dir = 0; dir < 2; dir++) { + if (dir ? k > 0 : i > 0) { + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2Vector(0, 0); + joint.localFrame2.translation = dir ? r2Vector(-1, 0) : r2Vector(0, 1); + r2InsertImpulseJoint(handles[k * 100 + i - (dir ? 100 : 1)], handle, + &joint); + } + } + handles[k * 100 + i] = handle; + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 40.0, -40.0, 5); + free(handles); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/joint_fixed2.c b/c/testbed/examples2d/stress_tests/joint_fixed2.c new file mode 100644 index 000000000..1b3dd43b7 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/joint_fixed2.c @@ -0,0 +1,54 @@ +/* Port of examples2d/stress_tests/joint_fixed2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsJointFixed2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyHandle handles[900]; + for (int xx = 0; xx < 4; xx++) { + for (int yy = 0; yy < 4; yy++) { + for (int k = 0; k < 30; k++) { + for (int i = 0; i < 30; i++) { + R2RigidBodyHandle handle; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.bodyType = k ? R2_DYNAMIC : R2_FIXED; + rigidBody.position.translation = r2Vector(xx * 32 + k, yy * 34 - i); + R2ColliderDesc collider = r2BallColliderDesc(0.4); + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + if (i) { + R2JointDesc joint = r2DefaultJointDesc(); + joint.lockedAxes = R2_JOINT_FIXED_AXES; + joint.localFrame1.translation = r2Vector(0, 0); + joint.localFrame2.translation = r2Vector(0, 1); + r2InsertImpulseJoint(handles[k * 30 + i - 1], handle, &joint); + } + if (k) { + R2JointDesc joint = r2DefaultJointDesc(); + joint.lockedAxes = R2_JOINT_FIXED_AXES; + joint.localFrame1.translation = r2Vector(0, 0); + joint.localFrame2.translation = r2Vector(-1, 0); + r2InsertImpulseJoint(handles[k * 30 + i - 30], handle, &joint); + } + handles[k * 30 + i] = handle; + } + } + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 50, 50, 5); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/joint_prismatic2.c b/c/testbed/examples2d/stress_tests/joint_prismatic2.c new file mode 100644 index 000000000..c3aed3443 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/joint_prismatic2.c @@ -0,0 +1,50 @@ +/* Port of examples2d/stress_tests/joint_prismatic2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsJointPrismatic2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + for (int l = 0; l < 25; l++) { + for (int j = 0; j < 50; j++) { + R2Real x = j * 4; + R2Real y = l * 24; + R2RigidBodyHandle parent; + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, y); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.4, 0.4)); + rigidBody.canSleep = !testbed->noSleep; + parent = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(parent, &collider); + for (int i = 0; i < 10; i++) { + R2RigidBodyHandle handle; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x, y - i - 1); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.4, 0.4)); + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + R2JointDesc joint = r2PrismaticJointDesc(r2Vector(i % 2 ? -1 : 1, 1)); + joint.localFrame1.translation = r2Vector(0, 0); + joint.localFrame2.translation = r2Vector(0, 1); + r2JointDesc_SetLimits(&joint, R2_AXIS_LIN_X, -1.5, 1.5); + r2InsertImpulseJoint(parent, handle, &joint); + parent = handle; + } + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 80, 80, 15); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/large_pyramids2.c b/c/testbed/examples2d/stress_tests/large_pyramids2.c new file mode 100644 index 000000000..158fb8e86 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/large_pyramids2.c @@ -0,0 +1,45 @@ +/* Port of examples2d/stress_tests/large_pyramids2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsLargePyramids2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, -1); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(520, 1)); + groundBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle groundBodyHandle = r2InsertRigidBody(world, &groundBody); + r2InsertCollider(groundBodyHandle, &collider); + for (int p = 0; p < 8; p++) { + for (int i = 0; i < 55; i++) { + for (int j = i; j < 55; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = + r2Vector(p * 65 - 260 + i * 0.5 + j - i, i * 1.001 + 0.5); + rigidBody.canSleep = 0; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 27.5, 3); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/many_pyramids2.c b/c/testbed/examples2d/stress_tests/many_pyramids2.c new file mode 100644 index 000000000..05d083221 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/many_pyramids2.c @@ -0,0 +1,57 @@ +/* Port of examples2d/stress_tests/many_pyramids2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsManyPyramids2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyHandle floor; + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = r2Vector(0, 0); + groundBody.canSleep = !testbed->noSleep; + floor = r2InsertRigidBody(world, &groundBody); + + for (int i = 0; i < 20; i++) { + R2SharedShape *shape = r2SegmentSharedShape(r2Vector(-110, i * 11), r2Vector(110, i * 11)); + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + r2InsertCollider(floor, &collider); + + r2FreeSharedShape(shape); + } + for (int row = 0; row < 20; row++) { + for (int col = 0; col < 20; col++) { + for (int i = 0; i < 10; i++) { + for (int j = i; j < 10; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = + r2Vector((i + 1) * 0.5 + (j - i) - 110 + col * 11 + 1 - 0.5, + (2 * i + 1) * 0.5 + row * 11); + rigidBody.canSleep = 0; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 110, 3); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/pyramid2.c b/c/testbed/examples2d/stress_tests/pyramid2.c new file mode 100644 index 000000000..0a7c87155 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/pyramid2.c @@ -0,0 +1,38 @@ +/* Port of examples2d/stress_tests/pyramid2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsPyramid2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(100, 1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + for (int i = 0; i < 100; i++) { + for (int j = i; j < 100; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(i * 0.5 + j - i - 50, i + 2.25); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 5); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/ragdolls2.c b/c/testbed/examples2d/stress_tests/ragdolls2.c new file mode 100644 index 000000000..09625bdcd --- /dev/null +++ b/c/testbed/examples2d/stress_tests/ragdolls2.c @@ -0,0 +1,92 @@ +/* Port of examples2d/stress_tests/ragdolls2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +typedef struct Part { + R2RigidBodyHandle handle; + R2Vector offset; +} Part; + +/* A body and its offset from the ragdoll's torso. */ +static Part part(R2World *world, R2Vector origin, R2Vector offset, R2ColliderDesc collider, + int noSleep) { + R2RigidBodyDesc body = r2DynamicRigidBodyDesc(); + body.position.translation = r2VectorAdd(origin, offset); + body.canSleep = !noSleep; + Part result = {.offset = offset}; + result.handle = r2InsertRigidBody(world, &body); + r2InsertCollider(result.handle, &collider); + + return result; +} + +static R2JointDesc revolute(Part parent, Part child, R2Vector anchor, R2Real min, R2Real max) { + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2VectorSub(anchor, parent.offset); + joint.localFrame2.translation = r2VectorSub(anchor, child.offset); + r2JointDesc_SetLimits(&joint, R2_AXIS_ANG_X, min, max); + joint.contactsEnabled = 0; + return joint; +} + +/* Ten bodies and nine limited joints, matching the Rust ragdoll. */ +static void ragdoll(R2World *world, R2Vector origin, int noSleep) { + R2ColliderDesc collider = r2CapsuleYColliderDesc(.3, .15); + Part torso = part(world, origin, r2Vector(0, 0), collider, noSleep); + collider = r2BallColliderDesc(.15); + Part head = part(world, origin, r2Vector(0, 0.55), collider, noSleep); + R2JointDesc neck = revolute(torso, head, r2Vector(0, 0.42), -.5, .5); + R2ImpulseJointHandle jointHandle = + r2InsertImpulseJoint(torso.handle, head.handle, &neck); + + for (int side = -1; side <= 1; side += 2) { + collider = r2CapsuleXColliderDesc(.14, .06); + Part upperArm = part(world, origin, r2Vector(side * .36, .25), collider, noSleep); + collider = r2CapsuleXColliderDesc(.14, .06); + Part forearm = part(world, origin, r2Vector(side * .70, .25), collider, noSleep); + collider = r2CapsuleYColliderDesc(.16, .07); + Part thigh = part(world, origin, r2Vector(side * .09, -.52), collider, noSleep); + collider = r2CapsuleYColliderDesc(.16, .07); + Part shin = part(world, origin, r2Vector(side * .09, -.92), collider, noSleep); + R2JointDesc shoulder = revolute(torso, upperArm, r2Vector(side * .19, .25), -1.2, 1.2); + jointHandle = r2InsertImpulseJoint(torso.handle, upperArm.handle, &shoulder); + + R2JointDesc elbow = revolute(upperArm, forearm, r2Vector(side * .53, .25), 0, 2.5); + jointHandle = r2InsertImpulseJoint(upperArm.handle, forearm.handle, &elbow); + + R2JointDesc hip = revolute(torso, thigh, r2Vector(side * .09, -.33), -1, 1); + jointHandle = r2InsertImpulseJoint(torso.handle, thigh.handle, &hip); + + R2JointDesc knee = revolute(thigh, shin, r2Vector(side * .09, -.72), 0, 2.3); + jointHandle = r2InsertImpulseJoint(thigh.handle, shin.handle, &knee); + } +} + +void tbStressTestsRagdolls2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -1); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(1000, 1)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* 200 ragdolls: 20 columns and 10 layers. */ + for (int layer = 0; layer < 10; ++layer) { + for (int col = 0; col < 20; ++col) { + ragdoll(world, r2Vector(col * 2.2, 1.5 + layer * 2.6), testbed->noSleep); + } + } + tbCamera2(testbed, 22, 6, 15); + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/ropes2.c b/c/testbed/examples2d/stress_tests/ropes2.c new file mode 100644 index 000000000..48fce02e9 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/ropes2.c @@ -0,0 +1,48 @@ +/* Port of examples2d/stress_tests/ropes2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsRopes2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + for (int i = 0; i < 64; i++) { + R2Vector top = r2Vector(i * 4, 0); + R2RigidBodyHandle parent; + R2RigidBodyDesc groundBody = r2FixedRigidBodyDesc(); + groundBody.position.translation = top; + groundBody.canSleep = !testbed->noSleep; + parent = r2InsertRigidBody(world, &groundBody); + + for (int s = 0; s < 60; s++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2VectorAdd(top, r2Vector(0, -(s + 0.5))); + rigidBody.linvel = r2Vector(2, 0); + R2RigidBodyHandle handle; + R2ColliderDesc collider = r2CapsuleYColliderDesc(0.35, 0.1); + rigidBody.canSleep = !testbed->noSleep; + handle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(handle, &collider); + + R2JointDesc joint = r2RevoluteJointDesc(); + joint.localFrame1.translation = r2Vector(0, s ? -0.5 : 0); + joint.localFrame2.translation = r2Vector(0, 0.5); + joint.contactsEnabled = 0; + r2InsertImpulseJoint(parent, handle, &joint); + parent = handle; + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 128, -30, 4); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/soft_blobs2.c b/c/testbed/examples2d/stress_tests/soft_blobs2.c new file mode 100644 index 000000000..e9c5fb8db --- /dev/null +++ b/c/testbed/examples2d/stress_tests/soft_blobs2.c @@ -0,0 +1,58 @@ +/* Port of examples2d/stress_tests/soft_blobs2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftBlobs2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(20, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 20, 30); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 30)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int j = 0; j < 60; j++) { + for (int i = 0; i < 19; i++) { + R2SoftBodyDesc softBody = r2DiskSoftBodyDesc( + r2Vector(-19 + i * 2 + j % 2 * 0.5, 3 + j * 2), 0.45 + 0.1 * ((i + j) % 3), 20); + softBody.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){60, 3}); + softBody.volumeFactor = 1.05; + softBody.selfContacts = 1; + softBody.particleMass = 0.05; + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + } + } + R2SoftBodyDesc softBodyValue = r2GridSoftBodyDesc(r2Vector(0, 16), r2Vector(3, 0.15), 40, 3); + softBodyValue.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){120, 1}); + softBodyValue.selfContacts = 1; + softBodyValue.particleMass = 0.1; + if (testbed->noSleep) { + softBodyValue.canSleep = 0; + } + r2InsertSoftBody(world, &softBodyValue); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 6, 30); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/soft_cloth_keva2.c b/c/testbed/examples2d/stress_tests/soft_cloth_keva2.c new file mode 100644 index 000000000..73b9211ec --- /dev/null +++ b/c/testbed/examples2d/stress_tests/soft_cloth_keva2.c @@ -0,0 +1,68 @@ +/* Port of examples2d/stress_tests/soft_cloth_keva2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftClothKeva2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(40, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + R2Real y = 0; + for (int pair = 0; pair < 18; pair++) { + int n = 20 - pair; + for (int k = 0; k <= n; k++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-n + k * 2, y + 1); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.1, 1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int k = 0; k < n; k++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-n + k * 2 + 1, y + 2.1); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(1, 0.1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + y += 2.2; + } + R2SoftBodyDesc softBody = r2GridSoftBodyDesc(r2Vector(0, y + 6), r2Vector(24, 0.1), 241, 2); + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + softBody.cellModel = R2_SOFT_CELL_COROTATIONAL; + material.youngModulus = 2.0e4; + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.selfContacts = 1; + softBody.particleMass = 0.05; + softBody.particleRadius = (R2OptionalReal){1, 0.06}; + R2ColliderDesc surfaceCollider = r2BallColliderDesc(0.06); + surfaceCollider.friction = 0.6; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + /* Set up the viewer. */ + tbCamera2(testbed, 0, 20, 12); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/soft_fem_beams2.c b/c/testbed/examples2d/stress_tests/soft_fem_beams2.c new file mode 100644 index 000000000..fa650e69b --- /dev/null +++ b/c/testbed/examples2d/stress_tests/soft_fem_beams2.c @@ -0,0 +1,83 @@ +/* Port of examples2d/stress_tests/soft_fem_beams2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#ifdef RAPIER_FEM + +void tbStressTestsSoftFemBeams2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(40, 0.5)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Cantilevers bolted at their left end, stiffer on successive rows. */ + const R2Real length = 4.0, thickness = 0.4; + for (int row = 0; row < 5; ++row) { + for (int col = 0; col < 10; ++col) { + const R2Real x0 = -30.0 + col * 6.0; + const R2Real y = 3.0 + row * 5.0; + R2SoftBodyDesc beam = r2GridSoftBodyDesc( + r2Vector(x0 + length * 0.5, y), r2Vector(length * 0.5, thickness * 0.5), 33, 5); + beam.cellModel = R2_SOFT_CELL_COROTATIONAL; + beam.totalMass = (R2OptionalReal){1, 8.0}; + beam.solver = R2_SOFT_SOLVER_FEM; + { + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + material.youngModulus = 1.0e6 * (1.0 + row); + material.poissonRatio = 0.3; + material.elasticDampingRatio = 1.0; + beam.material = material; + } + beam.canSleep = !testbed->noSleep; + uint32_t *beamPins = NULL; + { + size_t count = r2SoftBodyDesc_ParticlePositions(&beam, NULL, 0); + R2Vector *positions = malloc(count * sizeof(*positions)); + beamPins = malloc(count * sizeof(*beamPins)); + if (!positions || !beamPins) { + abort(); + } + count = r2SoftBodyDesc_ParticlePositions(&beam, positions, count); + size_t beamPinsCount = 0; + for (size_t i = 0; i < count; ++i) { + if (positions[i].x < x0 + 1.0e-4) { + beamPins[beamPinsCount++] = (uint32_t)i; + } + } + r2SoftBodyDesc_SetPinnedParticles( + &beam, (R2IndexView){(const uint32_t *)beamPins, beamPinsCount}); + + free(positions); + } + r2InsertSoftBody(world, &beam); + free(beamPins); + + /* Load dropped on the free end. */ + { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(x0 + length - 0.5, y + 2.0); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.4, 0.4)); + collider.density = 20.0; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + } + tbCamera2(testbed, 0, 12, 15); + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} +#endif diff --git a/c/testbed/examples2d/stress_tests/soft_jellies2.c b/c/testbed/examples2d/stress_tests/soft_jellies2.c new file mode 100644 index 000000000..8b63958e5 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/soft_jellies2.c @@ -0,0 +1,61 @@ +/* Port of examples2d/stress_tests/soft_jellies2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftJellies2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(16, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 16, 30); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 30)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + int k = 0; + for (int layer = 0; layer < 40; layer++) { + for (int i = 0; i < 12; i++, k++) { + R2SoftBodyDesc softBody = + r2GridSoftBodyDesc(r2Vector(-14 + i * 2.5 + layer % 2 * 0.8, 2 + layer * 2.4), + r2Vector(0.6, 0.6), 5, 5); + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + softBody.cellModel = k % 3 ? R2_SOFT_CELL_COROTATIONAL : R2_SOFT_CELL_NEO_HOOKEAN; + material.youngModulus = 2.0e3 * (1 + k % 5 * 3); + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.particleMass = 0.1; + softBody.particleRadius = (R2OptionalReal){1, 0.08}; + R2ColliderDesc surfaceCollider = r2BallColliderDesc(0.08); + surfaceCollider.friction = 0.7; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 8, 25); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/soft_ropes2.c b/c/testbed/examples2d/stress_tests/soft_ropes2.c new file mode 100644 index 000000000..78f5d1a75 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/soft_ropes2.c @@ -0,0 +1,67 @@ +/* Port of examples2d/stress_tests/soft_ropes2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftRopes2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(10, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 10, 5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 10)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int i = 0; i < 5; i++) { + R2SharedShape *shape = r2CapsuleSharedShape(r2Vector(-0.6, 0), r2Vector(0.6, 0), 0.1); + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-6 + i * 3, 5); + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + r2FreeSharedShape(shape); + } + for (int layer = 0; layer < 80; layer++) { + for (int i = 0; i < 4; i++) { + R2Real x = -8.6 + i * 4.4 + layer % 2 * 0.6; + R2Real y = 8 + layer * 1.2; + R2SoftBodyDesc softBody = + r2RopeSoftBodyDesc(r2Vector(x, y), r2Vector(x + 4, y + 0.4), 40); + softBody.particleMass = 0.03; + R2ColliderDesc surfaceCollider = r2BallColliderDesc(0.4); + surfaceCollider.friction = 0.6; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 8, 25); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/soft_slab2.c b/c/testbed/examples2d/stress_tests/soft_slab2.c new file mode 100644 index 000000000..51272890a --- /dev/null +++ b/c/testbed/examples2d/stress_tests/soft_slab2.c @@ -0,0 +1,81 @@ +/* Port of examples2d/stress_tests/soft_slab2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftSlab2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(14, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 14, 30); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 30)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int i = 0; i < 12; i++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-11 + i * 2, 0.5); + R2ColliderDesc collider; + if (i % 2) { + collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + } else { + collider = r2BallColliderDesc(0.5); + } + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + R2SoftBodyDesc softBody = r2GridSoftBodyDesc(r2Vector(0, 3), r2Vector(12, 1.5), 161, 21); + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + softBody.cellModel = R2_SOFT_CELL_COROTATIONAL; + material.youngModulus = 4.0e4; + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.particleMass = 0.2; + R2ColliderDesc surfaceCollider = r2BallColliderDesc(0.1); + surfaceCollider.friction = 0.7; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + for (int j = 0; j < 40; j++) { + for (int i = 0; i < 24; i++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-11.5 + i + j % 2 * 0.5, 7.5 + j * 1.2); + R2ColliderDesc collider; + if ((i + j) % 2) { + collider = r2CuboidColliderDesc(r2Vector(0.3, 0.3)); + } else { + collider = r2BallColliderDesc(0.3); + } + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 8, 25); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/soft_strips2.c b/c/testbed/examples2d/stress_tests/soft_strips2.c new file mode 100644 index 000000000..271eff5f8 --- /dev/null +++ b/c/testbed/examples2d/stress_tests/soft_strips2.c @@ -0,0 +1,71 @@ +/* Port of examples2d/stress_tests/soft_strips2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftStrips2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, -0.5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(10, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + + for (int side = -1; side <= 1; side += 2) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(side * 10, 30); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 30)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + for (int row = 0; row < 4; row++) { + for (int i = 0; i < 6; i++) { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-7.5 + i * 3 + row % 2 * 1.5, 4 + row * 3); + R2ColliderDesc collider = r2BallColliderDesc(0.25); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + for (int layer = 0; layer < 30; layer++) { + for (int i = 0; i < 5; i++) { + R2SoftBodyDesc softBody = + r2GridSoftBodyDesc(r2Vector(-7.8 + i * 3.7 + layer % 2 * 0.7, 17 + layer * 1.5), + r2Vector(1.6, 0.1), 33, 2); + R2SoftBodyMaterial material = r2DefaultSoftBodyMaterial(); + softBody.cellModel = R2_SOFT_CELL_COROTATIONAL; + material.youngModulus = 2.0e4; + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.selfContacts = 1; + softBody.particleMass = 0.05; + softBody.particleRadius = (R2OptionalReal){1, 0.06}; + R2ColliderDesc surfaceCollider = r2BallColliderDesc(0.06); + surfaceCollider.friction = 0.6; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r2InsertSoftBody(world, &softBody); + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 8, 25); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/stress_tests/vertical_stacks2.c b/c/testbed/examples2d/stress_tests/vertical_stacks2.c new file mode 100644 index 000000000..e2f03da9e --- /dev/null +++ b/c/testbed/examples2d/stress_tests/vertical_stacks2.c @@ -0,0 +1,41 @@ +/* Port of examples2d/stress_tests/vertical_stacks2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsVerticalStacks2(Testbed *testbed) { + /* World. */ + R2World *world = r2NewWorld(); + + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(400, 1)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + for (int side = 0; side < 2; side++) { + for (int i = 0; i < 80; i++) { + for (int j = 0; j < 1 + i * 2; j++) { + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = + r2Vector((j - i) * (side ? 1.5 : 1) + (side ? 120 : -120), 80 - i - 1 + 1.5); + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(0.5, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera2(testbed, 0, 2.5, 5); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/trimesh2.c b/c/testbed/examples2d/trimesh2.c new file mode 100644 index 000000000..69b30c27a --- /dev/null +++ b/c/testbed/examples2d/trimesh2.c @@ -0,0 +1,61 @@ +/* Port of examples2d/trimesh2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include "utils/logo_mesh.h" + +void tbTrimesh2(Testbed *testbed) { + R2World *world = r2NewWorld(); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(0, 0); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(25, 1.2)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(25, 25); + rigidBody.position.rotation = r2Rotation(R2_PI / 2); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(25, 1.2)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-25, 25); + rigidBody.position.rotation = r2Rotation(R2_PI / 2); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2CuboidColliderDesc(r2Vector(25, 1.2)); + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + /* Tessellated by the same SVG utility as the Rust example. */ + for (size_t ith = 0; ith < TB_COUNT(logoMeshes); ++ith) { + const LogoMesh *mesh = &logoMeshes[ith]; + for (int k = 0; k < 5; ++k) { + R2ColliderDesc collider = r2DefaultColliderDesc(); + r2ShapeDesc_SetTrimesh( + &collider.shape, (R2VectorView){mesh->vertices, mesh->vertexCount}, + (R2TriangleView){(const R2Triangle *)mesh->indices, mesh->triangleCount}, 0); + collider.contactSkin = .2; + R2RigidBodyDesc rigidBody = r2DynamicRigidBodyDesc(); + rigidBody.position.translation = r2Vector(ith * 8.0 - 20, 20 + k * 11); + rigidBody.canSleep = !testbed->noSleep; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + } + tbCamera2(testbed, 0, 20, 17); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples2d/utils/character.h b/c/testbed/examples2d/utils/character.h new file mode 100644 index 000000000..18adb2e95 --- /dev/null +++ b/c/testbed/examples2d/utils/character.h @@ -0,0 +1,111 @@ +/* Port of examples2d/utils/character.rs. Shared by the character and tether demos. */ +#ifndef EXAMPLE_CHARACTER_2D_H +#define EXAMPLE_CHARACTER_2D_H +#include "testbed.h" +#include "rapier_math.h" + +typedef enum CharacterControlMode { CHARACTER_KINEMATIC, CHARACTER_PID } CharacterControlMode; + +static void updateCharacter(Testbed *viewer, R2World *world, CharacterControlMode *controlMode, + R2KinematicCharacterController *controller, R2PidController *pid, + R2RigidBodyHandle characterHandle) { + R2Real dt = r2TimeStep(world); + + static const char *const modes[] = {"Kinematic", "PID"}; + const CharacterControlMode mode = + (CharacterControlMode)tbChoice(viewer, "Control mode", 0, modes, TB_COUNT(modes), 1, 0); + if (mode != *controlMode) { + r2RigidBody_SetBodyType(characterHandle, + mode == CHARACTER_KINEMATIC ? R2_KINEMATIC_POSITION_BASED + : R2_DYNAMIC, + mode == CHARACTER_PID); + *controlMode = mode; + } + R2Real speed = tbLiveSetting(viewer, "Character speed", .1, 0, 1, 0); + if (viewer->slow) { + speed /= 10; + } + R2Vector desiredMovement = r2Vector(viewer->inputDirection.x, 0); + desiredMovement.y = (viewer->jump ? 2 : 0) - (viewer->descend ? 1 : 0); + desiredMovement = r2VectorScale(desiredMovement, speed); + R2Vector translation = r2RigidBody_Translation(characterHandle); + if (mode == CHARACTER_PID) { + R2PidGains gains = r2PidController_Gains(pid); + const R2Real linKp = tbLiveSetting(viewer, "Linear Kp", 60, 0, 100, 0); + gains.lin_kp = r2Vector(linKp, linKp); + const R2Real linKi = tbLiveSetting(viewer, "Linear Ki", 1, 0, 10, 0); + gains.lin_ki = r2Vector(linKi, linKi); + const R2Real linKd = tbLiveSetting(viewer, "Linear Kd", 0.8, 0, 1, 0); + gains.lin_kd = r2Vector(linKd, linKd); + const R2Real angKp = tbLiveSetting(viewer, "Angular Kp", 60, 0, 100, 0); + gains.ang_kp = angKp; + const R2Real angKi = tbLiveSetting(viewer, "Angular Ki", 1, 0, 10, 0); + gains.ang_ki = angKi; + const R2Real angKd = tbLiveSetting(viewer, "Angular Kd", 0.8, 0, 1, 0); + gains.ang_kd = angKd; + r2PidController_SetGains(pid, gains); + uint32_t axes = 32; /* Angular axes. */ + if (desiredMovement.x != 0 || desiredMovement.y != 0) { + axes |= desiredMovement.y == 0 ? 1 : 3; + } + r2PidController_SetAxes(pid, axes); + R2Pose target = r2TranslationPose(r2VectorAdd(translation, desiredMovement)); + R2Vector correctiveLinear, linvel; + R2AngVector correctiveAngular, angvel; + R2VelocityCorrection pidControllerRigidBodyCorrectionResult = r2PidController_RigidBodyCorrection(pid, dt, characterHandle, target, r2Vector(0, 0), 0); + correctiveLinear = pidControllerRigidBodyCorrectionResult.linear; + correctiveAngular = pidControllerRigidBodyCorrectionResult.angularVelocity; + linvel = r2RigidBody_Linvel(characterHandle); + angvel = r2RigidBody_Angvel(characterHandle); + r2RigidBody_SetLinvel(characterHandle, r2VectorAdd(linvel, correctiveLinear), 1); + r2RigidBody_SetAngvel(characterHandle, angvel + correctiveAngular, 1); + return; + } + /* Kinematic character settings, applied live. */ + R2CharacterControllerSettings settings = r2KinematicCharacterController_Settings(controller); + settings.slide = (R2Bool)tbLiveSetting(viewer, "Slide", settings.slide, 0, 1, 1); + settings.max_slope_climb_angle = tbLiveSetting( + viewer, "Maximum climb angle", settings.max_slope_climb_angle, 0, 2 * R2_PI, 0); + settings.min_slope_slide_angle = tbLiveSetting( + viewer, "Minimum slide angle", settings.min_slope_slide_angle, 0, R2_PI / 2, 0); + settings.snap_to_ground = + (R2Bool)tbLiveSetting(viewer, "Snap to ground", settings.snap_to_ground, 0, 1, 1); + settings.snap_distance.value = tbLiveSetting(viewer, "Snap distance (relative height)", + settings.snap_distance.value, 0, 10, 0); + r2KinematicCharacterController_SetSlide(controller, settings.slide); + r2KinematicCharacterController_SetSlopes(controller, settings.max_slope_climb_angle, + settings.min_slope_slide_angle); + r2KinematicCharacterController_SetSnapToGround(controller, settings.snap_to_ground, + settings.snap_distance); + desiredMovement.y -= speed; /* Artificial gravity, as in the Rust utility. */ + size_t colliderCount = r2RigidBody_Colliders(characterHandle, NULL, 0); + R2ColliderHandle *handles = malloc(colliderCount * sizeof(*handles)); + if (!colliderCount || !handles) { + abort(); + } + colliderCount = r2RigidBody_Colliders(characterHandle, handles, colliderCount); + + const R2ColliderHandle colliderHandle = handles[0]; + free(handles); + R2Pose pose; + R2SharedShape *shape = NULL; + R2Real mass; + pose = r2Collider_Position(colliderHandle); + shape = r2Collider_CloneShape(colliderHandle); + mass = r2RigidBody_Mass(characterHandle); + R2QueryFilter filter = r2DefaultQueryFilter(); + filter.exclude_rigid_body = characterHandle; + R2QueryOptions query = r2DefaultQueryOptions(); + query.filter = filter; + R2CharacterMovement movement = r2KinematicCharacterController_MoveShape(world, &query, controller, dt, shape, pose, desiredMovement); + + tbBodyColor(viewer, characterHandle, movement.grounded ? .1f : .8f, + movement.grounded ? .8f : .1f, .1f, 1); + r2KinematicCharacterController_SolveCharacterCollisionImpulses(controller, shape, dt, + mass, &filter); + r2FreeSharedShape(shape); + + r2RigidBody_SetNextKinematicTranslation(characterHandle, + r2VectorAdd(translation, movement.translation)); +} +#endif diff --git a/c/testbed/examples2d/utils/logo_mesh.h b/c/testbed/examples2d/utils/logo_mesh.h new file mode 100644 index 000000000..0b4022c3b --- /dev/null +++ b/c/testbed/examples2d/utils/logo_mesh.h @@ -0,0 +1,236 @@ +/* Generated from examples2d/utils/svg.rs by tools/logo-mesh. Do not edit. */ +#ifndef EXAMPLE_LOGO_MESH_H +#define EXAMPLE_LOGO_MESH_H +#include "rapier.h" + +typedef struct LogoMesh { + const R2Vector *vertices; + size_t vertexCount; + const uint32_t *indices; + size_t triangleCount; + const uint32_t *outline; + size_t edgeCount; +} LogoMesh; + +static const R2Vector logoVertices0[] = { + {3.670558691e0, 9.392311096e0}, {2.995823622e0, 9.351827621e0}, + {5.119515896e0, 9.262369156e0}, {1.187533617e0, 9.257364273e0}, + {3.238728046e-1, 9.243869781e0}, {8.501661420e-1, 9.243869781e0}, + {3.373675048e-1, 9.014459610e0}, {3.076791763e0, 8.973976135e0}, + {3.306201696e0, 8.973976135e0}, {2.833887100e0, 8.946986198e0}, + {6.018636703e0, 8.933491707e0}, {9.041449428e-1, 8.919997215e0}, + {4.271835804e0, 8.841166496e0}, {1.322480559e0, 8.812039375e0}, + {1.538395882e0, 8.650102615e0}, {4.952555180e0, 8.474672318e0}, + {1.632858753e0, 8.407198906e0}, {6.594491482e0, 8.328379631e0}, + {1.646353483e0, 8.056336403e0}, {5.385685921e0, 7.887979984e0}, + {6.801329136e0, 7.327621937e0}, {5.532826900e0, 7.111707211e0}, + {6.693478584e0, 6.598976612e0}, {5.265688896e0, 6.164303780e0}, + {6.396488190e0, 6.072615147e0}, {4.466856956e0, 5.590919018e0}, + {2.833887100e0, 5.397880077e0}, {5.168469906e0, 5.222448826e0}, + {2.833887100e0, 5.020028591e0}, {3.549106359e0, 5.020028591e0}, + {3.945671082e0, 4.918076515e0}, {4.223841190e0, 4.601692677e0}, + {6.652887344e0, 2.617971897e0}, {2.833887100e0, 1.862268567e0}, + {1.646353483e0, 1.781300426e0}, {7.192675114e0, 1.781300426e0}, + {2.968834162e0, 1.389954090e0}, {7.651494980e0, 1.335975289e0}, + {1.524901152e0, 1.241512418e0}, {3.414159060e0, 1.160544276e0}, + {8.123809814e0, 1.147049546e0}, {9.986078739e-1, 1.079576015e0}, + {8.731071472e0, 1.039091945e0}, {2.833887041e-1, 9.986078739e-1}, + {4.075399399e0, 9.986078739e-1}, {2.105173349e0, 8.096820116e-1}, + {6.720360756e0, 8.096820116e-1}, {8.110315323e0, 8.096820116e-1}, + {6.275035858e0, 7.961873412e-1}, {8.731071472e0, 7.961873412e-1}, + {2.833887041e-1, 7.557032704e-1}, {4.075399399e0, 7.557032704e-1}, +}; +static const uint32_t logoIndices0[] = { + 5, 4, 6, 0, 1, 2, 2, 1, 3, 3, 5, 6, 6, 7, 8, 3, 6, 8, 2, 3, 8, 2, 8, + 10, 10, 8, 12, 10, 12, 15, 10, 15, 19, 10, 19, 21, 17, 10, 20, 22, 20, 24, 20, 10, 24, 10, + 21, 24, 21, 23, 24, 24, 23, 25, 24, 25, 27, 25, 26, 27, 27, 28, 29, 27, 29, 30, 27, 30, 31, + 27, 31, 32, 32, 31, 35, 35, 31, 37, 42, 40, 46, 40, 37, 46, 37, 31, 46, 46, 31, 48, 42, 46, + 47, 42, 47, 49, 7, 6, 9, 9, 6, 11, 9, 11, 13, 9, 13, 14, 9, 14, 16, 9, 16, 18, 9, + 18, 26, 27, 26, 28, 26, 18, 28, 28, 18, 33, 33, 18, 34, 33, 34, 36, 36, 34, 38, 36, 38, 39, + 39, 38, 41, 44, 39, 45, 39, 41, 43, 39, 43, 45, 45, 43, 50, 44, 45, 51, +}; +static const uint32_t logoOutline0[] = { + 5, 4, 4, 6, 0, 1, 2, 0, 1, 3, 3, 5, 7, 8, 10, 2, 8, 12, 12, 15, 15, + 19, 19, 21, 17, 10, 20, 17, 22, 20, 24, 22, 21, 23, 23, 25, 27, 24, 25, 26, 28, 29, + 29, 30, 30, 31, 32, 27, 35, 32, 37, 35, 42, 40, 40, 37, 31, 48, 48, 46, 46, 47, 47, + 49, 49, 42, 9, 7, 6, 11, 11, 13, 13, 14, 14, 16, 16, 18, 26, 9, 33, 28, 18, 34, + 36, 33, 34, 38, 39, 36, 38, 41, 44, 39, 41, 43, 43, 50, 50, 45, 45, 51, 51, 44, +}; +static const R2Vector logoVertices1[] = { + {5.330406666e0, 6.140089035e0}, {5.451858997e0, 6.099604607e0}, + {3.279212236e0, 6.086110115e0}, {3.778516054e0, 6.032131195e0}, + {4.183357239e0, 5.910678864e0}, {2.280604362e0, 5.897184372e0}, + {4.574703217e0, 5.748742580e0}, {3.130770445e0, 5.667774200e0}, + {2.537003517e0, 5.559816360e0}, {3.832494974e0, 5.532826900e0}, + {1.389954090e0, 5.343901157e0}, {1.956731558e0, 5.181964874e0}, + {4.277820110e0, 5.127985954e0}, {1.538395882e0, 4.507229805e0}, + {4.534219265e0, 4.480240345e0}, {7.557032704e-1, 4.439756393e0}, + {5.465353489e0, 4.007925987e0}, {4.601692677e0, 3.643569231e0}, + {1.362964749e0, 3.508621931e0}, {4.601692677e0, 3.400664568e0}, + {5.127986073e-1, 3.171254635e0}, {1.484417081e0, 2.658455849e0}, + {4.466745853e0, 2.550498247e0}, {6.477456093e-1, 2.186141491e0}, + {4.601692677e0, 2.159152031e0}, {1.808289886e0, 1.929742217e0}, + {5.465353489e0, 1.902752757e0}, {4.129378319e0, 1.875763297e0}, + {4.601692677e0, 1.754310966e0}, {4.372282982e0, 1.605869412e0}, + {6.356003761e0, 1.592374682e0}, {6.490951061e0, 1.511406541e0}, + {5.584416389e0, 1.498087645e0}, {6.234551907e0, 1.497911811e0}, + {2.334583044e0, 1.443933010e0}, {6.099604607e0, 1.430438280e0}, + {3.657063723e0, 1.416943550e0}, {1.052586675e0, 1.389954090e0}, + {5.924173832e0, 1.389954090e0}, {3.049802303e0, 1.255007148e0}, + {3.980936527e0, 1.133554816e0}, {6.248046398e0, 1.079576015e0}, + {4.790618420e0, 9.581237435e-1}, {1.740816236e0, 8.636608720e-1}, + {3.441148520e0, 7.961873412e-1}, {5.910678864e0, 7.961873412e-1}, + {2.712434769e0, 6.612402797e-1}, {5.438364029e0, 6.612402797e-1}, +}; +static const uint32_t logoIndices1[] = { + 24, 22, 27, 24, 27, 29, 29, 27, 36, 29, 36, 39, 29, 39, 40, 31, 30, 33, 31, 33, 35, 31, 35, 38, + 31, 38, 41, 3, 2, 4, 4, 2, 5, 4, 5, 6, 6, 5, 7, 7, 5, 8, 8, 5, 10, 8, 10, 11, + 11, 10, 13, 13, 10, 15, 13, 15, 18, 18, 15, 21, 15, 20, 23, 21, 15, 23, 21, 23, 25, 25, 23, 34, + 34, 23, 37, 34, 37, 39, 40, 39, 44, 39, 37, 43, 39, 43, 44, 44, 43, 46, 1, 0, 6, 6, 7, 9, + 6, 9, 12, 1, 6, 12, 1, 12, 14, 1, 14, 16, 16, 14, 17, 16, 17, 19, 19, 22, 24, 16, 19, 24, + 16, 24, 26, 26, 24, 28, 26, 28, 32, 38, 32, 41, 32, 45, 41, 32, 28, 42, 32, 42, 45, 45, 42, 47, +}; +static const uint32_t logoOutline1[] = { + 22, 27, 29, 24, 27, 36, 36, 39, 40, 29, 31, 30, 30, 33, 33, 35, 35, 38, 41, 31, 3, 2, 4, 3, + 2, 5, 6, 4, 8, 7, 5, 10, 11, 8, 13, 11, 10, 15, 18, 13, 21, 18, 15, 20, 20, 23, 25, 21, + 34, 25, 23, 37, 39, 34, 44, 40, 37, 43, 43, 46, 46, 44, 1, 0, 0, 6, 7, 9, 9, 12, 12, 14, + 16, 1, 14, 17, 17, 19, 19, 22, 26, 16, 24, 28, 32, 26, 38, 32, 45, 41, 28, 42, 42, 47, 47, 45, +}; +static const R2Vector logoVertices2[] = { + {1.902752757e0, 6.693371296e0}, {2.105173349e0, 6.693371296e0}, + {1.673342824e0, 6.409982681e0}, {4.480240345e0, 6.194067478e0}, + {1.376459360e0, 6.180572987e0}, {3.364475727e0, 6.046182632e0}, + {9.986078739e-1, 5.978152275e0}, {5.337097168e0, 5.969727993e0}, + {4.048410058e-1, 5.721753120e0}, {3.643569350e-1, 5.532826900e0}, + {3.495127439e0, 5.532826900e0}, {4.331799030e0, 5.451858997e0}, + {1.025597215e0, 5.424869537e0}, {6.005141735e0, 5.424869537e0}, + {2.105173349e0, 5.370890617e0}, {4.993039131e0, 4.993039131e0}, + {2.105173349e0, 4.952555180e0}, {1.201028347e0, 4.912070751e0}, + {6.420537472e0, 4.628257275e0}, {5.411374569e0, 4.237336159e0}, + {6.571918964e0, 3.535611391e0}, {5.559816360e0, 3.211738825e0}, + {6.356003761e0, 2.388561964e0}, {5.424869537e0, 2.294099092e0}, + {2.105173349e0, 2.051194429e0}, {2.212646961e0, 1.611583352e0}, + {5.033523083e0, 1.578879952e0}, {5.748742580e0, 1.457427740e0}, + {2.537003517e0, 1.255007148e0}, {4.426261902e0, 1.106565475e0}, + {2.105173349e0, 9.986078739e-1}, {3.643569231e0, 9.311342835e-1}, + {4.831102848e0, 8.366714120e-1}, {3.670558691e0, 6.072615385e-1}, + {1.201028347e0, -1.039091945e0}, {2.105173349e0, -1.052586675e0}, + {2.226625681e0, -1.335975289e0}, {1.147049546e0, -1.389954090e0}, + {2.671950579e0, -1.484417081e0}, {8.771554828e-1, -1.511406541e0}, + {3.103781044e-1, -1.619364023e0}, {3.481632471e0, -1.619364023e0}, + {1.956731558e0, -1.794795156e0}, {2.510014296e0, -1.794795156e0}, + {1.066081405e0, -1.808289886e0}, {3.063297033e0, -1.821784616e0}, + {3.103781044e-1, -1.848773837e0}, {3.481632471e0, -1.848773837e0}, +}; +static const uint32_t logoIndices2[] = { + 3, 5, 7, 7, 5, 10, 10, 5, 14, 10, 14, 16, 7, 10, 11, 7, 11, 13, 13, 11, 15, 13, 15, 19, + 13, 19, 21, 18, 13, 20, 13, 22, 20, 13, 21, 22, 22, 21, 23, 22, 23, 26, 22, 26, 27, 27, 26, 29, + 27, 29, 32, 29, 31, 32, 28, 30, 31, 31, 30, 32, 32, 30, 33, 1, 0, 2, 1, 2, 4, 6, 8, 9, + 6, 9, 12, 4, 6, 12, 1, 4, 12, 14, 1, 16, 1, 12, 16, 16, 12, 17, 16, 17, 24, 24, 17, 25, + 28, 25, 30, 25, 17, 30, 30, 17, 34, 30, 34, 35, 35, 34, 36, 36, 34, 37, 36, 37, 38, 38, 37, 39, + 38, 39, 40, 41, 38, 42, 38, 40, 42, 42, 40, 44, 44, 40, 46, 41, 42, 43, 41, 43, 45, 41, 45, 47, +}; +static const uint32_t logoOutline2[] = { + 3, 5, 7, 3, 5, 14, 16, 10, 10, 11, 13, 7, 11, 15, 15, 19, 19, 21, 18, 13, 20, 18, 22, 20, + 21, 23, 23, 26, 27, 22, 26, 29, 32, 27, 29, 31, 31, 28, 30, 33, 33, 32, 1, 0, 0, 2, 2, 4, + 6, 8, 8, 9, 9, 12, 4, 6, 14, 1, 12, 17, 24, 16, 25, 24, 28, 25, 17, 34, 35, 30, 36, 35, + 34, 37, 38, 36, 37, 39, 39, 40, 41, 38, 44, 42, 40, 46, 46, 44, 42, 43, 43, 45, 45, 47, 47, 41, +}; +static const R2Vector logoVertices3[] = { + {1.848773837e0, 9.000965118e0}, {1.430438280e0, 8.825533867e0}, + {2.294099092e0, 8.825533867e0}, {1.241512418e0, 8.380208969e0}, + {2.483024836e0, 8.380208969e0}, {1.416943550e0, 7.975368023e0}, + {2.294099092e0, 7.975368023e0}, {1.821784616e0, 7.799936771e0}, + {2.132162809e0, 6.207562447e0}, {2.361572504e0, 6.207562447e0}, + {1.524901152e0, 5.600300789e0}, {6.612402797e-1, 5.235943794e0}, + {6.612402797e-1, 5.033523083e0}, {1.308985949e0, 4.777123928e0}, + {1.470922351e0, 4.304809570e0}, {1.470922351e0, 1.619364023e0}, + {2.361572504e0, 1.578879952e0}, {1.369922280e0, 1.297711253e0}, + {2.429046154e0, 1.281996489e0}, {2.658455849e0, 1.147049546e0}, + {1.052586675e0, 1.133554816e0}, {3.238728046e-1, 9.851131439e-1}, + {3.319696426e0, 9.851131439e-1}, {1.821784616e0, 8.096820116e-1}, + {9.176396728e-1, 7.961873412e-1}, {2.739423990e0, 7.961873412e-1}, + {3.238728046e-1, 7.557032704e-1}, {3.319696426e0, 7.557032704e-1}, +}; +static const uint32_t logoIndices3[] = { + 0, 1, 2, 1, 3, 5, 4, 2, 6, 2, 1, 5, 2, 5, 6, 6, 5, 7, 9, 8, 10, 10, 11, 12, + 10, 12, 13, 9, 10, 13, 9, 13, 14, 9, 14, 15, 9, 15, 16, 16, 15, 17, 16, 17, 18, 18, 17, 19, + 19, 17, 20, 19, 20, 21, 22, 19, 23, 19, 21, 23, 23, 21, 24, 24, 21, 26, 22, 23, 25, 22, 25, 27, +}; +static const uint32_t logoOutline3[] = { + 0, 1, 2, 0, 1, 3, 3, 5, 4, 2, 6, 4, 5, 7, 7, 6, 9, 8, 8, + 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 16, 9, 15, 17, 18, 16, 19, 18, + 17, 20, 20, 21, 22, 19, 24, 23, 21, 26, 26, 24, 23, 25, 25, 27, 27, 22, +}; +static const R2Vector logoVertices4[] = { + {3.144265175e0, 6.194067478e0}, {3.967442036e0, 6.045626163e0}, + {2.091678619e0, 5.978152275e0}, {3.117275953e0, 5.870194912e0}, + {4.601692677e0, 5.640784740e0}, {3.886473656e0, 5.586805820e0}, + {2.105173349e0, 5.505837440e0}, {1.255007148e0, 5.384385109e0}, + {5.033523083e0, 4.993039131e0}, {4.210346699e0, 4.952555180e0}, + {4.237336159e0, 4.682661057e0}, {4.115883827e0, 4.547714233e0}, + {7.152191401e-1, 4.493735313e0}, {3.872978926e0, 4.493735313e0}, + {1.565385222e0, 4.480240345e0}, {3.522116899e0, 4.480240345e0}, + {1.497911811e0, 4.169862270e0}, {5.208954334e0, 4.169862270e0}, + {1.457427740e0, 3.657063723e0}, {5.127986073e-1, 3.373675108e0}, + {1.605237007e0, 2.695984364e0}, {6.882296801e-1, 2.280604362e0}, + {5.249438286e0, 2.132162809e0}, {2.010710478e0, 1.983721018e0}, + {5.303417206e0, 1.821784616e0}, {4.399272442e0, 1.524901152e0}, + {2.652937412e0, 1.503908396e0}, {1.187533617e0, 1.389954090e0}, + {3.535611391e0, 1.335975289e0}, {4.250830650e0, 9.041449428e-1}, + {1.929742217e0, 8.096820116e-1}, {2.847381830e0, 5.937668085e-1}, +}; +static const uint32_t logoIndices4[] = { + 1, 0, 2, 1, 2, 3, 1, 3, 4, 4, 3, 5, 4, 5, 8, 8, 5, 9, 8, 9, 10, 8, 10, 11, + 13, 15, 16, 8, 11, 17, 11, 13, 17, 13, 16, 17, 24, 22, 25, 24, 25, 29, 25, 28, 29, 3, 2, 6, + 6, 2, 7, 6, 7, 12, 6, 12, 14, 15, 14, 16, 14, 12, 16, 16, 12, 18, 18, 12, 20, 12, 19, 21, + 20, 12, 21, 20, 21, 23, 23, 21, 26, 26, 21, 27, 28, 26, 29, 26, 27, 29, 29, 27, 30, 29, 30, 31, +}; +static const uint32_t logoOutline4[] = { + 1, 0, 0, 2, 4, 1, 3, 5, 8, 4, 5, 9, 9, 10, 10, 11, 13, 15, 17, 8, 11, 13, + 16, 17, 24, 22, 22, 25, 29, 24, 25, 28, 6, 3, 2, 7, 7, 12, 14, 6, 15, 14, 18, 16, + 20, 18, 12, 19, 19, 21, 23, 20, 26, 23, 21, 27, 28, 26, 27, 30, 30, 31, 31, 29, +}; +static const R2Vector logoVertices5[] = { + {2.010710478e0, 6.504445553e0}, {2.253614902e0, 6.504445553e0}, + {3.845989466e0, 6.194067478e0}, {4.372282982e0, 6.005141735e0}, + {3.022813082e0, 5.748742580e0}, {2.294099092e0, 5.721753120e0}, + {1.349470019e0, 5.694763660e0}, {4.561208725e0, 5.519332409e0}, + {2.617971897e0, 5.303417206e0}, {4.453251064e-1, 5.249438286e0}, + {3.130770445e0, 5.208954334e0}, {4.412766933e0, 5.100996494e0}, + {2.860876560e0, 5.074007034e0}, {3.549106359e0, 5.074007034e0}, + {4.183357060e-1, 5.047018051e0}, {7.691979408e-1, 4.966049671e0}, + {4.021420956e0, 4.939060211e0}, {2.307593822e0, 4.831102848e0}, + {1.281996489e0, 4.763628960e0}, {2.590982437e0, 4.750134468e0}, + {1.389954090e0, 4.372282982e0}, {2.388561964e0, 4.358788490e0}, + {2.307593822e0, 4.007925987e0}, {1.389954090e0, 1.781300426e0}, + {2.307593822e0, 1.754310966e0}, {1.322480559e0, 1.295491219e0}, + {2.429046154e0, 1.295491219e0}, {2.914855480e0, 1.120060086e0}, + {9.716184139e-1, 1.093070745e0}, {3.238728046e-1, 9.716184139e-1}, + {3.886473656e0, 9.716184139e-1}, {1.970226288e0, 8.096820116e-1}, + {2.159152031e0, 8.096820116e-1}, {1.093070745e0, 7.961873412e-1}, + {3.103781223e0, 7.961873412e-1}, {3.238728046e-1, 7.557032704e-1}, + {3.886473656e0, 7.557032704e-1}, +}; +static const uint32_t logoIndices5[] = { + 3, 2, 4, 4, 8, 10, 3, 4, 10, 7, 3, 11, 3, 10, 11, 11, 10, 13, 11, 13, 16, + 1, 0, 5, 5, 0, 6, 6, 9, 14, 6, 14, 15, 5, 6, 15, 5, 15, 17, 17, 15, 18, + 10, 8, 12, 12, 8, 17, 12, 17, 19, 17, 18, 19, 19, 18, 20, 19, 20, 21, 21, 20, 22, + 22, 20, 23, 22, 23, 24, 24, 23, 25, 24, 25, 26, 26, 25, 27, 27, 25, 28, 27, 28, 29, + 30, 27, 31, 27, 29, 31, 31, 29, 33, 33, 29, 35, 30, 31, 32, 30, 32, 34, 30, 34, 36, +}; +static const uint32_t logoOutline5[] = { + 3, 2, 2, 4, 4, 8, 7, 3, 11, 7, 10, 13, 13, 16, 16, 11, 1, 0, 5, + 1, 0, 6, 6, 9, 9, 14, 14, 15, 17, 5, 15, 18, 12, 10, 8, 17, 19, 12, + 18, 20, 21, 19, 22, 21, 20, 23, 24, 22, 23, 25, 26, 24, 27, 26, 25, 28, 28, + 29, 30, 27, 33, 31, 29, 35, 35, 33, 31, 32, 32, 34, 34, 36, 36, 30, +}; +static const LogoMesh logoMeshes[] = { + {logoVertices0, 52, logoIndices0, 52, logoOutline0, 52}, + {logoVertices1, 48, logoIndices1, 48, logoOutline1, 48}, + {logoVertices2, 48, logoIndices2, 48, logoOutline2, 48}, + {logoVertices3, 28, logoIndices3, 24, logoOutline3, 28}, + {logoVertices4, 32, logoIndices4, 32, logoOutline4, 32}, + {logoVertices5, 37, logoIndices5, 35, logoOutline5, 37}, +}; +#endif diff --git a/c/testbed/examples2d/voxels2.c b/c/testbed/examples2d/voxels2.c new file mode 100644 index 000000000..bda08b0db --- /dev/null +++ b/c/testbed/examples2d/voxels2.c @@ -0,0 +1,82 @@ +/* Port of examples2d/voxels2.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbVoxels2(Testbed *testbed) { + R2World *world = r2NewWorld(); + const int fallingObjects = (int)tbSetting( + testbed, "Falling objects: 0 Ball, 1 Cuboid, 2 Capsule, 3 Mixed", 3, 0, 3, 1); + const R2Real voxelSizeY = tbSetting(testbed, "Voxel size y", 1, .5, 2, 0); + const R2Vector voxelSize = r2Vector(1, voxelSizeY); + const int testCcd = (int)tbSetting(testbed, "Test CCD", 0, 0, 1, 1); + const int nx = 50; + for (int i = 0; i < nx; ++i) { + for (int j = 0; j < 10; ++j) { + R2RigidBodyDesc rb = r2DynamicRigidBodyDesc(); + rb.position.translation = r2Vector(i * 2.0 - nx / 2.0, 20 + j * 2); + rb.canSleep = !testbed->noSleep; + if (testCcd) { + rb.linvel = r2Vector(0, -1000); + rb.ccdEnabled = 1; + } + const int type = fallingObjects == 3 ? j % 3 : fallingObjects; + R2ColliderDesc co; + switch (type) { + case 0: + co = r2BallColliderDesc(.5); + break; + case 1: + co = r2CuboidColliderDesc(r2Vector(.5, .5)); + break; + case 2: + co = r2CapsuleYColliderDesc(.5, .5); + break; + } + R2RigidBodyHandle rbHandle = r2InsertRigidBody(world, &rb); + r2InsertCollider(rbHandle, &co); + } + } + const R2Vector polyline[] = {{0, 0}, {0, 10}, {7, 4}, {14, 10}, + {14, 0}, {13, 7}, {7, 2}, {1, 7}}; + uint32_t indices[16]; + for (uint32_t i = 0; i < 8; ++i) { + indices[2 * i] = i; + indices[2 * i + 1] = (i + 1) % 8; + } + R2SharedShape *shape = + r2VoxelizedMeshSharedShape((R2VectorView){polyline, TB_COUNT(polyline)}, + (R2SurfaceElementView){(const R2Edge *)indices, 8}, .2); + { + R2RigidBodyDesc rigidBody = r2FixedRigidBodyDesc(); + rigidBody.position.translation = r2Vector(-20, -10); + rigidBody.canSleep = !testbed->noSleep; + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + R2RigidBodyHandle rigidBodyHandle = r2InsertRigidBody(world, &rigidBody); + r2InsertCollider(rigidBodyHandle, &collider); + } + r2FreeSharedShape(shape); + R2Vector voxels[300]; + for (size_t i = 0; i < TB_COUNT(voxels); ++i) { + const R2Real y = fmax(-.5, fmin(.5, sin(i / 20.0))) * 20; + voxels[i] = r2Vector((i - 125.0) * voxelSize.x / 2, y * voxelSize.y); + } + shape = r2VoxelsSharedShapeFromPoints(voxelSize, (R2VectorView){voxels, TB_COUNT(voxels)}); + R2ColliderDesc collider = r2DefaultColliderDesc(); + collider.shape.kind = R2_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + r2InsertColliderWithoutParent(world, &collider); + + r2FreeSharedShape(shape); + tbCamera2(testbed, 0, 20, 17); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r2Step(world, NULL, NULL); + } + } + r2FreeWorld(world); +} diff --git a/c/testbed/examples3d/b3d_joint_grid.c b/c/testbed/examples3d/b3d_joint_grid.c new file mode 100644 index 000000000..4c21cf8e1 --- /dev/null +++ b/c/testbed/examples3d/b3d_joint_grid.c @@ -0,0 +1,60 @@ +/* Port of examples3d/b3d_joint_grid.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB3dJointGrid(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + r3SetGravity(world, r3Vector(0, -10, 0)); + R3RigidBodyHandle *handles = calloc(1, 10000 * sizeof(*handles)); + if (!handles) { + abort(); + } + for (int k = 0; k < 100; k++) { + for (int i = 0; i < 100; i++) { + int fixed = i == 0; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = fixed ? R3_FIXED : R3_DYNAMIC; + rigidBody.position.translation = r3Vector(k, -i, 0); + if (!fixed) { + rigidBody.canSleep = 0; + } + R3ColliderDesc collider = r3BallColliderDesc(0.4); + R3RigidBodyHandle handle; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + for (int dir = 0; dir < 2; dir++) { + if (dir ? k > 0 : i > 0) { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_SPHERICAL_AXES; + joint.localFrame1.translation = + dir ? r3Vector(0.5, 0, 0) : r3Vector(0, -0.5, 0); + joint.localFrame2.translation = + dir ? r3Vector(-0.5, 0, 0) : r3Vector(0, 0.5, 0); + r3InsertImpulseJoint(handles[k * 100 + i - (dir ? 100 : 1)], handle, + &joint); + } + } + handles[k * 100 + i] = handle; + } + } + /* Set up the viewer. */ + tbCamera(testbed, 50, -25, 90, 50, -50, 0); + free(handles); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/b3d_junkyard.c b/c/testbed/examples3d/b3d_junkyard.c new file mode 100644 index 000000000..4b5a5affa --- /dev/null +++ b/c/testbed/examples3d/b3d_junkyard.c @@ -0,0 +1,114 @@ +/* Port of examples3d/b3d_junkyard.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static void createCylinder(R3Real height, R3Real radius, R3Real yOffset, size_t sides, + R3Vector *points) { + const R3Real deltaAlpha = 2 * R3_PI / sides; + R3Real alpha = 0; + for (size_t i = 0; i < sides; ++i) { + const R3Real sinA = sin(alpha), cosA = cos(alpha); + points[2 * i] = r3Vector(radius * cosA, yOffset, radius * sinA); + points[2 * i + 1] = r3Vector(radius * cosA, yOffset + height, radius * sinA); + alpha += deltaAlpha; + } +} + +static void createRock(R3Real radius, R3Vector points[10]) { + const R3Real phi = (1 + sqrt(5)) / 2; + const R3Real theta = 2 * R3_PI / phi; + const R3Real deltaSin = sin(theta), deltaCos = cos(theta); + R3Real c = 1, s = 0; + for (int i = 0; i < 10; ++i) { + const R3Real z = 1 - (2.0 * i + 1) / 10; + const R3Real radiusXy = sqrt(1 - z * z); + points[i] = r3Vector(radius * radiusXy * c, radius * radiusXy * s, radius * z); + const R3Real c0 = c, s0 = s; + c = deltaCos * c0 - deltaSin * s0; + s = deltaSin * c0 + deltaCos * s0; + } +} + +void tbB3dJunkyard(Testbed *testbed) { + R3World *world = r3NewWorld(); + r3SetGravity(world, r3Vector(0, -10, 0)); + R3RigidBodyHandle ground; + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(120, 1, 120)); + ground = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(ground, &collider); + } + { + R3ColliderDesc wall = r3CuboidColliderDesc(r3Vector(1, 8, 50)); + wall.position.translation = r3Vector(-50, 8, 0); + r3InsertCollider(ground, &wall); + } + { + R3ColliderDesc wall = r3CuboidColliderDesc(r3Vector(1, 8, 50)); + wall.position.translation = r3Vector(50, 8, 0); + r3InsertCollider(ground, &wall); + } + { + R3ColliderDesc wall = r3CuboidColliderDesc(r3Vector(50, 8, 1)); + wall.position.translation = r3Vector(0, 8, -50); + r3InsertCollider(ground, &wall); + } + { + R3ColliderDesc wall = r3CuboidColliderDesc(r3Vector(50, 8, 1)); + wall.position.translation = r3Vector(0, 8, 50); + r3InsertCollider(ground, &wall); + } + R3Vector rockPoints[10]; + createRock(1.5, rockPoints); + R3SharedShape *rock = r3ConvexHullSharedShape((R3VectorView){rockPoints, 10}); + for (int y = 0; y < 24; ++y) { + for (int x = 0; x <= 20; ++x) { + for (int z = 0; z <= 20; ++z) { + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-40 + 4 * x, 4 * y + 25, -40 + 4 * z); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = rock; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + } + r3FreeSharedShape(rock); + const R3Real radius = 35; + R3Vector pusherHull[32]; + createCylinder(24, 4, 0, 16, pusherHull); + R3RigidBodyHandle pusher; + { + R3RigidBodyDesc rigidBody = r3KinematicPositionBasedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(radius, 0, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetConvexHull(&collider.shape, (R3VectorView){pusherHull, 32}); + pusher = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(pusher, &collider); + } + tbCamera(testbed, 0, 90, 125, 0, 0, 0); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + R3Real degrees = 0; + const R3Real timeStep = 1.0 / 60.0, omega = -6; + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + degrees += omega * timeStep; + const R3Real rad = degrees * R3_PI / 180; + + r3RigidBody_SetNextKinematicTranslation( + pusher, r3Vector(radius * cos(rad), 0, radius * sin(rad))); + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/b3d_large_pyramid.c b/c/testbed/examples3d/b3d_large_pyramid.c new file mode 100644 index 000000000..5e4fcdc00 --- /dev/null +++ b/c/testbed/examples3d/b3d_large_pyramid.c @@ -0,0 +1,45 @@ +/* Port of examples3d/b3d_large_pyramid.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB3dLargePyramid(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + r3SetGravity(world, r3Vector(0, -10, 0)); + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(400, 1, 400)); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &boxCollider); + for (int i = 0; i < 200; i++) { + for (int j = i; j < 200; j++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector((i + 1) * 0.5 + j - i - 100, (2 * i + 1) * 0.5, 0); + rigidBody.canSleep = 0; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + collider.density = 100; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera(testbed, 0, 40, 110, 0, 20, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/b3d_large_world.c b/c/testbed/examples3d/b3d_large_world.c new file mode 100644 index 000000000..f5a649ed8 --- /dev/null +++ b/c/testbed/examples3d/b3d_large_world.c @@ -0,0 +1,55 @@ +/* Port of examples3d/b3d_large_world.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB3dLargeWorld(Testbed *testbed) { + R3World *world = r3NewWorld(); + /* One million parentless floor colliders, matching the Rust benchmark. */ + r3SetGravity(world, r3Vector(0, -10, 0)); + const R3Real cell = 10; + const int grid = 1000, spheres = 100, dropInterval = 5; + const R3Real halfSpan = .5 * cell * grid; + for (int i = 0; i < grid; ++i) { + const R3Real x = -halfSpan + (i + .5) * cell; + for (int j = 0; j < grid; ++j) { + const R3Real z = -halfSpan + (j + .5) * cell; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(.5 * cell, .25, .5 * cell)); + collider.position.translation = r3Vector(x, 0, z); + r3InsertColliderWithoutParent(world, &collider); + } + } + tbCamera(testbed, 0, 60, 250, 0, 0, 0); + + tbSetWorld(testbed, world); + int side = 1; + while (side * side < spheres) { + ++side; + } + int stepCount = 0, dropped = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + if (dropped < spheres && stepCount > 0 && stepCount % dropInterval == 0) { + const int gi = dropped % side, gj = dropped / side; + const R3Real inset = .1 * 2 * halfSpan; + const R3Real usable = 2 * halfSpan - 2 * inset; + const R3Real x = -halfSpan + inset + (gi + .5) * (usable / side); + const R3Real z = -halfSpan + inset + (gj + .5) * (usable / side); + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x, 1.5, z); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3BallColliderDesc(.5); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + ++dropped; + } + ++stepCount; + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/b3d_many_pyramids.c b/c/testbed/examples3d/b3d_many_pyramids.c new file mode 100644 index 000000000..368eb5927 --- /dev/null +++ b/c/testbed/examples3d/b3d_many_pyramids.c @@ -0,0 +1,50 @@ +/* Port of examples3d/b3d_many_pyramids.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB3dManyPyramids(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + r3SetGravity(world, r3Vector(0, -10, 0)); + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(77, 1, 77)); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &boxCollider); + for (int row = 0; row < 14; row++) { + for (int col = 0; col < 14; col++) { + for (int i = 0; i < 10; i++) { + for (int j = i; j < 10; j++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector((i + 1) * 0.5 + j - i - 77 + col * 11 + 1 - 0.5, (2 * i + 1) * 0.5, + -76 + row * (152.0 / 13)); + rigidBody.canSleep = 0; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + collider.density = 100; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 0, 30, 120, 0, 5, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/b3d_rain.c b/c/testbed/examples3d/b3d_rain.c new file mode 100644 index 000000000..3e7a22d8c --- /dev/null +++ b/c/testbed/examples3d/b3d_rain.c @@ -0,0 +1,451 @@ +/* Port of examples3d/b3d_rain.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +enum { + GRID_COUNT = 10, + GROUP_SIZE = 3, + BONE_COUNT = 14, + PELVIS = 0, + SPINE_01 = 1, + SPINE_02 = 2, + SPINE_03 = 3, + NECK = 4, + HEAD = 5, + THIGH_L = 6, + CALF_L = 7, + THIGH_R = 8, + CALF_R = 9, + UPPER_ARM_L = 10, + LOWER_ARM_L = 11, + UPPER_ARM_R = 12, + LOWER_ARM_R = 13 +}; + +const R3Real GRID_SIZE = 15; + +enum JointKind { Spherical, Revolute }; + +typedef struct BoneDef { + int parent; + R3Vector refP; + R3Rotation refQ; + R3Vector capA, capB; + R3Real capR; + enum JointKind kind; + R3Vector frameAP; + R3Rotation frameAQ; + R3Vector frameBP; + R3Rotation frameBQ; + R3Real swingDeg, twistDeg[2]; + int filtered; +} BoneDef; + +static const BoneDef boneDefs[BONE_COUNT] = { + // pelvis + {.parent = -1, + .refP = {0.0, 0.932087, -0.051708}, + .refQ = {0.739169, 0.0, 0.0, 0.673520}, + .capA = {0.07, 0.0, -0.08}, + .capB = {-0.07, 0.0, -0.08}, + .capR = 0.13, + .kind = Spherical, + .frameAP = {0, 0, 0}, + .frameAQ = {0.0, 0.0, 0.0, 1.0}, + .frameBP = {0, 0, 0}, + .frameBQ = {0.0, 0.0, 0.0, 1.0}, + .swingDeg = 0.0, + .twistDeg = {0.0, 0.0}, + .filtered = 0}, + // spine_01 + {.parent = PELVIS, + .refP = {0.0, 1.113505, -0.03481}, + .refQ = {0.739973, 0.0, 0.0, 0.672637}, + .capA = {0.06, 0.0, -0.052264}, + .capB = {-0.06, 0.0, -0.052264}, + .capR = 0.12, + .kind = Spherical, + .frameAP = {0.0, 0.0, -0.182204}, + .frameAQ = {-0.999999, 0.0, 0.0, 0.001194}, + .frameBP = {0.0, 0.0, -0.007736}, + .frameBQ = {-1.0, 0.0, 0.0, 0.0}, + .swingDeg = 25.0, + .twistDeg = {-15.0, 15.0}, + .filtered = 1}, + // spine_02 + {.parent = SPINE_01, + .refP = {0.0, 1.194336, -0.027087}, + .refQ = {0.703611, 0.0, 0.0, 0.710586}, + .capA = {0.08, -0.015133, -0.091801}, + .capB = {-0.08, -0.015133, -0.091801}, + .capR = 0.10, + .kind = Spherical, + .frameAP = {0.0, 0.0, -0.088935}, + .frameAQ = {-0.998619, 0.0, 0.0, -0.052540}, + .frameBP = {0.0, 0.0, -0.008199}, + .frameBQ = {-1.0, 0.0, 0.0, 0.0}, + .swingDeg = 25.0, + .twistDeg = {-15.0, 15.0}, + .filtered = 0}, + // spine_03 + {.parent = SPINE_02, + .refP = {0.0, 1.31043, -0.028232}, + .refQ = {0.669856, 0.000001, -0.000001, 0.742491}, + .capA = {0.11, -0.039753, -0.13}, + .capB = {-0.11, -0.039753, -0.13}, + .capR = 0.145, + .kind = Spherical, + .frameAP = {0.0, 0.0, -0.124298}, + .frameAQ = {-0.998921, 0.000001, -0.000001, -0.046434}, + .frameBP = {0.0, 0.0, 0.0}, + .frameBQ = {-1.0, 0.0, -0.000001, 0.0}, + .swingDeg = 15.0, + .twistDeg = {-10.0, 10.0}, + .filtered = 0}, + // neck + {.parent = SPINE_03, + .refP = {0.0, 1.575582, -0.055837}, + .refQ = {0.879922, 0.0, 0.0, 0.475118}, + .capA = {-0.000001, 0.0, -0.02}, + .capB = {0.0, -0.005, -0.08}, + .capR = 0.07, + .kind = Spherical, + .frameAP = {0.000001, -0.000259, -0.266585}, + .frameAQ = {-0.942192, -0.000001, 0.0, 0.335074}, + .frameBP = {0.0, 0.0, 0.0}, + .frameBQ = {-1.0, 0.0, -0.000001, 0.0}, + .swingDeg = 45.0, + .twistDeg = {-15.0, 15.0}, + .filtered = 0}, + // head + {.parent = NECK, + .refP = {0.0, 1.653348, -0.003241}, + .refQ = {0.750288, 0.0, 0.0, 0.661111}, + .capA = {-0.000001, 0.016892, -0.05869}, + .capB = {0.0, -0.003629, -0.115072}, + .capR = 0.0975, + .kind = Spherical, + .frameAP = {0.0, 0.001321, -0.093873}, + .frameAQ = {-0.974301, 0.0, 0.0, -0.225251}, + .frameBP = {0.0, 0.001268, -0.005104}, + .frameBQ = {-1.0, 0.0, 0.0, 0.0}, + .swingDeg = 15.0, + .twistDeg = {-15.0, 15.0}, + .filtered = 0}, + // thigh_l + {.parent = PELVIS, + .refP = {0.090416, 0.986104, -0.035090}, + .refQ = {-0.703287, -0.070715, 0.053866, 0.705327}, + .capA = {0.023719, 0.006008, -0.039068}, + .capB = {-0.064492, -0.004664, -0.424718}, + .capR = 0.09, + .kind = Spherical, + .frameAP = {0.05, 0.011537, -0.055325}, + .frameAQ = {-0.714896, -0.022305, -0.698361, -0.026790}, + .frameBP = {0.0, 0.0, 0.0}, + .frameBQ = {-0.002064, 0.758987, 0.017046, 0.650880}, + .swingDeg = 10.0, + .twistDeg = {-60.0, 40.0}, + .filtered = 1}, + // calf_l + {.parent = THIGH_L, + .refP = {0.101198, 0.527027, -0.037374}, + .refQ = {-0.653328, -0.066860, 0.058582, 0.751838}, + .capA = {0.001778, 0.0, 0.009841}, + .capB = {-0.078577, 0.014707, -0.41816}, + .capR = 0.075, + .kind = Revolute, + .frameAP = {-0.069989, 0.000253, -0.453844}, + .frameAQ = {-0.000677, 0.760087, 0.105674, 0.641171}, + .frameBP = {0.0, 0.0, 0.0}, + .frameBQ = {-0.044589, 0.765540, 0.053368, 0.639619}, + .swingDeg = 0.0, + .twistDeg = {-5.0, 45.0}, + .filtered = 0}, + // thigh_r + {.parent = PELVIS, + .refP = {-0.090416, 0.986104, -0.03509}, + .refQ = {-0.703287, 0.070715, -0.053865, 0.705326}, + .capA = {-0.023719, 0.006008, -0.039068}, + .capB = {0.064492, -0.004664, -0.424718}, + .capR = 0.09, + .kind = Spherical, + .frameAP = {-0.05, 0.011537, -0.055326}, + .frameAQ = {-0.039089, -0.714094, 0.043177, 0.697623}, + .frameBP = {0.0, 0.0, 0.0}, + .frameBQ = {0.758805, -0.019886, -0.651012, -0.001759}, + .swingDeg = 10.0, + .twistDeg = {-30.0, 60.0}, + .filtered = 1}, + // calf_r + {.parent = THIGH_R, + .refP = {-0.101198, 0.527027, -0.037373}, + .refQ = {-0.653327, 0.06686, -0.058582, 0.751839}, + .capA = {-0.001820, 0.0, 0.010071}, + .capB = {0.077883, 0.014825, -0.418047}, + .capR = 0.075, + .kind = Revolute, + .frameAP = {0.069988, 0.000253, -0.453844}, + .frameAQ = {0.760086, -0.000675, -0.641171, -0.105676}, + .frameBP = {0.0, 0.0, 0.0}, + .frameBQ = {0.765540, -0.044589, -0.639619, -0.053368}, + .swingDeg = 0.0, + .twistDeg = {-45.0, 5.0}, + .filtered = 0}, + // upper_arm_l + {.parent = SPINE_03, + .refP = {0.20378, 1.484275, -0.115897}, + .refQ = {0.143082, 0.695980, -0.690130, 0.13733}, + .capA = {0.0, 0.0, 0.0}, + .capB = {-0.091118, 0.037775, 0.229719}, + .capR = 0.075, + .kind = Spherical, + .frameAP = {0.203780, -0.069369, -0.181921}, + .frameAQ = {-0.278486, 0.445600, -0.097014, 0.845266}, + .frameBP = {0.0, 0.0, 0.0}, + .frameBQ = {-0.201396, -0.001586, 0.901850, 0.382234}, + .swingDeg = 60.0, + .twistDeg = {-5.0, 5.0}, + .filtered = 0}, + // lower_arm_l + {.parent = UPPER_ARM_L, + .refP = {0.305614, 1.242908, -0.117599}, + .refQ = {0.165048, 0.563437, -0.802002, 0.109959}, + .capA = {0.0, 0.0, 0.0}, + .capB = {-0.142406, 0.039392, 0.261092}, + .capR = 0.05, + .kind = Revolute, + .frameAP = {-0.095482, 0.039584, 0.240723}, + .frameAQ = {0.512487, -0.180629, 0.839474, 0.003742}, + .frameBP = {0.0, 0.0, 0.0}, + .frameBQ = {0.503803, -0.029831, 0.858168, 0.094017}, + .swingDeg = 0.0, + .twistDeg = {-5.0, 60.0}, + .filtered = 0}, + // upper_arm_r + {.parent = SPINE_03, + .refP = {-0.20378, 1.484276, -0.115899}, + .refQ = {0.143083, -0.695978, 0.690132, 0.137329}, + .capA = {0.0, 0.0, 0.0}, + .capB = {0.091118, 0.037775, 0.229718}, + .capR = 0.075, + .kind = Spherical, + .frameAP = {-0.203779, -0.069371, -0.181922}, + .frameAQ = {-0.253621, -0.414842, 0.106962, 0.867261}, + .frameBP = {0.0, 0.0, 0.0}, + .frameBQ = {-0.201397, 0.001587, -0.901850, 0.382233}, + .swingDeg = 60.0, + .twistDeg = {-5.0, 5.0}, + .filtered = 0}, + // lower_arm_r + {.parent = UPPER_ARM_R, + .refP = {-0.305614, 1.242907, -0.117599}, + .refQ = {0.165048, -0.563437, 0.802002, 0.109959}, + .capA = {0.0, 0.0, 0.0}, + .capB = {0.142406, 0.039392, 0.261092}, + .capR = 0.05, + .kind = Revolute, + .frameAP = {0.095484, 0.039585, 0.240723}, + .frameAQ = {-0.180627, 0.512487, -0.003744, -0.839474}, + .frameBP = {0.0, 0.0, 0.0}, + .frameBQ = {-0.029831, 0.503803, -0.094017, -0.858169}, + .swingDeg = 0.0, + .twistDeg = {-60.0, 5.0}, + .filtered = 0}, +}; + +typedef struct HumanHandles { + R3RigidBodyHandle bones[BONE_COUNT]; +} HumanHandles; + +static R3Rotation quat(R3Rotation q) { + const R3Real norm = sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); + return (R3Rotation){q.x / norm, q.y / norm, q.z / norm, q.w / norm}; +} + +static HumanHandles createHuman(Testbed *testbed, R3World *world, R3Vector position, + R3Real frictionTorque, R3Real hertz, R3Real damping, + uint32_t groupBit) { + HumanHandles human; + const uint32_t bit = UINT32_C(1) << (groupBit % 24); + const R3InteractionGroups groups = {bit, ~bit, 0}; + (void)frictionTorque; /* The Rust port also omits the friction torque clamp. */ + for (size_t i = 0; i < BONE_COUNT; ++i) { + const BoneDef *def = &boneDefs[i]; + R3RigidBodyDesc body = r3DynamicRigidBodyDesc(); + body.canSleep = !testbed->noSleep; + body.position = r3Pose(r3VectorAdd(position, def->refP), quat(def->refQ)); + human.bones[i] = r3InsertRigidBody(world, &body); + + R3ColliderDesc collider = r3CapsuleColliderDesc(def->capA, def->capB, def->capR); + collider.density = 1000; + if (def->filtered) { + collider.collisionGroups = groups; + } + r3InsertCollider(human.bones[i], &collider); + } + const R3Real omega = 2 * R3_PI * hertz, stiffness = omega * omega, + motorDamping = 2 * damping * omega; + for (size_t i = 0; i < BONE_COUNT; ++i) { + const BoneDef *def = &boneDefs[i]; + if (def->parent < 0) { + continue; + } + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = 7; + joint.localFrame1 = r3Pose(def->frameAP, quat(def->frameAQ)); + joint.localFrame2 = r3Pose(def->frameBP, quat(def->frameBQ)); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_X, def->twistDeg[0] * R3_PI / 180, + def->twistDeg[1] * R3_PI / 180); + joint.contactsEnabled = 0; + r3JointDesc_SetMotorModel(&joint, R3_AXIS_ANG_X, 0); + r3JointDesc_SetMotorPosition(&joint, R3_AXIS_ANG_X, 0, stiffness, motorDamping); + if (def->kind == Spherical) { + const R3Real swing = def->swingDeg * R3_PI / 180; + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_Y, -swing, swing); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_Z, -swing, swing); + r3JointDesc_SetMotorModel(&joint, R3_AXIS_ANG_Y, 0); + r3JointDesc_SetMotorModel(&joint, R3_AXIS_ANG_Z, 0); + r3JointDesc_SetMotorPosition(&joint, R3_AXIS_ANG_Y, 0, stiffness, motorDamping); + r3JointDesc_SetMotorPosition(&joint, R3_AXIS_ANG_Z, 0, stiffness, motorDamping); + } else { + joint.lockedAxes = 7 | 16 | 32; + } + r3InsertImpulseJoint(human.bones[def->parent], human.bones[i], &joint); + } + return human; +} + +typedef struct RainState { + HumanHandles groups[GRID_COUNT * GRID_COUNT][GROUP_SIZE]; + size_t columnCount, columnIndex; +} RainState; + +static void createGroup(Testbed *testbed, R3World *world, RainState *state, size_t row, + size_t col) { + const size_t groupIndex = row * GRID_COUNT + col; + const R3Real span = GRID_COUNT * GRID_SIZE, groupDistance = span / GRID_COUNT; + R3Real x = -.5 * span + groupDistance * (col + .5); + const R3Real y = 20; + const R3Real z = -.5 * span + groupDistance * (row + .5); + for (size_t i = 0; i < GROUP_SIZE; ++i) { + state->groups[groupIndex][i] = + createHuman(testbed, world, r3Vector(x, y, z), 5, 1, .7, (uint32_t)groupIndex); + x += .75; + } +} + +static void destroyGroup(RainState *state, size_t row, size_t col) { + for (size_t i = 0; i < GROUP_SIZE; ++i) { + for (size_t j = 0; j < BONE_COUNT; ++j) { + r3RemoveRigidBody(state->groups[row * GRID_COUNT + col][i].bones[j], 1); + } + } +} + +static void stepRain(Testbed *testbed, R3World *world, RainState *state, size_t stepCount) { + if (stepCount & 0x2f) { + return; + } + if (state->columnCount < GRID_COUNT) { + const size_t col = state->columnCount; + for (size_t row = 0; row < GRID_COUNT; ++row) { + createGroup(testbed, world, state, row, col); + } + ++state->columnCount; + } else { + const size_t col = state->columnIndex; + for (size_t row = 0; row < GRID_COUNT; ++row) { + destroyGroup(state, row, col); + createGroup(testbed, world, state, row, col); + } + state->columnIndex = (state->columnIndex + 1) % GRID_COUNT; + } +} + +void tbB3dRain(Testbed *testbed) { + R3World *world = r3NewWorld(); + r3SetGravity(world, r3Vector(0, -10, 0)); + /* Flat grid and torus, reused in each static cell. */ + R3Vector gridVerts[81]; + uint32_t gridIndices[8 * 8 * 6]; + size_t next = 0; + R3Real x = -GRID_SIZE / 2; + for (size_t ix = 0; ix <= 8; ++ix) { + R3Real z = -GRID_SIZE / 2; + for (size_t iz = 0; iz <= 8; ++iz) { + gridVerts[ix * 9 + iz] = r3Vector(x, 0, z); + z += GRID_SIZE / 8; + } + x += GRID_SIZE / 8; + } + for (uint32_t ix = 0; ix < 8; ++ix) { + for (uint32_t iz = 0; iz < 8; ++iz) { + uint32_t i1 = iz + 9 * ix, i2 = i1 + 1, i3 = i2 + 9, i4 = i3 - 1; + uint32_t pair[] = {i1, i2, i3, i3, i4, i1}; + memcpy(&gridIndices[next], pair, sizeof(pair)); + next += 6; + } + } + R3Vector torusVerts[16 * 16]; + uint32_t torusIndices[16 * 16 * 6]; + next = 0; + for (size_t radial = 0; radial < 16; ++radial) { + for (size_t tubular = 0; tubular < 16; ++tubular) { + const R3Real u = tubular / 16.0 * 2 * R3_PI, v = radial / 16.0 * 2 * R3_PI; + torusVerts[radial * 16 + tubular] = r3Vector( + (.25 * GRID_SIZE + cos(v)) * cos(u), (.25 * GRID_SIZE + cos(v)) * sin(u), sin(v)); + } + } + for (uint32_t radial = 0; radial < 16; ++radial) { + for (uint32_t tubular = 0; tubular < 16; ++tubular) { + const uint32_t r2 = (radial + 1) % 16, t2 = (tubular + 1) % 16; + uint32_t i1 = radial * 16 + tubular, i2 = radial * 16 + t2, i3 = r2 * 16 + t2, + i4 = r2 * 16 + tubular; + uint32_t pair[] = {i1, i2, i3, i3, i4, i1}; + memcpy(&torusIndices[next], pair, sizeof(pair)); + next += 6; + } + } + const R3Real span = GRID_SIZE * GRID_COUNT; + x = -.5 * span + .5 * GRID_SIZE; + for (size_t i = 0; i < GRID_COUNT; ++i) { + R3Real z = -.5 * span + .5 * GRID_SIZE; + for (size_t j = 0; j < GRID_COUNT; ++j) { + R3RigidBodyDesc builder = r3FixedRigidBodyDesc(); + builder.position.translation = r3Vector(x, 0, z); + R3RigidBodyHandle cell = r3InsertRigidBody(world, &builder); + + R3ColliderDesc collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetTrimesh(&collider.shape, (R3VectorView){gridVerts, 81}, + (R3TriangleView){(const R3Triangle *)gridIndices, 128}, 0); + r3InsertCollider(cell, &collider); + + collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetTrimesh(&collider.shape, (R3VectorView){torusVerts, 256}, + (R3TriangleView){(const R3Triangle *)torusIndices, 512}, 0); + r3InsertCollider(cell, &collider); + + z += GRID_SIZE; + } + x += GRID_SIZE; + } + RainState *state = calloc(1, sizeof(*state)); + if (!state) { + abort(); + } + tbCamera(testbed, 70, 30, 70, 0, 5, 0); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + size_t stepCount = 0; + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + stepRain(testbed, world, state, stepCount); + ++stepCount; + r3Step(world, NULL, NULL); + } + } + free(state); + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/b3d_trees.c b/c/testbed/examples3d/b3d_trees.c new file mode 100644 index 000000000..23658c365 --- /dev/null +++ b/c/testbed/examples3d/b3d_trees.c @@ -0,0 +1,121 @@ +/* Port of examples3d/b3d_trees.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static void createCylinder(R3Real height, R3Real radius, R3Real yOffset, size_t sides, + R3Vector *points) { + const R3Real deltaAlpha = 2 * R3_PI / sides; + R3Real alpha = 0; + for (size_t i = 0; i < sides; ++i) { + const R3Real sinA = sin(alpha), cosA = cos(alpha); + points[2 * i] = r3Vector(radius * cosA, yOffset, radius * sinA); + points[2 * i + 1] = r3Vector(radius * cosA, yOffset + height, radius * sinA); + alpha += deltaAlpha; + } +} + +static void createWaveMesh(size_t xCount, size_t zCount, R3Real cellWidth, R3Real amplitude, + R3Real rowFrequency, R3Real columnFrequency, R3Vector *vertices, + uint32_t *indices) { + const R3Real omegaZ = 2 * R3_PI * rowFrequency * cellWidth; + const R3Real omegaX = 2 * R3_PI * columnFrequency * cellWidth; + R3Real x = -.5 * cellWidth * xCount; + for (size_t ix = 0; ix <= xCount; ++ix) { + R3Real z = -.5 * cellWidth * zCount; + for (size_t iz = 0; iz <= zCount; ++iz) { + vertices[ix * (zCount + 1) + iz] = + r3Vector(x, amplitude * sin(omegaX * ix) * sin(omegaZ * iz), z); + z += cellWidth; + } + x += cellWidth; + } + size_t next = 0; + for (size_t ix = 0; ix < xCount; ++ix) { + for (size_t iz = 0; iz < zCount; ++iz) { + const uint32_t i1 = iz + (zCount + 1) * ix, i2 = i1 + 1; + const uint32_t i3 = i2 + zCount + 1, i4 = i3 - 1; + indices[next++] = i1; + indices[next++] = i2; + indices[next++] = i3; + indices[next++] = i3; + indices[next++] = i4; + indices[next++] = i1; + } + } +} + +void b3dTreesRun(Testbed *testbed, size_t scale) { + R3World *world = r3NewWorld(); + r3SetGravity(world, r3Vector(0, -10, 0)); + const size_t xCount = scale * 150, zCount = scale * 200; + const size_t vertexCount = (xCount + 1) * (zCount + 1), triangleCount = 2 * xCount * zCount; + R3Vector *vertices = malloc(vertexCount * sizeof(*vertices)); + uint32_t *indices = malloc(triangleCount * 3 * sizeof(*indices)); + if (!vertices || !indices) { + abort(); + } + createWaveMesh(xCount, zCount, 1.0 / scale, .4, .05, .1, vertices, indices); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetTrimesh(&collider.shape, (R3VectorView){vertices, vertexCount}, + (R3TriangleView){(const R3Triangle *)indices, triangleCount}, 0); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + free(vertices); + free(indices); + R3SharedShape *hulls[22] = {0}; + R3Real y = 1, r = .75; + const R3Real l = 1.5; + for (size_t i = 0; i < TB_COUNT(hulls); ++i) { + R3Vector points[12]; + createCylinder(l + 2 * r, r, y - r, 6, points); + hulls[i] = r3ConvexHullSharedShape((R3VectorView){points, TB_COUNT(points)}); + y += l + 2 * r; + r *= .95; + } + + R3Real angularVelocity = -.5, z = -70; + const int bodyCount = 50; + for (int bodyIndex = 0; bodyIndex < bodyCount; ++bodyIndex) { + const R3Vector position = r3Vector(0, 1, z); + R3RigidBodyDesc builder = r3DynamicRigidBodyDesc(); + builder.position.translation = position; + builder.canSleep = !testbed->noSleep; + R3RigidBodyHandle handle = r3InsertRigidBody(world, &builder); + + for (size_t i = 0; i < TB_COUNT(hulls); ++i) { + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = hulls[i]; + collider.density = 1; + collider.friction = .9; + r3InsertCollider(handle, &collider); + } + const R3Real velocityScale = .5 + .5 * bodyIndex / bodyCount; + + R3Vector center = r3RigidBody_CenterOfMass(handle); + const R3Vector omega = r3Vector(0, 0, velocityScale * angularVelocity); + const R3Vector velocity = r3VectorCross(omega, r3VectorSub(center, position)); + r3RigidBody_SetAngvel(handle, omega, 1); + r3RigidBody_SetLinvel(handle, velocity, 1); + z += 3; + angularVelocity = -angularVelocity; + } + for (size_t i = 0; i < TB_COUNT(hulls); ++i) { + r3FreeSharedShape(hulls[i]); + } + tbCamera(testbed, 0, 30, 140, 0, 15, 0); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/b3d_trees_run100.c b/c/testbed/examples3d/b3d_trees_run100.c new file mode 100644 index 000000000..271bbea77 --- /dev/null +++ b/c/testbed/examples3d/b3d_trees_run100.c @@ -0,0 +1,7 @@ +/* Port of examples3d/b3d_trees.rs: run100. */ +#include "testbed.h" +void b3dTreesRun(Testbed *testbed, size_t scale); + +void tbB3dTreesRun100(Testbed *testbed) { + b3dTreesRun(testbed, 1); +} diff --git a/c/testbed/examples3d/b3d_trees_run25.c b/c/testbed/examples3d/b3d_trees_run25.c new file mode 100644 index 000000000..5e29a08e9 --- /dev/null +++ b/c/testbed/examples3d/b3d_trees_run25.c @@ -0,0 +1,7 @@ +/* Port of examples3d/b3d_trees.rs: run25. */ +#include "testbed.h" +void b3dTreesRun(Testbed *testbed, size_t scale); + +void tbB3dTreesRun25(Testbed *testbed) { + b3dTreesRun(testbed, 4); +} diff --git a/c/testbed/examples3d/b3d_trees_run50.c b/c/testbed/examples3d/b3d_trees_run50.c new file mode 100644 index 000000000..e2595fa52 --- /dev/null +++ b/c/testbed/examples3d/b3d_trees_run50.c @@ -0,0 +1,7 @@ +/* Port of examples3d/b3d_trees.rs: run50. */ +#include "testbed.h" +void b3dTreesRun(Testbed *testbed, size_t scale); + +void tbB3dTreesRun50(Testbed *testbed) { + b3dTreesRun(testbed, 2); +} diff --git a/c/testbed/examples3d/b3d_washer.c b/c/testbed/examples3d/b3d_washer.c new file mode 100644 index 000000000..36d3ddfcd --- /dev/null +++ b/c/testbed/examples3d/b3d_washer.c @@ -0,0 +1,79 @@ +/* Port of examples3d/b3d_washer.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbB3dWasher(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + r3SetGravity(world, r3Vector(0, -10, 0)); + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(60, 1, 60)); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &boxCollider); + + R3RigidBodyDesc rigidBody = r3KinematicVelocityBasedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 21, 0); + rigidBody.angvel = r3Vector(0, 0, R3_PI / 180 * 25); + rigidBody.linvel = r3Vector(0.001, -0.002, 0); + R3RigidBodyHandle washer; + rigidBody.canSleep = !testbed->noSleep; + washer = r3InsertRigidBody(world, &rigidBody); + R3Real angle = R3_PI / 18; + R3Vector u1 = r3Vector(1, 0, 0); + for (int i = 0; i < 36; i++) { + R3Vector u2 = i == 35 ? r3Vector(1, 0, 0) + : r3Vector(cos(angle) * u1.x - sin(angle) * u1.y, + sin(angle) * u1.x + cos(angle) * u1.y, 0); + R3Vector a1 = r3Vector(cos(angle * 0.1) * u1.x + sin(angle * 0.1) * u1.y, + -sin(angle * 0.1) * u1.x + cos(angle * 0.1) * u1.y, 0); + R3Vector a2 = r3Vector(cos(angle * 0.1) * u2.x - sin(angle * 0.1) * u2.y, + sin(angle * 0.1) * u2.x + cos(angle * 0.1) * u2.y, 0); + for (int part = 0; part < (i % 9 == 0 ? 2 : 1); part++) { + R3Vector vertices[8]; + R3Vector left = part ? u1 : a1; + R3Vector right = part ? u2 : a2; + R3Real rmin = part ? 14 : 16; + R3Real rmax = part ? 16 : 18; + for (int z = 0; z < 2; z++) { + R3Vector off = r3Vector(0, 0, z ? 10 : -10); + vertices[z * 4] = r3VectorAdd(r3VectorScale(left, rmin), off); + vertices[z * 4 + 1] = r3VectorAdd(r3VectorScale(left, rmax), off); + vertices[z * 4 + 2] = r3VectorAdd(r3VectorScale(right, rmin), off); + vertices[z * 4 + 3] = r3VectorAdd(r3VectorScale(right, rmax), off); + } + R3ColliderDesc collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetConvexHull(&collider.shape, (R3VectorView){vertices, TB_COUNT(vertices)}); + r3InsertCollider(washer, &collider); + } + u1 = u2; + } + for (int i = 0; i < 20; i++) { + for (int j = 0; j < 20; j++) { + for (int k = 0; k < 20; k++) { + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.2, 0.2, 0.2)); + collider.density = 1000; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-8 + i * 0.8, 13 + j * 0.8, -8 + k * 0.8); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 60, 35, 60, 0, 15, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/ccd3.c b/c/testbed/examples3d/ccd3.c new file mode 100644 index 000000000..99469f140 --- /dev/null +++ b/c/testbed/examples3d/ccd3.c @@ -0,0 +1,100 @@ +/* Port of examples3d/ccd3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static int same(R3RigidBodyHandle handle, R3RigidBodyHandle handleB) { + return handle.world == handleB.world && handle.index == handleB.index && + handle.generation == handleB.generation; +} + +void tbCcd3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle ground = {0}; + R3RigidBodyHandle sensor = {0}; + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(50, 0.1, 50)); + groundBody.canSleep = !testbed->noSleep; + ground = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(ground, &boxCollider); + + for (int wall = 0; wall < 5; wall++) { + for (int row = 0; row < 2; row++) { + int k = 0; + for (int i = 0; i < 8; i++) { + for (int j = i; j < 8; j++) { + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(wall * 6, i + 0.6, i + (j - i) * 2 + row * 20 - 8); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 1)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + + k++; + tbBodyColor(testbed, handle, k % 2 ? 131.0f / 255 : 1, k % 2 ? 1 : 131.0f / 255, + 244.0f / 255, 1); + } + } + } + } + for (int i = 0; i < 2; i++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-20, 2.6, i * 20); + rigidBody.linvel = r3Vector(1000, 0, 0); + rigidBody.ccdEnabled = 1; + R3ColliderDesc collider = r3BallColliderDesc(1); + collider.density = 10; + if (!i) { + collider.isSensor = 1; + collider.activeEvents = R3_COLLISION_EVENTS; + } + R3RigidBodyHandle handleH; + rigidBody.canSleep = !testbed->noSleep; + handleH = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handleH, &collider); + if (!i) { + sensor = handleH; + } else { + tbBodyColor(testbed, handleH, 0.2, 0.2, 1, 1); + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + R3EventCollector *eventHandler = r3NewEventCollector(); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3EventCollector_Clear(eventHandler); + r3Step(world, NULL, eventHandler); + + size_t n = r3EventCollector_CollisionEvents(eventHandler, NULL, 0); + R3CollisionEvent *events = calloc(n ? n : 1, sizeof(*events)); + if (!events) { + abort(); + } + n = r3EventCollector_CollisionEvents(eventHandler, events, n); + for (size_t i = 0; i < n; i++) { + R3ColliderHandle colliderHandles[] = {events[i].collider1, events[i].collider2}; + for (size_t j = 0; j < 2; j++) { + R3RigidBodyHandle handle = r3Collider_Parent(colliderHandles[j]); + if (!same(handle, ground) && !same(handle, sensor)) { + tbBodyColor(testbed, handle, events[i].started ? 1 : 0.5f, + events[i].started ? 1 : 0.5f, events[i].started ? 0 : 1, 1); + } + } + } + free(events); + } + } + r3FreeEventCollector(eventHandler); + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/character_controller3.c b/c/testbed/examples3d/character_controller3.c new file mode 100644 index 000000000..85f34e048 --- /dev/null +++ b/c/testbed/examples3d/character_controller3.c @@ -0,0 +1,169 @@ +/* Port of examples3d/character_controller3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include "utils/character.h" + +void tbCharacterController3(Testbed *testbed) { + R3World *world = r3NewWorld(); + + const R3Real groundSize = 5, groundHeight = .1; + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -groundHeight, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = + r3CuboidColliderDesc(r3Vector(groundSize, groundHeight, groundSize)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -groundHeight, -groundSize); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = + r3CuboidColliderDesc(r3Vector(groundSize, groundSize, groundHeight)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Character with stronger gravity and predictive CCD for PID control. */ + R3RigidBodyHandle characterHandle; + { + R3RigidBodyDesc rigidBody = r3KinematicPositionBasedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0.5, 0); + rigidBody.gravityScale = 10; + rigidBody.softCcdPrediction = 10; + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CapsuleYColliderDesc(.3, .15); + characterHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(characterHandle, &collider); + } + tbBodyColor(testbed, characterHandle, .8f, .1f, .1f, 1); + /* Cubes. */ + const int num = 8; + const R3Real rad = .1, shift = rad * 2, centerx = shift * (num / 2), centery = rad; + for (int j = 0; j < 4; ++j) { + for (int k = 0; k < 4; ++k) { + for (int i = 0; i < num; ++i) { + const R3Real x = i * shift - centerx, y = j * shift + centery; + const R3Real z = k * shift + centerx; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x, y, z); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(rad, rad, rad)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + } + /* Stairs. */ + const R3Real stairWidth = 1, stairHeight = .1; + for (int i = 0; i < 10; ++i) { + const R3Real x = i * stairWidth / 2, y = i * stairHeight * 1.5 + 3; + { + R3ColliderDesc collider = + r3CuboidColliderDesc(r3Vector(stairWidth / 2, stairHeight / 2, stairWidth)); + collider.position.translation = r3Vector(x, y, 0); + r3InsertColliderWithoutParent(world, &collider); + } + } + /* Climbable and unclimbable slopes. */ + const R3Real slopeAngle = .2, slopeSize = 2, impossibleSlopeSize = 2; + const R3Real impossibleSlopeAngle = .6; + { + R3ColliderDesc collider = + r3CuboidColliderDesc(r3Vector(slopeSize, groundHeight, slopeSize)); + collider.position.translation = r3Vector(.1 + slopeSize, -groundHeight + .4, 0); + collider.position.rotation = r3RotationFromAxisAngle(r3Vector(0, 0, 1), slopeAngle); + r3InsertColliderWithoutParent(world, &collider); + } + { + R3ColliderDesc collider = + r3CuboidColliderDesc(r3Vector(slopeSize, groundHeight, groundSize)); + collider.position.translation = + r3Vector(.1 + slopeSize * 2 + impossibleSlopeSize - .9, -groundHeight + 1.7, 0); + collider.position.rotation = + r3RotationFromAxisAngle(r3Vector(0, 0, 1), impossibleSlopeAngle); + r3InsertColliderWithoutParent(world, &collider); + } + /* Moving platform. */ + R3RigidBodyHandle platformHandle; + { + R3RigidBodyDesc rigidBody = r3KinematicVelocityBasedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-8, 0, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(2, groundHeight, 2)); + platformHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(platformHandle, &collider); + } + /* Wavy heightfield. */ + const int nsubdivs = 20; + R3Real heights[21 * 21]; + for (int j = 0; j <= nsubdivs; ++j) { + for (int i = 0; i <= nsubdivs; ++i) { + heights[j * 21 + i] = cos(i * 10.0 / nsubdivs / 2) + cos(j * 10.0 / nsubdivs / 2); + } + } + { + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_HEIGHTFIELD; + collider.shape.heights = (R3RealView){heights, (21) * (21)}; + collider.shape.rows = 21; + collider.shape.columns = 21; + collider.shape.scale = r3Vector(10, 1, 10); + collider.shape.flags = 0; + collider.position.translation = r3Vector(-8, 5, 0); + r3InsertColliderWithoutParent(world, &collider); + } + /* Tilting dynamic body with a limited joint. */ + R3RigidBodyDesc ground = r3FixedRigidBodyDesc(); + ground.position.translation = r3Vector(0, 5, 0); + R3RigidBodyHandle groundHandle, handle; + groundHandle = r3InsertRigidBody(world, &ground); + + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 5, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 0.1, 2)); + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + } + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_X, -.3, .3); + r3InsertImpulseJoint(groundHandle, handle, &joint); + } + CharacterControlMode controlMode = CHARACTER_KINEMATIC; + R3KinematicCharacterController *controller = NULL; + R3PidController *pid = NULL; + controller = r3NewKinematicCharacterController(); + pid = r3NewPidController(); + r3KinematicCharacterController_SetSlopes(controller, impossibleSlopeAngle - .02, + impossibleSlopeAngle - .02); + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + tbSetWorld(testbed, world); + size_t stepId = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + ++stepId; + + R3Real dt = r3TimeStep(world); + const R3Vector linvel = + r3Vector(sin(stepId * dt * 2) * 2, sin(stepId * dt * 5) * 1.5, 0); + + r3RigidBody_SetLinvel(platformHandle, linvel, 1); + updateCharacter(testbed, world, &controlMode, controller, pid, characterHandle); + } + } + r3FreePidController(pid); + r3FreeKinematicCharacterController(controller); + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/collision_groups3.c b/c/testbed/examples3d/collision_groups3.c new file mode 100644 index 000000000..292b9068b --- /dev/null +++ b/c/testbed/examples3d/collision_groups3.c @@ -0,0 +1,53 @@ +/* Port of examples3d/collision_groups3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbCollisionGroups3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle floor; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(5, 0.1, 5)); + rigidBody.canSleep = !testbed->noSleep; + floor = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(floor, &boxCollider); + for (int i = 1; i <= 2; i++) { + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 0.1, 1)); + collider.position.translation = r3Vector(0, i, 0); + collider.collisionGroups = (R3InteractionGroups){(uint32_t)i, (uint32_t)i, R3_GROUPS_AND}; + R3ColliderHandle colliderHandle = r3InsertCollider(floor, &collider); + tbColliderColor(testbed, colliderHandle, 0, i == 2, i == 1, 1); + } + for (int j = 0; j < 4; j++) { + for (int i = 0; i < 8; i++) { + for (int k = 0; k < 8; k++) { + uint32_t group = k % 2 ? 2 : 1; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.1, 0.1, 0.1)); + collider.collisionGroups = (R3InteractionGroups){group, group, R3_GROUPS_AND}; + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * 0.2 - 0.8, j * 0.2 + 2.5, k * 0.2 - 0.8); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + tbBodyColor(testbed, handle, 0, group == 1, group == 2, 1); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/compound3.c b/c/testbed/examples3d/compound3.c new file mode 100644 index 000000000..a5005b62c --- /dev/null +++ b/c/testbed/examples3d/compound3.c @@ -0,0 +1,97 @@ +/* Port of examples3d/compound3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbCompound3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + /* Ground. */ + const R3Real groundSize = 50.0; + const R3Real groundHeight = 0.1; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0.0, -groundHeight, 0.0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(groundSize, groundHeight, groundSize)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + /* Create the cubes. */ + const int num = 8; + const int numy = 15; + const R3Real rad = 0.2; + const R3Real shift = rad * 4.0 + rad; + const R3Real centerx = shift * (num / 2); + const R3Real centery = shift / 2.0; + const R3Real centerz = shift * (num / 2); + R3Real offset = -2.4; + + for (int j = 0; j < numy; j++) { + for (int i = 0; i < num; i++) { + for (int k = 0; k < num; k++) { + const R3Real x = i * shift * 5.0 - centerx + offset; + const R3Real y = j * (shift * 5.0) + centery + 3.0; + const R3Real z = k * shift * 2.0 - centerz + offset; + rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x, y, z); + rigidBody.canSleep = !testbed->noSleep; + + /* First option: attach several colliders to a single rigid body. */ + if (j < numy / 2) { + R3ColliderDesc collider1 = r3CuboidColliderDesc(r3Vector(rad * 10.0, rad, rad)); + R3ColliderDesc collider2 = r3CuboidColliderDesc(r3Vector(rad, rad * 10.0, rad)); + collider2.position.translation = r3Vector(rad * 10.0, rad * 10.0, 0.0); + R3ColliderDesc collider3 = r3CuboidColliderDesc(r3Vector(rad, rad * 10.0, rad)); + collider3.position.translation = r3Vector(-rad * 10.0, rad * 10.0, 0.0); + R3RigidBodyHandle handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider1); + r3InsertCollider(handle, &collider2); + r3InsertCollider(handle, &collider3); + } else { + /* Second option: attach one collider with a compound shape. */ + const R3Pose poses[] = { + r3TranslationPose(r3Vector(0.0, 0.0, 0.0)), + r3TranslationPose(r3Vector(rad * 10.0, rad * 10.0, 0.0)), + r3TranslationPose(r3Vector(-rad * 10.0, rad * 10.0, 0.0)), + }; + R3SharedShape *shapes[3] = {NULL}; + shapes[0] = r3CuboidSharedShape(r3Vector(rad * 10.0, rad, rad)); + shapes[1] = r3CuboidSharedShape(r3Vector(rad, rad * 10.0, rad)); + shapes[2] = r3CuboidSharedShape(r3Vector(rad, rad * 10.0, rad)); + R3CompoundShapeDesc colliderParts[TB_COUNT(shapes)]; + for (size_t part = 0; part < TB_COUNT(shapes); ++part) { + colliderParts[part].pose = poses[part]; + colliderParts[part].shape = r3DefaultShapeDesc(); + colliderParts[part].shape.kind = R3_SHAPE_DESC_SHARED; + colliderParts[part].shape.sharedShape = + ((const R3SharedShape *const *)shapes)[part]; + } + collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_COMPOUND; + collider.shape.children = + (R3CompoundShapeView){colliderParts, TB_COUNT(shapes)}; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + for (size_t part = 0; part < TB_COUNT(shapes); part++) { + r3FreeSharedShape(shapes[part]); + } + } + } + } + offset -= 0.07; + } + + /* Set up the viewer. */ + tbCamera(testbed, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/convex_decomposition3.c b/c/testbed/examples3d/convex_decomposition3.c new file mode 100644 index 000000000..fb862607e --- /dev/null +++ b/c/testbed/examples3d/convex_decomposition3.c @@ -0,0 +1,7 @@ +/* Port of examples3d/convex_decomposition3.rs. */ +#include "testbed.h" +void dynamicTrimesh3RunImpl(Testbed *testbed, int useConvexDecomposition); + +void tbConvexDecomposition3(Testbed *testbed) { + dynamicTrimesh3RunImpl(testbed, 1); +} diff --git a/c/testbed/examples3d/convex_polyhedron3.c b/c/testbed/examples3d/convex_polyhedron3.c new file mode 100644 index 000000000..92c3b62aa --- /dev/null +++ b/c/testbed/examples3d/convex_polyhedron3.c @@ -0,0 +1,58 @@ +/* Port of examples3d/convex_polyhedron3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "example_math.h" + +static R3SharedShape *randomHull(Testbed *testbed) { + R3Vector points[10]; + for (size_t i = 0; i < 10; i++) { + R3Real x = exampleRandom(&testbed->randomState) * 2; + R3Real y = exampleRandom(&testbed->randomState) * 2; + R3Real z = exampleRandom(&testbed->randomState) * 2; + points[i] = r3Vector(x, y, z); + } + R3SharedShape *shape = r3RoundConvexHullSharedShape((R3VectorView){points, 10}, 0.1); + return shape; +} + +void tbConvexPolyhedron3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(40, 0.1, 40)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + for (int j = 0; j < 25; j++) { + for (int i = 0; i < 5; i++) { + for (int k = 0; k < 5; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * 2.2 - 4.4, j * 2.2 + 4.1, k * 2.2 - 4.4); + rigidBody.canSleep = !testbed->noSleep; + R3SharedShape *shape = randomHull(testbed); + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + r3FreeSharedShape(shape); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 30, 30, 30, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/damping3.c b/c/testbed/examples3d/damping3.c new file mode 100644 index 000000000..60b326091 --- /dev/null +++ b/c/testbed/examples3d/damping3.c @@ -0,0 +1,39 @@ +/* Port of examples3d/damping3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDamping3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + r3SetGravity(world, r3Vector(0, 0, 0)); + const int num = 10; + const R3Real subdiv = (R3Real)1 / num; + for (int i = 0; i < num; i++) { + R3Real x = (R3Real)sin(i * subdiv * R3_PI * 2); + R3Real y = (R3Real)cos(i * subdiv * R3_PI * 2); + R3RigidBodyDesc body = r3DynamicRigidBodyDesc(); + body.position.translation = r3Vector(x, y, 0); + body.linvel = r3Vector(x * 10, y * 10, 0); + body.angvel = r3Vector(0, 0, 100); + body.linearDamping = (i + 1) * subdiv * 10; + body.angularDamping = (num - i) * subdiv * 10; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.2, 0.2, 0.2)); + body.canSleep = !testbed->noSleep; + R3RigidBodyHandle bodyHandle = r3InsertRigidBody(world, &body); + r3InsertCollider(bodyHandle, &collider); + } + /* Set up the viewer. */ + tbCamera(testbed, 2, 2.5, 20, 2, 2.5, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_add_remove_collider3.c b/c/testbed/examples3d/debug_add_remove_collider3.c new file mode 100644 index 000000000..dff284dff --- /dev/null +++ b/c/testbed/examples3d/debug_add_remove_collider3.c @@ -0,0 +1,41 @@ +/* Port of examples3d/debug_add_remove_collider3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugAddRemoveCollider3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle groundHandle = {0}; + R3ColliderHandle groundCollider = {0}; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + groundHandle = r3InsertRigidBody(world, &rigidBody); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(3, 0.1, 0.4)); + groundCollider = r3InsertCollider(groundHandle, &boxCollider); + R3ColliderDesc collider = r3BallColliderDesc(0.1); + collider.density = 100; + R3RigidBodyDesc dynamicBody = r3DynamicRigidBodyDesc(); + dynamicBody.position.translation = r3Vector(0, 0.2, 0); + dynamicBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle dynamicBodyHandle = r3InsertRigidBody(world, &dynamicBody); + r3InsertCollider(dynamicBodyHandle, &collider); + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + + r3RemoveCollider(groundCollider, 1); + groundCollider = r3InsertCollider(groundHandle, &boxCollider); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_angular_limits3.c b/c/testbed/examples3d/debug_angular_limits3.c new file mode 100644 index 000000000..ec2cf926b --- /dev/null +++ b/c/testbed/examples3d/debug_angular_limits3.c @@ -0,0 +1,60 @@ +/* Port of examples3d/debug_angular_limits3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugAngularLimits3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + r3SetGravity(world, r3Vector(0, 0, 0)); + int multi = (int)tbSetting(testbed, "Multibody joints", 0, 0, 1, 1); + const R3Real limits[][2] = {{-45, 45}, {0, 270}, {135, 225}, {-350, 0}, {-200, 200}}; + for (int i = 0; i < 5; i++) { + for (int dir = 1; dir >= -1; dir -= 2) { + R3Vector position = r3Vector(i * 4, dir > 0 ? 0 : -4, 0); + R3RigidBodyHandle anchor; + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = position; + R3ColliderDesc collider = r3BallColliderDesc(0.2); + groundBody.canSleep = !testbed->noSleep; + anchor = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(anchor, &collider); + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(position, r3Vector(1, 0, 0)); + rigidBody.angularDamping = 3; + rigidBody.canSleep = 0; + R3RigidBodyHandle handle; + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(0.5, 0.1, 0.1)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &boxCollider); + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = r3Vector(-1, 0, 0); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_X, limits[i][0] * R3_PI / 180, + limits[i][1] * R3_PI / 180); + r3JointDesc_SetMotor(&joint, R3_AXIS_ANG_X, 0, dir * 5, 0, 20); + if (multi) { + r3InsertMultibodyJoint(anchor, handle, &joint); + } else { + r3InsertImpulseJoint(anchor, handle, &joint); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 8, -2, 25, 8, -2, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_articulations3.c b/c/testbed/examples3d/debug_articulations3.c new file mode 100644 index 000000000..e8e51dbd2 --- /dev/null +++ b/c/testbed/examples3d/debug_articulations3.c @@ -0,0 +1,70 @@ +/* Port of examples3d/debug_articulations3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugArticulations3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3Rotation rotation = r3RotationFromAxisAngle(r3Vector(0.1, 0, 0.1), (R3Real)sqrt(0.02)); + for (int n = 0; n < 2; n++) { + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(30, 0.01, 30)); + collider.position = r3Pose(r3Vector(0, n ? -3 : -3.02, 0), rotation); + if (n) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0, 0); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } else { + r3InsertColliderWithoutParent(world, &collider); + } + } + R3RigidBodyHandle handles[225]; + for (int k = 0; k < 15; k++) { + for (int i = 0; i < 15; i++) { + R3SharedShape *shape = + r3CapsuleSharedShape(r3Vector(0, 0, -0.5), r3Vector(0, 0, 0.5), 0.4); + R3RigidBodyHandle child; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = i ? R3_DYNAMIC : R3_FIXED; + rigidBody.position.translation = r3Vector(k, 0, i * 2); + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + rigidBody.canSleep = !testbed->noSleep; + child = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(child, &collider); + + if (i) { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_SPHERICAL_AXES; + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = r3Vector(0, 0, -2); + r3InsertMultibodyJoint(handles[k * 15 + i - 1], child, &joint); + } + if (k && i) { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_SPHERICAL_AXES; + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = r3Vector(-1, 0, 0); + r3InsertImpulseJoint(handles[(k - 1) * 15 + i], child, &joint); + } + handles[k * 15 + i] = child; + r3FreeSharedShape(shape); + } + } + /* Set up the viewer. */ + tbCamera(testbed, 15, 5, 42, 13, 1, 1); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_balls3.c b/c/testbed/examples3d/debug_balls3.c new file mode 100644 index 000000000..22513a21f --- /dev/null +++ b/c/testbed/examples3d/debug_balls3.c @@ -0,0 +1,40 @@ +/* Port of examples3d/debug_balls3.rs. */ +#include "testbed.h" +#include "rapier_math.h" + +void tbDebugBalls3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + for (int k = 0; k < 10; k++) { + int fixed = j == 0 || i == 0 || k == 0 || i == 9 || k == 9; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = fixed ? R3_FIXED : R3_DYNAMIC; + rigidBody.position.translation = r3Vector(i - 5, j * (fixed ? 1 : 2) + 0.5, k - 5); + rigidBody.canSleep = 0; + R3ColliderDesc collider = r3BallColliderDesc(0.5); + collider.friction = 0; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_big_colliders3.c b/c/testbed/examples3d/debug_big_colliders3.c new file mode 100644 index 000000000..135ead67d --- /dev/null +++ b/c/testbed/examples3d/debug_big_colliders3.c @@ -0,0 +1,47 @@ +/* Port of examples3d/debug_big_colliders3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugBigColliders3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3SharedShape *shape = r3HalfspaceSharedShape(r3Vector(0, 1, 0)); + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0, 0); + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + R3Real y = 0; + R3Real width = 10000; + for (int i = 0; i < 12; i++) { + R3Real height = (R3Real)fmin(0.1, width); + y += height * 4; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, y, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(width, height, width)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + width /= 5; + } + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + r3FreeSharedShape(shape); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_boxes3.c b/c/testbed/examples3d/debug_boxes3.c new file mode 100644 index 000000000..d47f111bc --- /dev/null +++ b/c/testbed/examples3d/debug_boxes3.c @@ -0,0 +1,42 @@ +/* Port of examples3d/debug_boxes3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugBoxes3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + for (int i = 0; i < 6; i++) { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(100.1, 0.1, 100.1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + for (int i = 0; i < 2; i++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(1.1, 0, 0); + rigidBody.canSleep = 0; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(2, 0.1, 1)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_chain_high_mass_ratio3.c b/c/testbed/examples3d/debug_chain_high_mass_ratio3.c new file mode 100644 index 000000000..66b0d4fcd --- /dev/null +++ b/c/testbed/examples3d/debug_chain_high_mass_ratio3.c @@ -0,0 +1,46 @@ +/* Port of examples3d/debug_chain_high_mass_ratio3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugChainHighMassRatio3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle last = {0}; + for (int i = 0; i < 17; i++) { + R3Real r = i == 16 ? 2 : 0.2; + R3Real a = (R3Real)0.2 * (R3Real)1.1; + R3Real b = r + (R3Real)0.2 * (R3Real)0.1; + R3Real z = i ? (i - 1) * 2 * a + a + b : 0; + R3RigidBodyDesc body = r3DynamicRigidBodyDesc(); + body.bodyType = i ? R3_DYNAMIC : R3_FIXED; + body.position.translation = r3Vector(0, 0, z); + body.additionalSolverIterations = 16; + R3RigidBodyHandle handle; + R3ColliderDesc collider = r3BallColliderDesc(r); + body.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &body); + r3InsertCollider(handle, &collider); + if (i) { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_SPHERICAL_AXES; + joint.localFrame1.translation = r3Vector(0, 0, i == 1 ? 0 : a); + joint.localFrame2.translation = r3Vector(0, 0, i == 1 ? -a * 2 : -b); + r3InsertImpulseJoint(last, handle, &joint); + } + last = handle; + } + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_cube_high_mass_ratio3.c b/c/testbed/examples3d/debug_cube_high_mass_ratio3.c new file mode 100644 index 000000000..822849c38 --- /dev/null +++ b/c/testbed/examples3d/debug_cube_high_mass_ratio3.c @@ -0,0 +1,51 @@ +/* Port of examples3d/debug_cube_high_mass_ratio3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugCubeHighMassRatio3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -2.2, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(2, 2, 2)); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &collider); + for (int i = 0; i < 4; i++) { + for (int side = -1; side <= 1; side += 2) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, i * 0.8, side * 0.8); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 0.2, 0.2)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + R3RigidBodyDesc dynamicBody = r3DynamicRigidBodyDesc(); + dynamicBody.position.translation = r3Vector(side * 0.8, (i + 0.5) * 0.8, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(0.2, 0.2, 1)); + dynamicBody.canSleep = !testbed->noSleep; + rigidBodyHandle = r3InsertRigidBody(world, &dynamicBody); + r3InsertCollider(rigidBodyHandle, &boxCollider); + } + } + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 2 + (4 - 0.25) * 0.2 * 4, 0); + rigidBody.additionalSolverIterations = 36; + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(2, 2, 2)); + rigidBody.canSleep = !testbed->noSleep; + groundBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(groundBodyHandle, &boxCollider); + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_cylinder3.c b/c/testbed/examples3d/debug_cylinder3.c new file mode 100644 index 000000000..d64f3a11f --- /dev/null +++ b/c/testbed/examples3d/debug_cylinder3.c @@ -0,0 +1,36 @@ +/* Port of examples3d/debug_cylinder3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugCylinder3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(100.1, 0.1, 100.1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + R3RigidBodyDesc dynamicBody = r3DynamicRigidBodyDesc(); + dynamicBody.position.translation = r3Vector(-1.5, 4.5, -1.5); + R3ColliderDesc objectCollider = r3CylinderColliderDesc(1, 1); + dynamicBody.canSleep = !testbed->noSleep; + rigidBodyHandle = r3InsertRigidBody(world, &dynamicBody); + r3InsertCollider(rigidBodyHandle, &objectCollider); + + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_deserialize3.c b/c/testbed/examples3d/debug_deserialize3.c new file mode 100644 index 000000000..4a744c4e1 --- /dev/null +++ b/c/testbed/examples3d/debug_deserialize3.c @@ -0,0 +1,50 @@ +/* Port of examples3d/debug_deserialize3.rs. */ +#include "testbed.h" +#include + +void tbDebugDeserialize3(Testbed *testbed) { + const unsigned frameId = (unsigned)tbSetting(testbed, "frame", 0, 0, 1400, 1); + const char *frameDirs = getenv("RAPIER_SNAPSHOT_DIR"); + if (!frameDirs) { + frameDirs = "/Users/sebcrozet/work/hytopia/sdk/examples/bug-demo"; + } + char path[4096]; + snprintf(path, sizeof(path), "%s/snapshot%u.bincode", frameDirs, frameId); + FILE *file = fopen(path, "rb"); + if (!file) { + snprintf(testbed->error, sizeof(testbed->error), + "Cannot open snapshot%u.bincode: %s. Set RAPIER_SNAPSHOT_DIR to the snapshot " + "directory.", + frameId, strerror(errno)); + return; + } + if (fseek(file, 0, SEEK_END)) { + fclose(file); + return; + } + long length = ftell(file); + if (length < 0 || length > 256L * 1024 * 1024 || fseek(file, 0, SEEK_SET)) { + fclose(file); + return; + } + unsigned char *bytes = malloc(length ? (size_t)length : 1); + if (!bytes) { + abort(); + } + size_t count = fread(bytes, 1, (size_t)length, file); + fclose(file); + if (count != (size_t)length) { + free(bytes); + return; + } + R3World *world = r3DeserializeRigidState(bytes, count); + free(bytes); + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + tbSetWorld(testbed, world); + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_disabled3.c b/c/testbed/examples3d/debug_disabled3.c new file mode 100644 index 000000000..1b5e29748 --- /dev/null +++ b/c/testbed/examples3d/debug_disabled3.c @@ -0,0 +1,54 @@ +/* Port of examples3d/debug_disabled3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugDisabled3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3ColliderHandle platformCollider = {0}; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -2.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(10.1, 2.1, 10.1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + R3RigidBodyHandle handle; + R3RigidBodyDesc dynamicBody = r3DynamicRigidBodyDesc(); + dynamicBody.position.translation = r3Vector(0, 5, 0); + dynamicBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &dynamicBody); + + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(5, 1, 5)); + platformCollider = r3InsertCollider(handle, &boxCollider); + + /* Set up the viewer. */ + tbCamera(testbed, 30, 4, 30, 0, 1, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + + uint64_t step = testbed->step + 1; + if (step % 250 == 0) { + R3Bool enabled = r3Collider_IsEnabled(platformCollider); + r3Collider_SetEnabled(platformCollider, !enabled); + } + if (step % 25 == 0) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 20, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_dynamic_collider_add3.c b/c/testbed/examples3d/debug_dynamic_collider_add3.c new file mode 100644 index 000000000..1cbf2791c --- /dev/null +++ b/c/testbed/examples3d/debug_dynamic_collider_add3.c @@ -0,0 +1,72 @@ +/* Port of examples3d/debug_dynamic_collider_add3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugDynamicColliderAdd3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle ballHandle = {0}; + R3RigidBodyHandle groundHandle = {0}; + R3Vector savedLinvel = {0}; + R3AngVector savedAngvel = {0}; + R3Pose savedPosition = {0}; + unsigned stepId = {0}; + R3ColliderHandle addedColliders[200] = {0}; + size_t numAddedColliders = {0}; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(20, 0.1, 0.4)); + collider.friction = 0.15; + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -0.1, 0); + groundBody.canSleep = !testbed->noSleep; + groundHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundHandle, &collider); + + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0.2, 0); + rigidBody.linvel = r3Vector(10, 0, 0); + + collider = r3BallColliderDesc(0.1); + collider.density = 100; + rigidBody.canSleep = !testbed->noSleep; + ballHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(ballHandle, &collider); + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + + stepId++; + R3ColliderDesc collider = r3BallColliderDesc(0.1 + 0.01 * stepId); + collider.density = 100; + addedColliders[numAddedColliders++] = r3InsertCollider(ballHandle, &collider); + + if (stepId == 51) { + savedLinvel = r3RigidBody_Linvel(ballHandle); + savedAngvel = r3RigidBody_Angvel(ballHandle); + savedPosition = r3RigidBody_Position(ballHandle); + } + if (stepId == 100) { + r3RigidBody_SetLinvel(ballHandle, savedLinvel, 1); + r3RigidBody_SetAngvel(ballHandle, savedAngvel, 1); + r3RigidBody_SetPosition(ballHandle, savedPosition, 1); + stepId = 51; + for (size_t i = 0; i < numAddedColliders; i++) { + r3RemoveCollider(addedColliders[i], 1); + } + numAddedColliders = 0; + } + R3ColliderDesc floor = r3CuboidColliderDesc(r3Vector(20, 0.1 + stepId * 0.01, 0.4)); + floor.friction = 0.15; + addedColliders[numAddedColliders++] = r3InsertCollider(groundHandle, &floor); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_friction3.c b/c/testbed/examples3d/debug_friction3.c new file mode 100644 index 000000000..704d5a0dc --- /dev/null +++ b/c/testbed/examples3d/debug_friction3.c @@ -0,0 +1,39 @@ +/* Port of examples3d/debug_friction3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugFriction3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3ColliderDesc floor = r3CuboidColliderDesc(r3Vector(100, 0.1, 100)); + floor.friction = 1.5; + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, 0, 0); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &floor); + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 1.1, 0); + rigidBody.position = + r3Pose(r3Vector(0, 1.1, 0), r3RotationFromAxisAngle(r3Vector(0, 1, 0), 0.3)); + rigidBody.linvel = r3Vector(sin(0.3) * 50, 0, cos(0.3) * 50); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(2, 1, 3)); + collider.friction = 1.5; + rigidBody.canSleep = !testbed->noSleep; + groundBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(groundBodyHandle, &collider); + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_infinite_fall3.c b/c/testbed/examples3d/debug_infinite_fall3.c new file mode 100644 index 000000000..4e3dcadc4 --- /dev/null +++ b/c/testbed/examples3d/debug_infinite_fall3.c @@ -0,0 +1,40 @@ +/* Port of examples3d/debug_infinite_fall3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugInfiniteFall3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, 4, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(100.1, 2.1, 100.1)); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &collider); + for (int i = 0; i < 2; i++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, i ? 2 : 7, 0); + rigidBody.canSleep = 0; + R3ColliderDesc collider = r3BallColliderDesc(1); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Set up the viewer. */ + tbCamera(testbed, 100, -10, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_internal_edges3.c b/c/testbed/examples3d/debug_internal_edges3.c new file mode 100644 index 000000000..c26a9ce43 --- /dev/null +++ b/c/testbed/examples3d/debug_internal_edges3.c @@ -0,0 +1,61 @@ +/* Port of examples3d/debug_internal_edges3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugInternalEdges3(Testbed *testbed) { + R3World *world = r3NewWorld(); + R3Real *heights = calloc(100 * 100, sizeof(*heights)); + if (!heights) { + abort(); + } + R3ColliderDesc heightfield = r3DefaultColliderDesc(); + heightfield.shape.kind = R3_SHAPE_DESC_HEIGHTFIELD; + heightfield.shape.heights = (R3RealView){heights, (100) * (100)}; + heightfield.shape.rows = 100; + heightfield.shape.columns = 100; + heightfield.shape.scale = r3Vector(60, 1, 60); + heightfield.shape.flags = R3_HEIGHTFIELD_FIX_INTERNAL_EDGES; + r3InsertColliderWithoutParent(world, &heightfield); + + free(heights); + /* Dynamic rigid bodies. */ + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(4, 0.5, 0); + rigidBody.linvel = r3Vector(0, -40, 20); + rigidBody.canSleep = 0; + R3ColliderDesc collider = r3BallColliderDesc(.5); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-3, 5, 0); + rigidBody.linvel = r3Vector(0, -4, 20); + rigidBody.canSleep = 0; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(8, 0.2, 0); + rigidBody.linvel = r3Vector(0, -4, 20); + rigidBody.canSleep = 0; + R3ColliderDesc collider = r3CylinderColliderDesc(.5, .2); + collider.position.rotation = r3RotationFromAxisAngle(r3Vector(0, 0, 1), R3_PI / 2); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_long_chain3.c b/c/testbed/examples3d/debug_long_chain3.c new file mode 100644 index 000000000..ad51e9327 --- /dev/null +++ b/c/testbed/examples3d/debug_long_chain3.c @@ -0,0 +1,42 @@ +/* Port of examples3d/debug_long_chain3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugLongChain3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle last = {0}; + const R3Real shift = (R3Real)0.2 * (R3Real)2.2; + for (int i = 0; i < 85; i++) { + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = i ? R3_DYNAMIC : R3_FIXED; + rigidBody.position.translation = r3Vector(0, 0, i * shift); + R3ColliderDesc collider = r3BallColliderDesc(0.2); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + if (i) { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_SPHERICAL_AXES; + joint.localFrame1.translation = r3Vector(0, 0, i == 1 ? 0 : shift / 2); + joint.localFrame2.translation = r3Vector(0, 0, i == 1 ? -shift : -shift / 2); + r3InsertMultibodyJoint(last, handle, &joint); + } + last = handle; + } + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_multi_collider_body3.c b/c/testbed/examples3d/debug_multi_collider_body3.c new file mode 100644 index 000000000..c1c869d14 --- /dev/null +++ b/c/testbed/examples3d/debug_multi_collider_body3.c @@ -0,0 +1,41 @@ +/* Port of examples3d/debug_multi_collider_body3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugMultiColliderBody3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + r3SetGravity(world, r3Vector(0, -9.81, 0)); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(100, 0.5, 100)); + r3InsertColliderWithoutParent(world, &boxCollider); + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 40, 0); + rigidBody.position = + r3Pose(r3Vector(0, 40, 0), r3RotationFromAxisAngle(r3Vector(1, 2, 3), (R3Real)sqrt(14))); + R3RigidBodyHandle handle; + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + for (int i = 0; i < 20; i++) { + for (int j = 0; j < 20; j++) { + for (int k = 0; k < 20; k++) { + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + collider.position.translation = r3Vector(i - 10, j - 10, k - 10); + r3InsertCollider(handle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 40, 30, 40, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_multibody_ang_motor_pos3.c b/c/testbed/examples3d/debug_multibody_ang_motor_pos3.c new file mode 100644 index 000000000..f35f28dfd --- /dev/null +++ b/c/testbed/examples3d/debug_multibody_ang_motor_pos3.c @@ -0,0 +1,44 @@ +/* Port of examples3d/debug_multibody_ang_motor_pos3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugMultibodyAngMotorPos3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + R3RigidBodyHandle handleB; + R3RigidBodyDesc dynamicBody = r3DynamicRigidBodyDesc(); + dynamicBody.position.translation = r3Vector(0, 1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + dynamicBody.canSleep = !testbed->noSleep; + handleB = r3InsertRigidBody(world, &dynamicBody); + r3InsertCollider(handleB, &boxCollider); + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_SPHERICAL_AXES; + joint.localFrame1.translation = r3Vector(0, 4, 0); + joint.localFrame2.translation = r3Vector(0, 0, 0); + for (uint32_t axis = R3_AXIS_ANG_X; axis <= R3_AXIS_ANG_Z; axis++) { + r3JointDesc_SetMotor(&joint, axis, axis == R3_AXIS_ANG_X ? 1 : 0, 0, 1000, 200); + } + r3InsertMultibodyJoint(handle, handleB, &joint); + /* Set up the viewer. */ + tbCamera(testbed, 20, 0, 0, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_pop3.c b/c/testbed/examples3d/debug_pop3.c new file mode 100644 index 000000000..50c9a2ed5 --- /dev/null +++ b/c/testbed/examples3d/debug_pop3.c @@ -0,0 +1,38 @@ +/* Port of examples3d/debug_pop3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugPop3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -10, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(10, 10, 10)); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &collider); + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0, 0); + rigidBody.canSleep = 0; + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + groundBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(groundBodyHandle, &boxCollider); + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_prismatic3.c b/c/testbed/examples3d/debug_prismatic3.c new file mode 100644 index 000000000..55d2f4bcb --- /dev/null +++ b/c/testbed/examples3d/debug_prismatic3.c @@ -0,0 +1,57 @@ +/* Port of examples3d/debug_prismatic3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugPrismatic3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(50, 0.1, 50)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + R3RigidBodyHandle box; + R3RigidBodyDesc dynamicBody = r3DynamicRigidBodyDesc(); + dynamicBody.position.translation = r3Vector(0, 5, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(1, 0.25, 1)); + dynamicBody.canSleep = !testbed->noSleep; + box = r3InsertRigidBody(world, &dynamicBody); + r3InsertCollider(box, &boxCollider); + R3Vector offsets[] = {r3Vector(1, -1, -1), r3Vector(-1, -1, -1), r3Vector(1, -1, 1), + r3Vector(-1, -1, 1)}; + for (int i = 0; i < 4; i++) { + R3RigidBodyHandle wheel; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(r3Vector(0, 5, 0), offsets[i]); + R3ColliderDesc collider = r3BallColliderDesc(0.5); + rigidBody.canSleep = !testbed->noSleep; + wheel = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(wheel, &collider); + R3JointDesc joint = r3PrismaticJointDesc(r3Vector(0, 1, 0)); + joint.localFrame1.translation = offsets[i]; + joint.localFrame2.translation = r3Vector(0, 0, 0); + r3JointDesc_SetMotor(&joint, R3_AXIS_LIN_X, 0, 0, 0.05, 0.2); + r3InsertImpulseJoint(box, wheel, &joint); + } + R3RigidBodyDesc payloadBody = r3DynamicRigidBodyDesc(); + payloadBody.position.translation = r3Vector(1, 2.6, -1); + R3ColliderDesc payloadCollider = r3CuboidColliderDesc(r3Vector(0.5, 0.1, 0.5)); + payloadBody.canSleep = !testbed->noSleep; + rigidBodyHandle = r3InsertRigidBody(world, &payloadBody); + r3InsertCollider(rigidBodyHandle, &payloadCollider); + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_rollback3.c b/c/testbed/examples3d/debug_rollback3.c new file mode 100644 index 000000000..271240cf3 --- /dev/null +++ b/c/testbed/examples3d/debug_rollback3.c @@ -0,0 +1,59 @@ +/* Port of examples3d/debug_rollback3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugRollback3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle ballHandle = {0}; + R3Vector savedLinvel = {0}; + R3AngVector savedAngvel = {0}; + R3Pose savedPosition = {0}; + unsigned stepId = {0}; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(20, 0.1, 0.4)); + collider.friction = 0.15; + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -0.1, 0); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &collider); + + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0.2, 0); + rigidBody.linvel = r3Vector(10, 0, 0); + + collider = r3BallColliderDesc(0.1); + collider.density = 100; + rigidBody.canSleep = !testbed->noSleep; + ballHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(ballHandle, &collider); + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + + stepId++; + + if (stepId == 51) { + savedLinvel = r3RigidBody_Linvel(ballHandle); + savedAngvel = r3RigidBody_Angvel(ballHandle); + savedPosition = r3RigidBody_Position(ballHandle); + } + if (stepId == 100) { + r3RigidBody_SetLinvel(ballHandle, savedLinvel, 1); + r3RigidBody_SetAngvel(ballHandle, savedAngvel, 1); + r3RigidBody_SetPosition(ballHandle, savedPosition, 1); + stepId = 51; + } + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_self_intersect3.c b/c/testbed/examples3d/debug_self_intersect3.c new file mode 100644 index 000000000..66cd94619 --- /dev/null +++ b/c/testbed/examples3d/debug_self_intersect3.c @@ -0,0 +1,247 @@ +/* Port of examples3d/debug_self_intersect3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +typedef struct Shot { + R3SoftBodyHandle handle; + R3Vector *rest; + size_t count; + R3Real speed; + int onlyParticle; +} Shot; + +static void reset(const Shot *shot) { + for (size_t i = 0; i < shot->count; ++i) { + r3SoftBody_SetParticlePosition(shot->handle, i, shot->rest[i]); + const R3Real speed = + shot->onlyParticle < 0 || i == (size_t)shot->onlyParticle ? shot->speed : 0; + r3SoftBody_SetParticleVelocity(shot->handle, i, r3Vector(0, speed, 0)); + } +} + +static void refire(const Shot shots[3]) { + for (size_t i = 0; i < 3; ++i) { + reset(&shots[i]); + } +} + +void tbDebugSelfIntersect3(Testbed *testbed) { + R3World *world = r3NewWorld(); + + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -1.5, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(8, 0.5, 4)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Slab pinned at both ends, with its top-middle vertex below the bottom face. */ + R3SoftBodyDesc builder = + r3CuboidSoftBodyDesc(r3Vector(-3, 0.15, 0), r3Vector(1.5, 0.15, 0.15), 3, 2, 2); + builder.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){5, 1}); + builder.selfContacts = 1; + builder.canSleep = !testbed->noSleep; + uint32_t *builderPins = NULL; + { + size_t count = r3SoftBodyDesc_ParticlePositions(&builder, NULL, 0); + R3Vector *positions = malloc(count * sizeof(*positions)); + builderPins = malloc(count * sizeof(*builderPins)); + if (!positions || !builderPins) { + abort(); + } + count = r3SoftBodyDesc_ParticlePositions(&builder, positions, count); + size_t builderPinsCount = 0; + for (size_t i = 0; i < count; ++i) { + if (fabs(positions[i].x + 3) > 1.4) { + builderPins[builderPinsCount++] = (uint32_t)i; + } + } + r3SoftBodyDesc_SetPinnedParticles( + &builder, (R3IndexView){(const uint32_t *)builderPins, builderPinsCount}); + + free(positions); + } + R3Vector slabPositions[12]; + size_t slabCount = r3SoftBodyDesc_ParticlePositions(&builder, slabPositions, 12); + size_t captured = 0; + R3Real closest = INFINITY; + for (size_t i = 0; i < slabCount; ++i) { + R3Real distance = r3VectorLength(r3VectorSub(slabPositions[i], r3Vector(-3, .3, .15))); + if (distance < closest) { + captured = i; + closest = distance; + } + } + R3SoftBodyHandle slab = r3InsertSoftBody(world, &builder); + free(builderPins); + + r3SoftBody_SetParticlePosition(slab, captured, r3Vector(-2.7, -.4, 0)); + const size_t nu = 8, nv = 3; + uint32_t endPins[] = {0, 1, 2, 21, 22, 23}; + Shot shots[3] = {0}; + R3SoftBodyHandle clothHandle; + const R3Vector clothOrigin = r3Vector(1.5, 0, -0.25); + R3SoftBodyDesc cloth = + r3ClothSoftBodyDesc(clothOrigin, r3Vector(0.25, 0, 0), r3Vector(0, 0, 0.25), nu, nv); + cloth.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){40, 1}); + cloth.particleMass = .02; + cloth.particleRadius = (R3OptionalReal){1, .05}; + r3SoftBodyDesc_SetPinnedParticles(&cloth, + (R3IndexView){(const uint32_t *)endPins, TB_COUNT(endPins)}); + cloth.selfContacts = 1; + cloth.canSleep = !testbed->noSleep; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){40, 1.0}; + material.bendSoftness = (R3SpringCoefficients){40, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){40, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){40, 1.0}; + cloth.material = material; + } + clothHandle = r3InsertSoftBody(world, &cloth); + + for (size_t i = 4; i < nu; ++i) { + for (size_t j = 0; j < nv; ++j) { + r3SoftBody_SetParticlePosition( + clothHandle, i * nv + j, + r3VectorAdd(clothOrigin, r3Vector(.25 * (7 - i), 0.15, .25 * j))); + } + } + r3SoftBody_SetParticlePosition(clothHandle, 5 * nv + 1, + r3VectorAdd(clothOrigin, r3Vector(.5, -.15, .25))); + R3SoftBodyHandle flickHandle; + const R3Vector flickOrigin = r3Vector(-6.5, 0, -0.25); + R3SoftBodyDesc flick = + r3ClothSoftBodyDesc(flickOrigin, r3Vector(0.25, 0, 0), r3Vector(0, 0, 0.25), nu, nv); + flick.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){40, 1}); + flick.particleMass = .02; + flick.particleRadius = (R3OptionalReal){1, .05}; + r3SoftBodyDesc_SetPinnedParticles(&flick, + (R3IndexView){(const uint32_t *)endPins, TB_COUNT(endPins)}); + flick.selfContacts = 1; + flick.canSleep = !testbed->noSleep; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){40, 1.0}; + material.bendSoftness = (R3SpringCoefficients){40, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){40, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){40, 1.0}; + flick.material = material; + } + flickHandle = r3InsertSoftBody(world, &flick); + + for (size_t i = 4; i < nu; ++i) { + for (size_t j = 0; j < nv; ++j) { + r3SoftBody_SetParticlePosition( + flickHandle, i * nv + j, + r3VectorAdd(flickOrigin, r3Vector(.25 * (7 - i), 0.2, .25 * j))); + } + } + shots[0].handle = flickHandle; + shots[0].speed = -40; + shots[0].onlyParticle = 16; + const size_t n = 12; + const R3Real extent = .12 * (n - 1) / 2; + uint32_t edgePins[144]; + size_t edgeCount = 0; + for (uint32_t k = 0; k < n * n; ++k) { + size_t i = k / n, j = k % n; + if (i == 0 || j == 0 || i == n - 1 || j == n - 1) { + edgePins[edgeCount++] = k; + } + } + R3SoftBodyDesc net = r3ClothSoftBodyDesc(r3Vector(5 - extent, 1.2, -extent), + r3Vector(0.12, 0, 0), r3Vector(0, 0, 0.12), n, n); + net.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){40, 1}); + net.particleMass = .02; + net.particleRadius = (R3OptionalReal){1, .05}; + r3SoftBodyDesc_SetPinnedParticles(&net, (R3IndexView){(const uint32_t *)edgePins, edgeCount}); + net.selfContacts = 1; + net.canSleep = !testbed->noSleep; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){40, 1.0}; + material.bendSoftness = (R3SpringCoefficients){40, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){40, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){40, 1.0}; + net.material = material; + } + r3InsertSoftBody(world, &net); + + R3SoftBodyDesc bullet = r3SphereSoftBodyDesc(r3Vector(5, 2.6, 0), .22, 1); + bullet.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){20, 1}); + bullet.particleMass = .05; + bullet.canSleep = !testbed->noSleep; + shots[1].handle = r3InsertSoftBody(world, &bullet); + + shots[1].speed = -60; + shots[1].onlyParticle = -1; + R3SoftBodyDesc bottomStrip = r3ClothSoftBodyDesc(r3Vector(7.4, 0.3, -0.1), r3Vector(0.2, 0, 0), + r3Vector(0, 0, 0.2), 12, 2); + bottomStrip.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){40, 1}); + bottomStrip.particleMass = .02; + bottomStrip.particleRadius = (R3OptionalReal){1, .05}; + bottomStrip.selfContacts = 1; + bottomStrip.canSleep = !testbed->noSleep; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){40, 1.0}; + material.bendSoftness = (R3SpringCoefficients){40, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){40, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){40, 1.0}; + bottomStrip.material = material; + } + const uint32_t stripPins[] = {0, 1, 22, 23}; + r3SoftBodyDesc_SetPinnedParticles(&bottomStrip, (R3IndexView){(const uint32_t *)stripPins, 4}); + r3InsertSoftBody(world, &bottomStrip); + + R3SoftBodyDesc topStrip = r3ClothSoftBodyDesc(r3Vector(8.4, 1.3, -1.1), r3Vector(0, 0, 0.2), + r3Vector(0.2, 0, 0), 12, 2); + topStrip.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){40, 1}); + topStrip.particleMass = .02; + topStrip.particleRadius = (R3OptionalReal){1, .05}; + topStrip.selfContacts = 1; + topStrip.canSleep = !testbed->noSleep; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){40, 1.0}; + material.bendSoftness = (R3SpringCoefficients){40, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){40, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){40, 1.0}; + topStrip.material = material; + } + shots[2].handle = r3InsertSoftBody(world, &topStrip); + + shots[2].speed = -30; + shots[2].onlyParticle = -1; + for (size_t i = 0; i < TB_COUNT(shots); ++i) { + shots[i].count = r3SoftBody_NumParticles(shots[i].handle); + shots[i].rest = malloc(shots[i].count * sizeof(*shots[i].rest)); + if (!shots[i].rest) { + abort(); + } + shots[i].count = + r3SoftBody_ParticlePositions(shots[i].handle, shots[i].rest, shots[i].count); + } + refire(shots); + tbCamera(testbed, 1, 3.5, 11, 1, .5, 0); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + R3Real t = 0; + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R3Real dt = r3TimeStep(world); + t += dt; + if (fmod(t, 4) < dt && t > 0) { + refire(shots); + } + r3Step(world, NULL, NULL); + } + } + for (size_t i = 0; i < TB_COUNT(shots); ++i) { + free(shots[i].rest); + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_shape_modification3.c b/c/testbed/examples3d/debug_shape_modification3.c new file mode 100644 index 000000000..4750a4450 --- /dev/null +++ b/c/testbed/examples3d/debug_shape_modification3.c @@ -0,0 +1,116 @@ +/* Port of examples3d/debug_shape_modification3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugShapeModification3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(20, 0.1, 20)); + collider.friction = .15; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + R3RigidBodyHandle ballHandle; + R3ColliderHandle ballCollHandle; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0.2, 0); + rigidBody.linvel = r3Vector(10, 0, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3BallColliderDesc(.1); + collider.density = 100; + ballHandle = r3InsertRigidBody(world, &rigidBody); + ballCollHandle = r3InsertCollider(ballHandle, &collider); + } + { + R3ColliderDesc staticCollider = r3BallColliderDesc(3); + staticCollider.position.translation = r3Vector(-15, 3, 18); + r3InsertColliderWithoutParent(world, &staticCollider); + } + R3SharedShape *shapes[4] = {0}; + shapes[0] = r3BallSharedShape(3); + shapes[1] = r3CuboidSharedShape(r3Vector(3, 3, 3)); + shapes[2] = r3ConeSharedShape(3, 3); + shapes[3] = r3CylinderSharedShape(3, 3); + R3ColliderDesc shapeshiftingCollider = r3DefaultColliderDesc(); + shapeshiftingCollider.shape.kind = R3_SHAPE_DESC_SHARED; + shapeshiftingCollider.shape.sharedShape = shapes[0]; + shapeshiftingCollider.position.translation = r3Vector(-15, 3, 9); + R3ColliderHandle shapeshiftingCollHandle = + r3InsertColliderWithoutParent(world, &shapeshiftingCollider); + + R3Real offset = -12; + for (int j = 0; j < 20; ++j) { + for (int i = 0; i < 8; ++i) { + for (int k = 0; k < 8; ++k) { + R3ColliderDesc collider; + switch (j % 5) { + case 0: + collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + break; + case 1: + collider = r3BallColliderDesc(1); + break; + case 2: + collider = r3RoundCylinderColliderDesc(1, 1, .1); + break; + case 3: + collider = r3ConeColliderDesc(1, 1); + break; + default: + collider = r3CapsuleYColliderDesc(1, 1); + break; + } + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * 3 - 12 + offset + 5, j * 3 + 4.5, k * 3 - 12 + offset); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + offset -= .35; + } + tbCamera(testbed, 40, 40, 40, 0, 0, 0); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + size_t shapeIdx = 0, step = 0; + const size_t snappedFrame = 51; + R3Vector linvel = {0}, angvel = {0}; + R3Pose pos = r3TranslationPose(r3Vector(0, 0, 0)); + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + ++step; + + if (step == snappedFrame) { + linvel = r3RigidBody_Linvel(ballHandle); + angvel = r3RigidBody_Angvel(ballHandle); + pos = r3RigidBody_Position(ballHandle); + } + + if (step % 50 == 0) { + shapeIdx = (shapeIdx + 1) % 4; + r3Collider_SetShape(shapeshiftingCollHandle, shapes[shapeIdx]); + } + if (step == 100) { + r3RigidBody_SetLinvel(ballHandle, linvel, 1); + r3RigidBody_SetAngvel(ballHandle, angvel, 1); + r3RigidBody_SetPosition(ballHandle, pos, 1); + step = snappedFrame; + } + + R3SharedShape *shape = r3BallSharedShape(.1 * step * 2); + r3Collider_SetShape(ballCollHandle, shape); + r3FreeSharedShape(shape); + } + } + for (size_t i = 0; i < TB_COUNT(shapes); ++i) { + r3FreeSharedShape(shapes[i]); + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_sleeping_kinematic3.c b/c/testbed/examples3d/debug_sleeping_kinematic3.c new file mode 100644 index 000000000..ba2a3733f --- /dev/null +++ b/c/testbed/examples3d/debug_sleeping_kinematic3.c @@ -0,0 +1,61 @@ +/* Port of examples3d/debug_sleeping_kinematic3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugSleepingKinematic3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle handles[2] = {0}; + for (int i = 0; i < 2; i++) { + R3RigidBodyDesc rigidBody = r3KinematicVelocityBasedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, i ? 0 : 2.3, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(5, 0.5, 5)); + rigidBody.canSleep = !testbed->noSleep; + handles[i] = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handles[i], &collider); + } + /* Set up the viewer. */ + tbCamera(testbed, 10, 5, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + + uint64_t step = testbed->step + 1; + R3Real dt = r3TimeStep(world); + R3Real time = (R3Real)step * dt; + if (!testbed->noSleep && (step == 500 || step == 1000 || step > 1500)) { + for (int i = 0; i < 2; i++) { + R3Bool sleeping = r3RigidBody_IsSleeping(handles[i]); + if (sleeping != 0 != (step != 1000)) { + snprintf(testbed->error, sizeof(testbed->error), + "kinematic sleeping assertion failed at step %llu", + (unsigned long long)step); + fprintf(stderr, "%s\n", testbed->error); + abort(); + } + } + } + if (step == 1000) { + for (int i = 0; i < 2; i++) { + r3RigidBody_SetLinvel(handles[i], r3Vector(0, 0, 0), 1); + } + } + if (step >= 500 && step < 1000) { + if (step == 500) { + r3RigidBody_SetLinvel(handles[1], r3Vector(0, 0.01, 0), 1); + } + + r3RigidBody_SetLinvel(handles[0], r3Vector(0, cos(time * 2), sin(time) * 2), + 1); + } + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_thin_cube_on_mesh3.c b/c/testbed/examples3d/debug_thin_cube_on_mesh3.c new file mode 100644 index 000000000..76f9a1962 --- /dev/null +++ b/c/testbed/examples3d/debug_thin_cube_on_mesh3.c @@ -0,0 +1,43 @@ +/* Port of examples3d/debug_thin_cube_on_mesh3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugThinCubeOnMesh3(Testbed *testbed) { + R3World *world = r3NewWorld(); + R3Real *heights = calloc(2 * 2, sizeof(*heights)); + if (!heights) { + abort(); + } + R3ColliderDesc heightfield = r3DefaultColliderDesc(); + heightfield.shape.kind = R3_SHAPE_DESC_HEIGHTFIELD; + heightfield.shape.heights = (R3RealView){heights, (2) * (2)}; + heightfield.shape.rows = 2; + heightfield.shape.columns = 2; + heightfield.shape.scale = r3Vector(50, 1, 50); + heightfield.shape.flags = R3_HEIGHTFIELD_FIX_INTERNAL_EDGES; + r3InsertColliderWithoutParent(world, &heightfield); + + free(heights); + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 5, 0); + rigidBody.position.rotation = r3RotationFromAxisAngle(r3Vector(.5, 0, .5), sqrt(.5)); + rigidBody.linvel = r3Vector(0, -100, 0); + rigidBody.softCcdPrediction = 10; + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(5, 0.015, 5)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_triangle3.c b/c/testbed/examples3d/debug_triangle3.c new file mode 100644 index 000000000..fc3a7c299 --- /dev/null +++ b/c/testbed/examples3d/debug_triangle3.c @@ -0,0 +1,46 @@ +/* Port of examples3d/debug_triangle3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugTriangle3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3SharedShape *shape = + r3TriangleSharedShape(r3Vector(-10, 0, -10), r3Vector(10, 0, -10), r3Vector(0, 0, 10)); + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, 0, 0); + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &collider); + + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(1.1, 0.01, 0); + rigidBody.canSleep = 0; + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(20, 0.1, 1)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + groundBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(groundBodyHandle, &boxCollider); + + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + r3FreeSharedShape(shape); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_trimesh3.c b/c/testbed/examples3d/debug_trimesh3.c new file mode 100644 index 000000000..b9c562831 --- /dev/null +++ b/c/testbed/examples3d/debug_trimesh3.c @@ -0,0 +1,48 @@ +/* Port of examples3d/debug_trimesh3.rs. */ +#include "testbed.h" +#include "rapier_math.h" +#include "rapier_helpers.h" + +void tbDebugTrimesh3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3Vector vertices[] = {r3Vector(-0.5, 0, -0.5), r3Vector(0.5, 0, -0.5), + r3Vector(0.5, 0, 0.5), r3Vector(-0.5, 0, 0.5), + r3Vector(-0.5, -0.5, -0.5), r3Vector(0.5, -0.5, -0.5), + r3Vector(0.5, -0.5, 0.5), r3Vector(-0.5, -0.5, 0.5)}; + R3Triangle triangles[] = {{0, 2, 1}, {0, 3, 2}, {4, 5, 6}, {4, 6, 7}, {0, 4, 7}, {0, 7, 3}, + {1, 6, 5}, {1, 2, 6}, {3, 7, 2}, {2, 7, 6}, {0, 1, 5}, {0, 5, 4}}; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 35, 0); + rigidBody.canSleep = 0; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 2, 1)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + R3RigidBodyHandle ground; + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.canSleep = !testbed->noSleep; + R3ColliderDesc groundCollider = r3DefaultColliderDesc(); + r3ShapeDesc_SetTrimesh(&groundCollider.shape, (R3VectorView){vertices, TB_COUNT(vertices)}, + (R3TriangleView){triangles, TB_COUNT(triangles)}, 0); + ground = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(ground, &groundCollider); + tbBodyColor(testbed, ground, 0.75, 0.75, 0.75, 1); + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/debug_two_cubes3.c b/c/testbed/examples3d/debug_two_cubes3.c new file mode 100644 index 000000000..e89147f32 --- /dev/null +++ b/c/testbed/examples3d/debug_two_cubes3.c @@ -0,0 +1,34 @@ +/* Port of examples3d/debug_two_cubes3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDebugTwoCubes3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 2, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, 0, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + groundBody.canSleep = !testbed->noSleep; + rigidBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(rigidBodyHandle, &boxCollider); + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/domino3.c b/c/testbed/examples3d/domino3.c new file mode 100644 index 000000000..04b6aa004 --- /dev/null +++ b/c/testbed/examples3d/domino3.c @@ -0,0 +1,60 @@ +/* Port of examples3d/domino3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbDomino3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(200.1, 0.1, 200.1)); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &collider); + R3Real angle = 0; + R3Real radius = 10; + int skip = 0; + for (int i = 0; i < 4000; i++) { + R3Real perimeter = 2 * R3_PI * radius; + R3Real prev = angle; + angle += 2 * R3_PI * 0.4 / perimeter; + R3Real x = (R3Real)sin(angle); + R3Real z = (R3Real)cos(angle); + int nudged = fmod(angle, 2 * R3_PI) < fmod(prev, 2 * R3_PI); + R3Real tilt = nudged || i == 3999 ? 0.2 : 0; + if (!skip) { + R3Rotation rotation = r3RotationMul(r3RotationFromAxisAngle(r3Vector(x, 0, z), tilt), + r3RotationFromAxisAngle(r3Vector(0, 1, 0), angle)); + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x * radius, 2.1, z * radius); + rigidBody.position = r3Pose(r3Vector(x * radius, 2.1, z * radius), rotation); + R3RigidBodyHandle handle; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.1, 2, 1)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + tbBodyColor(testbed, handle, i % 2 ? 0.6f : 0.7f, i % 2 ? 1 : 0.5f, i % 2 ? 0.6f : 0.9f, + 1); + } else { + skip--; + } + if (nudged) { + skip = 5; + } + radius += 1.5 / perimeter; + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/dynamic_trimesh3.c b/c/testbed/examples3d/dynamic_trimesh3.c new file mode 100644 index 000000000..a30239d1e --- /dev/null +++ b/c/testbed/examples3d/dynamic_trimesh3.c @@ -0,0 +1,110 @@ +/* Port of examples3d/dynamic_trimesh3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include "utils/obj.h" + +void dynamicTrimesh3RunImpl(Testbed *testbed, int useConvexDecomposition) { + R3World *world = r3NewWorld(); + /* Floor made of a wavy mesh. */ + R3Real heights[101 * 101]; + for (size_t j = 0; j <= 100; ++j) { + for (size_t i = 0; i <= 100; ++i) { + heights[i + j * 101] = -cos(i * .2) - cos(j * .2); + } + } + R3SharedShape *heightfield = r3HeightfieldSharedShape((R3RealView){heights, (101) * (101)}, 101, + 101, r3Vector(100, 2, 100)); + R3TriMeshData *floor = r3SharedShape_ToTrimesh(heightfield, 3, 2); + r3FreeSharedShape(heightfield); + size_t vertexCount = 0, indexCount = 0; + vertexCount = r3TriMeshData_Vertices(floor, NULL, 0); + indexCount = r3TriMeshData_Indices(floor, NULL, 0); + R3Vector *vertices = malloc(vertexCount * sizeof(*vertices)); + uint32_t *indices = malloc(indexCount * sizeof(*indices)); + if (!vertices || !indices) { + abort(); + } + vertexCount = r3TriMeshData_Vertices(floor, vertices, vertexCount); + indexCount = r3TriMeshData_Indices(floor, indices, indexCount); + R3ColliderDesc collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetTrimesh(&collider.shape, (R3VectorView){vertices, vertexCount}, + (R3TriangleView){(const R3Triangle *)indices, indexCount / 3}, + R3_TRIMESH_FIX_INTERNAL_EDGES); + r3InsertColliderWithoutParent(world, &collider); + + free(vertices); + free(indices); + r3FreeTriMeshData(floor); + + const char *geoms[] = {"camel_decimated.obj", "chair.obj", + "cup_decimated.obj", "dilo_decimated.obj", + "tstTorusModel2.obj", "feline_decimated.obj", + "genus3_decimated.obj", "hornbug.obj", + "tstTorusModel.obj", "octopus_decimated.obj", + "rabbit_decimated.obj", "rust_logo_simplified.obj", + "screwdriver_decimated.obj", "table.obj", + "tstTorusModel3.obj"}; + const size_t width = (size_t)sqrt(TB_COUNT(geoms)); + const int numDuplications = 4; + const R3Real shiftY = 8, shiftXz = 9; + for (size_t igeom = 0; igeom < TB_COUNT(geoms); ++igeom) { + ObjMesh mesh; + if (!loadObj(testbed->assetRoot, geoms[igeom], &mesh)) { + r3FreeWorld(world); + return; + } + R3Vector mins = mesh.vertices[0], maxs = mins; + for (size_t i = 1; i < mesh.vertexCount; ++i) { + R3Vector p = mesh.vertices[i]; + mins = r3Vector(fmin(mins.x, p.x), fmin(mins.y, p.y), fmin(mins.z, p.z)); + maxs = r3Vector(fmax(maxs.x, p.x), fmax(maxs.y, p.y), fmax(maxs.z, p.z)); + } + const R3Vector center = r3VectorScale(r3VectorAdd(mins, maxs), .5); + const R3Real diag = r3VectorLength(r3VectorSub(maxs, mins)); + for (size_t i = 0; i < mesh.vertexCount; ++i) { + mesh.vertices[i] = r3VectorScale(r3VectorSub(mesh.vertices[i], center), 10 / diag); + } + R3SharedShape *decomposedShape = NULL; + if (useConvexDecomposition) { + decomposedShape = r3ConvexDecompositionSharedShape( + (R3VectorView){mesh.vertices, mesh.vertexCount}, + (R3SurfaceElementView){(const R3Triangle *)mesh.indices, mesh.indexCount / 3}); + } else { + decomposedShape = r3TrimeshSharedShapeWithFlags( + (R3VectorView){mesh.vertices, mesh.vertexCount}, + (R3TriangleView){(const R3Triangle *)mesh.indices, mesh.indexCount / 3}, + R3_TRIMESH_FIX_INTERNAL_EDGES); + } + freeObj(&mesh); + for (int k = 1; k <= numDuplications; ++k) { + const R3Real x = (igeom % width) * shiftXz - numDuplications * shiftXz / 2; + const R3Real y = (igeom / width) * shiftY + 7; + const R3Real z = k * shiftXz - numDuplications * shiftXz / 2; + R3RigidBodyDesc body = r3DynamicRigidBodyDesc(); + body.position.translation = r3Vector(x, y, z); + body.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = decomposedShape; + collider.contactSkin = .1; + R3RigidBodyHandle bodyHandle = r3InsertRigidBody(world, &body); + r3InsertCollider(bodyHandle, &collider); + } + r3FreeSharedShape(decomposedShape); + } + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} + +void tbDynamicTrimesh3(Testbed *testbed) { + dynamicTrimesh3RunImpl(testbed, 0); +} diff --git a/c/testbed/examples3d/fountain3.c b/c/testbed/examples3d/fountain3.c new file mode 100644 index 000000000..311c22b54 --- /dev/null +++ b/c/testbed/examples3d/fountain3.c @@ -0,0 +1,73 @@ +/* Port of examples3d/fountain3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbFountain3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -2.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(40, 2.1, 40)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + /* Set up the viewer. */ + tbCamera(testbed, 30, 4, 30, 0, 1, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + + unsigned k = (unsigned)((testbed->step + 1) % 3); + R3ColliderDesc collider; + if (k == 0) { + collider = r3RoundCylinderColliderDesc(0.5, 0.5, 0.05); + } else { + if (k == 1) { + collider = r3ConeColliderDesc(0.5, 0.5); + } else { + collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + } + } + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 10, 0); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + size_t n = r3RigidBodyCount(world); + if (n > 2001) { + R3RigidBodyHandle *handle = malloc(n * sizeof(*handle)); + if (!handle) { + abort(); + } + n = r3RigidBodyHandles(world, handle, n); + R3Real largest = -1; + R3RigidBodyHandle remove = {NULL, UINT32_MAX, UINT32_MAX}; + for (size_t i = 0; i < n; i++) { + R3Bool dynamic = r3RigidBody_IsDynamic(handle[i]); + if (dynamic) { + R3Vector position = r3RigidBody_Translation(handle[i]); + R3Real distance = (R3Real)(fabs(position.x) + fabs(position.z)); + if (distance > largest) { + largest = distance; + remove = handle[i]; + } + } + } + free(handle); + if (remove.index != UINT32_MAX) { + r3RemoveRigidBody(remove, 1); + } + } + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/gyroscopic3.c b/c/testbed/examples3d/gyroscopic3.c new file mode 100644 index 000000000..e0075875e --- /dev/null +++ b/c/testbed/examples3d/gyroscopic3.c @@ -0,0 +1,50 @@ +/* Port of examples3d/gyroscopic3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbGyroscopic3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3Pose poses[] = {r3TranslationPose(r3Vector(0, 0, 0)), r3TranslationPose(r3Vector(0, 0.8, 0))}; + R3SharedShape *shapes[2] = {NULL}; + shapes[0] = r3CuboidSharedShape(r3Vector(2.0, 0.2, 0.2)); + shapes[1] = r3CuboidSharedShape(r3Vector(0.2, 0.4, 0.2)); + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0, 0); + rigidBody.gravityScale = 0; + rigidBody.angvel = r3Vector(0, 20, 0.1); + rigidBody.gyroscopicForcesEnabled = 1; + R3ColliderDesc collider; + R3CompoundShapeDesc colliderParts[2]; + for (size_t part = 0; part < 2; ++part) { + colliderParts[part].pose = poses[part]; + colliderParts[part].shape = r3DefaultShapeDesc(); + colliderParts[part].shape.kind = R3_SHAPE_DESC_SHARED; + colliderParts[part].shape.sharedShape = ((const R3SharedShape *const *)shapes)[part]; + } + collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_COMPOUND; + collider.shape.children = (R3CompoundShapeView){colliderParts, 2}; + + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + for (size_t part = 0; part < 2; part++) { + r3FreeSharedShape(shapes[part]); + } + /* Set up the viewer. */ + tbCamera(testbed, 8, 0, 8, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/heightfield3.c b/c/testbed/examples3d/heightfield3.c new file mode 100644 index 000000000..26afb5070 --- /dev/null +++ b/c/testbed/examples3d/heightfield3.c @@ -0,0 +1,102 @@ +/* Port of examples3d/heightfield3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbHeightfield3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3Real heights[441]; + for (int j = 0; j <= 20; j++) { + for (int i = 0; i <= 20; i++) { + heights[i + j * 21] = + i == 0 || i == 20 || j == 0 || j == 20 ? 10 : (R3Real)(sin(i * 5) + cos(j * 5)); + } + } + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0, 0); + R3ColliderDesc groundCollider = r3DefaultColliderDesc(); + groundCollider.shape.kind = R3_SHAPE_DESC_HEIGHTFIELD; + groundCollider.shape.heights = (R3RealView){heights, (21) * (21)}; + groundCollider.shape.rows = 21; + groundCollider.shape.columns = 21; + groundCollider.shape.scale = r3Vector(100, 1, 100); + groundCollider.shape.flags = 0; + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &groundCollider); + + for (int j = 0; j < 20; j++) { + for (int i = 0; i < 8; i++) { + for (int k = 0; k < 8; k++) { + R3ColliderDesc collider; + switch (j % 6) { + case 0: + collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + break; + case 1: + collider = r3BallColliderDesc(1); + break; + case 2: + collider = r3RoundCylinderColliderDesc(1, 1, 0.1); + break; + case 3: + collider = r3ConeColliderDesc(1, 1); + break; + case 4: + collider = r3CapsuleYColliderDesc(1, 1); + break; + default: { + R3Pose poses[] = {r3TranslationPose(r3Vector(0, 0, 0)), + r3TranslationPose(r3Vector(1, 0, 0)), + r3TranslationPose(r3Vector(-1, 0, 0))}; + R3SharedShape *shapes[3] = {NULL}; + shapes[0] = r3CuboidSharedShape(r3Vector(1.0, 0.5, 0.5)); + shapes[1] = r3CuboidSharedShape(r3Vector(0.5, 1.0, 0.5)); + shapes[2] = r3CuboidSharedShape(r3Vector(0.5, 1.0, 0.5)); + R3CompoundShapeDesc colliderParts[TB_COUNT(shapes)]; + for (size_t part = 0; part < TB_COUNT(shapes); ++part) { + colliderParts[part].pose = poses[part]; + colliderParts[part].shape = r3DefaultShapeDesc(); + colliderParts[part].shape.kind = R3_SHAPE_DESC_SHARED; + colliderParts[part].shape.sharedShape = + ((const R3SharedShape *const *)shapes)[part]; + } + collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_COMPOUND; + collider.shape.children = + (R3CompoundShapeView){colliderParts, TB_COUNT(shapes)}; + R3SharedShape *compoundShape = r3ShapeDesc_Build(&collider.shape); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = compoundShape; + for (size_t part = 0; part < TB_COUNT(shapes); part++) { + r3FreeSharedShape(shapes[part]); + } + break; + } + } + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(i * 3 - 12, j * 3 + 4.5, k * 3 - 12); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + if (collider.shape.kind == R3_SHAPE_DESC_SHARED) { + r3FreeSharedShape((R3SharedShape *)collider.shape.sharedShape); + } + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/inverse_kinematics3.c b/c/testbed/examples3d/inverse_kinematics3.c new file mode 100644 index 000000000..5c97be9ed --- /dev/null +++ b/c/testbed/examples3d/inverse_kinematics3.c @@ -0,0 +1,75 @@ +/* Port of examples3d/inverse_kinematics3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbInverseKinematics3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.01, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.2, 0.01, 0.2)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + + const int numSegments = 10; + R3RigidBodyDesc body = r3FixedRigidBodyDesc(); + R3RigidBodyHandle lastBody = r3InsertRigidBody(world, &body); + R3MultibodyJointHandle lastLink = {NULL, UINT32_MAX, UINT32_MAX}; + for (int i = 0; i < numSegments; ++i) { + const R3Real size = 1.0 / numSegments; + R3RigidBodyHandle newBody; + /* Sensors draw the links; IK does not require colliders. */ + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0, 0); + rigidBody.canSleep = 0; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(size / 8, size / 2, size / 8)); + collider.density = 0; + collider.isSensor = 1; + newBody = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(newBody, &collider); + } + R3JointDesc linkAb = r3SphericalJointDesc(); + linkAb.localFrame1.translation = r3Vector(0, size / 2 * (i != 0), 0); + linkAb.localFrame2.translation = r3Vector(0, -size / 2, 0); + lastLink = r3InsertMultibodyJoint(lastBody, newBody, &linkAb); + + lastBody = newBody; + } + tbCamera(testbed, 0, .5, 2.5, 0, .5, 0); + + tbSetWorld(testbed, world); + R3Real *displacements = NULL; + size_t capacity = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + if (!testbed->cursorValid) { + continue; + } + size_t ndofs = r3MultibodyJoint_Ndofs(lastLink); + if (capacity < ndofs) { + R3Real *resized = realloc(displacements, ndofs * sizeof(*resized)); + if (!resized) { + abort(); + } + displacements = resized; + capacity = ndofs; + } + memset(displacements, 0, ndofs * sizeof(*displacements)); + R3InverseKinematicsOptions options = r3DefaultInverseKinematicsOptions(); + options.constrained_axes = 7; /* Linear axes only. */ + const R3Pose target = r3TranslationPose(testbed->cursor); + r3MultibodyJoint_InverseKinematics(lastLink, &options, target, NULL, NULL, + displacements, ndofs); + r3MultibodyJoint_ApplyDisplacements(lastLink, displacements, ndofs); + } + } + free(displacements); + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/joint_motor_position3.c b/c/testbed/examples3d/joint_motor_position3.c new file mode 100644 index 000000000..dd1827894 --- /dev/null +++ b/c/testbed/examples3d/joint_motor_position3.c @@ -0,0 +1,60 @@ +/* Port of examples3d/joint_motor_position3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbJointMotorPosition3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle ground; + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, 0, 0); + groundBody.canSleep = !testbed->noSleep; + ground = r3InsertRigidBody(world, &groundBody); + for (int row = 0; row < 2; row++) { + for (int num = 0; num < (row ? 8 : 9); num++) { + R3Real x = -6 + 1.5 * num; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x, row ? 4.5 : 2, 0); + rigidBody.canSleep = 0; + if (row) { + rigidBody.position = + r3Pose(r3Vector(x, 4.5, 0), r3RotationFromAxisAngle(r3Vector(0, 0, 1), R3_PI)); + } + R3RigidBodyHandle handle; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.1, 0.5, 0.1)); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + joint.localFrame1.translation = r3Vector(x, row ? 5 : 1.5, 0); + joint.localFrame2.translation = r3Vector(0, -0.5, 0); + R3Real angle = -R3_PI + R3_PI / 4 * num; + if (row) { + r3JointDesc_SetMotor(&joint, R3_AXIS_ANG_X, 0, 1.5, 0, 30); + r3JointDesc_SetMotorMaxForce(&joint, R3_AXIS_ANG_X, 100); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_X, -R3_PI, angle); + } else { + r3JointDesc_SetMotor(&joint, R3_AXIS_ANG_X, angle, 0, 1000, 150); + } + r3InsertImpulseJoint(ground, handle, &joint); + } + } + r3SetGravity(world, r3Vector(0, 0, 0)); + /* Set up the viewer. */ + tbCamera(testbed, 15, 5, 42, 13, 1, 1); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/joints3.c b/c/testbed/examples3d/joints3.c new file mode 100644 index 000000000..691b9a4b3 --- /dev/null +++ b/c/testbed/examples3d/joints3.c @@ -0,0 +1,486 @@ +/* Port of examples3d/joints3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include + +static void createPrismaticJoints(Testbed *testbed, R3World *world, R3Vector origin, size_t num, + int useArticulations) { + R3RigidBodyHandle currParent; + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = origin; + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(.4, .4, .4)); + currParent = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(currParent, &collider); + } + for (size_t i = 0; i < num; ++i) { + R3RigidBodyHandle currChild; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(origin.x, origin.y, origin.z + (i + 1) * 2); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(.4, .4, .4)); + currChild = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(currChild, &collider); + } + const R3Vector axis = r3VectorNormalize(r3Vector(i % 2 == 0 ? 1 : -1, 1, 0)); + R3JointDesc prism = r3PrismaticJointDesc(axis); + prism.localFrame1.translation = r3Vector(0, 0, 0); + prism.localFrame2.translation = r3Vector(0, 0, -2); + r3JointDesc_SetLimits(&prism, R3_AXIS_LIN_X, -2, 2); + if (useArticulations) { + r3InsertMultibodyJoint(currParent, currChild, &prism); + } else { + r3InsertImpulseJoint(currParent, currChild, &prism); + } + + currParent = currChild; + } +} + +static void createActuatedPrismaticJoints(Testbed *testbed, R3World *world, R3Vector origin, + size_t num, int useArticulations) { + R3RigidBodyHandle currParent; + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = origin; + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(.4, .4, .4)); + currParent = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(currParent, &collider); + } + for (size_t i = 0; i < num; ++i) { + R3RigidBodyHandle currChild; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(origin.x, origin.y, origin.z + (i + 1) * 2); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(.4, .4, .4)); + currChild = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(currChild, &collider); + } + const R3Vector axis = r3VectorNormalize(r3Vector(i % 2 == 0 ? 1 : -1, 1, 0)); + R3JointDesc prism = r3PrismaticJointDesc(axis); + prism.localFrame1.translation = r3Vector(0, 0, 2); + prism.localFrame2.translation = r3Vector(0, 0, 0); + if (i == 0) { + r3JointDesc_SetMotorVelocity(&prism, R3_AXIS_LIN_X, 2, 1e5); + r3JointDesc_SetLimits(&prism, R3_AXIS_LIN_X, -2, 5); + r3JointDesc_SetMotorMaxForce(&prism, R3_AXIS_LIN_X, 100); + } else if (i == 1) { + r3JointDesc_SetLimits(&prism, R3_AXIS_LIN_X, -FLT_MAX, 5); + r3JointDesc_SetMotorVelocity(&prism, R3_AXIS_LIN_X, 6, 1e3); + r3JointDesc_SetMotorMaxForce(&prism, R3_AXIS_LIN_X, 100); + } else { + r3JointDesc_SetMotorPosition(&prism, R3_AXIS_LIN_X, 2, 1e3, 1e2); + r3JointDesc_SetMotorMaxForce(&prism, R3_AXIS_LIN_X, 60); + } + if (useArticulations) { + r3InsertMultibodyJoint(currParent, currChild, &prism); + } else { + r3InsertImpulseJoint(currParent, currChild, &prism); + } + + currParent = currChild; + } +} + +static void createRevoluteJoints(Testbed *testbed, R3World *world, R3Vector origin, size_t num, + int useArticulations) { + R3RigidBodyHandle currParent; + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(origin.x, origin.y, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(.4, .4, .4)); + currParent = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(currParent, &collider); + } + for (size_t i = 0; i < num; ++i) { + const R3Real z = origin.z + i * 4 + 2; + const R3Vector positions[] = {{origin.x, origin.y, z}, + {origin.x + 2, origin.y, z}, + {origin.x + 2, origin.y, z + 2}, + {origin.x, origin.y, z + 2}}; + R3RigidBodyHandle handles[4]; + for (size_t k = 0; k < 4; ++k) { + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = positions[k]; + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.4, 0.4, 0.4)); + handles[k] = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handles[k], &collider); + } + } + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + joint.localFrame2.translation = r3Vector(0, 0, -2); + if (useArticulations) { + r3InsertMultibodyJoint(currParent, handles[0], &joint); + } else { + r3InsertImpulseJoint(currParent, handles[0], &joint); + } + } + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(1, 0, 0)); + joint.localFrame2.translation = r3Vector(-2, 0, 0); + if (useArticulations) { + r3InsertMultibodyJoint(handles[0], handles[1], &joint); + } else { + r3InsertImpulseJoint(handles[0], handles[1], &joint); + } + } + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + joint.localFrame2.translation = r3Vector(0, 0, -2); + if (useArticulations) { + r3InsertMultibodyJoint(handles[1], handles[2], &joint); + } else { + r3InsertImpulseJoint(handles[1], handles[2], &joint); + } + } + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(1, 0, 0)); + joint.localFrame2.translation = r3Vector(2, 0, 0); + if (useArticulations) { + r3InsertMultibodyJoint(handles[2], handles[3], &joint); + } else { + r3InsertImpulseJoint(handles[2], handles[3], &joint); + } + } + currParent = handles[3]; + } +} + +static void createRevoluteJointsWithLimits(Testbed *testbed, R3World *world, R3Vector origin, + int useArticulations) { + R3RigidBodyDesc groundBuilder = r3FixedRigidBodyDesc(); + groundBuilder.position.translation = origin; + R3RigidBodyHandle ground = r3InsertRigidBody(world, &groundBuilder); + + R3RigidBodyHandle body1; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(origin, r3Vector(0, 0, 0)); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(4, 0.2, 2)); + body1 = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(body1, &collider); + } + R3RigidBodyHandle body2; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(origin, r3Vector(0, 0, 6)); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(4, 0.2, 2)); + body2 = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(body2, &collider); + } + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_X, -.2, .2); + if (useArticulations) { + r3InsertMultibodyJoint(ground, body1, &joint); + } else { + r3InsertImpulseJoint(ground, body1, &joint); + } + } + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + joint.localFrame2.translation = r3Vector(0, 0, -6); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_X, -.2, .2); + if (useArticulations) { + r3InsertMultibodyJoint(body1, body2, &joint); + } else { + r3InsertImpulseJoint(body1, body2, &joint); + } + } + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(origin, r3Vector(-2, 4, 0)); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.6, 0.6, 0.6)); + collider.friction = 1; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(origin, r3Vector(2, 16, 6)); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.6, 0.6, 0.6)); + collider.friction = 1; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } +} + +static void createSphericalJointsWithLimits(Testbed *testbed, R3World *world, R3Vector origin, + int useArticulations) { + R3RigidBodyDesc groundBuilder = r3FixedRigidBodyDesc(); + groundBuilder.position.translation = origin; + R3RigidBodyHandle ground = r3InsertRigidBody(world, &groundBuilder); + + R3RigidBodyHandle body1; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(origin, r3Vector(0, 0, 3)); + rigidBody.linvel = r3Vector(20, 20, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + body1 = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(body1, &collider); + } + R3RigidBodyHandle body2; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(origin, r3Vector(0, 0, 6)); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + body2 = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(body2, &collider); + } + { + R3JointDesc joint = r3SphericalJointDesc(); + joint.localFrame2.translation = r3Vector(0, 0, -3); + r3JointDesc_SetLimits(&joint, R3_AXIS_LIN_X, -0.2, 0.2); + r3JointDesc_SetLimits(&joint, R3_AXIS_LIN_Y, -0.2, 0.2); + if (useArticulations) { + r3InsertMultibodyJoint(ground, body1, &joint); + } else { + r3InsertImpulseJoint(ground, body1, &joint); + } + } + { + R3JointDesc joint = r3SphericalJointDesc(); + joint.localFrame2.translation = r3Vector(0, 0, -3); + r3JointDesc_SetLimits(&joint, R3_AXIS_LIN_X, -0.3, 0.3); + r3JointDesc_SetLimits(&joint, R3_AXIS_LIN_Y, -0.3, 0.3); + if (useArticulations) { + r3InsertMultibodyJoint(body1, body2, &joint); + } else { + r3InsertImpulseJoint(body1, body2, &joint); + } + } +} + +static void createFixedJoints(Testbed *testbed, R3World *world, R3Vector origin, size_t num, + int useArticulations) { + R3RigidBodyHandle *bodyHandles = malloc(num * num * sizeof(*bodyHandles)); + if (!bodyHandles) { + abort(); + } + size_t count = 0; + for (size_t i = 0; i < num; ++i) { + for (size_t k = 0; k < num; ++k) { + const int fixed = i == 0 && ((k % 4 == 0 && k != num - 2) || k == num - 1); + R3RigidBodyHandle childHandle; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = fixed ? R3_FIXED : R3_DYNAMIC; + rigidBody.position.translation = r3Vector(origin.x + k, origin.y, origin.z + i); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3BallColliderDesc(.4); + childHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(childHandle, &collider); + } + if (i > 0) { + { + R3JointDesc joint = r3FixedJointDesc(); + joint.localFrame2.translation = r3Vector(0, 0, -1); + if (useArticulations) { + r3InsertMultibodyJoint(bodyHandles[count - num], childHandle, + &joint); + } else { + r3InsertImpulseJoint(bodyHandles[count - num], childHandle, &joint); + } + } + } + if (k > 0) { + { + R3JointDesc joint = r3FixedJointDesc(); + joint.localFrame2.translation = r3Vector(-1, 0, 0); + r3InsertImpulseJoint(bodyHandles[count - 1], childHandle, &joint); + } + } + bodyHandles[count++] = childHandle; + } + } + free(bodyHandles); +} + +static void createSphericalJoints(Testbed *testbed, R3World *world, size_t num, + int useArticulations) { + R3RigidBodyHandle *bodyHandles = malloc(num * num * sizeof(*bodyHandles)); + if (!bodyHandles) { + abort(); + } + size_t count = 0; + for (size_t k = 0; k < num; ++k) { + for (size_t i = 0; i < num; ++i) { + const int fixed = i == 0 && (k % 4 == 0 || k == num - 1); + R3RigidBodyHandle childHandle; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = fixed ? R3_FIXED : R3_DYNAMIC; + rigidBody.position.translation = r3Vector(k, 0, i * 2); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = + r3CapsuleColliderDesc(r3Vector(0, 0, -0.5), r3Vector(0, 0, 0.5), .4); + childHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(childHandle, &collider); + } + if (i > 0) { + { + R3JointDesc joint = r3SphericalJointDesc(); + joint.localFrame2.translation = r3Vector(0, 0, -2); + if (useArticulations) { + r3InsertMultibodyJoint(bodyHandles[count - 1], childHandle, &joint); + } else { + r3InsertImpulseJoint(bodyHandles[count - 1], childHandle, &joint); + } + } + } + if (k > 0) { + { + R3JointDesc joint = r3SphericalJointDesc(); + joint.localFrame2.translation = r3Vector(-1, 0, 0); + r3InsertImpulseJoint(bodyHandles[count - num], childHandle, &joint); + } + } + bodyHandles[count++] = childHandle; + } + } + free(bodyHandles); +} + +static void createActuatedRevoluteJoints(Testbed *testbed, R3World *world, R3Vector origin, + size_t num, int useArticulations) { + R3RigidBodyHandle parentHandle = {NULL, UINT32_MAX, UINT32_MAX}; + for (size_t i = 0; i < num; ++i) { + R3RigidBodyHandle childHandle; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = i == 0 ? R3_FIXED : R3_DYNAMIC; + rigidBody.position.translation = + r3Vector(origin.x, origin.y + (i >= 1 ? -2 : 0), origin.z + i * 2); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.8, 2.4 / (i + 1), 0.4)); + childHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(childHandle, &collider); + } + if (i > 0) { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + joint.localFrame2.translation = r3Vector(0, 0, -2); + r3JointDesc_SetMotorModel(&joint, R3_AXIS_ANG_X, 0); + if (i % 3 == 1) { + r3JointDesc_SetMotorVelocity(&joint, R3_AXIS_ANG_X, -20, 100); + } else if (i == num - 1) { + r3JointDesc_SetMotorPosition(&joint, R3_AXIS_ANG_X, R3_PI / 2, 200, 100); + } + if (i == 1) { + joint.localFrame2.translation = r3Vector(0, 2, -2); + r3JointDesc_SetMotorVelocity(&joint, R3_AXIS_ANG_X, -2, 1000); + } + if (useArticulations) { + r3InsertMultibodyJoint(parentHandle, childHandle, &joint); + } else { + r3InsertImpulseJoint(parentHandle, childHandle, &joint); + } + } + parentHandle = childHandle; + } +} + +static void createActuatedSphericalJoints(Testbed *testbed, R3World *world, R3Vector origin, + size_t num, int useArticulations) { + R3RigidBodyHandle parentHandle = {NULL, UINT32_MAX, UINT32_MAX}; + for (size_t i = 0; i < num; ++i) { + R3RigidBodyHandle childHandle; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = i == 0 ? R3_FIXED : R3_DYNAMIC; + rigidBody.position.translation = r3Vector(origin.x, origin.y, origin.z + i * 2); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CapsuleYColliderDesc(.8 / (i + 1), .4); + childHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(childHandle, &collider); + } + if (i > 0) { + R3JointDesc joint = r3SphericalJointDesc(); + joint.localFrame1.translation = r3Vector(0, 0, 2); + if (i == 1) { + r3JointDesc_SetMotorVelocity(&joint, R3_AXIS_ANG_X, 0, .1); + r3JointDesc_SetMotorVelocity(&joint, R3_AXIS_ANG_Y, .5, .1); + r3JointDesc_SetMotorVelocity(&joint, R3_AXIS_ANG_Z, -2, .1); + } else if (i == num - 1) { + r3JointDesc_SetMotorPosition(&joint, R3_AXIS_ANG_X, 0, .2, 1); + r3JointDesc_SetMotorPosition(&joint, R3_AXIS_ANG_Y, 1, .2, 1); + r3JointDesc_SetMotorPosition(&joint, R3_AXIS_ANG_Z, R3_PI / 2, .2, 1); + } + if (useArticulations) { + r3InsertMultibodyJoint(parentHandle, childHandle, &joint); + } else { + r3InsertImpulseJoint(parentHandle, childHandle, &joint); + } + } + parentHandle = childHandle; + } +} + +static void createCoupledJoints(Testbed *testbed, R3World *world, R3Vector origin, + int useArticulations) { + R3RigidBodyDesc builder = r3FixedRigidBodyDesc(); + builder.position.translation = origin; + R3RigidBodyHandle ground = r3InsertRigidBody(world, &builder); + + R3RigidBodyHandle body1; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = origin; + rigidBody.linvel = r3Vector(5, 5, 5); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + body1 = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(body1, &collider); + } + { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = 0; + r3JointDesc_SetLimits(&joint, R3_AXIS_LIN_X, -3, 3); + r3JointDesc_SetLimits(&joint, R3_AXIS_LIN_Y, 0, 3); + joint.coupledAxes = 2 | 4; + if (useArticulations) { + r3InsertMultibodyJoint(ground, body1, &joint); + } else { + r3InsertImpulseJoint(ground, body1, &joint); + } + } +} + +void joints3Run(Testbed *testbed, int useArticulations) { + R3World *world = r3NewWorld(); + createPrismaticJoints(testbed, world, r3Vector(20, 5, 0), 4, useArticulations); + createActuatedPrismaticJoints(testbed, world, r3Vector(25, 5, 0), 4, useArticulations); + createRevoluteJoints(testbed, world, r3Vector(20, 0, 0), 3, useArticulations); + createRevoluteJointsWithLimits(testbed, world, r3Vector(34, 0, 0), useArticulations); + createFixedJoints(testbed, world, r3Vector(0, 10, 0), 10, useArticulations); + createActuatedRevoluteJoints(testbed, world, r3Vector(20, 10, 0), 6, useArticulations); + createActuatedSphericalJoints(testbed, world, r3Vector(13, 10, 0), 3, useArticulations); + createSphericalJoints(testbed, world, 15, useArticulations); + createSphericalJointsWithLimits(testbed, world, r3Vector(-5, 0, 0), useArticulations); + createCoupledJoints(testbed, world, r3Vector(0, 20, 0), useArticulations); + tbCamera(testbed, 15, 5, 42, 13, 1, 1); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/joints3_run_impulse_joints.c b/c/testbed/examples3d/joints3_run_impulse_joints.c new file mode 100644 index 000000000..05e9c5a6c --- /dev/null +++ b/c/testbed/examples3d/joints3_run_impulse_joints.c @@ -0,0 +1,7 @@ +/* Port of examples3d/joints3.rs: run_impulse_joints. */ +#include "testbed.h" +void joints3Run(Testbed *testbed, int useArticulations); + +void tbJoints3RunImpulseJoints(Testbed *testbed) { + joints3Run(testbed, 0); +} diff --git a/c/testbed/examples3d/joints3_run_multibody_joints.c b/c/testbed/examples3d/joints3_run_multibody_joints.c new file mode 100644 index 000000000..da2093fe4 --- /dev/null +++ b/c/testbed/examples3d/joints3_run_multibody_joints.c @@ -0,0 +1,7 @@ +/* Port of examples3d/joints3.rs: run_multibody_joints. */ +#include "testbed.h" +void joints3Run(Testbed *testbed, int useArticulations); + +void tbJoints3RunMultibodyJoints(Testbed *testbed) { + joints3Run(testbed, 1); +} diff --git a/c/testbed/examples3d/keva3.c b/c/testbed/examples3d/keva3.c new file mode 100644 index 000000000..76632d0fb --- /dev/null +++ b/c/testbed/examples3d/keva3.c @@ -0,0 +1,97 @@ +/* Port of examples3d/keva3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +/* Corresponds to keva3::buildBlock, shared by the Keva and cloth examples. */ +void buildBlock(Testbed *testbed, R3World *world, R3Vector halfExtents, R3Vector shift, int numx, + int numy, int numz) { + const R3Vector dimensions[] = { + halfExtents, + r3Vector(halfExtents.z, halfExtents.y, halfExtents.x), + }; + const R3Real blockWidth = 2.0 * halfExtents.z * numx; + const R3Real blockHeight = 2.0 * halfExtents.y * numy; + const R3Real spacing = (halfExtents.z * numx - halfExtents.x) / (numz - 1); + + for (int i = 0; i < numy; i++) { + const int oldNumx = numx; + numx = numz; + numz = oldNumx; + const R3Vector dim = dimensions[i % 2]; + const R3Real y = dim.y * i * 2.0; + + for (int j = 0; j < numx; j++) { + const R3Real x = i % 2 == 0 ? spacing * j * 2.0 : dim.x * j * 2.0; + for (int k = 0; k < numz; k++) { + const R3Real z = i % 2 == 0 ? dim.z * k * 2.0 : spacing * k * 2.0; + + /* Build the rigid body. */ + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(x + dim.x + shift.x, y + dim.y + shift.y, z + dim.z + shift.z); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(dim); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + + /* Close the top. */ + const R3Vector dim = r3Vector(halfExtents.z, halfExtents.x, halfExtents.y); + for (int i = 0; i < (int)(blockWidth / (dim.x * 2.0)); i++) { + for (int j = 0; j < (int)(blockWidth / (dim.z * 2.0)); j++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * dim.x * 2.0 + dim.x + shift.x, dim.y + blockHeight + shift.y, + j * dim.z * 2.0 + dim.z + shift.z); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(dim); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } +} + +void tbKeva3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + /* Ground. */ + const R3Real groundSize = 50.0; + const R3Real groundHeight = 0.1; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0.0, -groundHeight, 0.0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(groundSize, groundHeight, groundSize)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + /* Create the cubes. Odd layer counts keep adjacent blocks aligned. */ + const R3Vector halfExtents = r3Vector(0.1, 0.5, 2.0); + R3Real blockHeight = 0.0; + const int layers[] = {0, 9, 13, 17, 21, 41}; + + for (int i = 5; i >= 1; i--) { + const int numx = i; + const int numy = layers[i]; + const int numz = numx * 3 + 1; + const R3Real blockWidth = numx * halfExtents.z * 2.0; + buildBlock(testbed, world, halfExtents, + r3Vector(-blockWidth / 2.0, blockHeight, -blockWidth / 2.0), numx, numy, numz); + blockHeight += numy * halfExtents.y * 2.0 + halfExtents.x * 2.0; + } + + /* Set up the viewer. */ + tbCamera(testbed, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/locked_rotations3.c b/c/testbed/examples3d/locked_rotations3.c new file mode 100644 index 000000000..e952ba9d2 --- /dev/null +++ b/c/testbed/examples3d/locked_rotations3.c @@ -0,0 +1,55 @@ +/* Port of examples3d/locked_rotations3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbLockedRotations3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(5, 0.1, 5)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + R3RigidBodyDesc rectangle = r3DynamicRigidBodyDesc(); + rectangle.position.translation = r3Vector(0, 3, 0); + rectangle.lockedAxes = R3_LOCK_TRANSLATION_X | R3_LOCK_TRANSLATION_Y | R3_LOCK_TRANSLATION_Z | + R3_LOCK_ROTATION_Y | R3_LOCK_ROTATION_Z; + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(0.2, 0.6, 2)); + rectangle.canSleep = !testbed->noSleep; + rigidBodyHandle = r3InsertRigidBody(world, &rectangle); + r3InsertCollider(rigidBodyHandle, &boxCollider); + + R3RigidBodyDesc capsule = r3DynamicRigidBodyDesc(); + capsule.position.translation = r3Vector(0, 5, 0); + capsule.position = r3Pose(r3Vector(0, 5, 0), r3RotationFromAxisAngle(r3Vector(1, 0, 0), 1)); + capsule.lockedAxes = R3_LOCK_ROTATION_X | R3_LOCK_ROTATION_Y | R3_LOCK_ROTATION_Z; + R3RigidBodyHandle handle; + R3ColliderDesc capsuleCollider = r3CapsuleYColliderDesc(0.6, 0.4); + capsule.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &capsule); + r3InsertCollider(handle, &capsuleCollider); + + R3SharedShape *shape = r3CapsuleSharedShape(r3Vector(-0.6, 0, 0), r3Vector(0.6, 0, 0), 0.4); + R3ColliderDesc shapeCollider = r3DefaultColliderDesc(); + shapeCollider.shape.kind = R3_SHAPE_DESC_SHARED; + shapeCollider.shape.sharedShape = shape; + r3InsertCollider(handle, &shapeCollider); + + /* Set up the viewer. */ + tbCamera(testbed, 10, 3, 0, 0, 3, 0); + r3FreeSharedShape(shape); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/mjcf3.c b/c/testbed/examples3d/mjcf3.c new file mode 100644 index 000000000..534c76db8 --- /dev/null +++ b/c/testbed/examples3d/mjcf3.c @@ -0,0 +1,38 @@ +/* Port of examples3d/mjcf3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" +#ifdef RAPIER_ROBOTICS +void tbMjcf3(Testbed *testbed) { + R3World *world = r3NewWorld(); + R3MjcfLoaderOptions options = r3DefaultMjcfLoaderOptions(); + + options.makeRootsFixed = 1; + /* Z-up to Y-up, matching the Rust example's model convention. */ + options.shift = r3Pose(r3Vector(0, 0, 0), r3RotationFromAxisAngle(r3Vector(1, 0, 0), -R3_PI / 2)); + R3RigidBodyDesc blueprint = r3DynamicRigidBodyDesc(); + blueprint.canSleep = !testbed->noSleep; + options.rigidBodyBlueprint = blueprint; + + char path[4096]; + snprintf(path, sizeof(path), "%s/3d/agility_cassie/scene.xml", testbed->assetRoot); + R3MjcfRobot *robot = r3MjcfRobotFromFile(path, &options); + /* Insert the same robot with each joint representation. */ + R3MjcfRobotHandles *impulse = NULL, *multibody = NULL; + impulse = r3MjcfRobot_InsertUsingImpulseJoints(world, robot); + r3MjcfRobot_AppendTransform(robot, r3TranslationPose(r3Vector(0, 0, 1))); + multibody = r3MjcfRobot_InsertUsingMultibodyJoints( + world, robot, R3_MULTIBODY_SKIP_LOOP_CLOSURES | R3_MULTIBODY_DISABLE_SELF_CONTACTS); + r3FreeMjcfRobotHandles(impulse); + r3FreeMjcfRobotHandles(multibody); + r3FreeMjcfRobot(robot); + tbSetWorld(testbed, world); + tbCamera(testbed, 2, 2, 2, 0, 0, 0); + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} +#endif diff --git a/c/testbed/examples3d/mujoco_menagerie3.c b/c/testbed/examples3d/mujoco_menagerie3.c new file mode 100644 index 000000000..df4ff3900 --- /dev/null +++ b/c/testbed/examples3d/mujoco_menagerie3.c @@ -0,0 +1,372 @@ +/* Port of examples3d/mujoco_menagerie3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" +#ifdef RAPIER_ROBOTICS +#include "utils/files.h" + +static size_t lastFramedScene = SIZE_MAX; + +static int comparePaths(const void *a, const void *b) { + return strcmp(*(const char *const *)a, *(const char *const *)b); +} + +/* Like the Rust discovery loop, look one directory below the root for scene*.xml. */ +static FileNames discoverScenes(const char *root) { + FileNames scenes = {0}, robots = listDirectory(root); + for (size_t i = 0; i < robots.count; ++i) { + char directory[4096]; + snprintf(directory, sizeof(directory), "%s/%s", root, robots.names[i]); + FileNames files = listDirectory(directory); + for (size_t j = 0; j < files.count; ++j) { + const char *name = files.names[j]; + size_t length = strlen(name); + if (strncmp(name, "scene", 5) || length < 4 || strcmp(name + length - 4, ".xml")) { + continue; + } + char path[8192]; + snprintf(path, sizeof(path), "%s/%s", directory, name); + addFilename(&scenes, path); + } + freeFilenames(&files); + } + freeFilenames(&robots); + qsort(scenes.names, scenes.count, sizeof(*scenes.names), comparePaths); + return scenes; +} + +static R3MjcfLoaderOptions loaderOptions(Testbed *testbed) { + R3MjcfLoaderOptions options = r3DefaultMjcfLoaderOptions(); + options.skipPlaneGeoms = 1; + options.makeRootsFixed = 0; + options.createCollidersFromVisualShapes = 0; + R3ColliderDesc collider = r3BallColliderDesc(.5); + collider.density = 0; + options.colliderBlueprint = collider; + + R3RigidBodyDesc body = r3DynamicRigidBodyDesc(); + body.canSleep = !testbed->noSleep; + options.rigidBodyBlueprint = body; + + return options; +} + +static char *keyframeName(const R3MjcfRobot *robot, size_t key) { + size_t count = r3MjcfRobot_KeyframeName(robot, key, NULL, 0); + char *name = malloc(count); + if (!name) { + abort(); + } + count = r3MjcfRobot_KeyframeName(robot, key, name, count); + return name; +} + +static void mergeSiblingKeyframes(R3MjcfRobot *robot, const char *path, + const R3MjcfLoaderOptions *options) { + const char *slash = strrchr(path, '/'); + if (!slash) { + return; + } + char sibling[8192]; + snprintf(sibling, sizeof(sibling), "%.*s/keyframes.xml", (int)(slash - path), path); + FILE *file = fopen(sibling, "rb"); + if (!file) { + return; + } + fclose(file); + R3MjcfRobot *keys = NULL; + R3ErrorHandler handler = r3SetErrorHandler((R3ErrorHandler){0}); + keys = r3MjcfRobotFromFile(sibling, options); + R3Status status = r3LastStatus(); + r3SetErrorHandler(handler); + if (status != R3_OK) { + fprintf(stderr, "Failed to load sibling keyframes %s: %s\n", sibling, r3LastError()); + return; + } + size_t originalCount = 0, count = 0; + originalCount = r3MjcfRobot_KeyframeCount(robot); + count = r3MjcfRobot_KeyframeCount(keys); + for (size_t i = 0; i < count; ++i) { + char *name = keyframeName(keys, i); + int exists = 0; + for (size_t j = 0; name[0] && j < originalCount; ++j) { + char *existing = keyframeName(robot, j); + exists |= !strcmp(existing, name); + free(existing); + } + if (!exists) { + r3MjcfRobot_AppendKeyframe(robot, keys, i); + } + free(name); + } + r3FreeMjcfRobot(keys); +} + +static void addFloor(R3World *world) { + size_t count = r3ColliderHandles(world, NULL, 0); + if (!count) { + return; + } + R3ColliderHandle *handles = malloc(count * sizeof(*handles)); + if (!handles) { + abort(); + } + count = r3ColliderHandles(world, handles, count); + R3Aabb total = {r3Vector(INFINITY, INFINITY, INFINITY), + r3Vector(-INFINITY, -INFINITY, -INFINITY)}; + for (size_t i = 0; i < count; ++i) { + R3Aabb bounds = r3Collider_ComputeAabb(handles[i]); + total.mins.x = fmin(total.mins.x, bounds.mins.x); + total.mins.y = fmin(total.mins.y, bounds.mins.y); + total.mins.z = fmin(total.mins.z, bounds.mins.z); + total.maxs.x = fmax(total.maxs.x, bounds.maxs.x); + total.maxs.y = fmax(total.maxs.y, bounds.maxs.y); + total.maxs.z = fmax(total.maxs.z, bounds.maxs.z); + } + free(handles); + R3Vector half = r3VectorScale(r3VectorSub(total.maxs, total.mins), .5); + R3Vector center = r3VectorScale(r3VectorAdd(total.maxs, total.mins), .5); + half.x *= 10; + half.y *= 10; + center.z -= half.z; + half.z = .2; + center.z -= .2; + R3ColliderDesc floor = r3CuboidColliderDesc(half); + floor.position.translation = center; + r3InsertColliderWithoutParent(world, &floor); +} + +static void registerVisualMeshes(Testbed *testbed, const R3MjcfRobot *robot, + const R3MjcfRobotHandles *handles, int includePrimitives) { + size_t bodyCount = r3MjcfRobotHandles_Bodies(handles, NULL, 0); + R3RigidBodyHandle *bodies = malloc(bodyCount * sizeof(*bodies)); + if (!bodies && bodyCount) { + abort(); + } + bodyCount = r3MjcfRobotHandles_Bodies(handles, bodies, bodyCount); + for (size_t i = 0; i < bodyCount; ++i) { + if (bodies[i].index == UINT32_MAX) { + continue; + } + size_t count = r3MjcfRobot_BodyVisualCount(robot, i); + for (size_t j = 0; j < count; ++j) { + const R3MjcfVisualMesh *visual = NULL; + R3MjcfVisualMeshInfo info; + visual = r3MjcfRobot_BodyVisual(robot, i, j); + info = r3MjcfVisualMesh_Info(visual); + if (!info.is_trimesh && !includePrimitives) { + continue; + } + R3SharedShape *shape = NULL; + R3TriMeshData *geometry = NULL; + shape = r3MjcfVisualMesh_CloneShape(visual); + geometry = r3SharedShape_ToTrimesh(shape, 24, 12); + r3FreeSharedShape(shape); + TbRenderMesh mesh = {.body = bodies[i], + .localPose = info.local_pose, + .metallic = info.material.metallic, + .roughness = info.material.roughness, + .reflectance = info.material.reflectance}; + memcpy(mesh.rgba, info.rgba, sizeof(mesh.rgba)); + memcpy(mesh.emissive, info.material.emissive, sizeof(mesh.emissive)); + mesh.vertexCount = r3TriMeshData_Vertices(geometry, NULL, 0); + mesh.indexCount = r3TriMeshData_Indices(geometry, NULL, 0); + mesh.vertices = malloc(mesh.vertexCount * sizeof(*mesh.vertices)); + mesh.indices = malloc(mesh.indexCount * sizeof(*mesh.indices)); + if (!mesh.vertices || !mesh.indices) { + abort(); + } + mesh.vertexCount = r3TriMeshData_Vertices(geometry, mesh.vertices, mesh.vertexCount); + mesh.indexCount = r3TriMeshData_Indices(geometry, mesh.indices, mesh.indexCount); + r3FreeTriMeshData(geometry); + size_t uvCount = 0, normalCount = 0, pathCount = 0; + uvCount = r3MjcfVisualMesh_Uvs(visual, NULL, 0); + normalCount = r3MjcfVisualMesh_Normals(visual, NULL, 0); + pathCount = r3MjcfVisualMesh_Texture(visual, NULL, 0); + if (uvCount == mesh.vertexCount * 2) { + mesh.uvs = malloc(uvCount * sizeof(float)); + if (!mesh.uvs) { + abort(); + } + uvCount = r3MjcfVisualMesh_Uvs(visual, mesh.uvs, uvCount); + } + if (normalCount == mesh.vertexCount * 3) { + mesh.normals = malloc(normalCount * sizeof(float)); + if (!mesh.normals) { + abort(); + } + normalCount = r3MjcfVisualMesh_Normals(visual, mesh.normals, normalCount); + } + mesh.texture = malloc(pathCount); + if (!mesh.texture) { + abort(); + } + pathCount = r3MjcfVisualMesh_Texture(visual, mesh.texture, pathCount); + if (mesh.texture[0] && !info.has_color) { + for (int k = 0; k < 4; ++k) { + mesh.rgba[k] = 1; + } + } + tbAddBodyRenderMesh(testbed, &mesh); + free(mesh.vertices); + free(mesh.indices); + free(mesh.uvs); + free(mesh.normals); + free(mesh.texture); + } + } + free(bodies); +} + +void tbMujocoMenagerie3(Testbed *testbed) { + const char *root = getenv("RAPIER_MENAGERIE_DIR"); + if (!root) { + root = "../mujoco_menagerie"; + } + FileNames scenes = discoverScenes(root), labels = {0}, keyNames = {0}; + size_t defaultScene = 0; + const char *rootName = strrchr(root, '/'); + rootName = rootName ? rootName + 1 : root; + for (size_t i = 0; i < scenes.count; ++i) { + if (strstr(scenes.names[i], "unitree_a1")) { + defaultScene = i; + } + char label[8192]; + snprintf(label, sizeof(label), "%s/%s", rootName, scenes.names[i] + strlen(root) + 1); + addFilename(&labels, label); + } + const int useMultibody = tbSetting(testbed, "Use multibody joints", 1, 0, 1, 1); + const int renderColliders = tbSetting(testbed, "Render colliders", 0, 0, 1, 1); + const int renderVisualMeshes = tbSetting(testbed, "Render visual meshes", 1, 0, 1, 1); + const int renderVisualPrimitives = tbSetting(testbed, "Render visual primitives", 0, 0, 1, 1); + const int disableCollisions = tbSetting(testbed, "Disable collisions", 1, 0, 1, 1); + const int enableControls = tbSetting(testbed, "Enable joint controls", 1, 0, 1, 1); + tbLiveSetting(testbed, "Actuator strength", 1, .02, 2, 0); + const int enableSprings = tbSetting(testbed, "Enable joint springs", 1, 0, 1, 1); + /* Reserve the keyframe slot above the scene list. */ + tbSetting(testbed, "Keyframe", 0, 0, 0, 1); + size_t selected = tbChoice(testbed, "Scene", defaultScene, (const char *const *)labels.names, + labels.count, 0, 0); + int sceneChanged = selected != lastFramedScene; + lastFramedScene = selected; + R3World *world = r3NewWorld(); + R3MjcfRobot *robot = NULL; + R3MjcfRobotHandles *handles = NULL; + size_t keyCount = 0, actuatorCount = 0; + R3Real **controls = NULL; + size_t *controlCounts = NULL; + if (!scenes.count) { + tbLabel(testbed, "NO MODEL FOUND", + "Set RAPIER_MENAGERIE_DIR to your google-deepmind/mujoco_menagerie checkout."); + } else { + R3MjcfLoaderOptions options = loaderOptions(testbed); + robot = r3MjcfRobotFromFile(scenes.names[selected], &options); + mergeSiblingKeyframes(robot, scenes.names[selected], &options); + if (disableCollisions) { + size_t bodyCount = r3MjcfRobot_BodyCount(robot); + for (size_t i = 0; i < bodyCount; ++i) { + size_t colliderCount = r3MjcfRobot_BodyColliderCount(robot, i); + for (size_t j = 0; j < colliderCount; ++j) { + r3MjcfRobot_SetBodyColliderCollisionGroups(robot, i, j, + (R3InteractionGroups){1, 2, 0}); + } + } + } + R3Vector gravity = r3MjcfRobot_Gravity(robot); + r3SetGravity(world, r3Vector(0, 0, -r3VectorLength(gravity))); + uint8_t flags = disableCollisions ? R3_MULTIBODY_DISABLE_SELF_CONTACTS : 0; + if (!enableSprings) { + flags |= R3_MULTIBODY_SKIP_JOINT_SPRINGS; + } + keyCount = r3MjcfRobot_KeyframeCount(robot); + addFilename(&keyNames, "(none)"); + size_t defaultKey = keyCount ? 1 : 0; + for (size_t i = 0; i < keyCount; ++i) { + char *name = keyframeName(robot, i); + char fallback[64]; + snprintf(fallback, sizeof(fallback), "key %zu", i); + addFilename(&keyNames, name[0] ? name : fallback); + if (!strcmp(name, "home")) { + defaultKey = i + 1; + } + free(name); + } + size_t key = tbChoice(testbed, "Keyframe", defaultKey, (const char *const *)keyNames.names, + keyNames.count, useMultibody && enableControls, sceneChanged); + if (useMultibody) { + handles = r3MjcfRobot_InsertUsingMultibodyJoints(world, robot, flags); + } else { + handles = r3MjcfRobot_InsertUsingImpulseJoints(world, robot); + } + if (key) { + r3MjcfRobotHandles_ApplyKeyframe(handles, robot, key - 1); + } + addFloor(world); + if (renderVisualMeshes) { + registerVisualMeshes(testbed, robot, handles, renderVisualPrimitives); + } + if (useMultibody && enableControls) { + actuatorCount = r3MjcfRobotHandles_ActuatorCount(handles); + controls = calloc(keyCount + 1, sizeof(*controls)); + controlCounts = calloc(keyCount + 1, sizeof(*controlCounts)); + if (!controls || !controlCounts) { + abort(); + } + controls[0] = calloc(actuatorCount ? actuatorCount : 1, sizeof(R3Real)); + controlCounts[0] = actuatorCount; + if (!controls[0]) { + abort(); + } + for (size_t i = 0; i < keyCount; ++i) { + controlCounts[i + 1] = r3MjcfRobot_KeyframeControls(robot, i, NULL, 0); + size_t count = controlCounts[i + 1]; + controls[i + 1] = calloc(count ? count : 1, sizeof(R3Real)); + if (!controls[i + 1]) { + abort(); + } + controlCounts[i + 1] = + r3MjcfRobot_KeyframeControls(robot, i, controls[i + 1], count); + } + } + } + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; /* Source robot and actuator handles are scene-local. */ + testbed->collidersVisible = renderColliders; + testbed->up[1] = 0; + testbed->up[2] = 1; + testbed->frameAll = sceneChanged; + testbed->preserveCamera = !sceneChanged; + tbCamera(testbed, 2, 2, 2, 0, 0, .5); + if (!useMultibody) { + r3SetTimeStep(testbed->world, 1.0 / 240.0); + r3SetNumSolverIterations(testbed->world, 12); + } else { + r3SetNumInternalPgsIterations(testbed->world, 4); + } + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + if (controls) { + size_t key = tbChoice(testbed, "Keyframe", 0, (const char *const *)keyNames.names, + keyNames.count, 1, 0); + R3Real gain = tbLiveSetting(testbed, "Actuator strength", 1, .02, 2, 0); + r3MjcfRobotHandles_ApplyControlsScaled(handles, controls[key], + controlCounts[key], gain); + } + } + } + if (controls) { + for (size_t i = 0; i <= keyCount; ++i) { + free(controls[i]); + } + } + free(controls); + free(controlCounts); + r3FreeMjcfRobotHandles(handles); + r3FreeMjcfRobot(robot); + r3FreeWorld(world); + freeFilenames(&scenes); + freeFilenames(&labels); + freeFilenames(&keyNames); +} +#endif diff --git a/c/testbed/examples3d/newton_cradle3.c b/c/testbed/examples3d/newton_cradle3.c new file mode 100644 index 000000000..92f81bb92 --- /dev/null +++ b/c/testbed/examples3d/newton_cradle3.c @@ -0,0 +1,43 @@ +/* Port of examples3d/newton_cradle3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbNewtonCradle3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + for (int i = 0; i < 5; i++) { + R3RigidBodyHandle ground; + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(i * 1.01, 5, 0); + groundBody.canSleep = !testbed->noSleep; + ground = r3InsertRigidBody(world, &groundBody); + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(i * 1.01, 0, 0); + rigidBody.linvel = r3Vector(i == 4 ? 7 : 0, 0, 0); + R3ColliderDesc collider = r3BallColliderDesc(0.5); + collider.restitution = 1; + R3RigidBodyHandle handle; + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = 7; + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = r3Vector(0, 5, 0); + r3InsertImpulseJoint(ground, handle, &joint); + } + /* Set up the viewer. */ + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/one_way_platforms3.c b/c/testbed/examples3d/one_way_platforms3.c new file mode 100644 index 000000000..aaee250f4 --- /dev/null +++ b/c/testbed/examples3d/one_way_platforms3.c @@ -0,0 +1,96 @@ +/* Port of examples3d/one_way_platforms3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +typedef struct OneWayPlatformHook { + R3ColliderHandle platform1, platform2; +} OneWayPlatformHook; + +static int sameCollider(R3ColliderHandle a, R3ColliderHandle b) { + return a.world == b.world && a.index == b.index && a.generation == b.generation; +} + +static void RAPIER_CALL modifySolverContacts(void *userData, const R3ReadContext *read, + R3ColliderHandle collider1, R3ColliderHandle collider2, + R3ContactModificationContext *context) { + (void)read; + const OneWayPlatformHook *hook = userData; + R3Vector allowedLocalN1 = r3Vector(0, 0, 0); + /* Flip the allowed normal when the platform is collider2. */ + if (sameCollider(collider1, hook->platform1)) { + allowedLocalN1 = r3Vector(0, 1, 0); + } else if (sameCollider(collider2, hook->platform1)) { + allowedLocalN1 = r3Vector(0, -1, 0); + } + if (sameCollider(collider1, hook->platform2)) { + allowedLocalN1 = r3Vector(0, -1, 0); + } else if (sameCollider(collider2, hook->platform2)) { + allowedLocalN1 = r3Vector(0, 1, 0); + } + r3ContactModificationContext_UpdateAsOnewayPlatform(context, allowedLocalN1, .1); + const R3Real tangentVelocity = + sameCollider(collider1, hook->platform1) || sameCollider(collider2, hook->platform2) ? -12 + : 12; + r3ContactModificationContext_SetTangentVelocity(context, r3Vector(0, 0, tangentVelocity)); +} + +void tbOneWayPlatforms3(Testbed *testbed) { + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(9, 0.5, 25)); + collider.position.translation = r3Vector(0, 2, 30); + collider.activeHooks = R3_MODIFY_SOLVER_CONTACTS; + R3RigidBodyHandle handle; + OneWayPlatformHook platformHook; + handle = r3InsertRigidBody(world, &rigidBody); + platformHook.platform1 = r3InsertCollider(handle, &collider); + collider = r3CuboidColliderDesc(r3Vector(9, 0.5, 25)); + collider.position.translation = r3Vector(0, -2, -30); + collider.activeHooks = R3_MODIFY_SOLVER_CONTACTS; + platformHook.platform2 = r3InsertCollider(handle, &collider); + R3PhysicsHooks physicsHooks = {0}; + physicsHooks.user_data = &platformHook; + physicsHooks.modify_solver_contacts_context = modifySolverContacts; + tbCamera(testbed, 100, 0, 0, 0, 0, 0); + + tbSetWorld(testbed, world); + size_t stepId = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, &physicsHooks, NULL); + ++stepId; + size_t bodyCount = r3RigidBodyCount(world); + /* Spawn cubes periodically and reverse gravity below the lower platform. */ + if (stepId % 200 == 0 && bodyCount <= 7) { + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 6, 20); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 2, 1.5)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + size_t activeCount = r3ActiveRigidBodies(world, NULL, 0); + R3RigidBodyHandle *active = malloc(activeCount * sizeof(*active)); + if (activeCount && !active) { + abort(); + } + activeCount = r3ActiveRigidBodies(world, active, activeCount); + for (size_t i = 0; i < activeCount; ++i) { + R3Vector position = r3RigidBody_Translation(active[i]); + if (position.y > 1) { + r3RigidBody_SetGravityScale(active[i], 1, 0); + } else if (position.y < -1) { + r3RigidBody_SetGravityScale(active[i], -1, 0); + } + } + free(active); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/platform3.c b/c/testbed/examples3d/platform3.c new file mode 100644 index 000000000..3e64827b0 --- /dev/null +++ b/c/testbed/examples3d/platform3.c @@ -0,0 +1,72 @@ +/* Port of examples3d/platform3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbPlatform3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(10, 0.1, 10)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 6; j++) { + for (int k = 0; k < 6; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * 0.4 - 1.2, j * 0.4 + (j >= 3 ? 5 : 3), k * 0.4 - 1.2); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.2, 0.2, 0.2)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + R3RigidBodyHandle velocityBasedPlatformHandle = {0}; + R3RigidBodyHandle positionBasedPlatformHandle = {0}; + R3RigidBodyDesc platformBody = r3KinematicVelocityBasedRigidBodyDesc(); + platformBody.position.translation = r3Vector(0, 2.3, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(2, 0.2, 2)); + platformBody.canSleep = !testbed->noSleep; + velocityBasedPlatformHandle = r3InsertRigidBody(world, &platformBody); + r3InsertCollider(velocityBasedPlatformHandle, &boxCollider); + R3RigidBodyDesc positionBasedPlatform = r3KinematicPositionBasedRigidBodyDesc(); + positionBasedPlatform.position.translation = r3Vector(0, 5.3, 0); + R3ColliderDesc positionBasedCollider = r3CuboidColliderDesc(r3Vector(2, 0.2, 2)); + positionBasedPlatform.canSleep = !testbed->noSleep; + positionBasedPlatformHandle = r3InsertRigidBody(world, &positionBasedPlatform); + r3InsertCollider(positionBasedPlatformHandle, &positionBasedCollider); + /* Set up the viewer. */ + tbCamera(testbed, 10, 5, 10, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + + R3Real dt = r3TimeStep(world); + R3Real time = (R3Real)(testbed->step + 1) * dt; + R3Vector velocity = r3Vector(0, cos(time * 2), sin(time) * 2); + + r3RigidBody_SetLinvel(velocityBasedPlatformHandle, velocity, 1); + r3RigidBody_SetAngvel(velocityBasedPlatformHandle, r3Vector(0, 1, 0), 1); + + R3Vector position = r3RigidBody_Translation(positionBasedPlatformHandle); + r3RigidBody_SetNextKinematicTranslation( + positionBasedPlatformHandle, + r3VectorAdd(position, r3VectorScale(velocity, -dt))); + R3Rotation rotation = r3RigidBody_Rotation(positionBasedPlatformHandle); + r3RigidBody_SetNextKinematicRotation( + positionBasedPlatformHandle, + r3RotationMul(r3RotationFromAxisAngle(r3Vector(0, 1, 0), -0.5 * dt), rotation)); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/primitives3.c b/c/testbed/examples3d/primitives3.c new file mode 100644 index 000000000..c154c1ffe --- /dev/null +++ b/c/testbed/examples3d/primitives3.c @@ -0,0 +1,81 @@ +/* Port of examples3d/primitives3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbPrimitives3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + /* Ground. */ + const R3Real groundSize = 100.1; + const R3Real groundHeight = 2.1; + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0.0, -groundHeight, 0.0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(groundSize, groundHeight, groundSize)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + /* Create the primitives. */ + const int num = 8; + const R3Real rad = 1.0; + + const R3Real shiftx = rad * 2.0 + rad; + const R3Real shifty = rad * 2.0 + rad; + const R3Real shiftz = rad * 2.0 + rad; + const R3Real centerx = shiftx * (num / 2); + const R3Real centery = shifty / 2.0; + const R3Real centerz = shiftz * (num / 2); + + R3Real offset = -num * (rad * 2.0 + rad) * 0.5; + + for (int j = 0; j < 20; j++) { + for (int i = 0; i < num; i++) { + for (int k = 0; k < num; k++) { + const R3Real x = i * shiftx - centerx + offset; + const R3Real y = j * shifty + centery + 3.0; + const R3Real z = k * shiftz - centerz + offset; + + /* Build the rigid body. */ + rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x, y, z); + rigidBody.canSleep = !testbed->noSleep; + + switch (j % 5) { + case 1: + collider = r3BallColliderDesc(rad); + break; + case 2: + /* Rounded cylinders are faster even with a small rounding margin. */ + collider = r3RoundCylinderColliderDesc(rad, rad, rad / 10.0); + break; + case 3: + collider = r3ConeColliderDesc(rad, rad); + break; + default: + collider = r3CapsuleYColliderDesc(rad, rad); + break; + } + + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + + offset -= 0.05 * rad * (num - 1.0); + } + + /* Set up the viewer. */ + tbCamera(testbed, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/restitution3.c b/c/testbed/examples3d/restitution3.c new file mode 100644 index 000000000..b02f0373b --- /dev/null +++ b/c/testbed/examples3d/restitution3.c @@ -0,0 +1,41 @@ +/* Port of examples3d/restitution3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbRestitution3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3ColliderDesc floor = r3CuboidColliderDesc(r3Vector(20, 1, 2)); + floor.restitution = 1; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &floor); + const int num = 10; + for (int j = 0; j < 2; j++) { + for (int i = 0; i <= num; i++) { + R3ColliderDesc collider = r3BallColliderDesc(0.5); + collider.restitution = (R3Real)i / num; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector((i - num / 2.0) * 2, 10 * (j + 1), 0); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera(testbed, 0, 3, 30, 0, 3, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/rope_joints3.c b/c/testbed/examples3d/rope_joints3.c new file mode 100644 index 000000000..7266dbc70 --- /dev/null +++ b/c/testbed/examples3d/rope_joints3.c @@ -0,0 +1,94 @@ +/* Port of examples3d/rope_joints3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include "utils/character.h" + +void tbRopeJoints3(Testbed *testbed) { + R3World *world = r3NewWorld(); + + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.75, 0.1, 0.75)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-0.85, 0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.1, 0.1, 0.75)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0.85, 0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.1, 0.1, 0.75)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0.1, -0.85); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.75, 0.1, 0.1)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0.1, 0.85); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.75, 0.1, 0.1)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Manually controlled character, tethered to a ball. */ + R3RigidBodyHandle characterHandle; + { + R3RigidBodyDesc rigidBody = r3KinematicPositionBasedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0.3, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.15, 0.3, 0.15)); + characterHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(characterHandle, &collider); + } + tbBodyColor(testbed, characterHandle, 1, 131.0f / 255, 244.0f / 255, 1); + R3RigidBodyHandle childHandle; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(1, 1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3BallColliderDesc(.04); + childHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(childHandle, &collider); + } + { + R3JointDesc joint = r3RopeJointDesc(2); + r3InsertImpulseJoint(characterHandle, childHandle, &joint); + } + CharacterControlMode controlMode = CHARACTER_KINEMATIC; + R3KinematicCharacterController *controller = NULL; + R3PidController *pid = NULL; + controller = r3NewKinematicCharacterController(); + pid = r3NewPidController(); + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + updateCharacter(testbed, world, &controlMode, controller, pid, characterHandle); + } + } + r3FreePidController(pid); + r3FreeKinematicCharacterController(controller); + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/sensor3.c b/c/testbed/examples3d/sensor3.c new file mode 100644 index 000000000..8a8f306d4 --- /dev/null +++ b/c/testbed/examples3d/sensor3.c @@ -0,0 +1,84 @@ +/* Port of examples3d/sensor3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static int same(R3RigidBodyHandle handle, R3RigidBodyHandle handleB) { + return handle.world == handleB.world && handle.index == handleB.index && + handle.generation == handleB.generation; +} + +void tbSensor3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle ground = {0}; + R3RigidBodyHandle sensor = {0}; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(10.1, 0.1, 10.1)); + rigidBody.canSleep = !testbed->noSleep; + ground = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(ground, &boxCollider); + + for (int i = 0; i < 10; i++) { + for (int k = 0; k < 10; k++) { + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(i * 0.4 - 2, 3, k * 0.4 - 2); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.2, 0.2, 0.2)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + + tbBodyColor(testbed, handle, 0.5, 0.5, 1, 1); + } + } + R3RigidBodyDesc dynamicBody = r3DynamicRigidBodyDesc(); + dynamicBody.position.translation = r3Vector(0, 5, 0); + R3ColliderDesc sensorBodyCollider = r3CuboidColliderDesc(r3Vector(0.2, 0.2, 0.2)); + dynamicBody.canSleep = !testbed->noSleep; + sensor = r3InsertRigidBody(world, &dynamicBody); + r3InsertCollider(sensor, &sensorBodyCollider); + + R3ColliderDesc collider = r3BallColliderDesc(1); + collider.density = 0; + collider.isSensor = 1; + collider.activeEvents = R3_COLLISION_EVENTS; + r3InsertCollider(sensor, &collider); + tbBodyColor(testbed, sensor, 0.5, 1, 1, 1); + /* Set up the viewer. */ + tbCamera(testbed, 6, 4, 6, 0, 1, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + R3EventCollector *eventHandler = r3NewEventCollector(); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3EventCollector_Clear(eventHandler); + r3Step(world, NULL, eventHandler); + + size_t n = r3EventCollector_CollisionEvents(eventHandler, NULL, 0); + R3CollisionEvent *events = calloc(n ? n : 1, sizeof(*events)); + if (!events) { + abort(); + } + n = r3EventCollector_CollisionEvents(eventHandler, events, n); + for (size_t i = 0; i < n; i++) { + R3ColliderHandle colliderHandles[] = {events[i].collider1, events[i].collider2}; + for (size_t j = 0; j < 2; j++) { + R3RigidBodyHandle handle = r3Collider_Parent(colliderHandles[j]); + if (!same(handle, ground) && !same(handle, sensor)) { + tbBodyColor(testbed, handle, events[i].started ? 1 : 0.5f, + events[i].started ? 1 : 0.5f, events[i].started ? 0 : 1, 1); + } + } + } + free(events); + } + } + r3FreeEventCollector(eventHandler); + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_bodies3.c b/c/testbed/examples3d/soft_bodies3.c new file mode 100644 index 000000000..752411808 --- /dev/null +++ b/c/testbed/examples3d/soft_bodies3.c @@ -0,0 +1,132 @@ +/* Port of examples3d/soft_bodies3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftBodies3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + /* Ground. */ + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(12, 0.1, 12)); + + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + + /* A cloth pinned by its four corners, with a box dropped on it. */ + const uint32_t n = 24; + const uint32_t pinnedParticles[] = {0, n - 1, n * (n - 1), n * n - 1}; + R3SoftBodyDesc cloth = r3DefaultSoftBodyDesc(); + cloth.kind = R3_SOFT_DESC_CLOTH; + cloth.a = r3Vector(-3.5, 2.5, -1.2); + cloth.du = r3Vector(0.1, 0.0, 0.0); + cloth.dv = r3Vector(0.0, 0.0, 0.1); + cloth.nx = n; + cloth.ny = n; + cloth.pinned = (R3IndexView){pinnedParticles, TB_COUNT(pinnedParticles)}; + cloth.material.edgeSoftness = cloth.material.bendSoftness = cloth.material.volumeSoftness = + cloth.material.shapeMatchingSoftness = (R3SpringCoefficients){30.0, 1.0}; + cloth.particleMass = 0.05; + cloth.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &cloth); + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-2.35, 4, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.3, 0.3, 0.3)); + collider.density = 0.5; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + + /* A balloon: hollow sphere with pressure (global volume preservation). */ + R3SoftBodyDesc balloon = r3SphereSoftBodyDesc(r3Vector(0.5, 3.0, 0.0), 0.8, 2); + balloon.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){15.0, 1.0}); + balloon.volumeFactor = 1.2; + balloon.particleMass = 0.05; + balloon.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &balloon); + + /* Jelly bodies: corotational, Neo-Hookean, and per-cell volume constraints. */ + { + R3SoftBodyDesc jelly = + r3CuboidSoftBodyDesc(r3Vector(3, 1, 1.5), r3Vector(0.6, 0.6, 0.6), 5, 5, 5); + jelly.cellModel = R3_SOFT_CELL_COROTATIONAL; + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = 2.0e3; + material.poissonRatio = 0.35; + material.elasticDampingRatio = 0.5; + jelly.material = material; + jelly.particleMass = 0.2; + jelly.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &jelly); + } + { + R3SoftBodyDesc jelly = + r3CuboidSoftBodyDesc(r3Vector(3, 1, 4.5), r3Vector(0.6, 0.6, 0.6), 5, 5, 5); + jelly.cellModel = R3_SOFT_CELL_NEO_HOOKEAN; + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = 2.0e3; + material.poissonRatio = 0.35; + material.elasticDampingRatio = 0.5; + jelly.material = material; + jelly.particleMass = 0.2; + jelly.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &jelly); + } + { + R3SoftBodyDesc jelly = + r3CuboidSoftBodyDesc(r3Vector(3, 1, -1.5), r3Vector(0.6, 0.6, 0.6), 5, 5, 5); + jelly.cellModel = R3_SOFT_CELL_VOLUME; + jelly.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){20.0, 1.0}); + jelly.particleMass = 0.2; + jelly.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &jelly); + } + + /* A rope hanging from a fixed anchor, holding a rigid weight. */ + const uint32_t pinnedParticle = 0; + R3SoftBodyDesc rope = r3DefaultSoftBodyDesc(); + rope.kind = R3_SOFT_DESC_ROPE; + rope.a = r3Vector(-0.5, 5, 3); + rope.b = r3Vector(2.5, 5, 3); + rope.nx = 30; + rope.pinned = (R3IndexView){&pinnedParticle, 1}; + rope.material.edgeSoftness = rope.material.bendSoftness = rope.material.volumeSoftness = + rope.material.shapeMatchingSoftness = (R3SpringCoefficients){40.0, 1.0}; + rope.particleMass = 0.05; + rope.canSleep = !testbed->noSleep; + R3SoftBodyHandle ropeHandle = r3InsertSoftBody(world, &rope); + + R3Vector lastPos = r3SoftBody_ParticlePosition(ropeHandle, 29); + R3RigidBodyHandle weight; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(lastPos, r3Vector(0, -0.3, 0)); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3BallColliderDesc(0.25); + collider.density = 2.0; + weight = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(weight, &collider); + } + + r3SoftBody_AttachParticle(ropeHandle, 29, weight); + + /* Set up the viewer. */ + tbCamera(testbed, 9.0, 6.0, 12.0, 0.0, 1.5, 0.0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_cloth3.c b/c/testbed/examples3d/soft_cloth3.c new file mode 100644 index 000000000..a7e083ac5 --- /dev/null +++ b/c/testbed/examples3d/soft_cloth3.c @@ -0,0 +1,88 @@ +/* Port of examples3d/soft_cloth3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftCloth3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(12, 0.1, 12)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* A sheet dropped over a ball and a box. */ + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-1, 1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3BallColliderDesc(1); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(1.5, 0.6, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.6, 0.6, 0.6)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + const size_t n = 40; + R3SoftBodyDesc sheet = r3ClothSoftBodyDesc(r3Vector(-3, 3, -2), r3Vector(0.1, 0, 0), + r3Vector(0, 0, 0.1), n + 20, n); + sheet.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + sheet.particleMass = .02; + sheet.canSleep = !testbed->noSleep; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){30, 1.0}; + material.bendSoftness = (R3SpringCoefficients){30, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){30, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){30, 1.0}; + material.bendSoftness = (R3SpringCoefficients){3, 1}; + sheet.material = material; + } + { + R3ColliderDesc surface = r3BallColliderDesc(.05); + surface.friction = .8; + sheet.collider = surface; + } + r3InsertSoftBody(world, &sheet); + + /* Curtain pinned along its top edge, with a ball rolling into it. */ + uint32_t pinned[40]; + for (uint32_t i = 0; i < 40; ++i) { + pinned[i] = i * 30; + } + R3SoftBodyDesc curtain = + r3ClothSoftBodyDesc(r3Vector(-2, 4, 5), r3Vector(0.1, 0, 0), r3Vector(0, -0.1, 0), 40, 30); + r3SoftBodyDesc_SetPinnedParticles(&curtain, (R3IndexView){(const uint32_t *)pinned, 40}); + curtain.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + curtain.particleMass = .02; + curtain.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &curtain); + + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0.5, 9); + rigidBody.linvel = r3Vector(0, 0, -6); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3BallColliderDesc(.5); + collider.density = 3; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + tbCamera(testbed, 8, 6, 14, 0, 1.5, 2); + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_cloth_stress3.c b/c/testbed/examples3d/soft_cloth_stress3.c new file mode 100644 index 000000000..2588d374b --- /dev/null +++ b/c/testbed/examples3d/soft_cloth_stress3.c @@ -0,0 +1,144 @@ +/* Port of examples3d/soft_cloth_stress3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static R3SoftBodyDesc cloth(R3Vector origin, R3Vector du, R3Vector dv, size_t nu, size_t nv) { + R3SoftBodyDesc builder = r3ClothSoftBodyDesc(origin, du, dv, nu, nv); + builder.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + builder.particleMass = .02; + builder.particleRadius = (R3OptionalReal){1, .05}; + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){30, 1.0}; + material.bendSoftness = (R3SpringCoefficients){30, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){30, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){30, 1.0}; + material.bendSoftness = (R3SpringCoefficients){3, 1}; + builder.material = material; + { + R3ColliderDesc surface = r3BallColliderDesc(.05); + surface.friction = .5; + builder.collider = surface; + } + return builder; +} + +void tbSoftClothStress3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.5, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(30, 0.5, 30)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Sheets slide down a slope into a stopper. */ + const R3Real slope = .5; + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-6, 2, 0); + rigidBody.position.rotation = r3RotationFromAxisAngle(r3Vector(0, 0, 1), -slope); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(5, 0.2, 4)); + collider.friction = .4; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-0.6, 0.6, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.2, 0.6, 4)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + const R3Rotation rot = r3RotationFromAxisAngle(r3Vector(0, 0, 1), -slope); + for (int k = 0; k < 6; ++k) { + const R3Vector origin = r3VectorAdd( + r3Vector(-6, 2, 0), + r3RotationTransformVector(rot, r3Vector(-3.5 + k * .3, .3 + k * .12, -1.5))); + { + R3SoftBodyDesc sheet = + cloth(origin, r3RotationTransformVector(rot, r3Vector(.15, 0, 0)), + r3Vector(0, 0, .15), 21, 21); + sheet.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &sheet); + } + } + /* Banner pinned along both ends. */ + const size_t nu = 31, nv = 13; + uint32_t right[13], bannerPinned[26]; + for (size_t j = 0; j < nv; ++j) { + right[j] = (uint32_t)((nu - 1) * nv + j); + bannerPinned[j] = (uint32_t)j; + bannerPinned[nv + j] = right[j]; + } + R3SoftBodyHandle banner; + { + R3SoftBodyDesc body = + cloth(r3Vector(2, 4, -4), r3Vector(.15, 0, 0), r3Vector(0, -.15, 0), nu, nv); + r3SoftBodyDesc_SetPinnedParticles(&body, (R3IndexView){(const uint32_t *)bannerPinned, 26}); + body.canSleep = !testbed->noSleep; + banner = r3InsertSoftBody(world, &body); + } + + R3Vector bannerRightRest[13]; + for (size_t j = 0; j < nv; ++j) { + bannerRightRest[j] = r3SoftBody_ParticlePosition(banner, right[j]); + } + /* Strip pinned top and bottom; its bottom clamp rotates. */ + const size_t su = 11, sv = 41; + uint32_t bottom[11], stripPinned[22]; + for (size_t i = 0; i < su; ++i) { + bottom[i] = (uint32_t)(i * sv + sv - 1); + stripPinned[i] = (uint32_t)(i * sv); + stripPinned[su + i] = bottom[i]; + } + R3SoftBodyHandle strip; + { + R3SoftBodyDesc body = + cloth(r3Vector(9, 6.5, -.75), r3Vector(0, 0, .15), r3Vector(0, -.15, 0), su, sv); + r3SoftBodyDesc_SetPinnedParticles(&body, (R3IndexView){(const uint32_t *)stripPinned, 22}); + body.selfContacts = 1; + body.canSleep = !testbed->noSleep; + strip = r3InsertSoftBody(world, &body); + } + const R3Vector stripAxis = r3Vector(9, 0, 0); + + R3Vector stripBottomRest[11]; + for (size_t i = 0; i < su; ++i) { + stripBottomRest[i] = r3SoftBody_ParticlePosition(strip, bottom[i]); + } + + tbCamera(testbed, 2, 8, 18, 1.5, 3, 0); + + tbSetWorld(testbed, world); + R3Real t = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R3Real dt = r3TimeStep(world); + t += dt; + /* Stretch/release the banner and twist the strip. */ + const R3Real stretch = 1.5 * (1 - cos(.5 * t)); + + for (size_t j = 0; j < nv; ++j) { + r3SoftBody_SetParticleKinematicTarget( + banner, right[j], + r3VectorAdd(bannerRightRest[j], r3Vector(stretch, 0, 0))); + } + const R3Rotation twist = r3RotationFromAxisAngle(r3Vector(0, 1, 0), .8 * t); + + for (size_t i = 0; i < su; ++i) { + const R3Vector target = r3VectorAdd( + stripAxis, + r3RotationTransformVector(twist, r3VectorSub(stripBottomRest[i], stripAxis))); + r3SoftBody_SetParticleKinematicTarget(strip, bottom[i], target); + } + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_dress3.c b/c/testbed/examples3d/soft_dress3.c new file mode 100644 index 000000000..264e538d3 --- /dev/null +++ b/c/testbed/examples3d/soft_dress3.c @@ -0,0 +1,206 @@ +/* Port of examples3d/soft_dress3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +enum { + PELVIS, + TORSO, + HEAD, + UPPER_ARM_LEFT, + UPPER_ARM_RIGHT, + FOREARM_LEFT, + FOREARM_RIGHT, + THIGH_LEFT, + THIGH_RIGHT, + SHIN_LEFT, + SHIN_RIGHT, + NUM_PARTS +}; + +static const R3Real UPPER_ARM = .34, FOREARM = .32, THIGH = .5, SHIN = .5, TORSO_RADIUS = .15; + +static R3Pose limbPose(R3Vector joint, R3Vector dir, R3Real halfLength) { + dir = r3VectorNormalize(dir); + R3Vector axis = r3VectorCross(r3Vector(0, 1, 0), dir); + R3Rotation rotation; + if (r3VectorLength(axis) < 1e-7) { + rotation = r3RotationFromAxisAngle(r3Vector(1, 0, 0), dir.y < 0 ? R3_PI : 0); + } else { + rotation = r3RotationFromAxisAngle(axis, acos(fmax(-1, fmin(1, dir.y)))); + } + return r3Pose(r3VectorAdd(joint, r3VectorScale(dir, halfLength)), rotation); +} + +/* Procedural dance, matching frameAt in the Rust example. */ +static void frameAt(R3Real t, R3Pose poses[NUM_PARTS]) { + const R3Real beat = 2 * t, yaw = .7 * sin(.35 * t) + .25 * t; + const R3Real sway = .25 * sin(beat), bounce = .04 * fabs(sin(2 * beat)), roll = .12 * sin(beat); + const R3Rotation turn = r3RotationFromAxisAngle(r3Vector(0, 1, 0), yaw); + const R3Rotation hipsRot = + r3RotationMul(turn, r3RotationFromAxisAngle(r3Vector(0, 0, 1), roll)); + const R3Vector hips = r3VectorAdd(r3Vector(0, .95 + bounce, 0), + r3RotationTransformVector(turn, r3Vector(sway, 0, 0))); + const R3Vector up = r3RotationTransformVector(hipsRot, r3Vector(0, 1, 0)); + const R3Vector side = r3RotationTransformVector(hipsRot, r3Vector(1, 0, 0)); + const R3Vector forward = r3RotationTransformVector(hipsRot, r3Vector(0, 0, 1)); + const R3Rotation lean = r3RotationFromAxisAngle(forward, -.5 * roll); + const R3Rotation torsoRot = r3RotationMul(lean, hipsRot); + const R3Vector torsoCenter = r3VectorAdd(hips, r3VectorScale(up, .42)); + poses[PELVIS] = r3Pose(hips, hipsRot); + poses[TORSO] = r3Pose(torsoCenter, torsoRot); + const R3Vector neck = + r3VectorAdd(torsoCenter, r3RotationTransformVector(torsoRot, r3Vector(0, .28, 0))); + poses[HEAD] = r3Pose(r3VectorAdd(neck, r3VectorScale(up, .13)), torsoRot); + for (size_t i = 0; i < 2; ++i) { + const R3Real s = i == 0 ? 1 : -1; + const R3Vector hip = + r3VectorSub(r3VectorAdd(hips, r3VectorScale(side, .11 * s)), r3VectorScale(up, .05)); + const R3Real phase = i == 0 ? 0 : R3_PI; + const R3Real swing = .45 * sin(beat + phase); + const R3Vector thighDir = r3VectorNormalize(r3VectorAdd( + r3VectorAdd(r3VectorScale(up, -cos(swing)), r3VectorScale(forward, sin(swing))), + r3VectorScale(side, .05 * s))); + const R3Vector knee = r3VectorAdd(hip, r3VectorScale(thighDir, THIGH)); + const R3Real bend = .9 * fmax(swing, 0); + const R3Vector shinDir = r3VectorAdd(r3VectorScale(up, -cos(swing - bend)), + r3VectorScale(forward, sin(swing - bend))); + poses[THIGH_LEFT + i] = limbPose(hip, thighDir, THIGH * .5); + poses[SHIN_LEFT + i] = limbPose(knee, shinDir, SHIN * .5); + } + for (size_t i = 0; i < 2; ++i) { + const R3Real s = i == 0 ? 1 : -1; + const R3Vector shoulder = + r3VectorAdd(r3VectorSub(neck, r3VectorScale(up, .06)), + r3RotationTransformVector(torsoRot, r3Vector(.22 * s, 0, 0))); + const R3Real raise = .6 + .6 * sin(beat + (i == 0 ? 0 : 1.5)); + const R3Vector armDir = r3VectorNormalize(r3VectorAdd( + r3VectorSub(r3VectorScale(side, s * cos(raise)), r3VectorScale(up, sin(raise) * .6)), + r3VectorScale(forward, .2 * sin(.7 * beat)))); + const R3Vector elbow = r3VectorAdd(shoulder, r3VectorScale(armDir, UPPER_ARM)); + const R3Vector foreDir = r3VectorNormalize( + r3VectorAdd(r3VectorAdd(armDir, r3VectorScale(up, .9)), r3VectorScale(forward, .5))); + poses[UPPER_ARM_LEFT + i] = limbPose(shoulder, armDir, UPPER_ARM * .5); + poses[FOREARM_LEFT + i] = limbPose(elbow, foreDir, FOREARM * .5); + } +} + +typedef struct Pin { + R3SoftBodyHandle handle; + size_t particle, part; + R3Vector offset; +} Pin; + +static void piece(Testbed *testbed, R3World *world, R3SoftBodyDesc *tube, size_t numAround, + size_t part, int selfContacts, const float color[4], + const R3Pose frame0[NUM_PARTS], Pin *pins, size_t *pinCount) { + uint32_t *pinned = malloc(numAround * sizeof(*pinned)); + if (!pinned) { + abort(); + } + for (uint32_t i = 0; i < numAround; ++i) { + pinned[i] = i; + } + r3SoftBodyDesc_SetPinnedParticles(tube, (R3IndexView){(const uint32_t *)pinned, numAround}); + + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){80, 1.0}; + material.bendSoftness = (R3SpringCoefficients){80, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){80, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){80, 1.0}; + material.bendSoftness = (R3SpringCoefficients){3, 1}; + tube->material = material; + tube->particleMass = .01; + tube->particleRadius = (R3OptionalReal){1, .02}; + tube->selfContacts = selfContacts; + tube->canSleep = !testbed->noSleep; + { + R3ColliderDesc surface = r3BallColliderDesc(.02); + surface.friction = .5; + tube->collider = surface; + } + R3SoftBodyHandle handle = r3InsertSoftBody(world, tube); + free(pinned); + + R3RigidBodyHandle root = r3SoftBody_RootBody(handle); + tbBodyColor(testbed, root, color[0], color[1], color[2], color[3]); + const R3Pose inverse = r3PoseInverse(frame0[part]); + for (size_t k = 0; k < numAround; ++k) { + R3Vector position = r3SoftBody_ParticlePosition(handle, k); + pins[(*pinCount)++] = (Pin){handle, k, part, r3PoseTransformPoint(inverse, position)}; + } +} + +void tbSoftDress3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(10, .1, 10)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + R3Pose frame0[NUM_PARTS]; + frameAt(0, frame0); + R3ColliderDesc shapes[NUM_PARTS] = {0}; + shapes[PELVIS] = r3CapsuleXColliderDesc(.08, .13); + shapes[TORSO] = r3CapsuleYColliderDesc(.18, TORSO_RADIUS); + shapes[HEAD] = r3BallColliderDesc(.12); + shapes[UPPER_ARM_LEFT] = r3CapsuleYColliderDesc(UPPER_ARM * .5 - .03, .05); + shapes[UPPER_ARM_RIGHT] = r3CapsuleYColliderDesc(UPPER_ARM * .5 - .03, .05); + shapes[FOREARM_LEFT] = r3CapsuleYColliderDesc(FOREARM * .5 - .03, .045); + shapes[FOREARM_RIGHT] = r3CapsuleYColliderDesc(FOREARM * .5 - .03, .045); + shapes[THIGH_LEFT] = r3CapsuleYColliderDesc(THIGH * .5 - .05, .085); + shapes[THIGH_RIGHT] = r3CapsuleYColliderDesc(THIGH * .5 - .05, .085); + shapes[SHIN_LEFT] = r3CapsuleYColliderDesc(SHIN * .5 - .04, .065); + shapes[SHIN_RIGHT] = r3CapsuleYColliderDesc(SHIN * .5 - .04, .065); + R3RigidBodyHandle parts[NUM_PARTS]; + for (size_t i = 0; i < NUM_PARTS; ++i) { + R3RigidBodyDesc body = r3KinematicPositionBasedRigidBodyDesc(); + body.position = frame0[i]; + shapes[i].friction = .4; + parts[i] = r3InsertRigidBody(world, &body); + r3InsertCollider(parts[i], &shapes[i]); + + tbBodyColor(testbed, parts[i], .93, .8, .68, 1); + } + Pin pins[56 + 48]; + size_t pinCount = 0; + const R3Vector waist = r3PoseTransformPoint(frame0[PELVIS], r3Vector(0, .1, 0)); + const R3Vector hem = {waist.x, .25, waist.z}; + R3SoftBodyDesc skirt = r3ClothTubeSoftBodyDesc(waist, r3VectorSub(hem, waist), .2, .62, 56, 18); + const float red[] = {.75, .15, .3, 1}; + piece(testbed, world, &skirt, 56, PELVIS, 1, red, frame0, pins, &pinCount); + + const R3Vector shirtTop = r3PoseTransformPoint(frame0[TORSO], r3Vector(0, .26, 0)); + const R3Vector shirtBottom = r3PoseTransformPoint(frame0[TORSO], r3Vector(0, -.26, 0)); + R3SoftBodyDesc shirt = r3ClothTubeSoftBodyDesc(shirtTop, r3VectorSub(shirtBottom, shirtTop), + TORSO_RADIUS + .012, TORSO_RADIUS + .09, 48, 16); + const float white[] = {.95, .95, .9, 1}; + piece(testbed, world, &shirt, 48, TORSO, 0, white, frame0, pins, &pinCount); + + tbCamera(testbed, 3, 2, 4, 0, .9, 0); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + R3Real t = 0; + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R3Real dt = r3TimeStep(world); + t += dt; + R3Pose frame[NUM_PARTS]; + frameAt(t, frame); + + for (size_t i = 0; i < NUM_PARTS; ++i) { + r3RigidBody_SetNextKinematicPosition(parts[i], frame[i]); + } + for (size_t i = 0; i < pinCount; ++i) { + r3SoftBody_SetParticleKinematicTarget( + pins[i].handle, pins[i].particle, + r3PoseTransformPoint(frame[pins[i].part], pins[i].offset)); + } + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_fem3.c b/c/testbed/examples3d/soft_fem3.c new file mode 100644 index 000000000..da9de8374 --- /dev/null +++ b/c/testbed/examples3d/soft_fem3.c @@ -0,0 +1,132 @@ +/* Port of examples3d/soft_fem3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#ifdef RAPIER_FEM + +void tbSoftFem3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(20, 0.1, 20)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Compare FEM (1) with the constraint solver (0). */ + const uint32_t solvers[] = {R3_SOFT_SOLVER_FEM, R3_SOFT_SOLVER_CONSTRAINTS}; + for (size_t row = 0; row < TB_COUNT(solvers); ++row) { + const R3Real z = row == 0 ? -3.5 : 3.5; + /* Cantilever bolted to a wall. */ + R3SoftBodyDesc beam = + r3CuboidSoftBodyDesc(r3Vector(-3.5, 3, z), r3Vector(1.5, 0.15, 0.15), 13, 3, 3); + beam.cellModel = R3_SOFT_CELL_COROTATIONAL; + beam.totalMass = (R3OptionalReal){1, 12.0}; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = 2.0e6; + material.poissonRatio = .3; + material.elasticDampingRatio = 1; + beam.material = material; + } + beam.canSleep = !testbed->noSleep; + uint32_t *beamPins = NULL; + { + size_t count = r3SoftBodyDesc_ParticlePositions(&beam, NULL, 0); + R3Vector *positions = malloc(count * sizeof(*positions)); + beamPins = malloc(count * sizeof(*beamPins)); + if (!positions || !beamPins) { + abort(); + } + count = r3SoftBodyDesc_ParticlePositions(&beam, positions, count); + size_t beamPinsCount = 0; + for (size_t i = 0; i < count; ++i) { + if (positions[i].x < -5.0 + 1.0e-4) { + beamPins[beamPinsCount++] = (uint32_t)i; + } + } + r3SoftBodyDesc_SetPinnedParticles( + &beam, (R3IndexView){(const uint32_t *)beamPins, beamPinsCount}); + + free(positions); + } + beam.solver = solvers[row]; + r3InsertSoftBody(world, &beam); + free(beamPins); + + /* Plank pinned at both ends. */ + R3SoftBodyDesc plank = + r3CuboidSoftBodyDesc(r3Vector(1, 2, z), r3Vector(2, 0.15, 0.6), 17, 3, 5); + plank.cellModel = R3_SOFT_CELL_COROTATIONAL; + plank.totalMass = (R3OptionalReal){1, 20.0}; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = 4.0e6; + material.poissonRatio = .3; + material.elasticDampingRatio = 1; + plank.material = material; + } + plank.canSleep = !testbed->noSleep; + uint32_t *plankPins = NULL; + { + size_t count = r3SoftBodyDesc_ParticlePositions(&plank, NULL, 0); + R3Vector *positions = malloc(count * sizeof(*positions)); + plankPins = malloc(count * sizeof(*plankPins)); + if (!positions || !plankPins) { + abort(); + } + count = r3SoftBodyDesc_ParticlePositions(&plank, positions, count); + size_t plankPinsCount = 0; + for (size_t i = 0; i < count; ++i) { + if (fabs(positions[i].x - 1.0) > 1.9) { + plankPins[plankPinsCount++] = (uint32_t)i; + } + } + r3SoftBodyDesc_SetPinnedParticles( + &plank, (R3IndexView){(const uint32_t *)plankPins, plankPinsCount}); + + free(positions); + } + plank.solver = solvers[row]; + r3InsertSoftBody(world, &plank); + free(plankPins); + + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(1, 4.5, z); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.35, 0.35, 0.35)); + collider.density = 30; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Neo-Hookean jelly. */ + R3SoftBodyDesc jelly = + r3CuboidSoftBodyDesc(r3Vector(6, 2, z), r3Vector(0.7, 0.7, 0.7), 5, 5, 5); + jelly.cellModel = R3_SOFT_CELL_NEO_HOOKEAN; + jelly.particleMass = .1; + jelly.solver = solvers[row]; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = 2.0e4; + material.poissonRatio = .4; + material.elasticDampingRatio = .5; + jelly.material = material; + } + jelly.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &jelly); + } + tbCamera(testbed, 2, 8, 16, .5, 2, 0); + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} +#endif diff --git a/c/testbed/examples3d/soft_jelly3.c b/c/testbed/examples3d/soft_jelly3.c new file mode 100644 index 000000000..88c0aecf1 --- /dev/null +++ b/c/testbed/examples3d/soft_jelly3.c @@ -0,0 +1,183 @@ +/* Port of examples3d/soft_jelly3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static R3SoftBodyDesc *jelly(R3SoftBodyDesc *builder, R3Real young) { + builder->cellModel = R3_SOFT_CELL_COROTATIONAL; + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = young; + material.poissonRatio = .35; + material.elasticDampingRatio = .5; + builder->material = material; + builder->particleMass = .05; + { + R3ColliderDesc surface = r3BallColliderDesc(.1); + surface.friction = .7; + builder->collider = surface; + } + return builder; +} + +void tbSoftJelly3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(12, 0.1, 12)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + const R3Real walls[][4] = { + {-2.5, 0, .1, 2.5}, {2.5, 0, .1, 2.5}, {0, -2.5, 2.5, .1}, {0, 2.5, 2.5, .1}}; + for (size_t i = 0; i < TB_COUNT(walls); ++i) { + const R3Real dx = walls[i][0], dz = walls[i][1], hx = walls[i][2], hz = walls[i][3]; + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(dx + 5, 1, dz); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(hx, 1, hz)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + /* Elastic cubes, softer at the top. */ + const R3Real stiffness[] = {1.0e4, 4.0e3, 1.5e3}; + for (size_t i = 0; i < TB_COUNT(stiffness); ++i) { + R3SoftBodyDesc cube = + r3CuboidSoftBodyDesc(r3Vector(-4, .7 + 1.5 * i, 0), r3Vector(0.6, 0.6, 0.6), 5, 5, 5); + cube.cellModel = R3_SOFT_CELL_COROTATIONAL; + cube.particleMass = .1; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = stiffness[i]; + material.poissonRatio = .4; + material.elasticDampingRatio = .5; + cube.material = material; + } + cube.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &cube); + } + /* Balloons in the container. */ + for (int i = 0; i < 6; ++i) { + const R3Real x = 5 + (i % 3 - 1) * 1.2; + const R3Real z = (i / 3 - .5) * 1.2; + R3SoftBodyDesc balloon = r3SphereSoftBodyDesc(r3Vector(x, 2 + i * 1.5, z), .6, 2); + balloon.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){15, 1}); + balloon.volumeFactor = 1.1; + balloon.particleMass = .03; + balloon.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &balloon); + } + /* Shape-matched cube driven toward an animated pose. */ + R3SoftBodyHandle driven; + R3SoftBodyDesc cube = + r3CuboidSoftBodyDesc(r3Vector(0.45, 0.45, 0.45), r3Vector(0.45, 0.45, 0.45), 4, 4, 4); + cube.shapeMatching = (R3OptionalBool){1, 1}; + cube.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){15, 1}); + cube.particleRadius = (R3OptionalReal){1, .1}; + cube.particleMass = .2; + cube.gravityScale = 0; + cube.canSleep = 0; + driven = r3InsertSoftBody(world, &cube); + + /* Volumetric ball from its boundary mesh. */ + { + R3SharedShape *shape = r3BallSharedShape(.7); + R3TriMeshData *mesh = r3SharedShape_ToTrimesh(shape, 16, 16); + r3FreeSharedShape(shape); + size_t vertexCount, indexCount; + vertexCount = r3TriMeshData_Vertices(mesh, NULL, 0); + indexCount = r3TriMeshData_Indices(mesh, NULL, 0); + R3Vector *vertices = malloc(vertexCount * sizeof(*vertices)); + uint32_t *indices = malloc(indexCount * sizeof(*indices)); + if (!vertices || !indices) { + abort(); + } + vertexCount = r3TriMeshData_Vertices(mesh, vertices, vertexCount); + indexCount = r3TriMeshData_Indices(mesh, indices, indexCount); + for (size_t i = 0; i < vertexCount; ++i) { + vertices[i] = r3VectorAdd(vertices[i], r3Vector(-4, 3, -4)); + } + + R3VolumeMeshParameters ballMeshing = r3NewVolumeMeshParameters(.2); + R3SoftBodyDesc ball = r3VolumetricSoftBodyDesc( + (R3VectorView){vertices, vertexCount}, + (R3SurfaceElementView){(const R3Triangle *)indices, indexCount / 3}, ballMeshing); + { + jelly(&ball, 1.0e4); + ball.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &ball); + } + free(vertices); + free(indices); + r3FreeTriMeshData(mesh); + } + /* Volumetric capsule from its boundary mesh. */ + { + R3SharedShape *shape = r3CapsuleSharedShape(r3Vector(0, -.6, 0), r3Vector(0, .6, 0), .45); + R3TriMeshData *mesh = r3SharedShape_ToTrimesh(shape, 12, 12); + r3FreeSharedShape(shape); + size_t vertexCount, indexCount; + vertexCount = r3TriMeshData_Vertices(mesh, NULL, 0); + indexCount = r3TriMeshData_Indices(mesh, NULL, 0); + R3Vector *vertices = malloc(vertexCount * sizeof(*vertices)); + uint32_t *indices = malloc(indexCount * sizeof(*indices)); + if (!vertices || !indices) { + abort(); + } + vertexCount = r3TriMeshData_Vertices(mesh, vertices, vertexCount); + indexCount = r3TriMeshData_Indices(mesh, indices, indexCount); + for (size_t i = 0; i < vertexCount; ++i) { + vertices[i] = + r3VectorAdd(r3RotationTransformVector( + r3RotationFromAxisAngle(r3Vector(0, 0, 1), 1.2), vertices[i]), + r3Vector(-4, 6, -4)); + } + + R3VolumeMeshParameters capsuleMeshing = r3NewVolumeMeshParameters(.2); + R3SoftBodyDesc capsule = r3VolumetricSoftBodyDesc( + (R3VectorView){vertices, vertexCount}, + (R3SurfaceElementView){(const R3Triangle *)indices, indexCount / 3}, capsuleMeshing); + { + jelly(&capsule, 3.0e4); + capsule.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &capsule); + } + free(vertices); + free(indices); + r3FreeTriMeshData(mesh); + } + /* Boxes shoved by the driven cube. */ + for (int i = 0; i < 8; ++i) { + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-.5 + (i % 4) * .5, 0.25, 4 + (i / 4) * .5); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.2, 0.2, 0.2)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + + tbCamera(testbed, 8, 7, 14, 0, 1.5, 1); + + tbSetWorld(testbed, world); + R3Real t = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R3Real dt = r3TimeStep(world); + t += dt; + const R3Vector center = r3Vector(2 * cos(t), .6, 4 + 2 * sin(t)); + const R3Rotation rotation = r3RotationFromAxisAngle(r3Vector(0, 1, 0), 2 * t); + const R3Pose target = r3Pose(center, rotation); + + r3SoftBody_SetClusterShapeMatchingTarget(driven, 0, &target); + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_joints3.c b/c/testbed/examples3d/soft_joints3.c new file mode 100644 index 000000000..f12eceacc --- /dev/null +++ b/c/testbed/examples3d/soft_joints3.c @@ -0,0 +1,396 @@ +/* Port of examples3d/soft_joints3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static R3SoftBodyDesc jelly(R3Vector center, R3Vector half, R3Real young) { + R3SoftBodyDesc builder = r3CuboidSoftBodyDesc(center, half, 4, 4, 4); + builder.cellModel = R3_SOFT_CELL_COROTATIONAL; + builder.particleMass = .08; + builder.particleRadius = (R3OptionalReal){1, .06}; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = young; + material.poissonRatio = .35; + material.elasticDampingRatio = .8; + builder.material = material; + } + { + R3ColliderDesc surface = r3BallColliderDesc(.06); + surface.friction = .6; + builder.collider = surface; + } + return builder; +} + +void tbSoftJoints3(Testbed *testbed) { + R3World *world = r3NewWorld(); + + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(18, 0.1, 18)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Revolute joint and velocity motor. */ + R3SoftBodyHandle spinner; + { + R3SoftBodyDesc builder = jelly(r3Vector(-6, 1.6, -4), r3Vector(0.5, 0.5, 0.5), 8e3); + builder.canSleep = !testbed->noSleep; + spinner = r3InsertSoftBody(world, &builder); + } + R3RigidBodyHandle spinnerRoot = r3SoftBody_RootBody(spinner); + R3Vector com = r3SoftBody_CenterOfMass(spinner); + R3RigidBodyHandle pivot; + { + R3RigidBodyDesc builder = r3FixedRigidBodyDesc(); + builder.position.translation = com; + pivot = r3InsertRigidBody(world, &builder); + } + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 1, 0)); + r3JointDesc_SetMotorVelocity(&joint, R3_AXIS_ANG_X, 1.5, 60); + r3InsertImpulseJoint(pivot, spinnerRoot, &joint); + } + /* A cloth corner follows a circling kinematic mover. */ + R3SoftBodyHandle cloth; + R3SoftBodyDesc clothBuilder = r3ClothSoftBodyDesc(r3Vector(0, 2.6, 2.5), r3Vector(0.16, 0, 0), + r3Vector(0, 0, 0.16), 12, 12); + clothBuilder.canSleep = !testbed->noSleep; + cloth = r3InsertSoftBody(world, &clothBuilder); + + const uint32_t cornerParticles[] = {0, 1, 12}; + uint32_t corner = r3SoftBody_AddCluster(cloth, cornerParticles, 3); + R3RigidBodyHandle cornerProxy = r3SoftBody_ClusterProxy(cloth, corner); + R3Vector cornerPos; + { cornerPos = r3RigidBody_Translation(cornerProxy); } + R3RigidBodyHandle mover; + { + R3RigidBodyDesc builder = r3KinematicPositionBasedRigidBodyDesc(); + builder.position.translation = cornerPos; + mover = r3InsertRigidBody(world, &builder); + } + { + R3JointDesc joint = r3SphericalJointDesc(); + r3InsertImpulseJoint(mover, cornerProxy, &joint); + } + /* Weld a rigid plate to the jelly's top cluster. */ + R3SoftBodyHandle wobbler; + { + R3SoftBodyDesc builder = jelly(r3Vector(-2.5, 0.61, -4), r3Vector(0.6, 0.6, 0.6), 2.5e3); + builder.canSleep = !testbed->noSleep; + wobbler = r3InsertSoftBody(world, &builder); + } + + R3Vector positions[64]; + size_t particleCount = + r3SoftBody_ParticlePositions(wobbler, positions, TB_COUNT(positions)); + R3Real maxY = 0; + for (size_t i = 0; i < particleCount; ++i) { + maxY = fmax(maxY, positions[i].y); + } + uint32_t top[16]; + size_t topCount = 0; + for (size_t i = 0; i < particleCount; ++i) { + if (fabs(positions[i].y - maxY) < 1e-3) { + top[topCount++] = (uint32_t)i; + } + } + uint32_t topCluster = r3SoftBody_AddCluster(wobbler, top, topCount); + R3RigidBodyHandle topProxy = r3SoftBody_ClusterProxy(wobbler, topCluster); + R3Vector topPos; + { topPos = r3RigidBody_Translation(topProxy); } + R3RigidBodyHandle plate; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(topPos, r3Vector(0, 0.12, 0)); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.7, 0.06, 0.7)); + collider.density = .4; + plate = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(plate, &collider); + } + { + R3JointDesc joint = r3FixedJointDesc(); + joint.localFrame1.translation = r3Vector(0, -0.12, 0); + r3InsertImpulseJoint(plate, topProxy, &joint); + } + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(topPos, r3Vector(0.3, 1.4, 0.2)); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.15, 0.15, 0.15)); + collider.density = 1.5; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Prismatic motor with two stops. */ + R3SoftBodyHandle shuttle; + { + R3SoftBodyDesc builder = jelly(r3Vector(2, 0.85, -4), r3Vector(0.4, 0.4, 0.4), 6e3); + builder.canSleep = !testbed->noSleep; + shuttle = r3InsertSoftBody(world, &builder); + } + R3RigidBodyHandle shuttleRoot = r3SoftBody_RootBody(shuttle); + R3Vector shuttleCom = r3SoftBody_CenterOfMass(shuttle); + R3RigidBodyHandle rail; + { + R3RigidBodyDesc builder = r3FixedRigidBodyDesc(); + builder.position.translation = shuttleCom; + rail = r3InsertRigidBody(world, &builder); + } + R3ImpulseJointHandle railJoint; + { + R3JointDesc joint = r3PrismaticJointDesc(r3Vector(1, 0, 0)); + r3JointDesc_SetLimits(&joint, R3_AXIS_LIN_X, -1.8, 1.8); + r3JointDesc_SetMotorPosition(&joint, R3_AXIS_LIN_X, 0, 40, 8); + railJoint = r3InsertImpulseJoint(rail, shuttleRoot, &joint); + } + /* Rope over a ledge. */ + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-1, 1, -9); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1.6, 1, 1.2)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + R3SoftBodyHandle anchorJelly; + { + R3SoftBodyDesc builder = jelly(r3Vector(-1, 2.6, -9), r3Vector(0.5, 0.5, 0.5), 1.2e4); + builder.canSleep = !testbed->noSleep; + anchorJelly = r3InsertSoftBody(world, &builder); + } + R3SoftBodyHandle hangingJelly; + { + R3SoftBodyDesc builder = jelly(r3Vector(1.6, 2.6, -9), r3Vector(0.5, 0.5, 0.5), 1.2e4); + builder.canSleep = !testbed->noSleep; + hangingJelly = r3InsertSoftBody(world, &builder); + } + R3RigidBodyHandle anchorRoot = r3SoftBody_RootBody(anchorJelly); + R3RigidBodyHandle hangingRoot = r3SoftBody_RootBody(hangingJelly); + { + R3JointDesc joint = r3RopeJointDesc(2.2); + r3InsertImpulseJoint(anchorRoot, hangingRoot, &joint); + } + /* Spring bungee under a gantry. */ + R3SoftBodyHandle bungee; + { + R3SoftBodyDesc builder = jelly(r3Vector(5.5, 3.2, 2.5), r3Vector(0.45, 0.45, 0.45), 6e3); + builder.canSleep = !testbed->noSleep; + bungee = r3InsertSoftBody(world, &builder); + } + R3RigidBodyHandle bungeeRoot = r3SoftBody_RootBody(bungee); + R3RigidBodyHandle gantry; + { + R3RigidBodyDesc builder = r3FixedRigidBodyDesc(); + builder.position.translation = r3Vector(5.5, 5.5, 2.5); + gantry = r3InsertRigidBody(world, &builder); + } + { + R3JointDesc joint = r3SpringJointDesc(1.2, 25, 1.5); + r3InsertImpulseJoint(gantry, bungeeRoot, &joint); + } + /* Bead on a visual-only pole. */ + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(8.5, 2.5, -4); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CylinderColliderDesc(2.5, .05); + collider.collisionGroups = (R3InteractionGroups){0, 0, 0}; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + R3SoftBodyHandle bead; + { + R3SoftBodyDesc builder = jelly(r3Vector(8.5, 4.2, -4), r3Vector(0.35, 0.35, 0.35), 8e3); + builder.canSleep = !testbed->noSleep; + bead = r3InsertSoftBody(world, &builder); + } + R3RigidBodyHandle beadRoot = r3SoftBody_RootBody(bead); + R3RigidBodyHandle pole; + { + R3RigidBodyDesc builder = r3FixedRigidBodyDesc(); + builder.position.translation = r3Vector(8.5, 2.5, -4); + pole = r3InsertRigidBody(world, &builder); + } + { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = 1 | 4; + r3JointDesc_SetLocalAxis1(&joint, r3Vector(0, 1, 0)); + r3JointDesc_SetLocalAxis2(&joint, r3Vector(0, 1, 0)); + r3JointDesc_SetLimits(&joint, R3_AXIS_LIN_Y, -1.8, 1.8); + r3InsertImpulseJoint(pole, beadRoot, &joint); + } + /* Hinge two disjoint clusters of one soft bar. */ + R3SoftBodyHandle bar; + R3SoftBodyDesc barBuilder = + r3CuboidSoftBodyDesc(r3Vector(2.5, 2.2, 5.5), r3Vector(1, 0.22, 0.4), 7, 3, 3); + barBuilder.cellModel = R3_SOFT_CELL_COROTATIONAL; + barBuilder.particleMass = .08; + barBuilder.particleRadius = (R3OptionalReal){1, .06}; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = 2e4; + material.poissonRatio = .35; + material.elasticDampingRatio = 1; + barBuilder.material = material; + } + { + R3ColliderDesc surface = r3BallColliderDesc(.06); + surface.friction = .5; + barBuilder.collider = surface; + } + barBuilder.canSleep = !testbed->noSleep; + bar = r3InsertSoftBody(world, &barBuilder); + + R3Vector barCom = r3SoftBody_CenterOfMass(bar); + uint32_t leftHalf[64], rightHalf[64]; + size_t leftCount = 0, rightCount = 0; + particleCount = r3SoftBody_ParticlePositions(bar, positions, TB_COUNT(positions)); + for (size_t i = 0; i < particleCount; ++i) { + if (positions[i].x < barCom.x - 1e-3) { + leftHalf[leftCount++] = i; + } else if (positions[i].x > barCom.x + 1e-3) { + rightHalf[rightCount++] = i; + } + } + uint32_t leftCluster = r3SoftBody_AddCluster(bar, leftHalf, leftCount); + uint32_t rightCluster = r3SoftBody_AddCluster(bar, rightHalf, rightCount); + R3RigidBodyHandle leftProxy = r3SoftBody_ClusterProxy(bar, leftCluster); + R3RigidBodyHandle rightProxy = r3SoftBody_ClusterProxy(bar, rightCluster); + R3Vector leftPos; + { leftPos = r3RigidBody_Translation(leftProxy); } + R3Vector rightPos; + { rightPos = r3RigidBody_Translation(rightProxy); } + R3RigidBodyHandle barAnchor; + { + R3RigidBodyDesc builder = r3FixedRigidBodyDesc(); + builder.position.translation = leftPos; + barAnchor = r3InsertRigidBody(world, &builder); + } + { + R3JointDesc joint = r3FixedJointDesc(); + r3InsertImpulseJoint(barAnchor, leftProxy, &joint); + } + R3ImpulseJointHandle flapJoint; + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + joint.localFrame1.translation = r3VectorSub(barCom, leftPos); + joint.localFrame2.translation = r3VectorSub(barCom, rightPos); + r3JointDesc_SetMotorPosition(&joint, R3_AXIS_ANG_X, 0, 80, 10); + flapJoint = r3InsertImpulseJoint(leftProxy, rightProxy, &joint); + } + /* Multibody arm and rope attached to one jelly. */ + R3RigidBodyHandle armRoot; + { + R3RigidBodyDesc builder = r3FixedRigidBodyDesc(); + builder.position.translation = r3Vector(7, 5, 6); + armRoot = r3InsertRigidBody(world, &builder); + } + R3RigidBodyHandle link1; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(8.2, 5, 6); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CapsuleXColliderDesc(.5, .08); + collider.density = 2; + link1 = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(link1, &collider); + } + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = r3Vector(-1.2, 0, 0); + r3InsertMultibodyJoint(armRoot, link1, &joint); + } + R3RigidBodyHandle link2; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(9.4, 5, 6); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CapsuleXColliderDesc(.5, .08); + collider.density = 2; + link2 = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(link2, &collider); + } + { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + joint.localFrame1.translation = r3Vector(0.6, 0, 0); + joint.localFrame2.translation = r3Vector(-0.6, 0, 0); + r3InsertMultibodyJoint(link1, link2, &joint); + } + R3SoftBodyHandle pendulum; + { + R3SoftBodyDesc builder = jelly(r3Vector(10.2, 4.2, 6), r3Vector(0.5, 0.5, 0.5), 5e3); + builder.canSleep = !testbed->noSleep; + pendulum = r3InsertSoftBody(world, &builder); + } + R3RigidBodyHandle pendulumRoot = r3SoftBody_RootBody(pendulum); + { + R3JointDesc joint = r3SphericalJointDesc(); + joint.localFrame1.translation = r3Vector(0.7, 0, 0); + joint.localFrame2.translation = r3Vector(0, 0.6, 0); + r3InsertImpulseJoint(link2, pendulumRoot, &joint); + } + R3RigidBodyHandle crateBody; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(10.2, 0.3, 6); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.3, 0.3, 0.3)); + collider.density = .5; + crateBody = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(crateBody, &collider); + } + { + R3JointDesc joint = r3RopeJointDesc(3.2); + r3InsertImpulseJoint(pendulumRoot, crateBody, &joint); + } + /* Wave a pinned cluster without a joint. */ + R3SoftBodyHandle banner; + R3SoftBodyDesc gripBuilder = r3ClothSoftBodyDesc(r3Vector(-8, 3.2, 4), r3Vector(0.18, 0, 0), + r3Vector(0, -0.18, 0), 14, 10); + gripBuilder.canSleep = !testbed->noSleep; + banner = r3InsertSoftBody(world, &gripBuilder); + + uint32_t gripParticles[14]; + for (uint32_t i = 0; i < 14; ++i) { + gripParticles[i] = i; + } + uint32_t grip = r3SoftBody_AddCluster(banner, gripParticles, 14); + R3RigidBodyHandle gripProxy = r3SoftBody_ClusterProxy(banner, grip); + r3SoftBody_SetClusterPinned(banner, grip, 1); + R3Vector gripHome; + { gripHome = r3RigidBody_Translation(gripProxy); } + tbCamera(testbed, 13, 9, 15, 0, 1.5, 0); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + R3Real t = 0; + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R3Real dt = r3TimeStep(world); + t += dt; + + r3RigidBody_SetNextKinematicTranslation( + mover, + r3VectorAdd(cornerPos, r3Vector(1.2 * sin(.8 * t), .4 * sin(1.3 * t), + 1.2 * cos(.8 * t) - 1.2))); + + r3ImpulseJoint_SetMotorPosition(railJoint, R3_AXIS_LIN_X, 1.5 * sin(.6 * t), 40, + 8, 1); + + r3ImpulseJoint_SetMotorPosition(flapJoint, R3_AXIS_ANG_Z, .8 * sin(1.4 * t), 80, + 10, 1); + + r3SoftBody_SetClusterKinematicTarget( + banner, grip, + r3Pose(r3VectorAdd(gripHome, r3Vector(1.5 * sin(.7 * t), .2 * sin(1.9 * t), 0)), + r3RotationFromAxisAngle(r3Vector(1, 0, 0), .35 * sin(1.1 * t)))); + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_meshes3.c b/c/testbed/examples3d/soft_meshes3.c new file mode 100644 index 000000000..fa211af13 --- /dev/null +++ b/c/testbed/examples3d/soft_meshes3.c @@ -0,0 +1,275 @@ +/* Port of examples3d/soft_meshes3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +typedef struct Face { + uint32_t key[3], vertices[3]; + R3Vector centroid; +} Face; + +static int compareFaces(const void *a, const void *b) { + const Face *fa = a, *fb = b; + for (size_t i = 0; i < 3; ++i) { + if (fa->key[i] != fb->key[i]) { + return fa->key[i] < fb->key[i] ? -1 : 1; + } + } + return 0; +} + +/* Closed boundary of cells owned by a cluster, capped over the cut. */ +static void clusterSurface(R3SoftBodyHandle soft, const uint32_t *particles, + size_t count, R3Vector **vertices, uint32_t **indices, + size_t *triangleCount) { + size_t n, cellIndexCount; + n = r3SoftBody_NumParticles(soft); + uint32_t *vertexOf = malloc(n * sizeof(*vertexOf)); + *vertices = malloc(count * sizeof(**vertices)); + if (!vertexOf || !*vertices) { + abort(); + } + for (size_t i = 0; i < n; ++i) { + vertexOf[i] = UINT32_MAX; + } + for (size_t i = 0; i < count; ++i) { + vertexOf[particles[i]] = i; + (*vertices)[i] = r3SoftBody_ParticlePosition(soft, particles[i]); + } + cellIndexCount = r3SoftBody_Cells(soft, NULL, 0); + uint32_t *cells = malloc(cellIndexCount * sizeof(*cells)); + Face *faces = malloc(cellIndexCount * sizeof(*faces)); + if (!cells || !faces) { + abort(); + } + cellIndexCount = r3SoftBody_Cells(soft, cells, cellIndexCount); + size_t faceCount = 0; + for (size_t i = 0; i < cellIndexCount; i += 4) { + const uint32_t *cell = &cells[i]; + int owned = 1; + R3Vector centroid = {0}; + for (size_t j = 0; j < 4; ++j) { + if (vertexOf[cell[j]] == UINT32_MAX) { + owned = 0; + break; + } + centroid = r3VectorAdd(centroid, (*vertices)[vertexOf[cell[j]]]); + } + if (!owned) { + continue; + } + centroid = r3VectorScale(centroid, .25); + for (size_t k = 0; k < 4; ++k) { + Face *face = &faces[faceCount++]; + face->centroid = centroid; + for (size_t j = 0; j < 3; ++j) { + face->key[j] = face->vertices[j] = cell[(k + j + 1) % 4]; + } + for (size_t a = 0; a < 3; ++a) { + for (size_t b = a + 1; b < 3; ++b) { + if (face->key[a] > face->key[b]) { + uint32_t tmp = face->key[a]; + face->key[a] = face->key[b]; + face->key[b] = tmp; + } + } + } + } + } + qsort(faces, faceCount, sizeof(*faces), compareFaces); + *indices = malloc(faceCount * 3 * sizeof(**indices)); + if (faceCount && !*indices) { + abort(); + } + *triangleCount = 0; + for (size_t i = 0; i < faceCount;) { + size_t next = i + 1; + while (next < faceCount && !compareFaces(&faces[i], &faces[next])) { + ++next; + } + if (next == i + 1) { + const Face *face = &faces[i]; + uint32_t mapped[3]; + R3Vector p[3]; + for (size_t j = 0; j < 3; ++j) { + mapped[j] = vertexOf[face->vertices[j]]; + p[j] = (*vertices)[mapped[j]]; + } + const R3Vector normal = r3VectorCross(r3VectorSub(p[1], p[0]), r3VectorSub(p[2], p[0])); + const R3Vector center = + r3VectorScale(r3VectorAdd(r3VectorAdd(p[0], p[1]), p[2]), 1.0 / 3); + if (r3VectorDot(normal, r3VectorSub(center, face->centroid)) < 0) { + uint32_t tmp = mapped[1]; + mapped[1] = mapped[2]; + mapped[2] = tmp; + } + memcpy(&(*indices)[3 * (*triangleCount)++], mapped, sizeof(mapped)); + } + i = next; + } + free(faces); + free(cells); + free(vertexOf); +} + +static R3SoftBodyDesc jelly(R3Vector center, R3Real halfExtents, size_t n, R3Real young) { + R3SoftBodyDesc builder = + r3CuboidSoftBodyDesc(center, r3Vector(halfExtents, halfExtents, halfExtents), n, n, n); + builder.cellModel = R3_SOFT_CELL_COROTATIONAL; + builder.particleMass = .1; + builder.particleRadius = (R3OptionalReal){1, .05}; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = young; + material.poissonRatio = .4; + material.elasticDampingRatio = .5; + builder.material = material; + } + { + R3ColliderDesc surface = r3BallColliderDesc(.05); + surface.friction = .7; + builder.collider = surface; + } + return builder; +} + +void tbSoftMeshes3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(12, 0.1, 12)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + + /* Skinned jelly. */ + R3SoftBodyHandle skinned; + { + const R3Vector center = {-3, 1.2, 0}; + R3SoftBodyDesc builder = jelly(center, .6, 4, 200); + builder.canSleep = !testbed->noSleep; + skinned = r3InsertSoftBody(world, &builder); + + R3SharedShape *ball = NULL; + R3TriMeshData *mesh = NULL; + ball = r3BallSharedShape(1.2); + mesh = r3SharedShape_ToTrimesh(ball, 20, 20); + r3FreeSharedShape(ball); + size_t vertexCount, indexCount; + vertexCount = r3TriMeshData_Vertices(mesh, NULL, 0); + indexCount = r3TriMeshData_Indices(mesh, NULL, 0); + R3Vector *vertices = malloc(vertexCount * sizeof(*vertices)); + uint32_t *indices = malloc(indexCount * sizeof(*indices)); + if (!vertices || !indices) { + abort(); + } + vertexCount = r3TriMeshData_Vertices(mesh, vertices, vertexCount); + indexCount = r3TriMeshData_Indices(mesh, indices, indexCount); + r3FreeTriMeshData(mesh); + for (size_t i = 0; i < vertexCount; ++i) { + vertices[i] = r3VectorAdd(vertices[i], center); + } + R3RigidBodyHandle proxy = r3SoftBody_RootBody(skinned); + R3Pose pose = r3RigidBody_Position(proxy); + R3ColliderDesc collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetTrimesh(&collider.shape, (R3VectorView){vertices, vertexCount}, + (R3TriangleView){(const R3Triangle *)indices, indexCount / 3}, + R3_TRIMESH_DEFORMABLE); + + collider.position = r3PoseInverse(pose); + collider.isSensor = 1; + R3SoftMeshBindingDesc binding = r3DefaultSoftMeshBindingDesc(); + r3InsertDeformableCollider(&collider, &binding, proxy); + free(vertices); + free(indices); + } + /* Plated jelly. */ + R3SoftBodyHandle plated; + { + const R3Vector center = {0, 1.2, 0}; + R3SoftBodyDesc builder = jelly(center, .6, 4, 400); + builder.canSleep = !testbed->noSleep; + plated = r3InsertSoftBody(world, &builder); + + R3Vector positions[64]; + size_t count = r3SoftBody_ParticlePositions(plated, positions, 64); + uint32_t top[64]; + size_t topCount = 0; + for (size_t i = 0; i < count; ++i) { + if (positions[i].y > center.y + .3) { + top[topCount++] = i; + } + } + uint32_t cluster = r3SoftBody_AddCluster(plated, top, topCount); + + R3RigidBodyHandle proxy = r3SoftBody_ClusterProxy(plated, cluster); + R3ColliderDesc plate = r3CuboidColliderDesc(r3Vector(.7, .05, .7)); + plate.position.translation = r3Vector(0, .65, 0); + r3InsertCollider(proxy, &plate); + } + /* Split jelly. */ + R3SoftBodyHandle split; + { + const R3Vector center = {3, 1.2, 0}; + R3SoftBodyDesc builder = jelly(center, .6, 4, 400); + builder.canSleep = !testbed->noSleep; + builder.collisionEnabled = 0; + split = r3InsertSoftBody(world, &builder); + + R3Vector positions[64]; + size_t count = r3SoftBody_ParticlePositions(split, positions, 64); + uint32_t halves[2][64]; + size_t counts[2] = {0}; + for (size_t i = 0; i < count; ++i) { + size_t side = positions[i].x < center.x ? 0 : 1; + halves[side][counts[side]++] = i; + } + for (size_t side = 0; side < 2; ++side) { + uint32_t cluster = r3SoftBody_AddCluster(split, halves[side], counts[side]); + + R3RigidBodyHandle proxy = r3SoftBody_ClusterProxy(split, cluster); + R3Vector *vertices; + uint32_t *indices; + size_t triangleCount; + clusterSurface(split, halves[side], counts[side], &vertices, &indices, + &triangleCount); + R3Pose pose = r3RigidBody_Position(proxy); + R3ColliderDesc collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetTrimesh(&collider.shape, (R3VectorView){vertices, counts[side]}, + (R3TriangleView){(const R3Triangle *)indices, triangleCount}, + R3_TRIMESH_DEFORMABLE); + + collider.position = r3PoseInverse(pose); + collider.friction = .7; + R3SoftMeshBindingDesc binding = r3DefaultSoftMeshBindingDesc(); + binding.kind = R3_SOFT_BINDING_DIRECT; + binding.particles = (R3IndexView){halves[side], counts[side]}; + binding.selfContacts = 1; + r3InsertDeformableCollider(&collider, &binding, proxy); + free(vertices); + free(indices); + } + } + for (int i = 0; i < 3; ++i) { + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-3 + 3 * i, 4 + i, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.3, 0.3, 0.3)); + collider.density = 4; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + tbCamera(testbed, -6, 4, 8, 0, 1, 0); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_pile3.c b/c/testbed/examples3d/soft_pile3.c new file mode 100644 index 000000000..7fa18ce81 --- /dev/null +++ b/c/testbed/examples3d/soft_pile3.c @@ -0,0 +1,94 @@ +/* Port of examples3d/soft_pile3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftPile3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle floor; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.5, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(5.5, 0.5, 5.5)); + rigidBody.canSleep = !testbed->noSleep; + floor = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(floor, &boxCollider); + + tbBodyColor(testbed, floor, 0.6, 0.7, 1, 0.3); + const int dx[] = {1, -1, 0, 0}; + const int dz[] = {0, 0, 1, -1}; + for (int i = 0; i < 4; i++) { + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(dx[i] * 5.25, 4, dz[i] * 5.25); + R3ColliderDesc collider = + r3CuboidColliderDesc(r3Vector(dx[i] ? 0.25 : 5.5, 4, dx[i] ? 5.5 : 0.25)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + + tbBodyColor(testbed, handle, 0.6, 0.7, 1, 0.3); + } + int k = 0; + for (int layer = 0; layer < 40; layer++) { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++, k++) { + R3Vector position = r3Vector(-3 + i * 3 + layer % 2 * 0.7, 3 + layer * 2.5, + -3 + j * 3 + layer % 3 * 0.5); + if (k % 5 == 4) { + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.4, 0.4, 0.4)); + collider.density = 0.5; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = position; + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } else { + int large = k % 5 == 0 || k % 5 == 2; + R3Real radius = large ? 0.08 : 0.07; + R3SoftBodyDesc softBody = r3SphereSoftBodyDesc(position, large ? 0.6 : 0.45, 1); + softBody.material = + r3UniformSoftBodyMaterial((R3SpringCoefficients){large ? 15 : 10, 1}); + softBody.volumeFactor = large ? 1.15 : 1.3; + softBody.particleMass = 0.03; + softBody.particleRadius = (R3OptionalReal){1, radius}; + R3ColliderDesc surfaceCollider = r3BallColliderDesc(radius); + surfaceCollider.friction = 0.6; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + } + } + } + } + for (int i = 0; i < 3; i++) { + R3SoftBodyDesc softBody = r3RopeSoftBodyDesc(r3Vector(-3.5, 24 + i, -2 + i * 2), + r3Vector(3.5, 24 + i, -1.5 + i * 2), 30); + softBody.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + softBody.particleMass = 0.03; + R3ColliderDesc surfaceCollider2 = r3BallColliderDesc(0.08); + surfaceCollider2.friction = 0.6; + softBody.collider = surfaceCollider2; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + } + /* Set up the viewer. */ + tbCamera(testbed, 14, 12, 14, 0, 4, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_plasticity3.c b/c/testbed/examples3d/soft_plasticity3.c new file mode 100644 index 000000000..5817ca473 --- /dev/null +++ b/c/testbed/examples3d/soft_plasticity3.c @@ -0,0 +1,186 @@ +/* Port of examples3d/soft_plasticity3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static R3SoftBodyMaterial clay(R3Real young, R3Real plasticYield, R3Real plasticCreep) { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = young; + material.poissonRatio = .35; + material.elasticDampingRatio = 1; + material.plasticYield = plasticYield; + material.plasticCreep = plasticCreep; + material.deformationDamping = 4; + return material; +} + +void tbSoftPlasticity3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(14, 0.1, 14)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(9, 2, 5); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.2, 2, 3)); + collider.friction = .8; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Yield ladder: elastic to increasingly plastic, hit by identical balls. */ + const R3Real yields[] = {0, .2, .08, .02}; + for (size_t i = 0; i < TB_COUNT(yields); ++i) { + const R3Real x = -7 + i * 2.6; + R3SoftBodyDesc block = + r3CuboidSoftBodyDesc(r3Vector(x, 0.6, -4), r3Vector(0.6, 0.6, 0.6), 5, 5, 5); + block.cellModel = R3_SOFT_CELL_COROTATIONAL; + block.particleMass = .1; + block.canSleep = !testbed->noSleep; + { + R3SoftBodyMaterial material = clay(1.0e4, yields[i], 20); + block.material = material; + } + { + R3ColliderDesc surface = r3BallColliderDesc(.12); + surface.friction = .8; + block.collider = surface; + } + r3InsertSoftBody(world, &block); + + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x, 5, -4); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3BallColliderDesc(.4); + collider.density = 5; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + /* Clay slab stamped by a kinematic press. */ + R3SoftBodyDesc slab = + r3CuboidSoftBodyDesc(r3Vector(0, 0.4, 1.5), r3Vector(2.4, 0.4, 1.4), 13, 3, 8); + slab.cellModel = R3_SOFT_CELL_COROTATIONAL; + slab.particleMass = .1; + slab.canSleep = !testbed->noSleep; + { + R3SoftBodyMaterial material = clay(3.0e4, .02, 50); + slab.material = material; + } + { + R3ColliderDesc surface = r3BallColliderDesc(.15); + surface.friction = .8; + slab.collider = surface; + } + r3InsertSoftBody(world, &slab); + + const R3Vector pressRest = r3Vector(-1.5, 2.2, 1.5); + R3RigidBodyHandle press; + { + R3RigidBodyDesc rigidBody = r3KinematicPositionBasedRigidBodyDesc(); + rigidBody.position.translation = pressRest; + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.35, 0.35, 0.35)); + collider.position.rotation = r3RotationFromAxisAngle(r3Vector(0, 0, 1), R3_PI / 4); + collider.friction = .5; + press = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(press, &collider); + } + /* Elastic and creeping columns under their own weight. */ + for (int i = 0; i < 2; ++i) { + const R3Real x = -6 + i * 2; + R3SoftBodyDesc column = + r3CuboidSoftBodyDesc(r3Vector(x, 1.2, 5.5), r3Vector(0.3, 1.2, 0.3), 3, 12, 3); + column.cellModel = R3_SOFT_CELL_COROTATIONAL; + column.particleMass = .05; + column.canSleep = !testbed->noSleep; + { + R3SoftBodyMaterial material = clay(2.0e3, i == 0 ? 0 : .05, .5); + column.material = material; + } + { + R3ColliderDesc surface = r3BallColliderDesc(.1); + surface.friction = 1; + column.collider = surface; + } + r3InsertSoftBody(world, &column); + } + /* Volumetric clay balls thrown at the wall. */ + R3SharedShape *shape = r3BallSharedShape(.5); + R3TriMeshData *mesh = r3SharedShape_ToTrimesh(shape, 12, 12); + r3FreeSharedShape(shape); + size_t vertexCount, indexCount; + vertexCount = r3TriMeshData_Vertices(mesh, NULL, 0); + indexCount = r3TriMeshData_Indices(mesh, NULL, 0); + R3Vector *vertices = malloc(vertexCount * sizeof(*vertices)); + uint32_t *indices = malloc(indexCount * sizeof(*indices)); + if (!vertices || !indices) { + abort(); + } + indexCount = r3TriMeshData_Indices(mesh, indices, indexCount); + for (int i = 0; i < 3; ++i) { + const R3Vector center = r3Vector(4 - i * 1.5, 2 + i * .3, 3.5 + i * 1.5); + vertexCount = r3TriMeshData_Vertices(mesh, vertices, vertexCount); + for (size_t k = 0; k < vertexCount; ++k) { + vertices[k] = r3VectorAdd(vertices[k], center); + } + + R3VolumeMeshParameters ballMeshing = r3NewVolumeMeshParameters(.2); + R3SoftBodyDesc ball = r3VolumetricSoftBodyDesc( + (R3VectorView){vertices, vertexCount}, + (R3SurfaceElementView){(const R3Triangle *)indices, indexCount / 3}, ballMeshing); + { + ball.cellModel = R3_SOFT_CELL_COROTATIONAL; + R3SoftBodyMaterial material = clay(1.0e4, .03, 60); + ball.material = material; + ball.particleMass = .05; + ball.canSleep = !testbed->noSleep; + { + R3ColliderDesc surface = r3BallColliderDesc(.1); + surface.friction = .8; + ball.collider = surface; + } + R3SoftBodyHandle handle = r3InsertSoftBody(world, &ball); + + size_t count = r3SoftBody_NumParticles(handle); + for (size_t k = 0; k < count; ++k) { + r3SoftBody_SetParticleVelocity(handle, k, r3Vector(10, 1, 0)); + } + } + } + free(vertices); + free(indices); + r3FreeTriMeshData(mesh); + + tbCamera(testbed, 4, 9, 16, 0, .5, 0); + + tbSetWorld(testbed, world); + R3Real t = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + R3Real dt = r3TimeStep(world); + t += dt; + /* One second down, one second up, then move to the next stamp. */ + const R3Real period = 3; + const R3Real cycle = floor(t / period); + const R3Real phase = t - cycle * period; + const R3Real x = pressRest.x + fmod(cycle, 4); + const R3Real depth = 1; + const R3Real y = pressRest.y - (phase < 1 ? depth * phase + : phase < 2 ? depth * (2 - phase) + : 0); + + r3RigidBody_SetNextKinematicTranslation(press, r3Vector(x, y, pressRest.z)); + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_surface3.c b/c/testbed/examples3d/soft_surface3.c new file mode 100644 index 000000000..8dfdb9d8a --- /dev/null +++ b/c/testbed/examples3d/soft_surface3.c @@ -0,0 +1,154 @@ +/* Port of examples3d/soft_surface3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSoftSurface3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(40, 1, 40)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &boxCollider); + + uint32_t pins[76]; + size_t np = 0; + for (uint32_t i = 0; i < 400; i++) { + if (i / 20 == 0 || i / 20 == 19 || i % 20 == 0 || i % 20 == 19) { + pins[np++] = i; + } + } + R3SoftBodyDesc softBody = + r3ClothSoftBodyDesc(r3Vector(-2, 3, -2), r3Vector(0.2, 0, 0), r3Vector(0, 0, 0.2), 20, 20); + r3SoftBodyDesc_SetPinnedParticles(&softBody, (R3IndexView){(const uint32_t *)pins, np}); + softBody.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){40, 1}); + softBody.particleMass = 0.05; + softBody.particleRadius = (R3OptionalReal){1, 0.05}; + R3ColliderDesc surfaceCollider = r3BallColliderDesc(0.05); + surfaceCollider.friction = 0.6; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 6; j++) { + for (int h = 0; h < 3; h++) { + R3ColliderDesc collider = r3BallColliderDesc(0.08); + collider.density = 2; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-1.2 + i * 0.5 + h % 2 * 0.2, 5 + h * 0.6, + -1.2 + j * 0.5 + h % 2 * 0.2); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + for (int i = 0; i < 5; i++) { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-3 + i * 1.5, 0.5, 6); + R3ColliderDesc collider = r3CapsuleYColliderDesc(0.5, 0.08); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + R3RigidBodyDesc bar = r3FixedRigidBodyDesc(); + bar.position.translation = r3Vector(0, 1, 8); + bar.position = r3Pose(r3Vector(0, 1, 8), r3RotationFromAxisAngle(r3Vector(1, 0, 0), R3_PI / 4)); + R3ColliderDesc barCollider = r3CuboidColliderDesc(r3Vector(3.5, 0.4, 0.4)); + bar.canSleep = !testbed->noSleep; + rigidBodyHandle = r3InsertRigidBody(world, &bar); + r3InsertCollider(rigidBodyHandle, &barCollider); + + softBody = + r3ClothSoftBodyDesc(r3Vector(-4, 3, 5), r3Vector(0.2, 0, 0), r3Vector(0, 0, 0.2), 40, 25); + softBody.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){30, 1}; + material.volumeSoftness = (R3SpringCoefficients){30, 1}; + material.shapeMatchingSoftness = (R3SpringCoefficients){30, 1}; + material.bendSoftness = (R3SpringCoefficients){3, 1}; + softBody.material = material; + softBody.particleMass = 0.02; + softBody.particleRadius = (R3OptionalReal){1, 0.04}; + R3ColliderDesc drapedClothCollider = r3BallColliderDesc(0.04); + drapedClothCollider.friction = 0.8; + softBody.collider = drapedClothCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + for (int i = 0; i < 3; i++) { + softBody = r3CuboidSoftBodyDesc(r3Vector(9, 0.8 + i * 1.7, 6), r3Vector(0.75, 0.75, 0.75), + 4, 4, 4); + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + softBody.cellModel = R3_SOFT_CELL_COROTATIONAL; + material.youngModulus = 6.0e3; + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.particleMass = 0.1; + softBody.particleRadius = (R3OptionalReal){1, 0.05}; + R3ColliderDesc jellySurfaceCollider = r3BallColliderDesc(0.05); + jellySurfaceCollider.friction = 0.8; + softBody.collider = jellySurfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + } + R3ColliderDesc colliderC = r3CuboidColliderDesc(r3Vector(0.4, 0.4, 0.4)); + colliderC.density = 2; + R3RigidBodyDesc dynamicBody = r3DynamicRigidBodyDesc(); + dynamicBody.position.translation = r3Vector(9, 6.5, 6); + dynamicBody.canSleep = !testbed->noSleep; + rigidBodyHandle = r3InsertRigidBody(world, &dynamicBody); + r3InsertCollider(rigidBodyHandle, &colliderC); + + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(-9, 0.75, 6); + R3ColliderDesc obstacleCollider = r3CuboidColliderDesc(r3Vector(0.4, 0.75, 0.4)); + groundBody.canSleep = !testbed->noSleep; + rigidBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(rigidBodyHandle, &obstacleCollider); + + softBody = + r3ClothSoftBodyDesc(r3Vector(-13, 4, 5.6), r3Vector(0.1, 0, 0), r3Vector(0, 0, 0.1), 80, 8); + softBody.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + R3SoftBodyMaterial materialValue = r3DefaultSoftBodyMaterial(); + materialValue.edgeSoftness = (R3SpringCoefficients){30, 1}; + materialValue.volumeSoftness = (R3SpringCoefficients){30, 1}; + materialValue.shapeMatchingSoftness = (R3SpringCoefficients){30, 1}; + materialValue.bendSoftness = (R3SpringCoefficients){2, 1}; + softBody.material = materialValue; + softBody.selfContacts = 1; + softBody.particleMass = 0.02; + softBody.particleRadius = (R3OptionalReal){1, 0.05}; + R3ColliderDesc stripSurfaceCollider = r3BallColliderDesc(0.05); + stripSurfaceCollider.friction = 0.6; + softBody.collider = stripSurfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + /* Set up the viewer. */ + tbCamera(testbed, 0, 12, 24, 0, 1.5, 3); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_tearing3.c b/c/testbed/examples3d/soft_tearing3.c new file mode 100644 index 000000000..86b349231 --- /dev/null +++ b/c/testbed/examples3d/soft_tearing3.c @@ -0,0 +1,217 @@ +/* Port of examples3d/soft_tearing3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +typedef struct DrivenParticle { + R3SoftBodyHandle body; + uint32_t index; + R3Vector rest; + int right; +} DrivenParticle; + +/* Follow driven particle indices through compaction and newly split bodies. */ +static void followTears(R3EventCollector *events, DrivenParticle *ends, size_t endCount) { + size_t count = r3EventCollector_TearEventCount(events); + for (size_t i = 0; i < count; ++i) { + R3SoftBodyTearEvent *event = r3EventCollector_TearEvent(events, i); + R3SoftBodyHandle origin = r3SoftBodyTearEvent_SoftBody(event); + for (size_t j = 0; j < endCount; ++j) { + if (ends[j].body.index == origin.index && + ends[j].body.generation == origin.generation) { + R3Bool found; + R3SoftBodyHandle destination; + uint32_t index; + R3OptionalParticleDestination softBodyTearEventTryParticleDestinationResult = + r3SoftBodyTearEvent_TryParticleDestination(event, ends[j].index); + destination = softBodyTearEventTryParticleDestinationResult.body; + index = softBodyTearEventTryParticleDestinationResult.index; + found = softBodyTearEventTryParticleDestinationResult.found; + if (found) { + ends[j].body = destination; + ends[j].index = index; + } + } + } + r3FreeSoftBodyTearEvent(event); + } +} + +void tbSoftTearing3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(30, 0.1, 30)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Sheet. */ + { + const size_t nu = 40, nv = 40; + uint32_t pinned[1600]; + size_t pinnedCount = 0; + for (size_t i = 0; i < nu; ++i) { + for (size_t j = 0; j < nv; ++j) { + if (i == 0 || j == 0 || i == nu - 1 || j == nv - 1) { + pinned[pinnedCount++] = (uint32_t)(i * nv + j); + } + } + } + R3SoftBodyDesc sheet = r3ClothSoftBodyDesc(r3Vector(-6, 3, -2), r3Vector(0.1, 0, 0), + r3Vector(0, 0, 0.1), nu, nv); + r3SoftBodyDesc_SetPinnedParticles(&sheet, + (R3IndexView){(const uint32_t *)pinned, pinnedCount}); + sheet.particleMass = .02; + sheet.canSleep = !testbed->noSleep; + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){100, 1.0}; + material.bendSoftness = (R3SpringCoefficients){100, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){100, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){100, 1.0}; + material.bendSoftness = (R3SpringCoefficients){3, 1}; + material.tearStrain = (R3OptionalReal){1, .4}; + sheet.material = material; + { + R3ColliderDesc surface = r3BallColliderDesc(.05); + surface.friction = .8; + sheet.collider = surface; + } + r3InsertSoftBody(world, &sheet); + } + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-4, 6, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3BallColliderDesc(.6); + collider.density = 30; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Curtain. */ + { + const size_t nu = 50, nv = 40; + uint32_t pinned[2000]; + size_t pinnedCount = 0; + for (size_t i = 0; i < nu; ++i) { + for (size_t j = 0; j < nv; ++j) { + if (j == 0) { + pinned[pinnedCount++] = (uint32_t)(i * nv + j); + } + } + } + R3SoftBodyDesc curtain = r3ClothSoftBodyDesc(r3Vector(0, 4.5, 4), r3Vector(0.1, 0, 0), + r3Vector(0, -0.1, 0), nu, nv); + r3SoftBodyDesc_SetPinnedParticles(&curtain, + (R3IndexView){(const uint32_t *)pinned, pinnedCount}); + curtain.particleMass = .02; + curtain.canSleep = !testbed->noSleep; + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){100, 1.0}; + material.bendSoftness = (R3SpringCoefficients){100, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){100, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){100, 1.0}; + material.bendSoftness = (R3SpringCoefficients){3, 1}; + material.tearStrain = (R3OptionalReal){1, .2}; + curtain.material = material; + r3InsertSoftBody(world, &curtain); + } + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(2.5, 2.5, 12); + rigidBody.linvel = r3Vector(0, 0, -25); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.3, 0.3, 0.3)); + collider.position.rotation = r3RotationFromAxisAngle(r3Vector(.5, .5, .5), sqrt(.75)); + collider.density = 20; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Jelly bar pulled apart by its two pinned ends. */ + R3SoftBodyHandle bar; + R3SoftBodyDesc builder = + r3CuboidSoftBodyDesc(r3Vector(3, 1, -4), r3Vector(2, 0.4, 0.4), 21, 5, 5); + builder.cellModel = R3_SOFT_CELL_COROTATIONAL; + builder.particleMass = .05; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = 5.0e4; + material.poissonRatio = .3; + material.elasticDampingRatio = 1; + material.tearStrain = (R3OptionalReal){1, .4}; + builder.material = material; + } + { + R3ColliderDesc surface = r3BallColliderDesc(.1); + surface.friction = .8; + builder.collider = surface; + } + builder.canSleep = !testbed->noSleep; + bar = r3InsertSoftBody(world, &builder); + + size_t count = r3SoftBody_NumParticles(bar); + DrivenParticle *ends = malloc(count * sizeof(*ends)); + if (!ends) { + abort(); + } + size_t endCount = 0; + for (size_t i = 0; i < count; ++i) { + R3Vector position = r3SoftBody_ParticlePosition(bar, i); + int left = position.x < 1.01; + int right = position.x > 4.99; + if (left || right) { + ends[endCount++] = (DrivenParticle){bar, (uint32_t)i, position, right}; + r3SoftBody_SetParticlePinned(bar, i, 1); + } + } + R3EventCollector *events = r3NewEventCollector(); + + tbCamera(testbed, 9, 8, 16, 0, 1.5, 1); + + tbSetWorld(testbed, world); + R3Real t = 0; + uint32_t previousMinPiece = 0; + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + const int useDefault = (int)tbLiveSetting(testbed, "Default minimum piece", 1, 0, 1, 1); + const uint32_t selected = + (uint32_t)tbLiveSetting(testbed, "Minimum piece (elements)", 6, 1, 40, 1); + const uint32_t minPiece = useDefault ? 0 : selected; + if (minPiece != previousMinPiece) { + size_t bodyCount = r3SoftBodyCount(world); + R3SoftBodyHandle *handles = malloc(bodyCount * sizeof(*handles)); + if (bodyCount && !handles) { + abort(); + } + bodyCount = r3SoftBodyHandles(world, handles, bodyCount); + for (size_t i = 0; i < bodyCount; ++i) { + R3SoftBodyMaterial material = r3SoftBody_Material(handles[i]); + material.minPiece = (R3OptionalU32){minPiece != 0, minPiece}; + r3SoftBody_SetMaterial(handles[i], &material); + } + free(handles); + previousMinPiece = minPiece; + } + if (tbSimulating(testbed)) { + R3Real dt = r3TimeStep(world); + t += dt; + /* Move the right clamp after one second, stopping at twice the bar's length. */ + const R3Real shift = fmin(fmax(t - 1, 0) * .5, 4); + for (size_t i = 0; i < endCount; ++i) { + if (ends[i].right) { + r3SoftBody_SetParticleKinematicTarget( + ends[i].body, ends[i].index, + r3VectorAdd(ends[i].rest, r3Vector(shift, 0, 0))); + } + } + r3EventCollector_Clear(events); + r3Step(world, NULL, events); + followTears(events, ends, endCount); + } + } + free(ends); + r3FreeEventCollector(events); + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_thin_features3.c b/c/testbed/examples3d/soft_thin_features3.c new file mode 100644 index 000000000..cadcbe20e --- /dev/null +++ b/c/testbed/examples3d/soft_thin_features3.c @@ -0,0 +1,240 @@ +/* Port of examples3d/soft_thin_features3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +static R3SoftBodyDesc jelly(R3Vector center, R3Real half, size_t n, R3Real young) { + R3SoftBodyDesc builder = r3CuboidSoftBodyDesc(center, r3Vector(half, half, half), n, n, n); + builder.cellModel = R3_SOFT_CELL_COROTATIONAL; + builder.particleMass = .1; + builder.particleRadius = (R3OptionalReal){1, .06}; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = young; + material.poissonRatio = .4; + material.elasticDampingRatio = .5; + builder.material = material; + } + { + R3ColliderDesc surface = r3BallColliderDesc(.06); + surface.friction = .7; + builder.collider = surface; + } + return builder; +} + +static R3SoftBodyDesc cloth(R3Vector origin, R3Vector du, R3Vector dv, size_t nu, size_t nv) { + R3SoftBodyDesc builder = r3ClothSoftBodyDesc(origin, du, dv, nu, nv); + builder.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + builder.particleMass = .02; + builder.particleRadius = (R3OptionalReal){1, .05}; + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){30, 1.0}; + material.bendSoftness = (R3SpringCoefficients){30, 1.0}; + material.volumeSoftness = (R3SpringCoefficients){30, 1.0}; + material.shapeMatchingSoftness = (R3SpringCoefficients){30, 1.0}; + material.bendSoftness = (R3SpringCoefficients){3, 1}; + builder.material = material; + { + R3ColliderDesc surface = r3BallColliderDesc(.05); + surface.friction = .6; + builder.collider = surface; + } + return builder; +} + +void tbSoftThinFeatures3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.5, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(30, 0.5, 30)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Bed of nails. */ + for (int i = 0; i < 10; ++i) { + for (int j = 0; j < 10; ++j) { + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-8 + i * .45, 0.6, -2 + j * .45); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CapsuleYColliderDesc(.6, .03); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + { + R3SoftBodyDesc body = jelly(r3Vector(-7, 3.5, -1), .75, 5, 2.0e4); + body.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &body); + } + { + R3SoftBodyDesc balloon = r3SphereSoftBodyDesc(r3Vector(-4.8, 3.5, 1.2), .7, 2); + balloon.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){15, 1}); + balloon.volumeFactor = 1.2; + balloon.particleMass = .03; + balloon.particleRadius = (R3OptionalReal){1, .06}; + { + R3ColliderDesc surface = r3BallColliderDesc(.06); + surface.friction = .6; + balloon.collider = surface; + } + balloon.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &balloon); + } + { + R3SoftBodyDesc sheet = + cloth(r3Vector(-8.5, 5.5, -2.5), r3Vector(.12, 0, 0), r3Vector(0, 0, .12), 30, 30); + sheet.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &sheet); + } + /* Knife edges. */ + for (int i = 0; i < 4; ++i) { + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-1.5, 0.75, -3 + i * .8); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1.2, 0.75, 0.015)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + { + R3SoftBodyDesc body = jelly(r3Vector(-1.5, 3, -1.8), .9, 5, 5.0e3); + body.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &body); + } + /* Wire grid. */ + for (int i = 0; i < 7; ++i) { + const R3Real t = -1.8 + i * .6; + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(t, 2.5, 5); + rigidBody.position.rotation = r3RotationFromAxisAngle(r3Vector(1, 0, 0), R3_PI / 2); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CapsuleYColliderDesc(1.8, .02); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 2.5, 5 + t); + rigidBody.position.rotation = r3RotationFromAxisAngle(r3Vector(0, 0, 1), R3_PI / 2); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CapsuleYColliderDesc(1.8, .02); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + { + R3SoftBodyDesc sheet = + cloth(r3Vector(-1.5, 4.5, 3.5), r3Vector(.12, 0, 0), r3Vector(0, 0, .12), 26, 26); + sheet.selfContacts = 1; + sheet.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &sheet); + } + /* Needle rain on a trampoline and a jelly block. */ + const size_t n = 26; + uint32_t pinned[26 * 26]; + size_t pinnedCount = 0; + for (size_t i = 0; i < n; ++i) { + for (size_t j = 0; j < n; ++j) { + if (i == 0 || j == 0 || i == n - 1 || j == n - 1) { + pinned[pinnedCount++] = (uint32_t)(i * n + j); + } + } + } + { + R3SoftBodyDesc trampoline = + cloth(r3Vector(3, 2.5, -3.5), r3Vector(.12, 0, 0), r3Vector(0, 0, .12), n, n); + r3SoftBodyDesc_SetPinnedParticles(&trampoline, + (R3IndexView){(const uint32_t *)pinned, pinnedCount}); + trampoline.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){40, 1}); + trampoline.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &trampoline); + } + { + R3SoftBodyDesc body = jelly(r3Vector(4.5, .9, 3), .9, 5, 3.0e4); + body.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &body); + } + const R3Real locations[][3] = {{4.5, -2, 6}, {4.5, 3, 5}}; + for (int i = 0; i < 6; ++i) { + for (int j = 0; j < 6; ++j) { + for (size_t k = 0; k < TB_COUNT(locations); ++k) { + const R3Real cx = locations[k][0], cz = locations[k][1], h = locations[k][2]; + const R3Vector rotation = r3Vector(.3 * i, 0, .2 * j); + const R3Real angle = sqrt(rotation.x * rotation.x + rotation.z * rotation.z); + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(cx - 1 + i * .4, h + (i + j) * .3, cz - 1 + j * .4); + rigidBody.position.rotation = r3RotationFromAxisAngle(rotation, angle); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CapsuleYColliderDesc(.5, .02); + collider.density = 3; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + } + /* Thin plates and a long rod. */ + { + R3SoftBodyDesc body = jelly(r3Vector(9, .9, -2), .9, 5, 1.0e4); + body.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &body); + } + for (int i = 0; i < 4; ++i) { + const R3Vector rotation = r3Vector(.1 * i, .5 * i, .05); + const R3Real angle = + sqrt(rotation.x * rotation.x + rotation.y * rotation.y + rotation.z * rotation.z); + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(9, 3.5 + i * .5, -2); + rigidBody.position.rotation = r3RotationFromAxisAngle(rotation, angle); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.8, 0.015, 0.8)); + collider.density = 1; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + { + R3SoftBodyDesc balloon = r3SphereSoftBodyDesc(r3Vector(9, 1, 3), .9, 2); + balloon.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){15, 1}); + balloon.volumeFactor = 1.2; + balloon.particleMass = .03; + balloon.particleRadius = (R3OptionalReal){1, .06}; + { + R3ColliderDesc surface = r3BallColliderDesc(.06); + surface.friction = .6; + balloon.collider = surface; + } + balloon.canSleep = !testbed->noSleep; + r3InsertSoftBody(world, &balloon); + } + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(9, 3.5, 3); + rigidBody.position.rotation = r3RotationFromAxisAngle(r3Vector(0, 0, 1), R3_PI / 2); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CapsuleYColliderDesc(2.5, .03); + collider.density = 2; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + tbCamera(testbed, 2, 10, 18, 1, 1.5, 1); + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/soft_trimesh3.c b/c/testbed/examples3d/soft_trimesh3.c new file mode 100644 index 000000000..9215fc180 --- /dev/null +++ b/c/testbed/examples3d/soft_trimesh3.c @@ -0,0 +1,165 @@ +/* Port of examples3d/soft_trimesh3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include "utils/obj.h" + +void tbSoftTrimesh3(Testbed *testbed) { + R3World *world = r3NewWorld(); + tbLabel(testbed, "Tetrahedra", "..."); + static const char *const modelNames[] = { + "All", "Camel", "Chair", "Cup", "Dinosaur", "Torus 2", "Feline", "Genus 3", + "Torus", "Octopus", "Rabbit", "Rust logo", "Screwdriver", "Table", "Torus 3", "Hornbug"}; + const int selectedModel = + (int)tbChoice(testbed, "Model", 0, modelNames, TB_COUNT(modelNames), 0, 0); + R3VolumeMeshParameters meshing = + r3NewVolumeMeshParameters(tbSetting(testbed, "Cell size", 2, .4, 4, 0)); + meshing.enclosure = (uint32_t)tbSetting(testbed, "Crust (surface shell only)", 0, 0, 1, 1); + meshing.cover_smoothing = (uint32_t)tbSetting(testbed, "Cover smoothing", 20, 0, 50, 1); + meshing.cover_guard = tbSetting(testbed, "Cover guard", .15, .02, .5, 0); + meshing.cover_subdivisions = (uint32_t)tbSetting(testbed, "Cover subdivision", 1, 0, 3, 1); + const int wearSkin = (int)tbSetting(testbed, "Wear the model as a skin", 1, 0, 1, 1); + const int skinCollision = wearSkin ? (int)tbSetting(testbed, "Skin collisions", 0, 0, 1, 1) : 0; +#ifdef RAPIER_FEM + const int femSolver = (int)tbSetting(testbed, "FEM solver", 0, 0, 1, 1); +#endif + const int cellModel = + (int)tbSetting(testbed, "Cell model: 0 Corotational, 1 Neo-Hookean, 2 Volume", 0, 0, 2, 1); + const R3Real youngModulus = + cellModel == 2 ? 1e5 : tbSetting(testbed, "Young modulus", 1e5, 1e4, 1e6, 0); + const int selfContacts = (int)tbSetting(testbed, "Self contacts", 0, 0, 1, 1); + /* Floor made of a wavy mesh. */ + R3Real heights[101 * 101]; + for (size_t j = 0; j <= 100; ++j) { + for (size_t i = 0; i <= 100; ++i) { + heights[i + j * 101] = -cos(i * .2) - cos(j * .2); + } + } + R3SharedShape *heightfield = r3HeightfieldSharedShape((R3RealView){heights, (101) * (101)}, 101, + 101, r3Vector(100, 2, 100)); + R3TriMeshData *floor = r3SharedShape_ToTrimesh(heightfield, 3, 2); + r3FreeSharedShape(heightfield); + size_t vertexCount = 0, indexCount = 0; + vertexCount = r3TriMeshData_Vertices(floor, NULL, 0); + indexCount = r3TriMeshData_Indices(floor, NULL, 0); + R3Vector *vertices = malloc(vertexCount * sizeof(*vertices)); + uint32_t *indices = malloc(indexCount * sizeof(*indices)); + if (!vertices || !indices) { + abort(); + } + vertexCount = r3TriMeshData_Vertices(floor, vertices, vertexCount); + indexCount = r3TriMeshData_Indices(floor, indices, indexCount); + R3ColliderDesc collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetTrimesh(&collider.shape, (R3VectorView){vertices, vertexCount}, + (R3TriangleView){(const R3Triangle *)indices, indexCount / 3}, + R3_TRIMESH_FIX_INTERNAL_EDGES); + r3InsertColliderWithoutParent(world, &collider); + + free(vertices); + free(indices); + r3FreeTriMeshData(floor); + + const char *models[] = {"camel_decimated.obj", + "chair.obj", + "cup_decimated.obj", + "dilo_decimated.obj", + "tstTorusModel2.obj", + "feline_decimated.obj", + "genus3_decimated.obj", + "tstTorusModel.obj", + "octopus_decimated.obj", + "rabbit_decimated.obj", + "rust_logo_simplified.obj", + "screwdriver_decimated.obj", + "table.obj", + "tstTorusModel3.obj", + "hornbug.obj"}; + const size_t ngeoms = selectedModel == 0 ? TB_COUNT(models) : 1; + const size_t width = (size_t)fmax(ceil(sqrt(ngeoms)), 1); + size_t totalCells = 0, totalBodies = 0, igeom = 0; + for (size_t modelIndex = 0; modelIndex < TB_COUNT(models); ++modelIndex) { + if (selectedModel && modelIndex + 1 != (size_t)selectedModel) { + continue; + } + const size_t slot = igeom++; + ObjMesh mesh; + if (!loadObj(testbed->assetRoot, models[modelIndex], &mesh)) { + continue; + } + R3Vector mins = mesh.vertices[0], maxs = mins; + for (size_t i = 1; i < mesh.vertexCount; ++i) { + R3Vector p = mesh.vertices[i]; + mins = r3Vector(fmin(mins.x, p.x), fmin(mins.y, p.y), fmin(mins.z, p.z)); + maxs = r3Vector(fmax(maxs.x, p.x), fmax(maxs.y, p.y), fmax(maxs.z, p.z)); + } + const R3Vector center = r3VectorScale(r3VectorAdd(mins, maxs), .5); + const R3Real diag = r3VectorLength(r3VectorSub(maxs, mins)); + for (size_t i = 0; i < mesh.vertexCount; ++i) { + mesh.vertices[i] = r3VectorScale(r3VectorSub(mesh.vertices[i], center), 10 / diag); + } + R3SoftBodyDesc filled = r3VolumetricSoftBodyDesc( + (R3VectorView){mesh.vertices, mesh.vertexCount}, + (R3SurfaceElementView){(const R3Triangle *)mesh.indices, mesh.indexCount / 3}, meshing); + const R3Real x = (slot % width) * 9.0 - (width - 1) * 9.0 / 2; + const R3Real y = (slot / width) * 8.0 + 7; + if (wearSkin) { + r3SoftBodyDesc_SetSkin( + &filled, (R3VectorView){mesh.vertices, mesh.vertexCount}, + (R3SurfaceElementView){(const R3Triangle *)mesh.indices, mesh.indexCount / 3}); + filled.skinCollision = skinCollision; + } + filled.translation = r3Vector(x, y, 0); + filled.cellModel = cellModel == 1 ? R3_SOFT_CELL_NEO_HOOKEAN + : cellModel == 2 ? R3_SOFT_CELL_VOLUME + : R3_SOFT_CELL_COROTATIONAL; + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = youngModulus; + material.poissonRatio = .35; + material.elasticDampingRatio = .5; + material.deformationDamping = 2.5; + filled.material = material; + filled.particleMass = .05; + filled.particleRadius = + (R3OptionalReal){1, meshing.cell_size / (1u << meshing.cover_subdivisions) * .25}; + filled.selfContacts = selfContacts; + filled.canSleep = !testbed->noSleep; + { + R3ColliderDesc surface = r3BallColliderDesc(.1); + surface.friction = .6; + filled.collider = surface; + } +#ifdef RAPIER_FEM + filled.solver = femSolver ? 1 : 0; +#endif + R3SoftBodyHandle handle; + /* As in the Rust demo, skip models the volume mesher cannot fill. */ + R3ErrorHandler handler = r3SetErrorHandler((R3ErrorHandler){0}); + handle = r3InsertSoftBody(world, &filled); + R3Status status = r3LastStatus(); + r3SetErrorHandler(handler); + freeObj(&mesh); + if (status != R3_OK) { + continue; + } + + size_t cellIndices = r3SoftBody_Cells(handle, NULL, 0); + totalCells += cellIndices / 4; + ++totalBodies; + + R3RigidBodyHandle proxy = r3SoftBody_RootBody(handle); + tbBodyColor(testbed, proxy, .85, .35, .3, 1); + } + char label[128]; + snprintf(label, sizeof(label), "%zu in %zu bodies", totalCells, totalBodies); + tbLabel(testbed, "Tetrahedra", label); + tbCamera(testbed, 60, 40, 60, 0, 5, 0); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/spring_joints3.c b/c/testbed/examples3d/spring_joints3.c new file mode 100644 index 000000000..6271e9df6 --- /dev/null +++ b/c/testbed/examples3d/spring_joints3.c @@ -0,0 +1,52 @@ +/* Port of examples3d/spring_joints3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbSpringJoints3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle ground; + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, 0, 0); + groundBody.canSleep = !testbed->noSleep; + ground = r3InsertRigidBody(world, &groundBody); + R3Real damping = 2 * (R3Real)sqrt(1000 * 4 * R3_PI / 3 * 0.125); + for (int i = 0; i <= 30; i++) { + R3Real x = -6 + 1.5 * i; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x, 4.5, 0); + rigidBody.canSleep = 0; + R3RigidBodyHandle handle; + R3ColliderDesc ballCollider = r3BallColliderDesc(0.5); + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &ballCollider); + R3JointDesc joint = r3SpringJointDesc(0, 1000, i / 15.0 * damping); + joint.localFrame1.translation = r3Vector(x, 1.5, 0); + r3InsertImpulseJoint(ground, handle, &joint); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + collider.density = 100; + R3RigidBodyDesc dynamicBody = r3DynamicRigidBodyDesc(); + dynamicBody.position.translation = r3Vector(x, 9.5, 0); + dynamicBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle dynamicBodyHandle = r3InsertRigidBody(world, &dynamicBody); + r3InsertCollider(dynamicBodyHandle, &collider); + } + /* Set up the viewer. */ + tbCamera(testbed, 15, 5, 42, 13, 1, 1); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/balls3.c b/c/testbed/examples3d/stress_tests/balls3.c new file mode 100644 index 000000000..d92006185 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/balls3.c @@ -0,0 +1,35 @@ +/* Port of examples3d/stress_tests/balls3.rs. */ +#include "testbed.h" +#include "rapier_math.h" + +void tbStressTestsBalls3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + for (int i = 0; i < 20; i++) { + for (int j = 0; j < 20; j++) { + for (int k = 0; k < 20; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = j ? R3_DYNAMIC : R3_FIXED; + rigidBody.position.translation = r3Vector(i * 3 - 30.0, j * 3 + 1.5, k * 3 - 30.0); + R3ColliderDesc collider = r3BallColliderDesc(1); + collider.density = 0.477; + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/boxes3.c b/c/testbed/examples3d/stress_tests/boxes3.c new file mode 100644 index 000000000..99ec3d7ad --- /dev/null +++ b/c/testbed/examples3d/stress_tests/boxes3.c @@ -0,0 +1,42 @@ +/* Port of examples3d/stress_tests/boxes3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsBoxes3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(200.1, 0.1, 200.1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + R3Real offset = -10.0; + for (int j = 0; j < 10; j++, offset -= 0.45) { + for (int i = 0; i < 10; i++) { + for (int k = 0; k < 10; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * 2 - 10.0 + offset, j * 2 + 1, k * 2 - 10.0 + offset); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/capsules3.c b/c/testbed/examples3d/stress_tests/capsules3.c new file mode 100644 index 000000000..db455a7e9 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/capsules3.c @@ -0,0 +1,43 @@ +/* Port of examples3d/stress_tests/capsules3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsCapsules3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(200.1, 0.1, 200.1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + R3Real offset = -12.0; + for (int j = 0; j < 47; j++, offset -= 0.35) { + for (int i = 0; i < 8; i++) { + for (int k = 0; k < 8; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * 3 - 12.0 + offset, j * 4 + 4.5, k * 3 - 12.0 + offset); + R3ColliderDesc collider = r3CapsuleYColliderDesc(1, 1); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/ccd3.c b/c/testbed/examples3d/stress_tests/ccd3.c new file mode 100644 index 000000000..585c14c3d --- /dev/null +++ b/c/testbed/examples3d/stress_tests/ccd3.c @@ -0,0 +1,62 @@ +/* Port of examples3d/stress_tests/ccd3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsCcd3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(100.1, 0.1, 100.1)); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &boxCollider); + + R3Real offset = -6; + for (int j = 0; j < 20; j++, offset -= 0.15) { + for (int i = 0; i < 4; i++) { + for (int k = 0; k < 4; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * 3 - 6 + offset, j * 3 + 4.5, k * 3 - 6 + offset); + rigidBody.linvel = r3Vector(0, -1000, 0); + rigidBody.ccdEnabled = 1; + R3ColliderDesc collider; + switch (j % 5) { + case 0: + collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + break; + case 1: + collider = r3BallColliderDesc(1); + break; + case 2: + collider = r3RoundCylinderColliderDesc(1, 1, 0.1); + break; + case 3: + collider = r3ConeColliderDesc(1, 1); + break; + default: + collider = r3CapsuleYColliderDesc(1, 1); + break; + } + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/compound3.c b/c/testbed/examples3d/stress_tests/compound3.c new file mode 100644 index 000000000..2397d0976 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/compound3.c @@ -0,0 +1,48 @@ +/* Port of examples3d/stress_tests/compound3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsCompound3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(200.1, 0.1, 200.1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &boxCollider); + R3Real offset = -2.4; + for (int j = 0; j < 25; j++, offset -= 0.07) { + for (int i = 0; i < 8; i++) { + for (int k = 0; k < 8; k++) { + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * 5 - 4 + offset, j * 5 + 3.5, k * 2 - 4 + offset); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(2, 0.2, 0.2)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &boxCollider); + for (int side = -1; side <= 1; side += 2) { + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.2, 2, 0.2)); + collider.position.translation = r3Vector(side * 2, 2, 0); + r3InsertCollider(handle, &collider); + } + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/convex_polyhedron3.c b/c/testbed/examples3d/stress_tests/convex_polyhedron3.c new file mode 100644 index 000000000..056a5ee82 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/convex_polyhedron3.c @@ -0,0 +1,65 @@ +/* Port of examples3d/stress_tests/convex_polyhedron3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "example_math.h" + +static R3SharedShape *randomHull(Testbed *testbed) { + R3Vector points[10]; + for (size_t i = 0; i < 10; i++) { + R3Real x = exampleRandom(&testbed->randomState) * 2; + R3Real y = exampleRandom(&testbed->randomState) * 2; + R3Real z = exampleRandom(&testbed->randomState) * 2; + points[i] = r3Vector(x, y, z); + } + R3SharedShape *shape = r3RoundConvexHullSharedShape((R3VectorView){points, 10}, 0.1); + return shape; +} + +void tbStressTestsConvexPolyhedron3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(200.1, 0.1, 200.1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + R3SharedShape *shapes[5]; + for (int i = 0; i < 5; i++) { + shapes[i] = randomHull(testbed); + } + R3Real offset = -8.8; + for (int j = 0; j < 47; j++) { + for (int i = 0; i < 8; i++) { + for (int k = 0; k < 8; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * 2.2 - 8.8 + offset, j * 2.2 + 4.1, k * 2.2 - 8.8 + offset); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shapes[(i + k) % 5]; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + offset -= 0.35; + } + for (size_t i = 0; i < TB_COUNT(shapes); ++i) { + r3FreeSharedShape(shapes[i]); + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/heightfield3.c b/c/testbed/examples3d/stress_tests/heightfield3.c new file mode 100644 index 000000000..9d6db6106 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/heightfield3.c @@ -0,0 +1,59 @@ +/* Port of examples3d/stress_tests/heightfield3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsHeightfield3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3Real heights[441]; + for (int j = 0; j <= 20; j++) { + for (int i = 0; i <= 20; i++) { + heights[i + j * 21] = + i == 0 || i == 20 || j == 0 || j == 20 ? 10 : (R3Real)(sin(i * 10) + cos(j * 10)); + } + } + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 0, 0); + R3ColliderDesc groundCollider = r3DefaultColliderDesc(); + groundCollider.shape.kind = R3_SHAPE_DESC_HEIGHTFIELD; + groundCollider.shape.heights = (R3RealView){heights, (21) * (21)}; + groundCollider.shape.rows = 21; + groundCollider.shape.columns = 21; + groundCollider.shape.scale = r3Vector(200, 1, 200); + groundCollider.shape.flags = 0; + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &groundCollider); + + for (int j = 0; j < 47; j++) { + for (int i = 0; i < 8; i++) { + for (int k = 0; k < 8; k++) { + R3ColliderDesc collider; + if (j % 2) { + collider = r3BallColliderDesc(1); + } else { + collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + } + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(i * 3 - 12, j * 3 + 4.5, k * 3 - 12); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/joint_ball3.c b/c/testbed/examples3d/stress_tests/joint_ball3.c new file mode 100644 index 000000000..11d1261a4 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/joint_ball3.c @@ -0,0 +1,55 @@ +/* Port of examples3d/stress_tests/joint_ball3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsJointBall3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle *handles = calloc(1, 10000 * sizeof(*handles)); + if (!handles) { + abort(); + } + for (int k = 0; k < 100; k++) { + for (int i = 0; i < 100; i++) { + int fixed = i == 0 && (k % 4 == 0 || k == 99); + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = fixed ? R3_FIXED : R3_DYNAMIC; + rigidBody.position.translation = r3Vector(k, 0, i); + R3ColliderDesc collider = r3BallColliderDesc(0.4); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + if (i) { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_SPHERICAL_AXES; + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = r3Vector(0, 0, -1); + r3InsertImpulseJoint(handles[k * 100 + i - 1], handle, &joint); + } + if (k) { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_SPHERICAL_AXES; + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = r3Vector(-1, 0, 0); + r3InsertImpulseJoint(handles[k * 100 + i - 100], handle, &joint); + } + handles[k * 100 + i] = handle; + } + } + /* Set up the viewer. */ + tbCamera(testbed, -110, -46, 170, 54, -38, 29); + free(handles); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/joint_fixed3.c b/c/testbed/examples3d/stress_tests/joint_fixed3.c new file mode 100644 index 000000000..61ea63b3c --- /dev/null +++ b/c/testbed/examples3d/stress_tests/joint_fixed3.c @@ -0,0 +1,57 @@ +/* Port of examples3d/stress_tests/joint_fixed3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsJointFixed3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle handles[25]; + for (int m = 0; m < 10; m++) { + for (int l = 0; l < 10; l++) { + for (int j = 0; j < 5; j++) { + for (int k = 0; k < 5; k++) { + for (int i = 0; i < 5; i++) { + int fixed = i == 0 && ((k % 4 == 0 && k != 3) || k == 4); + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = fixed ? R3_FIXED : R3_DYNAMIC; + rigidBody.position.translation = r3Vector(j * 10 + k, l * 3, m * 7 + i); + R3ColliderDesc collider = r3BallColliderDesc(0.4); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + if (i) { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_FIXED_AXES; + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = r3Vector(0, 0, -1); + r3InsertImpulseJoint(handles[k * 5 + i - 1], handle, &joint); + } + if (k) { + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_FIXED_AXES; + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = r3Vector(-1, 0, 0); + r3InsertImpulseJoint(handles[k * 5 + i - 5], handle, &joint); + } + handles[k * 5 + i] = handle; + } + } + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, -38, 14, 108, 46, 12, 23); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/joint_prismatic3.c b/c/testbed/examples3d/stress_tests/joint_prismatic3.c new file mode 100644 index 000000000..73f2b6a38 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/joint_prismatic3.c @@ -0,0 +1,53 @@ +/* Port of examples3d/stress_tests/joint_prismatic3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsJointPrismatic3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + for (int m = 0; m < 8; m++) { + for (int l = 0; l < 8; l++) { + for (int j = 0; j < 50; j++) { + R3Real x = j * 4; + R3Real y = l * 10; + R3Real z = m * 7; + R3RigidBodyHandle parent; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x, y, z); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.4, 0.4, 0.4)); + rigidBody.canSleep = !testbed->noSleep; + parent = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(parent, &collider); + for (int i = 0; i < 5; i++) { + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x, y, z + i + 1); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.4, 0.4, 0.4)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + R3JointDesc joint = r3PrismaticJointDesc(r3Vector(i % 2 ? -1 : 1, 1, 0)); + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = r3Vector(0, 0, -1); + r3JointDesc_SetLimits(&joint, R3_AXIS_LIN_X, -2, 0); + r3InsertImpulseJoint(parent, handle, &joint); + parent = handle; + } + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 262, 63, 124, 101, 4, -3); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/joint_revolute3.c b/c/testbed/examples3d/stress_tests/joint_revolute3.c new file mode 100644 index 000000000..76eec266d --- /dev/null +++ b/c/testbed/examples3d/stress_tests/joint_revolute3.c @@ -0,0 +1,60 @@ +/* Port of examples3d/stress_tests/joint_revolute3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsJointRevolute3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + for (int l = 0; l < 4; l++) { + for (int j = 0; j < 50; j++) { + R3Real x = j * 8; + R3Real y = l * 60; + R3RigidBodyHandle parent; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x, y, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.4, 0.4, 0.4)); + rigidBody.canSleep = !testbed->noSleep; + parent = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(parent, &collider); + for (int i = 0; i < 10; i++) { + R3Real z = i * 4 + 2; + R3Vector positions[] = {r3Vector(x, y, z), r3Vector(x + 2, y, z), + r3Vector(x + 2, y, z + 2), r3Vector(x, y, z + 2)}; + R3Vector axes[] = {r3Vector(0, 0, 1), r3Vector(1, 0, 0), r3Vector(0, 0, 1), + r3Vector(1, 0, 0)}; + R3Vector anchors[] = {r3Vector(0, 0, -2), r3Vector(-2, 0, 0), r3Vector(0, 0, -2), + r3Vector(2, 0, 0)}; + R3RigidBodyHandle handles[4]; + for (int k = 0; k < 4; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = positions[k]; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.4, 0.4, 0.4)); + rigidBody.canSleep = !testbed->noSleep; + handles[k] = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handles[k], &collider); + } + for (int k = 0; k < 4; k++) { + R3JointDesc joint = r3RevoluteJointDesc(axes[k]); + joint.localFrame1.translation = r3Vector(0, 0, 0); + joint.localFrame2.translation = anchors[k]; + r3InsertImpulseJoint(k ? handles[k - 1] : parent, handles[k], &joint); + } + parent = handles[3]; + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 478, 83, 228, 134, 83, -116); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/keva3.c b/c/testbed/examples3d/stress_tests/keva3.c new file mode 100644 index 000000000..4d6a8afbe --- /dev/null +++ b/c/testbed/examples3d/stress_tests/keva3.c @@ -0,0 +1,48 @@ +/* Port of examples3d/stress_tests/keva3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void buildBlock(Testbed *, R3World *, R3Vector, R3Vector, int, int, int); + +void tbStressTestsKeva3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + /* Ground. */ + const R3Real groundSize = 50.0; + const R3Real groundHeight = 0.1; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0.0, -groundHeight, 0.0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(groundSize, groundHeight, groundSize)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + /* Create the cubes. Odd layer counts keep adjacent blocks aligned. */ + const R3Vector halfExtents = r3Vector(0.1, 0.5, 2.0); + R3Real blockHeight = 0.0; + const int layers[] = {0, 13, 17, 21, 41, 83}; + + for (int i = 5; i >= 1; i--) { + const int numx = i * 2; + const int numy = layers[i]; + const int numz = numx * 3 + 1; + const R3Real blockWidth = numx * halfExtents.z * 2.0; + buildBlock(testbed, world, halfExtents, + r3Vector(-blockWidth / 2.0, blockHeight, -blockWidth / 2.0), numx, numy, numz); + blockHeight += numy * halfExtents.y * 2.0 + halfExtents.x * 2.0; + } + + /* Set up the viewer. */ + tbCamera(testbed, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/many_kinematics3.c b/c/testbed/examples3d/stress_tests/many_kinematics3.c new file mode 100644 index 000000000..d93723cb6 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/many_kinematics3.c @@ -0,0 +1,62 @@ +/* Port of examples3d/stress_tests/many_kinematics3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "example_math.h" + +static R3Real bounce(R3Real p, R3Real v) { + return (v > 0 && p > 105) || (v < 0 && p < -105) ? -v : v; +} + +void tbStressTestsManyKinematics3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + for (int i = 0; i < 30; i++) { + for (int j = 0; j < 30; j++) { + for (int k = 0; k < 30; k++) { + R3RigidBodyDesc rigidBody = r3KinematicVelocityBasedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(i * 7 - 105, j * 7 - 105, k * 7 - 105); + R3Real vx = (exampleRandom(&testbed->randomState) - 0.5) * 30; + R3Real vy = (exampleRandom(&testbed->randomState) - 0.5) * 30; + R3Real vz = (exampleRandom(&testbed->randomState) - 0.5) * 30; + rigidBody.linvel = r3Vector(vx, vy, vz); + R3ColliderDesc collider = r3BallColliderDesc(1); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + testbed->snapshotSupported = 0; + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + + size_t n = r3RigidBodyHandles(world, NULL, 0); + R3RigidBodyHandle *handle = malloc(n * sizeof(*handle)); + if (!handle) { + abort(); + } + n = r3RigidBodyHandles(world, handle, n); + for (size_t i = 0; i < n; i++) { + R3Vector position; + R3Vector translation; + position = r3RigidBody_Translation(handle[i]); + translation = r3RigidBody_Linvel(handle[i]); + r3RigidBody_SetLinvel(handle[i], + r3Vector(bounce(position.x, translation.x), + bounce(position.y, translation.y), + bounce(position.z, translation.z)), + 0); + } + free(handle); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/many_pyramids3.c b/c/testbed/examples3d/stress_tests/many_pyramids3.c new file mode 100644 index 000000000..8b36b3591 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/many_pyramids3.c @@ -0,0 +1,40 @@ +/* Port of examples3d/stress_tests/many_pyramids3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsManyPyramids3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(50, 0.1, 130)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + for (int p = 0; p < 40; p++) { + for (int i = 0; i < 20; i++) { + for (int j = i; j < 20; j++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(i * 0.5 + j - i, i + 0.5, (p - 20) * 4); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/many_sleep3.c b/c/testbed/examples3d/stress_tests/many_sleep3.c new file mode 100644 index 000000000..89dc64d1f --- /dev/null +++ b/c/testbed/examples3d/stress_tests/many_sleep3.c @@ -0,0 +1,39 @@ +/* Port of examples3d/stress_tests/many_sleep3.rs. */ +#include "testbed.h" +#include "rapier_math.h" + +void tbStressTestsManySleep3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + for (int i = 0; i < 50; i++) { + for (int j = 0; j < 50; j++) { + for (int k = 0; k < 50; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = j ? R3_DYNAMIC : R3_FIXED; + rigidBody.position.translation = r3Vector(i * 3 - 75.0, j * 3 + 1.5, k * 3 - 75.0); + rigidBody.sleeping = 1; + R3ColliderDesc collider = r3BallColliderDesc(1); + collider.density = 0.477; + if (testbed->noSleep) { + rigidBody.canSleep = 0; + rigidBody.sleeping = 0; + } + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/many_static3.c b/c/testbed/examples3d/stress_tests/many_static3.c new file mode 100644 index 000000000..f2e7c7ecd --- /dev/null +++ b/c/testbed/examples3d/stress_tests/many_static3.c @@ -0,0 +1,35 @@ +/* Port of examples3d/stress_tests/many_static3.rs. */ +#include "testbed.h" +#include "rapier_math.h" + +void tbStressTestsManyStatic3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + for (int i = 0; i < 50; i++) { + for (int j = 0; j < 50; j++) { + for (int k = 0; k < 50; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.bodyType = j < 49 ? R3_FIXED : R3_DYNAMIC; + rigidBody.position.translation = r3Vector(i * 3 - 75.0, j * 3 + 1.5, k * 3 - 75.0); + R3ColliderDesc collider = r3BallColliderDesc(1); + collider.density = 0.477; + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/pyramid3.c b/c/testbed/examples3d/stress_tests/pyramid3.c new file mode 100644 index 000000000..35432dbda --- /dev/null +++ b/c/testbed/examples3d/stress_tests/pyramid3.c @@ -0,0 +1,43 @@ +/* Port of examples3d/stress_tests/pyramid3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsPyramid3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + r3SetGravity(world, r3Vector(0, -9.81, 0)); + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(100, 1, 100)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &boxCollider); + for (int i = 0; i < 50; i++) { + for (int j = i / 2; j < 50 - (i + 1) / 2; j++) { + for (int k = i / 2; k < 50 - (i + 1) / 2; k++) { + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.975, 0.975, 0.975)); + collider.density = 1000; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(-50 + 2.25 * j + (i & 1), 1 + 2.5 * i, -50 + 2.25 * k + (i & 1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 200, 130, 200, 5, 50, 5); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/ragdolls3.c b/c/testbed/examples3d/stress_tests/ragdolls3.c new file mode 100644 index 000000000..60f4a2b59 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/ragdolls3.c @@ -0,0 +1,104 @@ +/* Port of examples3d/stress_tests/ragdolls3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +typedef struct Part { + R3RigidBodyHandle handle; + R3Vector offset; +} Part; + +/* A body and its offset from the ragdoll's torso. */ +static Part part(R3World *world, R3Vector origin, R3Vector offset, R3ColliderDesc collider, + int noSleep) { + R3RigidBodyDesc body = r3DynamicRigidBodyDesc(); + body.position.translation = r3VectorAdd(origin, offset); + body.canSleep = !noSleep; + Part result = {.offset = offset}; + result.handle = r3InsertRigidBody(world, &body); + r3InsertCollider(result.handle, &collider); + + return result; +} + +static R3JointDesc revolute(Part parent, Part child, R3Vector anchor, R3Real min, R3Real max) { + R3JointDesc joint = r3RevoluteJointDesc(r3Vector(0, 0, 1)); + joint.localFrame1.translation = r3VectorSub(anchor, parent.offset); + joint.localFrame2.translation = r3VectorSub(anchor, child.offset); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_X, min, max); + joint.contactsEnabled = 0; + return joint; +} + +static R3JointDesc spherical(Part parent, Part child, R3Vector anchor, R3Real limit) { + R3JointDesc joint = r3SphericalJointDesc(); + joint.localFrame1.translation = r3VectorSub(anchor, parent.offset); + joint.localFrame2.translation = r3VectorSub(anchor, child.offset); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_X, -limit, limit); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_Y, -limit, limit); + r3JointDesc_SetLimits(&joint, R3_AXIS_ANG_Z, -limit, limit); + joint.contactsEnabled = 0; + return joint; +} + +/* Ten bodies and nine limited joints, matching the Rust ragdoll. */ +static void ragdoll(R3World *world, R3Vector origin, int noSleep) { + R3ColliderDesc collider = r3CapsuleYColliderDesc(.3, .15); + Part torso = part(world, origin, r3Vector(0, 0, 0), collider, noSleep); + collider = r3BallColliderDesc(.15); + Part head = part(world, origin, r3Vector(0, 0.55, 0), collider, noSleep); + R3JointDesc neck = spherical(torso, head, r3Vector(0, 0.42, 0), .5); + r3InsertImpulseJoint(torso.handle, head.handle, &neck); + + for (int side = -1; side <= 1; side += 2) { + collider = r3CapsuleXColliderDesc(.14, .06); + Part upperArm = part(world, origin, r3Vector(side * .36, .25, 0), collider, noSleep); + collider = r3CapsuleXColliderDesc(.14, .06); + Part forearm = part(world, origin, r3Vector(side * .70, .25, 0), collider, noSleep); + collider = r3CapsuleYColliderDesc(.16, .07); + Part thigh = part(world, origin, r3Vector(side * .09, -.52, 0), collider, noSleep); + collider = r3CapsuleYColliderDesc(.16, .07); + Part shin = part(world, origin, r3Vector(side * .09, -.92, 0), collider, noSleep); + R3JointDesc shoulder = spherical(torso, upperArm, r3Vector(side * .19, .25, 0), 1.2); + r3InsertImpulseJoint(torso.handle, upperArm.handle, &shoulder); + + R3JointDesc elbow = revolute(upperArm, forearm, r3Vector(side * .53, .25, 0), 0, 2.5); + r3InsertImpulseJoint(upperArm.handle, forearm.handle, &elbow); + + R3JointDesc hip = spherical(torso, thigh, r3Vector(side * .09, -.33, 0), 1); + r3InsertImpulseJoint(torso.handle, thigh.handle, &hip); + + R3JointDesc knee = revolute(thigh, shin, r3Vector(side * .09, -.72, 0), 0, 2.3); + r3InsertImpulseJoint(thigh.handle, shin.handle, &knee); + } +} + +void tbStressTestsRagdolls3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(100, 1, 100)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* 125 ragdolls: a 5 by 5 grid with 5 layers. */ + for (int layer = 0; layer < 5; ++layer) { + for (int row = 0; row < 5; ++row) { + for (int col = 0; col < 5; ++col) { + ragdoll(world, r3Vector(col * 2.2, 1.5 + layer * 2.6, row * 2.2), testbed->noSleep); + } + } + } + tbCamera(testbed, -12, 10, -12, 4.5, 1, 4.5); + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/ray_cast3.c b/c/testbed/examples3d/stress_tests/ray_cast3.c new file mode 100644 index 000000000..5ed31175d --- /dev/null +++ b/c/testbed/examples3d/stress_tests/ray_cast3.c @@ -0,0 +1,95 @@ +/* Port of examples3d/stress_tests/ray_cast3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsRayCast3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(200.1, 0.1, 200.1)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + R3Real offset = -10; + for (int j = 0; j < 10; ++j) { + for (int i = 0; i < 10; ++i) { + for (int k = 0; k < 10; ++k) { + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * 2 - 10 + offset, j * 2 + 1, k * 2 - 10 + offset); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + offset -= .45; + } + const R3Real rayBallRadius = 100; + R3SharedShape *rayBall = r3BallSharedShape(rayBallRadius); + R3TriMeshData *mesh = r3SharedShape_ToTrimesh(rayBall, 100, 100); + r3FreeSharedShape(rayBall); + size_t rayCount = r3TriMeshData_Vertices(mesh, NULL, 0); + R3Vector *rayOrigins = malloc(rayCount * sizeof(*rayOrigins)); + R3Vector *directions = malloc(rayCount * sizeof(*directions)); + R3Vector *centeredRays = malloc(rayCount * sizeof(*centeredRays)); + if (!rayOrigins || !directions || !centeredRays) { + abort(); + } + rayCount = r3TriMeshData_Vertices(mesh, rayOrigins, rayCount); + r3FreeTriMeshData(mesh); + for (size_t i = 0; i < rayCount; ++i) { + directions[i] = r3VectorScale(r3VectorNormalize(rayOrigins[i]), -1); + } + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + + size_t bodyCount = r3RigidBodyCount(world); + R3RigidBodyHandle *handles = malloc(bodyCount * sizeof(*handles)); + if (!handles) { + abort(); + } + bodyCount = r3RigidBodyHandles(world, handles, bodyCount); + R3Vector center = {0}; + for (size_t i = 0; i < bodyCount; ++i) { + R3Vector translation = r3RigidBody_Translation(handles[i]); + center = r3VectorAdd(center, translation); + } + free(handles); + center = r3VectorScale(center, 1.0 / bodyCount); + for (size_t i = 0; i < rayCount; ++i) { + centeredRays[i] = r3VectorAdd(center, rayOrigins[i]); + } + const double t1 = tbClock(); + + size_t hits = 0; + for (size_t i = 0; i < rayCount; ++i) { + R3RayToi hit = + r3CastRayToi(world, NULL, centeredRays[i], directions[i], rayBallRadius - 1, 1); + hits += hit.found != 0; + } + const double mainCheckTime = tbClock() - t1; + + char label[64]; + snprintf(label, sizeof(label), "%zu", rayCount); + tbLabel(testbed, "Ray count:", label); + snprintf(label, sizeof(label), "%zu", hits); + tbLabel(testbed, "Ray hits:", label); + snprintf(label, sizeof(label), "%.2f ms", mainCheckTime * 1000); + tbLabel(testbed, "Ray-cast time:", label); + } + } + free(rayOrigins); + free(directions); + free(centeredRays); + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/ropes3.c b/c/testbed/examples3d/stress_tests/ropes3.c new file mode 100644 index 000000000..11506f322 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/ropes3.c @@ -0,0 +1,49 @@ +/* Port of examples3d/stress_tests/ropes3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsRopes3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + for (int i = 0; i < 64; i++) { + R3Vector top = r3Vector(i / 8 * 4, 0, i % 8 * 4); + R3RigidBodyHandle parent; + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = top; + groundBody.canSleep = !testbed->noSleep; + parent = r3InsertRigidBody(world, &groundBody); + + for (int s = 0; s < 60; s++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(top, r3Vector(0, -(s + 0.5), 0)); + rigidBody.linvel = r3Vector(2, 0, 0); + R3RigidBodyHandle handle; + R3ColliderDesc collider = r3CapsuleYColliderDesc(0.35, 0.1); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + + R3JointDesc joint = r3DefaultJointDesc(); + joint.lockedAxes = R3_JOINT_SPHERICAL_AXES; + joint.localFrame1.translation = r3Vector(0, s ? -0.5 : 0, 0); + joint.localFrame2.translation = r3Vector(0, 0.5, 0); + joint.contactsEnabled = 0; + r3InsertImpulseJoint(parent, handle, &joint); + parent = handle; + } + } + /* Set up the viewer. */ + tbCamera(testbed, -45, -10, -45, 14, -30, 14); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/soft_blobs3.c b/c/testbed/examples3d/stress_tests/soft_blobs3.c new file mode 100644 index 000000000..b4a3702f6 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/soft_blobs3.c @@ -0,0 +1,87 @@ +/* Port of examples3d/stress_tests/soft_blobs3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftBlobs3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle floor; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.5, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(7.5, 0.5, 7.5)); + rigidBody.canSleep = !testbed->noSleep; + floor = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(floor, &collider); + + tbBodyColor(testbed, floor, 0.6, 0.7, 1, 0.3); + const int dx[] = {1, -1, 0, 0}; + const int dz[] = {0, 0, 1, -1}; + for (int k = 0; k < 4; k++) { + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(dx[k] * 7.25, 4, dz[k] * 7.25); + R3ColliderDesc collider = + r3CuboidColliderDesc(r3Vector(dx[k] ? 0.25 : 7.5, 4, dx[k] ? 7.5 : 0.25)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + + tbBodyColor(testbed, handle, 0.6, 0.7, 1, 0.3); + } + for (int layer = 0; layer < 8; layer++) { + for (int i = 0; i < 7; i++) { + for (int j = 0; j < 7; j++) { + R3SoftBodyDesc softBody = + r3SphereSoftBodyDesc(r3Vector(-5.7 + i * 1.9 + layer % 2 * 0.4, 2 + layer * 2, + -5.7 + j * 1.9 + layer % 3 * 0.3), + 0.45 + 0.1 * ((i + j + layer) % 3), 1); + softBody.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){20, 1}); + softBody.volumeFactor = 1.1; + softBody.particleMass = 0.03; + softBody.particleRadius = (R3OptionalReal){1, 0.08}; + R3ColliderDesc surfaceCollider = r3BallColliderDesc(0.08); + surfaceCollider.friction = 0.6; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + } + } + } + R3SoftBodyDesc softBodyValue = r3ClothSoftBodyDesc(r3Vector(-4, 28, -0.6), r3Vector(0.15, 0, 0), + r3Vector(0, 0, 0.15), 54, 9); + softBodyValue.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){30, 1}; + material.volumeSoftness = (R3SpringCoefficients){30, 1}; + material.shapeMatchingSoftness = (R3SpringCoefficients){30, 1}; + material.bendSoftness = (R3SpringCoefficients){3, 1}; + softBodyValue.material = material; + softBodyValue.selfContacts = 1; + softBodyValue.particleMass = 0.02; + softBodyValue.particleRadius = (R3OptionalReal){1, 0.05}; + R3ColliderDesc surfaceCollider2 = r3BallColliderDesc(0.05); + surfaceCollider2.friction = 0.5; + softBodyValue.collider = surfaceCollider2; + + if (testbed->noSleep) { + softBodyValue.canSleep = 0; + } + r3InsertSoftBody(world, &softBodyValue); + /* Set up the viewer. */ + tbCamera(testbed, 20, 18, 20, 0, 6, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/soft_cloth_drape3.c b/c/testbed/examples3d/stress_tests/soft_cloth_drape3.c new file mode 100644 index 000000000..3ed5f0691 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/soft_cloth_drape3.c @@ -0,0 +1,81 @@ +/* Port of examples3d/stress_tests/soft_cloth_drape3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftClothDrape3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc boxCollider = r3CuboidColliderDesc(r3Vector(12, 0.1, 12)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &boxCollider); + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + int shape = (i + j) % 3; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-4.5 + i * 3, 1, -4.5 + j * 3); + R3ColliderDesc collider; + if (shape == 0) { + collider = r3BallColliderDesc(0.8); + } else { + if (shape == 1) { + collider = r3CuboidColliderDesc(r3Vector(0.6, 1, 0.6)); + } else { + collider = r3CapsuleYColliderDesc(0.6, 0.4); + } + } + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + R3SoftBodyDesc softBody = r3ClothSoftBodyDesc(r3Vector(-6, 3.5, -6), r3Vector(0.1, 0, 0), + r3Vector(0, 0, 0.1), 101, 101); + softBody.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){30, 1}; + material.volumeSoftness = (R3SpringCoefficients){30, 1}; + material.shapeMatchingSoftness = (R3SpringCoefficients){30, 1}; + material.bendSoftness = (R3SpringCoefficients){3, 1}; + softBody.material = material; + softBody.selfContacts = 1; + softBody.particleMass = 0.02; + softBody.particleRadius = (R3OptionalReal){1, 0.05}; + R3ColliderDesc surfaceCollider = r3BallColliderDesc(0.05); + surfaceCollider.friction = 0.6; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + R3ColliderDesc collider = r3BallColliderDesc(0.4); + collider.density = 2; + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(-4 + i * 2 + j % 2 * 0.5, 6 + (i + j) * 0.5, -4 + j * 2); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera(testbed, 14, 10, 14, 0, 1, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/soft_cloth_keva3.c b/c/testbed/examples3d/stress_tests/soft_cloth_keva3.c new file mode 100644 index 000000000..89436d6f5 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/soft_cloth_keva3.c @@ -0,0 +1,70 @@ +/* Port of examples3d/stress_tests/soft_cloth_keva3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +extern void buildBlock(Testbed *, R3World *, R3Vector, R3Vector, int, int, int); + +static void cloth(Testbed *testbed, R3World *world, R3Real side, R3Real spacing, R3Real y) { + size_t n = (size_t)round(side / spacing) + 1; + R3Real width = (n - 1) * spacing; + R3SoftBodyDesc softBody = + r3ClothSoftBodyDesc(r3Vector(-width * 0.5, y, -width * 0.5), r3Vector(spacing, 0, 0), + r3Vector(0, 0, spacing), n, n); + softBody.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.edgeSoftness = (R3SpringCoefficients){30, 1}; + material.volumeSoftness = (R3SpringCoefficients){30, 1}; + material.shapeMatchingSoftness = (R3SpringCoefficients){30, 1}; + material.bendSoftness = (R3SpringCoefficients){3, 1}; + softBody.material = material; + softBody.selfContacts = 1; + softBody.particleMass = 0.05; + softBody.particleRadius = (R3OptionalReal){1, 0.15}; + R3ColliderDesc surfaceCollider = r3BallColliderDesc(0.15); + surfaceCollider.friction = 0.6; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); +} + +void tbStressTestsSoftClothKeva3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(50, 0.1, 50)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + + R3Real height = 0; + const int layers[] = {0, 9, 13, 17}; + for (int i = 3; i >= 1; i--) { + R3Real width = i * 4; + buildBlock(testbed, world, r3Vector(0.1, 0.5, 2), r3Vector(-width / 2, height, -width / 2), + i, layers[i], i * 3 + 1); + height += layers[i] + 0.2; + if (i > 1) { + cloth(testbed, world, width + 4, 0.5, height + 0.15); + height += 0.3; + } + } + cloth(testbed, world, 18, 0.75, height + 6); + /* Set up the viewer. */ + tbCamera(testbed, 50, 50, 50, 0, 15, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/soft_fem_beams3.c b/c/testbed/examples3d/stress_tests/soft_fem_beams3.c new file mode 100644 index 000000000..60d679bfb --- /dev/null +++ b/c/testbed/examples3d/stress_tests/soft_fem_beams3.c @@ -0,0 +1,84 @@ +/* Port of examples3d/stress_tests/soft_fem_beams3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#ifdef RAPIER_FEM + +void tbStressTestsSoftFemBeams3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(30, 0.1, 30)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + /* Cantilevers bolted at their left end, stiffer on successive rows. */ + const R3Real length = 3.0, thickness = 0.3; + for (int row = 0; row < 8; ++row) { + for (int col = 0; col < 8; ++col) { + const R3Real x0 = -20.0 + col * 5.0; + const R3Real z = -17.5 + row * 5.0; + R3SoftBodyDesc beam = r3CuboidSoftBodyDesc( + r3Vector(x0 + length * 0.5, 2.5, z), + r3Vector(length * 0.5, thickness * 0.5, thickness * 0.5), 13, 3, 3); + beam.cellModel = R3_SOFT_CELL_COROTATIONAL; + beam.totalMass = (R3OptionalReal){1, 12.0}; + beam.solver = R3_SOFT_SOLVER_FEM; + { + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + material.youngModulus = 1.0e6 * (1.0 + row); + material.poissonRatio = 0.3; + material.elasticDampingRatio = 1.0; + beam.material = material; + } + beam.canSleep = !testbed->noSleep; + uint32_t *beamPins = NULL; + { + size_t count = r3SoftBodyDesc_ParticlePositions(&beam, NULL, 0); + R3Vector *positions = malloc(count * sizeof(*positions)); + beamPins = malloc(count * sizeof(*beamPins)); + if (!positions || !beamPins) { + abort(); + } + count = r3SoftBodyDesc_ParticlePositions(&beam, positions, count); + size_t beamPinsCount = 0; + for (size_t i = 0; i < count; ++i) { + if (positions[i].x < x0 + 1.0e-4) { + beamPins[beamPinsCount++] = (uint32_t)i; + } + } + r3SoftBodyDesc_SetPinnedParticles( + &beam, (R3IndexView){(const uint32_t *)beamPins, beamPinsCount}); + + free(positions); + } + r3InsertSoftBody(world, &beam); + free(beamPins); + + /* Load dropped on the free end. */ + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(x0 + length - 0.4, 4, z); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.3, 0.3, 0.3)); + collider.density = 30.0; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + tbCamera(testbed, 0, 28, 34, 0, 1, 0); + + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} +#endif diff --git a/c/testbed/examples3d/stress_tests/soft_jellies3.c b/c/testbed/examples3d/stress_tests/soft_jellies3.c new file mode 100644 index 000000000..1218f0b2d --- /dev/null +++ b/c/testbed/examples3d/stress_tests/soft_jellies3.c @@ -0,0 +1,72 @@ +/* Port of examples3d/stress_tests/soft_jellies3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftJellies3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle floor; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.5, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(6.5, 0.5, 6.5)); + rigidBody.canSleep = !testbed->noSleep; + floor = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(floor, &collider); + + tbBodyColor(testbed, floor, 0.6, 0.7, 1, 0.3); + const int dx[] = {1, -1, 0, 0}; + const int dz[] = {0, 0, 1, -1}; + for (int k = 0; k < 4; k++) { + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(dx[k] * 6.25, 3.5, dz[k] * 6.25); + R3ColliderDesc collider = + r3CuboidColliderDesc(r3Vector(dx[k] ? 0.25 : 6.5, 3.5, dx[k] ? 6.5 : 0.25)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + + tbBodyColor(testbed, handle, 0.6, 0.7, 1, 0.3); + } + int k = 0; + for (int layer = 0; layer < 8; layer++) { + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++, k++) { + R3SoftBodyDesc softBody = + r3CuboidSoftBodyDesc(r3Vector(-4.8 + i * 2.4 + layer % 2 * 0.6, 2 + layer * 2.2, + -4.8 + j * 2.4 + layer % 3 * 0.4), + r3Vector(0.55, 0.55, 0.55), 4, 4, 4); + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + softBody.cellModel = k % 3 ? R3_SOFT_CELL_COROTATIONAL : R3_SOFT_CELL_NEO_HOOKEAN; + material.youngModulus = 2.0e3 * (1 + k % 5 * 3); + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.particleMass = 0.1; + softBody.particleRadius = (R3OptionalReal){1, 0.08}; + R3ColliderDesc surfaceCollider = r3BallColliderDesc(0.08); + surfaceCollider.friction = 0.7; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 18, 16, 18, 0, 5, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/soft_ropes3.c b/c/testbed/examples3d/stress_tests/soft_ropes3.c new file mode 100644 index 000000000..0d822c7ed --- /dev/null +++ b/c/testbed/examples3d/stress_tests/soft_ropes3.c @@ -0,0 +1,84 @@ +/* Port of examples3d/stress_tests/soft_ropes3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftRopes3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle floor; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.5, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(6.5, 0.5, 6.5)); + rigidBody.canSleep = !testbed->noSleep; + floor = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(floor, &collider); + + tbBodyColor(testbed, floor, 0.6, 0.7, 1, 0.3); + const int dx[] = {1, -1, 0, 0}; + const int dz[] = {0, 0, 1, -1}; + for (int k = 0; k < 4; k++) { + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(dx[k] * 6.25, 3, dz[k] * 6.25); + R3ColliderDesc collider = + r3CuboidColliderDesc(r3Vector(dx[k] ? 0.25 : 6.5, 3, dx[k] ? 6.5 : 0.25)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + + tbBodyColor(testbed, handle, 0.6, 0.7, 1, 0.3); + } + for (int k = 0; k < 4; k++) { + R3SharedShape *shape = r3CapsuleSharedShape(r3Vector(-5, 0, 0), r3Vector(5, 0, 0), 0.12); + R3RigidBodyHandle handleH; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 4, -4.5 + k * 3); + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = shape; + rigidBody.canSleep = !testbed->noSleep; + handleH = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handleH, &collider); + + tbBodyColor(testbed, handleH, 0.6, 0.7, 1, 0.3); + r3FreeSharedShape(shape); + } + for (int layer = 0; layer < 10; layer++) { + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 3; j++) { + R3Real across = -5 + i * 2 + layer % 2 * 0.5; + R3Real along = -5.7 + j * 3.8; + R3Real y = 8 + layer * 1.5; + R3Vector start = + layer % 2 ? r3Vector(across, y, along) : r3Vector(along, y, across); + R3Vector end = layer % 2 ? r3Vector(across + 0.4, y + 0.3, along + 3.5) + : r3Vector(along + 3.5, y + 0.3, across + 0.4); + R3SoftBodyDesc softBody = r3RopeSoftBodyDesc(start, end, 40); + softBody.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30, 1}); + softBody.particleMass = 0.03; + R3ColliderDesc surfaceCollider = r3BallColliderDesc(0.08); + surfaceCollider.friction = 0.6; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 16, 14, 16, 0, 4, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/soft_slab3.c b/c/testbed/examples3d/stress_tests/soft_slab3.c new file mode 100644 index 000000000..f30321fab --- /dev/null +++ b/c/testbed/examples3d/stress_tests/soft_slab3.c @@ -0,0 +1,96 @@ +/* Port of examples3d/stress_tests/soft_slab3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsSoftSlab3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyHandle floor; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.5, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(9.5, 0.5, 9.5)); + rigidBody.canSleep = !testbed->noSleep; + floor = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(floor, &collider); + + tbBodyColor(testbed, floor, 0.6, 0.7, 1, 0.3); + const int dx[] = {1, -1, 0, 0}; + const int dz[] = {0, 0, 1, -1}; + for (int k = 0; k < 4; k++) { + R3RigidBodyHandle handle; + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(dx[k] * 9.25, 3.5, dz[k] * 9.25); + R3ColliderDesc collider = + r3CuboidColliderDesc(r3Vector(dx[k] ? 0.25 : 9.5, 3.5, dx[k] ? 9.5 : 0.25)); + rigidBody.canSleep = !testbed->noSleep; + handle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(handle, &collider); + + tbBodyColor(testbed, handle, 0.6, 0.7, 1, 0.3); + } + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(-6 + i * 3, 0.5, -6 + j * 3); + R3ColliderDesc collider; + if ((i + j) % 2) { + collider = r3CuboidColliderDesc(r3Vector(0.5, 0.5, 0.5)); + } else { + collider = r3BallColliderDesc(0.5); + } + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + R3SoftBodyDesc softBody = + r3CuboidSoftBodyDesc(r3Vector(0, 2.2, 0), r3Vector(8, 1, 8), 25, 4, 25); + R3SoftBodyMaterial material = r3DefaultSoftBodyMaterial(); + softBody.cellModel = R3_SOFT_CELL_COROTATIONAL; + material.youngModulus = 4.0e4; + material.poissonRatio = 0.4; + material.elasticDampingRatio = 0.5; + softBody.material = material; + softBody.particleMass = 0.2; + R3ColliderDesc surfaceCollider = r3BallColliderDesc(0.3); + surfaceCollider.friction = 0.7; + softBody.collider = surfaceCollider; + + if (testbed->noSleep) { + softBody.canSleep = 0; + } + r3InsertSoftBody(world, &softBody); + for (int wave = 0; wave < 3; wave++) { + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(-7.5 + i + wave % 2 * 0.5, 5.5 + wave * 3 + (i + j) % 3 * 0.7, + -7.5 + j + wave % 3 * 0.3); + R3ColliderDesc collider; + if ((i + j + wave) % 2) { + collider = r3CuboidColliderDesc(r3Vector(0.35, 0.35, 0.35)); + } else { + collider = r3BallColliderDesc(0.35); + } + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + /* Set up the viewer. */ + tbCamera(testbed, 20, 14, 20, 0, 2, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/stacks3.c b/c/testbed/examples3d/stress_tests/stacks3.c new file mode 100644 index 000000000..199dc2539 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/stacks3.c @@ -0,0 +1,71 @@ +/* Port of examples3d/stress_tests/stacks3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsStacks3(Testbed *testbed) { + /* World. */ + R3World *world = r3NewWorld(); + + R3RigidBodyDesc groundBody = r3FixedRigidBodyDesc(); + groundBody.position.translation = r3Vector(0, -0.1, 0); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(200, 0.1, 200)); + groundBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle groundBodyHandle = r3InsertRigidBody(world, &groundBody); + r3InsertCollider(groundBodyHandle, &collider); + for (int p = 0; p < 4; p++) { + for (int i = 0; i < 12; i++) { + for (int j = i; j < 12; j++) { + for (int k = i; k < 12; k++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(i + (k - i) * 2 - 110 + p * 30 - 12, + i * 2 + 50, i + (j - i) * 2 - 12); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + } + for (int w = 0; w < 3; w++) { + for (int i = 0; i < 12; i++) { + for (int j = i; j < 12; j++) { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(-2 + w * 6, i * 2 + 50, i + (j - i) * 2 - 12); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + R3Real radius = 1.3 * 24 / R3_PI; + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 24; j++) { + R3Real angle = (i / 2.0 + j) * R3_PI * 2 / 24; + R3Vector position = r3Vector(25 + sin(angle) * radius, 50 + i * 2, cos(angle) * radius); + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = position; + rigidBody.position = + r3Pose(position, r3RotationFromAxisAngle(r3Vector(0, 1, 0), angle)); + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + /* Set up the viewer. */ + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + + /* Set up rendering and run the simulation. */ + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/stress_tests/trimesh3.c b/c/testbed/examples3d/stress_tests/trimesh3.c new file mode 100644 index 000000000..79a390ff4 --- /dev/null +++ b/c/testbed/examples3d/stress_tests/trimesh3.c @@ -0,0 +1,71 @@ +/* Port of examples3d/stress_tests/trimesh3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbStressTestsTrimesh3(Testbed *testbed) { + R3World *world = r3NewWorld(); + const R3Vector groundSize = r3Vector(200, 1, 200); + const size_t nsubdivs = 20; + R3Real heights[21 * 21]; + for (size_t j = 0; j <= nsubdivs; ++j) { + for (size_t i = 0; i <= nsubdivs; ++i) { + const R3Real x = i * groundSize.x / nsubdivs; + const R3Real z = j * groundSize.z / nsubdivs; + heights[i + j * 21] = + i == 0 || i == nsubdivs || j == 0 || j == nsubdivs ? 10 : sin(x) + cos(z); + } + } + /* Build the triangle mesh from the native heightfield's mesh representation. */ + R3SharedShape *heightfield = + r3HeightfieldSharedShape((R3RealView){heights, (21) * (21)}, 21, 21, groundSize); + R3TriMeshData *mesh = r3SharedShape_ToTrimesh(heightfield, 3, 2); + r3FreeSharedShape(heightfield); + size_t vertexCount = 0, indexCount = 0; + vertexCount = r3TriMeshData_Vertices(mesh, NULL, 0); + indexCount = r3TriMeshData_Indices(mesh, NULL, 0); + R3Vector *vertices = malloc(vertexCount * sizeof(*vertices)); + uint32_t *indices = malloc(indexCount * sizeof(*indices)); + if (!vertices || !indices) { + abort(); + } + vertexCount = r3TriMeshData_Vertices(mesh, vertices, vertexCount); + indexCount = r3TriMeshData_Indices(mesh, indices, indexCount); + R3ColliderDesc collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetTrimesh(&collider.shape, (R3VectorView){vertices, vertexCount}, + (R3TriangleView){(const R3Triangle *)indices, indexCount / 3}, 0); + + r3FreeTriMeshData(mesh); + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + free(vertices); + free(indices); + + for (int j = 0; j < 47; ++j) { + for (int i = 0; i < 8; ++i) { + for (int k = 0; k < 8; ++k) { + R3ColliderDesc collider; + if (j % 2 == 0) { + collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + } else { + collider = r3BallColliderDesc(1); + } + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(i * 3 - 12, j * 3 + 4.5, k * 3 - 12); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/trimesh3.c b/c/testbed/examples3d/trimesh3.c new file mode 100644 index 000000000..c5a8f9458 --- /dev/null +++ b/c/testbed/examples3d/trimesh3.c @@ -0,0 +1,115 @@ +/* Port of examples3d/trimesh3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbTrimesh3(Testbed *testbed) { + R3World *world = r3NewWorld(); + const R3Vector groundSize = r3Vector(100, 1, 100); + const size_t nsubdivs = 20; + R3Real heights[21 * 21]; + for (size_t j = 0; j <= nsubdivs; ++j) { + for (size_t i = 0; i <= nsubdivs; ++i) { + const R3Real x = i * groundSize.x / nsubdivs; + const R3Real z = j * groundSize.z / nsubdivs; + heights[i + j * 21] = + i == 0 || i == nsubdivs || j == 0 || j == nsubdivs ? 10 : sin(x) + cos(z); + } + } + /* Build the triangle mesh from the native heightfield's mesh representation. */ + R3SharedShape *heightfield = + r3HeightfieldSharedShape((R3RealView){heights, (21) * (21)}, 21, 21, groundSize); + R3TriMeshData *mesh = r3SharedShape_ToTrimesh(heightfield, 3, 2); + r3FreeSharedShape(heightfield); + size_t vertexCount = 0, indexCount = 0; + vertexCount = r3TriMeshData_Vertices(mesh, NULL, 0); + indexCount = r3TriMeshData_Indices(mesh, NULL, 0); + R3Vector *vertices = malloc(vertexCount * sizeof(*vertices)); + uint32_t *indices = malloc(indexCount * sizeof(*indices)); + if (!vertices || !indices) { + abort(); + } + vertexCount = r3TriMeshData_Vertices(mesh, vertices, vertexCount); + indexCount = r3TriMeshData_Indices(mesh, indices, indexCount); + R3ColliderDesc collider = r3DefaultColliderDesc(); + r3ShapeDesc_SetTrimesh(&collider.shape, (R3VectorView){vertices, vertexCount}, + (R3TriangleView){(const R3Triangle *)indices, indexCount / 3}, + R3_TRIMESH_MERGE_DUPLICATE_VERTICES); + + r3FreeTriMeshData(mesh); + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + free(vertices); + free(indices); + + for (int j = 0; j < 20; j++) { + for (int i = 0; i < 8; i++) { + for (int k = 0; k < 8; k++) { + R3ColliderDesc collider; + switch (j % 6) { + case 0: + collider = r3CuboidColliderDesc(r3Vector(1, 1, 1)); + break; + case 1: + collider = r3BallColliderDesc(1); + break; + case 2: + collider = r3RoundCylinderColliderDesc(1, 1, 0.1); + break; + case 3: + collider = r3ConeColliderDesc(1, 1); + break; + case 4: + collider = r3CapsuleYColliderDesc(1, 1); + break; + default: { + R3Pose poses[] = {r3TranslationPose(r3Vector(0, 0, 0)), + r3TranslationPose(r3Vector(1, 0, 0)), + r3TranslationPose(r3Vector(-1, 0, 0))}; + R3SharedShape *shapes[3] = {NULL}; + shapes[0] = r3CuboidSharedShape(r3Vector(1.0, 0.5, 0.5)); + shapes[1] = r3CuboidSharedShape(r3Vector(0.5, 1.0, 0.5)); + shapes[2] = r3CuboidSharedShape(r3Vector(0.5, 1.0, 0.5)); + R3CompoundShapeDesc colliderParts[TB_COUNT(shapes)]; + for (size_t part = 0; part < TB_COUNT(shapes); ++part) { + colliderParts[part].pose = poses[part]; + colliderParts[part].shape = r3DefaultShapeDesc(); + colliderParts[part].shape.kind = R3_SHAPE_DESC_SHARED; + colliderParts[part].shape.sharedShape = + ((const R3SharedShape *const *)shapes)[part]; + } + collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_COMPOUND; + collider.shape.children = + (R3CompoundShapeView){colliderParts, TB_COUNT(shapes)}; + R3SharedShape *compoundShape = r3ShapeDesc_Build(&collider.shape); + collider.shape.kind = R3_SHAPE_DESC_SHARED; + collider.shape.sharedShape = compoundShape; + for (size_t part = 0; part < TB_COUNT(shapes); part++) { + r3FreeSharedShape(shapes[part]); + } + break; + } + } + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(i * 3 - 12, j * 3 + 4.5, k * 3 - 12); + rigidBody.canSleep = !testbed->noSleep; + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + if (collider.shape.kind == R3_SHAPE_DESC_SHARED) { + r3FreeSharedShape((R3SharedShape *)collider.shape.sharedShape); + } + } + } + } + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/urdf3.c b/c/testbed/examples3d/urdf3.c new file mode 100644 index 000000000..cafc2d8bb --- /dev/null +++ b/c/testbed/examples3d/urdf3.c @@ -0,0 +1,39 @@ +/* Port of examples3d/urdf3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" +#ifdef RAPIER_ROBOTICS +void tbUrdf3(Testbed *testbed) { + R3World *world = r3NewWorld(); + R3UrdfLoaderOptions options = r3DefaultUrdfLoaderOptions(); + options.createCollidersFromVisualShapes = 1; + options.createCollidersFromCollisionShapes = 0; + options.makeRootsFixed = 1; + /* Z-up to Y-up, matching the Rust example's model convention. */ + options.shift = r3Pose(r3Vector(0, 0, 0), r3RotationFromAxisAngle(r3Vector(1, 0, 0), R3_PI / 2)); + R3RigidBodyDesc blueprint = r3DynamicRigidBodyDesc(); + blueprint.canSleep = !testbed->noSleep; + options.rigidBodyBlueprint = blueprint; + + char path[4096]; + snprintf(path, sizeof(path), "%s/3d/T12/urdf/T12.URDF", testbed->assetRoot); + R3UrdfRobot *robot = r3UrdfRobotFromFile(path, &options); + /* Insert the same robot with each joint representation. */ + R3UrdfRobotHandles *impulse = NULL, *multibody = NULL; + impulse = r3UrdfRobot_InsertUsingImpulseJoints(world, robot); + r3UrdfRobot_AppendTransform(robot, r3TranslationPose(r3Vector(10, 0, 0))); + multibody = + r3UrdfRobot_InsertUsingMultibodyJoints(world, robot, R3_MULTIBODY_DISABLE_SELF_CONTACTS); + r3FreeUrdfRobotHandles(impulse); + r3FreeUrdfRobotHandles(multibody); + r3FreeUrdfRobot(robot); + tbSetWorld(testbed, world); + tbCamera(testbed, 20, 20, 20, 5, 0, 0); + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + } + } + r3FreeWorld(world); +} +#endif diff --git a/c/testbed/examples3d/utils/character.h b/c/testbed/examples3d/utils/character.h new file mode 100644 index 000000000..df30869fb --- /dev/null +++ b/c/testbed/examples3d/utils/character.h @@ -0,0 +1,114 @@ +/* Port of examples3d/utils/character.rs. Shared by the character and tether demos. */ +#ifndef EXAMPLE_CHARACTER_3D_H +#define EXAMPLE_CHARACTER_3D_H +#include "testbed.h" +#include "rapier_math.h" + +typedef enum CharacterControlMode { CHARACTER_KINEMATIC, CHARACTER_PID } CharacterControlMode; + +static void updateCharacter(Testbed *viewer, R3World *world, CharacterControlMode *controlMode, + R3KinematicCharacterController *controller, R3PidController *pid, + R3RigidBodyHandle characterHandle) { + R3Real dt = r3TimeStep(world); + + static const char *const modes[] = {"Kinematic", "PID"}; + const CharacterControlMode mode = + (CharacterControlMode)tbChoice(viewer, "Control mode", 0, modes, TB_COUNT(modes), 1, 0); + if (mode != *controlMode) { + r3RigidBody_SetBodyType(characterHandle, + mode == CHARACTER_KINEMATIC ? R3_KINEMATIC_POSITION_BASED + : R3_DYNAMIC, + mode == CHARACTER_PID); + *controlMode = mode; + } + R3Real speed = tbLiveSetting(viewer, "Character speed", .1, 0, 1, 0); + if (viewer->slow) { + speed /= 10; + } + R3Vector desiredMovement = + r3VectorAdd(r3VectorScale(viewer->cameraRight, viewer->inputDirection.x), + r3VectorScale(viewer->cameraForward, viewer->inputDirection.y)); + desiredMovement.y = (viewer->jump ? 2 : 0) - (viewer->descend ? 1 : 0); + desiredMovement = r3VectorScale(desiredMovement, speed); + R3Vector translation = r3RigidBody_Translation(characterHandle); + if (mode == CHARACTER_PID) { + R3PidGains gains = r3PidController_Gains(pid); + const R3Real linKp = tbLiveSetting(viewer, "Linear Kp", 60, 0, 100, 0); + gains.lin_kp = r3Vector(linKp, linKp, linKp); + const R3Real linKi = tbLiveSetting(viewer, "Linear Ki", 1, 0, 10, 0); + gains.lin_ki = r3Vector(linKi, linKi, linKi); + const R3Real linKd = tbLiveSetting(viewer, "Linear Kd", 0.8, 0, 1, 0); + gains.lin_kd = r3Vector(linKd, linKd, linKd); + const R3Real angKp = tbLiveSetting(viewer, "Angular Kp", 60, 0, 100, 0); + gains.ang_kp = r3Vector(angKp, angKp, angKp); + const R3Real angKi = tbLiveSetting(viewer, "Angular Ki", 1, 0, 10, 0); + gains.ang_ki = r3Vector(angKi, angKi, angKi); + const R3Real angKd = tbLiveSetting(viewer, "Angular Kd", 0.8, 0, 1, 0); + gains.ang_kd = r3Vector(angKd, angKd, angKd); + r3PidController_SetGains(pid, gains); + uint32_t axes = 56; /* Angular axes. */ + if (desiredMovement.x != 0 || desiredMovement.y != 0 || desiredMovement.z != 0) { + axes |= desiredMovement.y == 0 ? 5 : 7; + } + r3PidController_SetAxes(pid, axes); + R3Pose target = r3TranslationPose(r3VectorAdd(translation, desiredMovement)); + target.rotation = r3RigidBody_Rotation(characterHandle); + R3Vector correctiveLinear, linvel; + R3AngVector correctiveAngular, angvel; + R3VelocityCorrection pidControllerRigidBodyCorrectionResult = r3PidController_RigidBodyCorrection(pid, dt, characterHandle, target, r3Vector(0, 0, 0), r3Vector(0, 0, 0)); + correctiveLinear = pidControllerRigidBodyCorrectionResult.linear; + correctiveAngular = pidControllerRigidBodyCorrectionResult.angularVelocity; + linvel = r3RigidBody_Linvel(characterHandle); + angvel = r3RigidBody_Angvel(characterHandle); + r3RigidBody_SetLinvel(characterHandle, r3VectorAdd(linvel, correctiveLinear), 1); + r3RigidBody_SetAngvel(characterHandle, r3VectorAdd(angvel, correctiveAngular), 1); + return; + } + /* Kinematic character settings, applied live. */ + R3CharacterControllerSettings settings = r3KinematicCharacterController_Settings(controller); + settings.slide = (R3Bool)tbLiveSetting(viewer, "Slide", settings.slide, 0, 1, 1); + settings.max_slope_climb_angle = tbLiveSetting( + viewer, "Maximum climb angle", settings.max_slope_climb_angle, 0, 2 * R3_PI, 0); + settings.min_slope_slide_angle = tbLiveSetting( + viewer, "Minimum slide angle", settings.min_slope_slide_angle, 0, R3_PI / 2, 0); + settings.snap_to_ground = + (R3Bool)tbLiveSetting(viewer, "Snap to ground", settings.snap_to_ground, 0, 1, 1); + settings.snap_distance.value = tbLiveSetting(viewer, "Snap distance (relative height)", + settings.snap_distance.value, 0, 10, 0); + r3KinematicCharacterController_SetSlide(controller, settings.slide); + r3KinematicCharacterController_SetSlopes(controller, settings.max_slope_climb_angle, + settings.min_slope_slide_angle); + r3KinematicCharacterController_SetSnapToGround(controller, settings.snap_to_ground, + settings.snap_distance); + desiredMovement.y -= speed; /* Artificial gravity, as in the Rust utility. */ + size_t colliderCount = r3RigidBody_Colliders(characterHandle, NULL, 0); + R3ColliderHandle *handles = malloc(colliderCount * sizeof(*handles)); + if (!colliderCount || !handles) { + abort(); + } + colliderCount = r3RigidBody_Colliders(characterHandle, handles, colliderCount); + + const R3ColliderHandle colliderHandle = handles[0]; + free(handles); + R3Pose pose; + R3SharedShape *shape = NULL; + R3Real mass; + pose = r3Collider_Position(colliderHandle); + shape = r3Collider_CloneShape(colliderHandle); + mass = r3RigidBody_Mass(characterHandle); + R3QueryFilter filter = r3DefaultQueryFilter(); + filter.exclude_rigid_body = characterHandle; + R3QueryOptions query = r3DefaultQueryOptions(); + query.filter = filter; + R3CharacterMovement movement = r3KinematicCharacterController_MoveShape(world, &query, controller, dt, shape, pose, desiredMovement); + + tbBodyColor(viewer, characterHandle, movement.grounded ? .1f : .8f, + movement.grounded ? .8f : .1f, .1f, 1); + r3KinematicCharacterController_SolveCharacterCollisionImpulses(controller, shape, dt, + mass, &filter); + r3FreeSharedShape(shape); + + r3RigidBody_SetNextKinematicTranslation(characterHandle, + r3VectorAdd(translation, movement.translation)); +} +#endif diff --git a/c/testbed/examples3d/utils/files.h b/c/testbed/examples3d/utils/files.h new file mode 100644 index 000000000..338f32c25 --- /dev/null +++ b/c/testbed/examples3d/utils/files.h @@ -0,0 +1,69 @@ +#ifndef EXAMPLE_FILES_H +#define EXAMPLE_FILES_H + +/* Directory enumeration shared by the example's scene-discovery code. */ +typedef struct FileNames { + char **names; + size_t count; +} FileNames; + +static void addFilename(FileNames *files, const char *name) { + char **names = realloc(files->names, (files->count + 1) * sizeof(*names)); + if (!names) { + abort(); + } + files->names = names; + names[files->count] = malloc(strlen(name) + 1); + if (!names[files->count]) { + abort(); + } + strcpy(names[files->count++], name); +} + +static void freeFilenames(FileNames *files) { + for (size_t i = 0; i < files->count; ++i) { + free(files->names[i]); + } + free(files->names); + *files = (FileNames){0}; +} +#ifdef _WIN32 +#include + +static FileNames listDirectory(const char *path) { + FileNames result = {0}; + char pattern[8192]; + snprintf(pattern, sizeof(pattern), "%s/*", path); + WIN32_FIND_DATAA entry; + HANDLE directory = FindFirstFileA(pattern, &entry); + if (directory == INVALID_HANDLE_VALUE) { + return result; + } + do { + if (strcmp(entry.cFileName, ".") && strcmp(entry.cFileName, "..")) { + addFilename(&result, entry.cFileName); + } + } while (FindNextFileA(directory, &entry)); + FindClose(directory); + return result; +} +#else +#include + +static FileNames listDirectory(const char *path) { + FileNames result = {0}; + DIR *directory = opendir(path); + if (!directory) { + return result; + } + struct dirent *entry; + while ((entry = readdir(directory))) { + if (strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..")) { + addFilename(&result, entry->d_name); + } + } + closedir(directory); + return result; +} +#endif +#endif diff --git a/c/testbed/examples3d/utils/obj.h b/c/testbed/examples3d/utils/obj.h new file mode 100644 index 000000000..f42e622f7 --- /dev/null +++ b/c/testbed/examples3d/utils/obj.h @@ -0,0 +1,128 @@ +/* Position/index subset of OBJ used by the Rust mesh examples. No runtime dependency. */ +#ifndef EXAMPLE_OBJ_H +#define EXAMPLE_OBJ_H +#include "testbed.h" +#include "rapier_math.h" +#include +#include +#include + +typedef struct ObjMesh { + R3Vector *vertices; + uint32_t *indices; + size_t vertexCount, indexCount; +} ObjMesh; + +static void freeObj(ObjMesh *mesh) { + free(mesh->vertices); + free(mesh->indices); + *mesh = (ObjMesh){0}; +} + +/* Preserve polygon index order, as obj::raw::parse_obj does in the Rust examples. */ +static int loadObj(const char *root, const char *name, ObjMesh *mesh) { + char path[4096]; + if (snprintf(path, sizeof(path), "%s/3d/%s", root, name) >= (int)sizeof(path)) { + return 0; + } + FILE *file = fopen(path, "rb"); + if (!file) { + fprintf(stderr, "Cannot open %s: %s\n", path, strerror(errno)); + return 0; + } + *mesh = (ObjMesh){0}; + char *line = NULL; + size_t capacity = 0, vertexCapacity = 0, indexCapacity = 0; + int valid = 1; + while (valid) { + size_t length = 0; + int ch; + while ((ch = fgetc(file)) != EOF && ch != '\n') { + if (length + 1 >= capacity) { + capacity = capacity ? capacity * 2 : 256; + char *next = realloc(line, capacity); + if (!next) { + abort(); + } + line = next; + } + line[length++] = (char)ch; + } + if (ch == EOF && !length) { + break; + } + if (!line) { + capacity = 256; + line = malloc(capacity); + if (!line) { + abort(); + } + } + line[length] = '\0'; + char *p = line; + while (isspace((unsigned char)*p)) { + ++p; + } + if (p[0] == 'v' && isspace((unsigned char)p[1])) { + double x, y, z; + if (sscanf(p + 1, "%lf %lf %lf", &x, &y, &z) != 3) { + valid = 0; + break; + } + if (mesh->vertexCount == vertexCapacity) { + vertexCapacity = vertexCapacity ? vertexCapacity * 2 : 256; + R3Vector *next = realloc(mesh->vertices, vertexCapacity * sizeof(*next)); + if (!next) { + abort(); + } + mesh->vertices = next; + } + mesh->vertices[mesh->vertexCount++] = r3Vector(x, y, z); + } else if (p[0] == 'f' && isspace((unsigned char)p[1])) { + ++p; + while (*p) { + while (isspace((unsigned char)*p)) { + ++p; + } + if (!*p || *p == '#') { + break; + } + char *end; + long index = strtol(p, &end, 10); + if (end == p || index == 0) { + valid = 0; + break; + } + long vertex = index > 0 ? index - 1 : (long)mesh->vertexCount + index; + if (vertex < 0 || (size_t)vertex >= mesh->vertexCount || + (unsigned long)vertex > UINT32_MAX) { + valid = 0; + break; + } + if (mesh->indexCount == indexCapacity) { + indexCapacity = indexCapacity ? indexCapacity * 2 : 768; + uint32_t *next = realloc(mesh->indices, indexCapacity * sizeof(*next)); + if (!next) { + abort(); + } + mesh->indices = next; + } + mesh->indices[mesh->indexCount++] = (uint32_t)vertex; + p = end; + while (*p && !isspace((unsigned char)*p)) { + ++p; + } + } + } + } + valid = valid && !ferror(file) && mesh->vertexCount && mesh->indexCount && + mesh->indexCount % 3 == 0; + free(line); + fclose(file); + if (!valid) { + fprintf(stderr, "Invalid triangle OBJ: %s\n", path); + freeObj(mesh); + } + return valid; +} +#endif diff --git a/c/testbed/examples3d/vehicle_controller3.c b/c/testbed/examples3d/vehicle_controller3.c new file mode 100644 index 000000000..e1cdb9458 --- /dev/null +++ b/c/testbed/examples3d/vehicle_controller3.c @@ -0,0 +1,105 @@ +/* Port of examples3d/vehicle_controller3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbVehicleController3(Testbed *testbed) { + R3World *world = r3NewWorld(); + { + R3RigidBodyDesc rigidBody = r3FixedRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, -0.1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(5, 0.1, 5)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + const R3Real hw = .3, hh = .15; + R3RigidBodyHandle vehicleHandle; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3Vector(0, 1, 0); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(hw * 2, hh, hw)); + collider.density = 100; + vehicleHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(vehicleHandle, &collider); + } + + R3WheelTuning tuning = r3DefaultWheelTuning(); + tuning.suspension_stiffness = 100; + tuning.suspension_damping = 10; + R3DynamicRayCastVehicleController *vehicle = + r3NewDynamicRayCastVehicleController(vehicleHandle); + const R3Vector wheelPositions[] = { + {hw * 1.5, -hh, hw}, {hw * 1.5, -hh, -hw}, {-hw * 1.5, -hh, hw}, {-hw * 1.5, -hh, -hw}}; + for (size_t i = 0; i < TB_COUNT(wheelPositions); ++i) { + r3DynamicRayCastVehicleController_AddWheel(vehicle, wheelPositions[i], r3Vector(0, -1, 0), + r3Vector(0, 0, 1), hh, hh / 4, &tuning); + } + for (int j = 0; j < 1; ++j) { + for (int k = 0; k < 4; ++k) { + for (int i = 0; i < 8; ++i) { + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = + r3Vector(i * .2 - .8, j * .2 + .1, k * .2 + .8); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.1, 0.1, 0.1)); + R3RigidBodyHandle rigidBodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(rigidBodyHandle, &collider); + } + } + } + } + { + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(2, .1, 5)); + collider.position.translation = r3Vector(7, 0.3, 0); + collider.position.rotation = r3RotationFromAxisAngle(r3Vector(0, 0, 1), 0.2); + r3InsertColliderWithoutParent(world, &collider); + } + { + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(2, .1, 5)); + collider.position.translation = r3Vector(10.1, 2.2, 0); + collider.position.rotation = r3RotationFromAxisAngle(r3Vector(0, 0, 1), 0.9); + r3InsertColliderWithoutParent(world, &collider); + } + R3Real heights[21 * 21]; + for (int j = 0; j <= 20; ++j) { + for (int i = 0; i <= 20; ++i) { + heights[i + j * 21] = -cos(i * .25) - cos(j * .25); + } + } + R3ColliderDesc collider = r3DefaultColliderDesc(); + collider.shape.kind = R3_SHAPE_DESC_HEIGHTFIELD; + collider.shape.heights = (R3RealView){heights, (21) * (21)}; + collider.shape.rows = 21; + collider.shape.columns = 21; + collider.shape.scale = r3Vector(10, .4, 10); + collider.shape.flags = 0; + collider.position.translation = r3Vector(-7, 0, 0); + r3InsertColliderWithoutParent(world, &collider); + + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + const R3Real engineForce = 30 * testbed->inputDirection.y; + const R3Real steeringAngle = -.7 * testbed->inputDirection.x; + for (size_t i = 0; i < 2; ++i) { + r3DynamicRayCastVehicleController_SetWheelControls(vehicle, i, steeringAngle, + engineForce, 0); + } + R3QueryFilter filter = r3DefaultQueryFilter(); + filter.flags = R3_QUERY_EXCLUDE_DYNAMIC; + filter.exclude_rigid_body = vehicleHandle; + + R3Real dt = r3TimeStep(world); + r3DynamicRayCastVehicleController_UpdateVehicle(vehicle, dt, &filter); + r3Step(world, NULL, NULL); + } + } + r3FreeDynamicRayCastVehicleController(vehicle); + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/vehicle_joints3.c b/c/testbed/examples3d/vehicle_joints3.c new file mode 100644 index 000000000..bcb7784e1 --- /dev/null +++ b/c/testbed/examples3d/vehicle_joints3.c @@ -0,0 +1,130 @@ +/* Port of examples3d/vehicle_joints3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +void tbVehicleJoints3(Testbed *testbed) { + R3World *world = r3NewWorld(); + R3Real heights[101 * 101]; + for (int j = 0; j <= 100; ++j) { + for (int i = 0; i <= 100; ++i) { + heights[i + j * 101] = -cos(i * .3) - cos(j * .3); + } + } + R3ColliderDesc ground = r3DefaultColliderDesc(); + ground.shape.kind = R3_SHAPE_DESC_HEIGHTFIELD; + ground.shape.heights = (R3RealView){heights, (101) * (101)}; + ground.shape.rows = 101; + ground.shape.columns = 101; + ground.shape.scale = r3Vector(60, .4, 60); + ground.shape.flags = 0; + ground.position.translation = r3Vector(-7, 0, 0); + ground.friction = 1; + r3InsertColliderWithoutParent(world, &ground); + + const R3InteractionGroups carGroups = {1, ~UINT32_C(1), 0}; + const R3Vector wheelParams[] = {{.6874, .2783, -.7802}, + {-.6874, .2783, -.7802}, + {.64, .2783, 1.0254}, + {-.64, .2783, 1.0254}}; + const R3Real suspensionHeight = .12, maxSteeringAngle = 35 * R3_PI / 180; + const R3Real driveStrength = 1, wheelRadius = .28; + const R3Vector carPosition = {0, wheelRadius + suspensionHeight, 0}; + const R3Vector bodyPositionInCarSpace = {0, .4739, 0}; + R3RigidBodyHandle bodyHandle; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = r3VectorAdd(carPosition, bodyPositionInCarSpace); + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3CuboidColliderDesc(r3Vector(0.65, 0.3, 0.9)); + collider.density = 100; + collider.collisionGroups = carGroups; + bodyHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(bodyHandle, &collider); + } + + R3ImpulseJointHandle steeringJoints[2], motorJoints[2]; + for (size_t wheelId = 0; wheelId < TB_COUNT(wheelParams); ++wheelId) { + const int isFront = wheelId >= 2; + const R3Vector wheelCenter = r3VectorAdd(carPosition, wheelParams[wheelId]); + R3SharedShape *ball = r3BallSharedShape(wheelRadius); + R3MassProperties axleMassProps = r3SharedShape_MassProperties(ball, 100); + r3FreeSharedShape(ball); + R3RigidBodyDesc axleRb = r3DynamicRigidBodyDesc(); + axleRb.position.translation = wheelCenter; + axleRb.canSleep = !testbed->noSleep; + R3RigidBodyHandle axleHandle = r3InsertRigidBody(world, &axleRb); + + r3RigidBody_SetAdditionalMassProperties(axleHandle, axleMassProps, 1); + R3RigidBodyHandle wheelHandle; + { + R3RigidBodyDesc rigidBody = r3DynamicRigidBodyDesc(); + rigidBody.position.translation = wheelCenter; + rigidBody.canSleep = !testbed->noSleep; + R3ColliderDesc collider = r3BallColliderDesc(wheelRadius); + collider.density = 100; + collider.collisionGroups = carGroups; + collider.friction = 1; + wheelHandle = r3InsertRigidBody(world, &rigidBody); + r3InsertCollider(wheelHandle, &collider); + } + R3ColliderDesc wheelFakeCo = r3CylinderColliderDesc(wheelRadius / 2, wheelRadius); + wheelFakeCo.position.rotation = r3RotationFromAxisAngle(r3Vector(0, 0, 1), R3_PI / 2); + wheelFakeCo.isSensor = 1; + wheelFakeCo.density = 0; + wheelFakeCo.collisionGroups = (R3InteractionGroups){0, 0, 0}; + r3InsertCollider(wheelHandle, &wheelFakeCo); + + uint8_t lockedAxes = 1 | 4 | 8 | 32; + if (!isFront) { + lockedAxes |= 16; + } + R3JointDesc suspensionJoint = r3DefaultJointDesc(); + suspensionJoint.lockedAxes = lockedAxes; + r3JointDesc_SetLimits(&suspensionJoint, R3_AXIS_LIN_Y, 0, suspensionHeight); + r3JointDesc_SetMotorPosition(&suspensionJoint, R3_AXIS_LIN_Y, 0, 1e4, 1e3); + suspensionJoint.localFrame1.translation = + r3VectorSub(wheelParams[wheelId], bodyPositionInCarSpace); + if (isFront) { + r3JointDesc_SetLimits(&suspensionJoint, R3_AXIS_ANG_Y, -maxSteeringAngle, + maxSteeringAngle); + } + R3ImpulseJointHandle bodyAxleJointHandle = + r3InsertImpulseJoint(bodyHandle, axleHandle, &suspensionJoint); + + R3JointDesc wheelJoint = r3RevoluteJointDesc(r3Vector(1, 0, 0)); + R3ImpulseJointHandle wheelJointHandle = + r3InsertImpulseJoint(axleHandle, wheelHandle, &wheelJoint); + + if (isFront) { + steeringJoints[wheelId - 2] = bodyAxleJointHandle; + motorJoints[wheelId - 2] = wheelJointHandle; + } + } + tbCamera(testbed, 10, 10, 10, 0, 0, 0); + testbed->snapshotSupported = 0; + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + const R3Real thrust = -driveStrength * testbed->inputDirection.y; + const R3Real steering = -testbed->inputDirection.x; + const R3Real boost = testbed->boost ? 1.5 : 1; + const R3Bool shouldWakeUp = thrust != 0 || steering != 0; + for (size_t i = 0; i < TB_COUNT(steeringJoints); ++i) { + r3ImpulseJoint_SetMotorPosition(steeringJoints[i], R3_AXIS_ANG_Y, + maxSteeringAngle * steering, 1e4, 1e3, shouldWakeUp); + } + const R3Real sidewaysShift = sin(maxSteeringAngle * steering) * .5; + const R3Real speedDiff = + sidewaysShift > 0 ? hypot(1, sidewaysShift) : 1 / hypot(1, sidewaysShift); + const R3Real ms[] = {1 / speedDiff, speedDiff}; + for (size_t i = 0; i < TB_COUNT(motorJoints); ++i) { + r3ImpulseJoint_SetMotorVelocity(motorJoints[i], R3_AXIS_ANG_X, + -30 * thrust * ms[i] * boost, 1e2, shouldWakeUp); + } + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/examples3d/voxels3.c b/c/testbed/examples3d/voxels3.c new file mode 100644 index 000000000..7880461eb --- /dev/null +++ b/c/testbed/examples3d/voxels3.c @@ -0,0 +1,177 @@ +/* Port of examples3d/voxels3.rs. */ +#include "testbed.h" +#include "rapier_helpers.h" +#include "rapier_math.h" + +#include + +static R3Bool RAPIER_CALL isVoxel(void *userData, const R3ReadContext *read, + R3ColliderHandle handle) { + (void)userData; + R3Bool result = r3ReadCollider_IsVoxels(read, handle); + return result; +} + +void tbVoxels3(Testbed *testbed) { + R3World *world = r3NewWorld(); + const int fallingObjects = (int)tbSetting( + testbed, "Falling objects: 0 Ball, 1 Cuboid, 2 Cylinder, 3 Cone, 4 Capsule, 5 Mixed", 5, 0, + 5, 1); + const R3Real voxelSizeY = tbSetting(testbed, "Voxel size y", 1, .5, 2, 0); + const R3Vector voxelSize = {1, voxelSizeY, 1}; + const int testCcd = (int)tbSetting(testbed, "Test CCD", 0, 0, 1, 1); + /* The Rust example leaves its optional OBJ block disabled too. */ + const int n = 200; + R3Vector *samples = malloc((200 * 200 + 4 * 4 * 200) * sizeof(*samples)); + if (!samples) { + abort(); + } + size_t sampleCount = 0; + for (int i = 0; i < n; ++i) { + for (int j = 0; j < n; ++j) { + const R3Real y = fmax(-.8, fmin(.8, sin(i / (R3Real)n * 10))) * + fmax(-.8, fmin(.8, cos(j / (R3Real)n * 10))) * 16; + samples[sampleCount++] = r3Vector(i, y * voxelSizeY, j); + if (i == 0 || i == n - 1 || j == 0 || j == n - 1) { + for (int k = 0; k < 4; ++k) { + samples[sampleCount++] = r3Vector(i, (y + k) * voxelSizeY, j); + } + } + } + } + R3SharedShape *shape = + r3VoxelsSharedShapeFromPoints(voxelSize, (R3VectorView){samples, sampleCount}); + free(samples); + R3Aabb floorAabb = r3SharedShape_ComputeAabb(shape, r3TranslationPose(r3Vector(0, 0, 0))); + R3ColliderDesc floor = r3DefaultColliderDesc(); + floor.shape.kind = R3_SHAPE_DESC_SHARED; + floor.shape.sharedShape = shape; + + r3InsertColliderWithoutParent(world, &floor); + r3FreeSharedShape(shape); + + const R3Vector size = r3VectorSub(floorAabb.maxs, floorAabb.mins); + const R3Vector extents = r3VectorScale(size, .75); + const R3Vector margin = r3VectorScale(r3VectorSub(size, extents), .5); + const int nik = 30; + for (int i = 0; i < nik; ++i) { + for (int j = 0; j < 5; ++j) { + for (int k = 0; k < nik; ++k) { + R3RigidBodyDesc rb = r3DynamicRigidBodyDesc(); + rb.position.translation = r3Vector( + floorAabb.mins.x + margin.x + i * extents.x / nik, floorAabb.maxs.y + j * 2, + floorAabb.mins.z + margin.z + k * extents.z / nik); + rb.canSleep = !testbed->noSleep; + if (testCcd) { + rb.linvel = r3Vector(0, -1000, 0); + rb.ccdEnabled = 1; + } + R3ColliderDesc co; + switch (fallingObjects == 5 ? j % 5 : fallingObjects) { + case 0: + co = r3BallColliderDesc(.5); + break; + case 1: + co = r3CuboidColliderDesc(r3Vector(.5, .5, .5)); + break; + case 2: + co = r3CylinderColliderDesc(.5, .5); + break; + case 3: + co = r3ConeColliderDesc(.5, .5); + break; + case 4: + co = r3CapsuleYColliderDesc(.5, .5); + break; + } + R3RigidBodyHandle rbHandle = r3InsertRigidBody(world, &rb); + r3InsertCollider(rbHandle, &co); + } + } + } + R3ColliderHandle hitIndicatorHandle, hitHighlightHandle; + R3ColliderDesc indicator = r3BallColliderDesc(.1); + indicator.collisionGroups = (R3InteractionGroups){0, 0, 0}; + hitIndicatorHandle = r3InsertColliderWithoutParent(world, &indicator); + + R3ColliderDesc highlight = r3CuboidColliderDesc(r3Vector(.51, .51, .51)); + highlight.collisionGroups = (R3InteractionGroups){0, 0, 0}; + hitHighlightHandle = r3InsertColliderWithoutParent(world, &highlight); + + tbColliderColor(testbed, hitIndicatorHandle, .5, .5, .1, 1); + tbColliderColor(testbed, hitHighlightHandle, .1, .5, .1, 1); + tbCamera(testbed, 100, 100, 100, 0, 0, 0); + testbed->snapshotSupported = 0; + tbLabel(testbed, "Voxel editing:", "Space: add | Left Shift + Space: remove"); + tbSetWorld(testbed, world); + + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + r3Step(world, NULL, NULL); + if (!testbed->rayValid) { + continue; + } + R3QueryOptions query = r3DefaultQueryOptions(); + + query.predicate = isVoxel; + R3RayHit hit; + R3Bool found; + R3OptionalRayHit tryCastRayResult = + r3TryCastRay(world, &query, testbed->rayOrigin, testbed->rayDirection, FLT_MAX, 1); + hit = tryCastRayResult.hit; + found = tryCastRayResult.found; + + if (found) { + R3Pose hitPos = r3Collider_Position(hit.collider); + const R3Vector hitLocalNormal = + r3RotationTransformVector(r3RotationInverse(hitPos.rotation), hit.normal); + R3VoxelKey voxelKey; + R3Vector voxelCenterLocal, size; + R3Bool voxelFound; + R3VoxelQuery colliderVoxelAtFlatIdResult2 = + r3Collider_VoxelAtFlatId(hit.collider, hit.feature_id); + voxelKey = colliderVoxelAtFlatIdResult2.key; + voxelCenterLocal = colliderVoxelAtFlatIdResult2.center; + size = colliderVoxelAtFlatIdResult2.size; + voxelFound = colliderVoxelAtFlatIdResult2.found; + if (!voxelFound) { + continue; + } + const R3Vector voxelCenter = r3PoseTransformPoint(hitPos, voxelCenterLocal); + r3Collider_SetTranslation(hitHighlightHandle, voxelCenter); + R3SharedShape *shape = r3CuboidSharedShape( + r3VectorAdd(r3VectorScale(size, .5), r3Vector(.001, .001, .001))); + r3Collider_SetShape(hitHighlightHandle, shape); + r3FreeSharedShape(shape); + const R3Vector hitPt = r3VectorAdd( + testbed->rayOrigin, r3VectorScale(testbed->rayDirection, hit.time_of_impact)); + r3Collider_SetTranslation(hitIndicatorHandle, hitPt); + shape = r3BallSharedShape(r3VectorLength(size) / 3.5); + r3Collider_SetShape(hitIndicatorHandle, shape); + r3FreeSharedShape(shape); + if (testbed->jump) { + R3VoxelKey affectedKey = voxelKey; + if (!testbed->removeVoxel) { + const R3Vector a = {fabs(hitLocalNormal.x), fabs(hitLocalNormal.y), + fabs(hitLocalNormal.z)}; + if (a.x >= a.y && a.x >= a.z) { + affectedKey.x += hitLocalNormal.x >= 0 ? 1 : -1; + } else if (a.y >= a.z) { + affectedKey.y += hitLocalNormal.y >= 0 ? 1 : -1; + } else { + affectedKey.z += hitLocalNormal.z >= 0 ? 1 : -1; + } + } + + r3Collider_SetVoxel(hit.collider, affectedKey, !testbed->removeVoxel); + } + } else { + const R3Vector behindCamera = + r3VectorSub(testbed->rayOrigin, r3VectorScale(testbed->rayDirection, 1000)); + r3Collider_SetTranslation(hitIndicatorHandle, behindCamera); + r3Collider_SetTranslation(hitHighlightHandle, behindCamera); + } + } + } + r3FreeWorld(world); +} diff --git a/c/testbed/font_data.h.in b/c/testbed/font_data.h.in new file mode 100644 index 000000000..3ec40f1e6 --- /dev/null +++ b/c/testbed/font_data.h.in @@ -0,0 +1,5 @@ +/* Generated from the unmodified SIL OFL font downloaded from Mozilla Fira. Do not edit. */ +#ifndef RAPIER_TESTBED_FONT_DATA_H +#define RAPIER_TESTBED_FONT_DATA_H +static const unsigned char tbFontData[] = { @TB_FONT_BYTES@ }; +#endif diff --git a/c/testbed/grab.c b/c/testbed/grab.c new file mode 100644 index 000000000..3ee82e32b --- /dev/null +++ b/c/testbed/grab.c @@ -0,0 +1,309 @@ +#include "grab.h" +#include "rapier_math.h" +#include "rapier_helpers.h" +#include + +#define TRY(call) \ + do { \ + status = (call); \ + if (status != RAPIER_CONST(OK)) \ + goto done; \ + } while (0) + +static bool contains(Testbed *t, RAPIER_TYPE(RigidBodyHandle) handle) { + if (handle.world != t->world) return false; + RAPIER_TYPE(Bool) found = RAPIER_FN(RigidBody_Contains)(handle); + return RAPIER_FN(LastStatus)() == RAPIER_CONST(OK) && found; +} + +static RAPIER_TYPE(RigidBodyHandle) pulledBody(Testbed *t, const TbGrab *grab) { + if (grab->joint.world != t->world) return RAPIER_CONST(INVALID_RIGID_BODY_HANDLE); + RAPIER_TYPE(RigidBodyHandle) second = + RAPIER_FN(ImpulseJoint_Bodies)(grab->joint).body2; + if (RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)) { + return second; /* Tearing may transfer the joint to another proxy. */ + } + return grab->body; +} + +RAPIER_TYPE(Status) tbGrabRelease(Testbed *t, TbGrab *grab) { + if (grab->active && grab->body.world != t->world) { + *grab = (TbGrab){0}; /* The old world was replaced, so its handles are unusable. */ + } + if (!grab->active) { + return RAPIER_CONST(OK); + } + RAPIER_TYPE(Status) status = RAPIER_CONST(OK); + RAPIER_TYPE(RigidBodyHandle) pulled = pulledBody(t, grab); + grab->active = false; + if (contains(t, pulled)) { + TRY(RAPIER_FN(RigidBody_WakeUp)(pulled, 1)); + } + RAPIER_FN(RemoveRigidBody)(grab->mouseBody, 1); + TRY(RAPIER_FN(LastStatus)()); + if (grab->soft) { + RAPIER_TYPE(RigidBodyHandle) proxies[] = {grab->body, pulled}; + for (size_t i = 0; i < TB_COUNT(proxies); ++i) { + if (!contains(t, proxies[i])) { + continue; + } + + RAPIER_TYPE(Bool) isFrame; + TRY(RAPIER_FN(RigidBody_ValidateHandle)(proxies[i])); + isFrame = RAPIER_FN(RigidBody_IsSoftFrame)(proxies[i]); + TRY(RAPIER_FN(LastStatus)()); + if (isFrame) { + RAPIER_FN(RemoveRigidBody)(proxies[i], 1); + TRY(RAPIER_FN(LastStatus)()); + } + } + } +done: + return status; +} + +static RAPIER_TYPE(Status) pick(Testbed *t, RAPIER_TYPE(Real) radius, + RAPIER_TYPE(RigidBodyHandle) *body, RAPIER_TYPE(Vector) *point, + bool *found) { + RAPIER_TYPE(Status) status = RAPIER_CONST(OK); + RAPIER_TYPE(QueryOptions) query; + RAPIER_TYPE(QueryFilter) filter; +#if defined(RAPIER_DIM2) + RAPIER_TYPE(ColliderHandle) *hits = NULL; +#endif + RAPIER_TYPE(ColliderHandle) collider; + *found = false; + filter = RAPIER_FN(DefaultQueryFilter)(); + filter.flags = RAPIER_CONST(QUERY_EXCLUDE_FIXED) | RAPIER_CONST(QUERY_EXCLUDE_KINEMATIC) | + RAPIER_CONST(QUERY_EXCLUDE_SENSORS); + query = RAPIER_FN(DefaultQueryOptions)(); + query.filter = filter; +#if defined(RAPIER_DIM2) + if (!t->cursorValid) { + goto done; + } + /* Prefer a solid interior, but ignore the outward side of an open rope. */ + size_t count = RAPIER_FN(IntersectPoint)(t->world, &query, t->cursor, NULL, 0); + TRY(RAPIER_FN(LastStatus)()); + if (count) { + hits = malloc(count * sizeof(*hits)); + if (!hits) { + abort(); + } + count = RAPIER_FN(IntersectPoint)(t->world, &query, t->cursor, hits, count); + TRY(RAPIER_FN(LastStatus)()); + for (size_t i = 0; i < count; ++i) { + RAPIER_TYPE(SoftBodyHandle) soft; + TRY(RAPIER_FN(Collider_ValidateHandle)(hits[i])); + *body = RAPIER_FN(Collider_Parent)(hits[i]); + TRY(RAPIER_FN(LastStatus)()); + if (body->index == UINT32_MAX) { + continue; + } + TRY(RAPIER_FN(RigidBody_ValidateHandle)(*body)); + soft = RAPIER_FN(RigidBody_SoftBody)(*body); + TRY(RAPIER_FN(LastStatus)()); + if (soft.index != UINT32_MAX) { + RAPIER_TYPE(Bool) closed; + TRY(RAPIER_FN(SoftBody_ValidateHandle)(soft)); + closed = RAPIER_FN(SoftBody_MeshIsClosed)(soft, hits[i]); + TRY(RAPIER_FN(LastStatus)()); + if (!closed) { + continue; + } + } + *point = t->cursor; + *found = true; + goto done; + } + } + /* Thin surfaces can also be picked within eight screen pixels. Project onto + * the boundary so an open polyline's nominal inside half-plane is not a hit. */ + RAPIER_TYPE(PointProjection) + hit = RAPIER_FN(ProjectPoint)(t->world, &query, t->cursor, radius, 0); + status = RAPIER_FN(LastStatus)(); + if (status == RAPIER_CONST(NOT_FOUND)) { + status = RAPIER_CONST(OK); + goto done; + } + if (status != RAPIER_CONST(OK)) { + goto done; + } + collider = hit.collider; + *point = hit.point; +#else + (void)radius; + if (!t->rayValid) { + goto done; + } + RAPIER_TYPE(Real) toi; + RAPIER_TYPE(Bool) hit; + RAPIER_TYPE(RayToi) + castRayToiResult2 = + RAPIER_FN(CastRayToi)(t->world, &query, t->rayOrigin, t->rayDirection, FLT_MAX, 1); + collider = castRayToiResult2.collider; + toi = castRayToiResult2.toi; + hit = castRayToiResult2.found; + TRY(RAPIER_FN(LastStatus)()); + if (!hit) { + goto done; + } + *point = RAPIER_FN(VectorAdd)(t->rayOrigin, RAPIER_FN(VectorScale)(t->rayDirection, toi)); +#endif + + TRY(RAPIER_FN(Collider_ValidateHandle)(collider)); + *body = RAPIER_FN(Collider_Parent)(collider); + TRY(RAPIER_FN(LastStatus)()); + *found = body->index != UINT32_MAX; +done: +#if defined(RAPIER_DIM2) + free(hits); +#endif + return status; +} + +RAPIER_TYPE(Status) tbGrabBegin(Testbed *t, TbGrab *grab, RAPIER_TYPE(Real) pickRadius) { + if (grab->active) { + return RAPIER_CONST(OK); + } + RAPIER_TYPE(Status) status = RAPIER_CONST(OK); + RAPIER_TYPE(RigidBodyDesc) builder; + RAPIER_TYPE(JointDesc) joint; + RAPIER_TYPE(Vector) point, localAnchor = V(0, 0, 0); + RAPIER_TYPE(RigidBodyHandle) body; + bool found; + TRY(pick(t, pickRadius, &body, &point, &found)); + if (!found) { + goto done; + } + + RAPIER_TYPE(Bool) dynamic; + RAPIER_TYPE(SoftBodyHandle) soft; + TRY(RAPIER_FN(RigidBody_ValidateHandle)(body)); + dynamic = RAPIER_FN(RigidBody_IsDynamic)(body); + TRY(RAPIER_FN(LastStatus)()); + if (!dynamic) { + goto done; + } + soft = RAPIER_FN(RigidBody_SoftBody)(body); + TRY(RAPIER_FN(LastStatus)()); + *grab = (TbGrab){.body = body, + .mouseBody = {NULL, UINT32_MAX, UINT32_MAX}, + .joint = {NULL, UINT32_MAX, UINT32_MAX}, + .soft = soft.index != UINT32_MAX}; + if (grab->soft) { + size_t count; + uint32_t nearest = 0, cluster; + RAPIER_TYPE(Real) best = FLT_MAX; + TRY(RAPIER_FN(SoftBody_ValidateHandle)(soft)); + count = RAPIER_FN(SoftBody_NumParticles)(soft); + TRY(RAPIER_FN(LastStatus)()); + if (!count) { + goto done; + } + for (size_t i = 0; i < count; ++i) { + RAPIER_TYPE(Vector) position = RAPIER_FN(SoftBody_ParticlePosition)(soft, i); + TRY(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(Vector) delta = RAPIER_FN(VectorSub)(position, point); + RAPIER_TYPE(Real) distance = RAPIER_FN(VectorDot)(delta, delta); + if (distance < best) { + best = distance; + nearest = (uint32_t)i; + } + } + point = RAPIER_FN(SoftBody_ParticlePosition)(soft, nearest); + TRY(RAPIER_FN(LastStatus)()); + cluster = RAPIER_FN(SoftBody_AddCluster)(soft, &nearest, 1); + TRY(RAPIER_FN(LastStatus)()); + TRY(RAPIER_FN(SoftBody_ValidateHandle)(soft)); + grab->body = RAPIER_FN(SoftBody_ClusterProxy)(soft, cluster); + TRY(RAPIER_FN(LastStatus)()); + } else { + RAPIER_TYPE(Pose) pose = RAPIER_FN(RigidBody_Position)(body); + TRY(RAPIER_FN(LastStatus)()); + localAnchor = RAPIER_FN(PoseTransformPoint)(RAPIER_FN(PoseInverse)(pose), point); + } + grab->active = true; + grab->planePoint = point; + builder = RAPIER_FN(KinematicPositionBasedRigidBodyDesc)(); + builder.position.translation = point; + grab->mouseBody = RAPIER_FN(InsertRigidBody)(t->world, &builder); + TRY(RAPIER_FN(LastStatus)()); + joint = RAPIER_FN(DefaultJointDesc)(); + joint.localFrame2.translation = localAnchor; + joint.motorAxes = (1u << RAPIER_CONST(DIMENSION)) - 1; + for (uint32_t axis = 0; axis < RAPIER_CONST(DIMENSION); ++axis) { + joint.motors[axis].stiffness = 1000; + joint.motors[axis].damping = 50; + } + grab->joint = RAPIER_FN(InsertImpulseJoint)(grab->mouseBody, grab->body, &joint); + TRY(RAPIER_FN(LastStatus)()); + TRY(RAPIER_FN(RigidBody_WakeUp)(grab->body, 1)); +done: + if (status != RAPIER_CONST(OK)) { + (void)tbGrabRelease(t, grab); + } + return status; +} + +RAPIER_TYPE(Status) tbGrabUpdate(Testbed *t, TbGrab *grab, RAPIER_TYPE(Vector) cameraForward) { + if (!grab->active) { + return RAPIER_CONST(OK); + } + RAPIER_TYPE(RigidBodyHandle) pulled = pulledBody(t, grab); + if (!contains(t, pulled) || !contains(t, grab->mouseBody)) { + return tbGrabRelease(t, grab); + } + RAPIER_TYPE(Vector) target; +#if defined(RAPIER_DIM2) + (void)cameraForward; + if (!t->cursorValid) { + return RAPIER_CONST(OK); + } + target = t->cursor; +#else + if (!t->rayValid) { + return RAPIER_CONST(OK); + } + RAPIER_TYPE(Real) denominator = RAPIER_FN(VectorDot)(t->rayDirection, cameraForward); + if (fabs(denominator) < 1.0e-6) { + return RAPIER_CONST(OK); + } + RAPIER_TYPE(Real) + distance = + RAPIER_FN(VectorDot)(RAPIER_FN(VectorSub)(grab->planePoint, t->rayOrigin), cameraForward) / + denominator; + if (distance <= 0) { + return RAPIER_CONST(OK); + } + target = RAPIER_FN(VectorAdd)(t->rayOrigin, RAPIER_FN(VectorScale)(t->rayDirection, distance)); +#endif + RAPIER_TYPE(Status) status = RAPIER_CONST(OK); + + TRY(RAPIER_FN(RigidBody_ValidateHandle)(grab->mouseBody)); + TRY(RAPIER_FN(RigidBody_SetNextKinematicTranslation)(grab->mouseBody, target)); + TRY(RAPIER_FN(RigidBody_WakeUp)(pulled, 1)); +done: + return status; +} + +void tbGrabDrawCue(Testbed *t, const TbGrab *grab) { + if (!grab->active) { + return; + } + RAPIER_TYPE(RigidBodyHandle) pulled = pulledBody(t, grab); + if (!contains(t, pulled) || !contains(t, grab->mouseBody)) { + return; + } + + RAPIER_TYPE(Vector) a, b; + a = RAPIER_FN(RigidBody_Translation)(grab->mouseBody); + b = RAPIER_FN(RigidBody_Translation)(pulled); + if (RAPIER_FN(RigidBody_ValidateHandle)(grab->mouseBody) != RAPIER_CONST(OK) || + RAPIER_FN(LastStatus)() != RAPIER_CONST(OK) || + RAPIER_FN(RigidBody_ValidateHandle)(pulled) != RAPIER_CONST(OK) || + RAPIER_FN(LastStatus)() != RAPIER_CONST(OK)) { + return; + } + tbLine(t, a, b, .2f, .3f, .4f, 1); +} diff --git a/c/testbed/grab.h b/c/testbed/grab.h new file mode 100644 index 000000000..3aa77ee72 --- /dev/null +++ b/c/testbed/grab.h @@ -0,0 +1,18 @@ +#ifndef RAPIER_TESTBED_GRAB_H +#define RAPIER_TESTBED_GRAB_H +#include "testbed.h" +#include + +/* Mirrors src_testbed/grab.rs: a motor joint pulls a body or one soft particle. */ +typedef struct TbGrab { + bool active, soft; + RAPIER_TYPE(RigidBodyHandle) body, mouseBody; + RAPIER_TYPE(ImpulseJointHandle) joint; + RAPIER_TYPE(Vector) planePoint; +} TbGrab; + +RAPIER_TYPE(Status) tbGrabBegin(Testbed *, TbGrab *, RAPIER_TYPE(Real) pickRadius); +RAPIER_TYPE(Status) tbGrabUpdate(Testbed *, TbGrab *, RAPIER_TYPE(Vector) cameraForward); +RAPIER_TYPE(Status) tbGrabRelease(Testbed *, TbGrab *); +void tbGrabDrawCue(Testbed *, const TbGrab *); +#endif diff --git a/c/testbed/graphics.c b/c/testbed/graphics.c new file mode 100644 index 000000000..cc44238a4 --- /dev/null +++ b/c/testbed/graphics.c @@ -0,0 +1,816 @@ +#include "testbed_internal.h" +#include "graphics.h" +#include "raymath.h" +#include "rlgl.h" +#include +#include + +/* Cache by collider generation and immutable shape identity. Geometry is deduplicated + * by content so separately constructed equal shapes still share instanced draws. */ +typedef struct Geometry { + Mesh mesh; + RAPIER_TYPE(Vector) *triangles, *lines; + size_t nt, nl; + uint64_t hash; + bool allocated, live; +} Geometry; + +typedef struct Entry { + uint32_t generation; + uintptr_t identity; + RAPIER_TYPE(SharedShape) *shape; + size_t geometry; + bool valid, soft, seen; +} Entry; + +typedef struct Batch { + size_t geometry; + Color color; + Matrix *transforms; + size_t count, capacity; +} Batch; + +typedef struct TransparentDraw { + size_t batch, instance; + float depth; + bool triangle; + Vector3 vertices[3]; + Color color; +} TransparentDraw; + +typedef struct VisualMesh { + Mesh mesh; + Material material; + Texture2D texture; +} VisualMesh; + +struct TbGraphics { + Geometry *geometries; + size_t geometryCount, geometryCapacity; + Entry *entries; + size_t entryCapacity; + Batch *batches; + size_t batchCount, batchCapacity; + TransparentDraw *transparent; + size_t transparentCount, transparentCapacity; + RAPIER_TYPE(ColliderHandle) *handles; + RAPIER_TYPE(SoftMeshInfo) *softMeshes; + size_t handlesCapacity, softMeshCapacity; + RAPIER_TYPE(SoftBodyHandle) *softHandles; + size_t softHandlesCapacity; + RAPIER_TYPE(Vector) *vertices; + size_t vertexCapacity; + uint32_t *indices; + size_t indexCapacity; + RAPIER_TYPE(DebugLine) *debug; + size_t debugCapacity; + VisualMesh *visuals; + size_t visualCount; + Shader visualShader; + Shader shader; + Material material; +}; + +static void *grow(void *p, size_t *cap, size_t n, size_t elem) { + if (n <= *cap) { + return p; + } + size_t old = *cap; + size_t next = old ? old * 2 : 16; + if (next < n) { + next = n; + } + if (next > SIZE_MAX / elem) { + abort(); + } + void *q = realloc(p, next * elem); + if (!q) { + abort(); + } + memset((char *)q + old * elem, 0, (next - old) * elem); + *cap = next; + return q; +} + +static Vector3 vec(RAPIER_TYPE(Vector) p) { +#if defined(RAPIER_DIM3) + return (Vector3){(float)p.x, (float)p.y, (float)p.z}; +#else + return (Vector3){(float)p.x, (float)p.y, 0}; +#endif +} + +static Matrix transform(RAPIER_TYPE(Pose) p) { +#if defined(RAPIER_DIM3) + Matrix m = QuaternionToMatrix((Quaternion){(float)p.rotation.x, (float)p.rotation.y, + (float)p.rotation.z, (float)p.rotation.w}); +#else + Matrix m = MatrixRotateZ((float)p.rotation.angle); +#endif + Vector3 v = vec(p.translation); + m.m12 = v.x; + m.m13 = v.y; + m.m14 = v.z; + return m; +} + +static const Color palette[] = {{82, 130, 150, 255}, {222, 139, 89, 255}, {121, 165, 108, 255}, + {159, 133, 184, 255}, {217, 180, 75, 255}, {94, 162, 164, 255}, + {193, 115, 136, 255}, {132, 149, 178, 255}}; + +static Color color(Testbed *t, RAPIER_TYPE(ColliderHandle) h) { + RAPIER_TYPE(RigidBodyHandle) p; + RAPIER_TYPE(Bool) sensor; + p = RAPIER_FN(Collider_Parent)(h); + TB(t, RAPIER_FN(LastStatus)()); + sensor = RAPIER_FN(Collider_IsSensor)(h); + TB(t, RAPIER_FN(LastStatus)()); + Color result = palette[(p.index == UINT32_MAX ? h.index : p.index) % TB_COUNT(palette)]; + if (p.index == UINT32_MAX) { + result = (Color){170, 174, 175, 255}; + } else { + RAPIER_TYPE(Bool) fixed; + TB(t, RAPIER_FN(RigidBody_ValidateHandle)(p)); + fixed = RAPIER_FN(RigidBody_IsFixed)(p); + TB(t, RAPIER_FN(LastStatus)()); + if (fixed) { + result = (Color){170, 174, 175, 255}; + } + } + const float *rgba = tbFindColor(t, p, h); + if (rgba) { + result = (Color){(unsigned char)(rgba[0] * 255), (unsigned char)(rgba[1] * 255), + (unsigned char)(rgba[2] * 255), (unsigned char)(rgba[3] * 255)}; + } + if (sensor) { + result.a = (unsigned char)(result.a * 0.4f); + } + return result; +} + +TbGraphics *tbGraphicsNew(void) { + TbGraphics *g = calloc(1, sizeof(*g)); + if (!g) { + abort(); + } + const char *vs = + "#version 330\nin vec3 vertexPosition;in vec3 vertexNormal;in mat4 " + "instanceTransform;uniform mat4 mvp;out float light;void main(){vec3 " + "n=normalize(mat3(instanceTransform)*vertexNormal);light=0.5+0.5*abs(dot(n,normalize(vec3(" + "0.5,0.8,0.6))));gl_Position=mvp*instanceTransform*vec4(vertexPosition,1.0);}"; + const char *fs = "#version 330\nin float light;uniform vec4 colDiffuse;out vec4 " + "finalColor;void main(){finalColor=vec4(colDiffuse.rgb*light,colDiffuse.a);}"; + g->shader = LoadShaderFromMemory(vs, fs); + if (!IsShaderValid(g->shader)) { + fputs("instancing shader failed\n", stderr); + abort(); + } + g->shader.locs[SHADER_LOC_VERTEX_INSTANCETRANSFORM] = + GetShaderLocationAttrib(g->shader, "instanceTransform"); + const char *visualVs = + "#version 330\nin vec3 vertexPosition;in vec3 vertexNormal;in vec2 vertexTexCoord;" + "in mat4 instanceTransform;uniform mat4 mvp;out vec3 position;out vec3 normal;out vec2 uv;" + "void main(){position=vec3(instanceTransform*vec4(vertexPosition,1));" + "normal=mat3(instanceTransform)*vertexNormal;uv=vertexTexCoord;" + "gl_Position=mvp*vec4(position,1);}"; + const char *visualFs = + "#version 330\nin vec3 position;in vec3 normal;in vec2 uv;out vec4 finalColor;" + "uniform sampler2D texture0;uniform vec4 colDiffuse;uniform vec3 eye;" + "uniform float metallic;uniform float roughness;uniform float reflectance;uniform vec3 " + "emissive;" + "void main(){vec4 base=texture(texture0,uv)*colDiffuse;vec3 n=normalize(normal);" + "if(!gl_FrontFacing)n=-n;vec3 l=normalize(vec3(0.5,0.8,0.6));vec3 " + "v=normalize(eye-position);" + "vec3 h=normalize(l+v);float nl=max(dot(n,l),0.0),nv=max(dot(n,v),0.001);" + "float nh=max(dot(n,h),0.0),vh=max(dot(v,h),0.0);float a=max(roughness*roughness,0.002);" + "float a2=a*a,d=nh*nh*(a2-1.0)+1.0;float D=a2/(3.141593*d*d);" + "float k=(roughness+1.0)*(roughness+1.0)/8.0;" + "float G=nl/(nl*(1.0-k)+k)*nv/(nv*(1.0-k)+k);" + "vec3 F0=mix(vec3(0.16*reflectance*reflectance),base.rgb,metallic);" + "vec3 F=F0+(1.0-F0)*pow(1.0-vh,5.0);" + "vec3 diffuse=(1.0-F)*(1.0-metallic)*base.rgb/3.141593;" + "vec3 specular=D*G*F/max(4.0*nl*nv,0.001);" + "vec3 color=base.rgb*0.22+(diffuse+specular)*nl*2.5+emissive;" + "finalColor=vec4(color,base.a);}"; + g->visualShader = LoadShaderFromMemory(visualVs, visualFs); + if (!IsShaderValid(g->visualShader)) { + abort(); + } + g->visualShader.locs[SHADER_LOC_VERTEX_INSTANCETRANSFORM] = + GetShaderLocationAttrib(g->visualShader, "instanceTransform"); + g->material = LoadMaterialDefault(); + g->material.shader = g->shader; + return g; +} + +void tbGraphicsFree(TbGraphics *g) { + if (!g) { + return; + } + for (size_t i = 0; i < g->visualCount; ++i) { + UnloadMesh(g->visuals[i].mesh); + if (g->visuals[i].texture.id) { + UnloadTexture(g->visuals[i].texture); + } + MemFree(g->visuals[i].material.maps); + } + free(g->visuals); + UnloadShader(g->visualShader); + for (size_t i = 0; i < g->geometryCount; i++) { + Geometry *m = &g->geometries[i]; + if (m->nt) { + UnloadMesh(m->mesh); + } + free(m->triangles); + free(m->lines); + } + for (size_t i = 0; i < g->entryCapacity; i++) { + (void)RAPIER_FN(FreeSharedShape)(g->entries[i].shape); + } + for (size_t i = 0; i < g->batchCount; i++) { + free(g->batches[i].transforms); + } + free(g->geometries); + free(g->entries); + free(g->batches); + free(g->transparent); + free(g->handles); + free(g->softMeshes); + free(g->softHandles); + free(g->vertices); + free(g->indices); + free(g->debug); /* shader is owned separately from Material */ + g->material.shader = (Shader){0}; + UnloadMaterial(g->material); + UnloadShader(g->shader); + free(g); +} + +static uint64_t hashBytes(uint64_t h, const void *v, size_t n) { + const unsigned char *p = v; + while (n--) { + h = (h ^ *p++) * UINT64_C(1099511628211); + } + return h; +} + +static size_t geometry(TbGraphics *g, Testbed *t, RAPIER_TYPE(SharedShape) *shape) { + RAPIER_TYPE(ShapeMesh) *source = RAPIER_FN(SharedShape_Tessellate)(shape, 16); + TB(t, RAPIER_FN(LastStatus)()); + size_t nt = 0, nl = 0; + nt = RAPIER_FN(ShapeMesh_Triangles)(source, NULL, 0); + TB(t, RAPIER_FN(LastStatus)()); + nl = RAPIER_FN(ShapeMesh_Lines)(source, NULL, 0); + TB(t, RAPIER_FN(LastStatus)()); + RAPIER_TYPE(Vector) *tri = calloc(nt ? nt : 1, sizeof(*tri)), + *lines = calloc(nl ? nl : 1, sizeof(*lines)); + if (!tri || !lines) { + abort(); + } + nt = RAPIER_FN(ShapeMesh_Triangles)(source, tri, nt); + TB(t, RAPIER_FN(LastStatus)()); + nl = RAPIER_FN(ShapeMesh_Lines)(source, lines, nl); + TB(t, RAPIER_FN(LastStatus)()); + TB(t, RAPIER_FN(FreeShapeMesh)(source)); + uint64_t hash = hashBytes(hashBytes(UINT64_C(14695981039346656037), tri, nt * sizeof(*tri)), + lines, nl * sizeof(*lines)); + for (size_t i = 0; i < g->geometryCount; i++) { + Geometry *m = &g->geometries[i]; + if (m->allocated && m->hash == hash && m->nt == nt && m->nl == nl && + !memcmp(m->triangles, tri, nt * sizeof(*tri)) && + !memcmp(m->lines, lines, nl * sizeof(*lines))) { + free(tri); + free(lines); + return i; + } + } + g->geometries = + grow(g->geometries, &g->geometryCapacity, g->geometryCount + 1, sizeof(*g->geometries)); + size_t id = 0; + while (id < g->geometryCount && g->geometries[id].allocated) { + id++; + } + if (id == g->geometryCount) { + g->geometryCount++; + } + Geometry *m = &g->geometries[id]; + m->allocated = true; + m->triangles = tri; + m->lines = lines; + m->nt = nt; + m->nl = nl; + m->hash = hash; + if (nt) { + if (nt > INT_MAX / 3) { + abort(); + } + m->mesh.vertexCount = (int)nt; + m->mesh.triangleCount = (int)(nt / 3); + m->mesh.vertices = MemAlloc((unsigned int)(nt * 3 * sizeof(float))); + m->mesh.normals = MemAlloc((unsigned int)(nt * 3 * sizeof(float))); + if (!m->mesh.vertices || !m->mesh.normals) { + abort(); + } + for (size_t i = 0; i < nt; i += 3) { + Vector3 a = vec(tri[i]), b = vec(tri[i + 1]), c = vec(tri[i + 2]); + Vector3 n = + Vector3Normalize(Vector3CrossProduct(Vector3Subtract(b, a), Vector3Subtract(c, a))); + for (size_t j = 0; j < 3; j++) { + Vector3 v = vec(tri[i + j]); + memcpy(&m->mesh.vertices[(i + j) * 3], &v, sizeof(v)); + memcpy(&m->mesh.normals[(i + j) * 3], &n, sizeof(n)); + } + } + UploadMesh(&m->mesh, false); + } + return id; +} + +static void instance(TbGraphics *g, size_t geometryId, Color c, Matrix m) { + Batch *b = NULL; + for (size_t i = 0; i < g->batchCount; i++) { + if (g->batches[i].geometry == geometryId && !memcmp(&g->batches[i].color, &c, sizeof(c))) { + b = &g->batches[i]; + break; + } + } + if (!b) { + g->batches = grow(g->batches, &g->batchCapacity, g->batchCount + 1, sizeof(*g->batches)); + b = &g->batches[g->batchCount++]; + memset(b, 0, sizeof(*b)); + b->geometry = geometryId; + b->color = c; + } + b->transforms = grow(b->transforms, &b->capacity, b->count + 1, sizeof(*b->transforms)); + b->transforms[b->count++] = m; +} + +static Entry *entry(TbGraphics *g, RAPIER_TYPE(ColliderHandle) h) { + /* A render-only mesh has no collider and must never index this cache. */ + if (h.index == UINT32_MAX) { + abort(); + } + g->entries = grow(g->entries, &g->entryCapacity, (size_t)h.index + 1, sizeof(*g->entries)); + return &g->entries[h.index]; +} + +/* Deforming triangles share the same sorted transparency pass as rigid sensors. */ +static void softTriangle(TbGraphics *g, Vector3 a, Vector3 b, Vector3 c, Color color, + Camera3D camera) { + if (color.a == 255) { + DrawTriangle3D(a, b, c, color); + return; + } + Vector3 center = Vector3Scale(Vector3Add(Vector3Add(a, b), c), 1.0f / 3); + Vector3 forward = Vector3Normalize(Vector3Subtract(camera.target, camera.position)); + g->transparent = grow(g->transparent, &g->transparentCapacity, g->transparentCount + 1, + sizeof(*g->transparent)); + g->transparent[g->transparentCount++] = (TransparentDraw){ + .depth = Vector3DotProduct(Vector3Subtract(center, camera.position), forward), + .triangle = true, + .vertices = {a, b, c}, + .color = color}; +} + +/* Screen-facing ribbons keep deformable polylines readable on every GL backend, + * including those that only support one-pixel native lines. */ +static void drawSoftSegment(TbGraphics *g, Vector3 a, Vector3 b, Color color, Camera3D camera) { + Vector3 middle = Vector3Scale(Vector3Add(a, b), .5f); + Vector3 view = Vector3Normalize(Vector3Subtract(camera.target, camera.position)); + Vector3 side = Vector3Normalize(Vector3CrossProduct(Vector3Subtract(b, a), view)); + float height = fmaxf(1, (float)GetScreenHeight()); + float worldPerPixel = camera.fovy / height; + if (camera.projection == CAMERA_PERSPECTIVE) { + float depth = Vector3DotProduct(Vector3Subtract(middle, camera.position), view); + worldPerPixel = 2 * fmaxf(.001f, depth) * tanf(camera.fovy * DEG2RAD * .5f) / height; + } + side = Vector3Scale(side, worldPerPixel * 1.5f); /* Three pixels wide. */ + Vector3 a0 = Vector3Subtract(a, side), a1 = Vector3Add(a, side); + Vector3 b0 = Vector3Subtract(b, side), b1 = Vector3Add(b, side); + softTriangle(g, a0, b0, b1, color, camera); + softTriangle(g, a0, b1, a1, color, camera); +} + +static void drawSoft(TbGraphics *g, Testbed *t, bool surfaces, Camera3D camera) { + size_t n = RAPIER_FN(SoftBodyHandles)(t->world, NULL, 0); + TB(t, RAPIER_FN(LastStatus)()); + g->softHandles = grow(g->softHandles, &g->softHandlesCapacity, n, sizeof(*g->softHandles)); + n = RAPIER_FN(SoftBodyHandles)(t->world, g->softHandles, g->softHandlesCapacity); + TB(t, RAPIER_FN(LastStatus)()); + for (size_t i = 0; i < n; i++) { + TB(t, RAPIER_FN(SoftBody_ValidateHandle)(g->softHandles[i])); + size_t nm = RAPIER_FN(SoftBody_Meshes)(g->softHandles[i], NULL, 0); + TB(t, RAPIER_FN(LastStatus)()); + g->softMeshes = grow(g->softMeshes, &g->softMeshCapacity, nm, sizeof(*g->softMeshes)); + nm = RAPIER_FN(SoftBody_Meshes)(g->softHandles[i], g->softMeshes, + g->softMeshCapacity); + TB(t, RAPIER_FN(LastStatus)()); + bool drawnSkin = false; + for (size_t j = 0; j < nm; ++j) { + drawnSkin |= g->softMeshes[j].is_skinned && !g->softMeshes[j].collision_enabled; + } + for (size_t j = 0; j < nm; j++) { + const RAPIER_TYPE(SoftMeshInfo) *mesh = &g->softMeshes[j]; + RAPIER_TYPE(ColliderHandle) h = mesh->collider; + if (h.index != UINT32_MAX) { + entry(g, h)->soft = true; + entry(g, h)->seen = true; + } + /* Match the Rust viewer: drawn skins replace collision boundaries; + * non-colliding computational boundaries are not render objects. */ + if (!surfaces || (!mesh->collision_enabled && !mesh->is_skinned) || + (drawnSkin && mesh->collision_enabled)) { + continue; + } + size_t nv = 0, ni = 0, arity = mesh->arity; + nv = + RAPIER_FN(SoftBody_MeshVerticesById)(g->softHandles[i], mesh->id, NULL, 0); + TB(t, RAPIER_FN(LastStatus)()); + ni = RAPIER_FN(SoftBody_MeshIndicesById)(g->softHandles[i], mesh->id, NULL, 0); + TB(t, RAPIER_FN(LastStatus)()); + g->vertices = grow(g->vertices, &g->vertexCapacity, nv, sizeof(*g->vertices)); + g->indices = grow(g->indices, &g->indexCapacity, ni, sizeof(*g->indices)); + nv = RAPIER_FN(SoftBody_MeshVerticesById)(g->softHandles[i], mesh->id, + g->vertices, g->vertexCapacity); + TB(t, RAPIER_FN(LastStatus)()); + ni = RAPIER_FN(SoftBody_MeshIndicesById)(g->softHandles[i], mesh->id, + g->indices, g->indexCapacity); + TB(t, RAPIER_FN(LastStatus)()); + Color c = palette[g->softHandles[i].index % TB_COUNT(palette)]; + RAPIER_TYPE(RigidBodyHandle) + root = RAPIER_FN(SoftBody_RootBody)(g->softHandles[i]); + TB(t, RAPIER_FN(LastStatus)()); + const float *tint = tbFindColor(t, root, h); + if (tint) { + c = (Color){(unsigned char)(tint[0] * 255), (unsigned char)(tint[1] * 255), + (unsigned char)(tint[2] * 255), (unsigned char)(tint[3] * 255)}; + } + if (h.index != UINT32_MAX) { + RAPIER_TYPE(Bool) sensor; + TB(t, RAPIER_FN(Collider_ValidateHandle)(h)); + sensor = RAPIER_FN(Collider_IsSensor)(h); + TB(t, RAPIER_FN(LastStatus)()); + if (sensor) { + c.a = (unsigned char)(c.a * .4f); + } + } + if (arity == 3) { + for (size_t k = 0; k + 2 < ni; k += 3) { + Vector3 a = vec(g->vertices[g->indices[k]]), + b = vec(g->vertices[g->indices[k + 1]]), + v = vec(g->vertices[g->indices[k + 2]]); + Vector3 norm = Vector3Normalize( + Vector3CrossProduct(Vector3Subtract(b, a), Vector3Subtract(v, a))); + float light = + 0.5f + 0.5f * fabsf(Vector3DotProduct( + norm, Vector3Normalize((Vector3){0.5f, 0.8f, 0.6f}))); + softTriangle(g, a, b, v, + (Color){(unsigned char)(c.r * light), (unsigned char)(c.g * light), + (unsigned char)(c.b * light), c.a}, + camera); + } + } else { + for (size_t k = 0; k + 1 < ni; k += 2) { + drawSoftSegment(g, vec(g->vertices[g->indices[k]]), + vec(g->vertices[g->indices[k + 1]]), c, camera); + } + } + } + } +} + +static void drawVisualMeshes(TbGraphics *g, Testbed *t, Camera3D camera) { + if (!g->visuals && t->renderMeshCount) { + g->visuals = calloc(t->renderMeshCount, sizeof(*g->visuals)); + if (!g->visuals) { + abort(); + } + g->visualCount = t->renderMeshCount; + for (size_t i = 0; i < g->visualCount; ++i) { + const TbRenderMesh *source = &t->renderMeshes[i]; + VisualMesh *visual = &g->visuals[i]; + Mesh *mesh = &visual->mesh; + if (source->indexCount > INT_MAX || + source->indexCount > UINT_MAX / (3 * sizeof(float))) { + abort(); + } + mesh->vertexCount = (int)source->indexCount; + mesh->triangleCount = mesh->vertexCount / 3; + mesh->vertices = MemAlloc((unsigned)(source->indexCount * 3 * sizeof(float))); + mesh->normals = MemAlloc((unsigned)(source->indexCount * 3 * sizeof(float))); + mesh->texcoords = MemAlloc((unsigned)(source->indexCount * 2 * sizeof(float))); + if (!mesh->vertices || !mesh->normals || !mesh->texcoords) { + abort(); + } + for (size_t j = 0; j < source->indexCount; j += 3) { + Vector3 a = vec(source->vertices[source->indices[j]]); + Vector3 b = vec(source->vertices[source->indices[j + 1]]); + Vector3 c = vec(source->vertices[source->indices[j + 2]]); + Vector3 normal = Vector3Normalize( + Vector3CrossProduct(Vector3Subtract(b, a), Vector3Subtract(c, a))); + for (size_t k = j; k < j + 3; ++k) { + uint32_t id = source->indices[k]; + Vector3 vertex = vec(source->vertices[id]); + memcpy(mesh->vertices + k * 3, &vertex, sizeof(vertex)); + memcpy(mesh->normals + k * 3, + source->normals ? source->normals + id * 3 : (float *)&normal, + 3 * sizeof(float)); + mesh->texcoords[k * 2] = source->uvs ? source->uvs[id * 2] : 0; + mesh->texcoords[k * 2 + 1] = source->uvs ? source->uvs[id * 2 + 1] : 0; + } + } + UploadMesh(mesh, false); + visual->material = LoadMaterialDefault(); + visual->material.shader = g->visualShader; + const float *color = source->rgba; + visual->material.maps[MATERIAL_MAP_DIFFUSE].color = + (Color){(unsigned char)(color[0] * 255), (unsigned char)(color[1] * 255), + (unsigned char)(color[2] * 255), (unsigned char)(color[3] * 255)}; + if (source->texture && source->texture[0]) { + visual->texture = LoadTexture(source->texture); + if (visual->texture.id) { + visual->material.maps[MATERIAL_MAP_DIFFUSE].texture = visual->texture; + } + } + } + } + SetShaderValue(g->visualShader, GetShaderLocation(g->visualShader, "eye"), &camera.position, + SHADER_UNIFORM_VEC3); + for (size_t i = 0; i < g->visualCount; ++i) { + const TbRenderMesh *source = &t->renderMeshes[i]; + + RAPIER_TYPE(Pose) pose; + TB(t, RAPIER_FN(RigidBody_ValidateHandle)(source->body)); + pose = RAPIER_FN(RigidBody_Position)(source->body); + TB(t, RAPIER_FN(LastStatus)()); + Matrix matrix = MatrixMultiply(transform(source->localPose), transform(pose)); + SetShaderValue(g->visualShader, GetShaderLocation(g->visualShader, "metallic"), + &source->metallic, SHADER_UNIFORM_FLOAT); + SetShaderValue(g->visualShader, GetShaderLocation(g->visualShader, "roughness"), + &source->roughness, SHADER_UNIFORM_FLOAT); + SetShaderValue(g->visualShader, GetShaderLocation(g->visualShader, "reflectance"), + &source->reflectance, SHADER_UNIFORM_FLOAT); + SetShaderValue(g->visualShader, GetShaderLocation(g->visualShader, "emissive"), + source->emissive, SHADER_UNIFORM_VEC3); + DrawMeshInstanced(g->visuals[i].mesh, g->visuals[i].material, &matrix, 1); + } +} + +static int backToFront(const void *left, const void *right) { + float a = ((const TransparentDraw *)left)->depth; + float b = ((const TransparentDraw *)right)->depth; + return (a < b) - (a > b); +} + +static void drawInstances(TbGraphics *g, Batch *batch, const Matrix *transforms, size_t count) { + Geometry *geometry = &g->geometries[batch->geometry]; + g->material.maps[MATERIAL_MAP_DIFFUSE].color = batch->color; + if (geometry->nt) { + DrawMeshInstanced(geometry->mesh, g->material, transforms, (int)count); + } + for (size_t j = 0; j < count; ++j) { + for (size_t k = 0; k + 1 < geometry->nl; k += 2) { + DrawLine3D(Vector3Transform(vec(geometry->lines[k]), transforms[j]), + Vector3Transform(vec(geometry->lines[k + 1]), transforms[j]), batch->color); + } + } +} + +static void drawTransparent(TbGraphics *g) { + if (!g->transparentCount) { + return; + } + qsort(g->transparent, g->transparentCount, sizeof(*g->transparent), backToFront); + rlDrawRenderBatchActive(); + rlDisableDepthMask(); + for (size_t i = 0; i < g->transparentCount; ++i) { + TransparentDraw *draw = &g->transparent[i]; + if (draw->triangle) { + DrawTriangle3D(draw->vertices[0], draw->vertices[1], draw->vertices[2], draw->color); + } else { + /* Instanced meshes draw immediately; flush queued triangles first to + * preserve the sorted order across both rendering paths. */ + rlDrawRenderBatchActive(); + Batch *batch = &g->batches[draw->batch]; + drawInstances(g, batch, &batch->transforms[draw->instance], 1); + } + } + rlDrawRenderBatchActive(); + rlEnableDepthMask(); +} + +int tbGraphicsDraw(TbGraphics *g, Testbed *t, Camera3D camera, uint32_t debug, bool surfaces) { + if (!t->world || t->error[0]) { + return 0; + } + if (setjmp(t->failure)) { + EndMode3D(); + rlEnableBackfaceCulling(); + return 0; + } + BeginMode3D(camera); + rlDisableBackfaceCulling(); + for (size_t i = 0; i < g->entryCapacity; i++) { + g->entries[i].soft = false; + g->entries[i].seen = false; + } + for (size_t i = 0; i < g->batchCount; i++) { + g->batches[i].count = 0; + } + g->transparentCount = 0; + drawSoft(g, t, surfaces && t->collidersVisible, camera); + if (surfaces) { + drawVisualMeshes(g, t, camera); + } + size_t n = RAPIER_FN(ColliderHandles)(t->world, NULL, 0); + TB(t, RAPIER_FN(LastStatus)()); + g->handles = grow(g->handles, &g->handlesCapacity, n, sizeof(*g->handles)); + n = RAPIER_FN(ColliderHandles)(t->world, g->handles, g->handlesCapacity); + TB(t, RAPIER_FN(LastStatus)()); + for (size_t i = 0; i < n; i++) { + RAPIER_TYPE(ColliderHandle) h = g->handles[i]; + Entry *e = entry(g, h); + e->seen = true; + if (!surfaces || !t->collidersVisible || e->soft) { + continue; + } + TB(t, RAPIER_FN(Collider_ValidateHandle)(h)); + RAPIER_TYPE(Bool) enabled = RAPIER_FN(Collider_IsEnabled)(h); + TB(t, RAPIER_FN(LastStatus)()); + if (!enabled) { + continue; + } + uintptr_t identity = 0; + identity = RAPIER_FN(Collider_ShapeIdentity)(h); + TB(t, RAPIER_FN(LastStatus)()); + if (!e->valid || e->generation != h.generation || e->identity != identity) { + (void)RAPIER_FN(FreeSharedShape)(e->shape); + e->shape = NULL; + e->shape = RAPIER_FN(Collider_CloneShape)(h); + TB(t, RAPIER_FN(LastStatus)()); + e->geometry = geometry(g, t, e->shape); + e->valid = true; + e->generation = h.generation; + e->identity = identity; + } + RAPIER_TYPE(Pose) p = RAPIER_FN(Collider_Position)(h); + TB(t, RAPIER_FN(LastStatus)()); + instance(g, e->geometry, color(t, h), transform(p)); + } + /* Opaque objects retain instancing. Sort translucent instances by depth so + * sensors reveal objects behind them and do not occlude later sensors. */ + Vector3 forward = Vector3Normalize(Vector3Subtract(camera.target, camera.position)); + for (size_t i = 0; i < g->batchCount; ++i) { + Batch *batch = &g->batches[i]; + if (!batch->count) { + continue; + } + if (batch->color.a == 255) { + drawInstances(g, batch, batch->transforms, batch->count); + } else { + g->transparent = grow(g->transparent, &g->transparentCapacity, + g->transparentCount + batch->count, sizeof(*g->transparent)); + for (size_t j = 0; j < batch->count; ++j) { + Matrix transform = batch->transforms[j]; + Vector3 center = {transform.m12, transform.m13, transform.m14}; + g->transparent[g->transparentCount++] = (TransparentDraw){ + .batch = i, + .instance = j, + .depth = Vector3DotProduct(Vector3Subtract(center, camera.position), forward)}; + } + } + } + drawTransparent(g); + if (debug) { + size_t count = RAPIER_FN(DebugRender)(t->world, debug, NULL, 0); + TB(t, RAPIER_FN(LastStatus)()); + g->debug = grow(g->debug, &g->debugCapacity, count, sizeof(*g->debug)); + count = RAPIER_FN(DebugRender)(t->world, debug, g->debug, g->debugCapacity); + TB(t, RAPIER_FN(LastStatus)()); + for (size_t i = 0; i < count; i++) { + RAPIER_TYPE(DebugLine) *line = &g->debug[i]; /* Rapier colors are HSLA, not RGBA. */ + float h = line->color[0], s = line->color[1], l = line->color[2]; + float v = l + s * fminf(l, 1 - l); + Color c = ColorFromHSV(h, v > 0 ? 2 * (1 - l / v) : 0, v); + c.a = (unsigned char)(255 * line->color[3]); + DrawLine3D(vec(line->a), vec(line->b), c); + } + } + for (size_t i = 0; i < t->lineCount; ++i) { + const float *rgba = t->lines[i].rgba; + Color color = {(unsigned char)(255 * rgba[0]), (unsigned char)(255 * rgba[1]), + (unsigned char)(255 * rgba[2]), (unsigned char)(255 * rgba[3])}; + DrawLine3D(vec(t->lines[i].a), vec(t->lines[i].b), color); + } + t->lineCount = 0; + /* Reclaim removed/replaced geometry so fountains and shape edits have bounded caches. */ + for (size_t i = 0; i < g->geometryCount; i++) { + g->geometries[i].live = false; + } + for (size_t i = 0; i < g->entryCapacity; i++) { + Entry *e = &g->entries[i]; + if (!e->seen || e->soft) { + (void)RAPIER_FN(FreeSharedShape)(e->shape); + e->shape = NULL; + e->valid = false; + } else if (e->valid) { + g->geometries[e->geometry].live = true; + } + } + for (size_t i = 0; i < g->geometryCount; i++) { + Geometry *m = &g->geometries[i]; + if (m->allocated && !m->live) { + if (m->nt) { + UnloadMesh(m->mesh); + } + free(m->triangles); + free(m->lines); + memset(m, 0, sizeof(*m)); + } + } + size_t kept = 0; + for (size_t i = 0; i < g->batchCount; i++) { + if (g->batches[i].count) { + g->batches[kept++] = g->batches[i]; + } else { + free(g->batches[i].transforms); + } + } + g->batchCount = kept; + /* EndMode3D flushes queued soft triangles. Keep both faces visible until + * that batch has been drawn, then restore raylib's default for the UI. */ + EndMode3D(); + rlEnableBackfaceCulling(); + return 1; +} + +void tbGraphicsFrameAll(Testbed *t, Camera3D *camera) { + if (!t->world) { + return; + } + if (setjmp(t->failure)) { + return; + } + size_t n = RAPIER_FN(ColliderHandles)(t->world, NULL, 0); + TB(t, RAPIER_FN(LastStatus)()); + RAPIER_TYPE(ColliderHandle) *h = malloc((n ? n : 1) * sizeof(*h)); + if (!h) { + abort(); + } + n = RAPIER_FN(ColliderHandles)(t->world, h, n); + TB(t, RAPIER_FN(LastStatus)()); + Vector3 lo = {1e20f, 1e20f, 1e20f}, hi = {-1e20f, -1e20f, -1e20f}; + bool found = false; + for (size_t i = 0; i < t->renderMeshCount; ++i) { + const TbRenderMesh *mesh = &t->renderMeshes[i]; + + RAPIER_TYPE(Pose) pose; + TB(t, RAPIER_FN(RigidBody_ValidateHandle)(mesh->body)); + pose = RAPIER_FN(RigidBody_Position)(mesh->body); + TB(t, RAPIER_FN(LastStatus)()); + Matrix matrix = MatrixMultiply(transform(mesh->localPose), transform(pose)); + for (size_t j = 0; j < mesh->vertexCount; ++j) { + Vector3 point = Vector3Transform(vec(mesh->vertices[j]), matrix); + lo = Vector3Min(lo, point); + hi = Vector3Max(hi, point); + found = true; + } + } + for (size_t i = 0; t->collidersVisible && i < n; i++) { + RAPIER_TYPE(Aabb) a; + TB(t, RAPIER_FN(Collider_ValidateHandle)(h[i])); + RAPIER_TYPE(RigidBodyHandle) p = RAPIER_FN(Collider_Parent)(h[i]); + TB(t, RAPIER_FN(LastStatus)()); + if (p.index != UINT32_MAX) { + RAPIER_TYPE(Bool) fixed; + TB(t, RAPIER_FN(RigidBody_ValidateHandle)(p)); + fixed = RAPIER_FN(RigidBody_IsFixed)(p); + TB(t, RAPIER_FN(LastStatus)()); + if (fixed) { + continue; + } + } + a = RAPIER_FN(Collider_ComputeAabb)(h[i]); + TB(t, RAPIER_FN(LastStatus)()); + Vector3 l = vec(a.mins), u = vec(a.maxs); + if (Vector3Distance(l, u) > 1e8f) { + continue; + } + lo = Vector3Min(lo, l); + hi = Vector3Max(hi, u); + found = true; + } + free(h); + if (!found) { + return; + } + camera->target = Vector3Scale(Vector3Add(lo, hi), 0.5f); + float size = fmaxf(Vector3Distance(lo, hi), 1.0f); + Vector3 dir = Vector3Normalize(Vector3Subtract(camera->position, camera->target)); +#if defined(RAPIER_DIM2) + dir = (Vector3){0, 0, 1}; + float inverseAspect = (float)GetScreenHeight() / (float)GetScreenWidth(); + camera->fovy = fmaxf((hi.y - lo.y) * 1.2f, (hi.x - lo.x) * 1.2f * inverseAspect); +#endif + camera->position = Vector3Add(camera->target, Vector3Scale(dir, size * 1.2f)); +} diff --git a/c/testbed/graphics.h b/c/testbed/graphics.h new file mode 100644 index 000000000..9136b567b --- /dev/null +++ b/c/testbed/graphics.h @@ -0,0 +1,10 @@ +#ifndef TB_GRAPHICS_H +#define TB_GRAPHICS_H +#include "testbed.h" +#include "raylib.h" +typedef struct TbGraphics TbGraphics; +TbGraphics *tbGraphicsNew(void); +void tbGraphicsFree(TbGraphics *); +int tbGraphicsDraw(TbGraphics *, Testbed *, Camera3D, uint32_t, bool); +void tbGraphicsFrameAll(Testbed *, Camera3D *); +#endif diff --git a/c/testbed/gui.c b/c/testbed/gui.c new file mode 100644 index 000000000..bd6a87183 --- /dev/null +++ b/c/testbed/gui.c @@ -0,0 +1,928 @@ +#include "testbed_internal.h" +#include "font_data.h" +#include "graphics.h" +#include "grab.h" +#include "raymath.h" +#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS +#include "cimgui.h" +#include "rlImGui.h" +#include +#include +#include +#include + +#define UI2(x, y) ((ImVec2_c){(float)(x), (float)(y)}) +#define UI4(r, g, b, a) ((ImVec4_c){(r), (g), (b), (a)}) +#define UI_HISTORY 180 + +typedef struct UiState { + bool running, surfaces, debug[6]; + bool stepOnce, restart, frameAll, save, restore; + int selected, next, frame, historyCount, historyOffset; + char search[96]; + int threadInput; + char threadError[256]; + const char *profile, *initialTab; + double physicsMs, renderMs; + int physicsSteps; + float physicsHistory[UI_HISTORY], renderHistory[UI_HISTORY]; + RAPIER_TYPE(Bytes) *snapshot; + uint64_t snapshotStep; + double snapshotTime; +} UiState; + +static const uint32_t debugModes[] = {RAPIER_CONST(DEBUG_COLLIDER_SHAPES), + RAPIER_CONST(DEBUG_RIGID_BODY_AXES), + RAPIER_CONST(DEBUG_CONTACTS), + RAPIER_CONST(DEBUG_IMPULSE_JOINTS) | + RAPIER_CONST(DEBUG_MULTIBODY_JOINTS), + RAPIER_CONST(DEBUG_SOFT_BODIES), + RAPIER_CONST(DEBUG_SOFT_BODY_STRESS)}; +static const char *debugNames[] = {"Wireframes", "Body axes", "Contacts", + "Joints", "Soft constraints", "Soft stress"}; + +static bool configureUi(void) { + ImGuiIO *io = igGetIO_Nil(); + io->IniFilename = NULL; + io->LogFilename = NULL; + io->ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + ImFontConfig *config = ImFontConfig_ImFontConfig(); + // The embedded bytes remain alive for the entire ImGui context lifetime. + config->FontDataOwnedByAtlas = false; + io->FontDefault = ImFontAtlas_AddFontFromMemoryTTF( + io->Fonts, (void *)tbFontData, (int)sizeof(tbFontData), 16.0f, config, NULL); + ImFontConfig_destroy(config); + if (!io->FontDefault) { + return false; + } + // ImGui 1.92 rasterizes at the framebuffer density reported by rlImGui. + ImGuiStyle *style = igGetStyle(); + style->FontSizeBase = 16; + style->WindowPadding = UI2(16, 14); + style->FramePadding = UI2(8, 5); + style->ItemSpacing = UI2(8, 7); + style->WindowRounding = 6; + style->FrameRounding = 4; + style->GrabRounding = 4; + style->ChildRounding = 4; + style->ScrollbarRounding = 6; + style->Colors[ImGuiCol_Text] = UI4(0.235f, 0.227f, 0.204f, 1); + style->Colors[ImGuiCol_TextDisabled] = UI4(0.52f, 0.52f, 0.49f, 1); + style->Colors[ImGuiCol_WindowBg] = UI4(0.988f, 0.988f, 0.973f, 1); + style->Colors[ImGuiCol_Border] = UI4(0.784f, 0.776f, 0.745f, 0.6f); + style->Colors[ImGuiCol_FrameBg] = UI4(0.922f, 0.922f, 0.882f, 1); + style->Colors[ImGuiCol_FrameBgHovered] = UI4(0.882f, 0.882f, 0.843f, 1); + style->Colors[ImGuiCol_FrameBgActive] = UI4(0.843f, 0.843f, 0.804f, 1); + style->Colors[ImGuiCol_Button] = UI4(0.922f, 0.922f, 0.882f, 1); + style->Colors[ImGuiCol_ButtonHovered] = UI4(0.78f, 0.85f, 0.86f, 1); + style->Colors[ImGuiCol_ButtonActive] = UI4(0.62f, 0.76f, 0.80f, 1); + style->Colors[ImGuiCol_Header] = UI4(0.88f, 0.91f, 0.88f, 1); + style->Colors[ImGuiCol_HeaderHovered] = UI4(0.78f, 0.85f, 0.86f, 1); + style->Colors[ImGuiCol_HeaderActive] = UI4(0.62f, 0.76f, 0.80f, 1); + style->Colors[ImGuiCol_Tab] = UI4(0.92f, 0.92f, 0.88f, 1); + style->Colors[ImGuiCol_TabHovered] = UI4(0.78f, 0.85f, 0.86f, 1); + style->Colors[ImGuiCol_TabSelected] = UI4(0.78f, 0.85f, 0.86f, 1); + style->Colors[ImGuiCol_CheckMark] = UI4(0.322f, 0.510f, 0.588f, 1); + style->Colors[ImGuiCol_SliderGrab] = UI4(0.322f, 0.510f, 0.588f, 1); + style->Colors[ImGuiCol_PlotLines] = UI4(0.322f, 0.510f, 0.588f, 1); + return true; +} + +static bool setupUi(void) { + rlImGuiSetup(false); + return configureUi(); +} + +static void keyboardShortcuts(UiState *ui) { + ImGuiIO *io = igGetIO_Nil(); + if (io->WantCaptureKeyboard || io->WantTextInput) { + return; + } + if (igIsKeyPressed_Bool(ImGuiKey_T, false)) { + ui->running = !ui->running; + } + if (igIsKeyPressed_Bool(ImGuiKey_S, false)) { + ui->stepOnce = true; + } + if (igIsKeyPressed_Bool(ImGuiKey_R, false)) { + ui->restart = true; + } + if (igIsKeyPressed_Bool(ImGuiKey_F, false)) { + ui->frameAll = true; + } +} + +static void tooltip(const char *text) { + if (igIsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && igBeginTooltip()) { + igPushTextWrapPos(igGetFontSize() * 26); + igTextUnformatted(text, NULL); + igPopTextWrapPos(); + igEndTooltip(); + } +} + +// UI setters report ordinary errors without unwinding an open ImGui window. +static void uiStatus(Testbed *t, RAPIER_TYPE(Status) status) { + if (status != RAPIER_CONST(OK)) { + snprintf(t->error, sizeof(t->error), "%s", RAPIER_FN(LastError)()); + } +} + +static Camera3D cameraFor(Testbed *t) { + Camera3D c = {0}; + c.position = (Vector3){t->eye[0], t->eye[1], t->eye[2]}; + c.target = (Vector3){t->target[0], t->target[1], t->target[2]}; + c.up = (Vector3){t->up[0], t->up[1], t->up[2]}; + c.fovy = 45; + c.projection = CAMERA_PERSPECTIVE; +#if defined(RAPIER_DIM2) + c.position = (Vector3){t->target[0], t->target[1], 100}; + c.target.z = 0; + c.fovy = t->viewWidth * (float)GetScreenHeight() / (float)GetScreenWidth(); + c.projection = CAMERA_ORTHOGRAPHIC; +#endif + return c; +} + +static bool matches(const char *text, const char *query) { + for (; *text; text++) { + size_t i = 0; + while (query[i] && text[i] && + tolower((unsigned char)text[i]) == tolower((unsigned char)query[i])) { + i++; + } + if (!query[i]) { + return true; + } + } + return !*query; +} + +static int abi(void) { + RAPIER_TYPE(Status) + s = RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), RAPIER_CONST(DIMENSION), + sizeof(RAPIER_TYPE(Real)), sizeof(RAPIER_TYPE(Vector)), + sizeof(RAPIER_TYPE(Pose))); + if (s) { + fprintf(stderr, "ABI mismatch: %s\n", RAPIER_FN(LastError)()); + } + return !s; +} + +static void worldSnapshot(Testbed *t, RAPIER_TYPE(Bytes) **snapshot, int save, uint64_t *step, + double *time) { + if (setjmp(t->failure)) { + return; + } + if (save) { + (void)RAPIER_FN(FreeBytes)(*snapshot); + *snapshot = NULL; + *snapshot = RAPIER_FN(SerializeWorld)(t->world); + TB(t, RAPIER_FN(LastStatus)()); + *step = t->step; + *time = t->time; + } else if (*snapshot) { + const uint8_t *data; + size_t n; + RAPIER_TYPE(World) *w = NULL; + RAPIER_TYPE(ByteView) bytesDataResult = RAPIER_FN(Bytes_Data)(*snapshot); + data = bytesDataResult.data; + n = bytesDataResult.count; + TB(t, RAPIER_FN(LastStatus)()); + w = RAPIER_FN(DeserializeWorld)(data, n); + TB(t, RAPIER_FN(LastStatus)()); + TB(t, RAPIER_FN(FreeWorld)(t->world)); + t->world = w; + t->step = *step; + t->time = *time; + tbRefreshWorld(t); + } +} + +static bool exampleMatches(const TbExample *e, const UiState *ui) { + return !e-> + requires && (matches(e->name, ui->search) || matches(e->group, ui->search) || + matches(e->id, ui->search)); +} + +/* Navigation follows the filtered list, wraps, and skips unavailable builds. */ +static int adjacentExample(const UiState *ui, int direction) { + int count = (int)tbExampleCount; + for (int offset = 1; offset < count; ++offset) { + int index = (ui->selected + direction * offset + count) % count; + if (exampleMatches(&tbExamples[index], ui)) { + return index; + } + } + return ui->selected; +} + +static void examplesUi(UiState *ui) { + igSetNextItemWidth(-FLT_MIN); + igInputTextWithHint("##search", "Search examples...", ui->search, sizeof(ui->search), 0, NULL, + NULL); + for (size_t start = 0; start < tbExampleCount;) { + size_t end = start + 1, count = 0; + while (end < tbExampleCount && !strcmp(tbExamples[start].group, tbExamples[end].group)) { + end++; + } + for (size_t i = start; i < end; i++) { + count += exampleMatches(&tbExamples[i], ui); + } + if (count) { + igPushID_Str(tbExamples[start].group); + if (ui->search[0]) { + igSetNextItemOpen(true, ImGuiCond_Always); + } else { + igSetNextItemOpen((size_t)ui->selected >= start && (size_t)ui->selected < end, + ImGuiCond_Once); + } + char label[128]; + snprintf(label, sizeof(label), "%s (%zu)", tbExamples[start].group, count); + if (igCollapsingHeader_TreeNodeFlags(label, 0)) { + for (size_t i = start; i < end; i++) { + const TbExample *e = &tbExamples[i]; + if (!exampleMatches(e, ui)) { + continue; + } + igPushID_Str(e->id); + igBeginDisabled(e->requires != NULL); + if (igSelectable_Bool(e->name, i == (size_t)ui->selected, 0, UI2(0, 0))) { + ui->next = (int)i; + } + igEndDisabled(); + tooltip(e->requires ? e->requires : e->source); + igPopID(); + } + } + igPopID(); + } + start = end; + } +} + +typedef size_t (*ParameterGet)(const RAPIER_TYPE(World) *); +typedef RAPIER_TYPE(Status) (*ParameterSet)(RAPIER_TYPE(World) *, size_t); + +static void iterationSlider(Testbed *t, const char *name, ParameterGet get, ParameterSet set, + int min, int max) { + size_t value = get(t->world); + if (RAPIER_FN(LastStatus)() != RAPIER_CONST(OK)) { + return; + } + int edit = value > (size_t)INT32_MAX ? INT32_MAX : (int)value; + igTextUnformatted(name, NULL); + igSetNextItemWidth(-FLT_MIN); + igPushID_Str(name); + if (igSliderInt("##value", &edit, min, max, "%d", ImGuiSliderFlags_AlwaysClamp)) { + uiStatus(t, set(t->world, (size_t)edit)); + } + igPopID(); +} + +static void threadingUi(Testbed *t, UiState *ui) { + igText("SIMD: %u lanes", t->buildFeatures.simd_lanes); + if (!t->buildFeatures.parallel) { + igTextUnformatted("Parallelism: disabled in this build", NULL); + igTextWrapped("Rebuild with RAPIER_ENABLE_PARALLEL=ON to select multiple workers."); + return; + } + igText("Active workers: %zu", t->activeThreads); + igSetNextItemWidth(145); + igInputInt("Requested workers", &ui->threadInput, 1, 4, 0); + tooltip("0 = automatic selection, 1 = one worker. Applies without resetting " + "the simulation."); + bool valid = ui->threadInput >= 0 && ui->threadInput <= TB_MAX_THREADS; + igBeginDisabled(!valid || (size_t)ui->threadInput == t->requestedThreads); + if (igButton("Apply threads", UI2(0, 0))) { + RAPIER_TYPE(Status) status = tbSetThreads(t, (size_t)ui->threadInput); + if (status != RAPIER_CONST(OK)) { + snprintf(ui->threadError, sizeof(ui->threadError), "%s", RAPIER_FN(LastError)()); + } else { + ui->threadError[0] = 0; + } + } + igEndDisabled(); + igTextDisabled("0 = automatic | 1 = single worker"); + if (!valid) { + igTextWrapped("Choose a worker count from 0 to %d.", TB_MAX_THREADS); + } + if (ui->threadError[0]) { + igTextWrapped("%s", ui->threadError); + } +} + +static void settingsUi(Testbed *t, UiState *ui) { + if (!t->world) { + return; + } + if (igCollapsingHeader_TreeNodeFlags("Execution", ImGuiTreeNodeFlags_DefaultOpen)) { + threadingUi(t, ui); + } + if (igCollapsingHeader_TreeNodeFlags("Simulation", ImGuiTreeNodeFlags_DefaultOpen)) { + RAPIER_TYPE(Real) dt = RAPIER_FN(TimeStep)(t->world); + uiStatus(t, RAPIER_FN(LastStatus)()); + float value = (float)dt; + igTextUnformatted("Timestep (seconds)", NULL); + igSetNextItemWidth(-FLT_MIN); + if (igSliderFloat("##dt", &value, 0.001f, 0.05f, "%.4f", ImGuiSliderFlags_AlwaysClamp)) { + uiStatus(t, RAPIER_FN(SetTimeStep)(t->world, (RAPIER_TYPE(Real))value)); + } + RAPIER_TYPE(Vector) gravity = RAPIER_FN(Gravity)(t->world); + uiStatus(t, RAPIER_FN(LastStatus)()); + igTextUnformatted("Gravity", NULL); + igSetNextItemWidth(-FLT_MIN); + RAPIER_TYPE(Real) + g[] = { + gravity.x, + gravity.y, +#if defined(RAPIER_DIM3) + gravity.z, +#endif + }; + if (igInputScalarN("##gravity", + sizeof(RAPIER_TYPE(Real)) == 4 ? ImGuiDataType_Float + : ImGuiDataType_Double, + g, RAPIER_CONST(DIMENSION), NULL, NULL, "%.2f", 0)) { + uiStatus(t, RAPIER_FN(SetGravity)(t->world, V(g[0], g[1], g[2]))); + } + } + if (igCollapsingHeader_TreeNodeFlags("Solver", 0)) { + iterationSlider(t, "Solver iterations", RAPIER_FN(NumSolverIterations), + RAPIER_FN(SetNumSolverIterations), 1, 32); + iterationSlider(t, "Internal PGS iterations", RAPIER_FN(NumInternalPgsIterations), + RAPIER_FN(SetNumInternalPgsIterations), 1, 32); + iterationSlider(t, "Stabilization iterations", + RAPIER_FN(NumInternalStabilizationIterations), + RAPIER_FN(SetNumInternalStabilizationIterations), 0, 32); + iterationSlider(t, "CCD substeps", RAPIER_FN(MaxCcdSubsteps), RAPIER_FN(SetMaxCcdSubsteps), + 1, 32); + } + if (igCollapsingHeader_TreeNodeFlags("Example settings", ImGuiTreeNodeFlags_DefaultOpen)) { + igTextWrapped("Settings marked Live apply immediately; other changes require a restart."); + bool noSleep = t->noSleep != 0; + if (igCheckbox("Disable sleeping", &noSleep)) { + t->noSleep = noSleep; + } + for (size_t i = 0; i < t->labelCount; ++i) { + igText("%s %s", t->labels[i].name, t->labels[i].value); + } + if (!t->settingCount && !t->labelCount) { + igTextDisabled("No example-specific parameters."); + } + for (size_t i = 0; i < t->settingCount; i++) { + TbSetting *s = &t->settings[i]; + igPushID_Int((int)i); + if (!s->choices && s->integer && s->min == 0 && s->max == 1) { + bool value = s->value != 0; + if (igCheckbox(s->name, &value)) { + s->value = value; + } + igPopID(); + continue; + } + igTextWrapped("%s%s", s->name, s->live ? " (Live)" : ""); + igSetNextItemWidth(-FLT_MIN); + if (s->choices && s->choiceCount) { + int selected = (int)s->value; + if (igBeginCombo("##value", s->choices[selected], ImGuiComboFlags_HeightLarge)) { + for (size_t j = 0; j < s->choiceCount; ++j) { + if (igSelectable_Bool(s->choices[j], selected == (int)j, 0, UI2(0, 0))) { + s->value = (double)j; + } + if (selected == (int)j) { + igSetItemDefaultFocus(); + } + } + igEndCombo(); + } + } else if (s->integer && s->min == 0 && s->max == 1) { + bool value = s->value != 0; + if (igCheckbox("##value", &value)) { + s->value = value; + } + } else if (igSliderScalar("##value", ImGuiDataType_Double, &s->value, &s->min, &s->max, + s->integer ? "%.0f" : "%.3g", ImGuiSliderFlags_AlwaysClamp) && + s->integer) { + s->value = round(s->value); + } + igPopID(); + } + } +} + +static void performanceUi(Testbed *t, UiState *ui) { + igText("%.0f FPS", igGetIO_Nil()->Framerate); + igText("Physics: %.2f ms/step", t->physicsStepMs); + igText("Simulation: %.2f ms/frame (%d step%s)", ui->physicsMs, ui->physicsSteps, + ui->physicsSteps == 1 ? "" : "s"); + igText("Draw CPU: %.2f ms/frame", ui->renderMs); + igTextWrapped("Physics uses the same per-step engine counter as the Rust " + "testbed. Simulation includes the step and scene callbacks " + "in a rendered frame. Draw CPU excludes the UI and GPU execution."); + if (ui->historyCount) { + int offset = ui->historyCount == UI_HISTORY ? ui->historyOffset : 0; + igPlotLines_FloatPtr("##physics", ui->physicsHistory, ui->historyCount, offset, + "Physics (ms/step)", 0, FLT_MAX, UI2(-1, 100), sizeof(float)); + igPlotLines_FloatPtr("##draw", ui->renderHistory, ui->historyCount, offset, "Draw CPU (ms)", + 0, FLT_MAX, UI2(-1, 100), sizeof(float)); + } + igSeparator(); + igText("Simulation time: %.2f s", t->time); + igText("Steps: %" PRIu64, t->step); +} + +static void sidebar(Testbed *t, UiState *ui, float width) { + igSetNextWindowPos(UI2(0, 0), ImGuiCond_Always, UI2(0, 0)); + igSetNextWindowSize(UI2(width, GetScreenHeight()), ImGuiCond_Always); + igBegin("Rapier Testbed", NULL, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings); + igPushFont(NULL, 22); + igTextUnformatted("Rapier C testbed", NULL); + igPopFont(); + const bool release = !strcmp(ui->profile, "release"); + igTextColored(release ? UI4(0.21f, 0.45f, 0.34f, 1) : UI4(0.65f, 0.38f, 0.10f, 1), + "Physics: %s | %dD / f%d", release ? "Release" : "Debug", + RAPIER_CONST(DIMENSION), (int)sizeof(RAPIER_TYPE(Real)) * 8); + tooltip("Cargo build profile reported by the loaded Rapier physics library, " + "independent of the viewer's C/C++ build mode."); + igText("SIMD: %u lanes | Parallel: %s", t->buildFeatures.simd_lanes, + t->buildFeatures.parallel ? "enabled" : "disabled"); + if (t->buildFeatures.parallel && t->activeThreads) { + igTextDisabled("Workers: %zu%s", t->activeThreads, + t->requestedThreads == 0 ? " (automatic)" : ""); + } + tooltip("SIMD lane count and parallel support are reported by the loaded " + "physics library. Configure workers in Settings > Execution."); + igSeparator(); + int previous = adjacentExample(ui, -1), next = adjacentExample(ui, 1); + float navigationWidth = (igGetContentRegionAvail().x - 8) / 2; + igBeginDisabled(previous == ui->selected); + if (igButton("Prev", UI2(navigationWidth, 0))) { + ui->next = previous; + } + igEndDisabled(); + igSameLine(0, -1); + igBeginDisabled(next == ui->selected); + if (igButton("Next", UI2(navigationWidth, 0))) { + ui->next = next; + } + igEndDisabled(); + float contentHeight = fmaxf(96, igGetContentRegionAvail().y - 196); + if (igBeginTabBar("Main tabs", ImGuiTabBarFlags_None)) { + const char *tabs[] = {"Examples", "Settings", "Performance", "Debug"}; + for (size_t i = 0; i < TB_COUNT(tabs); i++) { + ImGuiTabItemFlags flags = (ui->frame == 0 && !strcmp(ui->initialTab, tabs[i])) + ? ImGuiTabItemFlags_SetSelected + : 0; + if (igBeginTabItem(tabs[i], NULL, flags)) { + if (igBeginChild_Str(tabs[i], UI2(0, contentHeight), 0, 0)) { + if (i == 0) { + examplesUi(ui); + } else if (i == 1) { + settingsUi(t, ui); + } else if (i == 2) { + performanceUi(t, ui); + } else { + igCheckbox("Surfaces", &ui->surfaces); + igSeparator(); + for (size_t j = 0; j < TB_COUNT(debugNames); j++) { + igCheckbox(debugNames[j], &ui->debug[j]); + } + igSpacing(); + igTextWrapped("Wireframes and overlays use Rapier's debug renderer."); + } + } + igEndChild(); + igEndTabItem(); + } + } + igEndTabBar(); + } + igSeparator(); + float buttonWidth = (igGetContentRegionAvail().x - 16) / 3; + if (igButton(ui->running ? "Pause [T]" : "Play [T]", UI2(buttonWidth, 0))) { + ui->running = !ui->running; + } + igSameLine(0, -1); + if (igButton("Step [S]", UI2(buttonWidth, 0))) { + ui->stepOnce = true; + } + igSameLine(0, -1); + if (igButton("Restart [R]", UI2(buttonWidth, 0))) { + ui->restart = true; + } + if (igButton("Frame [F]", UI2(buttonWidth, 0))) { + ui->frameAll = true; + } + igSameLine(0, -1); + bool canSnapshot = t->snapshotSupported && !t->error[0]; + igBeginDisabled(!canSnapshot); + if (igButton("Save", UI2(buttonWidth, 0))) { + ui->save = true; + } + tooltip(canSnapshot ? "Save a physics snapshot." + : "This example has local simulation state that physics " + "snapshots do not serialize."); + igSameLine(0, -1); + igBeginDisabled(!ui->snapshot); + if (igButton("Restore", UI2(buttonWidth, 0))) { + ui->restore = true; + } + igEndDisabled(); + igEndDisabled(); + size_t nb = 0, nc = 0, ns = 0; + if (t->world) { + nb = RAPIER_FN(RigidBodyCount)(t->world); + uiStatus(t, RAPIER_FN(LastStatus)()); + nc = RAPIER_FN(ColliderCount)(t->world); + uiStatus(t, RAPIER_FN(LastStatus)()); + ns = RAPIER_FN(SoftBodyCount)(t->world); + uiStatus(t, RAPIER_FN(LastStatus)()); + } + igText("Bodies %zu | Colliders %zu | Soft %zu", nb, nc, ns); + igText("Step %" PRIu64 " | Physics %.2f ms/step", t->step, t->physicsStepMs); + igTextDisabled("Draw CPU %.2f ms | %d FPS", ui->renderMs, GetFPS()); + igEnd(); +} + +static void sceneOverlay(Testbed *t, float sidebarWidth) { + igSetNextWindowPos(UI2(sidebarWidth + 18, 12), ImGuiCond_Always, UI2(0, 0)); + igSetNextWindowSize(UI2(fmaxf(100, (float)GetScreenWidth() - sidebarWidth - 36), 0), ImGuiCond_Always); + igBegin("Scene", NULL, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_AlwaysAutoResize); + igPushFont(NULL, 22); + igTextUnformatted(t->example->name, NULL); + igPopFont(); +#if defined(RAPIER_DIM2) + igTextDisabled("Left drag: grab | Right drag: pan | Wheel: zoom"); +#else + igTextDisabled("Left drag: grab | Right drag: orbit | Wheel: zoom"); + igTextDisabled("Shift + right drag / middle drag: pan"); +#endif + igTextDisabled("Arrows: move | Space: jump | Enter: action | Hold C: cut"); + if (t->error[0]) { + igSpacing(); + igTextColored(UI4(0.70f, 0.18f, 0.14f, 1), "Scene error"); + igTextWrapped("%s", t->error); + } + igEnd(); +} + +#if defined(RAPIER_DIM3) +/* Orbit about the scene target, preserving distance and the example's up axis. */ +static void orbitCamera(Camera3D *camera, Vector2 delta, bool pan, float wheel, + float viewportHeight) { + Vector3 offset = Vector3Subtract(camera->position, camera->target); + float distance = fmaxf(Vector3Length(offset), .001f); + Vector3 up = Vector3Normalize(camera->up); + Vector3 right = Vector3Normalize(Vector3CrossProduct(up, offset)); + if (Vector3LengthSqr(right) < 1.0e-8f) { + right = Vector3Normalize( + Vector3CrossProduct(fabsf(up.x) < .9f ? (Vector3){1, 0, 0} : (Vector3){0, 0, 1}, up)); + } + if (pan) { + Vector3 screenUp = Vector3Normalize(Vector3CrossProduct(offset, right)); + float scale = 2 * distance * tanf(camera->fovy * DEG2RAD * .5f) / viewportHeight; + Vector3 shift = Vector3Add(Vector3Scale(right, -delta.x * scale), + Vector3Scale(screenUp, delta.y * scale)); + camera->target = Vector3Add(camera->target, shift); + } else { + offset = Vector3RotateByAxisAngle(offset, up, -delta.x * .005f); + Vector3 rotatedRight = Vector3CrossProduct(up, offset); + if (Vector3LengthSqr(rotatedRight) > 1.0e-8f) { + right = Vector3Normalize(rotatedRight); + } + float cosine = Vector3DotProduct(Vector3Normalize(offset), up); + float polar = acosf(fmaxf(-1, fminf(1, cosine))); + float nextPolar = fmaxf(.01f, fminf(PI - .01f, polar - delta.y * .005f)); + offset = Vector3RotateByAxisAngle(offset, right, nextPolar - polar); + } + float zoom = fmaxf(.001f, distance * expf(-wheel * .12f)); + camera->position = Vector3Add(camera->target, Vector3Scale(Vector3Normalize(offset), zoom)); +} +#endif + +typedef struct GuiViewer { + UiState *ui; + TbGraphics *graphics; + TbGrab grab; + Camera3D camera; + int frames, sceneStarted; + double physicsStart; + uint64_t previousStep; +} GuiViewer; + +static int renderFrame(Testbed *t, void *context) { + GuiViewer *viewer = context; + UiState *ui = viewer->ui; + if (!viewer->sceneStarted) { + viewer->sceneStarted = 1; + viewer->grab = (TbGrab){0}; + for (size_t i = 0; i < TB_COUNT(debugModes); ++i) { + ui->debug[i] = (t->initialDebug & debugModes[i]) != 0; + } + if (!t->preserveCamera || !ui->frame) { + viewer->camera = cameraFor(t); + } + if (t->frameAll) { + tbGraphicsFrameAll(t, &viewer->camera); + } + viewer->previousStep = 0; + ui->physicsSteps = 0; + ui->physicsMs = 0; + TraceLog(LOG_INFO, "UI: SIMD %u lanes; parallel %s; workers %zu (requested %zu)", + t->buildFeatures.simd_lanes, t->buildFeatures.parallel ? "enabled" : "disabled", + t->activeThreads, t->requestedThreads); + } else { + ui->physicsSteps = t->step > viewer->previousStep; + ui->physicsMs = ui->physicsSteps ? (GetTime() - viewer->physicsStart) * 1000 : 0; + } + viewer->previousStep = t->step; + if (WindowShouldClose() || (viewer->frames && ui->frame >= viewer->frames)) { + uiStatus(t, tbGrabRelease(t, &viewer->grab)); + return 0; + } + Camera3D camera = viewer->camera; + TbGraphics *g = viewer->graphics; + + ui->stepOnce = ui->restart = ui->frameAll = ui->save = ui->restore = false; + // Build the UI first so widgets can consume mouse and keyboard input. + rlImGuiBegin(); + const float sidebarWidth = 390; + sidebar(t, ui, sidebarWidth); + sceneOverlay(t, sidebarWidth); + ImGuiIO *io = igGetIO_Nil(); + bool captureKeyboard = io->WantCaptureKeyboard || io->WantTextInput; + keyboardShortcuts(ui); + Vector2 mouse = GetMousePosition(); + bool captureMouse = io->WantCaptureMouse || mouse.x < sidebarWidth; + if (!captureMouse && !viewer->grab.active) { +#if defined(RAPIER_DIM2) + float wheel = GetMouseWheelMove(); + camera.fovy = fmaxf(0.05f, camera.fovy * expf(-wheel * 0.12f)); + if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) { + Vector2 d = GetMouseDelta(); + float scale = camera.fovy / (float)GetScreenHeight(); + camera.position.x -= d.x * scale; + camera.target.x -= d.x * scale; + camera.position.y += d.y * scale; + camera.target.y += d.y * scale; + } +#else + bool pan = IsMouseButtonDown(MOUSE_BUTTON_MIDDLE) || IsKeyDown(KEY_LEFT_SHIFT) || + IsKeyDown(KEY_RIGHT_SHIFT); + Vector2 delta = + (IsMouseButtonDown(MOUSE_BUTTON_RIGHT) || IsMouseButtonDown(MOUSE_BUTTON_MIDDLE)) + ? GetMouseDelta() + : (Vector2){0}; + orbitCamera(&camera, delta, pan, GetMouseWheelMove(), (float)GetScreenHeight()); +#endif + } + t->inputDirection = captureKeyboard + ? V(0, 0, 0) + : V((IsKeyDown(KEY_RIGHT) ? 1 : 0) - (IsKeyDown(KEY_LEFT) ? 1 : 0), + (IsKeyDown(KEY_UP) ? 1 : 0) - (IsKeyDown(KEY_DOWN) ? 1 : 0), 0); + t->jump = !captureKeyboard && IsKeyDown(KEY_SPACE); + t->boost = !captureKeyboard && IsKeyDown(KEY_RIGHT_SHIFT); + t->descend = !captureKeyboard && IsKeyDown(KEY_RIGHT_CONTROL); + t->slow = !captureKeyboard && (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)); +#if defined(RAPIER_DIM3) + Vector3 forward = Vector3Normalize(Vector3Subtract(camera.target, camera.position)); + Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up)); + t->cameraRight = V(right.x, 0, right.z); + t->cameraForward = V(forward.x, 0, forward.z); +#endif + t->action = !captureKeyboard && IsKeyPressed(KEY_ENTER); + t->cutting = !captureKeyboard && IsKeyDown(KEY_C); + t->cursorValid = 0; + t->rayValid = 0; + t->removeVoxel = !captureKeyboard && IsKeyDown(KEY_LEFT_SHIFT); +#if defined(RAPIER_DIM2) + if (!captureMouse) { + Ray ray = GetScreenToWorldRay(mouse, camera); + t->rayValid = 1; + t->rayOrigin = V(ray.position.x, ray.position.y, ray.position.z); + t->rayDirection = V(ray.direction.x, ray.direction.y, ray.direction.z); + if (fabsf(ray.direction.z) > 1.0e-6f) { + float distance = -ray.position.z / ray.direction.z; + t->cursor = V(ray.position.x + distance * ray.direction.x, + ray.position.y + distance * ray.direction.y, 0); + t->cursorValid = 1; + } + } +#else + if (!captureMouse) { + Ray ray = GetScreenToWorldRay(mouse, camera); + t->rayValid = 1; + t->rayOrigin = V(ray.position.x, ray.position.y, ray.position.z); + t->rayDirection = V(ray.direction.x, ray.direction.y, ray.direction.z); + Vector3 normal = Vector3Normalize(Vector3Subtract(camera.position, camera.target)); + float denominator = Vector3DotProduct(ray.direction, normal); + if (fabsf(denominator) > 1.0e-6f) { + float distance = -Vector3DotProduct(ray.position, normal) / denominator; + if (distance >= 0) { + Vector3 point = Vector3Add(ray.position, Vector3Scale(ray.direction, distance)); + t->cursor = V(point.x, point.y, point.z); + t->cursorValid = 1; + } + } + } +#endif + bool transition = ui->next != ui->selected || ui->restart; + if (!IsMouseButtonDown(MOUSE_BUTTON_LEFT) || transition || ui->save || ui->restore || + !IsWindowFocused()) { + uiStatus(t, tbGrabRelease(t, &viewer->grab)); + } else if (!captureMouse && !t->error[0]) { + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + RAPIER_TYPE(Real) radius = (RAPIER_TYPE(Real))(camera.fovy * 8 / (float)GetScreenHeight()); + uiStatus(t, tbGrabBegin(t, &viewer->grab, radius)); + } + Vector3 direction = Vector3Normalize(Vector3Subtract(camera.target, camera.position)); + uiStatus(t, tbGrabUpdate(t, &viewer->grab, V(direction.x, direction.y, direction.z))); + } + tbGrabDrawCue(t, &viewer->grab); + if (ui->save) { + worldSnapshot(t, &ui->snapshot, 1, &ui->snapshotStep, &ui->snapshotTime); + } + if (ui->restore) { + worldSnapshot(t, &ui->snapshot, 0, &ui->snapshotStep, &ui->snapshotTime); + tbGraphicsFree(g); + g = tbGraphicsNew(); + } + if (ui->frameAll) { + tbGraphicsFrameAll(t, &camera); + } + if (t->error[0]) { + ui->running = false; + } + t->simulating = !transition && !t->error[0] && (ui->running || ui->stepOnce); + ui->stepOnce = false; + double start; + BeginDrawing(); + ClearBackground((Color){250, 250, 245, 255}); + uint32_t debug = 0; + for (size_t i = 0; i < TB_COUNT(debugModes); i++) { + if (ui->debug[i]) { + debug |= debugModes[i]; + } + } + start = GetTime(); + tbGraphicsDraw(g, t, camera, debug, ui->surfaces); + ui->renderMs = (GetTime() - start) * 1000; + rlImGuiEnd(); + EndDrawing(); + ui->physicsHistory[ui->historyOffset] = (float)(ui->physicsSteps ? t->physicsStepMs : 0); + ui->renderHistory[ui->historyOffset] = (float)ui->renderMs; + ui->historyOffset = (ui->historyOffset + 1) % UI_HISTORY; + if (ui->historyCount < UI_HISTORY) { + ui->historyCount++; + } + ui->frame++; + + viewer->graphics = g; + viewer->camera = camera; + viewer->physicsStart = GetTime(); + return !transition; +} + +int tbGui(int argc, char **argv) { + if (!abi()) { + return 1; + } + int initial = 0, noSleep = 0, frames = 0; + size_t threads = 0; + const char *initialTab = "Examples"; + const char *screenshot = NULL, *assets = TB_ASSET_ROOT; + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--headless") || !strcmp(argv[i], "--list") || + !strcmp(argv[i], "--all")) { + return tbHeadless(argc, argv); + } + if (!strcmp(argv[i], "--example") && i + 1 < argc) { + const char *name = argv[++i]; + initial = -1; + for (size_t j = 0; j < tbExampleCount; j++) { + if (!strcmp(name, tbExamples[j].id)) { + initial = (int)j; + } + } + if (initial < 0) { + fprintf(stderr, "Unknown example: %s\n", name); + return 2; + } + } else if (!strcmp(argv[i], "--no-sleep")) { + noSleep = 1; + } else if (!strcmp(argv[i], "--frames") && i + 1 < argc) { + frames = atoi(argv[++i]); + if (frames <= 0) { + return 2; + } + } else if (!strcmp(argv[i], "--screenshot") && i + 1 < argc) { + screenshot = argv[++i]; + } else if (!strcmp(argv[i], "--ui-tab") && i + 1 < argc) { + initialTab = argv[++i]; + if (strcmp(initialTab, "Examples") && strcmp(initialTab, "Settings") && + strcmp(initialTab, "Performance") && strcmp(initialTab, "Debug")) { + fputs("--ui-tab expects Examples, Settings, Performance, or Debug\n", stderr); + return 2; + } + } else if (!strcmp(argv[i], "--threads") && i + 1 < argc) { + if (!tbParseThreads(argv[++i], &threads)) { + fprintf(stderr, "--threads expects an integer from 0 (automatic) to %d\n", + TB_MAX_THREADS); + return 2; + } + } else if (!strcmp(argv[i], "--assets") && i + 1 < argc) { + assets = argv[++i]; + } else { + printf("Usage: %s [--example ID] [--no-sleep] [--frames N --screenshot " + "FILE] [--assets PATH] [--ui-tab TAB] [--threads N]\n " + "--headless [--all | " + "--example ID] " + "[--steps N]\n", + argv[0]); + return !strcmp(argv[i], "--help") ? 0 : 2; + } + } + + SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_MSAA_4X_HINT | FLAG_WINDOW_HIGHDPI); + InitWindow(1440, 900, "Rapier C testbed - Dear ImGui"); + if (!IsWindowReady()) { + fputs("Cannot create a graphics window. Use rapier_testbed_headless " + "without a display.\n", + stderr); + return 1; + } + SetWindowMinSize(850, 500); + SetTargetFPS(60); + SetExitKey(KEY_NULL); + if (!setupUi()) { + rlImGuiShutdown(); + CloseWindow(); + return 1; + } + UiState ui = {0}; + ui.running = true; + ui.surfaces = true; + ui.selected = initial; + ui.next = initial; + ui.initialTab = initialTab; + ui.threadInput = (int)threads; + ui.profile = RAPIER_FN(BuildProfile)(); + TraceLog(LOG_INFO, "UI: Dear ImGui %s; physics compiled in %s mode", igGetVersion(), + ui.profile); + Testbed *t = calloc(1, sizeof(*t)); + if (!t) { + abort(); + } + t->noSleep = noSleep; + t->requestedThreads = threads; + t->assetRoot = assets; + GuiViewer viewer = {.ui = &ui, .frames = frames}; + t->renderFrame = renderFrame; + t->viewer = &viewer; + int preserve = 0; + while (!WindowShouldClose() && (!frames || ui.frame < frames)) { + viewer.graphics = tbGraphicsNew(); + viewer.sceneStarted = 0; + ui.historyCount = ui.historyOffset = 0; + (void)tbRun(t, &tbExamples[ui.selected], preserve); + if (t->error[0]) { + ui.running = false; + /* Keep the error visible and allow selection of another example. */ + while (renderFrame(t, &viewer)) { + } + } + tbGraphicsFree(viewer.graphics); + viewer.graphics = NULL; + preserve = ui.next == ui.selected; + ui.selected = ui.next; + (void)RAPIER_FN(FreeBytes)(ui.snapshot); + ui.snapshot = NULL; + } + if (screenshot) { + Image capture = LoadImageFromScreen(); + if (!ExportImage(capture, screenshot)) { + snprintf(t->error, sizeof(t->error), "Cannot save screenshot: %s", screenshot); + } + UnloadImage(capture); + } + int result = t->error[0] ? 1 : 0; + TraceLog(LOG_INFO, "UI: completed %d frames, physics step %llu, profile %s", ui.frame, + (unsigned long long)t->step, ui.profile); + tbDestroy(t); + free(t); + (void)RAPIER_FN(FreeBytes)(ui.snapshot); + rlImGuiShutdown(); + CloseWindow(); + return result; +} +#ifndef TB_UI_TESTING +int main(int argc, char **argv) { + return tbGui(argc, argv); +} +#endif diff --git a/c/testbed/headless.c b/c/testbed/headless.c new file mode 100644 index 000000000..132ad4b8d --- /dev/null +++ b/c/testbed/headless.c @@ -0,0 +1,222 @@ +#include "testbed_internal.h" +#include "testbed.h" +#include +#include +#include + +static int finiteVector(RAPIER_TYPE(Vector) p) { +#if defined(RAPIER_DIM3) + if (!isfinite(p.z)) { + return 0; + } +#endif + return isfinite(p.x) && isfinite(p.y); +} + +int tbValidate(Testbed *t, size_t *nb, size_t *nc, size_t *ns) { + if (setjmp(t->failure)) { + return 0; + } + *nb = RAPIER_FN(RigidBodyCount)(t->world); + TB(t, RAPIER_FN(LastStatus)()); + *nc = RAPIER_FN(ColliderCount)(t->world); + TB(t, RAPIER_FN(LastStatus)()); + *ns = RAPIER_FN(SoftBodyCount)(t->world); + TB(t, RAPIER_FN(LastStatus)()); + RAPIER_TYPE(RigidBodyHandle) *b = malloc((*nb ? *nb : 1) * sizeof(*b)); + if (!b) { + return 0; + } + *nb = RAPIER_FN(RigidBodyHandles)(t->world, b, *nb); + RAPIER_TYPE(Status) status = RAPIER_FN(LastStatus)(); + if (status) { + free(b); + TB(t, status); + } + for (size_t i = 0; i < *nb; i++) { + RAPIER_TYPE(Vector) p, v; + status = RAPIER_FN(RigidBody_ValidateHandle)(b[i]); + if (!status) { + p = RAPIER_FN(RigidBody_Translation)(b[i]); + status = RAPIER_FN(LastStatus)(); + } + if (!status) { + v = RAPIER_FN(RigidBody_Linvel)(b[i]); + status = RAPIER_FN(LastStatus)(); + } + if (status || !finiteVector(p) || !finiteVector(v)) { + free(b); + snprintf(t->error, sizeof(t->error), "non-finite or invalid rigid body"); + return 0; + } + } + free(b); + RAPIER_TYPE(SoftBodyHandle) *s = malloc((*ns ? *ns : 1) * sizeof(*s)); + if (!s) { + return 0; + } + *ns = RAPIER_FN(SoftBodyHandles)(t->world, s, *ns); + status = RAPIER_FN(LastStatus)(); + if (status) { + free(s); + TB(t, status); + } + for (size_t i = 0; i < *ns; i++) { + size_t n = 0; + status = RAPIER_FN(SoftBody_ValidateHandle)(s[i]); + if (!status) { + n = RAPIER_FN(SoftBody_ParticlePositions)(s[i], NULL, 0); + status = RAPIER_FN(LastStatus)(); + } + if (status) { + free(s); + TB(t, status); + } + RAPIER_TYPE(Vector) *p = malloc((n ? n : 1) * sizeof(*p)); + if (!p) { + free(s); + return 0; + } + n = RAPIER_FN(SoftBody_ParticlePositions)(s[i], p, n); + status = RAPIER_FN(LastStatus)(); + for (size_t j = 0; j < n && !status; j++) { + if (!finiteVector(p[j])) { + status = RAPIER_CONST(INVALID_ARGUMENT); + } + } + free(p); + if (status) { + free(s); + snprintf(t->error, sizeof(t->error), "non-finite or invalid soft body"); + return 0; + } + } + free(s); + return 1; +} + +typedef struct HeadlessViewer { + unsigned long steps; + size_t bodies, colliders, softBodies; + int valid; +} HeadlessViewer; + +static int headlessFrame(Testbed *t, void *context) { + HeadlessViewer *viewer = context; + if (t->step == 0) { + printf("CONFIG %s SIMD=%u parallel=%s workers=%zu requested=%zu\n", t->example->id, + t->buildFeatures.simd_lanes, t->buildFeatures.parallel ? "on" : "off", + t->activeThreads, t->requestedThreads); + } + if (t->step < viewer->steps && !t->error[0]) { + t->simulating = 1; + return 1; + } + viewer->valid = tbValidate(t, &viewer->bodies, &viewer->colliders, &viewer->softBodies); + return 0; +} + +static void usage(const char *name) { + printf("Usage: %s [--list] [--example ID] [--all] [--steps N] [--no-sleep] [--threads N] " + "[--assets PATH]\n", + name); +} + +int tbHeadless(int argc, char **argv) { + const char *id = NULL, *assets = TB_ASSET_ROOT; + int all = 0, noSleep = 0; + unsigned long steps = 120; + size_t threads = 0; + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--headless")) { + continue; + } + if (!strcmp(argv[i], "--list")) { + for (size_t j = 0; j < tbExampleCount; j++) { + printf("%s\t%s / %s%s%s\n", tbExamples[j].id, tbExamples[j].group, + tbExamples[j].name, tbExamples[j].requires ? "\tUNAVAILABLE: " : "", + tbExamples[j].requires ? tbExamples[j].requires : ""); + } + return 0; + } + if (!strcmp(argv[i], "--all")) { + all = 1; + continue; + } + if (!strcmp(argv[i], "--no-sleep")) { + noSleep = 1; + continue; + } + if (!strcmp(argv[i], "--threads") && i + 1 < argc) { + if (!tbParseThreads(argv[++i], &threads)) { + fprintf(stderr, "--threads expects an integer from 0 (automatic) to %d\n", + TB_MAX_THREADS); + return 2; + } + continue; + } + if (!strcmp(argv[i], "--example") && i + 1 < argc) { + id = argv[++i]; + continue; + } + if (!strcmp(argv[i], "--assets") && i + 1 < argc) { + assets = argv[++i]; + continue; + } + if (!strcmp(argv[i], "--steps") && i + 1 < argc) { + char *end; + errno = 0; + const char *s = argv[++i]; + steps = strtoul(s, &end, 10); + if (errno || *end || s == end || s[0] == '-') { + usage(argv[0]); + return 2; + } + continue; + } + usage(argv[0]); + return !strcmp(argv[i], "--help") ? 0 : 2; + } + Testbed *t = calloc(1, sizeof(*t)); + if (!t) { + return 1; + } + t->noSleep = noSleep; + t->assetRoot = assets; + t->requestedThreads = threads; + size_t ran = 0, failed = 0, skipped = 0; + for (size_t i = 0; i < tbExampleCount; i++) { + const TbExample *e = &tbExamples[i]; + if (!all && ((id && strcmp(e->id, id)) || (!id && i))) { + continue; + } + if (e->requires) { + printf("SKIP %s: %s\n", e->id, e->requires); + skipped++; + continue; + } + HeadlessViewer viewer = {.steps = steps}; + t->renderFrame = headlessFrame; + t->viewer = &viewer; + int ok = tbRun(t, e, 0) && viewer.valid; + printf("%s %s steps=%" PRIu64 " bodies=%zu colliders=%zu soft=%zu%s%s\n", + ok ? "PASS" : "FAIL", e->id, t->step, viewer.bodies, viewer.colliders, + viewer.softBodies, ok ? "" : " ", ok ? "" : t->error); + fflush(stdout); + ran++; + failed += !ok; + } + tbDestroy(t); + free(t); + if (!ran && !skipped) { + fprintf(stderr, "unknown example: %s\n", id ? id : ""); + return 2; + } + printf("%zu passed, %zu failed, %zu unavailable\n", ran - failed, failed, skipped); + return failed || (!all && skipped) ? 1 : 0; +} +#ifdef TB_HEADLESS_ONLY +int main(int argc, char **argv) { + return tbHeadless(argc, argv); +} +#endif diff --git a/c/testbed/headless_main.c b/c/testbed/headless_main.c new file mode 100644 index 000000000..adfe13bc4 --- /dev/null +++ b/c/testbed/headless_main.c @@ -0,0 +1,5 @@ +#include "testbed.h" + +int main(int argc, char **argv) { + return tbHeadless(argc, argv); +} diff --git a/c/testbed/licenses/Apache-2.0.txt b/c/testbed/licenses/Apache-2.0.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/c/testbed/licenses/Apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/c/testbed/licenses/CC0-1.0.txt b/c/testbed/licenses/CC0-1.0.txt new file mode 100644 index 000000000..0e259d42c --- /dev/null +++ b/c/testbed/licenses/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/c/testbed/licenses/FiraSans-LICENSE.txt b/c/testbed/licenses/FiraSans-LICENSE.txt new file mode 100644 index 000000000..b82722716 --- /dev/null +++ b/c/testbed/licenses/FiraSans-LICENSE.txt @@ -0,0 +1,93 @@ +Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/c/testbed/licenses/GLAD.txt b/c/testbed/licenses/GLAD.txt new file mode 100644 index 000000000..4965a6bff --- /dev/null +++ b/c/testbed/licenses/GLAD.txt @@ -0,0 +1,63 @@ +The glad source code: + + The MIT License (MIT) + + Copyright (c) 2013-2022 David Herberth + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +The Khronos Specifications: + + Copyright (c) 2013-2020 The Khronos Group Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +The EGL Specification and various headers: + + Copyright (c) 2007-2016 The Khronos Group Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and/or associated documentation files (the + "Materials"), to deal in the Materials without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Materials, and to + permit persons to whom the Materials are furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Materials. + + THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. diff --git a/c/testbed/licenses/LGPL-2.1.txt b/c/testbed/licenses/LGPL-2.1.txt new file mode 100644 index 000000000..f6683e74e --- /dev/null +++ b/c/testbed/licenses/LGPL-2.1.txt @@ -0,0 +1,501 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Moe Ghoul, President of Vice + +That's all there is to it! diff --git a/c/testbed/licenses/MIT.txt b/c/testbed/licenses/MIT.txt new file mode 100644 index 000000000..d817195da --- /dev/null +++ b/c/testbed/licenses/MIT.txt @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/c/testbed/licenses/SOURCES.md b/c/testbed/licenses/SOURCES.md new file mode 100644 index 000000000..bc7b78013 --- /dev/null +++ b/c/testbed/licenses/SOURCES.md @@ -0,0 +1,12 @@ +# Supplemental license texts + +Retrieved on 2026-09-24. Original per-file notices remain in the downloaded sources. + +- `Apache-2.0.txt`: https://www.apache.org/licenses/LICENSE-2.0.txt +- `LGPL-2.1.txt`: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +- `GLAD.txt`: https://raw.githubusercontent.com/Dav1dde/glad/v2.0.2/LICENSE +- `MIT.txt`: https://raw.githubusercontent.com/spdx/license-list-data/main/text/MIT.txt +- `WTFPL.txt`: https://raw.githubusercontent.com/spdx/license-list-data/main/text/WTFPL.txt +- `CC0-1.0.txt`: https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt + +- `FiraSans-LICENSE.txt`: https://raw.githubusercontent.com/mozilla/Fira/fd8c8c0a3d353cd99e8ca1662942d165e6961407/LICENSE diff --git a/c/testbed/licenses/WTFPL.txt b/c/testbed/licenses/WTFPL.txt new file mode 100644 index 000000000..7a3094a82 --- /dev/null +++ b/c/testbed/licenses/WTFPL.txt @@ -0,0 +1,11 @@ +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/c/testbed/registry2.c b/c/testbed/registry2.c new file mode 100644 index 000000000..8977ca0c8 --- /dev/null +++ b/c/testbed/registry2.c @@ -0,0 +1,189 @@ +/* Generated by update_catalog.py; order matches the Rust testbed. */ +#include "testbed.h" +extern void tbAddRemove2(Testbed *); +extern void tbDrum2(Testbed *); +extern void tbInvPyramid2(Testbed *); +extern void tbPlatform2(Testbed *); +extern void tbPyramid2(Testbed *); +extern void tbSensor2(Testbed *); +extern void tbConvexPolygons2(Testbed *); +extern void tbHeightfield2(Testbed *); +extern void tbPolyline2(Testbed *); +extern void tbTrimesh2(Testbed *); +extern void tbVoxels2(Testbed *); +extern void tbCollisionGroups2(Testbed *); +extern void tbOneWayPlatforms2(Testbed *); +extern void tbLockedRotations2(Testbed *); +extern void tbRestitution2(Testbed *); +extern void tbDamping2(Testbed *); +extern void tbCcd2(Testbed *); +extern void tbJoints2(Testbed *); +extern void tbRopeJoints2(Testbed *); +extern void tbPinSlotJoint2(Testbed *); +extern void tbJointMotorPosition2(Testbed *); +extern void tbInverseKinematics2(Testbed *); +extern void tbMultiPendulum2(Testbed *); +extern void tbSoftBodies2(Testbed *); +extern void tbSoftBlobs2(Testbed *); +extern void tbSoftJelly2(Testbed *); +extern void tbSoftSurface2(Testbed *); +extern void tbSoftPile2(Testbed *); +extern void tbSoftThinFeatures2(Testbed *); +extern void tbSoftLetters2(Testbed *); +extern void tbSoftPlasticity2(Testbed *); +extern void tbSoftTearing2(Testbed *); +extern void tbSoftForceTearing2(Testbed *); +extern void tbSoftCutting2(Testbed *); +extern void tbSoftStress2(Testbed *); +extern void tbSoftFem2(Testbed *); +extern void tbSoftJoints2(Testbed *); +extern void tbCharacterController2(Testbed *); +extern void tbDebugAngularLimits2(Testbed *); +extern void tbDebugBoxBall2(Testbed *); +extern void tbDebugCompression2(Testbed *); +extern void tbDebugIntersection2(Testbed *); +extern void tbDebugManyColliders2(Testbed *); +extern void tbDebugTotalOverlap2(Testbed *); +extern void tbDebugVerticalColumn2(Testbed *); +extern void tbDebugSelfIntersect2(Testbed *); +extern void tbS2dHighMassRatio1(Testbed *); +extern void tbS2dHighMassRatio2(Testbed *); +extern void tbS2dHighMassRatio3(Testbed *); +extern void tbS2dConfined(Testbed *); +extern void tbS2dPyramid(Testbed *); +extern void tbS2dCardHouse(Testbed *); +extern void tbS2dArch(Testbed *); +extern void tbS2dBridge(Testbed *); +extern void tbS2dBallAndChain(Testbed *); +extern void tbS2dJointGrid(Testbed *); +extern void tbS2dFarPyramid(Testbed *); +extern void tbB2dCompounds(Testbed *); +extern void tbB2dJointGrid(Testbed *); +extern void tbB2dJunkyard(Testbed *); +extern void tbB2dLargePyramid(Testbed *); +extern void tbB2dManyPyramids(Testbed *); +extern void tbB2dRain(Testbed *); +extern void tbB2dSmash(Testbed *); +extern void tbB2dSpinner(Testbed *); +extern void tbB2dTumbler(Testbed *); +extern void tbB2dWasher(Testbed *); +extern void tbStressTestsBalls2(Testbed *); +extern void tbStressTestsBoxes2(Testbed *); +extern void tbStressTestsCapsules2(Testbed *); +extern void tbStressTestsConvexPolygons2(Testbed *); +extern void tbStressTestsHeightfield2(Testbed *); +extern void tbStressTestsLargePyramids2(Testbed *); +extern void tbStressTestsManyPyramids2(Testbed *); +extern void tbStressTestsPyramid2(Testbed *); +extern void tbStressTestsRagdolls2(Testbed *); +extern void tbStressTestsRopes2(Testbed *); +extern void tbStressTestsVerticalStacks2(Testbed *); +extern void tbStressTestsJointBall2(Testbed *); +extern void tbStressTestsJointFixed2(Testbed *); +extern void tbStressTestsJointPrismatic2(Testbed *); +extern void tbStressTestsSoftBlobs2(Testbed *); +extern void tbStressTestsSoftJellies2(Testbed *); +extern void tbStressTestsSoftRopes2(Testbed *); +extern void tbStressTestsSoftStrips2(Testbed *); +extern void tbStressTestsSoftClothKeva2(Testbed *); +extern void tbStressTestsSoftSlab2(Testbed *); +extern void tbStressTestsSoftFemBeams2(Testbed *); +const TbExample tbExamples[] = { + {"add_remove2", "Collisions", "Add remove", "examples2d/add_remove2.rs", tbAddRemove2, NULL}, + {"drum2", "Collisions", "Drum", "examples2d/drum2.rs", tbDrum2, NULL}, + {"inv_pyramid2", "Collisions", "Inv pyramid", "examples2d/inv_pyramid2.rs", tbInvPyramid2, NULL}, + {"platform2", "Collisions", "Platform", "examples2d/platform2.rs", tbPlatform2, NULL}, + {"pyramid2", "Collisions", "Pyramid", "examples2d/pyramid2.rs", tbPyramid2, NULL}, + {"sensor2", "Collisions", "Sensor", "examples2d/sensor2.rs", tbSensor2, NULL}, + {"convex_polygons2", "Collisions", "Convex polygons", "examples2d/convex_polygons2.rs", tbConvexPolygons2, NULL}, + {"heightfield2", "Collisions", "Heightfield", "examples2d/heightfield2.rs", tbHeightfield2, NULL}, + {"polyline2", "Collisions", "Polyline", "examples2d/polyline2.rs", tbPolyline2, NULL}, + {"trimesh2", "Collisions", "Trimesh", "examples2d/trimesh2.rs", tbTrimesh2, NULL}, + {"voxels2", "Collisions", "Voxels", "examples2d/voxels2.rs", tbVoxels2, NULL}, + {"collision_groups2", "Collisions", "Collision groups", "examples2d/collision_groups2.rs", tbCollisionGroups2, NULL}, + {"one_way_platforms2", "Collisions", "One-way platforms", "examples2d/one_way_platforms2.rs", tbOneWayPlatforms2, NULL}, + {"locked_rotations2", "Dynamics", "Locked rotations", "examples2d/locked_rotations2.rs", tbLockedRotations2, NULL}, + {"restitution2", "Dynamics", "Restitution", "examples2d/restitution2.rs", tbRestitution2, NULL}, + {"damping2", "Dynamics", "Damping", "examples2d/damping2.rs", tbDamping2, NULL}, + {"ccd2", "Dynamics", "CCD", "examples2d/ccd2.rs", tbCcd2, NULL}, + {"joints2", "Joints", "Joints", "examples2d/joints2.rs", tbJoints2, NULL}, + {"rope_joints2", "Joints", "Rope Joints", "examples2d/rope_joints2.rs", tbRopeJoints2, NULL}, + {"pin_slot_joint2", "Joints", "Pin Slot Joint", "examples2d/pin_slot_joint2.rs", tbPinSlotJoint2, NULL}, + {"joint_motor_position2", "Joints", "Joint motor position", "examples2d/joint_motor_position2.rs", tbJointMotorPosition2, NULL}, + {"inverse_kinematics2", "Joints", "Inverse kinematics", "examples2d/inverse_kinematics2.rs", tbInverseKinematics2, NULL}, + {"multi_pendulum2", "Joints", "Multi Pendulum", "examples2d/multi_pendulum2.rs", tbMultiPendulum2, NULL}, + {"soft_bodies2", "Soft bodies", "Soft bodies", "examples2d/soft_bodies2.rs", tbSoftBodies2, NULL}, + {"soft_blobs2", "Soft bodies", "Blobs", "examples2d/soft_blobs2.rs", tbSoftBlobs2, NULL}, + {"soft_jelly2", "Soft bodies", "Jelly", "examples2d/soft_jelly2.rs", tbSoftJelly2, NULL}, + {"soft_surface2", "Soft bodies", "Deformable polylines", "examples2d/soft_surface2.rs", tbSoftSurface2, NULL}, + {"soft_pile2", "Soft bodies", "Soft pile", "examples2d/soft_pile2.rs", tbSoftPile2, NULL}, + {"soft_thin_features2", "Soft bodies", "Thin features", "examples2d/soft_thin_features2.rs", tbSoftThinFeatures2, NULL}, + {"soft_letters2", "Soft bodies", "Soft letters", "examples2d/soft_letters2.rs", tbSoftLetters2, NULL}, + {"soft_plasticity2", "Soft bodies", "Plasticity", "examples2d/soft_plasticity2.rs", tbSoftPlasticity2, NULL}, + {"soft_tearing2", "Soft bodies", "Tearing", "examples2d/soft_tearing2.rs", tbSoftTearing2, NULL}, + {"soft_force_tearing2", "Soft bodies", "Force tearing", "examples2d/soft_force_tearing2.rs", tbSoftForceTearing2, NULL}, + {"soft_cutting2", "Soft bodies", "Cutting", "examples2d/soft_cutting2.rs", tbSoftCutting2, NULL}, + {"soft_stress2", "Soft bodies", "Stress coloring", "examples2d/soft_stress2.rs", tbSoftStress2, NULL}, +#ifdef RAPIER_FEM + {"soft_fem2", "Soft bodies", "Soft FEM", "examples2d/soft_fem2.rs", tbSoftFem2, NULL}, +#else + {"soft_fem2", "Soft bodies", "Soft FEM", "examples2d/soft_fem2.rs", NULL, "Requires RAPIER_FEATURES=fem"}, +#endif + {"soft_joints2", "Soft bodies", "Soft joints", "examples2d/soft_joints2.rs", tbSoftJoints2, NULL}, + {"character_controller2", "Controls", "Character controller", "examples2d/character_controller2.rs", tbCharacterController2, NULL}, + {"debug_angular_limits2", "Debug", "Angular limits", "examples2d/debug_angular_limits2.rs", tbDebugAngularLimits2, NULL}, + {"debug_box_ball2", "Debug", "Box ball", "examples2d/debug_box_ball2.rs", tbDebugBoxBall2, NULL}, + {"debug_compression2", "Debug", "Compression", "examples2d/debug_compression2.rs", tbDebugCompression2, NULL}, + {"debug_intersection2", "Debug", "Intersection", "examples2d/debug_intersection2.rs", tbDebugIntersection2, NULL}, + {"debug_many_colliders2", "Debug", "Many colliders", "examples2d/debug_many_colliders2.rs", tbDebugManyColliders2, NULL}, + {"debug_total_overlap2", "Debug", "Total overlap", "examples2d/debug_total_overlap2.rs", tbDebugTotalOverlap2, NULL}, + {"debug_vertical_column2", "Debug", "Vertical column", "examples2d/debug_vertical_column2.rs", tbDebugVerticalColumn2, NULL}, + {"debug_self_intersect2", "Debug", "Self intersect", "examples2d/debug_self_intersect2.rs", tbDebugSelfIntersect2, NULL}, + {"s2d_high_mass_ratio_1", "Inspired by Solver 2D", "High mass ratio 1", "examples2d/s2d_high_mass_ratio_1.rs", tbS2dHighMassRatio1, NULL}, + {"s2d_high_mass_ratio_2", "Inspired by Solver 2D", "High mass ratio 2", "examples2d/s2d_high_mass_ratio_2.rs", tbS2dHighMassRatio2, NULL}, + {"s2d_high_mass_ratio_3", "Inspired by Solver 2D", "High mass ratio 3", "examples2d/s2d_high_mass_ratio_3.rs", tbS2dHighMassRatio3, NULL}, + {"s2d_confined", "Inspired by Solver 2D", "Confined", "examples2d/s2d_confined.rs", tbS2dConfined, NULL}, + {"s2d_pyramid", "Inspired by Solver 2D", "Pyramid", "examples2d/s2d_pyramid.rs", tbS2dPyramid, NULL}, + {"s2d_card_house", "Inspired by Solver 2D", "Card house", "examples2d/s2d_card_house.rs", tbS2dCardHouse, NULL}, + {"s2d_arch", "Inspired by Solver 2D", "Arch", "examples2d/s2d_arch.rs", tbS2dArch, NULL}, + {"s2d_bridge", "Inspired by Solver 2D", "Bridge", "examples2d/s2d_bridge.rs", tbS2dBridge, NULL}, + {"s2d_ball_and_chain", "Inspired by Solver 2D", "Ball and chain", "examples2d/s2d_ball_and_chain.rs", tbS2dBallAndChain, NULL}, + {"s2d_joint_grid", "Inspired by Solver 2D", "Joint grid", "examples2d/s2d_joint_grid.rs", tbS2dJointGrid, NULL}, + {"s2d_far_pyramid", "Inspired by Solver 2D", "Far pyramid", "examples2d/s2d_far_pyramid.rs", tbS2dFarPyramid, NULL}, + {"b2d_compounds", "Third-party benchmarks", "Compounds", "examples2d/b2d_compounds.rs", tbB2dCompounds, NULL}, + {"b2d_joint_grid", "Third-party benchmarks", "Joint grid", "examples2d/b2d_joint_grid.rs", tbB2dJointGrid, NULL}, + {"b2d_junkyard", "Third-party benchmarks", "Junkyard", "examples2d/b2d_junkyard.rs", tbB2dJunkyard, NULL}, + {"b2d_large_pyramid", "Third-party benchmarks", "Large pyramid", "examples2d/b2d_large_pyramid.rs", tbB2dLargePyramid, NULL}, + {"b2d_many_pyramids", "Third-party benchmarks", "Many pyramids", "examples2d/b2d_many_pyramids.rs", tbB2dManyPyramids, NULL}, + {"b2d_rain", "Third-party benchmarks", "Rain", "examples2d/b2d_rain.rs", tbB2dRain, NULL}, + {"b2d_smash", "Third-party benchmarks", "Smash", "examples2d/b2d_smash.rs", tbB2dSmash, NULL}, + {"b2d_spinner", "Third-party benchmarks", "Spinner", "examples2d/b2d_spinner.rs", tbB2dSpinner, NULL}, + {"b2d_tumbler", "Third-party benchmarks", "Tumbler", "examples2d/b2d_tumbler.rs", tbB2dTumbler, NULL}, + {"b2d_washer", "Third-party benchmarks", "Washer", "examples2d/b2d_washer.rs", tbB2dWasher, NULL}, + {"stress_tests_balls2", "Stress tests", "Balls", "examples2d/stress_tests/balls2.rs", tbStressTestsBalls2, NULL}, + {"stress_tests_boxes2", "Stress tests", "Boxes", "examples2d/stress_tests/boxes2.rs", tbStressTestsBoxes2, NULL}, + {"stress_tests_capsules2", "Stress tests", "Capsules", "examples2d/stress_tests/capsules2.rs", tbStressTestsCapsules2, NULL}, + {"stress_tests_convex_polygons2", "Stress tests", "Convex polygons", "examples2d/stress_tests/convex_polygons2.rs", tbStressTestsConvexPolygons2, NULL}, + {"stress_tests_heightfield2", "Stress tests", "Heightfield", "examples2d/stress_tests/heightfield2.rs", tbStressTestsHeightfield2, NULL}, + {"stress_tests_large_pyramids2", "Stress tests", "Large pyramids", "examples2d/stress_tests/large_pyramids2.rs", tbStressTestsLargePyramids2, NULL}, + {"stress_tests_many_pyramids2", "Stress tests", "Many pyramids", "examples2d/stress_tests/many_pyramids2.rs", tbStressTestsManyPyramids2, NULL}, + {"stress_tests_pyramid2", "Stress tests", "Pyramid", "examples2d/stress_tests/pyramid2.rs", tbStressTestsPyramid2, NULL}, + {"stress_tests_ragdolls2", "Stress tests", "Ragdoll piles", "examples2d/stress_tests/ragdolls2.rs", tbStressTestsRagdolls2, NULL}, + {"stress_tests_ropes2", "Stress tests", "Ropes", "examples2d/stress_tests/ropes2.rs", tbStressTestsRopes2, NULL}, + {"stress_tests_vertical_stacks2", "Stress tests", "Verticals stacks", "examples2d/stress_tests/vertical_stacks2.rs", tbStressTestsVerticalStacks2, NULL}, + {"stress_tests_joint_ball2", "Stress tests", "(Stress test) joint ball", "examples2d/stress_tests/joint_ball2.rs", tbStressTestsJointBall2, NULL}, + {"stress_tests_joint_fixed2", "Stress tests", "(Stress test) joint fixed", "examples2d/stress_tests/joint_fixed2.rs", tbStressTestsJointFixed2, NULL}, + {"stress_tests_joint_prismatic2", "Stress tests", "(Stress test) joint prismatic", "examples2d/stress_tests/joint_prismatic2.rs", tbStressTestsJointPrismatic2, NULL}, + {"stress_tests_soft_blobs2", "Stress tests", "Soft blobs", "examples2d/stress_tests/soft_blobs2.rs", tbStressTestsSoftBlobs2, NULL}, + {"stress_tests_soft_jellies2", "Stress tests", "Soft jellies", "examples2d/stress_tests/soft_jellies2.rs", tbStressTestsSoftJellies2, NULL}, + {"stress_tests_soft_ropes2", "Stress tests", "Soft ropes", "examples2d/stress_tests/soft_ropes2.rs", tbStressTestsSoftRopes2, NULL}, + {"stress_tests_soft_strips2", "Stress tests", "Soft strips", "examples2d/stress_tests/soft_strips2.rs", tbStressTestsSoftStrips2, NULL}, + {"stress_tests_soft_cloth_keva2", "Stress tests", "Soft cloth on Keva tower", "examples2d/stress_tests/soft_cloth_keva2.rs", tbStressTestsSoftClothKeva2, NULL}, + {"stress_tests_soft_slab2", "Stress tests", "Soft slab shower", "examples2d/stress_tests/soft_slab2.rs", tbStressTestsSoftSlab2, NULL}, +#ifdef RAPIER_FEM + {"stress_tests_soft_fem_beams2", "Stress tests", "Soft FEM beams", "examples2d/stress_tests/soft_fem_beams2.rs", tbStressTestsSoftFemBeams2, NULL}, +#else + {"stress_tests_soft_fem_beams2", "Stress tests", "Soft FEM beams", "examples2d/stress_tests/soft_fem_beams2.rs", NULL, "Requires RAPIER_FEATURES=fem"}, +#endif +}; +const size_t tbExampleCount=TB_COUNT(tbExamples); diff --git a/c/testbed/registry3.c b/c/testbed/registry3.c new file mode 100644 index 000000000..59ae96c9e --- /dev/null +++ b/c/testbed/registry3.c @@ -0,0 +1,253 @@ +/* Generated by update_catalog.py; order matches the Rust testbed. */ +#include "testbed.h" +extern void tbFountain3(Testbed *); +extern void tbPrimitives3(Testbed *); +extern void tbKeva3(Testbed *); +extern void tbNewtonCradle3(Testbed *); +extern void tbDomino3(Testbed *); +extern void tbPlatform3(Testbed *); +extern void tbSensor3(Testbed *); +extern void tbCompound3(Testbed *); +extern void tbConvexDecomposition3(Testbed *); +extern void tbConvexPolyhedron3(Testbed *); +extern void tbTrimesh3(Testbed *); +extern void tbDynamicTrimesh3(Testbed *); +extern void tbHeightfield3(Testbed *); +extern void tbVoxels3(Testbed *); +extern void tbCollisionGroups3(Testbed *); +extern void tbOneWayPlatforms3(Testbed *); +extern void tbLockedRotations3(Testbed *); +extern void tbRestitution3(Testbed *); +extern void tbDamping3(Testbed *); +extern void tbGyroscopic3(Testbed *); +extern void tbCcd3(Testbed *); +extern void tbJoints3RunImpulseJoints(Testbed *); +extern void tbJoints3RunMultibodyJoints(Testbed *); +extern void tbRopeJoints3(Testbed *); +extern void tbSpringJoints3(Testbed *); +extern void tbJointMotorPosition3(Testbed *); +extern void tbInverseKinematics3(Testbed *); +extern void tbSoftBodies3(Testbed *); +extern void tbSoftCloth3(Testbed *); +extern void tbSoftJelly3(Testbed *); +extern void tbSoftJoints3(Testbed *); +extern void tbSoftMeshes3(Testbed *); +extern void tbSoftSurface3(Testbed *); +extern void tbSoftPile3(Testbed *); +extern void tbSoftThinFeatures3(Testbed *); +extern void tbSoftClothStress3(Testbed *); +extern void tbSoftPlasticity3(Testbed *); +extern void tbSoftTearing3(Testbed *); +extern void tbSoftDress3(Testbed *); +extern void tbSoftFem3(Testbed *); +extern void tbSoftTrimesh3(Testbed *); +extern void tbCharacterController3(Testbed *); +extern void tbVehicleController3(Testbed *); +extern void tbVehicleJoints3(Testbed *); +extern void tbUrdf3(Testbed *); +extern void tbMjcf3(Testbed *); +extern void tbMujocoMenagerie3(Testbed *); +extern void tbDebugAngularLimits3(Testbed *); +extern void tbDebugArticulations3(Testbed *); +extern void tbDebugAddRemoveCollider3(Testbed *); +extern void tbDebugMultiColliderBody3(Testbed *); +extern void tbDebugBigColliders3(Testbed *); +extern void tbDebugBoxes3(Testbed *); +extern void tbDebugBalls3(Testbed *); +extern void tbDebugDisabled3(Testbed *); +extern void tbDebugTwoCubes3(Testbed *); +extern void tbDebugPop3(Testbed *); +extern void tbDebugDynamicColliderAdd3(Testbed *); +extern void tbDebugFriction3(Testbed *); +extern void tbDebugInternalEdges3(Testbed *); +extern void tbDebugSelfIntersect3(Testbed *); +extern void tbDebugLongChain3(Testbed *); +extern void tbDebugChainHighMassRatio3(Testbed *); +extern void tbDebugCubeHighMassRatio3(Testbed *); +extern void tbDebugTriangle3(Testbed *); +extern void tbDebugTrimesh3(Testbed *); +extern void tbDebugThinCubeOnMesh3(Testbed *); +extern void tbDebugCylinder3(Testbed *); +extern void tbDebugInfiniteFall3(Testbed *); +extern void tbDebugPrismatic3(Testbed *); +extern void tbDebugRollback3(Testbed *); +extern void tbDebugShapeModification3(Testbed *); +extern void tbDebugSleepingKinematic3(Testbed *); +extern void tbDebugDeserialize3(Testbed *); +extern void tbDebugMultibodyAngMotorPos3(Testbed *); +extern void tbStressTestsBalls3(Testbed *); +extern void tbStressTestsBoxes3(Testbed *); +extern void tbStressTestsCapsules3(Testbed *); +extern void tbStressTestsCcd3(Testbed *); +extern void tbStressTestsCompound3(Testbed *); +extern void tbStressTestsConvexPolyhedron3(Testbed *); +extern void tbStressTestsManyKinematics3(Testbed *); +extern void tbStressTestsManyStatic3(Testbed *); +extern void tbStressTestsManySleep3(Testbed *); +extern void tbStressTestsHeightfield3(Testbed *); +extern void tbStressTestsStacks3(Testbed *); +extern void tbStressTestsPyramid3(Testbed *); +extern void tbStressTestsTrimesh3(Testbed *); +extern void tbStressTestsJointBall3(Testbed *); +extern void tbStressTestsJointFixed3(Testbed *); +extern void tbStressTestsJointRevolute3(Testbed *); +extern void tbStressTestsJointPrismatic3(Testbed *); +extern void tbStressTestsRagdolls3(Testbed *); +extern void tbStressTestsRopes3(Testbed *); +extern void tbStressTestsManyPyramids3(Testbed *); +extern void tbStressTestsKeva3(Testbed *); +extern void tbStressTestsRayCast3(Testbed *); +extern void tbStressTestsSoftBlobs3(Testbed *); +extern void tbStressTestsSoftJellies3(Testbed *); +extern void tbStressTestsSoftRopes3(Testbed *); +extern void tbStressTestsSoftClothDrape3(Testbed *); +extern void tbStressTestsSoftClothKeva3(Testbed *); +extern void tbStressTestsSoftSlab3(Testbed *); +extern void tbStressTestsSoftFemBeams3(Testbed *); +extern void tbB3dLargePyramid(Testbed *); +extern void tbB3dManyPyramids(Testbed *); +extern void tbB3dJointGrid(Testbed *); +extern void tbB3dJunkyard(Testbed *); +extern void tbB3dWasher(Testbed *); +extern void tbB3dTreesRun100(Testbed *); +extern void tbB3dTreesRun50(Testbed *); +extern void tbB3dTreesRun25(Testbed *); +extern void tbB3dRain(Testbed *); +extern void tbB3dLargeWorld(Testbed *); +const TbExample tbExamples[] = { + {"fountain3", "Collisions", "Fountain", "examples3d/fountain3.rs", tbFountain3, NULL}, + {"primitives3", "Collisions", "Primitives", "examples3d/primitives3.rs", tbPrimitives3, NULL}, + {"keva3", "Collisions", "Keva tower", "examples3d/keva3.rs", tbKeva3, NULL}, + {"newton_cradle3", "Collisions", "Newton cradle", "examples3d/newton_cradle3.rs", tbNewtonCradle3, NULL}, + {"domino3", "Collisions", "Domino", "examples3d/domino3.rs", tbDomino3, NULL}, + {"platform3", "Collisions", "Platform", "examples3d/platform3.rs", tbPlatform3, NULL}, + {"sensor3", "Collisions", "Sensor", "examples3d/sensor3.rs", tbSensor3, NULL}, + {"compound3", "Collisions", "Compound", "examples3d/compound3.rs", tbCompound3, NULL}, + {"convex_decomposition3", "Collisions", "Convex decomposition", "examples3d/convex_decomposition3.rs", tbConvexDecomposition3, NULL}, + {"convex_polyhedron3", "Collisions", "Convex polyhedron", "examples3d/convex_polyhedron3.rs", tbConvexPolyhedron3, NULL}, + {"trimesh3", "Collisions", "TriMesh", "examples3d/trimesh3.rs", tbTrimesh3, NULL}, + {"dynamic_trimesh3", "Collisions", "Dynamic trimeshes", "examples3d/dynamic_trimesh3.rs", tbDynamicTrimesh3, NULL}, + {"heightfield3", "Collisions", "Heightfield", "examples3d/heightfield3.rs", tbHeightfield3, NULL}, + {"voxels3", "Collisions", "Voxels", "examples3d/voxels3.rs", tbVoxels3, NULL}, + {"collision_groups3", "Collisions", "Collision groups", "examples3d/collision_groups3.rs", tbCollisionGroups3, NULL}, + {"one_way_platforms3", "Collisions", "One-way platforms", "examples3d/one_way_platforms3.rs", tbOneWayPlatforms3, NULL}, + {"locked_rotations3", "Dynamics", "Locked rotations", "examples3d/locked_rotations3.rs", tbLockedRotations3, NULL}, + {"restitution3", "Dynamics", "Restitution", "examples3d/restitution3.rs", tbRestitution3, NULL}, + {"damping3", "Dynamics", "Damping", "examples3d/damping3.rs", tbDamping3, NULL}, + {"gyroscopic3", "Dynamics", "Gyroscopic", "examples3d/gyroscopic3.rs", tbGyroscopic3, NULL}, + {"ccd3", "Dynamics", "CCD", "examples3d/ccd3.rs", tbCcd3, NULL}, + {"joints3_run_impulse_joints", "Joints", "Impulse Joints", "examples3d/joints3.rs", tbJoints3RunImpulseJoints, NULL}, + {"joints3_run_multibody_joints", "Joints", "Multibody Joints", "examples3d/joints3.rs", tbJoints3RunMultibodyJoints, NULL}, + {"rope_joints3", "Joints", "Rope Joints", "examples3d/rope_joints3.rs", tbRopeJoints3, NULL}, + {"spring_joints3", "Joints", "Spring Joints", "examples3d/spring_joints3.rs", tbSpringJoints3, NULL}, + {"joint_motor_position3", "Joints", "Joint Motor Position", "examples3d/joint_motor_position3.rs", tbJointMotorPosition3, NULL}, + {"inverse_kinematics3", "Joints", "Inverse kinematics", "examples3d/inverse_kinematics3.rs", tbInverseKinematics3, NULL}, + {"soft_bodies3", "Soft bodies", "Soft bodies", "examples3d/soft_bodies3.rs", tbSoftBodies3, NULL}, + {"soft_cloth3", "Soft bodies", "Cloth", "examples3d/soft_cloth3.rs", tbSoftCloth3, NULL}, + {"soft_jelly3", "Soft bodies", "Jelly", "examples3d/soft_jelly3.rs", tbSoftJelly3, NULL}, + {"soft_joints3", "Soft bodies", "Soft joints", "examples3d/soft_joints3.rs", tbSoftJoints3, NULL}, + {"soft_meshes3", "Soft bodies", "Cluster meshes", "examples3d/soft_meshes3.rs", tbSoftMeshes3, NULL}, + {"soft_surface3", "Soft bodies", "Deformable trimeshes", "examples3d/soft_surface3.rs", tbSoftSurface3, NULL}, + {"soft_pile3", "Soft bodies", "Soft pile", "examples3d/soft_pile3.rs", tbSoftPile3, NULL}, + {"soft_thin_features3", "Soft bodies", "Thin features", "examples3d/soft_thin_features3.rs", tbSoftThinFeatures3, NULL}, + {"soft_cloth_stress3", "Soft bodies", "Cloth stress", "examples3d/soft_cloth_stress3.rs", tbSoftClothStress3, NULL}, + {"soft_plasticity3", "Soft bodies", "Plasticity", "examples3d/soft_plasticity3.rs", tbSoftPlasticity3, NULL}, + {"soft_tearing3", "Soft bodies", "Tearing", "examples3d/soft_tearing3.rs", tbSoftTearing3, NULL}, + {"soft_dress3", "Soft bodies", "Dancing dress", "examples3d/soft_dress3.rs", tbSoftDress3, NULL}, +#ifdef RAPIER_FEM + {"soft_fem3", "Soft bodies", "Soft FEM", "examples3d/soft_fem3.rs", tbSoftFem3, NULL}, +#else + {"soft_fem3", "Soft bodies", "Soft FEM", "examples3d/soft_fem3.rs", NULL, "Requires RAPIER_FEATURES=fem"}, +#endif + {"soft_trimesh3", "Soft bodies", "Soft trimeshes", "examples3d/soft_trimesh3.rs", tbSoftTrimesh3, NULL}, + {"character_controller3", "Controls", "Character controller", "examples3d/character_controller3.rs", tbCharacterController3, NULL}, + {"vehicle_controller3", "Controls", "Vehicle controller", "examples3d/vehicle_controller3.rs", tbVehicleController3, NULL}, + {"vehicle_joints3", "Controls", "Vehicle joints", "examples3d/vehicle_joints3.rs", tbVehicleJoints3, NULL}, +#ifdef RAPIER_ROBOTICS + {"urdf3", "Robotics", "URDF", "examples3d/urdf3.rs", tbUrdf3, NULL}, +#else + {"urdf3", "Robotics", "URDF", "examples3d/urdf3.rs", NULL, "Requires RAPIER_FEATURES=robotics"}, +#endif +#ifdef RAPIER_ROBOTICS + {"mjcf3", "Robotics", "MJCF", "examples3d/mjcf3.rs", tbMjcf3, NULL}, +#else + {"mjcf3", "Robotics", "MJCF", "examples3d/mjcf3.rs", NULL, "Requires RAPIER_FEATURES=robotics"}, +#endif +#ifdef RAPIER_ROBOTICS + {"mujoco_menagerie3", "Robotics", "Mujoco Menagerie", "examples3d/mujoco_menagerie3.rs", tbMujocoMenagerie3, NULL}, +#else + {"mujoco_menagerie3", "Robotics", "Mujoco Menagerie", "examples3d/mujoco_menagerie3.rs", NULL, "Requires RAPIER_FEATURES=robotics"}, +#endif + {"debug_angular_limits3", "Debug", "Angular limits", "examples3d/debug_angular_limits3.rs", tbDebugAngularLimits3, NULL}, + {"debug_articulations3", "Debug", "Multibody joints", "examples3d/debug_articulations3.rs", tbDebugArticulations3, NULL}, + {"debug_add_remove_collider3", "Debug", "Add/rm collider", "examples3d/debug_add_remove_collider3.rs", tbDebugAddRemoveCollider3, NULL}, + {"debug_multi_collider_body3", "Debug", "Multi-collider body", "examples3d/debug_multi_collider_body3.rs", tbDebugMultiColliderBody3, NULL}, + {"debug_big_colliders3", "Debug", "Big colliders", "examples3d/debug_big_colliders3.rs", tbDebugBigColliders3, NULL}, + {"debug_boxes3", "Debug", "Boxes", "examples3d/debug_boxes3.rs", tbDebugBoxes3, NULL}, + {"debug_balls3", "Debug", "Balls", "examples3d/debug_balls3.rs", tbDebugBalls3, NULL}, + {"debug_disabled3", "Debug", "Disabled", "examples3d/debug_disabled3.rs", tbDebugDisabled3, NULL}, + {"debug_two_cubes3", "Debug", "Two cubes", "examples3d/debug_two_cubes3.rs", tbDebugTwoCubes3, NULL}, + {"debug_pop3", "Debug", "Pop", "examples3d/debug_pop3.rs", tbDebugPop3, NULL}, + {"debug_dynamic_collider_add3", "Debug", "Dyn. collider add", "examples3d/debug_dynamic_collider_add3.rs", tbDebugDynamicColliderAdd3, NULL}, + {"debug_friction3", "Debug", "Friction", "examples3d/debug_friction3.rs", tbDebugFriction3, NULL}, + {"debug_internal_edges3", "Debug", "Internal edges", "examples3d/debug_internal_edges3.rs", tbDebugInternalEdges3, NULL}, + {"debug_self_intersect3", "Debug", "Self intersect", "examples3d/debug_self_intersect3.rs", tbDebugSelfIntersect3, NULL}, + {"debug_long_chain3", "Debug", "Long chain", "examples3d/debug_long_chain3.rs", tbDebugLongChain3, NULL}, + {"debug_chain_high_mass_ratio3", "Debug", "High mass ratio: chain", "examples3d/debug_chain_high_mass_ratio3.rs", tbDebugChainHighMassRatio3, NULL}, + {"debug_cube_high_mass_ratio3", "Debug", "High mass ratio: cube", "examples3d/debug_cube_high_mass_ratio3.rs", tbDebugCubeHighMassRatio3, NULL}, + {"debug_triangle3", "Debug", "Triangle", "examples3d/debug_triangle3.rs", tbDebugTriangle3, NULL}, + {"debug_trimesh3", "Debug", "Trimesh", "examples3d/debug_trimesh3.rs", tbDebugTrimesh3, NULL}, + {"debug_thin_cube_on_mesh3", "Debug", "Thin cube", "examples3d/debug_thin_cube_on_mesh3.rs", tbDebugThinCubeOnMesh3, NULL}, + {"debug_cylinder3", "Debug", "Cylinder", "examples3d/debug_cylinder3.rs", tbDebugCylinder3, NULL}, + {"debug_infinite_fall3", "Debug", "Infinite fall", "examples3d/debug_infinite_fall3.rs", tbDebugInfiniteFall3, NULL}, + {"debug_prismatic3", "Debug", "Prismatic", "examples3d/debug_prismatic3.rs", tbDebugPrismatic3, NULL}, + {"debug_rollback3", "Debug", "Rollback", "examples3d/debug_rollback3.rs", tbDebugRollback3, NULL}, + {"debug_shape_modification3", "Debug", "Shape modification", "examples3d/debug_shape_modification3.rs", tbDebugShapeModification3, NULL}, + {"debug_sleeping_kinematic3", "Debug", "Sleeping kinematics", "examples3d/debug_sleeping_kinematic3.rs", tbDebugSleepingKinematic3, NULL}, + {"debug_deserialize3", "Debug", "Deserialize", "examples3d/debug_deserialize3.rs", tbDebugDeserialize3, NULL}, + {"debug_multibody_ang_motor_pos3", "Debug", "Multibody ang. motor pos.", "examples3d/debug_multibody_ang_motor_pos3.rs", tbDebugMultibodyAngMotorPos3, NULL}, + {"stress_tests_balls3", "Stress tests", "Balls", "examples3d/stress_tests/balls3.rs", tbStressTestsBalls3, NULL}, + {"stress_tests_boxes3", "Stress tests", "Boxes", "examples3d/stress_tests/boxes3.rs", tbStressTestsBoxes3, NULL}, + {"stress_tests_capsules3", "Stress tests", "Capsules", "examples3d/stress_tests/capsules3.rs", tbStressTestsCapsules3, NULL}, + {"stress_tests_ccd3", "Stress tests", "CCD", "examples3d/stress_tests/ccd3.rs", tbStressTestsCcd3, NULL}, + {"stress_tests_compound3", "Stress tests", "Compound", "examples3d/stress_tests/compound3.rs", tbStressTestsCompound3, NULL}, + {"stress_tests_convex_polyhedron3", "Stress tests", "Convex polyhedron", "examples3d/stress_tests/convex_polyhedron3.rs", tbStressTestsConvexPolyhedron3, NULL}, + {"stress_tests_many_kinematics3", "Stress tests", "Many kinematics", "examples3d/stress_tests/many_kinematics3.rs", tbStressTestsManyKinematics3, NULL}, + {"stress_tests_many_static3", "Stress tests", "Many static", "examples3d/stress_tests/many_static3.rs", tbStressTestsManyStatic3, NULL}, + {"stress_tests_many_sleep3", "Stress tests", "Many sleep", "examples3d/stress_tests/many_sleep3.rs", tbStressTestsManySleep3, NULL}, + {"stress_tests_heightfield3", "Stress tests", "Heightfield", "examples3d/stress_tests/heightfield3.rs", tbStressTestsHeightfield3, NULL}, + {"stress_tests_stacks3", "Stress tests", "Stacks", "examples3d/stress_tests/stacks3.rs", tbStressTestsStacks3, NULL}, + {"stress_tests_pyramid3", "Stress tests", "Pyramid", "examples3d/stress_tests/pyramid3.rs", tbStressTestsPyramid3, NULL}, + {"stress_tests_trimesh3", "Stress tests", "Trimesh", "examples3d/stress_tests/trimesh3.rs", tbStressTestsTrimesh3, NULL}, + {"stress_tests_joint_ball3", "Stress tests", "ImpulseJoint ball", "examples3d/stress_tests/joint_ball3.rs", tbStressTestsJointBall3, NULL}, + {"stress_tests_joint_fixed3", "Stress tests", "ImpulseJoint fixed", "examples3d/stress_tests/joint_fixed3.rs", tbStressTestsJointFixed3, NULL}, + {"stress_tests_joint_revolute3", "Stress tests", "ImpulseJoint revolute", "examples3d/stress_tests/joint_revolute3.rs", tbStressTestsJointRevolute3, NULL}, + {"stress_tests_joint_prismatic3", "Stress tests", "ImpulseJoint prismatic", "examples3d/stress_tests/joint_prismatic3.rs", tbStressTestsJointPrismatic3, NULL}, + {"stress_tests_ragdolls3", "Stress tests", "Ragdoll piles", "examples3d/stress_tests/ragdolls3.rs", tbStressTestsRagdolls3, NULL}, + {"stress_tests_ropes3", "Stress tests", "Ropes", "examples3d/stress_tests/ropes3.rs", tbStressTestsRopes3, NULL}, + {"stress_tests_many_pyramids3", "Stress tests", "Many pyramids", "examples3d/stress_tests/many_pyramids3.rs", tbStressTestsManyPyramids3, NULL}, + {"stress_tests_keva3", "Stress tests", "Keva tower", "examples3d/stress_tests/keva3.rs", tbStressTestsKeva3, NULL}, + {"stress_tests_ray_cast3", "Stress tests", "Ray cast", "examples3d/stress_tests/ray_cast3.rs", tbStressTestsRayCast3, NULL}, + {"stress_tests_soft_blobs3", "Stress tests", "Soft blobs", "examples3d/stress_tests/soft_blobs3.rs", tbStressTestsSoftBlobs3, NULL}, + {"stress_tests_soft_jellies3", "Stress tests", "Soft jellies", "examples3d/stress_tests/soft_jellies3.rs", tbStressTestsSoftJellies3, NULL}, + {"stress_tests_soft_ropes3", "Stress tests", "Soft ropes", "examples3d/stress_tests/soft_ropes3.rs", tbStressTestsSoftRopes3, NULL}, + {"stress_tests_soft_cloth_drape3", "Stress tests", "Soft cloth drape", "examples3d/stress_tests/soft_cloth_drape3.rs", tbStressTestsSoftClothDrape3, NULL}, + {"stress_tests_soft_cloth_keva3", "Stress tests", "Soft cloth on Keva tower", "examples3d/stress_tests/soft_cloth_keva3.rs", tbStressTestsSoftClothKeva3, NULL}, + {"stress_tests_soft_slab3", "Stress tests", "Soft slab shower", "examples3d/stress_tests/soft_slab3.rs", tbStressTestsSoftSlab3, NULL}, +#ifdef RAPIER_FEM + {"stress_tests_soft_fem_beams3", "Stress tests", "Soft FEM beams", "examples3d/stress_tests/soft_fem_beams3.rs", tbStressTestsSoftFemBeams3, NULL}, +#else + {"stress_tests_soft_fem_beams3", "Stress tests", "Soft FEM beams", "examples3d/stress_tests/soft_fem_beams3.rs", NULL, "Requires RAPIER_FEATURES=fem"}, +#endif + {"b3d_large_pyramid", "Third-party benchmarks", "Large pyramid", "examples3d/b3d_large_pyramid.rs", tbB3dLargePyramid, NULL}, + {"b3d_many_pyramids", "Third-party benchmarks", "Many pyramids", "examples3d/b3d_many_pyramids.rs", tbB3dManyPyramids, NULL}, + {"b3d_joint_grid", "Third-party benchmarks", "Joint grid", "examples3d/b3d_joint_grid.rs", tbB3dJointGrid, NULL}, + {"b3d_junkyard", "Third-party benchmarks", "Junkyard", "examples3d/b3d_junkyard.rs", tbB3dJunkyard, NULL}, + {"b3d_washer", "Third-party benchmarks", "Washer", "examples3d/b3d_washer.rs", tbB3dWasher, NULL}, + {"b3d_trees_run100", "Third-party benchmarks", "Trees 100", "examples3d/b3d_trees.rs", tbB3dTreesRun100, NULL}, + {"b3d_trees_run50", "Third-party benchmarks", "Trees 50", "examples3d/b3d_trees.rs", tbB3dTreesRun50, NULL}, + {"b3d_trees_run25", "Third-party benchmarks", "Trees 25", "examples3d/b3d_trees.rs", tbB3dTreesRun25, NULL}, + {"b3d_rain", "Third-party benchmarks", "Rain", "examples3d/b3d_rain.rs", tbB3dRain, NULL}, + {"b3d_large_world", "Third-party benchmarks", "Large world", "examples3d/b3d_large_world.rs", tbB3dLargeWorld, NULL}, +}; +const size_t tbExampleCount=TB_COUNT(tbExamples); diff --git a/c/testbed/testbed.c b/c/testbed/testbed.c new file mode 100644 index 000000000..ef76e2498 --- /dev/null +++ b/c/testbed/testbed.c @@ -0,0 +1,361 @@ +#include "testbed_internal.h" +#include "testbed.h" +#include +#include +#include + +void tbCheck(Testbed *t, RAPIER_TYPE(Status) s, const char *call, const char *file, int line) { + if (s == RAPIER_CONST(OK)) { + return; + } + snprintf(t->error, sizeof(t->error), "%s:%d: %s: %s", file, line, call, RAPIER_FN(LastError)()); + longjmp(t->failure, 1); +} + +void tbDestroy(Testbed *t) { + for (size_t i = 0; i < t->renderMeshCount; ++i) { + TbRenderMesh *mesh = &t->renderMeshes[i]; + free(mesh->vertices); + free(mesh->indices); + free(mesh->uvs); + free(mesh->normals); + free(mesh->texture); + } + free(t->renderMeshes); + t->renderMeshes = NULL; + t->renderMeshCount = 0; + free(t->bodyColors); + free(t->colliderColors); + t->bodyColors = t->colliderColors = NULL; + t->bodyColorCount = t->colliderColorCount = 0; + t->world = NULL; + t->activeThreads = 0; +} + +int tbParseThreads(const char *text, size_t *out) { + if (!text || !*text || !out) { + return 0; + } + for (const char *p = text; *p; p++) { + if (*p < '0' || *p > '9') { + return 0; + } + } + char *end; + errno = 0; + unsigned long value = strtoul(text, &end, 10); + if (errno || *end || value > TB_MAX_THREADS) { + return 0; + } + *out = (size_t)value; + return 1; +} + +RAPIER_TYPE(Status) tbSetThreads(Testbed *t, size_t count) { + RAPIER_TYPE(BuildFeatures) features = RAPIER_FN(BuildFeatures)(); + RAPIER_TYPE(Status) status; + size_t active = 1; + if (features.parallel || count > 1) { + status = RAPIER_FN(SetNumThreads)(t->world, count); + if (status != RAPIER_CONST(OK)) { + return status; + } + active = RAPIER_FN(NumThreads)(t->world); + status = RAPIER_FN(LastStatus)(); + if (status != RAPIER_CONST(OK)) { + return status; + } + } + t->requestedThreads = count; + t->activeThreads = active; + t->buildFeatures = features; + return RAPIER_CONST(OK); +} + +void tbRefreshWorld(Testbed *t) { + t->buildFeatures = RAPIER_FN(BuildFeatures)(); + TB(t, tbSetThreads(t, t->requestedThreads)); + TB(t, RAPIER_FN(SetCountersEnabled)(t->world, 1)); + t->physicsStepMs = 0; +} + +static void sceneError(RAPIER_TYPE(Status) status, const char *message, void *userData) { + const Testbed *t = userData; + fprintf(stderr, "Rapier error in %s (status %u): %s\n", t->example->id, (unsigned)status, + message); + /* Do not longjmp out of a Rust frame. Stop before using a failed call's outputs. */ + exit(EXIT_FAILURE); +} + +int tbRun(Testbed *t, const TbExample *e, int preserve) { + tbDestroy(t); + t->error[0] = 0; + t->example = e; + t->step = 0; + t->time = 0; + t->randomState = 42; + t->framePending = 0; + t->snapshotSupported = 1; + t->inputDirection = V(0, 0, 0); + t->action = 0; + t->cutting = 0; + t->cursorValid = 0; + t->jump = t->descend = t->slow = t->boost = 0; + t->cameraRight = V(1, 0, 0); + t->cameraForward = V(0, 0, -1); + t->initialDebug = 0; + t->collidersVisible = 1; + t->frameAll = 0; + t->preserveCamera = 0; + t->up[0] = 0; + t->up[1] = 1; + t->up[2] = 0; + t->rayValid = t->removeVoxel = 0; + t->lineCount = 0; + t->labelCount = 0; + if (!preserve) { + t->settingCount = 0; + } + if (e->requires) { + snprintf(t->error, sizeof(t->error), "%s", e->requires); + return 0; + } + t->previousErrorHandler = + RAPIER_FN(SetErrorHandler)((RAPIER_TYPE(ErrorHandler)){sceneError, t}); + if (setjmp(t->failure)) { + RAPIER_FN(SetErrorHandler)(t->previousErrorHandler); + (void)RAPIER_FN(FreeWorld)(t->world); + t->world = NULL; + return 0; + } + tbCamera(t, 20, 15, 25, 0, 3, 0); + t->viewWidth = 30; + e->run(t); + RAPIER_FN(SetErrorHandler)(t->previousErrorHandler); + t->world = NULL; + return !t->error[0]; +} + +int tbRenderFrame(Testbed *t, RAPIER_TYPE(World) **world) { + /* UI operations handle recoverable errors themselves. The example resumes + * with its fail-fast handler after rendering and input have completed. */ + RAPIER_TYPE(ErrorHandler) sceneHandler = RAPIER_FN(SetErrorHandler)(t->previousErrorHandler); + if (setjmp(t->failure)) { + RAPIER_FN(SetErrorHandler)(sceneHandler); + *world = t->world; + return 0; + } + if (t->framePending) { + t->step++; + t->time += t->frameDt; + t->physicsStepMs = RAPIER_FN(StepTimeMs)(t->world); + TB(t, RAPIER_FN(LastStatus)()); + } + t->framePending = 0; + t->simulating = 0; + int keepOpen = t->renderFrame && t->renderFrame(t, t->viewer); + *world = t->world; + if (keepOpen && t->simulating && !t->error[0]) { + t->frameDt = RAPIER_FN(TimeStep)(t->world); + RAPIER_TYPE(Status) status = RAPIER_FN(LastStatus)(); + if (status != RAPIER_CONST(OK)) { + snprintf(t->error, sizeof(t->error), "%s", RAPIER_FN(LastError)()); + keepOpen = 0; + } + } + RAPIER_FN(SetErrorHandler)(sceneHandler); + return keepOpen; +} + +int tbSimulating(Testbed *t) { + t->framePending = t->simulating && !t->error[0]; + return t->framePending; +} + +void tbCamera(Testbed *t, float x, float y, float z, float tx, float ty, float tz) { + t->eye[0] = x; + t->eye[1] = y; + t->eye[2] = z; + t->target[0] = tx; + t->target[1] = ty; + t->target[2] = tz; +} + +void tbCamera2(Testbed *t, float x, float y, float zoom) { + tbCamera(t, x, y, 100, x, y, 0); + t->viewWidth = 1440.0f / zoom; +} + +double tbSetting(Testbed *t, const char *name, double initial, double min, double max, + int integer) { + for (size_t i = 0; i < t->settingCount; i++) { + if (!strcmp(t->settings[i].name, name)) { + return t->settings[i].value; + } + } + if (t->settingCount == TB_COUNT(t->settings)) { + snprintf(t->error, sizeof(t->error), "too many settings"); + longjmp(t->failure, 1); + } + t->settings[t->settingCount++] = (TbSetting){.name = name, + .value = initial, + .initial = initial, + .min = min, + .max = max, + .integer = integer}; + return initial; +} + +double tbLiveSetting(Testbed *t, const char *name, double initial, double min, double max, + int integer) { + double value = tbSetting(t, name, initial, min, max, integer); + for (size_t i = 0; i < t->settingCount; ++i) { + if (!strcmp(t->settings[i].name, name)) { + t->settings[i].live = 1; + } + } + return value; +} + +static void tint(TbTint **colors, size_t *count, uint32_t index, uint32_t generation, float r, + float g, float b, float a) { + if (index == UINT32_MAX) { + return; + } + if (index >= *count) { + size_t n = (size_t)index + 16; + TbTint *p = realloc(*colors, n * sizeof(*p)); + if (!p) { + abort(); + } + memset(p + *count, 0, (n - *count) * sizeof(*p)); + *colors = p; + *count = n; + } + (*colors)[index] = (TbTint){generation, {r, g, b, a}, 1}; +} + +void tbBodyColor(Testbed *t, RAPIER_TYPE(RigidBodyHandle) h, float r, float g, float b, float a) { + tint(&t->bodyColors, &t->bodyColorCount, h.index, h.generation, r, g, b, a); +} + +void tbColliderColor(Testbed *t, RAPIER_TYPE(ColliderHandle) h, float r, float g, float b, + float a) { + tint(&t->colliderColors, &t->colliderColorCount, h.index, h.generation, r, g, b, a); +} + +const float *tbFindColor(Testbed *t, RAPIER_TYPE(RigidBodyHandle) b, + RAPIER_TYPE(ColliderHandle) c) { + if (c.index < t->colliderColorCount && t->colliderColors[c.index].valid && + t->colliderColors[c.index].generation == c.generation) { + return t->colliderColors[c.index].rgba; + } + if (b.index < t->bodyColorCount && t->bodyColors[b.index].valid && + t->bodyColors[b.index].generation == b.generation) { + return t->bodyColors[b.index].rgba; + } + return NULL; +} + +void tbSetWorld(Testbed *testbed, RAPIER_TYPE(World) *world) { + testbed->world = world; + tbRefreshWorld(testbed); +} + +void tbLine(Testbed *t, RAPIER_TYPE(Vector) a, RAPIER_TYPE(Vector) b, float r, float g, float blue, + float alpha) { + if (t->lineCount == TB_COUNT(t->lines)) { + return; + } + size_t i = t->lineCount++; + t->lines[i].a = a; + t->lines[i].b = b; + t->lines[i].rgba[0] = r; + t->lines[i].rgba[1] = g; + t->lines[i].rgba[2] = blue; + t->lines[i].rgba[3] = alpha; +} + +void tbLabel(Testbed *t, const char *name, const char *value) { + size_t i = 0; + for (; i < t->labelCount; ++i) { + if (!strcmp(t->labels[i].name, name)) { + break; + } + } + if (i == TB_COUNT(t->labels)) { + return; + } + if (i == t->labelCount) { + ++t->labelCount; + } + t->labels[i].name = name; + snprintf(t->labels[i].value, sizeof(t->labels[i].value), "%s", value); +} +#ifdef _WIN32 +#include + +double tbClock(void) { + LARGE_INTEGER frequency, counter; + QueryPerformanceFrequency(&frequency); + QueryPerformanceCounter(&counter); + return (double)counter.QuadPart / frequency.QuadPart; +} +#else +#include + +double tbClock(void) { + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return time.tv_sec + time.tv_nsec * 1e-9; +} +#endif + +size_t tbChoice(Testbed *t, const char *name, size_t initial, const char *const *choices, + size_t count, int live, int reset) { + tbSetting(t, name, initial, 0, count ? count - 1 : 0, 1); + for (size_t i = 0; i < t->settingCount; ++i) { + TbSetting *s = &t->settings[i]; + if (strcmp(s->name, name)) { + continue; + } + s->choices = choices; + s->choiceCount = count; + s->live = live; + s->max = count ? count - 1 : 0; + if (reset || s->value > s->max) { + s->value = initial; + } + return (size_t)s->value; + } + return initial; +} + +static void *copyRenderData(const void *data, size_t bytes) { + if (!data || !bytes) { + return NULL; + } + void *copy = malloc(bytes); + if (!copy) { + abort(); + } + memcpy(copy, data, bytes); + return copy; +} + +void tbAddBodyRenderMesh(Testbed *t, const TbRenderMesh *source) { + TbRenderMesh *meshes = realloc(t->renderMeshes, (t->renderMeshCount + 1) * sizeof(*meshes)); + if (!meshes) { + abort(); + } + t->renderMeshes = meshes; + TbRenderMesh *mesh = &meshes[t->renderMeshCount++]; + *mesh = *source; + mesh->vertices = + copyRenderData(source->vertices, source->vertexCount * sizeof(*source->vertices)); + mesh->indices = copyRenderData(source->indices, source->indexCount * sizeof(*source->indices)); + mesh->uvs = copyRenderData(source->uvs, source->vertexCount * 2 * sizeof(float)); + mesh->normals = copyRenderData(source->normals, source->vertexCount * 3 * sizeof(float)); + mesh->texture = + source->texture ? copyRenderData(source->texture, strlen(source->texture) + 1) : NULL; +} diff --git a/c/testbed/testbed.h b/c/testbed/testbed.h new file mode 100644 index 000000000..43f6d145e --- /dev/null +++ b/c/testbed/testbed.h @@ -0,0 +1,138 @@ +#ifndef RAPIER_TESTBED_H +#define RAPIER_TESTBED_H +#include "rapier.h" +#include +#include +#include +#include +#include +#define TB_PI ((RAPIER_TYPE(Real))3.14159265358979323846) +#define TB_MAX_THREADS 256 +#define TB_COUNT(a) (sizeof(a) / sizeof((a)[0])) +#if defined(RAPIER_DIM2) +#define V(x, y, ...) ((RAPIER_TYPE(Vector)){(RAPIER_TYPE(Real))(x), (RAPIER_TYPE(Real))(y)}) +#else +#define V(x, y, z) ((RAPIER_TYPE(Vector)){(RAPIER_TYPE(Real))(x), (RAPIER_TYPE(Real))(y), (RAPIER_TYPE(Real))(z)}) +#endif +typedef struct Testbed Testbed; + +typedef struct TbSetting { + const char *name; + double value, initial, min, max; + int integer, live; + const char *const *choices; + size_t choiceCount; +} TbSetting; + +typedef struct TbExample { + const char *id, *group, *name, *source; + void (*run)(Testbed *); + const char * + requires; +} TbExample; + +typedef struct TbTint { + uint32_t generation; + float rgba[4]; + int valid; +} TbTint; + +/* Render-only body geometry. Arrays are copied by tbAddBodyRenderMesh. */ +typedef struct TbRenderMesh { + RAPIER_TYPE(RigidBodyHandle) body; + RAPIER_TYPE(Pose) localPose; + RAPIER_TYPE(Vector) *vertices; + uint32_t *indices; + size_t vertexCount, indexCount; + float *uvs, *normals; + char *texture; + float rgba[4], metallic, roughness, reflectance, emissive[3]; +} TbRenderMesh; + +#ifdef _MSC_VER +#pragma warning(push) +/* Windows jmp_buf requires 16-byte alignment; padding here is intentional. */ +#pragma warning(disable : 4324) +#endif +struct Testbed { + TbTint *bodyColors, *colliderColors; + size_t bodyColorCount, colliderColorCount; + RAPIER_TYPE(World) *world; + double physicsStepMs; + /* The renderer returns control to the example once per frame. */ + int (*renderFrame)(Testbed *, void *); + void *viewer; + int simulating, framePending, snapshotSupported; + RAPIER_TYPE(Real) frameDt; + RAPIER_TYPE(ErrorHandler) previousErrorHandler; + jmp_buf failure; + char error[1024]; + const TbExample *example; + uint64_t step, randomState; + double time; + int noSleep; + size_t requestedThreads, activeThreads; + RAPIER_TYPE(BuildFeatures) buildFeatures; + float eye[3], target[3], up[3], viewWidth; + int collidersVisible, frameAll, preserveCamera; + TbRenderMesh *renderMeshes; + size_t renderMeshCount; + RAPIER_TYPE(Vector) inputDirection; + int action, cutting, cursorValid, jump, descend, slow, boost; + RAPIER_TYPE(Vector) cameraRight, cameraForward; + RAPIER_TYPE(Vector) cursor, rayOrigin, rayDirection; + int rayValid, removeVoxel; + uint32_t initialDebug; + + struct { + RAPIER_TYPE(Vector) a, b; + float rgba[4]; + } lines[256]; + + size_t lineCount; + + struct { + const char *name; + char value[256]; + } labels[32]; + + size_t labelCount; + TbSetting settings[64]; + size_t settingCount; + const char *assetRoot; +}; +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +double tbClock(void); +size_t tbChoice(Testbed *, const char *, size_t, const char *const *, size_t, int live, int reset); +void tbAddBodyRenderMesh(Testbed *, const TbRenderMesh *); +void tbLabel(Testbed *, const char *, const char *); +void tbLine(Testbed *, RAPIER_TYPE(Vector), RAPIER_TYPE(Vector), float, float, float, float); +void tbBodyColor(Testbed *, RAPIER_TYPE(RigidBodyHandle), float, float, float, float); +void tbColliderColor(Testbed *, RAPIER_TYPE(ColliderHandle), float, float, float, float); +const float *tbFindColor(Testbed *, RAPIER_TYPE(RigidBodyHandle), RAPIER_TYPE(ColliderHandle)); +extern const TbExample tbExamples[]; +extern const size_t tbExampleCount; +void tbDestroy(Testbed *); +int tbRun(Testbed *, const TbExample *, int preserveSettings); +/* Render one frame; update *world if the viewer restored a snapshot. */ +int tbRenderFrame(Testbed *, RAPIER_TYPE(World) **world); +int tbSimulating(Testbed *); +void tbRefreshWorld(Testbed *); +/* Borrow a scene world for rendering and controls; the example owns its lifetime. */ +void tbSetWorld(Testbed *, RAPIER_TYPE(World) *); +RAPIER_TYPE(Status) tbSetThreads(Testbed *, size_t); +int tbParseThreads(const char *, size_t *); +void tbCamera(Testbed *, float, float, float, float, float, float); +void tbCamera2(Testbed *, float, float, float); +double tbSetting(Testbed *, const char *, double, double, double, int); + +/* A setting read each frame by the example, applied without restarting. */ +double tbLiveSetting(Testbed *, const char *, double, double, double, int); + +int tbValidate(Testbed *, size_t *, size_t *, size_t *); +int tbHeadless(int, char **); +int tbGui(int, char **); +#endif diff --git a/c/testbed/testbed_internal.h b/c/testbed/testbed_internal.h new file mode 100644 index 000000000..46f7bf008 --- /dev/null +++ b/c/testbed/testbed_internal.h @@ -0,0 +1,7 @@ +#ifndef RAPIER_TESTBED_INTERNAL_H +#define RAPIER_TESTBED_INTERNAL_H +#include "testbed.h" +void tbCheck(Testbed *, RAPIER_TYPE(Status), const char *, const char *, int); +/* Checked viewer operations return to a C-only recovery boundary. */ +#define TB(t, call) tbCheck((t), (call), #call, __FILE__, __LINE__) +#endif diff --git a/c/testbed/tests/errors.c b/c/testbed/tests/errors.c new file mode 100644 index 000000000..40d182725 --- /dev/null +++ b/c/testbed/tests/errors.c @@ -0,0 +1,46 @@ +#include "testbed.h" +#include "rapier_helpers.h" + +static int renderFrame(Testbed *testbed, void *context) { + (void)context; + testbed->simulating = 1; + return 1; +} + +static void invalidCall(void) { + RAPIER_TYPE(ColliderDesc) collider = RAPIER_FN(BallColliderDesc)(-1); + RAPIER_FN(ShapeDesc_Build)(&collider.shape); + fputs("continued after failed call\n", stderr); + abort(); +} + +static void run(Testbed *testbed) { + if (testbed->action) { + invalidCall(); + } + RAPIER_TYPE(World) *world = RAPIER_FN(NewWorld)(); + tbSetWorld(testbed, world); + while (tbRenderFrame(testbed, &world)) { + if (tbSimulating(testbed)) { + RAPIER_FN(Step)(world, NULL, NULL); + invalidCall(); + } + } + RAPIER_FN(FreeWorld)(world); +} + +static void failSetup(Testbed *testbed) { + (void)testbed; + invalidCall(); +} + +int main(int argc, char **argv) { + Testbed testbed = {0}; + testbed.requestedThreads = 1; + testbed.renderFrame = renderFrame; + TbExample example = {"error-test", "", "", "", run, NULL}; + if (argc == 2 && !strcmp(argv[1], "setup")) { + example.run = failSetup; + } + return tbRun(&testbed, &example, 0) ? 0 : 2; +} diff --git a/c/testbed/tests/errors.cmake b/c/testbed/tests/errors.cmake new file mode 100644 index 000000000..033872f76 --- /dev/null +++ b/c/testbed/tests/errors.cmake @@ -0,0 +1,8 @@ +foreach(phase setup frame) + execute_process(COMMAND "${EXECUTABLE}" "${phase}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE diagnostic) + if(NOT result STREQUAL "1" OR NOT diagnostic MATCHES "Rapier error in error-test.*positive" OR + diagnostic MATCHES "continued after failed call") + message(FATAL_ERROR "Unexpected ${phase} error behavior: ${result}\n${output}\n${diagnostic}") + endif() +endforeach() diff --git a/c/testbed/tests/grab.c b/c/testbed/tests/grab.c new file mode 100644 index 000000000..e752800cf --- /dev/null +++ b/c/testbed/tests/grab.c @@ -0,0 +1,185 @@ +/* Drive the same picking and spring joints as the viewer, without a GPU. */ +#include "grab.h" +#include "rapier_math.h" +#include +#define CHECK(call) \ + do { \ + RAPIER_TYPE(Status) status = (call); \ + if (status != RAPIER_CONST(OK)) { \ + fprintf(stderr, "%s: %s\n", #call, RAPIER_FN(LastError)()); \ + abort(); \ + } \ + } while (0) + +static void pointCursor(Testbed *t, RAPIER_TYPE(Vector) point) { + t->cursorValid = t->rayValid = 1; + t->cursor = point; + t->rayOrigin = RAPIER_FN(VectorAdd)(point, V(0, 0, 10)); + t->rayDirection = V(0, 0, -1); +} + +static void init(Testbed *t) { + t->world = RAPIER_FN(NewWorld)(); + CHECK(RAPIER_FN(LastStatus)()); + t->requestedThreads = 1; + tbRefreshWorld(t); + CHECK(RAPIER_FN(SetGravity)(t->world, V(0, 0, 0))); +} + +static RAPIER_TYPE(RigidBodyHandle) addBall(Testbed *t, RAPIER_TYPE(Vector) center, uint32_t kind, + bool sensor) { + RAPIER_TYPE(RigidBodyDesc) body = RAPIER_FN(DynamicRigidBodyDesc)(); + body.bodyType = kind; + body.position.translation = center; + RAPIER_TYPE(ColliderDesc) collider = RAPIER_FN(DefaultColliderDesc)(); + collider.isSensor = sensor; + RAPIER_TYPE(RigidBodyHandle) handle = RAPIER_FN(InsertRigidBody)(t->world, &body); + RAPIER_FN(InsertCollider)(handle, &collider); + CHECK(RAPIER_FN(LastStatus)()); + return handle; +} + +static void checkCounts(Testbed *t, size_t bodies, size_t joints) { + size_t actual = RAPIER_FN(RigidBodyCount)(t->world); + CHECK(RAPIER_FN(LastStatus)()); + assert(actual == bodies); + actual = RAPIER_FN(ImpulseJointHandles)(t->world, NULL, 0); + CHECK(RAPIER_FN(LastStatus)()); + assert(actual == joints); +} + +static void rigidDrag(void) { + Testbed t = {0}; + init(&t); + TbGrab grab = {0}; + RAPIER_TYPE(RigidBodyHandle) picked = addBall(&t, V(0, 0, 0), RAPIER_CONST(DYNAMIC), false); + addBall(&t, V(-3, 0, 0), RAPIER_CONST(DYNAMIC), true); + addBall(&t, V(3, 0, 0), RAPIER_CONST(FIXED), false); + CHECK(RAPIER_FN(Step)(t.world, NULL, NULL)); + pointCursor(&t, V(-3, 0, 0)); + CHECK(tbGrabBegin(&t, &grab, (RAPIER_TYPE(Real))0.1)); + assert(!grab.active); + pointCursor(&t, V(3, 0, 0)); + CHECK(tbGrabBegin(&t, &grab, (RAPIER_TYPE(Real))0.1)); + assert(!grab.active); + pointCursor(&t, V(0, 0, 0)); + CHECK(tbGrabBegin(&t, &grab, (RAPIER_TYPE(Real))0.1)); + assert(grab.active && !grab.soft && grab.body.index == picked.index); + checkCounts(&t, 4, 1); + pointCursor(&t, V(2, 1, 0)); + for (int i = 0; i < 120; ++i) { + CHECK(tbGrabUpdate(&t, &grab, V(0, 0, -1))); + CHECK(RAPIER_FN(Step)(t.world, NULL, NULL)); + } + + RAPIER_TYPE(Vector) position; + CHECK(RAPIER_FN(RigidBody_ValidateHandle)(picked)); + position = RAPIER_FN(RigidBody_Translation)(picked); + CHECK(RAPIER_FN(LastStatus)()); + assert(position.x > 1.5 && position.y > .5); + CHECK(tbGrabRelease(&t, &grab)); + assert(!grab.active); + checkCounts(&t, 3, 0); + pointCursor(&t, position); + CHECK(tbGrabBegin(&t, &grab, (RAPIER_TYPE(Real))0.1)); + assert(grab.active); + RAPIER_TYPE(Bool) removed = RAPIER_FN(RemoveRigidBody)(picked, 1); + CHECK(RAPIER_FN(LastStatus)()); + assert(removed); + CHECK(tbGrabUpdate(&t, &grab, V(0, 0, -1))); + assert(!grab.active); + checkCounts(&t, 2, 0); + CHECK(RAPIER_FN(FreeWorld)(t.world)); +} + +static void articulatedDrag(void) { + Testbed t = {0}; + init(&t); + TbGrab grab = {0}; + RAPIER_TYPE(RigidBodyDesc) builder = RAPIER_FN(FixedRigidBodyDesc)(); + RAPIER_TYPE(RigidBodyHandle) fixed = RAPIER_FN(InsertRigidBody)(t.world, &builder); + CHECK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(RigidBodyHandle) link = addBall(&t, V(0, 0, 0), RAPIER_CONST(DYNAMIC), false); + + RAPIER_TYPE(JointDesc) joint = RAPIER_FN(PrismaticJointDesc)(V(1, 0, 0)); + RAPIER_FN(InsertMultibodyJoint)(fixed, link, &joint); + CHECK(RAPIER_FN(LastStatus)()); + CHECK(RAPIER_FN(Step)(t.world, NULL, NULL)); + pointCursor(&t, V(0, 0, 0)); + CHECK(tbGrabBegin(&t, &grab, (RAPIER_TYPE(Real))0.1)); + assert(grab.active && grab.body.index == link.index); + pointCursor(&t, V(2, 0, 0)); + for (int i = 0; i < 120; ++i) { + CHECK(tbGrabUpdate(&t, &grab, V(0, 0, -1))); + CHECK(RAPIER_FN(Step)(t.world, NULL, NULL)); + } + + RAPIER_TYPE(Vector) position; + CHECK(RAPIER_FN(RigidBody_ValidateHandle)(link)); + position = RAPIER_FN(RigidBody_Translation)(link); + CHECK(RAPIER_FN(LastStatus)()); + assert(position.x > 1.5 && fabs(position.y) < .01); + CHECK(tbGrabRelease(&t, &grab)); + checkCounts(&t, 2, 0); + CHECK(RAPIER_FN(FreeWorld)(t.world)); +} + +static void softDrag(void) { + Testbed t = {0}; + init(&t); + TbGrab grab = {0}; + +#if defined(RAPIER_DIM2) + RAPIER_TYPE(SoftBodyDesc) builder = RAPIER_FN(GridSoftBodyDesc)(V(0, 0, 0), V(.5, .5, 0), 3, 3); +#else + RAPIER_TYPE(SoftBodyDesc) + builder = RAPIER_FN(ClothSoftBodyDesc)(V(-.5, -.5, 0), V(.5, 0, 0), V(0, .5, 0), 3, 3); +#endif + + RAPIER_TYPE(ColliderDesc) surface = RAPIER_FN(BallColliderDesc)((RAPIER_TYPE(Real))0.05); + builder.collider = surface; + RAPIER_TYPE(SoftBodyHandle) handle = RAPIER_FN(InsertSoftBody)(t.world, &builder); + CHECK(RAPIER_FN(LastStatus)()); + CHECK(RAPIER_FN(Step)(t.world, NULL, NULL)); + + size_t before, after, bodyCount; + CHECK(RAPIER_FN(SoftBody_ValidateHandle)(handle)); + before = RAPIER_FN(SoftBody_Clusters)(handle, NULL, 0); + CHECK(RAPIER_FN(LastStatus)()); + bodyCount = RAPIER_FN(RigidBodyCount)(t.world); + CHECK(RAPIER_FN(LastStatus)()); + pointCursor(&t, V(-.4, -.4, 0)); + CHECK(tbGrabBegin(&t, &grab, (RAPIER_TYPE(Real))0.2)); + assert(grab.active && grab.soft); + CHECK(RAPIER_FN(SoftBody_ValidateHandle)(handle)); + after = RAPIER_FN(SoftBody_Clusters)(handle, NULL, 0); + CHECK(RAPIER_FN(LastStatus)()); + assert(after == before + 1); + RAPIER_TYPE(Vector) start = RAPIER_FN(SoftBody_ParticlePosition)(handle, 0); + CHECK(RAPIER_FN(LastStatus)()); + pointCursor(&t, V(-1.5, 1, 0)); + for (int i = 0; i < 60; ++i) { + CHECK(tbGrabUpdate(&t, &grab, V(0, 0, -1))); + CHECK(RAPIER_FN(Step)(t.world, NULL, NULL)); + } + RAPIER_TYPE(Vector) end; + CHECK(RAPIER_FN(SoftBody_ValidateHandle)(handle)); + end = RAPIER_FN(SoftBody_ParticlePosition)(handle, 0); + CHECK(RAPIER_FN(LastStatus)()); + assert(RAPIER_FN(VectorLength)(RAPIER_FN(VectorSub)(end, start)) > .3); + CHECK(tbGrabRelease(&t, &grab)); + CHECK(RAPIER_FN(SoftBody_ValidateHandle)(handle)); + after = RAPIER_FN(SoftBody_Clusters)(handle, NULL, 0); + CHECK(RAPIER_FN(LastStatus)()); + assert(after == before); + checkCounts(&t, bodyCount, 0); + CHECK(RAPIER_FN(FreeWorld)(t.world)); +} + +int main(void) { + rigidDrag(); + articulatedDrag(); + softDrag(); + puts("Mouse picking, rigid/soft spring dragging, release, and deletion passed"); + return 0; +} diff --git a/c/testbed/tests/interactive.c b/c/testbed/tests/interactive.c new file mode 100644 index 000000000..401e419c3 --- /dev/null +++ b/c/testbed/tests/interactive.c @@ -0,0 +1,165 @@ +/* Exercise the real example loops with synthetic input; no renderer is involved. */ +#include "testbed.h" +#include "rapier_math.h" +#include + +#define CHECK(call) \ + do { \ + if ((call) != RAPIER_CONST(OK)) { \ + fprintf(stderr, "%s: %s\n", #call, RAPIER_FN(LastError)()); \ + abort(); \ + } \ + } while (0) + +typedef struct InputRun { + int kind, frame, mode; + RAPIER_TYPE(RigidBodyHandle) tracked; + RAPIER_TYPE(Vector) start, middle; + RAPIER_TYPE(Real) initialDistance; + uintptr_t initialShape; + RAPIER_TYPE(SharedShape) *retainedShape; + int verified; +} InputRun; + +static RAPIER_TYPE(Vector) bodyPosition(Testbed *t, RAPIER_TYPE(RigidBodyHandle) handle) { + assert(handle.world == t->world); + RAPIER_TYPE(Vector) position; + CHECK(RAPIER_FN(RigidBody_ValidateHandle)(handle)); + position = RAPIER_FN(RigidBody_Translation)(handle); + CHECK(RAPIER_FN(LastStatus)()); + return position; +} + +static int inputFrame(Testbed *t, void *context) { + InputRun *run = context; + size_t bodies, colliders, soft; + assert(tbValidate(t, &bodies, &colliders, &soft)); + if (run->kind == 0) { + t->cursorValid = 1; + t->cursor = V(.3, .65, 0); + if (!run->frame) { + RAPIER_TYPE(RigidBodyHandle) *handles = malloc(bodies * sizeof(*handles)); + assert(handles); + bodies = RAPIER_FN(RigidBodyHandles)(t->world, handles, bodies); + CHECK(RAPIER_FN(LastStatus)()); + run->tracked = handles[bodies - 1]; + free(handles); + } + RAPIER_TYPE(Vector) delta = RAPIER_FN(VectorSub)(bodyPosition(t, run->tracked), t->cursor); + RAPIER_TYPE(Real) distance = RAPIER_FN(VectorLength)(delta); + if (run->frame == 1) { + run->initialDistance = distance; + } + if (run->frame == 60) { + assert(distance < .03); + assert(distance < run->initialDistance * .25); + run->verified = 1; + return 0; + } + } else if (run->kind == 1) { + if (!run->frame) { + RAPIER_TYPE(RigidBodyHandle) *handles = malloc(bodies * sizeof(*handles)); + assert(handles); + bodies = RAPIER_FN(RigidBodyHandles)(t->world, handles, bodies); + CHECK(RAPIER_FN(LastStatus)()); + int found = 0; + for (size_t i = 0; i < bodies; ++i) { + uint32_t type; + CHECK(RAPIER_FN(RigidBody_ValidateHandle)(handles[i])); + type = RAPIER_FN(RigidBody_BodyType)(handles[i]); + CHECK(RAPIER_FN(LastStatus)()); + if (type == RAPIER_CONST(KINEMATIC_POSITION_BASED)) { + run->tracked = handles[i]; + found = 1; + break; + } + } + assert(found); + free(handles); + run->start = bodyPosition(t, run->tracked); + } + t->inputDirection = V(1, 0, 0); + t->jump = 1; + if (run->frame == 20) { + run->middle = bodyPosition(t, run->tracked); + assert(run->middle.x > run->start.x + .2); + for (size_t i = 0; i < t->settingCount; ++i) { + if (!strcmp(t->settings[i].name, "Control mode")) { + t->settings[i].value = 1; + } + } + } + if (run->frame == 40) { + uint32_t type; + CHECK(RAPIER_FN(RigidBody_ValidateHandle)(run->tracked)); + type = RAPIER_FN(RigidBody_BodyType)(run->tracked); + CHECK(RAPIER_FN(LastStatus)()); + assert(type == RAPIER_CONST(DYNAMIC)); + assert(bodyPosition(t, run->tracked).x > run->middle.x + .1); + run->verified = 1; + return 0; + } + } +#if defined(RAPIER_DIM3) + else if (run->kind == 2) { + + RAPIER_TYPE(ColliderHandle) handle = {t->world, 0, 0}; + CHECK(r3Collider_ValidateHandle(handle)); + uintptr_t identity; + identity = r3Collider_ShapeIdentity(handle); + CHECK(r3LastStatus()); + if (!run->frame) { + run->initialShape = identity; + run->retainedShape = r3Collider_CloneShape(handle); + CHECK(r3LastStatus()); + } + t->rayValid = 1; + t->rayOrigin = r3Vector(100, 100, 100); + t->rayDirection = r3Vector(0, -1, 0); + t->jump = 1; + t->removeVoxel = run->mode; + if (run->frame == 2) { + assert(identity != run->initialShape); + CHECK(r3FreeSharedShape(run->retainedShape)); + run->verified = 1; + return 0; + } + } +#endif + ++run->frame; + t->simulating = 1; + return 1; +} + +static void checkExample(const char *id, int kind, int mode) { + Testbed t = {0}; + InputRun input = {.kind = kind, .mode = mode}; + t.renderFrame = inputFrame; + t.viewer = &input; + t.noSleep = 1; + t.requestedThreads = 1; + t.assetRoot = TB_ASSET_ROOT; + const TbExample *example = NULL; + for (size_t i = 0; i < tbExampleCount; ++i) { + if (!strcmp(tbExamples[i].id, id)) { + example = &tbExamples[i]; + } + } + assert(example && tbRun(&t, example, 0)); + assert(input.verified); + tbDestroy(&t); + printf("PASS %s interaction mode %d\n", id, mode); +} + +int main(void) { +#if defined(RAPIER_DIM2) + checkExample("inverse_kinematics2", 0, 0); + checkExample("character_controller2", 1, 0); +#else + checkExample("inverse_kinematics3", 0, 0); + checkExample("character_controller3", 1, 0); + checkExample("voxels3", 2, 0); + checkExample("voxels3", 2, 1); +#endif + return 0; +} diff --git a/c/testbed/tests/soft_render.c b/c/testbed/tests/soft_render.c new file mode 100644 index 000000000..764aa2dc1 --- /dev/null +++ b/c/testbed/tests/soft_render.c @@ -0,0 +1,188 @@ +/* Exercise the real soft-mesh renderer without creating a window or GPU context. */ +#include "testbed.h" +#include "raylib.h" +#include "rlgl.h" +#include + +static size_t triangles, lineCount, translucent; +static bool depthWrite = true, pendingTranslucent; +static int culling = 1, flushed; + +static void begin3d(Camera3D camera) { + (void)camera; +} + +static void end3d(void) { + /* raylib flushes buffered triangles here, using the current culling state. */ + assert(!culling); + ++flushed; +} + +static void enableCulling(void) { + culling = 1; +} + +static void disableCulling(void) { + culling = 0; +} + +static void flushBatch(void) { + if (pendingTranslucent) { + assert(!depthWrite); + pendingTranslucent = false; + } +} + +static void disableDepthWrite(void) { + assert(!pendingTranslucent); + depthWrite = false; +} + +static void enableDepthWrite(void) { + assert(!pendingTranslucent); + depthWrite = true; +} + +static void checkPoint(Vector3 point) { + assert(isfinite(point.x) && isfinite(point.y) && isfinite(point.z)); +} + +static void recordTriangle(Vector3 a, Vector3 b, Vector3 c, Color color) { + if (color.a < 255) { + assert(color.a == 102); + assert(!depthWrite); + ++translucent; + pendingTranslucent = true; + } + checkPoint(a); + checkPoint(b); + checkPoint(c); + ++triangles; +} + +static void recordLine(Vector3 a, Vector3 b, Color color) { + (void)color; + checkPoint(a); + checkPoint(b); + ++lineCount; +} + +static void *boundedRealloc(void *pointer, size_t bytes) { + /* Catch a sentinel used as a cache index before allocating gigabytes. */ + if (bytes > 64 * 1024 * 1024) { + fprintf(stderr, "Unexpected renderer allocation: %zu bytes\n", bytes); + abort(); + } + return realloc(pointer, bytes); +} + +#define BeginMode3D begin3d +#define EndMode3D end3d +#define rlEnableBackfaceCulling enableCulling +#define rlDisableBackfaceCulling disableCulling +#define DrawTriangle3D recordTriangle +#define DrawLine3D recordLine +#define rlDrawRenderBatchActive flushBatch +#define rlDisableDepthMask disableDepthWrite +#define rlEnableDepthMask enableDepthWrite +#define realloc boundedRealloc +#include "../graphics.c" +#undef BeginMode3D +#undef EndMode3D +#undef rlEnableBackfaceCulling +#undef rlDisableBackfaceCulling +#undef rlDrawRenderBatchActive +#undef rlDisableDepthMask +#undef rlEnableDepthMask +#undef realloc +#undef DrawLine3D +#undef DrawTriangle3D + +typedef struct RenderTest { + TbGraphics graphics; + size_t frames; + bool forceSensors, expectTranslucent; +} RenderTest; + +static int renderFrame(Testbed *t, void *context) { + RenderTest *test = context; + if (!test->frames && test->forceSensors) { + size_t count = RAPIER_FN(ColliderHandles)(t->world, NULL, 0); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + RAPIER_TYPE(ColliderHandle) *handles = malloc(count * sizeof(*handles)); + assert(handles); + count = RAPIER_FN(ColliderHandles)(t->world, handles, count); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + for (size_t i = 0; i < count; ++i) { + assert(RAPIER_FN(Collider_ValidateHandle)(handles[i]) == RAPIER_CONST(OK)); + assert(RAPIER_FN(Collider_SetSensor)(handles[i], 1) == RAPIER_CONST(OK)); + } + free(handles); + } + triangles = lineCount = translucent = 0; + test->graphics.transparentCount = 0; + drawSoft(&test->graphics, t, true, (Camera3D){.position = {0, 0, 100}, .fovy = 20}); + assert(translucent == 0); /* No sensor geometry in the opaque pass. */ + size_t queued = test->graphics.transparentCount; + drawTransparent(&test->graphics); + assert(translucent == queued && depthWrite && !pendingTranslucent); + assert((translucent > 0) == test->expectTranslucent); + for (size_t i = 1; i < queued; ++i) { + assert(test->graphics.transparent[i - 1].depth >= test->graphics.transparent[i].depth); + } + assert(triangles + lineCount > 0); + assert(test->graphics.entryCapacity < 1024); + /* Turning surfaces off must also avoid indexing render-only colliders. */ + drawSoft(&test->graphics, t, false, (Camera3D){0}); + /* The full frame must flush before restoring culling, even on an empty + * surface pass. Mesh draw calls above are recorded without a GPU. */ + int previousFlushes = flushed; + assert(tbGraphicsDraw(&test->graphics, t, (Camera3D){0}, 0, false)); + assert(flushed == previousFlushes + 1 && culling); + if (++test->frames == 4) { + return 0; + } + t->simulating = 1; + return 1; +} + +static void checkScene(const char *id, bool forceSensors, bool expectTranslucent) { + const TbExample *example = NULL; + for (size_t i = 0; i < tbExampleCount; ++i) { + if (!strcmp(tbExamples[i].id, id)) { + example = &tbExamples[i]; + } + } + assert(example && !example->requires); + Testbed t = {0}; + RenderTest test = {.forceSensors = forceSensors, .expectTranslucent = expectTranslucent}; + t.renderFrame = renderFrame; + t.viewer = &test; + t.requestedThreads = 1; + t.assetRoot = TB_ASSET_ROOT; + t.noSleep = 1; + assert(tbRun(&t, example, 0)); + assert(test.frames == 4 && t.step == 3); + free(test.graphics.transparent); + free(test.graphics.entries); + free(test.graphics.softHandles); + free(test.graphics.softMeshes); + free(test.graphics.vertices); + free(test.graphics.indices); + tbDestroy(&t); + printf("PASS soft renderer: %s\n", id); +} + +int main(void) { +#if defined(RAPIER_DIM3) + checkScene("soft_meshes3", false, true); + checkScene("soft_trimesh3", false, false); + checkScene("soft_surface3", false, false); + checkScene("soft_cloth3", false, false); +#else + checkScene("soft_letters2", false, false); + checkScene("soft_surface2", false, false); + checkScene("soft_surface2", true, true); +#endif + return 0; +} diff --git a/c/testbed/tests/threading.c b/c/testbed/tests/threading.c new file mode 100644 index 000000000..21a06775e --- /dev/null +++ b/c/testbed/tests/threading.c @@ -0,0 +1,122 @@ +#include "testbed.h" +#include + +#define CHECK(call) \ + do { \ + if ((call) != RAPIER_CONST(OK)) { \ + fprintf(stderr, "%s: %s\n", #call, RAPIER_FN(LastError)()); \ + abort(); \ + } \ + } while (0) + +typedef struct Viewer { + unsigned frame; + uint64_t expectedStep, snapshotStep; + double snapshotTime; + RAPIER_TYPE(Bytes) *snapshot; + RAPIER_TYPE(World) *original; +} Viewer; + +static const TbExample *example(const char *name) { + for (size_t i = 0; i < tbExampleCount; i++) { + if (!strcmp(tbExamples[i].name, name)) { + return &tbExamples[i]; + } + } + abort(); +} + +static int renderFrame(Testbed *t, void *context) { + Viewer *viewer = context; + assert(t->step == viewer->expectedStep); + assert(t->activeThreads == (t->requestedThreads ? t->requestedThreads : t->activeThreads)); + if (viewer->frame == 0) { + viewer->original = t->world; + CHECK(RAPIER_FN(SetTimeStep)(t->world, (RAPIER_TYPE(Real))(1.0 / 120))); + } + if (viewer->frame == 3) { + if (t->buildFeatures.parallel) { + CHECK(tbSetThreads(t, 2)); + assert(t->activeThreads == 2); + CHECK(tbSetThreads(t, 0)); + assert(t->activeThreads > 0); + CHECK(tbSetThreads(t, 2)); + } else { + assert(tbSetThreads(t, 2) == RAPIER_CONST(UNSUPPORTED)); + assert(t->activeThreads == 1); + } + assert(t->world == viewer->original); + RAPIER_TYPE(Real) dt = RAPIER_FN(TimeStep)(t->world); + CHECK(RAPIER_FN(LastStatus)()); + assert(fabs(dt - 1.0 / 120) < 1.0e-6); + } + if (viewer->frame == 10) { + viewer->snapshot = RAPIER_FN(SerializeWorld)(t->world); + CHECK(RAPIER_FN(LastStatus)()); + viewer->snapshotStep = t->step; + viewer->snapshotTime = t->time; + } + if (viewer->frame == 15) { + const uint8_t *data; + size_t size; + RAPIER_TYPE(World) *restored = NULL; + RAPIER_TYPE(ByteView) bytesDataResult = RAPIER_FN(Bytes_Data)(viewer->snapshot); + data = bytesDataResult.data; + size = bytesDataResult.count; + CHECK(RAPIER_FN(LastStatus)()); + restored = RAPIER_FN(DeserializeWorld)(data, size); + CHECK(RAPIER_FN(LastStatus)()); + CHECK(RAPIER_FN(FreeWorld)(t->world)); + t->world = restored; + t->step = viewer->snapshotStep; + t->time = viewer->snapshotTime; + viewer->expectedStep = t->step; + tbRefreshWorld(t); + assert(t->activeThreads == t->requestedThreads); + } + if (viewer->frame == 30) { + size_t bodies, colliders, softBodies; + assert(tbValidate(t, &bodies, &colliders, &softBodies)); + assert(bodies > 0 && colliders > 0); + CHECK(RAPIER_FN(FreeBytes)(viewer->snapshot)); + viewer->snapshot = NULL; + return 0; + } + /* Paused frames still return to the example. One requested step advances once. */ + t->simulating = viewer->frame != 7 && viewer->frame != 9; + viewer->expectedStep += t->simulating; + viewer->frame++; + return 1; +} + +int main(void) { + size_t parsed; + assert(tbParseThreads("0", &parsed) && parsed == 0); + assert(tbParseThreads("1", &parsed) && parsed == 1); + assert(tbParseThreads("256", &parsed) && parsed == 256); + assert(!tbParseThreads("257", &parsed)); + assert(!tbParseThreads("-1", &parsed)); + assert(!tbParseThreads("", &parsed)); + assert(!tbParseThreads("2x", &parsed)); + assert(!tbParseThreads("999999999999999999999999999999999999999", &parsed)); + Testbed testbed = {0}; + testbed.requestedThreads = 1; + testbed.noSleep = 1; + testbed.assetRoot = TB_ASSET_ROOT; + testbed.renderFrame = renderFrame; + const char *names[] = {"Restitution", "Restitution", "Damping"}; + for (size_t i = 0; i < TB_COUNT(names); i++) { + Viewer viewer = {0}; + testbed.viewer = &viewer; + size_t requested = testbed.requestedThreads; + assert(tbRun(&testbed, example(names[i]), i == 1)); + assert(!testbed.world); /* The example freed its world after its loop. */ + assert(viewer.frame == 30); + if (i) { + assert(testbed.requestedThreads == requested); + } + } + tbDestroy(&testbed); + puts("Example-owned loops, pause/step, worker changes, restart/switch, and snapshot restore " + "passed"); +} diff --git a/c/testbed/tests/ui.c b/c/testbed/tests/ui.c new file mode 100644 index 000000000..4111b749b --- /dev/null +++ b/c/testbed/tests/ui.c @@ -0,0 +1,105 @@ +/* Exercise the actual C UI and ImGui input routing without a display or GPU. */ +#define TB_UI_TESTING +#include "../gui.c" +#include + +static void frame(UiState *ui, bool focusSearch) { + igNewFrame(); + igSetNextWindowPos(UI2(0, 0), ImGuiCond_Always, UI2(0, 0)); + igSetNextWindowSize(UI2(600, 800), ImGuiCond_Always); + igBegin("Input regression", NULL, ImGuiWindowFlags_NoSavedSettings); + if (focusSearch) { + igSetKeyboardFocusHere(0); + } + examplesUi(ui); + igEnd(); + keyboardShortcuts(ui); + igRender(); + /* A null renderer acknowledges atlas updates; no GPU upload is needed. */ + ImDrawData *draw = igGetDrawData(); + if (draw->Textures) { + for (int i = 0; i < draw->Textures->Size; i++) { + ImTextureData *texture = draw->Textures->Data[i]; + if (texture->Status == ImTextureStatus_WantCreate || + texture->Status == ImTextureStatus_WantUpdates) { + ImTextureData_SetTexID(texture, 1); + ImTextureData_SetStatus(texture, ImTextureStatus_OK); + } else if (texture->Status == ImTextureStatus_WantDestroy) { + ImTextureData_SetStatus(texture, ImTextureStatus_Destroyed); + } + } + } +} + +int main(void) { + ImGuiContext *context = igCreateContext(NULL); + assert(context); + ImGuiIO *io = igGetIO_Nil(); + io->DisplaySize = UI2(1440, 900); + io->DisplayFramebufferScale = UI2(1, 1); + io->DeltaTime = 1.0f / 60; + io->BackendFlags |= ImGuiBackendFlags_RendererHasTextures; + igStyleColorsLight(NULL); + assert(configureUi()); + UiState ui = {0}; + ui.running = true; + ui.initialTab = "Examples"; + /* Navigation wraps around the available, filtered examples. */ + ui.selected = 0; + int next = adjacentExample(&ui, 1); + assert(next != 0 && !tbExamples[next].requires); + ui.selected = next; + assert(adjacentExample(&ui, -1) == 0); + snprintf(ui.search, sizeof(ui.search), "no-such-example"); + assert(adjacentExample(&ui, 1) == next); + ui.search[0] = 0; +#if defined(RAPIER_DIM3) + Camera3D camera = {.position = {3, 2, 5}, .target = {1, 0, 0}, .up = {0, 1, 0}, .fovy = 45}; + float distance = Vector3Distance(camera.position, camera.target); + Vector3 target = camera.target; + orbitCamera(&camera, (Vector2){100, 50}, false, 0, 900); + assert(Vector3Distance(camera.target, target) < .0001f); + assert(fabsf(Vector3Distance(camera.position, camera.target) - distance) < .0001f); + Vector3 offset = Vector3Subtract(camera.position, camera.target); + orbitCamera(&camera, (Vector2){30, 20}, true, 0, 900); + assert(Vector3Distance(camera.target, target) > .01f); + assert(Vector3Distance(Vector3Subtract(camera.position, camera.target), offset) < .0001f); + orbitCamera(&camera, (Vector2){0}, false, 1, 900); + assert(Vector3Distance(camera.position, camera.target) < distance); +#endif + frame(&ui, true); + frame(&ui, false); + assert(io->WantCaptureKeyboard); + ImGuiIO_AddInputCharactersUTF8(io, "test"); + ImGuiKey keys[] = {ImGuiKey_T, ImGuiKey_S, ImGuiKey_R, ImGuiKey_F}; + for (size_t i = 0; i < TB_COUNT(keys); i++) { + ImGuiIO_AddKeyEvent(io, keys[i], true); + frame(&ui, false); + assert(ui.running && !ui.stepOnce && !ui.restart && !ui.frameAll); + ImGuiIO_AddKeyEvent(io, keys[i], false); + frame(&ui, false); + } + assert(!strcmp(ui.search, "test")); + assert(matches("Stress tests", ui.search)); + assert(!matches("Restitution", ui.search)); + /* Clicking the scene relinquishes capture; hotkeys work again. */ + ImGuiIO_AddMousePosEvent(io, 1000, 500); + frame(&ui, false); + ImGuiIO_AddMouseButtonEvent(io, 0, true); + frame(&ui, false); + ImGuiIO_AddMouseButtonEvent(io, 0, false); + frame(&ui, false); + frame(&ui, false); + assert(!io->WantCaptureKeyboard && !io->WantCaptureMouse); + ImGuiIO_AddKeyEvent(io, ImGuiKey_T, true); + frame(&ui, false); + assert(!ui.running); + ImGuiIO_AddKeyEvent(io, ImGuiKey_T, false); + frame(&ui, false); + ImGuiIO_AddKeyEvent(io, ImGuiKey_S, true); + frame(&ui, false); + assert(ui.stepOnce); + igDestroyContext(context); + puts("Dear ImGui text capture, scene input, and simulation shortcuts passed"); + return 0; +} diff --git a/c/testbed/tools/README.md b/c/testbed/tools/README.md new file mode 100644 index 000000000..273da62bc --- /dev/null +++ b/c/testbed/tools/README.md @@ -0,0 +1,38 @@ +# Comparing C and native Rust step costs + +`step_benchmark.c` runs the same C examples and their own physics loops as the +viewer, using a headless frame function to record timings and snapshots. `c/examples/compare_steps.rs` replays the C world's initial snapshot with +native `PhysicsWorld::step`. Both use one worker, disable sleeping, discard 120 +warmup steps, and measure the next 300 steps. Setup, rendering, and serialization +are outside the measured region. Native Rust checks every final rigid-body pose +and velocity for exact equality with C, and both verify that dynamic bodies stay +awake. Use only trusted snapshots from the same checkout/configuration. + +From the repository root, on a quiet machine: + +```sh +cmake -S c -B build/compare3 -DRAPIER_BUILD_TESTBED=ON \ + -DRAPIER_TESTBED_GRAPHICS=OFF -DRAPIER_PROFILE=release \ + -DCMAKE_BUILD_TYPE=Release -DRAPIER_ENABLE_PARALLEL=ON -DRAPIER_SIMD_LANES=4 +cmake --build build/compare3 --config Release --target rapier_testbed_step_benchmark +cargo build --release -p rapier3d-ffi --features parallel,profiler --example compare_steps \ + --target-dir build/compare3/cargo +build/compare3/testbed/rapier_testbed_step_benchmark primitives3 initial.bin final.bin +build/compare3/cargo/release/examples/compare_steps initial.bin final.bin +``` + +Repeat with `stress_tests_boxes3` or `stress_tests_keva3` (38,270 dynamic +bodies; a complete Keva pair takes a few minutes with one worker). For 2D, use a separate directory, set +`-DRAPIER_DIMENSION=2`, select `rapier2d-ffi`, and run `stress_tests_boxes2`. +Multi-configuration generators put the C executable under `testbed/Release/`; +Windows also needs the `.exe` suffix. The Rust example supports f32 builds only. +For a serial build, set `RAPIER_ENABLE_PARALLEL=OFF` and use Cargo's +`--features profiler`. Build both executables before timing, run them sequentially, +repeat the pair, and compare medians; do not benchmark while other builds run. + +This isolates C-boundary/testbed stepping overhead for identical initial worlds. +It does not establish scene-construction parity for the whole catalog, benchmark +renderers, or support scenes with C callbacks or soft bodies. `wall_ms` measures +each complete call; `engine_ms` uses Rapier's own per-step counter, matching the +primary timing shown in both UIs. Dedicated-pool dispatch is outside that engine +counter. Snapshots are temporary benchmark artifacts, not a stable file format. diff --git a/c/testbed/tools/logo-mesh/Cargo.toml b/c/testbed/tools/logo-mesh/Cargo.toml new file mode 100644 index 000000000..ccab60dbd --- /dev/null +++ b/c/testbed/tools/logo-mesh/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rapier-c-logo-mesh" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +rapier2d = { path = "../../../../crates/rapier2d" } +lyon = "0.17" +usvg = "0.14" diff --git a/c/testbed/tools/logo-mesh/src/main.rs b/c/testbed/tools/logo-mesh/src/main.rs new file mode 100644 index 000000000..d82793150 --- /dev/null +++ b/c/testbed/tools/logo-mesh/src/main.rs @@ -0,0 +1,37 @@ +//! Regenerate examples2d/utils/logo_mesh.h with the Rust demo's exact SVG tessellation. +#[path = "../../../../../examples2d/utils/svg.rs"] +mod svg; + +fn main() { + println!("/* Generated from examples2d/utils/svg.rs by tools/logo-mesh. Do not edit. */"); + println!("#ifndef EXAMPLE_LOGO_MESH_H\n#define EXAMPLE_LOGO_MESH_H\n#include \"rapier.h\""); + println!( + "typedef struct LogoMesh {{ const R2Vector *vertices; size_t vertex_count; const uint32_t *indices; size_t triangle_count; const uint32_t *outline; size_t edge_count; }} LogoMesh;" + ); + let meshes = svg::rapier_logo(); + for (i, (vertices, indices)) in meshes.iter().enumerate() { + println!("static const R2Vector logo_vertices_{i}[] = {{"); + for p in vertices { + println!(" {{{:.9e}, {:.9e}}},", p.x, p.y); + } + println!("}};\nstatic const uint32_t logo_indices_{i}[] = {{"); + for t in indices { + println!(" {}, {}, {},", t[0], t[1], t[2]); + } + println!("}};\nstatic const uint32_t logo_outline_{i}[] = {{"); + for e in svg::outline(indices) { + println!(" {}, {},", e[0], e[1]); + } + println!("}};"); + } + println!("static const LogoMesh logo_meshes[] = {{"); + for (i, (vertices, indices)) in meshes.iter().enumerate() { + println!( + " {{logo_vertices_{i}, {}, logo_indices_{i}, {}, logo_outline_{i}, {}}},", + vertices.len(), + indices.len(), + svg::outline(indices).len() + ); + } + println!("}};\n#endif"); +} diff --git a/c/testbed/tools/step_benchmark.c b/c/testbed/tools/step_benchmark.c new file mode 100644 index 000000000..0132b3af1 --- /dev/null +++ b/c/testbed/tools/step_benchmark.c @@ -0,0 +1,112 @@ +/* Manual no-sleep benchmark, linked to the actual C testbed and loaded library. + * Writes trusted snapshots for compare_steps.rs; excludes setup and + * serialization. Only rigid-body scenes without local animation state are accepted so native + * Rust can replay them. + */ +#include "testbed.h" +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +static double nowMs(void) { +#ifdef _WIN32 + LARGE_INTEGER value, frequency; + assert(QueryPerformanceCounter(&value)); + assert(QueryPerformanceFrequency(&frequency)); + return (double)value.QuadPart * 1000.0 / (double)frequency.QuadPart; +#else + struct timespec t; + assert(clock_gettime(CLOCK_MONOTONIC, &t) == 0); + return t.tv_sec * 1000.0 + t.tv_nsec * 1.0e-6; +#endif +} + +static void save(Testbed *t, const char *path) { + RAPIER_TYPE(Bytes) *bytes = NULL; + const uint8_t *data = NULL; + size_t size = 0; + bytes = RAPIER_FN(SerializeWorld)(t->world); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + RAPIER_TYPE(ByteView) bytesDataResult = RAPIER_FN(Bytes_Data)(bytes); + data = bytesDataResult.data; + size = bytesDataResult.count; + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + FILE *file = fopen(path, "wb"); + assert(file && fwrite(data, 1, size, file) == size); + assert(fclose(file) == 0); + assert(RAPIER_FN(FreeBytes)(bytes) == RAPIER_CONST(OK)); +} + +typedef struct Benchmark { + const char *initialPath, *finalPath; + double start, wall, engine; +} Benchmark; + +static int renderFrame(Testbed *t, void *context) { + Benchmark *benchmark = context; + if (t->step == 0) { + assert(t->snapshotSupported); + size_t softCount = RAPIER_FN(SoftBodyCount)(t->world); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + assert(softCount == 0); + save(t, benchmark->initialPath); + } else if (t->step > 120) { + benchmark->wall += nowMs() - benchmark->start; + benchmark->engine += t->physicsStepMs; + } + if (t->step < 420) { + t->simulating = 1; + benchmark->start = nowMs(); + return 1; + } + size_t n = RAPIER_FN(RigidBodyHandles)(t->world, NULL, 0); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + RAPIER_TYPE(RigidBodyHandle) *handles = malloc(n * sizeof(*handles)); + n = RAPIER_FN(RigidBodyHandles)(t->world, handles, n); + assert(handles && RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + size_t active = 0; + for (size_t i = 0; i < n; i++) { + RAPIER_TYPE(Bool) dynamic = 0, sleeping = 0; + assert(RAPIER_FN(RigidBody_ValidateHandle)(handles[i]) == RAPIER_CONST(OK)); + dynamic = RAPIER_FN(RigidBody_IsDynamic)(handles[i]); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + sleeping = RAPIER_FN(RigidBody_IsSleeping)(handles[i]); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + if (dynamic) { + assert(!sleeping); + active++; + } + } + save(t, benchmark->finalPath); + printf("C %s bodies=%zu awake_dynamic=%zu SIMD=%u workers=%zu warmup=120 " + "measured=300 wall_ms=%.6f engine_ms=%.6f\n", + t->example->id, n, active, t->buildFeatures.simd_lanes, t->activeThreads, + benchmark->wall / 300, benchmark->engine / 300); + free(handles); + return 0; +} + +int main(int argc, char **argv) { + if (argc != 4) { + fprintf(stderr, "Usage: %s SCENE INITIAL_SNAPSHOT FINAL_SNAPSHOT\n", argv[0]); + return 2; + } + Testbed testbed = {0}; + testbed.noSleep = 1; + testbed.requestedThreads = 1; + testbed.assetRoot = TB_ASSET_ROOT; + testbed.renderFrame = renderFrame; + Benchmark benchmark = {.initialPath = argv[2], .finalPath = argv[3]}; + testbed.viewer = &benchmark; + const TbExample *scene = NULL; + for (size_t i = 0; i < tbExampleCount; i++) { + if (!strcmp(tbExamples[i].id, argv[1])) { + scene = &tbExamples[i]; + } + } + assert(scene && tbRun(&testbed, scene, 0)); + tbDestroy(&testbed); +} diff --git a/c/testbed/update_catalog.py b/c/testbed/update_catalog.py new file mode 100644 index 000000000..ca27173d1 --- /dev/null +++ b/c/testbed/update_catalog.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Regenerate the example registry and audit coverage against the Rust testbed.""" +from pathlib import Path +import re, json +root=Path(__file__).resolve().parents[2] +target=root/'c/testbed' +entries=[] +for dim in [2,3]: + src=(root/f'examples{dim}d/all_examples{dim}.rs').read_text() + groups=dict(re.findall(r'const (\w+): &str = "([^"]+)";',src)) + out=['/* Generated by update_catalog.py; order matches the Rust testbed. */','#include "testbed.h"'] + rows=[] + for group,name,fn in re.findall(r'\b([A-Z0-9]+), "([^"]+)", ([\w:]+);',src): + bits=fn.split('::');stem='/'.join(bits[:-1]);suffix='' if bits[-1]=='run' else '_'+bits[-1] + ident=stem.replace('/','_')+suffix + source=f'examples{dim}d/{stem}.rs';port=f'examples{dim}d/{stem}{suffix}.c' + present=(target/port).exists() + symbol='tb'+''.join(part[:1].upper()+part[1:] for part in ident.split('_')) + if present:out.append(f'extern void {symbol}(Testbed *);') + feature='fem' if 'fem' in stem else 'robotics' if stem in ('urdf3', 'mjcf3', 'mujoco_menagerie3') else None + entries.append(dict(id=ident,dimension=dim,group=groups[group],name=name,rust=source,c=port if present else None,feature=feature)) + fields=', '.join(json.dumps(x) for x in [ident,groups[group],name,source]) + if present and feature: + rows.extend([f'#ifdef RAPIER_{feature.upper()}',f' {{{fields}, {symbol}, NULL}},','#else',f' {{{fields}, NULL, "Requires RAPIER_FEATURES={feature}"}},','#endif']) + else:rows.append(f' {{{fields}, '+(f'{symbol}, NULL' if present else 'NULL, "Port pending"')+'},') + out+=['const TbExample tbExamples[] = {',*rows,'};','const size_t tbExampleCount=TB_COUNT(tbExamples);'] + (target/f'registry{dim}.c').write_text('\n'.join(out)+'\n') +(target/'coverage.json').write_text(json.dumps(entries,indent=2)+'\n') +print(f'{sum(e["c"] is not None for e in entries)}/{len(entries)} C scene ports') diff --git a/c/tests/array_views.c b/c/tests/array_views.c new file mode 100644 index 000000000..3fbb4a803 --- /dev/null +++ b/c/tests/array_views.c @@ -0,0 +1,170 @@ +#include "rapier_helpers.h" +#include +#include +#include +#include +#include + +#define OK(call) \ + do { \ + RAPIER_TYPE(Status) status = (call); \ + if (status != RAPIER_CONST(OK)) { \ + fprintf(stderr, "%s: %s\n", #call, RAPIER_FN(LastError)()); \ + abort(); \ + } \ + } while (0) + +/* Aggregate initialization is shared by C11 and C++17. */ +static void test_meshes(RAPIER_TYPE(World) *world) { + RAPIER_TYPE(Vector) points[4]; + memset(points, 0, sizeof(points)); + points[1].x = 1; + points[2].y = 1; + points[3].x = points[3].y = 1; + RAPIER_TYPE(Triangle) triangles[] = {{0, 1, 2}, {1, 3, 2}}; + RAPIER_TYPE(Edge) edges[] = {{0, 1}, {1, 2}, {2, 3}}; + RAPIER_TYPE(VectorView) vertices = {points, 4}; + RAPIER_TYPE(TriangleView) faces = {triangles, 2}; + RAPIER_TYPE(EdgeView) segments = {edges, 3}; + RAPIER_TYPE(RigidBodyDesc) body = RAPIER_FN(FixedRigidBodyDesc)(); + RAPIER_TYPE(ColliderDesc) collider = RAPIER_FN(DefaultColliderDesc)(); + OK(RAPIER_FN(ShapeDesc_SetTrimesh)(&collider.shape, vertices, faces, 0)); + assert(collider.shape.triangles.count == 2 && collider.shape.vertices.count == 4); + assert(collider.shape.vertices.data == points); /* The setter borrows; insertion copies. */ + RAPIER_TYPE(RigidBodyHandle) bodyHandle = RAPIER_FN(InsertRigidBody)(world, &body); + OK(RAPIER_FN(LastStatus)()); + RAPIER_FN(InsertCollider)(bodyHandle, &collider); + OK(RAPIER_FN(LastStatus)()); + OK(RAPIER_FN(ShapeDesc_SetPolyline)(&collider.shape, vertices, segments, 0)); + assert(collider.shape.edges.count == 3 && + collider.shape.kind == RAPIER_CONST(SHAPE_DESC_POLYLINE)); + bodyHandle = RAPIER_FN(InsertRigidBody)(world, &body); + OK(RAPIER_FN(LastStatus)()); + RAPIER_FN(InsertCollider)(bodyHandle, &collider); + OK(RAPIER_FN(LastStatus)()); +#ifdef RAPIER_DIM3 + points[3].z = 1; +#endif + OK(RAPIER_FN(ShapeDesc_SetConvexHull)(&collider.shape, vertices)); + assert(collider.shape.triangles.count == 0 && collider.shape.triangles.data == NULL); + bodyHandle = RAPIER_FN(InsertRigidBody)(world, &body); + OK(RAPIER_FN(LastStatus)()); + RAPIER_FN(InsertCollider)(bodyHandle, &collider); + OK(RAPIER_FN(LastStatus)()); + + RAPIER_TYPE(ShapeDesc) original; + memcpy(&original, &collider.shape, sizeof(original)); + RAPIER_TYPE(TriangleView) null_faces = {NULL, 2}; + assert(RAPIER_FN(ShapeDesc_SetTrimesh)(&collider.shape, vertices, null_faces, 0) == + RAPIER_CONST(NULL_POINTER)); + assert(memcmp(&original, &collider.shape, sizeof(original)) == 0); + RAPIER_TYPE(TriangleView) huge_faces = {triangles, SIZE_MAX}; + assert(RAPIER_FN(ShapeDesc_SetTrimesh)(&collider.shape, vertices, huge_faces, 0) == + RAPIER_CONST(INVALID_ARGUMENT)); + assert(memcmp(&original, &collider.shape, sizeof(original)) == 0); + RAPIER_TYPE(TriangleView) + misaligned = {(const RAPIER_TYPE(Triangle) *)((const char *)triangles + 1), 1}; + assert(RAPIER_FN(ShapeDesc_SetTrimesh)(&collider.shape, vertices, misaligned, 0) == + RAPIER_CONST(INVALID_ARGUMENT)); + assert(memcmp(&original, &collider.shape, sizeof(original)) == 0); + assert(RAPIER_FN(ShapeDesc_SetTrimesh)(NULL, vertices, faces, 0) == RAPIER_CONST(NULL_POINTER)); + + /* Values and index bounds are checked when building, not when assigning views. */ + triangles[1].c = 100; + OK(RAPIER_FN(ShapeDesc_SetTrimesh)(&collider.shape, vertices, faces, 0)); + bodyHandle = RAPIER_FN(InsertRigidBody)(world, &body); + OK(RAPIER_FN(LastStatus)()); + RAPIER_FN(InsertCollider)(bodyHandle, &collider); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(INVALID_ARGUMENT)); + RAPIER_TYPE(TriangleView) empty = {NULL, 0}; + OK(RAPIER_FN(ShapeDesc_SetTrimesh)(&collider.shape, vertices, empty, 0)); + assert(collider.shape.triangles.count == 0); +} + +static RAPIER_TYPE(SoftBodyHandle) test_soft(RAPIER_TYPE(World) *world) { + RAPIER_TYPE(Vector) points[4]; + memset(points, 0, sizeof(points)); + points[0].y = 4; + points[1].x = 1; + points[1].y = 4; + points[2].y = 5; + points[3].x = 1; + points[3].y = 5; + RAPIER_TYPE(VectorView) vertices = {points, 4}; + RAPIER_TYPE(Edge) edges[] = {{0, 1}, {1, 2}, {2, 3}}; + RAPIER_TYPE(EdgeView) segments = {edges, 3}; + RAPIER_TYPE(Real) masses[] = {1, 2, 3, 4}; + RAPIER_TYPE(RealView) mass_view = {masses, 4}; + uint32_t pins[] = {0}; + RAPIER_TYPE(IndexView) pinned = {pins, 1}; + RAPIER_TYPE(SoftBodyDesc) soft = RAPIER_FN(DefaultSoftBodyDesc)(); + soft.gravityScale = 0.5; + OK(RAPIER_FN(SoftBodyDesc_SetParticles)(&soft, vertices)); + OK(RAPIER_FN(SoftBodyDesc_SetEdges)(&soft, segments)); + OK(RAPIER_FN(SoftBodyDesc_SetMasses)(&soft, mass_view)); + OK(RAPIER_FN(SoftBodyDesc_SetPinnedParticles)(&soft, pinned)); + assert(soft.gravityScale == 0.5 && soft.edges.count == 3); + RAPIER_TYPE(EdgeView) empty_edges = {NULL, 0}; + OK(RAPIER_FN(SoftBodyDesc_SetBendEdges)(&soft, empty_edges)); + RAPIER_TYPE(IndexView) empty_indices = {NULL, 0}; + OK(RAPIER_FN(SoftBodyDesc_SetTensionOnlyEdges)(&soft, empty_indices)); +#ifdef RAPIER_DIM3 + points[3].x = 0; + points[3].y = 4; + points[3].z = 1; + RAPIER_TYPE(Tetrahedron) cells[] = {{0, 1, 2, 3}}; + RAPIER_TYPE(Triangle) surface[] = {{0, 2, 1}, {0, 1, 3}, {0, 3, 2}, {1, 2, 3}}; + RAPIER_TYPE(SurfaceElementView) boundary = {surface, 4}; + RAPIER_TYPE(DihedralView) empty_dihedrals = {NULL, 0}; + OK(RAPIER_FN(SoftBodyDesc_SetDihedrals)(&soft, empty_dihedrals)); + OK(RAPIER_FN(SoftBodyDesc_SetWire)(&soft, empty_edges)); +#else + RAPIER_TYPE(Triangle) cells[] = {{0, 1, 2}, {1, 3, 2}}; + RAPIER_TYPE(Edge) surface[] = {{0, 1}, {1, 3}, {3, 2}, {2, 0}}; + RAPIER_TYPE(SurfaceElementView) boundary = {surface, 4}; +#endif + RAPIER_TYPE(CellView) volume = {cells, sizeof(cells) / sizeof(cells[0])}; + OK(RAPIER_FN(SoftBodyDesc_SetCells)(&soft, volume)); + OK(RAPIER_FN(SoftBodyDesc_SetSurface)(&soft, boundary)); + OK(RAPIER_FN(SoftBodyDesc_SetSkin)(&soft, vertices, boundary)); + assert(soft.cells.count == volume.count && soft.surface.count == 4 && + soft.skinIndices.count == 4); + RAPIER_TYPE(SoftBodyHandle) handle = RAPIER_FN(InsertSoftBody)(world, &soft); + OK(RAPIER_FN(LastStatus)()); + + RAPIER_TYPE(SoftBodyDesc) surface_body = RAPIER_FN(DefaultSoftBodyDesc)(); + OK(RAPIER_FN(SoftBodyDesc_SetSurfaceMesh)(&surface_body, vertices, boundary)); + assert(surface_body.kind == RAPIER_CONST(SOFT_DESC_SURFACE) && surface_body.surface.count == 4); + RAPIER_FN(InsertSoftBody)(world, &surface_body); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(SoftBodyDesc) original; + memcpy(&original, &soft, sizeof(original)); + RAPIER_TYPE(RealView) bad_masses = {NULL, 4}; + assert(RAPIER_FN(SoftBodyDesc_SetMasses)(&soft, bad_masses) == RAPIER_CONST(NULL_POINTER)); + assert(memcmp(&soft, &original, sizeof(soft)) == 0); + RAPIER_TYPE(VectorView) bad_vertices = {NULL, 4}; + assert(RAPIER_FN(SoftBodyDesc_SetSurfaceMesh)(&soft, bad_vertices, boundary) == + RAPIER_CONST(NULL_POINTER)); + assert(memcmp(&soft, &original, sizeof(soft)) == 0); + + /* Neither descriptor copies nor views own memory. Native storage is independent. */ + points[0].y = 999; + RAPIER_TYPE(Vector) position = RAPIER_FN(SoftBody_ParticlePosition)(handle, 0); + OK(RAPIER_FN(LastStatus)()); + assert(position.y == 4); + return handle; +} + +int main(void) { + RAPIER_TYPE(World) *world = RAPIER_FN(NewWorld)(); + OK(RAPIER_FN(LastStatus)()); + test_meshes(world); + RAPIER_TYPE(SoftBodyHandle) soft = test_soft(world); + /* All input arrays have left scope by the time physics reads its copies. */ + OK(RAPIER_FN(Step)(world, NULL, NULL)); + RAPIER_TYPE(Vector) position = RAPIER_FN(SoftBody_ParticlePosition)(soft, 0); + OK(RAPIER_FN(LastStatus)()); + assert(isfinite(position.y)); + OK(RAPIER_FN(FreeWorld)(world)); + return 0; +} diff --git a/c/tests/array_views.cpp b/c/tests/array_views.cpp new file mode 100644 index 000000000..c1e4a58c3 --- /dev/null +++ b/c/tests/array_views.cpp @@ -0,0 +1 @@ +#include "array_views.c" diff --git a/c/tests/consumer/CMakeLists.txt b/c/tests/consumer/CMakeLists.txt new file mode 100644 index 000000000..0f590f5d1 --- /dev/null +++ b/c/tests/consumer/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) +project(RapierInstalledConsumer LANGUAGES CXX) +find_package(Rapier CONFIG REQUIRED) +add_executable(consumer main.cpp) +target_link_libraries(consumer PRIVATE Rapier::rapier) +target_compile_features(consumer PRIVATE cxx_std_17) +get_target_property(rapier_kind Rapier::rapier TYPE) +if(WIN32 AND rapier_kind STREQUAL "SHARED_LIBRARY") + add_custom_command(TARGET consumer POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$" VERBATIM) +endif() +enable_testing() +add_test(NAME installed_consumer COMMAND consumer) diff --git a/c/tests/consumer/main.cpp b/c/tests/consumer/main.cpp new file mode 100644 index 000000000..620e0a534 --- /dev/null +++ b/c/tests/consumer/main.cpp @@ -0,0 +1,6 @@ +#include + +int main() { + auto world = rapier::make_world(); + rapier::check(RAPIER_FN(Step)(world.get(), nullptr, nullptr)); +} diff --git a/c/tests/cpp.cpp b/c/tests/cpp.cpp new file mode 100644 index 000000000..44f0af413 --- /dev/null +++ b/c/tests/cpp.cpp @@ -0,0 +1,73 @@ +#include "rapier.hpp" +#include +#include + +int main() { + static_assert(std::is_standard_layout::value, "POD pose"); + static_assert(sizeof(RAPIER_TYPE(RigidBodyHandle)) == sizeof(void *) + 2 * sizeof(uint32_t), "handle ABI"); + static_assert(std::is_trivially_copyable::value, "POD body"); + static_assert(std::is_standard_layout::value, "POD collider"); + static_assert(std::is_trivially_copyable::value, "POD soft recipe"); + static_assert(std::is_trivially_copyable::value, "POD joint"); + auto world = rapier::make_world(); + auto body = rapier::rigid_body(); + auto collider = rapier::ball(0.5); + body.position.translation.y = 5; + auto bodyHandle = RAPIER_FN(InsertRigidBody)(world.get(), &body); + rapier::check(RAPIER_FN(LastStatus)()); + auto colliderHandle = RAPIER_FN(InsertCollider)(bodyHandle, &collider); + rapier::check(RAPIER_FN(LastStatus)()); + auto query = rapier::queryOptions(); + assert(!query.predicate && !query.userData); + auto moved = std::move(world); + assert(!world && moved); + rapier::check(RAPIER_FN(Step)(moved.get(), nullptr, nullptr)); + { + rapier::Shape shape(RAPIER_FN(Collider_CloneShape)(colliderHandle)); + rapier::check(RAPIER_FN(LastStatus)()); + // The owned clone must remain usable after its source collider is removed. + rapier::check(RAPIER_FN(RemoveCollider)(colliderHandle, 1)); + rapier::ShapeMesh mesh(RAPIER_FN(SharedShape_Tessellate)(shape.get(), 8)); + rapier::EventCollector events(RAPIER_FN(NewEventCollector)()); + rapier::KinematicCharacterController character( + RAPIER_FN(NewKinematicCharacterController)()); + rapier::PidController pid(RAPIER_FN(NewPidController)()); + rapier::Snapshot snapshot(RAPIER_FN(SerializeWorld)(moved.get())); + rapier::SoftBodyTearEvent tear; + assert(shape && mesh && events && character && pid && snapshot); + auto transferred = std::move(mesh); + assert(transferred && !mesh); +#if defined(RAPIER_DIM3) + rapier::TriMeshData triangles(RAPIER_FN(SharedShape_ToTrimesh)(shape.get(), 8, 8)); + assert(triangles); + auto chassisDesc = RAPIER_FN(DynamicRigidBodyDesc)(); + auto chassis = RAPIER_FN(InsertRigidBody)(moved.get(), &chassisDesc); + rapier::DynamicRayCastVehicleController vehicle( + RAPIER_FN(NewDynamicRayCastVehicleController)(chassis)); + assert(vehicle); +#endif +#if defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32) + static_assert(std::is_trivially_copyable::value, "POD URDF options"); + static_assert(std::is_trivially_copyable::value, "POD MJCF options"); + const auto layout = r3PodLayout(); + assert(layout.urdfLoaderOptions == sizeof(R3UrdfLoaderOptions)); + assert(layout.mjcfLoaderOptions == sizeof(R3MjcfLoaderOptions)); + const auto urdf = r3DefaultUrdfLoaderOptions(); + const auto mjcf = r3DefaultMjcfLoaderOptions(); + assert(urdf.rigidBodyBlueprint.enabled && mjcf.rigidBodyBlueprint.enabled); + assert(urdf.colliderBlueprint.density == 0 && mjcf.colliderBlueprint.density == 0); + rapier::UrdfRobot urdfRobot; + rapier::UrdfRobotHandles urdfHandles; + rapier::MjcfRobot mjcfRobot; + rapier::MjcfRobotHandles mjcfHandles; +#endif + } + bool caught = false; + try { + RAPIER_FN(BallSharedShape)(-1); + rapier::check(RAPIER_FN(LastStatus)()); + } catch (const std::runtime_error &) { + caught = true; + } + assert(caught); +} diff --git a/c/tests/handles.c b/c/tests/handles.c new file mode 100644 index 000000000..8dfcfad95 --- /dev/null +++ b/c/tests/handles.c @@ -0,0 +1,125 @@ +#include "rapier.h" +#include +#include +#include +#include + +#define OK(call) \ + do { \ + RAPIER_TYPE(Status) status = (call); \ + if (status != RAPIER_CONST(OK)) { \ + fprintf(stderr, "%s: %s\n", #call, RAPIER_FN(LastError)()); \ + abort(); \ + } \ + } while (0) + +static void RAPIER_CALL on_error(RAPIER_TYPE(Status) status, const char *message, void *data) { + assert(status != RAPIER_CONST(OK) && message); + assert(RAPIER_FN(LastStatus)() == status); + ++*(unsigned *)data; +} + +int main(void) { + RAPIER_TYPE(World) *world = RAPIER_FN(NewWorld)(); + OK(RAPIER_FN(LastStatus)()); + + RAPIER_TYPE(RigidBodyDesc) desc = RAPIER_FN(DynamicRigidBodyDesc)(); + RAPIER_TYPE(ColliderDesc) collider = RAPIER_FN(BallColliderDesc)(0.5); + desc.canSleep = 0; + RAPIER_TYPE(RigidBodyHandle) body; + RAPIER_TYPE(ColliderHandle) shape; + body = RAPIER_FN(InsertRigidBody)(world, &desc); + OK(RAPIER_FN(LastStatus)()); + shape = RAPIER_FN(InsertCollider)(body, &collider); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(Vector) position = {0}; + position.y = 10; + OK(RAPIER_FN(RigidBody_SetTranslation)(body, position, 1)); + OK(RAPIER_FN(Collider_SetSensor)(shape, 1)); + RAPIER_TYPE(Bool) sensor = RAPIER_FN(Collider_IsSensor)(shape); + OK(RAPIER_FN(LastStatus)()); + assert(sensor); + OK(RAPIER_FN(Step)(world, NULL, NULL)); + position = RAPIER_FN(RigidBody_Translation)(body); + OK(RAPIER_FN(LastStatus)()); + assert(position.y < 10); + + RAPIER_TYPE(Vector) throughSet = RAPIER_FN(RigidBody_Translation)(body); + OK(RAPIER_FN(LastStatus)()); + assert(throughSet.y == position.y); + /* Retained handles survive storage growth and stepping; no element pointer is kept. */ + for (int i = 0; i < 512; ++i) { + desc.position.translation.x = (RAPIER_TYPE(Real))(10 + i); + RAPIER_FN(InsertRigidBody)(world, &desc); + OK(RAPIER_FN(LastStatus)()); + } + RAPIER_TYPE(RigidBodyHandle) order[] = {body, body}; + RAPIER_TYPE(RigidBodyState) states[2]; + size_t count = 99; + count = RAPIER_FN(RigidBodyReadStates)(world, order, 2, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count == 2); + states[0].position.translation.x = 123; + count = RAPIER_FN(RigidBodyReadStates)(world, order, 2, states, 1); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(BUFFER_TOO_SMALL)); + assert(count == 2 && states[0].position.translation.x == 123); + count = RAPIER_FN(RigidBodyReadStates)(world, order, 2, states, 2); + OK(RAPIER_FN(LastStatus)()); + assert(states[0].position.translation.y == position.y && + states[1].position.translation.y == position.y); + RAPIER_TYPE(Vector) invalid = position; + invalid.y = (RAPIER_TYPE(Real))NAN; + assert(RAPIER_FN(RigidBody_SetTranslation)(body, invalid, 1) == + RAPIER_CONST(INVALID_ARGUMENT)); + throughSet = RAPIER_FN(RigidBody_Translation)(body); + OK(RAPIER_FN(LastStatus)()); + assert(throughSet.y == position.y); + + RAPIER_TYPE(SoftBodyDesc) soft = RAPIER_FN(DefaultSoftBodyDesc)(); + soft.kind = RAPIER_CONST(SOFT_DESC_ROPE); + soft.nx = 3; + RAPIER_TYPE(SoftBodyHandle) softHandle = RAPIER_FN(InsertSoftBody)(world, &soft); + OK(RAPIER_FN(LastStatus)()); + position = RAPIER_FN(SoftBody_ParticlePosition)(softHandle, 0); + OK(RAPIER_FN(LastStatus)()); + position.y = 20; + OK(RAPIER_FN(SoftBody_SetParticlePosition)(softHandle, 0, position)); + throughSet = RAPIER_FN(SoftBody_ParticlePosition)(softHandle, 0); + OK(RAPIER_FN(LastStatus)()); + assert(throughSet.y == 20); + + RAPIER_TYPE(RigidBodyHandle) root; + OK(RAPIER_FN(SoftBody_ValidateHandle)(softHandle)); + root = RAPIER_FN(SoftBody_RootBody)(softHandle); + OK(RAPIER_FN(LastStatus)()); + assert(RAPIER_FN(RigidBody_SetTranslation)(root, position, 1) == + RAPIER_CONST(INVALID_ARGUMENT)); + + RAPIER_TYPE(Bool) removed = RAPIER_FN(RemoveRigidBody)(body, 1); + OK(RAPIER_FN(LastStatus)()); + assert(removed); + RAPIER_TYPE(RigidBodyHandle) replacement = RAPIER_FN(InsertRigidBody)(world, &desc); + OK(RAPIER_FN(LastStatus)()); + assert(replacement.index == body.index && replacement.generation != body.generation); + unsigned errors = 0; + RAPIER_TYPE(ErrorHandler) + previous = RAPIER_FN(SetErrorHandler)((RAPIER_TYPE(ErrorHandler)){on_error, &errors}); + position.y = 123; + position = RAPIER_FN(RigidBody_Translation)(body); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(INVALID_HANDLE)); + assert(errors == 1 && position.y == 0); + RAPIER_FN(SetErrorHandler)(previous); + sensor = RAPIER_FN(Collider_IsSensor)(shape); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(INVALID_HANDLE)); + order[0] = replacement; + count = 999; + states[0].position.translation.x = 123; + count = RAPIER_FN(RigidBodyReadStates)(world, order, 2, states, 2); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(INVALID_HANDLE)); + assert(count == 0 && states[0].position.translation.x == 123); + count = RAPIER_FN(RigidBodyReadStates)(world, NULL, 0, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count == 0); + OK(RAPIER_FN(FreeWorld)(world)); + return 0; +} diff --git a/c/tests/initializers.c b/c/tests/initializers.c new file mode 100644 index 000000000..e42dda449 --- /dev/null +++ b/c/tests/initializers.c @@ -0,0 +1,58 @@ +#include "rapier_helpers.h" +#include + +int main(void) { + RAPIER_TYPE(RigidBodyDesc) + bodies[] = {RAPIER_FN(DynamicRigidBodyDesc)(), RAPIER_FN(FixedRigidBodyDesc)(), + RAPIER_FN(KinematicPositionBasedRigidBodyDesc)(), + RAPIER_FN(KinematicVelocityBasedRigidBodyDesc)()}; + RAPIER_TYPE(World) *world = RAPIER_FN(NewWorld)(); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + RAPIER_TYPE(ColliderDesc) collider = RAPIER_FN(DefaultColliderDesc)(); + RAPIER_TYPE(RigidBodyHandle) handle = RAPIER_CONST(INVALID_RIGID_BODY_HANDLE); + assert(handle.index == UINT32_MAX && handle.generation == UINT32_MAX); + for (unsigned i = 0; i < 4; ++i) { + assert(bodies[i].bodyType == i); + bodies[i].position.translation.x = (RAPIER_TYPE(Real))(2 * i); + bodies[i].position.translation.y = 5; + handle = RAPIER_FN(InsertRigidBody)(world, &bodies[i]); + RAPIER_FN(InsertCollider)(handle, &collider); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + } + RAPIER_TYPE(ShapeDesc) shape = RAPIER_FN(DefaultShapeDesc)(); + assert(shape.kind == RAPIER_CONST(SHAPE_DESC_BALL) && !shape.vertices.data); + RAPIER_TYPE(SoftBodyDesc) soft = RAPIER_FN(DefaultSoftBodyDesc)(); + RAPIER_TYPE(SoftBodyMaterial) material = RAPIER_FN(DefaultSoftBodyMaterial)(); + soft.material = material; + soft.kind = RAPIER_CONST(SOFT_DESC_ROPE); + RAPIER_FN(InsertSoftBody)(world, &soft); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(OK)); + RAPIER_TYPE(JointDesc) joint = RAPIER_FN(DefaultJointDesc)(); + assert(joint.enabled && !joint.lockedAxes); + RAPIER_TYPE(SoftMeshBindingDesc) binding = RAPIER_FN(DefaultSoftMeshBindingDesc)(); + assert(binding.kind == RAPIER_CONST(SOFT_BINDING_SKINNED) && !binding.particles.data); + RAPIER_TYPE(IntegrationParameters) settings = RAPIER_FN(DefaultIntegrationParameters)(); + settings.softBodies = RAPIER_FN(DefaultSoftBodiesSettings)(); + settings.softBodies.recovery = RAPIER_FN(DefaultSoftRecoverySettings)(); +#ifdef RAPIER_FEM + settings.softBodies.fem = RAPIER_FN(DefaultSoftFemParameters)(); +#endif + + assert(RAPIER_FN(SetIntegrationParameters)(world, &settings) == RAPIER_CONST(OK)); + assert(RAPIER_FN(Step)(world, NULL, NULL) == RAPIER_CONST(OK)); + RAPIER_TYPE(QueryFilter) filter = RAPIER_FN(DefaultQueryFilter)(); + assert(filter.exclude_collider.index == UINT32_MAX && + filter.exclude_rigid_body.index == UINT32_MAX); + RAPIER_TYPE(QueryOptions) query = RAPIER_FN(DefaultQueryOptions)(); + assert(!query.predicate && !query.userData); + assert(query.filter.exclude_collider.index == UINT32_MAX && + query.filter.exclude_rigid_body.index == UINT32_MAX); + RAPIER_TYPE(ShapeCastOptions) options = RAPIER_FN(DefaultShapeCastOptions)(); + assert(options.max_time_of_impact > 0); + assert(RAPIER_CONST(INVALID_COLLIDER_HANDLE).index == UINT32_MAX); + assert(RAPIER_CONST(INVALID_IMPULSE_JOINT_HANDLE).index == UINT32_MAX); + assert(RAPIER_CONST(INVALID_MULTIBODY_JOINT_HANDLE).index == UINT32_MAX); + assert(RAPIER_CONST(INVALID_SOFT_BODY_HANDLE).index == UINT32_MAX); + assert(RAPIER_FN(FreeWorld)(world) == RAPIER_CONST(OK)); + return 0; +} diff --git a/c/tests/initializers.cpp b/c/tests/initializers.cpp new file mode 100644 index 000000000..d83a4a0cb --- /dev/null +++ b/c/tests/initializers.cpp @@ -0,0 +1,2 @@ +/* The same initializer surface must work as strict C++17 as well as C11. */ +#include "initializers.c" diff --git a/c/tests/integration.c b/c/tests/integration.c new file mode 100644 index 000000000..daa236b0f --- /dev/null +++ b/c/tests/integration.c @@ -0,0 +1,457 @@ +#include "rapier.h" +#include +#include +#include +#include +#include +#define OK(expr) \ + do { \ + RAPIER_TYPE(Status) status_ = (expr); \ + if (status_ != RAPIER_CONST(OK)) { \ + fprintf(stderr, "%s:%d: %s: %s (%u)\n", __FILE__, __LINE__, #expr, \ + RAPIER_FN(LastError)(), status_); \ + abort(); \ + } \ + } while (0) +#define EXPECT(expr, code) \ + do { \ + RAPIER_TYPE(Status) status_ = (expr); \ + RAPIER_TYPE(Status) expected_ = (code); \ + if (status_ != expected_) { \ + fprintf(stderr, "%s:%d: expected %u, got %u: %s\n", __FILE__, __LINE__, \ + expected_, status_, RAPIER_FN(LastError)()); \ + abort(); \ + } \ + } while (0) + +static RAPIER_TYPE(Vector) vector(RAPIER_TYPE(Real) x, RAPIER_TYPE(Real) y, RAPIER_TYPE(Real) z) { + RAPIER_TYPE(Vector) v; + v.x = x; + v.y = y; +#ifdef RAPIER_DIM3 + v.z = z; +#else + (void)z; +#endif + return v; +} + +static RAPIER_TYPE(Pose) pose(RAPIER_TYPE(Real) x, RAPIER_TYPE(Real) y, RAPIER_TYPE(Real) z) { + RAPIER_TYPE(Pose) p = {0}; + p.translation = vector(x, y, z); +#ifdef RAPIER_DIM3 + p.rotation.w = 1; +#endif + return p; +} + +static RAPIER_TYPE(RigidBodyHandle) add_body(RAPIER_TYPE(World) *world, uint32_t kind, + RAPIER_TYPE(Real) y) { + RAPIER_TYPE(RigidBodyDesc) body = RAPIER_FN(DynamicRigidBodyDesc)(); + body.bodyType = kind; + body.position = pose(0, y, 0); + body.canSleep = 0; + RAPIER_TYPE(RigidBodyHandle) handle = RAPIER_FN(InsertRigidBody)(world, &body); + OK(RAPIER_FN(LastStatus)()); + return handle; +} + +static RAPIER_TYPE(ColliderHandle) add_ball(RAPIER_TYPE(RigidBodyHandle) body) { + RAPIER_TYPE(ColliderDesc) collider = RAPIER_FN(DefaultColliderDesc)(); + collider.activeEvents = RAPIER_CONST(COLLISION_EVENTS) | RAPIER_CONST(CONTACT_FORCE_EVENTS); + collider.activeHooks = 1 | 4; + RAPIER_TYPE(ColliderHandle) handle = RAPIER_FN(InsertCollider)(body, &collider); + OK(RAPIER_FN(LastStatus)()); + return handle; +} + +static int filter_calls = 0, modify_calls = 0; + +static int32_t RAPIER_CALL contact_filter(void *user, const RAPIER_TYPE(ReadContext) *read, + RAPIER_TYPE(ColliderHandle) a, + RAPIER_TYPE(ColliderHandle) b, + RAPIER_TYPE(RigidBodyHandle) x, + RAPIER_TYPE(RigidBodyHandle) y) { + RAPIER_TYPE(Vector) position = RAPIER_FN(ReadCollider_Translation)(read, a); + OK(RAPIER_FN(LastStatus)()); + assert(isfinite(position.y)); + (void)a; + (void)b; + (void)x; + (void)y; + assert(user == &filter_calls); + ++filter_calls; + return 1; +} + +static void RAPIER_CALL modify_contact(void *user, const RAPIER_TYPE(ReadContext) *read, + RAPIER_TYPE(ColliderHandle) a, RAPIER_TYPE(ColliderHandle) b, + RAPIER_TYPE(ContactModification) *contact) { + (void)read; + (void)user; + (void)a; + (void)b; + ++modify_calls; + contact->friction = (RAPIER_TYPE(Real))0.7; +} + +static void test_world_pipeline(void) { + RAPIER_TYPE(World) *world = RAPIER_FN(NewWorld)(); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(BuildFeatures) profiling = RAPIER_FN(BuildFeatures)(); + EXPECT(RAPIER_FN(SetCountersEnabled)(world, 1), + profiling.profiling ? RAPIER_CONST(OK) : RAPIER_CONST(UNSUPPORTED)); + EXPECT(RAPIER_FN(SetCountersEnabled)(world, 2), RAPIER_CONST(INVALID_ARGUMENT)); + RAPIER_TYPE(RigidBodyHandle) body = add_body(world, RAPIER_CONST(DYNAMIC), 10); + (void)add_ball(body); + OK(RAPIER_FN(Step)(world, NULL, NULL)); + double step_ms = RAPIER_FN(StepTimeMs)(world); + OK(RAPIER_FN(LastStatus)()); + assert(isfinite(step_ms) && (profiling.profiling ? step_ms > 0 : step_ms == 0)); + OK(RAPIER_FN(SetCountersEnabled)(world, 0)); + RAPIER_FN(StepTimeMs)(NULL); + EXPECT(RAPIER_FN(LastStatus)(), RAPIER_CONST(NULL_POINTER)); + RAPIER_TYPE(Vector) position = RAPIER_FN(RigidBody_Translation)(body); + OK(RAPIER_FN(LastStatus)()); + assert(position.y < 10); + OK(RAPIER_FN(DetectCollisions)(world, NULL, NULL)); + RAPIER_TYPE(Vector) after = RAPIER_FN(RigidBody_Translation)(body); + OK(RAPIER_FN(LastStatus)()); + assert(after.y == position.y); + OK(RAPIER_FN(FreeWorld)(world)); +} + +int main(void) { + OK(RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), RAPIER_CONST(DIMENSION), + sizeof(RAPIER_TYPE(Real)), sizeof(RAPIER_TYPE(Vector)), + sizeof(RAPIER_TYPE(Pose)))); + EXPECT(RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), 99, sizeof(RAPIER_TYPE(Real)), + sizeof(RAPIER_TYPE(Vector)), sizeof(RAPIER_TYPE(Pose))), + RAPIER_CONST(INVALID_ARGUMENT)); + const char *version = RAPIER_FN(Version)(); + assert(version && strstr(version, "+c.")); + assert(version == RAPIER_FN(Version)()); +#ifdef RAPIER_EXPECTED_VERSION + assert(!strcmp(version, RAPIER_EXPECTED_VERSION)); +#endif + const char *profile = RAPIER_FN(BuildProfile)(); + assert(profile && (!strcmp(profile, "release") || !strcmp(profile, "debug"))); +#ifdef RAPIER_EXPECTED_PROFILE + assert(!strcmp(profile, RAPIER_EXPECTED_PROFILE)); +#endif + RAPIER_TYPE(BuildFeatures) features = RAPIER_FN(BuildFeatures)(); + assert(features.simd_lanes == 4 || features.simd_lanes == 8); +#ifdef RAPIER_EXPECTED_SIMD_LANES + assert(features.simd_lanes == RAPIER_EXPECTED_SIMD_LANES); +#endif +#ifdef RAPIER_PARALLEL + assert(features.parallel == 1); +#else + assert(features.parallel == 0); +#endif + RAPIER_TYPE(World) *pool_world = NULL, *other_pool_world = NULL; + pool_world = RAPIER_FN(NewWorld)(); + OK(RAPIER_FN(LastStatus)()); + other_pool_world = RAPIER_FN(NewWorld)(); + OK(RAPIER_FN(LastStatus)()); + size_t workers = 99; + if (features.parallel) { + workers = RAPIER_FN(NumThreads)(pool_world); + OK(RAPIER_FN(LastStatus)()); + assert(workers == 0); + OK(RAPIER_FN(SetNumThreads)(pool_world, 1)); + OK(RAPIER_FN(SetNumThreads)(other_pool_world, 2)); + workers = RAPIER_FN(NumThreads)(pool_world); + OK(RAPIER_FN(LastStatus)()); + assert(workers == 1); + OK(RAPIER_FN(SetNumThreads)(pool_world, 4)); + workers = RAPIER_FN(NumThreads)(pool_world); + OK(RAPIER_FN(LastStatus)()); + assert(workers == 4); + workers = RAPIER_FN(NumThreads)(other_pool_world); + OK(RAPIER_FN(LastStatus)()); + assert(workers == 2); + OK(RAPIER_FN(Step)(pool_world, NULL, NULL)); + OK(RAPIER_FN(ClearThreadPool)(pool_world)); + workers = RAPIER_FN(NumThreads)(pool_world); + OK(RAPIER_FN(LastStatus)()); + assert(workers == 0); + } else { + EXPECT(RAPIER_FN(SetNumThreads)(pool_world, 2), RAPIER_CONST(UNSUPPORTED)); + EXPECT(RAPIER_FN(ClearThreadPool)(pool_world), RAPIER_CONST(UNSUPPORTED)); + workers = RAPIER_FN(NumThreads)(pool_world); + OK(RAPIER_FN(LastStatus)()); + assert(workers == 1); + } + OK(RAPIER_FN(FreeWorld)(pool_world)); + OK(RAPIER_FN(FreeWorld)(other_pool_world)); + RAPIER_TYPE(BuildInfo) info = RAPIER_FN(BuildInfo)(); + assert(info.dimension == RAPIER_CONST(DIMENSION) && + info.real_size == sizeof(RAPIER_TYPE(Real))); + EXPECT(RAPIER_FN(SetGravity)(NULL, vector(0, 0, 0)), RAPIER_CONST(NULL_POINTER)); + assert(strstr(RAPIER_FN(LastError)(), "null")); + OK(RAPIER_FN(FreeWorld)(NULL)); + RAPIER_TYPE(SharedShape) *bad = (RAPIER_TYPE(SharedShape) *)(uintptr_t)1; + bad = RAPIER_FN(BallSharedShape)(-1); + EXPECT(RAPIER_FN(LastStatus)(), RAPIER_CONST(INVALID_ARGUMENT)); + assert(bad == NULL); + bad = RAPIER_FN(BallSharedShape)((RAPIER_TYPE(Real))NAN); + EXPECT(RAPIER_FN(LastStatus)(), RAPIER_CONST(INVALID_ARGUMENT)); + RAPIER_TYPE(Vector) triangle[3] = {vector(0, 0, 0), vector(1, 0, 0), vector(0, 1, 0)}; + uint32_t bad_indices[3] = {0, 1, 3}; + bad = RAPIER_FN(TrimeshSharedShape)( + (RAPIER_TYPE(VectorView)){triangle, 3}, + (RAPIER_TYPE(TriangleView)){(const RAPIER_TYPE(Triangle) *)bad_indices, 1}); + EXPECT(RAPIER_FN(LastStatus)(), RAPIER_CONST(INVALID_ARGUMENT)); + test_world_pipeline(); + RAPIER_TYPE(World) *world = RAPIER_FN(NewWorld)(); + OK(RAPIER_FN(LastStatus)()); + + RAPIER_TYPE(ColliderHandle) floor_h; + RAPIER_TYPE(ColliderDesc) + floor = RAPIER_FN(CuboidColliderDesc)(vector(10, (RAPIER_TYPE(Real))0.5, 10)); + floor.position = pose(0, (RAPIER_TYPE(Real))-0.5, 0); + floor_h = RAPIER_FN(InsertColliderWithoutParent)(world, &floor); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(RigidBodyHandle) ball = add_body(world, RAPIER_CONST(DYNAMIC), 5); + RAPIER_TYPE(ColliderHandle) ball_collider = add_ball(ball); + RAPIER_TYPE(EventCollector) *events = RAPIER_FN(NewEventCollector)(); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(PhysicsHooks) hooks = {0}; + hooks.user_data = &filter_calls; + hooks.filter_contact_pair = contact_filter; + hooks.modify_solver_contacts = modify_contact; + for (int i = 0; i < 240; ++i) { + OK(RAPIER_FN(Step)(world, &hooks, events)); + } + RAPIER_TYPE(Vector) position; + OK(RAPIER_FN(RigidBody_ValidateHandle)(ball)); + position = RAPIER_FN(RigidBody_Translation)(ball); + OK(RAPIER_FN(LastStatus)()); + assert(position.y > 0.45 && position.y < 0.6); + assert(filter_calls > 0 && modify_calls > 0); + size_t count = RAPIER_FN(EventCollector_CollisionEvents)(events, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count > 0); + RAPIER_TYPE(CollisionEvent) *collisions = malloc(count * sizeof(*collisions)); + assert(collisions); + count = RAPIER_FN(EventCollector_CollisionEvents)(events, collisions, count); + OK(RAPIER_FN(LastStatus)()); + assert(collisions[0].started == 1); + free(collisions); + count = RAPIER_FN(EventCollector_ContactForceEvents)(events, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count > 0); + count = RAPIER_FN(ContactPairs)(world, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count > 0); + RAPIER_TYPE(ContactPair) pair = RAPIER_FN(ContactPair)(floor_h, ball_collider); + OK(RAPIER_FN(LastStatus)()); + assert(pair.has_any_active_contact); + count = RAPIER_FN(ContactPoints)(floor_h, ball_collider, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count > 0); + RAPIER_TYPE(QueryOptions) query = RAPIER_FN(DefaultQueryOptions)(); + RAPIER_TYPE(RayHit) + hit = RAPIER_FN(CastRay)(world, &query, vector(0, 10, 0), vector(0, -1, 0), 20, 1); + OK(RAPIER_FN(LastStatus)()); + assert(hit.collider.index == ball_collider.index && hit.normal.y > 0.9); + hit = RAPIER_FN(CastRay)(world, &query, vector(100, 10, 0), vector(0, -1, 0), 20, 1); + EXPECT(RAPIER_FN(LastStatus)(), RAPIER_CONST(NOT_FOUND)); + count = RAPIER_FN(IntersectPoint)(world, &query, vector(0, (RAPIER_TYPE(Real))0.5, 0), NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count == 1); + RAPIER_TYPE(ColliderHandle) sentinel = {NULL, 123, 456}; + count = RAPIER_FN(IntersectPoint)(world, &query, vector(0, (RAPIER_TYPE(Real))0.5, 0), + &sentinel, 0); + EXPECT(RAPIER_FN(LastStatus)(), RAPIER_CONST(BUFFER_TOO_SMALL)); + assert(sentinel.index == 123 && count == 1); + RAPIER_TYPE(SharedShape) *cast_shape = RAPIER_FN(BallSharedShape)((RAPIER_TYPE(Real))0.25); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(ShapeCastOptions) cast_options = RAPIER_FN(DefaultShapeCastOptions)(); + cast_options.max_time_of_impact = 10; + RAPIER_TYPE(ShapeCastHit) + shape_hit = RAPIER_FN(CastShape)(world, &query, pose(0, 4, 0), vector(0, -1, 0), cast_shape, + cast_options); + OK(RAPIER_FN(LastStatus)()); + assert(shape_hit.collider.index == ball_collider.index && shape_hit.time_of_impact > 2 && + shape_hit.time_of_impact < 4); + count = RAPIER_FN(IntersectShape)(world, &query, pose(0, (RAPIER_TYPE(Real))0.5, 0), cast_shape, + NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count == 1); + RAPIER_TYPE(Aabb) aabb = {vector(-1, -1, -1), vector(1, 1, 1)}; + count = RAPIER_FN(IntersectAabbConservative)(world, &query, aabb, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count >= 2); + RAPIER_TYPE(MassProperties) + mass_properties = RAPIER_FN(SharedShape_MassProperties)(cast_shape, 1); + OK(RAPIER_FN(LastStatus)()); + assert(mass_properties.mass > 0); + OK(RAPIER_FN(FreeSharedShape)(cast_shape)); + RAPIER_TYPE(Real) heights[4] = {0, 0, 0, 0}; +#ifdef RAPIER_DIM3 + cast_shape = RAPIER_FN(HeightfieldSharedShape)((RAPIER_TYPE(RealView)){heights, 4}, 2, 2, + vector(2, 1, 2)); + OK(RAPIER_FN(LastStatus)()); +#else + cast_shape = RAPIER_FN(HeightfieldSharedShape)((RAPIER_TYPE(RealView)){heights, 2}, 2, 1, + vector(2, 1, 0)); + OK(RAPIER_FN(LastStatus)()); +#endif + aabb = RAPIER_FN(SharedShape_ComputeAabb)(cast_shape, pose(0, 0, 0)); + OK(RAPIER_FN(LastStatus)()); + assert(aabb.maxs.x > 0); + OK(RAPIER_FN(FreeSharedShape)(cast_shape)); + RAPIER_TYPE(PointProjection) + projection = RAPIER_FN(ProjectPoint)(world, &query, vector(0, 3, 0), 10, 1); + OK(RAPIER_FN(LastStatus)()); + assert(projection.point.y < 1.1); + RAPIER_TYPE(QueryFilter) filter = RAPIER_FN(DefaultQueryFilter)(); + filter.exclude_collider = ball_collider; + query.filter = filter; + hit = RAPIER_FN(CastRay)(world, &query, vector(0, 10, 0), vector(0, -1, 0), 20, 1); + OK(RAPIER_FN(LastStatus)()); + assert(hit.collider.index == floor_h.index); + RAPIER_TYPE(Bytes) *snapshot; + const uint8_t *data; + size_t length; + snapshot = RAPIER_FN(SerializeWorld)(world); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(ByteView) bytesDataResult = RAPIER_FN(Bytes_Data)(snapshot); + data = bytesDataResult.data; + length = bytesDataResult.count; + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(World) *restored = RAPIER_FN(DeserializeWorld)(data, length); + OK(RAPIER_FN(LastStatus)()); + + OK(RAPIER_FN(RigidBody_ValidateHandle)(ball)); + RAPIER_TYPE(RigidBodyHandle) restored_ball = ball; + restored_ball.world = restored; /* Same snapshot indices, new owning allocation. */ + RAPIER_TYPE(Vector) restored_position = RAPIER_FN(RigidBody_Translation)(restored_ball); + OK(RAPIER_FN(LastStatus)()); + assert(fabs((double)(restored_position.y - position.y)) < 1e-6); + OK(RAPIER_FN(FreeBytes)(snapshot)); + OK(RAPIER_FN(Step)(restored, NULL, NULL)); + OK(RAPIER_FN(FreeWorld)(restored)); + uint8_t malformed[6] = {'R', + 'P', + 'R', + RAPIER_CONST(ABI_VERSION), + RAPIER_CONST(DIMENSION), + sizeof(RAPIER_TYPE(Real))}; + restored = RAPIER_FN(DeserializeWorld)(malformed, sizeof(malformed)); + EXPECT(RAPIER_FN(LastStatus)(), RAPIER_CONST(INVALID_ARGUMENT)); + + RAPIER_TYPE(ImpulseJointHandle) jh; + RAPIER_TYPE(RigidBodyHandle) anchor = add_body(world, RAPIER_CONST(FIXED), 5); + RAPIER_TYPE(JointDesc) joint = RAPIER_FN(RopeJointDesc)(5); + jh = RAPIER_FN(InsertImpulseJoint)(anchor, ball, &joint); + OK(RAPIER_FN(LastStatus)()); + OK(RAPIER_FN(ImpulseJoint_SetContactsEnabled)(jh, 0, 1)); + OK(RAPIER_FN(Step)(world, NULL, NULL)); + OK(RAPIER_FN(RemoveImpulseJoint)(jh, 1)); + EXPECT(RAPIER_FN(RemoveImpulseJoint)(jh, 1), RAPIER_CONST(INVALID_HANDLE)); + RAPIER_TYPE(MultibodyJointHandle) mh; + joint = RAPIER_FN(FixedJointDesc)(); + mh = RAPIER_FN(InsertMultibodyJoint)(anchor, ball, &joint); + OK(RAPIER_FN(LastStatus)()); + OK(RAPIER_FN(Step)(world, NULL, NULL)); + OK(RAPIER_FN(RemoveMultibodyJoint)(mh, 1)); + RAPIER_TYPE(SoftBodyDesc) + rope = RAPIER_FN(RopeSoftBodyDesc)(vector(3, 4, 0), vector(3, 2, 0), 8); + uint32_t pin = 0; + OK(RAPIER_FN(SoftBodyDesc_SetPinnedParticles)(&rope, (RAPIER_TYPE(IndexView)){&pin, 1})); + RAPIER_TYPE(SoftBodyHandle) sh = RAPIER_FN(InsertSoftBody)(world, &rope); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(Vector) particles[8]; + count = RAPIER_FN(SoftBody_ParticlePositions)(sh, particles, 8); + OK(RAPIER_FN(LastStatus)()); + assert(count == 8 && particles[0].y == 4); + for (int i = 0; i < 10; ++i) { + OK(RAPIER_FN(Step)(world, NULL, events)); + } + count = RAPIER_FN(SoftBody_ParticlePositions)(sh, particles, 8); + OK(RAPIER_FN(LastStatus)()); + assert(fabs((double)(particles[0].y - 4)) < 1e-5); + EXPECT(RAPIER_FN(SoftBody_SetParticlePosition)(sh, 99, vector(0, 0, 0)), + RAPIER_CONST(INVALID_ARGUMENT)); + RAPIER_TYPE(RigidBodyHandle) root = RAPIER_FN(SoftBody_RootBody)(sh); + OK(RAPIER_FN(LastStatus)()); + EXPECT(RAPIER_FN(RigidBody_SetTranslation)(root, vector(0, 0, 0), 1), + RAPIER_CONST(INVALID_ARGUMENT)); + position = RAPIER_FN(RigidBody_Translation)(root); + OK(RAPIER_FN(LastStatus)()); + count = RAPIER_FN(SoftBody_MeshColliders)(sh, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count > 0); + RAPIER_TYPE(ColliderHandle) mesh; + count = RAPIER_FN(SoftBody_MeshColliders)(sh, &mesh, 1); + OK(RAPIER_FN(LastStatus)()); + count = RAPIER_FN(SoftBody_MeshVertices)(sh, mesh, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count > 0); + RAPIER_TYPE(Vector) blade[RAPIER_CONST(DIMENSION)]; + blade[0] = vector(2, 3, 0); + blade[1] = vector(4, 3, 0); +#ifdef RAPIER_DIM3 + blade[0] = vector(2, 3, -1); + blade[1] = vector(4, 3, -1); + blade[2] = vector(3, 3, 1); +#endif + RAPIER_TYPE(SoftBodyTearEvent) *tear = RAPIER_FN(CutSoftBody)(sh, blade); + OK(RAPIER_FN(LastStatus)()); + assert(tear); + count = RAPIER_FN(SoftBodyTearEvent_Bodies)(tear, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count >= 1); + OK(RAPIER_FN(FreeSoftBodyTearEvent)(tear)); + OK(RAPIER_FN(RemoveSoftBody)(sh)); + EXPECT(RAPIER_FN(SoftBody_ValidateHandle)(sh), RAPIER_CONST(INVALID_HANDLE)); + RAPIER_TYPE(SharedShape) *character_shape = RAPIER_FN(BallSharedShape)((RAPIER_TYPE(Real))0.3); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(KinematicCharacterController) *character = + RAPIER_FN(NewKinematicCharacterController)(); + OK(RAPIER_FN(LastStatus)()); + query = RAPIER_FN(DefaultQueryOptions)(); + RAPIER_TYPE(CharacterMovement) + movement = RAPIER_FN(KinematicCharacterController_MoveShape)( + world, &query, character, (RAPIER_TYPE(Real))(1.0 / 60.0), character_shape, pose(5, 2, 0), + vector(0, -5, 0)); + OK(RAPIER_FN(LastStatus)()); + assert(movement.translation.y > -2 && movement.grounded); + OK(RAPIER_FN(FreeKinematicCharacterController)(character)); + OK(RAPIER_FN(FreeSharedShape)(character_shape)); +#ifdef RAPIER_DIM3 + RAPIER_TYPE(DynamicRayCastVehicleController) *vehicle = + RAPIER_FN(NewDynamicRayCastVehicleController)(ball); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(WheelTuning) tuning = RAPIER_FN(DefaultWheelTuning)(); + size_t wheel = RAPIER_FN(DynamicRayCastVehicleController_AddWheel)( + vehicle, vector(0, 0, 0), vector(0, -1, 0), vector(1, 0, 0), (RAPIER_TYPE(Real))0.4, + (RAPIER_TYPE(Real))0.3, &tuning); + OK(RAPIER_FN(LastStatus)()); + assert(wheel == 0); + OK(RAPIER_FN(DynamicRayCastVehicleController_UpdateVehicle)( + vehicle, (RAPIER_TYPE(Real))(1.0 / 60.0), NULL)); + OK(RAPIER_FN(FreeDynamicRayCastVehicleController)(vehicle)); +#endif + count = RAPIER_FN(DebugRender)(world, 1, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count > 0); + RAPIER_FN(RemoveRigidBody)(ball, 1); + OK(RAPIER_FN(LastStatus)()); + EXPECT(RAPIER_FN(RigidBody_ValidateHandle)(ball), RAPIER_CONST(INVALID_HANDLE)); + EXPECT(RAPIER_FN(Collider_ValidateHandle)(ball_collider), RAPIER_CONST(INVALID_HANDLE)); + RAPIER_TYPE(RigidBodyHandle) replacement = add_body(world, RAPIER_CONST(DYNAMIC), 3); + assert(replacement.index != ball.index || replacement.generation != ball.generation); + OK(RAPIER_FN(EventCollector_Clear)(events)); + count = RAPIER_FN(EventCollector_CollisionEvents)(events, NULL, 0); + OK(RAPIER_FN(LastStatus)()); + assert(count == 0); + OK(RAPIER_FN(FreeEventCollector)(events)); + OK(RAPIER_FN(FreeWorld)(world)); + printf("Rapier C integration passed: %uD, %zu-bit real\n", info.dimension, + 8 * sizeof(RAPIER_TYPE(Real))); + return 0; +} diff --git a/c/tests/pod.c b/c/tests/pod.c new file mode 100644 index 000000000..63b2b6413 --- /dev/null +++ b/c/tests/pod.c @@ -0,0 +1,252 @@ +#include "rapier.h" +#include +#include +#include +#include + +#define OK(call) \ + do { \ + RAPIER_TYPE(Status) status = (call); \ + if (status != RAPIER_CONST(OK)) { \ + fprintf(stderr, "%s: %s\n", #call, RAPIER_FN(LastError)()); \ + abort(); \ + } \ + } while (0) + +static RAPIER_TYPE(Bool) RAPIER_CALL reject(void *data, const RAPIER_TYPE(ReadContext) *read, + RAPIER_TYPE(ColliderHandle) handle) { + (void)read; + (void)handle; + ++*(unsigned *)data; + return 0; +} + +static void test_world_and_deformable_bindings(void) { + RAPIER_TYPE(World) *world = RAPIER_FN(NewWorld)(); + OK(RAPIER_FN(LastStatus)()); + + RAPIER_TYPE(RigidBodyHandle) a, b; + RAPIER_TYPE(ColliderHandle) colliderHandle; + RAPIER_TYPE(RigidBodyDesc) body = RAPIER_FN(DynamicRigidBodyDesc)(); + body.additionalMass = 1; + a = RAPIER_FN(InsertRigidBody)(world, &body); + OK(RAPIER_FN(LastStatus)()); + body.position.translation.x = 2; + b = RAPIER_FN(InsertRigidBody)(world, &body); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(ColliderDesc) collider = RAPIER_FN(BallColliderDesc)(0.5); + colliderHandle = RAPIER_FN(InsertCollider)(a, &collider); + OK(RAPIER_FN(LastStatus)()); + collider.position.translation.y = -10; + colliderHandle = RAPIER_FN(InsertColliderWithoutParent)(world, &collider); + OK(RAPIER_FN(LastStatus)()); + + RAPIER_TYPE(ImpulseJointHandle) impulse; + RAPIER_TYPE(MultibodyJointHandle) multi; + RAPIER_TYPE(JointDesc) joint = RAPIER_FN(FixedJointDesc)(); + impulse = RAPIER_FN(InsertImpulseJoint)(a, b, &joint); + OK(RAPIER_FN(LastStatus)()); + assert(impulse.index != UINT32_MAX); + multi = RAPIER_FN(InsertMultibodyJoint)(a, b, &joint); + OK(RAPIER_FN(LastStatus)()); + assert(multi.index != UINT32_MAX); + + RAPIER_TYPE(SoftBodyDesc) soft; + RAPIER_TYPE(SoftBodyHandle) softHandle; + RAPIER_TYPE(Vector) vertices[3] = {0}; + vertices[1].x = 1; + vertices[2].y = 1; + soft = RAPIER_FN(DefaultSoftBodyDesc)(); + soft.positions = (RAPIER_TYPE(VectorView)){vertices, 3}; + soft.collisionEnabled = 0; + softHandle = RAPIER_FN(InsertSoftBody)(world, &soft); + OK(RAPIER_FN(LastStatus)()); + + RAPIER_TYPE(RigidBodyHandle) root; + OK(RAPIER_FN(SoftBody_ValidateHandle)(softHandle)); + root = RAPIER_FN(SoftBody_RootBody)(softHandle); + OK(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(SoftBodyMaterial) material = RAPIER_FN(SoftBody_Material)(softHandle); + OK(RAPIER_FN(LastStatus)()); + material.edgeSoftness.natural_frequency = 45; + material.tearStrain = (RAPIER_TYPE(OptionalReal)){1, 0.5}; + OK(RAPIER_FN(SoftBody_SetMaterial)(softHandle, &material)); + material = RAPIER_FN(SoftBody_Material)(softHandle); + OK(RAPIER_FN(LastStatus)()); + assert(material.edgeSoftness.natural_frequency == 45 && material.tearStrain.enabled); + + collider = RAPIER_FN(DefaultColliderDesc)(); + collider.isSensor = 1; + collider.shape.vertices = (RAPIER_TYPE(VectorView)){vertices, 3}; +#ifdef RAPIER_DIM3 + uint32_t indices[] = {0, 1, 2}; + collider.shape.kind = RAPIER_CONST(SHAPE_DESC_TRIMESH); + collider.shape.flags = RAPIER_CONST(TRIMESH_DEFORMABLE); +#else + uint32_t indices[] = {0, 1, 1, 2}; + collider.shape.kind = RAPIER_CONST(SHAPE_DESC_POLYLINE); + collider.shape.flags = RAPIER_CONST(POLYLINE_DEFORMABLE); +#endif +#ifdef RAPIER_DIM3 + collider.shape.triangles = + (RAPIER_TYPE(TriangleView)){(const RAPIER_TYPE(Triangle) *)indices, 1}; +#else + collider.shape.edges = (RAPIER_TYPE(EdgeView)){(const RAPIER_TYPE(Edge) *)indices, 2}; +#endif + RAPIER_TYPE(SoftMeshBindingDesc) binding = RAPIER_FN(DefaultSoftMeshBindingDesc)(); + uint32_t particleIndices[] = {0, 1, 2}; + binding.kind = RAPIER_CONST(SOFT_BINDING_DIRECT); + binding.particles = (RAPIER_TYPE(IndexView)){particleIndices, 3}; + colliderHandle = RAPIER_FN(InsertDeformableCollider)(&collider, &binding, root); + OK(RAPIER_FN(LastStatus)()); + particleIndices[2] = 999; + vertices[1].x = 100; + OK(RAPIER_FN(Step)(world, NULL, NULL)); + OK(RAPIER_FN(SoftBody_ValidateHandle)(softHandle)); + size_t count; + RAPIER_TYPE(Vector) copied[3]; + count = RAPIER_FN(SoftBody_MeshVertices)(softHandle, colliderHandle, copied, 3); + OK(RAPIER_FN(LastStatus)()); + assert(count == 3 && copied[1].x < 2); + OK(RAPIER_FN(FreeWorld)(world)); +} + +int main(void) { + test_world_and_deformable_bindings(); + RAPIER_TYPE(PodLayout) layout = RAPIER_FN(PodLayout)(); + assert(layout.rigidBodyDesc == sizeof(RAPIER_TYPE(RigidBodyDesc))); + assert(layout.colliderDesc == sizeof(RAPIER_TYPE(ColliderDesc))); + assert(layout.shapeDesc == sizeof(RAPIER_TYPE(ShapeDesc))); + assert(layout.jointDesc == sizeof(RAPIER_TYPE(JointDesc))); + assert(layout.softBodyMaterial == sizeof(RAPIER_TYPE(SoftBodyMaterial))); + assert(layout.integrationParameters == sizeof(RAPIER_TYPE(IntegrationParameters))); + assert(layout.softBodyDesc == sizeof(RAPIER_TYPE(SoftBodyDesc))); + assert(layout.softMeshBindingDesc == sizeof(RAPIER_TYPE(SoftMeshBindingDesc))); + assert(layout.queryOptions == sizeof(RAPIER_TYPE(QueryOptions))); + + RAPIER_TYPE(World) *world = RAPIER_FN(NewWorld)(); + OK(RAPIER_FN(LastStatus)()); + + RAPIER_TYPE(RigidBodyDesc) body = RAPIER_FN(DynamicRigidBodyDesc)(); + RAPIER_TYPE(ColliderDesc) collider = RAPIER_FN(BallColliderDesc)(0.5); + body.canSleep = 0; + body.position.translation.y = 5; + body.userData.low = 42; + RAPIER_TYPE(RigidBodyHandle) first, second; + RAPIER_TYPE(ColliderHandle) firstCollider; + first = RAPIER_FN(InsertRigidBody)(world, &body); + OK(RAPIER_FN(LastStatus)()); + firstCollider = RAPIER_FN(InsertCollider)(first, &collider); + OK(RAPIER_FN(LastStatus)()); + body.position.translation.x = 2; + second = RAPIER_FN(InsertRigidBody)(world, &body); + OK(RAPIER_FN(LastStatus)()); + RAPIER_FN(InsertCollider)(second, &collider); + OK(RAPIER_FN(LastStatus)()); + + RAPIER_TYPE(Vector) position; + OK(RAPIER_FN(RigidBody_ValidateHandle)(first)); + position = RAPIER_FN(RigidBody_Translation)(first); + OK(RAPIER_FN(LastStatus)()); + assert(position.x == 0 && position.y == 5); + RAPIER_TYPE(UserData) userData = RAPIER_FN(RigidBody_UserData)(first); + OK(RAPIER_FN(LastStatus)()); + assert(userData.low == 42); + + RAPIER_TYPE(JointDesc) joint = RAPIER_FN(SpringJointDesc)(2, 100, 1); + RAPIER_TYPE(ImpulseJointHandle) + jointHandle = RAPIER_FN(InsertImpulseJoint)(first, second, &joint); + OK(RAPIER_FN(LastStatus)()); + assert(jointHandle.index != UINT32_MAX); + joint.motors[0].stiffness = -1; + RAPIER_FN(InsertImpulseJoint)(first, second, &joint); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(INVALID_ARGUMENT)); + + RAPIER_TYPE(IntegrationParameters) settings = RAPIER_FN(IntegrationParameters)(world); + OK(RAPIER_FN(LastStatus)()); + settings.dt = (RAPIER_TYPE(Real))(1.0 / 120.0); + OK(RAPIER_FN(SetIntegrationParameters)(world, &settings)); + settings.dt = -1; + assert(RAPIER_FN(SetIntegrationParameters)(world, &settings) == RAPIER_CONST(INVALID_ARGUMENT)); + settings = RAPIER_FN(IntegrationParameters)(world); + OK(RAPIER_FN(LastStatus)()); + assert(settings.dt == (RAPIER_TYPE(Real))(1.0 / 120.0)); + + OK(RAPIER_FN(Step)(world, NULL, NULL)); + RAPIER_TYPE(QueryOptions) query = RAPIER_FN(DefaultQueryOptions)(); + RAPIER_TYPE(Vector) origin = {0}, direction = {0}; + origin.y = 10; + direction.y = -1; + RAPIER_TYPE(Bool) found = 0; + RAPIER_TYPE(RayHit) hit; + RAPIER_TYPE(OptionalRayHit) + tryCastRayResult2 = RAPIER_FN(TryCastRay)(world, &query, origin, direction, 20, 1); + hit = tryCastRayResult2.hit; + found = tryCastRayResult2.found; + OK(RAPIER_FN(LastStatus)()); + assert(found && hit.collider.index == firstCollider.index); + unsigned calls = 0; + query.predicate = reject; + query.userData = &calls; + RAPIER_TYPE(OptionalRayHit) + tryCastRayResult3 = RAPIER_FN(TryCastRay)(world, &query, origin, direction, 20, 1); + hit = tryCastRayResult3.hit; + found = tryCastRayResult3.found; + OK(RAPIER_FN(LastStatus)()); + assert(!found && calls); + query.predicate = NULL; + OK(RAPIER_FN(Step)(world, NULL, NULL)); + RAPIER_TYPE(OptionalRayHit) + tryCastRayResult4 = RAPIER_FN(TryCastRay)(world, &query, origin, direction, 20, 1); + hit = tryCastRayResult4.hit; + found = tryCastRayResult4.found; + OK(RAPIER_FN(LastStatus)()); + assert(found); + + /* A copied description borrows arrays until insertion. The world then owns copies. */ + RAPIER_TYPE(SoftBodyDesc) soft = RAPIER_FN(DefaultSoftBodyDesc)(); + RAPIER_TYPE(Vector) particles[2] = {0}; + particles[0].y = particles[1].y = 10; + particles[1].x = 1; + RAPIER_TYPE(Edge) edges[] = {{0, 1}}; + soft.positions = (RAPIER_TYPE(VectorView)){particles, 2}; + soft.edges = (RAPIER_TYPE(EdgeView)){edges, 1}; + soft.collisionEnabled = 0; + soft.canSleep = 0; + soft.material.edgeSoftness.natural_frequency = 40; + RAPIER_TYPE(SoftBodyHandle) softHandle = RAPIER_FN(InsertSoftBody)(world, &soft); + OK(RAPIER_FN(LastStatus)()); + particles[1].x = 100; + edges[0].b = 999; + + OK(RAPIER_FN(SoftBody_ValidateHandle)(softHandle)); + position = RAPIER_FN(SoftBody_ParticlePosition)(softHandle, 1); + OK(RAPIER_FN(LastStatus)()); + assert(position.x == 1); + RAPIER_TYPE(SoftBodyHandle) sentinel = {NULL, 123, 456}; + sentinel = RAPIER_FN(InsertSoftBody)(world, &soft); + assert(RAPIER_FN(LastStatus)() == RAPIER_CONST(INVALID_ARGUMENT)); + assert(sentinel.index == UINT32_MAX && sentinel.generation == UINT32_MAX); + + /* Retained shared shapes remain usable after the caller releases its reference. */ + RAPIER_TYPE(SharedShape) *shape = RAPIER_FN(BallSharedShape)(0.75); + OK(RAPIER_FN(LastStatus)()); + collider = RAPIER_FN(DefaultColliderDesc)(); + collider.shape.kind = RAPIER_CONST(SHAPE_DESC_SHARED); + collider.shape.sharedShape = shape; + collider.position.translation.x = 10; + RAPIER_FN(InsertColliderWithoutParent)(world, &collider); + OK(RAPIER_FN(LastStatus)()); + OK(RAPIER_FN(FreeSharedShape)(shape)); + OK(RAPIER_FN(Step)(world, NULL, NULL)); + origin.x = 10; + RAPIER_TYPE(OptionalRayHit) + tryCastRayResult5 = RAPIER_FN(TryCastRay)(world, &query, origin, direction, 20, 1); + hit = tryCastRayResult5.hit; + found = tryCastRayResult5.found; + OK(RAPIER_FN(LastStatus)()); + assert(found && isfinite(hit.time_of_impact)); + /* The only remaining owner is world. POD values and borrowed views need no cleanup. */ + OK(RAPIER_FN(FreeWorld)(world)); + return 0; +} diff --git a/c/tools/export_names.py b/c/tools/export_names.py new file mode 100644 index 000000000..986e222db --- /dev/null +++ b/c/tools/export_names.py @@ -0,0 +1,52 @@ +"""Read C export names from the same receiver annotations used by rapier_export.""" +from pathlib import Path +import re + +ROOT = Path(__file__).resolve().parents[2] +EXPORT_ATTRIBUTE = r"#\[rapier_export(?:\(([a-z][a-z0-9_]*)\))?\]" +DECLARATION = r'pub (?:unsafe )?extern "C" fn (rpr_[a-z0-9_]+)' + + +def pascal_case(name): + if not re.fullmatch(r"[a-z0-9]+(?:_[a-z0-9]+)*", name): + raise ValueError(f"Invalid snake_case export name: {name}") + return "".join(word[0].upper() + word[1:] for word in name.split("_")) + + +def export_suffix(name, receiver=None): + if not name.startswith("rpr_"): + raise ValueError(f"Missing rpr_ prefix: {name}") + suffix = name[4:] + if receiver is None: + return pascal_case(suffix) + if not suffix.startswith(receiver + "_"): + raise ValueError(f"{name} does not start with receiver {receiver}") + return pascal_case(receiver) + "_" + pascal_case(suffix[len(receiver) + 1:]) + + +def read_exports(): + exports = {} + # Attributes may be followed by cfg attributes and documentation comments. + pattern = EXPORT_ATTRIBUTE + r"(?:(?!#\[rapier_export)[\s\S])*?" + DECLARATION + for source in sorted((ROOT / "c/src").glob("*.rs")): + text = source.read_text() + declarations = set(re.findall(DECLARATION, text)) + annotated = set() + for receiver, name in re.findall(pattern, text): + suffix = export_suffix(name, receiver or None) + if name in exports and exports[name] != suffix: + raise ValueError(f"Conflicting export annotations: {name}") + exports[name] = suffix + annotated.add(name) + if declarations != annotated: + raise ValueError(f"Missing export annotation in {source}: {declarations - annotated}") + if len(set(exports.values())) != len(exports): + raise ValueError("Duplicate C export names") + return exports + + +EXPORTS = read_exports() + + +def c_name(name, dimension): + return f"r{dimension}{EXPORTS[name]}" diff --git a/c/tools/generate-header.py b/c/tools/generate-header.py new file mode 100644 index 000000000..61e467a2e --- /dev/null +++ b/c/tools/generate-header.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Generate dimension-specific C names from the shared Rust binding source.""" +from pathlib import Path +import re +import subprocess +import sys +import tempfile + +from export_names import EXPORT_ATTRIBUTE, c_name + +ROOT = Path(__file__).resolve().parents[2] +DESTINATION = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "c/include/rapier.h" + + +with tempfile.TemporaryDirectory(prefix="rapier-header-") as tmp: + project = Path(tmp) + source_dir = project / "src" + source_dir.mkdir() + # cbindgen parses source without expanding custom attributes. Give it a + # temporary declaration view where our export marker is the standard one. + # Signatures, docs, types, and feature gates all come from the real source. + sources = sorted((ROOT / "c/src").glob("*.rs")) + for source in sources: + text = re.sub(EXPORT_ATTRIBUTE, "#[unsafe(no_mangle)]", source.read_text()) + (source_dir / source.name).write_text(text) + (project / "Cargo.toml").write_text('''[package] +name = "rapier-c-header" +version = "0.0.0" +edition = "2024" +[workspace] +[features] +default = ["dim3", "f32"] +dim2 = [] +dim3 = [] +f32 = [] +f64 = [] +fem = [] +parallel = [] +robotics = [] +''') + raw = project / "rapier.h" + subprocess.run([ + "cbindgen", "--config", str(ROOT / "c/cbindgen.toml"), + "--crate", "rapier-c-header", "--output", str(raw), + ], cwd=project, check=True) + text = raw.read_text() + # MSVC requires __cdecl after the return type, notably for struct returns. + # cbindgen's function prefix goes before that type, so insert the calling + # convention at the function declarator instead. + text = re.sub(r"(RAPIER_API\s+[\w\s*]+?)\b(rpr_[a-z0-9_]+)\(", + r"\1RAPIER_CALL \2(", text) + # Transparent native wrappers remain opaque to C. + for source in sources: + for name, native in re.findall(r"pub struct (Rpr\w+)\(pub\(crate\) (\w+)\)", source.read_text()): + text = text.replace(f"typedef {native} {name};", f"typedef struct {name} {name};") + for name in ["RprPairFilter", "RprModifyContacts", "RprErrorCallback", "RprModifyContactContext", "RprIkJointCanMove", "RprQueryPredicate"]: + text = text.replace(f"(*{name})", f"(RAPIER_CALL *{name})") + text = text.replace("\n#endif\n ;", ";\n#endif") + + # cbindgen emits this mutually recursive pair in pointer-dependency order. + # C requires the by-value ShapeDesc field to be complete first. + compound = re.search(r"(?:/\*\*(?:(?!\*/).)*\*/\s*)?typedef struct RprCompoundShapeDesc \{.*?\} RprCompoundShapeDesc;\n", text, re.S) + if compound: + declaration = compound[0] + text = text[:compound.start()] + text[compound.end():] + shape_end = text.index("} RprShapeDesc;") + len("} RprShapeDesc;") + text = text[:shape_end] + "\n\n" + declaration + text[shape_end:] + + # Rust stays dimension-neutral. Emit concrete C type, constant, and function + # names for each selected dimension, without compatibility aliases. + start = list(re.finditer(r"^#include <[^>]+>\n", text, re.M))[-1].end() + end = text.rindex("#endif /* RAPIER_H */") + declarations = text[start:end] + variants = [] + for dim in (2, 3): + variant = re.sub(r"\brpr_[a-z0-9_]+\b", lambda m: c_name(m[0], dim), declarations) + variant = re.sub(r"\bRpr(\w+)\b", lambda m: f"R{dim}{m[1]}", variant) + variant = re.sub(r"\bRPR_(\w+)\b", lambda m: f"R{dim}_{m[1]}", variant) + variants.append(variant) + text = (text[:start] + "\n#if defined(RAPIER_DIM2)\n" + variants[0] + + "#else /* RAPIER_DIM3 */\n" + variants[1] + "#endif\n\n" + text[end:]) + DESTINATION.parent.mkdir(parents=True, exist_ok=True) + DESTINATION.write_text(text) diff --git a/c/tools/generate-header.sh b/c/tools/generate-header.sh new file mode 100644 index 000000000..c9c408842 --- /dev/null +++ b/c/tools/generate-header.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +set -eu +exec python3 "$(dirname -- "$0")/generate-header.py" "$@" diff --git a/c/tools/test-native.py b/c/tools/test-native.py new file mode 100644 index 000000000..d0150cc80 --- /dev/null +++ b/c/tools/test-native.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Test all native ABIs, exported names, linkage modes, and dimension coexistence.""" +import ctypes +import json +import os +from pathlib import Path +import re +import shlex +import subprocess +import sys +import tempfile + +from export_names import EXPORTS, c_name + +ROOT = Path(__file__).resolve().parents[2] +VERSION = (ROOT / "c/VERSION").read_text().strip() +CC = shlex.split(os.environ.get("CC", "cc")) +CXX = shlex.split(os.environ.get("CXX", "c++")) +VARIANTS = [(d, p) for d in (2, 3) for p in (32, 64)] +PACKAGES = [f"rapier{d}d" + ("-f64" if p == 64 else "") + "-ffi" for d, p in VARIANTS] +subprocess.run(["cargo", "build", *[arg for package in PACKAGES for arg in ("-p", package)]], + cwd=ROOT, check=True) +metadata = subprocess.check_output(["cargo", "metadata", "--no-deps", "--format-version", "1"], + cwd=ROOT, text=True) +LIBDIR = Path(json.loads(metadata)["target_directory"]) / "debug" +SYSTEM_LIBS = ["-lpthread", "-lm"] + (["-framework", "Security", "-framework", "CoreFoundation"] + if sys.platform == "darwin" else ["-ldl", "-lrt", "-lutil"]) + + +def link_flags(libraries, linkage): + if linkage == "shared": + return [f"-L{LIBDIR}", *[f"-l{lib}" for lib in libraries], f"-Wl,-rpath,{LIBDIR}"] + return [*[str(LIBDIR / f"lib{lib}.a") for lib in libraries], *SYSTEM_LIBS] + + +rust_names = set(EXPORTS) + +with tempfile.TemporaryDirectory(prefix="rapier-c-tests-") as directory: + temp = Path(directory) + for (dim, precision), package in zip(VARIANTS, PACKAGES): + library = package.replace("-", "_") + flags = ["-Wall", "-Wextra", "-Werror", "-UNDEBUG", "-I", str(ROOT / "c/include"), + f"-DRAPIER_DIM{dim}", f"-DRAPIER_F{precision}", + f'-DRAPIER_EXPECTED_VERSION="{VERSION}"'] + header = subprocess.check_output([*CC, "-E", "-P", "-x", "c", *flags, + str(ROOT / "c/include/rapier.h")], text=True) + symbols = set(re.findall(r"\b(r[23][A-Z]\w*)\s*\(", header)) + assert symbols and all(name.startswith(f"r{dim}") for name in symbols) + assert f"r{dim}InsertRigidBody" in symbols + # Reject the replaced abbreviated free-function names. + for old, new in { + "RemoveBody": "RemoveRigidBody", + "InsertDeformable": "InsertDeformableCollider", + "ActiveBodies": "ActiveRigidBodies", + "Parameters": "IntegrationParameters", + "SetParameters": "SetIntegrationParameters", + "Serialize": "SerializeWorld", + "Deserialize": "DeserializeWorld", + "RigidBodyLen": "RigidBodyCount", + "ColliderLen": "ColliderCount", + "SoftBodyLen": "SoftBodyCount", + "ReadRigidBodyLen": "ReadRigidBodyCount", + "ReadColliderLen": "ReadColliderCount", + "Dt": "TimeStep", + "SetDt": "SetTimeStep", + }.items(): + assert f"r{dim}{old}" not in symbols + assert f"r{dim}{new}" in symbols + assert f"r{dim}InsertColliderWithoutParent" in symbols + assert f"r{dim}Insert" not in symbols and f"r{dim}InsertBody" not in symbols + assert not re.search(rf"\bR{dim}BodyCollider\b", header) + assert not re.search(r"\brpr_\w+\s*\(", header) + assert not re.search(r"\b(?:Rpr\w*|RPR_\w+|R" + str(5 - dim) + r"[A-Z_]\w*)\b", header) + macros = subprocess.check_output([*CC, "-E", "-dM", "-x", "c", *flags, + str(ROOT / "c/include/rapier.h")], text=True) + assert re.search(rf"^#define R{dim}_OK 0$", macros, re.M) + assert re.search(rf"^#define R{dim}_ABI_VERSION 1$", macros, re.M) + assert not re.search(r"^#define (?:RPR_|R" + str(5 - dim) + r"_)", macros, re.M) + # Produced values must use direct returns, not the former scalar output pointers. + declarations = re.findall(r"\br[23][A-Z]\w*\([^;]*?\);", header, re.S) + assert not any(re.search(r"\*\s*(?:out(?:_\w+)?|count|found)\b", d) + for d in declarations) + # ABI 7 has one owner: no legacy component objects, query views, or aliases. + for legacy_type in ("PhysicsWorld", "PhysicsPipeline", "CollisionPipeline", + "RigidBodySet", "ColliderSet", "SoftBodySet", + "ImpulseJointSet", "MultibodyJointSet", "QueryView"): + assert not re.search(rf"\bR{dim}" + legacy_type + r"\b", header), legacy_type + assert not any(re.match(r"r[23](PhysicsWorld|PhysicsPipeline|CollisionPipeline|QueryView)", + symbol) for symbol in symbols) + # Compare the entire dynamic export set, not just the names used in examples. + extension = "dylib" if sys.platform == "darwin" else "so" + binary = LIBDIR / f"lib{library}.{extension}" + nm_args = ["-gU"] if sys.platform == "darwin" else ["-D", "--defined-only"] + exports = subprocess.check_output(["nm", *nm_args, str(binary)], text=True) + actual = set(re.findall(r"\b(r[23][A-Z]\w*)$", exports, re.MULTILINE)) + # macOS nm prints a leading underscore before C symbols. + if sys.platform == "darwin": + actual = set(re.findall(r"\b_(r[23][A-Z]\w*)$", exports, re.MULTILINE)) + assert actual == symbols, (package, "missing", symbols - actual, "unexpected", actual - symbols) + assert not re.search(r"\b_?rpr_\w+$", exports, re.MULTILINE) + loaded = ctypes.CDLL(str(binary)) + for name in rust_names: + assert not hasattr(loaded, name), (package, "legacy export", name) + assert not hasattr(loaded, c_name(name, 5 - dim)), (package, "wrong dimension", name) + if "_" in EXPORTS[name]: + old_name = c_name(name, dim).replace("_", "") + assert not hasattr(loaded, old_name), (package, "obsolete method export", old_name) + + symbol_source = temp / "symbols.c" + symbol_source.write_text( + '#include "rapier.h"\nstatic void (*const symbols[])(void) = {\n' + + "".join(f" (void (*)(void)){name},\n" for name in sorted(symbols)) + + "};\nint main(void) { for (unsigned i = 0; i < sizeof(symbols)/sizeof(symbols[0]); ++i) " + "if (!symbols[i]) return 1; return 0; }\n") + for linkage in ("shared", "static"): + for source, compiler, standard in [(ROOT / "c/tests/integration.c", CC, "c11"), + (ROOT / "c/tests/cpp.cpp", CXX, "c++17"), + (ROOT / "c/tests/pod.c", CC, "c11"), + (ROOT / "c/tests/handles.c", CC, "c11"), + (ROOT / "c/tests/initializers.c", CC, "c11"), + (ROOT / "c/tests/initializers.cpp", CXX, "c++17"), + (ROOT / "c/tests/array_views.c", CC, "c11"), + (ROOT / "c/tests/array_views.cpp", CXX, "c++17"), + (symbol_source, CC, "c11")]: + output = temp / f"{source.stem}-{library}-{linkage}" + subprocess.run([*compiler, f"-std={standard}", *flags, str(source), + *link_flags([library], linkage), "-o", str(output)], check=True) + subprocess.run([str(output)], check=True) + print(f"{package}: C, C++, and {len(symbols)} exports passed shared + static linking", flush=True) + + # Each dimension keeps its own POD layouts in a separate translation unit. + # Both libraries must coexist in one executable without symbol collisions. + main = temp / "both.c" + main.write_text("int run2(void); int run3(void); int main(void) { return run2() || run3(); }\n") + for precision in (32, 64): + objects, libraries = [], [] + for dim in (2, 3): + source, obj = temp / f"dim{dim}.c", temp / f"dim{dim}.o" + source.write_text(f'''#include "rapier.h" +int run{dim}(void) {{ + R{dim}World *world = 0; + if (r{dim}CheckAbi(R{dim}_ABI_VERSION, {dim}, sizeof(R{dim}Real), sizeof(R{dim}Vector), sizeof(R{dim}Pose))) return 1; + world = r{dim}NewWorld(); + if (r{dim}LastStatus()) return 2; + R{dim}Status status = r{dim}Step(world, 0, 0); + return r{dim}FreeWorld(world) || status; +}} +''') + subprocess.run([*CC, "-std=c11", "-Wall", "-Wextra", "-Werror", + f"-DRAPIER_DIM{dim}", f"-DRAPIER_F{precision}", "-I", str(ROOT / "c/include"), + "-c", str(source), "-o", str(obj)], check=True) + objects.append(str(obj)) + libraries.append(f"rapier{dim}d" + ("_f64" if precision == 64 else "") + "_ffi") + for linkage in ("shared", "static"): + output = temp / f"both-f{precision}-{linkage}" + subprocess.run([*CC, str(main), *objects, *link_flags(libraries, linkage), + "-o", str(output)], check=True) + subprocess.run([str(output)], check=True) + print(f"2D + 3D / f{precision}: coexistence passed shared + static linking", flush=True) diff --git a/src/pipeline/physics_world.rs b/src/pipeline/physics_world.rs index fef03e24d..5bc0b4655 100644 --- a/src/pipeline/physics_world.rs +++ b/src/pipeline/physics_world.rs @@ -12,7 +12,8 @@ use crate::geometry::{ }; use crate::math::{DIM, Real, Vector}; use crate::pipeline::{ - EventHandler, PhysicsHooks, PhysicsPipeline, Quarantine, QueryFilter, QueryPipeline, + CollisionPipeline, EventHandler, PhysicsHooks, PhysicsPipeline, Quarantine, QueryFilter, + QueryPipeline, }; use parry::bounding_volume::{Aabb, BoundingVolume}; use parry::partitioning::BvhNode; @@ -68,6 +69,9 @@ pub struct PhysicsWorld { /// The main simulation pipeline that orchestrates each physics step. #[cfg_attr(feature = "serde-serialize", serde(skip))] pub physics_pipeline: PhysicsPipeline, + /// Workspace for collision-only updates that do not integrate positions or solve constraints. + #[cfg_attr(feature = "serde-serialize", serde(skip))] + pub collision_pipeline: CollisionPipeline, /// Manages active/sleeping body groups (islands) for efficient simulation. pub islands: IslandManager, /// The broad-phase acceleration structure for fast spatial queries. @@ -97,6 +101,7 @@ impl Default for PhysicsWorld { gravity: Vector::Y * -9.81, integration_parameters: IntegrationParameters::default(), physics_pipeline: PhysicsPipeline::new(), + collision_pipeline: CollisionPipeline::new(), islands: IslandManager::new(), broad_phase: DefaultBroadPhase::default(), narrow_phase: NarrowPhase::new(), @@ -163,6 +168,23 @@ impl PhysicsWorld { ); } + /// Update broad-phase and narrow-phase collision detection without advancing simulation. + /// + /// Uses the prediction distance from this world's integration parameters. This is useful + /// after editing transforms when contacts and scene queries must be refreshed immediately. + pub fn detect_collisions(&mut self, hooks: &dyn PhysicsHooks, events: &dyn EventHandler) { + self.collision_pipeline.step( + self.integration_parameters.prediction_distance(), + &mut self.islands, + &mut self.broad_phase, + &mut self.narrow_phase, + &mut self.bodies, + &mut self.colliders, + hooks, + events, + ); + } + /// The bodies and colliders automatically disabled during the last step because their /// state became non-finite; see [`Quarantine`]. pub fn quarantine(&self) -> &Quarantine { @@ -222,6 +244,17 @@ impl PhysicsWorld { /// /// Returns the removed body, or `None` if the handle was invalid. pub fn remove_body(&mut self, handle: RigidBodyHandle) -> Option { + self.remove_body_with_colliders(handle, true) + } + + /// Remove a rigid body and its joints, optionally preserving attached colliders. + /// + /// Preserved colliders become standalone colliders. Returns `None` for an invalid handle. + pub fn remove_body_with_colliders( + &mut self, + handle: RigidBodyHandle, + remove_attached_colliders: bool, + ) -> Option { self.bodies.remove( handle, &mut self.islands, @@ -229,7 +262,7 @@ impl PhysicsWorld { &mut self.impulse_joints, &mut self.multibody_joints, &mut self.soft_bodies, - true, + remove_attached_colliders, ) } diff --git a/src_testbed/physics/mod.rs b/src_testbed/physics/mod.rs index 3db7ee6f5..63747fd7b 100644 --- a/src_testbed/physics/mod.rs +++ b/src_testbed/physics/mod.rs @@ -38,6 +38,7 @@ pub fn restore_world(world: &mut PhysicsWorld, snapshot: &PhysicsSnapshot) -> us gravity, integration_parameters, physics_pipeline: _, + collision_pipeline: _, islands, broad_phase, narrow_phase,