feat(engines/godot): Godot hébergé par l'adaptateur, scène SEQUA poussée dans Godot
Option SEQUA_BUILD_GODOT_RUNTIME (preset windows-msvc-prototypes, Windows seulement) : godot-cpp compilé avec l'API du fork si elle a été extraite, même bibliothèque d'exécution MSVC que SEQUA. GodotHost charge la DLL LibGodot, initialise godot-cpp en extension hôte et pilote iteration() ; GodotScene crée, déplace et détruit les nœuds sous SequaScene à partir des deltas validés par SceneState ; conversions pures (rotation, couleur sRGB, formes) testées sans moteur. Démo sequa_godot_demo (scène SIMULÉE) : 300 images, arrêt propre. La DLL de Godot n'est déchargée qu'après les globaux de godot-cpp (init_seg), sinon la sortie du processus plante. Frontière binaire de l'ADR 0008 reportée à l'étape 4 (README de l'adaptateur).
This commit is contained in:
1 parent
9358bd404e
commit
c6aa05680e
18 files changed
+1266
-22
No files matched your search
+2
-1
@@ -16,6 +16,7 @@ option(SEQUA_WARNINGS_AS_ERRORS "Avertissements traités comme erreurs" ON)
|
||||
option(SEQUA_CLANG_TIDY "Lancer clang-tidy pendant la compilation" OFF)
|
||||
set(SEQUA_SANITIZERS "" CACHE STRING "Sanitizers à activer (liste : address;undefined)")
|
||||
option(SEQUA_BUILD_PROTOTYPES "Construire les prototypes jetables (prototypes/)" OFF)
|
||||
option(SEQUA_BUILD_GODOT_RUNTIME "Héberger Godot dans l'adaptateur (godot-cpp ; Godot construit par scripts/build-godot.ps1)" OFF)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -32,7 +33,7 @@ endif()
|
||||
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(3d)
|
||||
add_subdirectory(engines/godot/adapter) # adaptateur seul : Godot lui-même n'est pas compilé ici
|
||||
add_subdirectory(engines/godot/adapter) # Godot lui-même n'est pas compilé ici (SCons, scripts/build-godot.ps1)
|
||||
if(SEQUA_BUILD_SERVER)
|
||||
add_subdirectory(studio/host) # dépend d'IXWebSocket et nlohmann/json
|
||||
endif()
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@
|
||||
"name": "windows-msvc-prototypes",
|
||||
"displayName": "Windows MSVC Release + prototypes",
|
||||
"inherits": "windows-msvc-release",
|
||||
"cacheVariables": { "SEQUA_BUILD_PROTOTYPES": "ON" }
|
||||
"cacheVariables": { "SEQUA_BUILD_PROTOTYPES": "ON", "SEQUA_BUILD_GODOT_RUNTIME": "ON" }
|
||||
},
|
||||
{
|
||||
"name": "linux-base",
|
||||
|
||||
@@ -1,18 +1,73 @@
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# Adaptateur SEQUA 3D vers Godot (côté SEQUA). Ne compile pas encore Godot : le moteur
|
||||
# (sous-module ../vendor/godot-src, tag 4.7.2-stable) se construit avec SCons en LibGodot ;
|
||||
# godot-cpp (../vendor/godot-cpp) sera ajouté quand l'hébergement sera écrit (ADR 0007).
|
||||
# Adaptateur SEQUA 3D vers Godot (côté SEQUA). Godot lui-même se construit avec SCons en
|
||||
# LibGodot (scripts/build-godot.ps1) ; avec SEQUA_BUILD_GODOT_RUNTIME, godot-cpp
|
||||
# (../vendor/godot-cpp) est compilé ici pour piloter Godot depuis l'adaptateur (ADR 0007).
|
||||
|
||||
add_library(sequa_godot_adapter src/godot_adapter.cpp)
|
||||
add_library(sequa_godot_adapter src/godot_adapter.cpp src/conversions.cpp)
|
||||
add_library(sequa::godot_adapter ALIAS sequa_godot_adapter)
|
||||
|
||||
target_include_directories(sequa_godot_adapter PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||
target_link_libraries(sequa_godot_adapter PUBLIC sequa::3d)
|
||||
sequa_target_defaults(sequa_godot_adapter)
|
||||
|
||||
if(SEQUA_BUILD_GODOT_RUNTIME)
|
||||
set(_godot_src "${PROJECT_SOURCE_DIR}/engines/godot/vendor/godot-src")
|
||||
set(SEQUA_GODOT_LIBRARY "${_godot_src}/bin/godot.windows.template_debug.x86_64.dll"
|
||||
CACHE FILEPATH "Godot construit en bibliothèque (LibGodot)")
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "SEQUA_BUILD_GODOT_RUNTIME : Windows seulement pour l'instant")
|
||||
endif()
|
||||
if(NOT EXISTS "${SEQUA_GODOT_LIBRARY}")
|
||||
message(FATAL_ERROR "Godot absent (${SEQUA_GODOT_LIBRARY}) : lancer scripts/build-godot.ps1")
|
||||
endif()
|
||||
|
||||
# Liste de l'API extraite du fork (scripts/build-godot.ps1 -Target editor, décision de
|
||||
# l'étape 3). À défaut, celle de godot-cpp pour la 4.7 : identique tant que le fork
|
||||
# n'ajoute rien à l'API.
|
||||
set(_godot_api "${PROJECT_SOURCE_DIR}/build/godot/extension_api.json")
|
||||
if(EXISTS "${_godot_api}")
|
||||
set(GODOTCPP_CUSTOM_API_FILE "${_godot_api}" CACHE FILEPATH "" FORCE)
|
||||
set(GODOTCPP_GDEXTENSION_DIR "${_godot_src}/core/extension" CACHE PATH "" FORCE)
|
||||
message(STATUS "godot-cpp : API du fork (${_godot_api})")
|
||||
else()
|
||||
set(GODOTCPP_CUSTOM_API_FILE "" CACHE FILEPATH "" FORCE)
|
||||
set(GODOTCPP_API_VERSION "4.7" CACHE STRING "" FORCE)
|
||||
message(STATUS "godot-cpp : API officielle 4.7 (extraire celle du fork : scripts/build-godot.ps1 -Target editor)")
|
||||
endif()
|
||||
set(GODOTCPP_TARGET "template_debug" CACHE STRING "" FORCE)
|
||||
# Même bibliothèque d'exécution que le reste de SEQUA (/MD, /MDd en Debug) : godot-cpp
|
||||
# choisirait sinon /MT pour tout le projet.
|
||||
set(GODOTCPP_USE_STATIC_CPP OFF CACHE BOOL "" FORCE)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL" CACHE STRING "" FORCE)
|
||||
|
||||
add_subdirectory("${PROJECT_SOURCE_DIR}/engines/godot/vendor/godot-cpp"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/godot-cpp" EXCLUDE_FROM_ALL SYSTEM)
|
||||
# godot-cpp écrirait sinon ses .pdb dans <source>/bin.
|
||||
set_target_properties(godot-cpp PROPERTIES PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/godot-cpp/bin")
|
||||
|
||||
target_sources(sequa_godot_adapter PRIVATE src/godot_host.cpp src/godot_scene.cpp)
|
||||
target_link_libraries(sequa_godot_adapter PRIVATE godot::cpp)
|
||||
# PROVISOIRE : chemins fixés à la construction, le processus de rendu les fournira (ADR 0008).
|
||||
target_compile_definitions(sequa_godot_adapter PRIVATE
|
||||
SEQUA_GODOT_RUNTIME=1
|
||||
SEQUA_GODOT_DEFAULT_LIBRARY="${SEQUA_GODOT_LIBRARY}"
|
||||
SEQUA_GODOT_DEFAULT_PROJECT="${CMAKE_CURRENT_SOURCE_DIR}/project")
|
||||
|
||||
# Démonstration de l'étape 3 : scène SIMULÉE poussée depuis SEQUA vers Godot.
|
||||
add_executable(sequa_godot_demo demo/main.cpp)
|
||||
target_link_libraries(sequa_godot_demo PRIVATE sequa::godot_adapter)
|
||||
sequa_target_defaults(sequa_godot_demo)
|
||||
if(SEQUA_BUILD_TESTS)
|
||||
add_test(NAME sequa_godot_demo COMMAND sequa_godot_demo --frames 300)
|
||||
set_tests_properties(sequa_godot_demo PROPERTIES LABELS "gpu;godot" TIMEOUT 120)
|
||||
endif()
|
||||
else()
|
||||
target_compile_definitions(sequa_godot_adapter PRIVATE SEQUA_GODOT_RUNTIME=0)
|
||||
endif()
|
||||
|
||||
if(SEQUA_BUILD_TESTS)
|
||||
add_executable(sequa_godot_adapter_tests tests/test_godot_adapter.cpp)
|
||||
add_executable(sequa_godot_adapter_tests tests/test_godot_adapter.cpp tests/test_conversions.cpp)
|
||||
target_link_libraries(sequa_godot_adapter_tests PRIVATE sequa::godot_adapter doctest::doctest)
|
||||
sequa_target_defaults(sequa_godot_adapter_tests)
|
||||
doctest_discover_tests(sequa_godot_adapter_tests)
|
||||
|
||||
@@ -11,7 +11,36 @@ Implémentation de `sequa::three_d::I3DEngineAdapter` (voir [3d/](../../../3d))
|
||||
|
||||
## État
|
||||
|
||||
**PROVISOIRE** : l'adaptateur déclare ses capacités (couleur, profondeur, normales, vecteurs de mouvement, à vérifier sur la 4.7.2), mais `start()` renvoie `not_implemented` : l'hébergement par LibGodot et la construction de Godot en bibliothèque restent à écrire.
|
||||
**PROVISOIRE.** Avec `SEQUA_BUILD_GODOT_RUNTIME=ON` (preset `windows-msvc-prototypes`, Windows seulement), `start()` charge la DLL de Godot construite par `scripts/build-godot.ps1`, démarre le projet `project/` et `apply()` crée les objets sous le nœud `SequaScene` ; `render_frame()` fait avancer Godot d'une image. Aucun tampon n'est encore lu (couleur affichée seulement) ; lumière et environnement sont fixes dans `project/main.tscn` en attendant l'éclairage du contrat. Sans l'option, `start()` renvoie `not_implemented` et l'adaptateur se construit sans Godot.
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---|---|
|
||||
| `src/godot_host.*` | LibGodot : chargement de la DLL, instance, godot-cpp en extension hôte, boucle |
|
||||
| `src/godot_scene.*` | Nœuds Godot qui reflètent la scène SEQUA (deltas déjà validés par `SceneState`) |
|
||||
| `include/sequa/godot/conversions.hpp` | Conversions pures (rotation, couleur sRGB, formes), testées sans moteur |
|
||||
|
||||
### Décisions de l'étape 3 (2026-11-07)
|
||||
|
||||
- **Frontière binaire reportée** (question ouverte de l'ADR 0008) : l'adaptateur reste une bibliothèque liée statiquement au programme qui l'utilise. Le passage en bibliothèque chargée, avec point d'entrée C versionné, se fera avec le processus de rendu (étape 4), en même temps que le choix du transport Core ↔ rendu.
|
||||
- **API de godot-cpp extraite du fork** : `scripts/build-godot.ps1 -Target editor` construit l'éditeur du fork et écrit `build/godot/extension_api.json`, que CMake passe à godot-cpp (`GODOTCPP_CUSTOM_API_FILE`) ; l'interface GDExtension vient alors aussi du fork (`core/extension`). À défaut, la liste officielle 4.7 de godot-cpp est utilisée, identique tant que le fork ne touche pas à l'API. À relancer après chaque modification du fork qui change l'API.
|
||||
- **Même bibliothèque d'exécution MSVC que SEQUA** (`/MD`, `/MDd`) : godot-cpp choisirait sinon `/MT` pour tout le projet.
|
||||
|
||||
### Démonstration (scène SIMULÉE)
|
||||
|
||||
`demo/main.cpp` pousse une petite scène (sol, boîtes, sphère, capsule, caméra) par deltas SEQUA 3D et fait tourner une boîte à chaque image ; la scène est écrite en dur, elle viendra de la simulation du Core à l'étape 4.
|
||||
|
||||
```powershell
|
||||
.\scripts\build-godot.ps1 # Godot en bibliothèque (une fois)
|
||||
.\scripts\build-godot.ps1 -Target editor # API du fork pour godot-cpp (une fois, ~10 min)
|
||||
.\scripts\build.ps1 -Preset windows-msvc-prototypes -BuildDir build\windows-msvc-prototypes
|
||||
.\build\windows-msvc-prototypes\engines\godot\adapter\sequa_godot_demo.exe [--frames N]
|
||||
```
|
||||
|
||||
Le test `sequa_godot_demo` (étiquettes `gpu;godot`) la lance pour 300 images. Résultat du 2026-11-07 (RTX 4090 Laptop) : `render_frame()` médiane 5,5 ms, p95 6,1 ms, synchronisation verticale comprise ; arrêt propre.
|
||||
|
||||
### Constat : ordre de déchargement de Godot
|
||||
|
||||
godot-cpp garde des tables globales (`ClassDB::instance_binding_callbacks`…) qui libèrent leur mémoire par l'allocateur de Godot à la fin du processus. Décharger la DLL de Godot dans `stop()` fait planter la sortie (`0xC0000005`), et la laisser à Windows aussi. `src/godot_host.cpp` détruit l'instance dans `stop()` mais ne décharge la DLL qu'à la toute fin, depuis un objet global construit avant ceux de godot-cpp (`#pragma init_seg(lib)` sous MSVC, `init_priority` ailleurs), donc détruit après eux.
|
||||
|
||||
### À prévoir
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
// Démonstration de l'étape 3 : une scène décrite par SEQUA (deltas SEQUA 3D) s'affiche
|
||||
// dans Godot hébergé par l'adaptateur. SIMULÉ : la scène et son animation sont écrites
|
||||
// ici en dur ; à l'étape 4 elles viendront de la simulation du Core.
|
||||
|
||||
#include "sequa/godot/godot_adapter.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
using namespace sequa;
|
||||
using three_d::Color;
|
||||
using three_d::CreateObject;
|
||||
using three_d::Primitive;
|
||||
using three_d::Quat;
|
||||
using three_d::SceneDelta;
|
||||
using three_d::Transform;
|
||||
using three_d::Vec3;
|
||||
using three_d::Visual;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr three_d::ObjectId spinning_box = 2;
|
||||
|
||||
CreateObject object(three_d::ObjectId id, Primitive primitive, Vec3 position, Vec3 size, Color color)
|
||||
{
|
||||
return CreateObject{id, Transform{position, Quat{}, Vec3{1, 1, 1}}, Visual{primitive, size, color, 0}};
|
||||
}
|
||||
|
||||
SceneDelta initial_scene()
|
||||
{
|
||||
SceneDelta delta{1, {}};
|
||||
delta.commands.emplace_back(object(1, Primitive::plane, {0, 0, 0}, {20, 1, 20}, {0.25f, 0.35f, 0.2f, 1}));
|
||||
delta.commands.emplace_back(object(spinning_box, Primitive::box, {0, 0.75f, 0}, {1, 1, 1}, {0.8f, 0.3f, 0.1f, 1}));
|
||||
delta.commands.emplace_back(object(3, Primitive::sphere, {-2, 0.6f, 0.5f}, {1.2f, 1.2f, 1.2f}, {0.1f, 0.3f, 0.8f, 1}));
|
||||
delta.commands.emplace_back(object(4, Primitive::capsule, {2, 1, 0.5f}, {0.8f, 2, 0.8f}, {0.7f, 0.7f, 0.2f, 1}));
|
||||
delta.commands.emplace_back(object(5, Primitive::box, {0, 0.5f, -3}, {4, 1, 0.5f}, {0.5f, 0.5f, 0.5f, 0.6f}));
|
||||
|
||||
// Caméra à (0, 2.5, 6), inclinée de 20° vers le bas (rotation autour de X).
|
||||
const float half = -20.0f * 3.14159265f / 360.0f;
|
||||
three_d::Camera camera;
|
||||
camera.transform = Transform{{0, 2.5f, 6}, Quat{std::sin(half), 0, 0, std::cos(half)}, {1, 1, 1}};
|
||||
delta.commands.emplace_back(three_d::SetCamera{camera});
|
||||
return delta;
|
||||
}
|
||||
|
||||
// Rotation de la boîte autour de Y à l'image donnée (un tour toutes les 4 s à 60 images/s).
|
||||
SceneDelta spin(std::uint64_t version, std::uint64_t frame)
|
||||
{
|
||||
const float half = static_cast<float>(frame % 240) * (3.14159265f / 240.0f);
|
||||
const Transform transform{{0, 0.75f, 0}, Quat{0, std::sin(half), 0, std::cos(half)}, {1, 1, 1}};
|
||||
return SceneDelta{version, {three_d::SetTransform{spinning_box, transform}}};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
long frames = 0; // 0 : jusqu'à la fermeture de la fenêtre
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::string arg = argv[i];
|
||||
if (arg == "--frames" && i + 1 < argc) {
|
||||
frames = std::strtol(argv[++i], nullptr, 10);
|
||||
} else {
|
||||
std::printf("usage : sequa_godot_demo [--frames N]\n");
|
||||
return arg == "--help" ? 0 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
godot::GodotEngineAdapter adapter;
|
||||
three_d::EngineConfig config;
|
||||
config.window_title = "SEQUA - démonstration adaptateur Godot (scène SIMULÉE)";
|
||||
if (auto error = adapter.start(config)) {
|
||||
std::fprintf(stderr, "start : %s\n", error->message.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (auto error = adapter.apply(initial_scene())) {
|
||||
std::fprintf(stderr, "scène initiale : %s\n", error->message.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<double> cpu_ms;
|
||||
std::uint64_t version = 1;
|
||||
for (std::uint64_t frame = 0; frames == 0 || frame < static_cast<std::uint64_t>(frames); ++frame) {
|
||||
if (auto error = adapter.apply(spin(++version, frame))) {
|
||||
std::fprintf(stderr, "delta %llu : %s\n", static_cast<unsigned long long>(version), error->message.c_str());
|
||||
return 1;
|
||||
}
|
||||
three_d::FrameRequest request;
|
||||
request.frame_index = frame;
|
||||
request.world_version = version;
|
||||
request.width = config.width;
|
||||
request.height = config.height;
|
||||
const three_d::FrameOutcome outcome = adapter.render_frame(request);
|
||||
if (const auto* error = std::get_if<three_d::Error>(&outcome)) {
|
||||
std::printf("arrêt après %llu images : %s\n", static_cast<unsigned long long>(frame),
|
||||
error->message.c_str());
|
||||
break;
|
||||
}
|
||||
const auto& result = std::get<three_d::FrameResult>(outcome);
|
||||
if (result.world_version != version) {
|
||||
std::fprintf(stderr, "version de scène %llu au lieu de %llu\n",
|
||||
static_cast<unsigned long long>(result.world_version),
|
||||
static_cast<unsigned long long>(version));
|
||||
return 1;
|
||||
}
|
||||
cpu_ms.push_back(result.measurements.cpu_ms);
|
||||
}
|
||||
|
||||
if (!cpu_ms.empty()) {
|
||||
std::sort(cpu_ms.begin(), cpu_ms.end());
|
||||
std::printf("%zu images, render_frame() médiane %.2f ms, p95 %.2f ms (attente de la synchro verticale incluse)\n",
|
||||
cpu_ms.size(), cpu_ms[cpu_ms.size() / 2], cpu_ms[cpu_ms.size() * 95 / 100]);
|
||||
}
|
||||
adapter.stop();
|
||||
std::printf("arrêt propre\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#pragma once
|
||||
|
||||
#include "sequa/3d/scene.hpp"
|
||||
#include "sequa/3d/types.hpp"
|
||||
|
||||
namespace sequa::godot::convert {
|
||||
|
||||
// Conversions pures SEQUA 3D -> réglages Godot. Aucune dépendance à godot-cpp :
|
||||
// testables sans le moteur. Les entrées peuvent être corrompues (NaN, inf, tailles
|
||||
// nulles ou négatives) : les fonctions les rendent toujours sûres.
|
||||
|
||||
// Colonnes de la matrice 3x3 : axes locaux X, Y, Z exprimés dans le repère parent.
|
||||
struct Basis {
|
||||
three_d::Vec3 x;
|
||||
three_d::Vec3 y;
|
||||
three_d::Vec3 z;
|
||||
};
|
||||
|
||||
// Normalise q. Si une composante n'est pas finie, ou si la norme au carré est quasi
|
||||
// nulle, renvoie l'identité (Quat{} : 0, 0, 0, 1).
|
||||
[[nodiscard]] three_d::Quat normalized_rotation(const three_d::Quat& q) noexcept;
|
||||
|
||||
// Équivalent de Godot Basis(Quaternion(q)).scaled_local(scale) : matrice de rotation du
|
||||
// quaternion (convention main droite) avec chaque colonne multipliée par l'échelle
|
||||
// correspondante.
|
||||
[[nodiscard]] Basis basis_from(const three_d::Quat& rotation, const three_d::Vec3& scale) noexcept;
|
||||
|
||||
// StandardMaterial3D interprète la couleur albedo en sRGB alors que SEQUA la transmet en
|
||||
// linéaire.
|
||||
[[nodiscard]] float linear_to_srgb(float c) noexcept;
|
||||
[[nodiscard]] three_d::Color to_srgb(const three_d::Color& linear) noexcept;
|
||||
|
||||
// Plancher imposé à chaque dimension pour éviter les volumes nuls côté Godot.
|
||||
inline constexpr float min_dimension = 0.001f;
|
||||
|
||||
struct MeshParams {
|
||||
three_d::Primitive primitive = three_d::Primitive::box;
|
||||
three_d::Vec3 box_size{1, 1, 1}; // box
|
||||
float radius = 0.5f; // sphere, capsule
|
||||
float height = 1.0f; // sphere, capsule
|
||||
float plane_width = 1.0f; // plane (axe X)
|
||||
float plane_depth = 1.0f; // plane (axe Z)
|
||||
};
|
||||
|
||||
// Réglages des formes Godot (BoxMesh, SphereMesh, CapsuleMesh, PlaneMesh) : seuls les
|
||||
// champs utiles à la primitive sont remplis, les autres gardent leur valeur par défaut.
|
||||
[[nodiscard]] MeshParams mesh_params(const three_d::Visual& visual) noexcept;
|
||||
|
||||
} // namespace sequa::godot::convert
|
||||
@@ -3,26 +3,34 @@
|
||||
|
||||
#include "sequa/3d/adapter.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace sequa::godot {
|
||||
|
||||
// Version de Godot visée (sous-module engines/godot/vendor/godot-src).
|
||||
inline constexpr const char* engine_version = "4.7.2-stable";
|
||||
|
||||
// Capacités déclarées pour Godot en rendu Forward+ (docs §7.2). À VÉRIFIER sur la 4.7 :
|
||||
// Capacités déclarées pour Godot en rendu Forward+ (docs §7.2, constats de l'ADR 0007) :
|
||||
// couleur, profondeur, normales et vecteurs de mouvement existent dans Forward+ ; albedo,
|
||||
// identifiants d'objets et masque sémantique demandent des passes supplémentaires et ne
|
||||
// sont pas déclarés tant qu'elles n'existent pas.
|
||||
[[nodiscard]] three_d::EngineCapabilities godot_capabilities();
|
||||
|
||||
// Adaptateur SEQUA 3D vers Godot. Seul ce fichier et l'agent côté moteur connaissent
|
||||
// Godot ; le Core et Studio ne voient que three_d::I3DEngineAdapter.
|
||||
// Vrai si l'adaptateur a été construit avec l'hébergement de Godot (SEQUA_BUILD_GODOT_RUNTIME).
|
||||
[[nodiscard]] bool runtime_available() noexcept;
|
||||
|
||||
// Adaptateur SEQUA 3D vers Godot. Seul ce module connaît Godot ; le Core et Studio ne
|
||||
// voient que three_d::I3DEngineAdapter.
|
||||
//
|
||||
// Godot est hébergé par LibGodot (ADR 0007) : l'adaptateur appellera
|
||||
// GodotInstance::iteration() dans render_frame(). PROVISOIRE : tant que l'hébergement
|
||||
// n'est pas écrit, start() renvoie not_implemented et l'adaptateur ne fait que déclarer
|
||||
// ses capacités.
|
||||
// Godot est hébergé par LibGodot (ADR 0007) : start() charge et démarre le moteur,
|
||||
// render_frame() appelle GodotInstance::iteration(). Construit sans
|
||||
// SEQUA_BUILD_GODOT_RUNTIME, start() renvoie not_implemented.
|
||||
// PROVISOIRE : render_frame() ne lit encore aucun tampon (étape 5).
|
||||
class GodotEngineAdapter final : public three_d::I3DEngineAdapter {
|
||||
public:
|
||||
GodotEngineAdapter();
|
||||
~GodotEngineAdapter() override;
|
||||
|
||||
[[nodiscard]] three_d::EngineCapabilities capabilities() const override { return godot_capabilities(); }
|
||||
|
||||
[[nodiscard]] std::optional<three_d::Error> start(const three_d::EngineConfig& config) override;
|
||||
@@ -33,7 +41,10 @@ public:
|
||||
[[nodiscard]] three_d::FrameOutcome render_frame(const three_d::FrameRequest& request) override;
|
||||
|
||||
private:
|
||||
struct Runtime; // moteur hébergé et nœuds de la scène (vide sans SEQUA_BUILD_GODOT_RUNTIME)
|
||||
|
||||
three_d::SceneState scene_;
|
||||
std::unique_ptr<Runtime> runtime_;
|
||||
bool running_ = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Cache créé par Godot à l'exécution
|
||||
.godot/
|
||||
@@ -0,0 +1,22 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[sub_resource type="ProceduralSkyMaterial" id="Sky_1"]
|
||||
|
||||
[sub_resource type="Sky" id="Sky_2"]
|
||||
sky_material = SubResource("Sky_1")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_1"]
|
||||
background_mode = 2
|
||||
sky = SubResource("Sky_2")
|
||||
ambient_light_source = 3
|
||||
|
||||
[node name="Main" type="Node3D"]
|
||||
|
||||
[node name="Environment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_1")
|
||||
|
||||
[node name="Sun" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 0)
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="SequaScene" type="Node3D" parent="."]
|
||||
@@ -0,0 +1,14 @@
|
||||
; Projet Godot chargé par l'adaptateur SEQUA (src/godot_host.cpp). Godot n'y sert que de
|
||||
; moteur d'exécution : la scène vient de SEQUA, sous le nœud SequaScene de main.tscn.
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="SEQUA"
|
||||
run/main_scene="res://main.tscn"
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="forward_plus"
|
||||
rendering_device/driver.windows="vulkan"
|
||||
@@ -0,0 +1,114 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#include "sequa/godot/conversions.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace sequa::godot::convert {
|
||||
namespace {
|
||||
|
||||
// Valeur absolue, non finie -> min_dimension, puis plancher à min_dimension.
|
||||
[[nodiscard]] float safe_dimension(float value) noexcept
|
||||
{
|
||||
if (!std::isfinite(value)) {
|
||||
return min_dimension;
|
||||
}
|
||||
return std::max(std::abs(value), min_dimension);
|
||||
}
|
||||
|
||||
[[nodiscard]] three_d::Vec3 safe_size(const three_d::Vec3& size) noexcept
|
||||
{
|
||||
return {safe_dimension(size.x), safe_dimension(size.y), safe_dimension(size.z)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
three_d::Quat normalized_rotation(const three_d::Quat& q) noexcept
|
||||
{
|
||||
if (!std::isfinite(q.x) || !std::isfinite(q.y) || !std::isfinite(q.z) || !std::isfinite(q.w)) {
|
||||
return three_d::Quat{};
|
||||
}
|
||||
const float norm_squared = q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;
|
||||
// Norme carrée infinie : possible pour des composantes finies mais énormes (float).
|
||||
if (norm_squared < 1e-12f || !std::isfinite(norm_squared)) {
|
||||
return three_d::Quat{};
|
||||
}
|
||||
const float inverse_norm = 1.0f / std::sqrt(norm_squared);
|
||||
return {q.x * inverse_norm, q.y * inverse_norm, q.z * inverse_norm, q.w * inverse_norm};
|
||||
}
|
||||
|
||||
Basis basis_from(const three_d::Quat& rotation, const three_d::Vec3& scale) noexcept
|
||||
{
|
||||
const three_d::Quat q = normalized_rotation(rotation);
|
||||
const float xx = q.x * q.x;
|
||||
const float yy = q.y * q.y;
|
||||
const float zz = q.z * q.z;
|
||||
const float xy = q.x * q.y;
|
||||
const float xz = q.x * q.z;
|
||||
const float yz = q.y * q.z;
|
||||
const float wx = q.w * q.x;
|
||||
const float wy = q.w * q.y;
|
||||
const float wz = q.w * q.z;
|
||||
|
||||
Basis basis;
|
||||
basis.x.x = (1.0f - 2.0f * (yy + zz)) * scale.x;
|
||||
basis.x.y = 2.0f * (xy + wz) * scale.x;
|
||||
basis.x.z = 2.0f * (xz - wy) * scale.x;
|
||||
basis.y.x = 2.0f * (xy - wz) * scale.y;
|
||||
basis.y.y = (1.0f - 2.0f * (xx + zz)) * scale.y;
|
||||
basis.y.z = 2.0f * (yz + wx) * scale.y;
|
||||
basis.z.x = 2.0f * (xz + wy) * scale.z;
|
||||
basis.z.y = 2.0f * (yz - wx) * scale.z;
|
||||
basis.z.z = (1.0f - 2.0f * (xx + yy)) * scale.z;
|
||||
return basis;
|
||||
}
|
||||
|
||||
float linear_to_srgb(float c) noexcept
|
||||
{
|
||||
if (!std::isfinite(c)) {
|
||||
return 0.0f;
|
||||
}
|
||||
const float clamped = std::clamp(c, 0.0f, 1.0f);
|
||||
return clamped <= 0.0031308f ? 12.92f * clamped
|
||||
: 1.055f * std::pow(clamped, 1.0f / 2.4f) - 0.055f;
|
||||
}
|
||||
|
||||
three_d::Color to_srgb(const three_d::Color& linear) noexcept
|
||||
{
|
||||
three_d::Color result;
|
||||
result.r = linear_to_srgb(linear.r);
|
||||
result.g = linear_to_srgb(linear.g);
|
||||
result.b = linear_to_srgb(linear.b);
|
||||
// L'alpha ne subit pas le gamma : borné seulement, non fini -> 1 (opaque).
|
||||
result.a = std::isfinite(linear.a) ? std::clamp(linear.a, 0.0f, 1.0f) : 1.0f;
|
||||
return result;
|
||||
}
|
||||
|
||||
MeshParams mesh_params(const three_d::Visual& visual) noexcept
|
||||
{
|
||||
MeshParams params;
|
||||
params.primitive = visual.primitive;
|
||||
const three_d::Vec3 size = safe_size(visual.size);
|
||||
|
||||
switch (visual.primitive) {
|
||||
case three_d::Primitive::box:
|
||||
params.box_size = size;
|
||||
break;
|
||||
case three_d::Primitive::sphere:
|
||||
params.radius = size.x * 0.5f;
|
||||
params.height = size.y;
|
||||
break;
|
||||
case three_d::Primitive::capsule:
|
||||
params.radius = size.x * 0.5f;
|
||||
// Godot impose height >= 2*radius.
|
||||
params.height = std::max(size.y, 2.0f * params.radius);
|
||||
break;
|
||||
case three_d::Primitive::plane:
|
||||
params.plane_width = size.x;
|
||||
params.plane_depth = size.z;
|
||||
break;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
} // namespace sequa::godot::convert
|
||||
@@ -1,14 +1,30 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#include "sequa/godot/godot_adapter.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <utility>
|
||||
|
||||
#if SEQUA_GODOT_RUNTIME
|
||||
#include "godot_host.hpp"
|
||||
#include "godot_scene.hpp"
|
||||
#endif
|
||||
|
||||
namespace sequa::godot {
|
||||
|
||||
using three_d::BufferSet;
|
||||
using three_d::Error;
|
||||
using three_d::FrameBuffer;
|
||||
|
||||
#if SEQUA_GODOT_RUNTIME
|
||||
struct GodotEngineAdapter::Runtime {
|
||||
// Ordre de destruction : les nœuds de la scène avant le moteur.
|
||||
std::unique_ptr<GodotHost> host;
|
||||
std::unique_ptr<GodotScene> scene;
|
||||
};
|
||||
#else
|
||||
struct GodotEngineAdapter::Runtime {};
|
||||
#endif
|
||||
|
||||
three_d::EngineCapabilities godot_capabilities()
|
||||
{
|
||||
three_d::EngineCapabilities caps;
|
||||
@@ -17,22 +33,49 @@ three_d::EngineCapabilities godot_capabilities()
|
||||
caps.buffers = BufferSet{FrameBuffer::color, FrameBuffer::depth, FrameBuffer::normals,
|
||||
FrameBuffer::motion_vectors};
|
||||
caps.windowed = true;
|
||||
caps.headless = false; // --headless de Godot coupe le rendu : hors écran à étudier
|
||||
caps.headless = false; // --headless de Godot coupe le rendu : vrai headless à faire dans le fork (ADR 0007)
|
||||
return caps;
|
||||
}
|
||||
|
||||
std::optional<Error> GodotEngineAdapter::start(const three_d::EngineConfig& /*config*/)
|
||||
bool runtime_available() noexcept
|
||||
{
|
||||
return SEQUA_GODOT_RUNTIME != 0;
|
||||
}
|
||||
|
||||
GodotEngineAdapter::GodotEngineAdapter() = default;
|
||||
|
||||
GodotEngineAdapter::~GodotEngineAdapter()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
std::optional<Error> GodotEngineAdapter::start(const three_d::EngineConfig& config)
|
||||
{
|
||||
if (running_) {
|
||||
return Error{Error::Code::already_started, "adaptateur Godot déjà démarré"};
|
||||
}
|
||||
return Error{Error::Code::not_implemented,
|
||||
"hébergement de Godot par LibGodot pas encore écrit (ADR 0007)"};
|
||||
#if SEQUA_GODOT_RUNTIME
|
||||
GodotHost::StartOutcome outcome = GodotHost::start(config);
|
||||
if (auto* error = std::get_if<Error>(&outcome)) {
|
||||
return std::move(*error);
|
||||
}
|
||||
auto runtime = std::make_unique<Runtime>();
|
||||
runtime->host = std::move(std::get<std::unique_ptr<GodotHost>>(outcome));
|
||||
runtime->scene = std::make_unique<GodotScene>(runtime->host->scene_root());
|
||||
runtime_ = std::move(runtime);
|
||||
scene_.clear();
|
||||
running_ = true;
|
||||
return std::nullopt;
|
||||
#else
|
||||
(void)config;
|
||||
return Error{Error::Code::not_implemented, "adaptateur construit sans SEQUA_BUILD_GODOT_RUNTIME"};
|
||||
#endif
|
||||
}
|
||||
|
||||
void GodotEngineAdapter::stop() noexcept
|
||||
{
|
||||
running_ = false;
|
||||
runtime_.reset();
|
||||
scene_.clear();
|
||||
}
|
||||
|
||||
@@ -41,7 +84,14 @@ std::optional<Error> GodotEngineAdapter::apply(const three_d::SceneDelta& delta)
|
||||
if (!running_) {
|
||||
return Error{Error::Code::not_started, "adaptateur Godot arrêté"};
|
||||
}
|
||||
return scene_.apply(delta);
|
||||
// Validation par le miroir d'abord (tout ou rien) : Godot ne reçoit que des deltas valides.
|
||||
if (std::optional<Error> error = scene_.apply(delta)) {
|
||||
return error;
|
||||
}
|
||||
#if SEQUA_GODOT_RUNTIME
|
||||
runtime_->scene->apply(delta);
|
||||
#endif
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
three_d::FrameOutcome GodotEngineAdapter::render_frame(const three_d::FrameRequest& request)
|
||||
@@ -52,7 +102,27 @@ three_d::FrameOutcome GodotEngineAdapter::render_frame(const three_d::FrameReque
|
||||
if (std::optional<Error> error = three_d::validate_request(request, capabilities())) {
|
||||
return *std::move(error);
|
||||
}
|
||||
return Error{Error::Code::not_implemented, "rendu Godot pas encore écrit"};
|
||||
#if SEQUA_GODOT_RUNTIME
|
||||
const auto begin = std::chrono::steady_clock::now();
|
||||
const bool keep_running = runtime_->host->iterate();
|
||||
const double cpu_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - begin).count();
|
||||
if (!keep_running) {
|
||||
stop();
|
||||
return Error{Error::Code::engine_failure, "Godot s'est arrêté (fenêtre fermée)"};
|
||||
}
|
||||
// PROVISOIRE : l'image n'est qu'affichée dans la fenêtre ; aucun tampon n'est encore lu
|
||||
// (étape 5). cpu_ms inclut l'attente de la synchronisation verticale.
|
||||
three_d::FrameResult result;
|
||||
result.frame_index = request.frame_index;
|
||||
result.world_version = scene_.world_version();
|
||||
if (request.buffers.contains(FrameBuffer::color)) {
|
||||
result.produced = BufferSet{FrameBuffer::color};
|
||||
}
|
||||
result.measurements.cpu_ms = cpu_ms;
|
||||
return result;
|
||||
#else
|
||||
return Error{Error::Code::not_implemented, "adaptateur construit sans SEQUA_BUILD_GODOT_RUNTIME"};
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace sequa::godot
|
||||
@@ -0,0 +1,195 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#include "godot_host.hpp"
|
||||
|
||||
#include <godot_cpp/classes/engine.hpp>
|
||||
#include <godot_cpp/classes/godot_instance.hpp>
|
||||
#include <godot_cpp/classes/main_loop.hpp>
|
||||
#include <godot_cpp/classes/node3d.hpp>
|
||||
#include <godot_cpp/classes/scene_tree.hpp>
|
||||
#include <godot_cpp/classes/window.hpp>
|
||||
#include <godot_cpp/core/object.hpp>
|
||||
#include <godot_cpp/godot.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX // déjà défini par godot-cpp
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
namespace sequa::godot {
|
||||
namespace {
|
||||
|
||||
using three_d::Error;
|
||||
|
||||
using CreateInstanceFn = GDExtensionObjectPtr (*)(int, char*[], GDExtensionInitializationFunction);
|
||||
using DestroyInstanceFn = void (*)(GDExtensionObjectPtr);
|
||||
|
||||
// LibGodot : une instance par processus, jamais recréée.
|
||||
bool g_started_once = false;
|
||||
|
||||
void no_op_level(::godot::ModuleInitializationLevel /*level*/) {}
|
||||
|
||||
// Fonction d'initialisation GDExtension passée à LibGodot : SEQUA est l'extension hôte, et
|
||||
// godot-cpp reçoit par elle l'accès à l'API du moteur.
|
||||
GDExtensionBool host_init(GDExtensionInterfaceGetProcAddress get_proc_address, GDExtensionClassLibraryPtr library,
|
||||
GDExtensionInitialization* initialization)
|
||||
{
|
||||
const ::godot::GDExtensionBinding::InitObject init(get_proc_address, library, initialization);
|
||||
init.register_initializer(&no_op_level);
|
||||
init.register_terminator(&no_op_level);
|
||||
init.set_minimum_library_initialization_level(::godot::MODULE_INITIALIZATION_LEVEL_SCENE);
|
||||
return init.init();
|
||||
}
|
||||
|
||||
void* load_library(const char* path)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return LoadLibraryA(path);
|
||||
#else
|
||||
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||||
#endif
|
||||
}
|
||||
|
||||
void* load_symbol(void* library, const char* name)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(library), name));
|
||||
#else
|
||||
return dlsym(library, name);
|
||||
#endif
|
||||
}
|
||||
|
||||
void unload_library(void* library)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
FreeLibrary(static_cast<HMODULE>(library));
|
||||
#else
|
||||
dlclose(library);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Décharge Godot après la destruction des objets globaux de godot-cpp, qui libèrent leur
|
||||
// mémoire par l'allocateur de Godot : décharger avant fait planter la sortie du processus,
|
||||
// et laisser Windows le faire aussi (constat de prototypes/libgodot_host).
|
||||
struct LibraryKeeper {
|
||||
void* library = nullptr;
|
||||
LibraryKeeper() = default;
|
||||
LibraryKeeper(const LibraryKeeper&) = delete;
|
||||
LibraryKeeper& operator=(const LibraryKeeper&) = delete;
|
||||
~LibraryKeeper()
|
||||
{
|
||||
if (library != nullptr) {
|
||||
unload_library(library);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef _MSC_VER
|
||||
// Construit avant les objets globaux de godot-cpp (segment « user »), donc détruit après eux.
|
||||
#pragma warning(disable : 4073)
|
||||
#pragma init_seg(lib)
|
||||
LibraryKeeper g_library_keeper;
|
||||
#else
|
||||
LibraryKeeper g_library_keeper __attribute__((init_priority(101)));
|
||||
#endif
|
||||
|
||||
::godot::Node3D* find_scene_root()
|
||||
{
|
||||
auto* tree = ::godot::Object::cast_to<::godot::SceneTree>(::godot::Engine::get_singleton()->get_main_loop());
|
||||
if (tree == nullptr || tree->get_current_scene() == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return ::godot::Object::cast_to<::godot::Node3D>(
|
||||
tree->get_current_scene()->get_node_or_null(::godot::NodePath("SequaScene")));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GodotHost::StartOutcome GodotHost::start(const three_d::EngineConfig& config)
|
||||
{
|
||||
if (config.headless) {
|
||||
// --headless de Godot coupe le rendu ; le vrai headless viendra du fork (ADR 0007).
|
||||
return Error{Error::Code::unsupported, "Godot : pas de rendu sans fenêtre pour l'instant"};
|
||||
}
|
||||
if (config.width == 0 || config.height == 0) {
|
||||
return Error{Error::Code::invalid_argument, "Godot : taille de fenêtre nulle"};
|
||||
}
|
||||
if (g_started_once) {
|
||||
return Error{Error::Code::engine_failure,
|
||||
"Godot ne redémarre pas dans le même processus (LibGodot) : relancer le processus de rendu"};
|
||||
}
|
||||
|
||||
// PROVISOIRE : chemins fixés à la construction ; le processus de rendu les fournira (ADR 0008).
|
||||
void* library = load_library(SEQUA_GODOT_DEFAULT_LIBRARY);
|
||||
if (library == nullptr) {
|
||||
return Error{Error::Code::engine_failure,
|
||||
std::string("Godot : impossible de charger ") + SEQUA_GODOT_DEFAULT_LIBRARY};
|
||||
}
|
||||
auto* create = reinterpret_cast<CreateInstanceFn>(load_symbol(library, "libgodot_create_godot_instance"));
|
||||
if (create == nullptr || load_symbol(library, "libgodot_destroy_godot_instance") == nullptr) {
|
||||
unload_library(library);
|
||||
return Error{Error::Code::engine_failure, "Godot : bibliothèque construite sans library_type=shared_library"};
|
||||
}
|
||||
|
||||
std::vector<std::string> args = {"sequa",
|
||||
"--path",
|
||||
SEQUA_GODOT_DEFAULT_PROJECT,
|
||||
"--rendering-driver",
|
||||
"vulkan",
|
||||
"--resolution",
|
||||
std::to_string(config.width) + "x" + std::to_string(config.height)};
|
||||
std::vector<char*> argv;
|
||||
for (std::string& arg : args) {
|
||||
argv.push_back(arg.data());
|
||||
}
|
||||
argv.push_back(nullptr);
|
||||
|
||||
g_started_once = true;
|
||||
GDExtensionObjectPtr instance = create(static_cast<int>(args.size()), argv.data(), &host_init);
|
||||
if (instance == nullptr) {
|
||||
unload_library(library);
|
||||
return Error{Error::Code::engine_failure, "Godot : échec de libgodot_create_godot_instance"};
|
||||
}
|
||||
|
||||
std::unique_ptr<GodotHost> host(new GodotHost());
|
||||
host->library_ = library;
|
||||
host->instance_ = instance;
|
||||
host->godot_ = ::godot::Object::cast_to<::godot::GodotInstance>(
|
||||
::godot::internal::get_object_instance_binding(instance));
|
||||
if (host->godot_ == nullptr || !host->godot_->start()) {
|
||||
return Error{Error::Code::engine_failure, "Godot : GodotInstance::start() a échoué"};
|
||||
}
|
||||
|
||||
host->scene_root_ = find_scene_root();
|
||||
if (host->scene_root_ == nullptr) {
|
||||
return Error{Error::Code::engine_failure, "Godot : nœud SequaScene introuvable dans project/main.tscn"};
|
||||
}
|
||||
host->scene_root_->get_tree()->get_root()->set_title(::godot::String::utf8(config.window_title.c_str()));
|
||||
return host;
|
||||
}
|
||||
|
||||
GodotHost::~GodotHost()
|
||||
{
|
||||
if (instance_ != nullptr) {
|
||||
auto* destroy = reinterpret_cast<DestroyInstanceFn>(load_symbol(library_, "libgodot_destroy_godot_instance"));
|
||||
destroy(instance_);
|
||||
}
|
||||
g_library_keeper.library = library_;
|
||||
}
|
||||
|
||||
bool GodotHost::iterate()
|
||||
{
|
||||
// iteration() renvoie true quand Godot veut s'arrêter.
|
||||
return !godot_->iteration();
|
||||
}
|
||||
|
||||
} // namespace sequa::godot
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#pragma once
|
||||
|
||||
#include "sequa/3d/adapter.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
|
||||
namespace godot {
|
||||
class GodotInstance;
|
||||
class Node3D;
|
||||
} // namespace godot
|
||||
|
||||
namespace sequa::godot {
|
||||
|
||||
// Godot hébergé par LibGodot (ADR 0007) : bibliothèque chargée à l'exécution, instance
|
||||
// créée avec godot-cpp comme extension hôte, boucle pilotée image par image.
|
||||
// Une seule instance par processus et pas de recréation après destruction (limite de
|
||||
// LibGodot) : un second start() dans le même processus échoue.
|
||||
class GodotHost {
|
||||
public:
|
||||
using StartOutcome = std::variant<std::unique_ptr<GodotHost>, three_d::Error>;
|
||||
[[nodiscard]] static StartOutcome start(const three_d::EngineConfig& config);
|
||||
|
||||
GodotHost(const GodotHost&) = delete;
|
||||
GodotHost& operator=(const GodotHost&) = delete;
|
||||
// Détruit l'instance puis décharge aussitôt la bibliothèque (voir prototypes/libgodot_host).
|
||||
~GodotHost();
|
||||
|
||||
// Une image de Godot. Renvoie false quand Godot demande l'arrêt (fenêtre fermée).
|
||||
[[nodiscard]] bool iterate();
|
||||
|
||||
// Nœud sous lequel SEQUA crée sa scène (SequaScene dans project/main.tscn).
|
||||
[[nodiscard]] ::godot::Node3D* scene_root() const noexcept { return scene_root_; }
|
||||
|
||||
private:
|
||||
GodotHost() = default;
|
||||
|
||||
void* library_ = nullptr;
|
||||
void* instance_ = nullptr; // GDExtensionObjectPtr
|
||||
::godot::GodotInstance* godot_ = nullptr;
|
||||
::godot::Node3D* scene_root_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace sequa::godot
|
||||
@@ -0,0 +1,229 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
// GodotScene : crée et met à jour les nœuds Godot (godot-cpp) d'après les commandes
|
||||
// de scène SEQUA. Le delta passe déjà par three_d::SceneState, qui garantit des ids
|
||||
// valides et une application tout ou rien ; ici, un id inconnu est simplement ignoré.
|
||||
|
||||
#include "godot_scene.hpp"
|
||||
|
||||
#include "sequa/godot/conversions.hpp"
|
||||
|
||||
#include <godot_cpp/classes/box_mesh.hpp>
|
||||
#include <godot_cpp/classes/camera3d.hpp>
|
||||
#include <godot_cpp/classes/capsule_mesh.hpp>
|
||||
#include <godot_cpp/classes/mesh_instance3d.hpp>
|
||||
#include <godot_cpp/classes/node3d.hpp>
|
||||
#include <godot_cpp/classes/plane_mesh.hpp>
|
||||
#include <godot_cpp/classes/ref.hpp>
|
||||
#include <godot_cpp/classes/sphere_mesh.hpp>
|
||||
#include <godot_cpp/classes/standard_material3d.hpp>
|
||||
#include <godot_cpp/core/memory.hpp>
|
||||
#include <godot_cpp/variant/basis.hpp>
|
||||
#include <godot_cpp/variant/color.hpp>
|
||||
#include <godot_cpp/variant/string_name.hpp>
|
||||
#include <godot_cpp/variant/transform3d.hpp>
|
||||
#include <godot_cpp/variant/vector2.hpp>
|
||||
#include <godot_cpp/variant/vector3.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace sequa::godot {
|
||||
namespace {
|
||||
|
||||
// Repères identiques (mètres, main droite, Y vers le haut) : aucune conversion d'axes.
|
||||
|
||||
[[nodiscard]] ::godot::Vector3 godot_vector3(const three_d::Vec3& v) noexcept
|
||||
{
|
||||
return ::godot::Vector3(v.x, v.y, v.z);
|
||||
}
|
||||
|
||||
// Transform SEQUA -> Transform3D Godot. Le constructeur Basis(Vector3, Vector3, Vector3)
|
||||
// prend les COLONNES (axes locaux exprimés dans le repère parent), ce que remplissent
|
||||
// convert::Basis::x/y/z avec la rotation et l'échelle de la commande.
|
||||
[[nodiscard]] ::godot::Transform3D godot_transform(const three_d::Transform& t) noexcept
|
||||
{
|
||||
const convert::Basis axes = convert::basis_from(t.rotation, t.scale);
|
||||
const ::godot::Basis basis(godot_vector3(axes.x), godot_vector3(axes.y), godot_vector3(axes.z));
|
||||
return ::godot::Transform3D(basis, godot_vector3(t.position));
|
||||
}
|
||||
|
||||
// Maillage de la primitive, étalonné par convert::mesh_params : seuls les champs utiles
|
||||
// à la primitive sont lus, les autres gardent le défaut des classes Godot.
|
||||
[[nodiscard]] ::godot::Ref<::godot::Mesh> godot_mesh(const convert::MeshParams& params)
|
||||
{
|
||||
switch (params.primitive) {
|
||||
case three_d::Primitive::box: {
|
||||
::godot::Ref<::godot::BoxMesh> mesh;
|
||||
mesh.instantiate();
|
||||
mesh->set_size(godot_vector3(params.box_size));
|
||||
return mesh;
|
||||
}
|
||||
case three_d::Primitive::sphere: {
|
||||
::godot::Ref<::godot::SphereMesh> mesh;
|
||||
mesh.instantiate();
|
||||
mesh->set_radius(params.radius);
|
||||
mesh->set_height(params.height);
|
||||
return mesh;
|
||||
}
|
||||
case three_d::Primitive::capsule: {
|
||||
::godot::Ref<::godot::CapsuleMesh> mesh;
|
||||
mesh.instantiate();
|
||||
mesh->set_radius(params.radius);
|
||||
mesh->set_height(params.height);
|
||||
return mesh;
|
||||
}
|
||||
case three_d::Primitive::plane:
|
||||
break; // plan : grille horizontale (X largeur, Z profondeur), traitée ci-dessous
|
||||
}
|
||||
|
||||
::godot::Ref<::godot::PlaneMesh> mesh;
|
||||
mesh.instantiate();
|
||||
mesh->set_size(::godot::Vector2(params.plane_width, params.plane_depth));
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// Matériau standard pour une couleur SEQUA : albedo converti en sRGB (StandardMaterial3D
|
||||
// le lit en sRGB), transparence activée dès que l'alpha passe sous 1.
|
||||
// semantic_class n'est pas traité ici, réservé au masque sémantique (§5.6).
|
||||
[[nodiscard]] ::godot::Ref<::godot::StandardMaterial3D> godot_material(
|
||||
const three_d::Color& color)
|
||||
{
|
||||
::godot::Ref<::godot::StandardMaterial3D> material;
|
||||
material.instantiate();
|
||||
const three_d::Color srgb = convert::to_srgb(color);
|
||||
material->set_albedo(::godot::Color(srgb.r, srgb.g, srgb.b, srgb.a));
|
||||
if (srgb.a < 1.0f) {
|
||||
material->set_transparency(::godot::BaseMaterial3D::TRANSPARENCY_ALPHA);
|
||||
}
|
||||
return material;
|
||||
}
|
||||
|
||||
void create_object(const three_d::CreateObject& command, ::godot::Node3D* root,
|
||||
std::map<three_d::ObjectId, ::godot::MeshInstance3D*>& objects)
|
||||
{
|
||||
// Racine absente ou id déjà créé : delta non conforme, on ignore plutôt que de fuiter.
|
||||
if (root == nullptr || objects.find(command.id) != objects.end()) {
|
||||
return;
|
||||
}
|
||||
::godot::MeshInstance3D* node = memnew(::godot::MeshInstance3D);
|
||||
node->set_name(("sequa_" + std::to_string(command.id)).c_str());
|
||||
node->set_mesh(godot_mesh(convert::mesh_params(command.visual)));
|
||||
node->set_material_override(godot_material(command.visual.color));
|
||||
node->set_transform(godot_transform(command.transform));
|
||||
root->add_child(node);
|
||||
objects[command.id] = node;
|
||||
}
|
||||
|
||||
void destroy_object(const three_d::DestroyObject& command, ::godot::Node3D* root,
|
||||
std::map<three_d::ObjectId, ::godot::MeshInstance3D*>& objects)
|
||||
{
|
||||
const auto found = objects.find(command.id);
|
||||
if (root == nullptr || found == objects.end()) {
|
||||
return; // id inconnu : ignoré (delta déjà validé, ne devrait jamais arriver)
|
||||
}
|
||||
::godot::MeshInstance3D* node = found->second;
|
||||
objects.erase(found);
|
||||
root->remove_child(node);
|
||||
::godot::memdelete(node);
|
||||
}
|
||||
|
||||
void change_transform(const three_d::SetTransform& command,
|
||||
const std::map<three_d::ObjectId, ::godot::MeshInstance3D*>& objects)
|
||||
{
|
||||
const auto found = objects.find(command.id);
|
||||
if (found != objects.end()) {
|
||||
found->second->set_transform(godot_transform(command.transform));
|
||||
}
|
||||
}
|
||||
|
||||
void change_visual(const three_d::SetVisual& command,
|
||||
const std::map<three_d::ObjectId, ::godot::MeshInstance3D*>& objects)
|
||||
{
|
||||
const auto found = objects.find(command.id);
|
||||
if (found != objects.end()) {
|
||||
found->second->set_mesh(godot_mesh(convert::mesh_params(command.visual)));
|
||||
found->second->set_material_override(godot_material(command.visual.color));
|
||||
}
|
||||
}
|
||||
|
||||
// Caméra créée au premier SetCamera, retargetée ensuite ; set_current seulement après
|
||||
// add_child, puisqu'un Node sortant ne peut pas être activé.
|
||||
[[nodiscard]] ::godot::Camera3D* place_camera(const three_d::SetCamera& command,
|
||||
::godot::Node3D* root,
|
||||
::godot::Camera3D* camera)
|
||||
{
|
||||
if (root == nullptr) {
|
||||
return camera; // racine absente : ne change rien plutôt que de perdre la caméra
|
||||
}
|
||||
if (camera == nullptr) {
|
||||
camera = memnew(::godot::Camera3D);
|
||||
camera->set_name("sequa_camera");
|
||||
root->add_child(camera);
|
||||
}
|
||||
const three_d::Camera& settings = command.camera;
|
||||
// Camera3D garde la hauteur par défaut (keep_aspect) : fov = valeur verticale.
|
||||
camera->set_fov(settings.vertical_fov_deg);
|
||||
camera->set_near(settings.near_plane);
|
||||
camera->set_far(settings.far_plane);
|
||||
// La caméra ne se déforme pas : échelle forcée à (1, 1, 1).
|
||||
const three_d::Transform transform{settings.transform.position, settings.transform.rotation,
|
||||
three_d::Vec3{1, 1, 1}};
|
||||
camera->set_transform(godot_transform(transform));
|
||||
camera->set_current(true);
|
||||
return camera;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GodotScene::GodotScene(::godot::Node3D* root) noexcept : root_{root} {}
|
||||
|
||||
GodotScene::~GodotScene()
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
void GodotScene::apply(const three_d::SceneDelta& delta)
|
||||
{
|
||||
for (const three_d::SceneCommand& command : delta.commands) {
|
||||
std::visit(
|
||||
[&](const auto& variant) {
|
||||
using Command = std::decay_t<decltype(variant)>;
|
||||
if constexpr (std::is_same_v<Command, three_d::CreateObject>) {
|
||||
create_object(variant, root_, objects_);
|
||||
}
|
||||
else if constexpr (std::is_same_v<Command, three_d::DestroyObject>) {
|
||||
destroy_object(variant, root_, objects_);
|
||||
}
|
||||
else if constexpr (std::is_same_v<Command, three_d::SetTransform>) {
|
||||
change_transform(variant, objects_);
|
||||
}
|
||||
else if constexpr (std::is_same_v<Command, three_d::SetVisual>) {
|
||||
change_visual(variant, objects_);
|
||||
}
|
||||
else { // SetCamera
|
||||
camera_ = place_camera(variant, root_, camera_);
|
||||
}
|
||||
},
|
||||
command);
|
||||
}
|
||||
}
|
||||
|
||||
void GodotScene::clear() noexcept
|
||||
{
|
||||
for (const auto& object : objects_) {
|
||||
root_->remove_child(object.second);
|
||||
::godot::memdelete(object.second);
|
||||
}
|
||||
objects_.clear();
|
||||
if (camera_ != nullptr) {
|
||||
root_->remove_child(camera_);
|
||||
::godot::memdelete(camera_);
|
||||
camera_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sequa::godot
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#pragma once
|
||||
|
||||
#include "sequa/3d/scene.hpp"
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace godot {
|
||||
class Camera3D;
|
||||
class MeshInstance3D;
|
||||
class Node3D;
|
||||
} // namespace godot
|
||||
|
||||
namespace sequa::godot {
|
||||
|
||||
// Nœuds Godot qui reflètent la scène SEQUA, sous une racine fournie par l'hébergement.
|
||||
// N'est utilisé que pendant que Godot tourne, depuis le fil qui appelle iteration().
|
||||
// Attention : dans ce namespace, « godot:: » désigne sequa::godot ; les classes du moteur
|
||||
// s'écrivent « ::godot:: ».
|
||||
class GodotScene {
|
||||
public:
|
||||
explicit GodotScene(::godot::Node3D* root) noexcept;
|
||||
GodotScene(const GodotScene&) = delete;
|
||||
GodotScene& operator=(const GodotScene&) = delete;
|
||||
~GodotScene();
|
||||
|
||||
// Le delta a déjà été validé par three_d::SceneState : il n'est pas revalidé ici.
|
||||
void apply(const three_d::SceneDelta& delta);
|
||||
|
||||
// Détache et détruit tous les nœuds créés (objets et caméra).
|
||||
void clear() noexcept;
|
||||
|
||||
private:
|
||||
::godot::Node3D* root_;
|
||||
std::map<three_d::ObjectId, ::godot::MeshInstance3D*> objects_;
|
||||
::godot::Camera3D* camera_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace sequa::godot
|
||||
@@ -0,0 +1,229 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#include "sequa/godot/conversions.hpp"
|
||||
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
|
||||
using sequa::three_d::Color;
|
||||
using sequa::three_d::Primitive;
|
||||
using sequa::three_d::Quat;
|
||||
using sequa::three_d::Vec3;
|
||||
using sequa::three_d::Visual;
|
||||
using namespace sequa::godot::convert;
|
||||
|
||||
const float nan_value = std::numeric_limits<float>::quiet_NaN();
|
||||
const float inf_value = std::numeric_limits<float>::infinity();
|
||||
const float sin_45 = 0.7071067811865475f;
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("convert : normalized_rotation garde l'identite")
|
||||
{
|
||||
CHECK(normalized_rotation(Quat{}) == Quat{});
|
||||
CHECK(normalized_rotation(Quat{0, 0, 0, 1}) == Quat{});
|
||||
CHECK(normalized_rotation(Quat{0, 0, 0, 2}) == Quat{});
|
||||
}
|
||||
|
||||
TEST_CASE("convert : normalized_rotation cas invalides -> identite")
|
||||
{
|
||||
CHECK(normalized_rotation(Quat{0, 0, 0, 0}) == Quat{});
|
||||
CHECK(normalized_rotation(Quat{nan_value, 0, 0, 1}) == Quat{});
|
||||
CHECK(normalized_rotation(Quat{0, inf_value, 0, 1}) == Quat{});
|
||||
CHECK(normalized_rotation(Quat{0, 0, 0, -inf_value}) == Quat{});
|
||||
}
|
||||
|
||||
TEST_CASE("convert : normalized_rotation normalise les quaternions non unitaires")
|
||||
{
|
||||
const Quat normalized = normalized_rotation(Quat{2, 0, 0, 0});
|
||||
CHECK(normalized.x == doctest::Approx(1.0f));
|
||||
CHECK(normalized.y == doctest::Approx(0.0f));
|
||||
CHECK(normalized.z == doctest::Approx(0.0f));
|
||||
CHECK(normalized.w == doctest::Approx(0.0f));
|
||||
|
||||
const Quat around_w = normalized_rotation(Quat{0, 0, 0, 3});
|
||||
CHECK(around_w == Quat{0, 0, 0, 1});
|
||||
}
|
||||
|
||||
TEST_CASE("convert : basis_from identite et echelle pure")
|
||||
{
|
||||
const Basis unit = basis_from(Quat{}, Vec3{1, 1, 1});
|
||||
CHECK(unit.x == Vec3{1, 0, 0});
|
||||
CHECK(unit.y == Vec3{0, 1, 0});
|
||||
CHECK(unit.z == Vec3{0, 0, 1});
|
||||
|
||||
const Basis scaled = basis_from(Quat{}, Vec3{2, 3, 4});
|
||||
CHECK(scaled.x == Vec3{2, 0, 0});
|
||||
CHECK(scaled.y == Vec3{0, 3, 0});
|
||||
CHECK(scaled.z == Vec3{0, 0, 4});
|
||||
}
|
||||
|
||||
TEST_CASE("convert : basis_from rotation de 90 degres autour de Y")
|
||||
{
|
||||
// q = (0, sin 45, 0, cos 45) ; colonnes attendues : X -> -Z, Z -> +X.
|
||||
const Basis basis = basis_from(Quat{0, sin_45, 0, sin_45}, Vec3{1, 1, 1});
|
||||
CHECK(basis.x.x == doctest::Approx(0.0f));
|
||||
CHECK(basis.x.y == doctest::Approx(0.0f));
|
||||
CHECK(basis.x.z == doctest::Approx(-1.0f));
|
||||
CHECK(basis.y == Vec3{0, 1, 0});
|
||||
CHECK(basis.z.x == doctest::Approx(1.0f));
|
||||
CHECK(basis.z.y == doctest::Approx(0.0f));
|
||||
CHECK(basis.z.z == doctest::Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : basis_from echelle multiplie chaque colonne")
|
||||
{
|
||||
const Basis basis = basis_from(Quat{0, sin_45, 0, sin_45}, Vec3{2, 3, 4});
|
||||
CHECK(basis.x.x == doctest::Approx(0.0f));
|
||||
CHECK(basis.x.y == doctest::Approx(0.0f));
|
||||
CHECK(basis.x.z == doctest::Approx(-2.0f));
|
||||
CHECK(basis.y == Vec3{0, 3, 0});
|
||||
CHECK(basis.z.x == doctest::Approx(4.0f));
|
||||
CHECK(basis.z.y == doctest::Approx(0.0f));
|
||||
CHECK(basis.z.z == doctest::Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : linear_to_srgb bornes et linearite basse")
|
||||
{
|
||||
CHECK(linear_to_srgb(0.0f) == doctest::Approx(0.0f));
|
||||
CHECK(linear_to_srgb(1.0f) == doctest::Approx(1.0f));
|
||||
// Frontiere du segment lineaire : 12.92 * 0.0031308.
|
||||
CHECK(linear_to_srgb(0.0031308f) == doctest::Approx(12.92f * 0.0031308f));
|
||||
CHECK(linear_to_srgb(0.002f) == doctest::Approx(0.002f * 12.92f));
|
||||
CHECK(linear_to_srgb(0.5f) == doctest::Approx(0.735357f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : linear_to_srgb valeurs hors bornes et non finies")
|
||||
{
|
||||
CHECK(linear_to_srgb(-0.5f) == doctest::Approx(0.0f));
|
||||
CHECK(linear_to_srgb(2.0f) == doctest::Approx(1.0f));
|
||||
CHECK(linear_to_srgb(nan_value) == doctest::Approx(0.0f));
|
||||
CHECK(linear_to_srgb(inf_value) == doctest::Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : to_srgb convertit rgb et conserve l'alpha")
|
||||
{
|
||||
const Color result = to_srgb(Color{0.5f, 0.0f, 2.0f, 0.25f});
|
||||
CHECK(result.r == doctest::Approx(0.735357f));
|
||||
CHECK(result.g == doctest::Approx(0.0f));
|
||||
CHECK(result.b == doctest::Approx(1.0f));
|
||||
CHECK(result.a == doctest::Approx(0.25f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : to_srgb alpha non fini -> opaque")
|
||||
{
|
||||
CHECK(to_srgb(Color{1, 1, 1, nan_value}).a == doctest::Approx(1.0f));
|
||||
CHECK(to_srgb(Color{1, 1, 1, 3.0f}).a == doctest::Approx(1.0f));
|
||||
CHECK(to_srgb(Color{1, 1, 1, -1.0f}).a == doctest::Approx(0.0f));
|
||||
CHECK(to_srgb(Color{1, 1, 1, inf_value}).a == doctest::Approx(1.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : mesh_params box")
|
||||
{
|
||||
Visual visual;
|
||||
visual.primitive = Primitive::box;
|
||||
visual.size = Vec3{2, 3, 4};
|
||||
const MeshParams params = mesh_params(visual);
|
||||
CHECK(params.primitive == Primitive::box);
|
||||
CHECK(params.box_size == Vec3{2, 3, 4});
|
||||
// Les champs des autres primitives gardent leur valeur par defaut.
|
||||
CHECK(params.radius == doctest::Approx(0.5f));
|
||||
CHECK(params.height == doctest::Approx(1.0f));
|
||||
CHECK(params.plane_width == doctest::Approx(1.0f));
|
||||
CHECK(params.plane_depth == doctest::Approx(1.0f));
|
||||
|
||||
// Visual par defaut (box 1x1x1).
|
||||
const MeshParams default_params = mesh_params(Visual{});
|
||||
CHECK(default_params.box_size == Vec3{1, 1, 1});
|
||||
}
|
||||
|
||||
TEST_CASE("convert : mesh_params sphere")
|
||||
{
|
||||
Visual visual;
|
||||
visual.primitive = Primitive::sphere;
|
||||
visual.size = Vec3{2, 3, 4};
|
||||
const MeshParams params = mesh_params(visual);
|
||||
CHECK(params.primitive == Primitive::sphere);
|
||||
CHECK(params.radius == doctest::Approx(1.0f));
|
||||
CHECK(params.height == doctest::Approx(3.0f));
|
||||
CHECK(params.box_size == Vec3{1, 1, 1});
|
||||
CHECK(params.plane_width == doctest::Approx(1.0f));
|
||||
CHECK(params.plane_depth == doctest::Approx(1.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : mesh_params plane")
|
||||
{
|
||||
Visual visual;
|
||||
visual.primitive = Primitive::plane;
|
||||
visual.size = Vec3{5, 2, 7};
|
||||
const MeshParams params = mesh_params(visual);
|
||||
CHECK(params.primitive == Primitive::plane);
|
||||
CHECK(params.plane_width == doctest::Approx(5.0f));
|
||||
CHECK(params.plane_depth == doctest::Approx(7.0f));
|
||||
CHECK(params.box_size == Vec3{1, 1, 1});
|
||||
CHECK(params.radius == doctest::Approx(0.5f));
|
||||
CHECK(params.height == doctest::Approx(1.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : mesh_params capsule trop courte -> height >= 2*radius")
|
||||
{
|
||||
Visual visual;
|
||||
visual.primitive = Primitive::capsule;
|
||||
visual.size = Vec3{2, 1, 2};
|
||||
const MeshParams params = mesh_params(visual);
|
||||
CHECK(params.primitive == Primitive::capsule);
|
||||
CHECK(params.radius == doctest::Approx(1.0f));
|
||||
CHECK(params.height == doctest::Approx(2.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : mesh_params capsule assez haute conserve height")
|
||||
{
|
||||
Visual visual;
|
||||
visual.primitive = Primitive::capsule;
|
||||
visual.size = Vec3{2, 4, 2};
|
||||
const MeshParams params = mesh_params(visual);
|
||||
CHECK(params.radius == doctest::Approx(1.0f));
|
||||
CHECK(params.height == doctest::Approx(4.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : mesh_params securise les tailles negatives et nulles")
|
||||
{
|
||||
Visual visual;
|
||||
visual.primitive = Primitive::box;
|
||||
visual.size = Vec3{-2, 0, inf_value};
|
||||
const MeshParams params = mesh_params(visual);
|
||||
CHECK(params.box_size == Vec3{2, min_dimension, min_dimension});
|
||||
|
||||
Visual negative_sphere;
|
||||
negative_sphere.primitive = Primitive::sphere;
|
||||
negative_sphere.size = Vec3{-4, -6, 0};
|
||||
const MeshParams sphere = mesh_params(negative_sphere);
|
||||
CHECK(sphere.radius == doctest::Approx(2.0f));
|
||||
CHECK(sphere.height == doctest::Approx(6.0f));
|
||||
|
||||
Visual tiny_capsule;
|
||||
tiny_capsule.primitive = Primitive::capsule;
|
||||
tiny_capsule.size = Vec3{0, 0, 0};
|
||||
const MeshParams capsule = mesh_params(tiny_capsule);
|
||||
CHECK(capsule.radius == doctest::Approx(0.0005f));
|
||||
CHECK(capsule.height == doctest::Approx(min_dimension));
|
||||
|
||||
Visual negative_plane;
|
||||
negative_plane.primitive = Primitive::plane;
|
||||
negative_plane.size = Vec3{-5, 1, -7};
|
||||
const MeshParams plane = mesh_params(negative_plane);
|
||||
CHECK(plane.plane_width == doctest::Approx(5.0f));
|
||||
CHECK(plane.plane_depth == doctest::Approx(7.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("convert : mesh_params securise les tailles non finies")
|
||||
{
|
||||
Visual visual;
|
||||
visual.primitive = Primitive::sphere;
|
||||
visual.size = Vec3{nan_value, nan_value, nan_value};
|
||||
const MeshParams params = mesh_params(visual);
|
||||
CHECK(params.radius == doctest::Approx(min_dimension * 0.5f));
|
||||
CHECK(params.height == doctest::Approx(min_dimension));
|
||||
}
|
||||
@@ -18,8 +18,11 @@ TEST_CASE("GodotEngineAdapter : capacites declarees sans demarrer le moteur")
|
||||
CHECK_FALSE(caps.buffers.contains(three_d::FrameBuffer::object_ids));
|
||||
}
|
||||
|
||||
TEST_CASE("GodotEngineAdapter : non demarre tant que l'agent n'existe pas")
|
||||
TEST_CASE("GodotEngineAdapter : construit sans hebergement, start() non implemente")
|
||||
{
|
||||
if (godot::runtime_available()) {
|
||||
return; // démarrage réel : couvert par la démonstration (fenêtre Godot)
|
||||
}
|
||||
godot::GodotEngineAdapter adapter;
|
||||
const auto error = adapter.start({});
|
||||
REQUIRE(error.has_value());
|
||||
@@ -27,3 +30,15 @@ TEST_CASE("GodotEngineAdapter : non demarre tant que l'agent n'existe pas")
|
||||
CHECK_FALSE(adapter.running());
|
||||
CHECK(adapter.apply({1, {}})->code == three_d::Error::Code::not_started);
|
||||
}
|
||||
|
||||
TEST_CASE("GodotEngineAdapter : rendu sans fenetre refuse")
|
||||
{
|
||||
godot::GodotEngineAdapter adapter;
|
||||
three_d::EngineConfig config;
|
||||
config.headless = true;
|
||||
const auto error = adapter.start(config);
|
||||
REQUIRE(error.has_value());
|
||||
CHECK((error->code == three_d::Error::Code::unsupported ||
|
||||
error->code == three_d::Error::Code::not_implemented));
|
||||
CHECK_FALSE(adapter.running());
|
||||
}
|
||||
Reference in new issue
Block a user