Sky and water are fixed for demo
This commit is contained in:
@@ -135,6 +135,67 @@ cd demos/demo-scene-switching-extra
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door,
|
||||
# Escape = pause menu (frees the cursor).
|
||||
|
||||
# Demo: same scene-switch-door setup as demo-scene-switching-extra, but
|
||||
# the exterior scene is a real outdoor environment instead of a flat
|
||||
# colored floor (demos/demo-interior-exterior-dynamics, target
|
||||
# demoInteriorExteriorDynamics). demo_scene_interior.json keeps the
|
||||
# interiorOnly CellGrid room (own ProceduralMaterial/ProceduralTexture,
|
||||
# F6 persistent locked door + inline scene script, F1 exit scene-switch
|
||||
# door Z:0:0:15 to demo_scene_exterior.json). demo_scene_exterior.json
|
||||
# holds the exteriorOnly shell (entity "x1"/"w1", grid-wide
|
||||
# doorSceneSwitchPath back to demo_scene_interior.json) standing on a
|
||||
# STREAMING terrain entity (streamingEnabled, worldSizeUnits 10000 ->
|
||||
# 5x5 pages of 2000 units, layers Ground23/Ground37; base heights from
|
||||
# baseNoise OpenSimplex2 seed 55, 3 octaves, frequency 0.0003,
|
||||
# amplitude 15 -> an archipelago of large islands, ~34% land, the rest
|
||||
# ocean). The bounded
|
||||
# streaming world spans [-1000, 9000] on both axes around the terrain
|
||||
# entity at the origin (world (0,0) is half a page in from the corner,
|
||||
# not the world center), so all exterior content is placed near the
|
||||
# world center ~(4000, 4000) on an island with 500+ units of dry land
|
||||
# in every direction: the grid at (4000, 10.0148, 4000) with
|
||||
# its floor exactly on the terrain surface, arrival_b at
|
||||
# (4000.6, 9.9905, 3995.9) 3 units outside (-Z of) the return door
|
||||
# Z:0:0:0, spawner s1 at (4000, 9.9039, 3975) - Y values probed from
|
||||
# TerrainSystem::getHeightAt; the --test-switch exterior arrival check
|
||||
# re-reads the expected Y from getHeightAt and polls while the streaming
|
||||
# window loads and the grounding watchdog re-clamps, then asserts the
|
||||
# grid site is above the water level (dry-island check). The "sky"
|
||||
# entity (skybox + sun) sits at the content site (4000, 10, 4000):
|
||||
# EditorSunSystem parents the sun/moon sphere nodes to the root node
|
||||
# and orbits them around the camera (radius 200), so they line up with
|
||||
# the shader sun/moon discs, but the light node stays on the sky
|
||||
# entity's transform node, so the entity should sit near the content
|
||||
# for sensible shadow placement. A "water"
|
||||
# entity (planeSize 12000, waterSurfaceY 6.0 in both waterPlane and
|
||||
# waterPhysics, buoyancy modeled on town8.json) completes the scene:
|
||||
# terrain below Y 6 is ocean floor while the content island (terrain
|
||||
# ~10 at the site) stays dry. The shipped heightmap
|
||||
# heightmaps/4242424300000001/heightmap.bin (generated by the demo's
|
||||
# gen_heightmap.py, staged via configure_file so source changes re-copy
|
||||
# on the next build) is unused in streaming mode but keeps the
|
||||
# non-streaming fallback working.
|
||||
cd demos/demo-interior-exterior-dynamics
|
||||
./demoInteriorExteriorDynamics
|
||||
# ...or headless smoke run (one frame, then exit):
|
||||
./demoInteriorExteriorDynamics --headless --exit-after-first-frame
|
||||
# ...or headless end-to-end check of the interior -> exterior ->
|
||||
# interior round trip through both scene-switch doors plus the F6
|
||||
# locked-door contract (exits non-zero on failure):
|
||||
./demoInteriorExteriorDynamics --headless --test-switch
|
||||
# Debug capture aids (offscreen/xvfb screenshots): teleport the player
|
||||
# once it is alive (optionally aiming the TPS camera, degrees) and dump
|
||||
# frame N to a file, e.g.
|
||||
# xvfb-run -a -s "-screen 0 1920x1080x24" \
|
||||
# ./demoInteriorExteriorDynamics demo_scene_exterior.json \
|
||||
# --force-pos 4800 9.5 4000 270 5 --screenshot 350 /tmp/shot.png
|
||||
# Swimming: the spawned character's ride height/damping come from
|
||||
# buoyancyInfo in prefabs/char_2.json (buoyancy 1.05 -> ~95% submerged,
|
||||
# linearDrag 0.6, angularDrag 0.2); the prefab is a runtime artifact
|
||||
# staged from <build>/src/features/editScene/prefabs.
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door,
|
||||
# Escape = pause menu (frees the cursor).
|
||||
|
||||
|
||||
# Road wedge/segment self-intersection regression test (no scene needed,
|
||||
# also registered as CTest roadGeometryOverlapTest)
|
||||
@@ -258,7 +319,12 @@ system that should drive player locomotion animation.
|
||||
|
||||
- Reads `GameInputState` and drives TPS/FPS camera and locomotion animation.
|
||||
- Sets animation states on the `locomotion` state machine:
|
||||
`idle` / `walking` / `running` and the swim equivalents.
|
||||
`idle` / `walking` / `running` and the swim equivalents. The swim
|
||||
states are only selected when the character is actually floating
|
||||
(`InWater` tag set *and* `CharacterComponent::isSupported` false);
|
||||
when wading in shallow water the character stands on the bottom, so
|
||||
the land states keep playing. `isSupported` is refreshed every frame
|
||||
by `CharacterSystem` from `JPH::Character::IsSupported()`.
|
||||
- In game mode it adds `PlayerControlledComponent` to the target entity so AI
|
||||
systems skip it.
|
||||
- When `targetCharacterName` resolves to a `CharacterSpawnerComponent`, it locks
|
||||
@@ -286,6 +352,29 @@ bool isSpawnerLocked(flecs::entity spawner) const;
|
||||
Spawned characters have `EditorMarkerComponent` removed and are removed from
|
||||
`EditorUISystem` caches so they don't appear in editor lists.
|
||||
|
||||
### EditorSkyboxSystem / EditorSunSystem
|
||||
|
||||
The procedural sky (`Skybox/Dynamic` material) is rendered as a fullscreen
|
||||
NDC triangle (`EditorSkyboxSystem::createSkyTriangle`); the vertex shader
|
||||
reconstructs the per-pixel view ray from the camera basis uniforms
|
||||
(`camForward` / `camRight` / `camUp` / `tanHalfFov`). It used to be a
|
||||
camera-following cube, but the cube faces behind the camera rasterised with
|
||||
a degenerate (zero) interpolated position and painted horizon-glow
|
||||
"circles" around the view direction. All vertex uniforms are set manually
|
||||
every frame from the main camera in `updateShaderParams()`: auto constants
|
||||
did not reach the vertex program on all render systems, and a manual
|
||||
`Matrix4` uniform did not land in the `OGRE_UNIFORMS` block either, so only
|
||||
vector types are used (the water reflection/refraction passes reuse the
|
||||
main camera's basis, which is close enough for a sky). The legacy
|
||||
`SkyboxComponent::size` field is still (de)serialized but unused; the
|
||||
skybox editor shows it disabled.
|
||||
|
||||
`EditorSunSystem` parents the sun/moon sphere nodes to the root node and
|
||||
orbits them around the camera (radius 200) so they line up with the shader
|
||||
sun/moon discs; the spheres hide once `sunElev` passes ~the horizon
|
||||
(`-0.03` / `0.03`, roughly the sphere angular radius) so they cannot shine
|
||||
through water or terrain.
|
||||
|
||||
### TerrainPrefabSpawnerSystem
|
||||
|
||||
Distance-based spawn/despawn of prefab instances on terrain
|
||||
|
||||
@@ -1050,3 +1050,4 @@ add_subdirectory(demos/demo-lua-scene-script)
|
||||
add_subdirectory(demos/demo-character-controller)
|
||||
add_subdirectory(demos/demo-scene-switching)
|
||||
add_subdirectory(demos/demo-scene-switching-extra)
|
||||
add_subdirectory(demos/demo-interior-exterior-dynamics)
|
||||
|
||||
@@ -542,6 +542,10 @@ void EditorApp::setup()
|
||||
m_world, m_sceneMgr);
|
||||
m_waterPlaneSystem = std::make_unique<EditorWaterPlaneSystem>(
|
||||
m_world, m_sceneMgr);
|
||||
if (m_uiSystem) {
|
||||
m_uiSystem->setSunSystem(m_sunSystem.get());
|
||||
m_uiSystem->setSkyboxSystem(m_skyboxSystem.get());
|
||||
}
|
||||
|
||||
// TerrainSystem — owns Ogre terrain singletons, needs camera for paging.
|
||||
{
|
||||
@@ -2264,10 +2268,32 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
|
||||
/* --- Buoyancy system (before physics so impulse is integrated) --- */
|
||||
if (m_buoyancySystem) {
|
||||
// Update camera position for water detection area
|
||||
if (m_camera) {
|
||||
/* Update the camera position for the water detection
|
||||
* area. Use the real camera node position, not
|
||||
* EditorCamera::getPosition() - that cached value is
|
||||
* only updated by editor-mode fly movement and stays
|
||||
* at its initial value in game mode, which would pin
|
||||
* the water detection box to the world origin. The
|
||||
* broadphase query works in world space, so convert
|
||||
* from render space. */
|
||||
Ogre::Camera *cam = nullptr;
|
||||
if (m_sceneMgr->hasCamera("PlayerCamera"))
|
||||
cam = m_sceneMgr->getCamera("PlayerCamera");
|
||||
if (!cam && m_camera)
|
||||
cam = m_camera->getCamera();
|
||||
if (cam) {
|
||||
Ogre::Vector3 cameraPos =
|
||||
m_camera->getPosition();
|
||||
cam->getDerivedPosition();
|
||||
RenderOriginSystem *ro =
|
||||
RenderOriginSystem::getInstance();
|
||||
if (ro) {
|
||||
JPH::DVec3 worldPos =
|
||||
ro->renderToWorld(cameraPos);
|
||||
cameraPos = Ogre::Vector3(
|
||||
(float)worldPos.GetX(),
|
||||
(float)worldPos.GetY(),
|
||||
(float)worldPos.GetZ());
|
||||
}
|
||||
m_buoyancySystem->setCameraPosition(cameraPos);
|
||||
}
|
||||
m_buoyancySystem->update(evt.timeSinceLastFrame);
|
||||
@@ -2291,7 +2317,12 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
|
||||
/* --- Rendering support systems --- */
|
||||
if (m_sunSystem) {
|
||||
m_sunSystem->update(evt.timeSinceLastFrame);
|
||||
Ogre::Camera *cam = nullptr;
|
||||
if (m_sceneMgr->hasCamera("PlayerCamera"))
|
||||
cam = m_sceneMgr->getCamera("PlayerCamera");
|
||||
if (!cam && m_camera)
|
||||
cam = m_camera->getCamera();
|
||||
m_sunSystem->update(evt.timeSinceLastFrame, cam);
|
||||
}
|
||||
if (m_skyboxSystem) {
|
||||
Ogre::Camera *cam = nullptr;
|
||||
|
||||
@@ -315,6 +315,10 @@ public:
|
||||
{
|
||||
return m_characterSpawnerSystem.get();
|
||||
}
|
||||
PlayerControllerSystem *getPlayerControllerSystem() const
|
||||
{
|
||||
return m_playerControllerSystem.get();
|
||||
}
|
||||
TerrainPrefabSpawnerSystem *getTerrainPrefabSpawnerSystem() const
|
||||
{
|
||||
return m_terrainPrefabSpawnerSystem.get();
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
/**
|
||||
* Character physics component
|
||||
*
|
||||
* Attaches a Jolt JPH::Character (kinematic capsule) to the entity.
|
||||
* The entity may also have CharacterSlotsComponent; the character
|
||||
* physics lives on the same entity as the visual character.
|
||||
* Attaches a Jolt JPH::Character (dynamic capsule, translation-only)
|
||||
* to the entity. The entity may also have CharacterSlotsComponent;
|
||||
* the character physics lives on the same entity as the visual
|
||||
* character.
|
||||
*
|
||||
* Child entities can add extra collision shapes via PhysicsColliderComponent.
|
||||
*/
|
||||
@@ -50,6 +51,12 @@ struct CharacterComponent {
|
||||
float floorCheckDistance = 2.0f;
|
||||
bool useGravity = true;
|
||||
|
||||
/* Ground contact state from JPH::Character::IsSupported(), refreshed
|
||||
* by CharacterSystem every frame. Runtime only — used to tell
|
||||
* "floating/swimming" apart from "standing on the bottom in shallow
|
||||
* water" (InWater alone cannot). */
|
||||
bool isSupported = false;
|
||||
|
||||
/* Per-character collision group. Body = subgroup 0, head = subgroup 1,
|
||||
* hair joints = 2+. Runtime only — regenerated when character is rebuilt. */
|
||||
uint32_t collisionGroupId = 0;
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Skybox component - procedural sky rendered as a large cube
|
||||
* with a fragment shader that creates dynamic day/night/sunset
|
||||
* sky gradients.
|
||||
* Skybox component - procedural sky rendered as a fullscreen
|
||||
* triangle with a fragment shader that creates dynamic
|
||||
* day/night/sunset sky gradients from the per-pixel view ray.
|
||||
*
|
||||
* Designed to work alongside SunComponent on the same entity.
|
||||
* If no SunComponent is present, uses default noon lighting.
|
||||
@@ -16,7 +16,8 @@ struct SkyboxComponent {
|
||||
// Enable/disable skybox
|
||||
bool enabled = true;
|
||||
|
||||
// Size of the skybox cube (default 500)
|
||||
// Legacy size of the old skybox cube; kept for scene
|
||||
// compatibility but unused - the sky is a fullscreen triangle now.
|
||||
float size = 500.0f;
|
||||
|
||||
// Day sky colors
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# demo-interior-exterior-dynamics — scene switching demo with a CellGrid
|
||||
# interior and a terrain/sky/water exterior
|
||||
# ---------------------------------------------------------------------------
|
||||
# Same scene-switch-door setup as demos/demo-scene-switching-extra, but
|
||||
# the exterior scene replaces the flat colored floor with a real outdoor
|
||||
# environment: a STREAMING terrain entity (worldSizeUnits 10000, 5x5 pages
|
||||
# of 2000 units, base heights from TerrainComponent::baseNoise —
|
||||
# OpenSimplex2 seed 55, 3 octaves, frequency 0.0003, amplitude 15: an
|
||||
# archipelago of large islands, ~34% land — with the shipped heightmap.bin
|
||||
# kept staged for non-streaming fallback), a sky entity (skybox + sun) and
|
||||
# a water entity (waterPlane + waterPhysics), modeled on town8.json. The
|
||||
# bounded streaming world spans [-1000, 9000] on both axes around the
|
||||
# terrain entity at the origin, so all exterior content (exteriorOnly
|
||||
# CellGrid shell, arrival_b, spawner s1, sky, water) is placed near the
|
||||
# world center at ~(4000, 4000) on an island with 500+ units of dry land
|
||||
# in every direction, with the grid floor and arrival marker resting on
|
||||
# the terrain surface (heights probed from the engine) and the water level
|
||||
# at Y 6 so the site (terrain ~10) stays dry while everything below is
|
||||
# ocean floor. The sky entity sits at the content site because
|
||||
# EditorSunSystem keeps the sun's light node on its transform node (the
|
||||
# sun/moon spheres themselves orbit the camera). The interior scene is
|
||||
# unchanged: an "interrior" entity
|
||||
# with a CellGridComponent (a room with a floor, ceiling, interior walls
|
||||
# and an exit door) that carries its own ProceduralMaterial +
|
||||
# ProceduralTexture. The grid's texture rectangle names reference the
|
||||
# texture's named rects ("floor" / "ceiling"), demonstrating material/UV
|
||||
# pickup from a grid entity without a Lot/District/Town parent.
|
||||
#
|
||||
# The executable is self-contained in this build directory: a POST_BUILD
|
||||
# step (stage_runtime.cmake) copies resources.cfg, the
|
||||
# character prefab (prefabs/char_2.json, written by a previous editor/game
|
||||
# run) and any runtime config JSONs here, and symlinks both demo scenes
|
||||
# (demo_scene_interior.json / demo_scene_exterior.json, so scene edits in
|
||||
# the source tree are visible without a rebuild) plus the big pre-staged
|
||||
# runtime directories (resources/, characters/, lua-scripts/) from the
|
||||
# editScene binary directory; the terrain heightmap is staged separately
|
||||
# by configure_file (see below) so source changes re-copy it on the next
|
||||
# build. Run it from here:
|
||||
# cd <build>/src/features/editScene/demos/demo-interior-exterior-dynamics
|
||||
# ./demoInteriorExteriorDynamics
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door,
|
||||
# Escape = pause menu (frees the cursor).
|
||||
|
||||
get_filename_component(EDITSCENE_SOURCE_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
|
||||
get_filename_component(EDITSCENE_BINARY_DIR
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/../.." ABSOLUTE)
|
||||
|
||||
# Reuse the editScene sources, swapping main.cpp for demo_main.cpp.
|
||||
set(DEMO_SOURCES ${EDITSCENE_SOURCES})
|
||||
list(REMOVE_ITEM DEMO_SOURCES main.cpp)
|
||||
list(TRANSFORM DEMO_SOURCES PREPEND "${EDITSCENE_SOURCE_DIR}/")
|
||||
|
||||
# --- Embedded project configuration (F8 release binary) ---------------
|
||||
# Read project.json at configure time and embed its parameters into the
|
||||
# binary via a generated project.h, so the release binary is attached to
|
||||
# its project directory without needing --project. Editing project.json
|
||||
# re-triggers the CMake configure step (CMAKE_CONFIGURE_DEPENDS).
|
||||
set(PROJECT_JSON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/project.json")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
"${PROJECT_JSON_PATH}")
|
||||
file(READ "${PROJECT_JSON_PATH}" PROJECT_JSON_TEXT)
|
||||
string(JSON PROJECT_APP_NAME GET "${PROJECT_JSON_TEXT}" appName)
|
||||
string(JSON PROJECT_START_SCENE GET "${PROJECT_JSON_TEXT}" startScene)
|
||||
string(JSON PROJECT_GAME_MODE GET "${PROJECT_JSON_TEXT}" gameMode)
|
||||
if(PROJECT_GAME_MODE)
|
||||
set(PROJECT_GAME_MODE_VALUE 1)
|
||||
else()
|
||||
set(PROJECT_GAME_MODE_VALUE 0)
|
||||
endif()
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/project.h.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated/project.h" @ONLY)
|
||||
|
||||
# Terrain heightmap for the exterior scene's terrain entity (terrainId
|
||||
# 4242424300000001, heightmapFile heightmap.bin — TerrainSystem resolves it
|
||||
# as heightmaps/<terrainId>/<heightmapFile> relative to the CWD). Generated
|
||||
# in the source tree by gen_heightmap.py. The terrain runs in streaming
|
||||
# mode (base heights come from baseNoise, not the heightmap), but the file
|
||||
# stays staged so flipping streamingEnabled off keeps working. A
|
||||
# configure_file COPYONLY copy is used on purpose: it registers a configure
|
||||
# dependency on the source file, so regenerating heightmap.bin re-copies it
|
||||
# into the staging directory on the next build without manual intervention.
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/heightmap.bin"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/heightmaps/4242424300000001/heightmap.bin"
|
||||
COPYONLY)
|
||||
|
||||
add_executable(demoInteriorExteriorDynamics
|
||||
demo_main.cpp
|
||||
${DEMO_SOURCES}
|
||||
)
|
||||
|
||||
target_compile_definitions(demoInteriorExteriorDynamics
|
||||
PRIVATE EDITSCENE_HAS_EMBEDDED_PROJECT)
|
||||
|
||||
add_dependencies(demoInteriorExteriorDynamics morph)
|
||||
|
||||
# Define JPH_DEBUG_RENDERER for physics debug drawing (same as editor)
|
||||
target_compile_definitions(demoInteriorExteriorDynamics PRIVATE JPH_DEBUG_RENDERER)
|
||||
|
||||
target_link_libraries(demoInteriorExteriorDynamics
|
||||
OgreMain
|
||||
OgreBites
|
||||
OgreOverlay
|
||||
OgreMeshLodGenerator
|
||||
OgrePaging
|
||||
OgreTerrain
|
||||
flecs::flecs_static
|
||||
nlohmann_json::nlohmann_json
|
||||
Jolt::Jolt
|
||||
OgreProcedural::OgreProcedural
|
||||
RecastNavigation::Recast
|
||||
RecastNavigation::Detour
|
||||
RecastNavigation::DetourTileCache
|
||||
RecastNavigation::DetourCrowd
|
||||
RecastNavigation::DebugUtils
|
||||
PackageArchive
|
||||
RoadGeometryLib
|
||||
lua
|
||||
SDL2::SDL2
|
||||
)
|
||||
|
||||
target_include_directories(demoInteriorExteriorDynamics PRIVATE
|
||||
${CMAKE_CURRENT_BINARY_DIR}/generated
|
||||
${EDITSCENE_SOURCE_DIR}
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/Recast/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/Detour/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/DetourTileCache/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/DetourCrowd/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/DebugUtils/Include
|
||||
${CMAKE_SOURCE_DIR}/src/FastNoiseLite
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lpeg-1.1.0
|
||||
)
|
||||
|
||||
# Stage the standalone runtime next to the executable (see header comment).
|
||||
add_custom_command(TARGET demoInteriorExteriorDynamics POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DDEMO_DIR=${CMAKE_CURRENT_BINARY_DIR}
|
||||
-DEDITSCENE_BIN=${EDITSCENE_BINARY_DIR}
|
||||
-DEDITSCENE_SRC=${EDITSCENE_SOURCE_DIR}
|
||||
-DSRC_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/stage_runtime.cmake"
|
||||
COMMENT "Staging demo-interior-exterior-dynamics standalone runtime"
|
||||
)
|
||||
@@ -0,0 +1,885 @@
|
||||
#include <iostream>
|
||||
#include <cstdlib>
|
||||
#include "EditorApp.hpp"
|
||||
#include "ProjectConfig.hpp"
|
||||
#include "camera/EditorCamera.hpp"
|
||||
#include "systems/CharacterRegistry.hpp"
|
||||
#include "systems/DoorSystem.hpp"
|
||||
#include "systems/PlayerControllerSystem.hpp"
|
||||
#include "systems/TerrainSystem.hpp"
|
||||
#include "systems/EventBus.hpp"
|
||||
#include "systems/GlobalStateStore.hpp"
|
||||
#include "components/Door.hpp"
|
||||
#include "components/EntityName.hpp"
|
||||
#include "components/Transform.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <OgreRoot.h>
|
||||
#include <filesystem>
|
||||
|
||||
#ifdef EDITSCENE_HAS_EMBEDDED_PROJECT
|
||||
#include "project.h"
|
||||
#endif
|
||||
|
||||
struct ExitAfterFirstFrameListener : public Ogre::FrameListener {
|
||||
Ogre::Root *root;
|
||||
bool triggered = false;
|
||||
ExitAfterFirstFrameListener(Ogre::Root *r) : root(r) {}
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &) override
|
||||
{
|
||||
if (!triggered) {
|
||||
triggered = true;
|
||||
root->queueEndRendering();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/* Debug capture aid (for offscreen/xvfb screenshots):
|
||||
* --force-pos x y z yawDeg [pitchDeg]
|
||||
* teleport the player character once, as
|
||||
* soon as it is alive (physics picks the
|
||||
* new position up through the usual
|
||||
* node->physics sync, gravity/buoyancy
|
||||
* then act naturally); yaw/pitch (degrees)
|
||||
* are re-applied to the TPS camera every
|
||||
* frame via PlayerControllerSystem;
|
||||
* --screenshot N path write frame N of the render window to
|
||||
* path (RenderWindow::writeContentsToFile)
|
||||
* and quit.
|
||||
* --debug-buoyancy enable BuoyancySystem debug logging (same
|
||||
* as the editor's --debug-buoyancy). */
|
||||
struct DebugCaptureListener : public Ogre::FrameListener {
|
||||
EditorApp *app;
|
||||
bool teleportEnabled = false;
|
||||
bool teleported = false;
|
||||
Ogre::Vector3 teleportPos = Ogre::Vector3::ZERO;
|
||||
float teleportYawDeg = 0.0f;
|
||||
float teleportPitchDeg = 0.0f;
|
||||
int shotFrame = -1;
|
||||
Ogre::String shotFile;
|
||||
int frame = 0;
|
||||
|
||||
DebugCaptureListener(EditorApp *a) : app(a) {}
|
||||
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &) override
|
||||
{
|
||||
frame++;
|
||||
if (teleportEnabled) {
|
||||
flecs::entity player = app->getPlayerCharacterEntity();
|
||||
if (player.is_alive() &&
|
||||
player.has<TransformComponent>()) {
|
||||
TransformComponent &t =
|
||||
player.get_mut<TransformComponent>();
|
||||
if (t.node) {
|
||||
/* Re-teleport while the character is below
|
||||
* the terrain surface at the target: the
|
||||
* streaming terrain colliders may not be
|
||||
* built yet on the first frames, so the
|
||||
* character can fall through. Once the
|
||||
* collider exists it stands (or floats in
|
||||
* deep water) at/above the surface and the
|
||||
* teleport stops. */
|
||||
bool fellThrough = false;
|
||||
TerrainSystem *ts =
|
||||
TerrainSystem::getInstance();
|
||||
if (teleported && ts && ts->isActive()) {
|
||||
float ground = ts->getHeightAt(
|
||||
teleportPos);
|
||||
fellThrough =
|
||||
t.node->getPosition().y <
|
||||
ground - 0.5f;
|
||||
}
|
||||
if (!teleported || fellThrough) {
|
||||
t.node->setPosition(teleportPos);
|
||||
t.node->setOrientation(Ogre::Quaternion(
|
||||
Ogre::Degree(teleportYawDeg),
|
||||
Ogre::Vector3::UNIT_Y));
|
||||
teleported = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* The TPS camera yaw lives in PlayerControllerSystem state,
|
||||
* not in the character node orientation, so re-apply the
|
||||
* requested yaw/pitch every frame once teleported. */
|
||||
if (teleported && app->getPlayerControllerSystem())
|
||||
app->getPlayerControllerSystem()->setControllerYawPitch(
|
||||
teleportYawDeg, teleportPitchDeg);
|
||||
if (shotFrame > 0 && frame >= shotFrame) {
|
||||
app->getRenderWindow()->writeContentsToFile(shotFile);
|
||||
app->getRoot()->queueEndRendering();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/* Same application as the editor/game, but with a larger initial window. */
|
||||
class DemoApp : public EditorApp {
|
||||
public:
|
||||
using EditorApp::EditorApp;
|
||||
|
||||
OgreBites::NativeWindowPair
|
||||
createWindow(const Ogre::String &name, uint32_t w, uint32_t h,
|
||||
Ogre::NameValuePairList miscParams) override
|
||||
{
|
||||
(void)w;
|
||||
(void)h;
|
||||
return EditorApp::createWindow(name, 1920, 1080, miscParams);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* The demo floor mesh and its flat colored material are created
|
||||
* programmatically because RenderableComponent only references a mesh by
|
||||
* name and carries no material/color of its own. Only the interior scene
|
||||
* needs one (the green "DemoFloorPlaneInterior"); the exterior scene
|
||||
* walks on the terrain instead.
|
||||
*/
|
||||
static void createFloorMesh(const Ogre::String &meshName,
|
||||
const Ogre::String &materialName,
|
||||
float dr, float dg, float db)
|
||||
{
|
||||
const Ogre::String group =
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME;
|
||||
|
||||
if (!Ogre::MeshManager::getSingleton()
|
||||
.getByName(meshName, group)
|
||||
.isNull())
|
||||
return;
|
||||
|
||||
Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().create(
|
||||
materialName, group);
|
||||
Ogre::Pass *pass = mat->getTechnique(0)->getPass(0);
|
||||
pass->setDiffuse(dr, dg, db, 1.0f);
|
||||
pass->setAmbient(dr * 0.4f, dg * 0.4f, db * 0.4f);
|
||||
pass->setSpecular(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
/* 60 x 60 units, matching the 30 x 0.1 x 30 static box collider
|
||||
* (top surface at y = 0) on the "demo_floor" entity in the scenes. */
|
||||
Ogre::Plane groundPlane(Ogre::Vector3::UNIT_Y, 0.0f);
|
||||
Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().createPlane(
|
||||
meshName, group, groundPlane, 60.0f, 60.0f, 1, 1, true, 1,
|
||||
1.0f, 1.0f, Ogre::Vector3::UNIT_Z);
|
||||
mesh->getSubMesh(0)->setMaterialName(materialName);
|
||||
}
|
||||
|
||||
static void createDemoResources()
|
||||
{
|
||||
/* Interior scene: green floor. The exterior scene has no floor
|
||||
* mesh — the terrain provides the ground. */
|
||||
createFloorMesh("DemoFloorPlaneInterior", "DemoFloorMaterialInterior",
|
||||
0.35f, 0.5f, 0.35f);
|
||||
}
|
||||
|
||||
/*
|
||||
* End-to-end check for --test-switch: drives the same path an E-press on
|
||||
* a scene-switch door takes (DoorComponent toggleRequested +
|
||||
* sceneSwitchPending -> DoorSystem swing -> EditorApp::switchScene()
|
||||
* queue once the leaf is fully open -> performSceneSwitch() on the next
|
||||
* frame with the "@arrival_*" teleport), for a full
|
||||
* interior -> exterior -> interior round trip: the interior scene exits
|
||||
* through the "interrior" grid's door Z:0:0:15 and the exterior scene
|
||||
* returns through its own grid's door Z:0:0:0. The prompt/targeting
|
||||
* glue is screen-space and therefore not covered headless.
|
||||
*/
|
||||
struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
EditorApp *app;
|
||||
int frame = 0;
|
||||
int phase = 10; /* 10-12: F6 locked door, then 0-3 scene switching */
|
||||
bool failed = false;
|
||||
Ogre::String failReason;
|
||||
|
||||
/* F6 demo door: internal doorway Z:0:0:8 of the "interrior" grid,
|
||||
* lockable + locked by default (doorConfigs in
|
||||
* demo_scene_interior.json); the inline scene script unlocks it on
|
||||
* the first door_locked bump. */
|
||||
const Ogre::String doorId =
|
||||
"d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6:Z:0:0:8";
|
||||
|
||||
/* F1 demo doors: the interior scene exits through the external
|
||||
* doorway Z:0:0:15 of the "interrior" grid (scene-switch door to
|
||||
* demo_scene_exterior.json), the exterior scene returns through the
|
||||
* external doorway Z:0:0:0 of its own exteriorOnly grid
|
||||
* (scene-switch door back to demo_scene_interior.json). For both
|
||||
* legs the switch must fire only when the leaf is fully open. */
|
||||
const Ogre::String exitDoorId =
|
||||
"d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6:Z:0:0:15";
|
||||
const Ogre::String returnDoorId =
|
||||
"77370b1d-e8ab-44a6-865b-e55c15b0fc78:Z:0:0:0";
|
||||
int exitOpenWait = 0;
|
||||
int returnOpenWait = 0;
|
||||
int arrivalDeadline = 0;
|
||||
|
||||
/* arrival_b in the exterior scene: 3 units outside (-Z of) the
|
||||
* exteriorOnly grid's return door Z:0:0:0 near the streaming-world
|
||||
* center. The expected Y is the terrain surface height there, read
|
||||
* back from the engine (streaming terrain: base heights come from
|
||||
* baseNoise, so no flat Y can be hardcoded). */
|
||||
Ogre::Vector3 expectedArrivalB() const
|
||||
{
|
||||
Ogre::Vector3 expected(4000.6f, 0.0f, 3995.9f);
|
||||
TerrainSystem *ts = TerrainSystem::getInstance();
|
||||
if (ts && ts->isActive())
|
||||
expected.y = ts->getHeightAt(ts->worldToRender(
|
||||
expected.x, 0.0, expected.z));
|
||||
return expected;
|
||||
}
|
||||
|
||||
SceneSwitchTestListener(EditorApp *a)
|
||||
: app(a)
|
||||
{
|
||||
}
|
||||
|
||||
bool entityExists(const char *name)
|
||||
{
|
||||
bool found = false;
|
||||
app->getWorld()->query<EntityNameComponent>().each(
|
||||
[&](flecs::entity, EntityNameComponent &n) {
|
||||
if (n.name == name)
|
||||
found = true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/* The F6 demo door entity (recreated on every scene load). */
|
||||
flecs::entity findDoor()
|
||||
{
|
||||
return findDoorById(doorId);
|
||||
}
|
||||
|
||||
flecs::entity findDoorById(const Ogre::String &id)
|
||||
{
|
||||
flecs::entity result = flecs::entity::null();
|
||||
app->getWorld()->query<DoorComponent>().each(
|
||||
[&](flecs::entity e, DoorComponent &d) {
|
||||
if (d.doorId == id)
|
||||
result = e;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Emulate the E-press on a scene-switch door. */
|
||||
bool requestDoorSwitch(const Ogre::String &id)
|
||||
{
|
||||
flecs::entity door = findDoorById(id);
|
||||
if (!door.is_alive())
|
||||
return false;
|
||||
DoorComponent &d = door.get_mut<DoorComponent>();
|
||||
if (!d.occluder) {
|
||||
failed = true;
|
||||
failReason = "F1 scene-switch door has no occluder";
|
||||
return true;
|
||||
}
|
||||
d.toggleRequested = true;
|
||||
d.sceneSwitchPending = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Waits for the queued scene switch of a scene-switch door;
|
||||
* returns true once the switch to expectPath is pending (the door
|
||||
* must be fully open by then). */
|
||||
bool waitDoorSwitch(const Ogre::String &id, const Ogre::String &label,
|
||||
const char *expectPath, int &openWait)
|
||||
{
|
||||
flecs::entity door = findDoorById(id);
|
||||
if (!door.is_alive())
|
||||
return false;
|
||||
const DoorComponent &d = door.get<DoorComponent>();
|
||||
bool fullyOpen = d.isOpen && d.currentAngle == d.openAngle;
|
||||
if (app->hasPendingSceneSwitch()) {
|
||||
if (!fullyOpen) {
|
||||
failed = true;
|
||||
failReason =
|
||||
"F1 scene switch fired before the door was fully open";
|
||||
return true;
|
||||
}
|
||||
if (app->getPendingSceneSwitchPath() != expectPath) {
|
||||
failed = true;
|
||||
failReason = "F1 wrong pending switch path";
|
||||
return true;
|
||||
}
|
||||
std::cout << "[test] F1 " << label
|
||||
<< " fully open, switch queued" << std::endl;
|
||||
return true;
|
||||
}
|
||||
if (fullyOpen) {
|
||||
/* Frame-listener ordering grace. */
|
||||
if (++openWait > 5) {
|
||||
failed = true;
|
||||
failReason = "F1 scene switch never fired";
|
||||
}
|
||||
} else {
|
||||
openWait = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool playerPos(Ogre::Vector3 &out) const
|
||||
{
|
||||
flecs::entity player = app->getPlayerCharacterEntity();
|
||||
if (!player.is_alive() || !player.has<TransformComponent>())
|
||||
return false;
|
||||
|
||||
const TransformComponent &t = player.get<TransformComponent>();
|
||||
out = t.node ? t.node->_getDerivedPosition() : t.position;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool checkPlayerNear(const Ogre::Vector3 &expected, float tol)
|
||||
{
|
||||
Ogre::Vector3 pos;
|
||||
if (!playerPos(pos))
|
||||
return false;
|
||||
if (pos.distance(expected) > tol) {
|
||||
failed = true;
|
||||
failReason = "player not at arrival point: (" +
|
||||
Ogre::StringConverter::toString(pos.x) +
|
||||
", " +
|
||||
Ogre::StringConverter::toString(pos.y) +
|
||||
", " +
|
||||
Ogre::StringConverter::toString(pos.z) +
|
||||
")";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The arrival markers face -Z (both scenes keep the same facing
|
||||
* convention; the exterior arrival_b at (4000.6, ~, 3995.9) faces
|
||||
* away from the return door); verify the camera took a position
|
||||
* behind the character (relative to charPos) looking at the walking
|
||||
* surface, and that the character's visual facing (local +Z) points
|
||||
* the same way. */
|
||||
bool checkCameraFacesCenter(const Ogre::Vector3 &charPos)
|
||||
{
|
||||
flecs::entity player = app->getPlayerCharacterEntity();
|
||||
if (!player.is_alive() || !player.has<TransformComponent>())
|
||||
return false;
|
||||
|
||||
const TransformComponent &t = player.get<TransformComponent>();
|
||||
Ogre::Quaternion q =
|
||||
t.node ? t.node->getOrientation() : t.rotation;
|
||||
Ogre::Vector3 charFacing = q * Ogre::Vector3::UNIT_Z;
|
||||
|
||||
EditorCamera *ec = app->getEditorCamera();
|
||||
Ogre::Camera *cam = ec ? ec->getCamera() : nullptr;
|
||||
Ogre::SceneNode *camNode =
|
||||
cam ? cam->getParentSceneNode() : nullptr;
|
||||
if (!camNode)
|
||||
return false;
|
||||
Ogre::Vector3 cp = camNode->_getDerivedPosition();
|
||||
Ogre::Vector3 vd = camNode->_getDerivedOrientation() *
|
||||
Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
if (vd.z > -0.9f || cp.z < charPos.z + 1.0f ||
|
||||
charFacing.z > -0.9f) {
|
||||
failed = true;
|
||||
failReason =
|
||||
"not facing the walking surface: camera pos (" +
|
||||
Ogre::StringConverter::toString(cp.x) + ", " +
|
||||
Ogre::StringConverter::toString(cp.y) + ", " +
|
||||
Ogre::StringConverter::toString(cp.z) +
|
||||
") view (" +
|
||||
Ogre::StringConverter::toString(vd.x) + ", " +
|
||||
Ogre::StringConverter::toString(vd.y) + ", " +
|
||||
Ogre::StringConverter::toString(vd.z) +
|
||||
") character facing (" +
|
||||
Ogre::StringConverter::toString(charFacing.x) +
|
||||
", " +
|
||||
Ogre::StringConverter::toString(charFacing.z) +
|
||||
")";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &) override
|
||||
{
|
||||
frame++;
|
||||
if (failed || phase == 3) {
|
||||
app->getRoot()->queueEndRendering();
|
||||
return true;
|
||||
}
|
||||
/* The swings are real-time (doorOpenSpeed deg/s) while a
|
||||
* headless frame is sub-millisecond, so the door legs need a
|
||||
* few thousand frames (~10 s wall clock at 1000+ fps); the
|
||||
* streaming terrain window and the grounding watchdog add a
|
||||
* few hundred frames on top. */
|
||||
if (frame > 20000) {
|
||||
failed = true;
|
||||
failReason = "timeout waiting for scene switch";
|
||||
app->getRoot()->queueEndRendering();
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (phase) {
|
||||
case 10: {
|
||||
/* F6: the demo door starts locked (lockedByDefault). */
|
||||
if (frame < 5)
|
||||
break;
|
||||
flecs::entity door = findDoor();
|
||||
if (!door.is_alive()) {
|
||||
if (frame > 60) {
|
||||
failed = true;
|
||||
failReason = "F6 demo door not found";
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!DoorSystem::isDoorLockedById(doorId)) {
|
||||
failed = true;
|
||||
failReason = "F6 demo door not locked at start";
|
||||
break;
|
||||
}
|
||||
/* Emulate the ActuatorSystem E-press on a locked door;
|
||||
* the inline scene script answers door_locked with
|
||||
* door_unlock_<doorId>. */
|
||||
EventBus::getInstance().send("door_locked", "door_id",
|
||||
doorId);
|
||||
phase = 11;
|
||||
break;
|
||||
}
|
||||
case 11: {
|
||||
/* F6: wait for the scene script to unlock the door
|
||||
* (the frame timeout covers a missing handler). */
|
||||
if (DoorSystem::isDoorLockedById(doorId))
|
||||
break;
|
||||
flecs::entity door = findDoor();
|
||||
if (!door.is_alive())
|
||||
break;
|
||||
door.get_mut<DoorComponent>().toggleRequested = true;
|
||||
std::cout << "[test] F6 door unlocked via scene script, "
|
||||
"opening"
|
||||
<< std::endl;
|
||||
phase = 12;
|
||||
break;
|
||||
}
|
||||
case 12: {
|
||||
/* F6: the swing completes and the open state lands in
|
||||
* the global store. */
|
||||
flecs::entity door = findDoor();
|
||||
if (!door.is_alive())
|
||||
break;
|
||||
const DoorComponent &d = door.get<DoorComponent>();
|
||||
if (!d.isOpen || d.currentAngle != d.openAngle)
|
||||
break;
|
||||
if (!GlobalStateStore::getInstance().getBool(
|
||||
"door." + doorId + ".isOpen")) {
|
||||
failed = true;
|
||||
failReason = "F6 door open state not persisted";
|
||||
break;
|
||||
}
|
||||
std::cout << "[test] F6 door open and persisted"
|
||||
<< std::endl;
|
||||
phase = 0;
|
||||
break;
|
||||
}
|
||||
case 0: {
|
||||
/* F1: interior -> exterior through the exit scene-switch
|
||||
* door. */
|
||||
if (frame < 5)
|
||||
break;
|
||||
if (!findDoorById(exitDoorId).is_alive()) {
|
||||
if (frame > 60) {
|
||||
failed = true;
|
||||
failReason = "F1 exit door not found";
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!requestDoorSwitch(exitDoorId))
|
||||
break;
|
||||
phase = 13;
|
||||
break;
|
||||
}
|
||||
case 13: {
|
||||
/* F1: the scene switch must fire only once the leaf
|
||||
* is fully open. */
|
||||
if (waitDoorSwitch(exitDoorId, "exit door",
|
||||
"demo_scene_exterior.json",
|
||||
exitOpenWait))
|
||||
phase = 1;
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
if (!entityExists("x1"))
|
||||
break;
|
||||
if (!arrivalDeadline)
|
||||
arrivalDeadline = frame + 600;
|
||||
/* The teleport clamps Y against whatever height source is
|
||||
* available at switch time (analytic baseNoise fallback
|
||||
* until the pages around the camera stream in), then the
|
||||
* grounding watchdog re-clamps against the real terrain
|
||||
* for ~120 frames; poll across frames until the player
|
||||
* settles at the arrival point instead of checking once. */
|
||||
Ogre::Vector3 expected = expectedArrivalB();
|
||||
Ogre::Vector3 pos;
|
||||
if (!playerPos(pos))
|
||||
break;
|
||||
if (pos.distance(expected) > 1.0f) {
|
||||
if (frame < arrivalDeadline)
|
||||
break;
|
||||
failed = true;
|
||||
failReason =
|
||||
"player not at arrival point: (" +
|
||||
Ogre::StringConverter::toString(pos.x) +
|
||||
", " +
|
||||
Ogre::StringConverter::toString(pos.y) +
|
||||
", " +
|
||||
Ogre::StringConverter::toString(pos.z) +
|
||||
"), expected (" +
|
||||
Ogre::StringConverter::toString(
|
||||
expected.x) +
|
||||
", " +
|
||||
Ogre::StringConverter::toString(
|
||||
expected.y) +
|
||||
", " +
|
||||
Ogre::StringConverter::toString(
|
||||
expected.z) +
|
||||
")";
|
||||
break;
|
||||
}
|
||||
if (!checkCameraFacesCenter(expected))
|
||||
break;
|
||||
if (failed)
|
||||
break;
|
||||
/* The island site must be dry: terrain height at the
|
||||
* grid XZ above the water surface (Y 6). Doubles as an
|
||||
* in-engine cross-check of the offline baseNoise mapping
|
||||
* used to place the exterior content. */
|
||||
{
|
||||
TerrainSystem *ts = TerrainSystem::getInstance();
|
||||
float h = ts && ts->isActive()
|
||||
? ts->getHeightAt(
|
||||
ts->worldToRender(4000.0,
|
||||
0.0,
|
||||
4000.0))
|
||||
: 0.0f;
|
||||
if (h <= 6.0f) {
|
||||
failed = true;
|
||||
failReason =
|
||||
"island site is not dry";
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::cout << "[test] arrived in the exterior scene at "
|
||||
"arrival_b, camera faces the walking surface"
|
||||
<< std::endl;
|
||||
/* F1: exterior -> interior through the exterior
|
||||
* scene's own scene-switch door (external doorway
|
||||
* Z:0:0:0 of its exteriorOnly grid). */
|
||||
if (!findDoorById(returnDoorId).is_alive()) {
|
||||
failed = true;
|
||||
failReason = "F1 return door not found";
|
||||
break;
|
||||
}
|
||||
if (!requestDoorSwitch(returnDoorId))
|
||||
break;
|
||||
phase = 14;
|
||||
break;
|
||||
}
|
||||
case 14:
|
||||
/* F1: same fully-open contract on the way back. */
|
||||
if (waitDoorSwitch(returnDoorId, "return door",
|
||||
"demo_scene_interior.json",
|
||||
returnOpenWait))
|
||||
phase = 2;
|
||||
break;
|
||||
case 2:
|
||||
if (!entityExists("interrior"))
|
||||
break;
|
||||
if (!checkPlayerNear(Ogre::Vector3(0.0f, 0.0f, 24.0f),
|
||||
1.0f))
|
||||
break;
|
||||
if (!checkCameraFacesCenter(Ogre::Vector3(0.0f, 0.0f,
|
||||
24.0f)))
|
||||
break;
|
||||
/* F6: the door rebuilt with the scene must be snapped
|
||||
* back to the persisted open + unlocked state. */
|
||||
{
|
||||
flecs::entity door = findDoor();
|
||||
if (!door.is_alive())
|
||||
break;
|
||||
const DoorComponent &d =
|
||||
door.get<DoorComponent>();
|
||||
if (!d.isOpen || d.currentAngle != d.openAngle ||
|
||||
DoorSystem::isDoorLockedById(doorId)) {
|
||||
failed = true;
|
||||
failReason =
|
||||
"F6 door state not restored after scene switch";
|
||||
break;
|
||||
}
|
||||
std::cout << "[test] F6 door state restored "
|
||||
"after scene switch"
|
||||
<< std::endl;
|
||||
}
|
||||
if (!failed) {
|
||||
std::cout << "[test] arrived back in the interior "
|
||||
"scene at arrival_a, camera faces the "
|
||||
"walking surface"
|
||||
<< std::endl;
|
||||
std::cout << "[test] PASS" << std::endl;
|
||||
}
|
||||
phase = 3;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* demo-interior-exterior-dynamics: same scene-switching demo as
|
||||
* demos/demo-scene-switching-extra, but the exterior scene is a real
|
||||
* outdoor environment instead of a flat colored floor. Two
|
||||
* hand-authored scenes (demo_scene_interior.json and
|
||||
* demo_scene_exterior.json), each with a character spawner ("s1",
|
||||
* character registry ID 2, same character setup as town8.json) and a
|
||||
* PlayerControllerComponent targeting it.
|
||||
*
|
||||
* The interior scene holds a flat colored floor plane with a static
|
||||
* physics collider plus an "interrior" entity with a CellGridComponent
|
||||
* in interiorOnly generation mode (a room with floor, ceiling, interior
|
||||
* walls, windows with opaque glass and an exit door) carrying its own
|
||||
* ProceduralMaterial + ProceduralTexture; the grid's texture rectangle
|
||||
* names reference the texture's named rects ("floor" / "ceiling")
|
||||
* without any Lot/District/Town parent.
|
||||
*
|
||||
* The exterior scene holds an exteriorOnly CellGridComponent (entity
|
||||
* "x1"/"w1" - just the building shell with opaque window glass, seen
|
||||
* from the outside) standing on a STREAMING terrain entity
|
||||
* (streamingEnabled, worldSizeUnits 10000 -> 5x5 pages of 2000 units,
|
||||
* base heights from TerrainComponent::baseNoise: OpenSimplex2 seed 55,
|
||||
* 3 octaves, frequency 0.0003, amplitude 15 - an archipelago of large
|
||||
* islands, ~34% land, the rest is ocean). The bounded streaming world
|
||||
* spans [-1000, 9000] on both axes around the terrain entity at the
|
||||
* origin (world (0,0) is half a page in from the corner, NOT the world
|
||||
* center), so all exterior content is placed near the world center on
|
||||
* an island with more than 500 units of dry land in every direction:
|
||||
* the grid at (4000, 10.0148, 4000) with its floor exactly on the
|
||||
* terrain surface, arrival_b at (4000.6, 9.9905, 3995.9) 3 units
|
||||
* outside (-Z of) the return door Z:0:0:0, spawner s1 at (4000,
|
||||
* 9.9039, 3975) - all Y values probed from the engine's
|
||||
* TerrainSystem::getHeightAt. The "sky" entity (skybox + sun
|
||||
* components) sits at (4000, 10, 4000): EditorSunSystem parents the
|
||||
* sun/moon sphere nodes to the root node and orbits them around the
|
||||
* camera (orbit radius 200), so they always stay inside the
|
||||
* camera-following skybox cube and line up with the shader sun/moon
|
||||
* discs, but the light node stays a child of the sky entity's
|
||||
* transform node, so the entity should still sit near the content for
|
||||
* sensible shadow placement (town8.json keeps its sky entity at the
|
||||
* origin next to its content). A "water"
|
||||
* entity (waterPlane + waterPhysics components, the buoyancy setup of
|
||||
* town8.json, planeSize 12000 centered on the content) completes the
|
||||
* scene; the water surface at Y 6 turns everything below into ocean
|
||||
* floor while the content island (terrain ~10 at the site) stays dry.
|
||||
* The shipped heightmaps/4242424300000001/heightmap.bin (generated by
|
||||
* gen_heightmap.py) is still staged - unused in streaming mode, it keeps
|
||||
* flipping streamingEnabled off a working fallback.
|
||||
*
|
||||
* F6 demo content: the interior grid's internal doorway Z:0:0:8 is
|
||||
* configured (doorConfigs in demo_scene_interior.json) as persistent +
|
||||
* lockable + locked by default, with a fixed gridUid so its global door
|
||||
* ID is stable, and the grid entity carries an inline scene script that
|
||||
* answers the "door_locked" event with "door_unlock_<doorId>" (the door
|
||||
* event contract; the door never opens by itself, the player presses E
|
||||
* again). The door's locked and open/closed state lives in the global
|
||||
* state store and survives the interior -> exterior -> interior scene
|
||||
* switches.
|
||||
*
|
||||
* F1 demo content: both scene transitions go through scene-switch doors.
|
||||
* The interior grid has the external exit doorway Z:0:0:15 configured as
|
||||
* a scene-switch door to demo_scene_exterior.json (target arrival_b);
|
||||
* the exterior grid sets
|
||||
* doorSceneSwitchPath/demo_scene_interior.json +
|
||||
* doorSceneSwitchTarget/arrival_a grid-wide, so its external doorway
|
||||
* Z:0:0:0 is the way back. E on such a door swings the leaf open first;
|
||||
* the scene switch fires only when the leaf reaches the open angle, and
|
||||
* a black occluder box behind the doorway hides the missing half of the
|
||||
* building while it swings. Both scenes carry their own player
|
||||
* controller, so each switch is a clean takeover (see
|
||||
* EditorApp::performSceneSwitch); travel back and forth is endless.
|
||||
*
|
||||
* The mouse is grabbed while Playing (built-in game-mode behaviour);
|
||||
* Escape toggles the pause menu, which frees the cursor.
|
||||
*
|
||||
* The binary is self-contained in its build directory: run it from there
|
||||
* (resources.cfg, resources/, characters/, lua-scripts/, prefabs/ and the
|
||||
* runtime config JSONs are staged next to it by the build; both scene
|
||||
* JSONs are symlinks into the source tree, so scene edits take effect
|
||||
* without a rebuild; the terrain heightmap is staged to
|
||||
* heightmaps/4242424300000001/ by configure_file, re-copied automatically
|
||||
* when the source file changes).
|
||||
*
|
||||
* Extra flags:
|
||||
* --test-switch headless-friendly end-to-end check: verifies the F6
|
||||
* locked door (locked at start, unlocked by the scene
|
||||
* script through the door event contract, open state
|
||||
* persisted and restored across the scene switches) and
|
||||
* both F1 scene-switch doors (interior -> exterior and
|
||||
* back each fire only when the leaf is fully open,
|
||||
* black occluder present), verifying the round-trip
|
||||
* arrival teleports (the exterior arrival check reads
|
||||
* the expected Y from TerrainSystem::getHeightAt and
|
||||
* polls while the streaming window loads and the
|
||||
* grounding watchdog re-clamps); exits 0 on PASS.
|
||||
*/
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
try {
|
||||
#ifdef EDITSCENE_HAS_EMBEDDED_PROJECT
|
||||
/* Release binary: project parameters are embedded at build
|
||||
* time from project.json (generated project.h); the project
|
||||
* root is the working directory the binary runs from. */
|
||||
DemoApp app(EDITSCENE_PROJECT_APP_NAME);
|
||||
{
|
||||
ProjectConfig cfg;
|
||||
cfg.rootDir =
|
||||
std::filesystem::current_path().string();
|
||||
cfg.appName = EDITSCENE_PROJECT_APP_NAME;
|
||||
cfg.startScene = EDITSCENE_PROJECT_START_SCENE;
|
||||
cfg.gameMode = EDITSCENE_PROJECT_GAME_MODE;
|
||||
cfg.loaded = true;
|
||||
app.setProjectConfig(cfg);
|
||||
}
|
||||
#else
|
||||
DemoApp app;
|
||||
#endif
|
||||
app.setGameMode(EditorApp::GameMode::Game);
|
||||
|
||||
bool headless = false;
|
||||
bool exitAfterFirstFrame = false;
|
||||
bool testSwitch = false;
|
||||
bool sceneArgGiven = false;
|
||||
Ogre::String shotFile;
|
||||
int shotFrame = -1;
|
||||
bool forcePos = false;
|
||||
Ogre::Vector3 forcePosVec = Ogre::Vector3::ZERO;
|
||||
float forcePosYaw = 0.0f;
|
||||
float forcePosPitch = 0.0f;
|
||||
#ifdef EDITSCENE_HAS_EMBEDDED_PROJECT
|
||||
Ogre::String sceneFile = EDITSCENE_PROJECT_START_SCENE;
|
||||
#else
|
||||
Ogre::String sceneFile = "demo_scene_interior.json";
|
||||
#endif
|
||||
for (int i = 1; i < argc; i++) {
|
||||
Ogre::String arg = argv[i];
|
||||
if (arg == "--headless") {
|
||||
headless = true;
|
||||
} else if (arg == "--exit-after-first-frame") {
|
||||
exitAfterFirstFrame = true;
|
||||
} else if (arg == "--test-switch") {
|
||||
testSwitch = true;
|
||||
} else if (arg == "--debug-buoyancy") {
|
||||
app.setDebugBuoyancy(true);
|
||||
} else if (arg == "--screenshot" && i + 2 < argc) {
|
||||
shotFrame = atoi(argv[i + 1]);
|
||||
shotFile = argv[i + 2];
|
||||
i += 2;
|
||||
} else if (arg == "--force-pos" && i + 4 < argc) {
|
||||
forcePos = true;
|
||||
forcePosVec = Ogre::Vector3(
|
||||
(float)atof(argv[i + 1]),
|
||||
(float)atof(argv[i + 2]),
|
||||
(float)atof(argv[i + 3]));
|
||||
forcePosYaw = (float)atof(argv[i + 4]);
|
||||
i += 4;
|
||||
/* Optional pitch (degrees); only consumed when
|
||||
* the next token is plainly numeric, so the
|
||||
* positional scene file is never eaten. */
|
||||
if (i + 1 < argc) {
|
||||
char *end = nullptr;
|
||||
float p = (float)strtod(argv[i + 1],
|
||||
&end);
|
||||
if (end && *end == '\0' &&
|
||||
end != argv[i + 1]) {
|
||||
forcePosPitch = p;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
} else if (arg.length() > 0 && arg[0] != '-') {
|
||||
sceneFile = arg;
|
||||
sceneArgGiven = true;
|
||||
}
|
||||
}
|
||||
app.setHeadless(headless);
|
||||
|
||||
app.initApp();
|
||||
|
||||
if (headless) {
|
||||
/* Headless mode never creates EditorUISystem, which
|
||||
* owns the CharacterRegistry singleton; the demo scene
|
||||
* has a character spawner, so provide a bare registry
|
||||
* to keep spawner resolution from asserting. The
|
||||
* character then falls back to an inline spawn without
|
||||
* a physics capsule (fine for a smoke run). */
|
||||
static CharacterRegistry s_characterRegistry;
|
||||
s_characterRegistry.setWorld(app.getWorld());
|
||||
s_characterRegistry.setSceneManager(app.getSceneManager());
|
||||
s_characterRegistry.initialize();
|
||||
}
|
||||
|
||||
/* Meshes + materials referenced by the scene entities. */
|
||||
createDemoResources();
|
||||
|
||||
/* With an embedded project, EditorApp::setup() skipped the
|
||||
* startup menu; the start scene defaults to the embedded
|
||||
* one and can still be overridden positionally. */
|
||||
(void)sceneArgGiven;
|
||||
std::cout << "[demo] starting new game with scene: "
|
||||
<< sceneFile << std::endl;
|
||||
app.startNewGame(sceneFile);
|
||||
std::cout << "[demo] controls: mouse = look, W/A/S/D = move, "
|
||||
"Shift = run, E = use door, Escape = pause menu"
|
||||
<< std::endl;
|
||||
|
||||
ExitAfterFirstFrameListener exitListener(app.getRoot());
|
||||
if (exitAfterFirstFrame && !testSwitch)
|
||||
app.getRoot()->addFrameListener(&exitListener);
|
||||
|
||||
SceneSwitchTestListener testListener(&app);
|
||||
if (testSwitch)
|
||||
app.getRoot()->addFrameListener(&testListener);
|
||||
|
||||
DebugCaptureListener captureListener(&app);
|
||||
if (forcePos) {
|
||||
captureListener.teleportEnabled = true;
|
||||
captureListener.teleportPos = forcePosVec;
|
||||
captureListener.teleportYawDeg = forcePosYaw;
|
||||
captureListener.teleportPitchDeg = forcePosPitch;
|
||||
}
|
||||
if (shotFrame > 0 && !shotFile.empty())
|
||||
captureListener.shotFrame = shotFrame;
|
||||
captureListener.shotFile = shotFile;
|
||||
if (forcePos || captureListener.shotFrame > 0)
|
||||
app.getRoot()->addFrameListener(&captureListener);
|
||||
|
||||
app.getRoot()->startRendering();
|
||||
|
||||
if (testSwitch) {
|
||||
if (testListener.failed) {
|
||||
std::cerr << "[test] FAIL: "
|
||||
<< testListener.failReason
|
||||
<< std::endl;
|
||||
app.clearScene();
|
||||
app.closeApp();
|
||||
return 1;
|
||||
}
|
||||
if (testListener.phase != 3) {
|
||||
std::cerr << "[test] FAIL: incomplete"
|
||||
<< std::endl;
|
||||
app.clearScene();
|
||||
app.closeApp();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Destroy scene entities while the systems are still alive:
|
||||
* CharacterSpawnerSystem registers an OnRemove observer on
|
||||
* CharacterSpawnerComponent that dereferences the system, and
|
||||
* the flecs world only dies after destroyEditorSystems() in
|
||||
* ~EditorApp, so letting the spawner entity live that long
|
||||
* would call back into a destroyed system. */
|
||||
app.clearScene();
|
||||
app.closeApp();
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "Error: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
+771
@@ -0,0 +1,771 @@
|
||||
{
|
||||
"actionDatabase": {
|
||||
"actions": [
|
||||
{
|
||||
"behaviorTree": {
|
||||
"children": [
|
||||
{
|
||||
"name": "luaHello",
|
||||
"params": "message=Welcome to the game!",
|
||||
"type": "luaTask"
|
||||
},
|
||||
{
|
||||
"name": "main/action",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "action/sitting-ground",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "dly",
|
||||
"params": "9.0",
|
||||
"type": "delay"
|
||||
},
|
||||
{
|
||||
"name": "main/locomotion",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "locomotion/idle",
|
||||
"type": "setAnimationState"
|
||||
}
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "lua_hello_action",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"behaviorTree": {
|
||||
"children": [
|
||||
{
|
||||
"name": "main/action",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "action/sitting-ground",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "dly",
|
||||
"params": "6.0",
|
||||
"type": "delay"
|
||||
},
|
||||
{
|
||||
"name": "main/locomotion",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "locomotion/idle",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "luaHello",
|
||||
"params": "message=\"hello, world!\"",
|
||||
"type": "luaTask"
|
||||
}
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "testAction",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"bitNames": [
|
||||
{
|
||||
"index": 1,
|
||||
"name": "hungry"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"name": "thirsty"
|
||||
}
|
||||
],
|
||||
"goals": []
|
||||
},
|
||||
"bookmarks": [],
|
||||
"entities": [
|
||||
{
|
||||
"children": [],
|
||||
"id": 488,
|
||||
"name": {
|
||||
"name": "arrival_b"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4000.6,
|
||||
"y": 9.9905,
|
||||
"z": 3995.9
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.0,
|
||||
"x": 0.0,
|
||||
"y": 1.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"cellGrid": {
|
||||
"ceilingRectName": "",
|
||||
"cellHeight": 4.0,
|
||||
"cellSize": 2.0,
|
||||
"cells": [
|
||||
{
|
||||
"flags": 37757955,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 2097667,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 41953283,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 20976643,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
},
|
||||
{
|
||||
"flags": 16781315,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
},
|
||||
{
|
||||
"flags": 25171971,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
}
|
||||
],
|
||||
"depth": 10,
|
||||
"doorActionName": "",
|
||||
"doorConfigs": {},
|
||||
"doorMeshName": "",
|
||||
"doorOpenAngle": 100.0,
|
||||
"doorOpenSpeed": 180.0,
|
||||
"doorRectName": "",
|
||||
"doorSceneSwitchPath": "demo_scene_interior.json",
|
||||
"doorSceneSwitchTarget": "arrival_a",
|
||||
"doorSwingReversed": true,
|
||||
"doorUseMeshMaterial": false,
|
||||
"doorsEnabled": true,
|
||||
"extDoorFrameRectName": "",
|
||||
"extWallRectName": "",
|
||||
"extWindowFrameRectName": "",
|
||||
"floorRectName": "",
|
||||
"friction": 0.5,
|
||||
"furnitureCells": [],
|
||||
"generationMode": "exteriorOnly",
|
||||
"generationScript": "",
|
||||
"glassColor": [
|
||||
0.4000000059604645,
|
||||
0.6000000238418579,
|
||||
0.800000011920929,
|
||||
0.3499999940395355
|
||||
],
|
||||
"glassMaterialName": "",
|
||||
"glassReflectivity": 0.800000011920929,
|
||||
"gridUid": "77370b1d-e8ab-44a6-865b-e55c15b0fc78",
|
||||
"height": 1,
|
||||
"intDoorFrameRectName": "",
|
||||
"intWallRectName": "",
|
||||
"intWindowFrameRectName": "",
|
||||
"roofSideRectName": "",
|
||||
"roofTopRectName": "",
|
||||
"width": 10
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"clearArea": {
|
||||
"clearCells": true,
|
||||
"clearFurniture": true,
|
||||
"clearRoofs": false,
|
||||
"clearRooms": false,
|
||||
"maxX": 10,
|
||||
"maxY": 1,
|
||||
"maxZ": 10,
|
||||
"minX": -10,
|
||||
"minY": 0,
|
||||
"minZ": -10
|
||||
},
|
||||
"id": 491,
|
||||
"name": {
|
||||
"name": "c1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 492,
|
||||
"name": {
|
||||
"name": "r1"
|
||||
},
|
||||
"room": {
|
||||
"connectedRoomIds": [],
|
||||
"createCeiling": true,
|
||||
"createFloor": true,
|
||||
"createInteriorWalls": true,
|
||||
"createWindows": true,
|
||||
"exits": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"fillRoomWithFurniture": false,
|
||||
"furnitureSeed": 42,
|
||||
"furnitureYOffset": 0.05000000074505806,
|
||||
"maxX": 2,
|
||||
"maxY": 1,
|
||||
"maxZ": 4,
|
||||
"minX": -1,
|
||||
"minY": 0,
|
||||
"minZ": 0,
|
||||
"persistentId": "room_1788689581735260047_499",
|
||||
"roomType": "",
|
||||
"tags": []
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 490,
|
||||
"name": {
|
||||
"name": "w1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 489,
|
||||
"name": {
|
||||
"name": "x1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4000.0,
|
||||
"y": 10.0148,
|
||||
"z": 4000.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 493,
|
||||
"light": {
|
||||
"castShadows": false,
|
||||
"constantAttenuation": 1.0,
|
||||
"diffuseColor": {
|
||||
"a": 1.0,
|
||||
"b": 1.0,
|
||||
"g": 1.0,
|
||||
"r": 1.0
|
||||
},
|
||||
"direction": {
|
||||
"x": 0.30000001192092896,
|
||||
"y": -1.0,
|
||||
"z": 0.20000000298023224
|
||||
},
|
||||
"intensity": 1.0,
|
||||
"lightType": "directional",
|
||||
"linearAttenuation": 0.0,
|
||||
"quadraticAttenuation": 0.0,
|
||||
"range": 100.0,
|
||||
"specularColor": {
|
||||
"a": 1.0,
|
||||
"b": 0.5,
|
||||
"g": 0.5,
|
||||
"r": 0.5
|
||||
},
|
||||
"spotlightFalloff": 1.0,
|
||||
"spotlightInnerAngle": 30.0,
|
||||
"spotlightOuterAngle": 45.0
|
||||
},
|
||||
"name": {
|
||||
"name": "demo_light"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4000.0,
|
||||
"y": 20.0,
|
||||
"z": 4000.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"characterSpawner": {
|
||||
"despawnDistance": 200.0,
|
||||
"registryId": 2,
|
||||
"spawnDistance": 100.0
|
||||
},
|
||||
"children": [],
|
||||
"id": 495,
|
||||
"name": {
|
||||
"name": "s1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4000.0,
|
||||
"y": 9.9039,
|
||||
"z": 3975.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 496,
|
||||
"name": {
|
||||
"name": "player"
|
||||
},
|
||||
"playerController": {
|
||||
"actuatorColor": [
|
||||
0.0,
|
||||
0.4000000059604645,
|
||||
1.0
|
||||
],
|
||||
"actuatorCooldown": 1.5,
|
||||
"actuatorDistance": 25.0,
|
||||
"actuatorLabelFontSize": 12.0,
|
||||
"cameraMode": 0,
|
||||
"distantCircleRadius": 8.0,
|
||||
"fpsBoneName": "Head",
|
||||
"idleState": "idle",
|
||||
"locomotionStateMachine": "locomotion",
|
||||
"mouseSensitivity": 0.20000000298023224,
|
||||
"nearCircleRadius": 14.0,
|
||||
"runState": "running",
|
||||
"swimFastState": "swimming-fast",
|
||||
"swimIdleState": "swim-idle",
|
||||
"swimState": "swimming",
|
||||
"targetCharacterName": "s1",
|
||||
"tpsDistance": 3.0,
|
||||
"tpsHeight": 2.0,
|
||||
"walkState": "walking"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 497,
|
||||
"name": {
|
||||
"name": "terrain"
|
||||
},
|
||||
"terrain": {
|
||||
"auxMaps": [],
|
||||
"baseNoise": {
|
||||
"amplitude": 15.0,
|
||||
"frequency": 0.0003,
|
||||
"lacunarity": 2.0,
|
||||
"octaves": 3,
|
||||
"persistence": 0.5,
|
||||
"seed": 55
|
||||
},
|
||||
"blendMapSize": 1024,
|
||||
"compositeMapDistance": 300.0,
|
||||
"detailNoise": {
|
||||
"amplitude": 10.0,
|
||||
"enabled": false,
|
||||
"frequency": 0.006,
|
||||
"lacunarity": 2.0,
|
||||
"octaves": 4,
|
||||
"persistence": 0.5,
|
||||
"seed": 26368
|
||||
},
|
||||
"enabled": true,
|
||||
"farClipDistance": 6000.0,
|
||||
"fogEnabled": true,
|
||||
"fogEnd": 5500.0,
|
||||
"fogStart": 2500.0,
|
||||
"heightmapFile": "heightmap.bin",
|
||||
"heightmapSize": 256,
|
||||
"layers": [
|
||||
{
|
||||
"diffuseTexture": "Ground23_col.jpg",
|
||||
"name": "Base",
|
||||
"normalTexture": "Ground23_normheight.dds",
|
||||
"worldSize": 100.0
|
||||
},
|
||||
{
|
||||
"diffuseTexture": "Ground37_diffspec.dds",
|
||||
"name": "Layer 1",
|
||||
"normalTexture": "Ground37_normheight.dds",
|
||||
"worldSize": 100.0
|
||||
}
|
||||
],
|
||||
"maxBatchSize": 65,
|
||||
"maxPixelError": 1.0,
|
||||
"minBatchSize": 17,
|
||||
"pageHoldRadius": 3,
|
||||
"pageLoadRadius": 2,
|
||||
"roadConfig": {
|
||||
"laneWidth": 3.0,
|
||||
"lanesPerDirection": 1,
|
||||
"prefabDespawnDistance": 250.0,
|
||||
"prefabSpawnDistance": 150.0,
|
||||
"roadLodDistance": 200.0,
|
||||
"roadMaterialName": "RoadMaterial",
|
||||
"roadMeshTemplate": "road_segment.mesh",
|
||||
"roadThickness": 0.3,
|
||||
"roadVisibilityDistance": 1000.0,
|
||||
"sidewalkEnabled": false,
|
||||
"sidewalkHeight": 0.15,
|
||||
"sidewalkMeshTemplate": "",
|
||||
"sidewalkThickness": 0.3,
|
||||
"sidewalkWidth": 1.5
|
||||
},
|
||||
"streamingEnabled": true,
|
||||
"terrainId": 4242424300000001,
|
||||
"terrainSize": 65,
|
||||
"worldSize": 2000.0,
|
||||
"worldSizeUnits": 10000.0
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 498,
|
||||
"name": {
|
||||
"name": "sky"
|
||||
},
|
||||
"skybox": {
|
||||
"cloudiness": 0.0,
|
||||
"dayBottomColor": [
|
||||
0.6,
|
||||
0.8,
|
||||
1.0
|
||||
],
|
||||
"dayTopColor": [
|
||||
0.2,
|
||||
0.5,
|
||||
1.0
|
||||
],
|
||||
"enabled": true,
|
||||
"moonSize": 0.03,
|
||||
"nightBottomColor": [
|
||||
0.05,
|
||||
0.05,
|
||||
0.15
|
||||
],
|
||||
"nightTopColor": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.05
|
||||
],
|
||||
"size": 443.0,
|
||||
"starsEnabled": false,
|
||||
"sunSize": 0.05,
|
||||
"sunriseColor": [
|
||||
1.0,
|
||||
0.5,
|
||||
0.2
|
||||
],
|
||||
"sunsetColor": [
|
||||
1.0,
|
||||
0.3,
|
||||
0.1
|
||||
]
|
||||
},
|
||||
"sun": {
|
||||
"ambientDay": [
|
||||
0.3,
|
||||
0.3,
|
||||
0.3
|
||||
],
|
||||
"ambientNight": [
|
||||
0.05,
|
||||
0.05,
|
||||
0.15
|
||||
],
|
||||
"ambientSunrise": [
|
||||
0.3,
|
||||
0.2,
|
||||
0.15
|
||||
],
|
||||
"ambientSunset": [
|
||||
0.25,
|
||||
0.15,
|
||||
0.1
|
||||
],
|
||||
"castShadows": true,
|
||||
"enabled": true,
|
||||
"intensity": 1.79,
|
||||
"moonColor": [
|
||||
0.3,
|
||||
0.3,
|
||||
0.5
|
||||
],
|
||||
"moonSphereSize": 3.4,
|
||||
"orbitTilt": 15.0,
|
||||
"showMoonSphere": true,
|
||||
"showSunSphere": true,
|
||||
"sunColor": [
|
||||
1.0,
|
||||
0.95,
|
||||
0.8
|
||||
],
|
||||
"sunSphereSize": 5.0,
|
||||
"timeOfDay": 6.226653575897217,
|
||||
"timeSpeed": 0.13
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4000.0,
|
||||
"y": 10.0,
|
||||
"z": 4000.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 499,
|
||||
"name": {
|
||||
"name": "water"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4000.0,
|
||||
"y": 0.0,
|
||||
"z": 4000.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"waterPhysics": {
|
||||
"defaultAngularDrag": 0.05,
|
||||
"defaultBuoyancy": 1.0,
|
||||
"defaultLinearDrag": 0.25,
|
||||
"defaultSubmergedThreshold": 0.1,
|
||||
"enabled": true,
|
||||
"gravity": 9.81,
|
||||
"waterDensity": 1000.0,
|
||||
"waterSurfaceY": 6.0
|
||||
},
|
||||
"waterPlane": {
|
||||
"autoUpdateFromWaterPhysics": true,
|
||||
"enabled": true,
|
||||
"planeSize": 12000.0,
|
||||
"reflectivity": 0.38,
|
||||
"renderTextureSize": 512,
|
||||
"tiling": 0.012,
|
||||
"waterColor": [
|
||||
0.0,
|
||||
0.3,
|
||||
0.5,
|
||||
0.8
|
||||
],
|
||||
"waterSurfaceY": 6.0,
|
||||
"waveScale": 0.031,
|
||||
"waveSpeed": 0.98
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": "1.0"
|
||||
}
|
||||
+1522
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate heightmap.bin for the demo-interior-exterior-dynamics terrain.
|
||||
|
||||
NOTE: the exterior scene's terrain (terrainId 4242424300000001) runs in
|
||||
streaming mode, where base heights come from the terrain's baseNoise
|
||||
(FastNoiseLite) and this file is NOT sampled — it is only staged next to
|
||||
the binary (heightmaps/4242424300000001/heightmap.bin) so the loader
|
||||
finds it. This script documents the historical non-streaming layout and
|
||||
is kept as the generator of record for that staged file.
|
||||
|
||||
Legacy description (non-streaming mode):
|
||||
|
||||
The exterior scene's terrain entity (terrainId 4242424300000001,
|
||||
non-streaming, worldSize 500 per page) loads its base heights from
|
||||
heightmaps/4242424300000001/heightmap.bin via
|
||||
TerrainSystem::loadSceneHeightmap. File format: uint32 resolution,
|
||||
then res*res little-endian float32 heights.
|
||||
|
||||
Height profile: a flat disc at y=0 around the origin (the playable area
|
||||
with the building shell and the scene-switch arrival point), sloping
|
||||
down to -5 in a ring so the water plane (waterSurfaceY -0.5) is visible
|
||||
as a lake around the centre.
|
||||
|
||||
Sampling math (see TerrainSystem::sampleBaseLocked / fillPageHeightData
|
||||
and the visualToPhysicalX/Z helpers): in non-streaming mode the system
|
||||
loads a fixed 3x3 page grid, so the heightmap covers the physical world
|
||||
rect [-worldSize, 2*worldSize] on both axes and texel (ix, iz) holds
|
||||
the height at physical coordinate (-ws + i * 3*ws / res). Physical
|
||||
space maps to visual (scene) space by a half-page shift on X and a
|
||||
per-page Z mirror; this script inverts that mapping so every texel gets
|
||||
the height of the *visual* position it will be rendered at, then
|
||||
verifies the round trip by re-sampling the generated grid the same
|
||||
bilinear way the engine does.
|
||||
"""
|
||||
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
|
||||
RES = 256 # must match "heightmapSize" in the scene JSON
|
||||
WORLD_SIZE = 500.0 # per-page world size ("worldSize" in the scene JSON)
|
||||
FLAT_RADIUS = 60.0 # flat-0 disc radius around the visual origin
|
||||
EDGE_RADIUS = 160.0 # slope reaches full depth here
|
||||
DEPTH = -5.0 # lake floor height (below waterSurfaceY -0.5)
|
||||
|
||||
|
||||
def profile(r):
|
||||
"""Visual height at distance r from the origin."""
|
||||
if r <= FLAT_RADIUS:
|
||||
return 0.0
|
||||
if r >= EDGE_RADIUS:
|
||||
return DEPTH
|
||||
t = (r - FLAT_RADIUS) / (EDGE_RADIUS - FLAT_RADIUS)
|
||||
t = t * t * (3.0 - 2.0 * t) # smoothstep
|
||||
return DEPTH * t
|
||||
|
||||
|
||||
def physical_to_visual(px, pz):
|
||||
"""Invert TerrainSystem::visualToPhysicalX/Z (terrain origin 0,0,0)."""
|
||||
vx = px - WORLD_SIZE * 0.5
|
||||
page = math.floor(pz / WORLD_SIZE)
|
||||
vz = 2.0 * page * WORLD_SIZE + WORLD_SIZE * 0.5 - pz
|
||||
return vx, vz
|
||||
|
||||
|
||||
def visual_to_physical(vx, vz):
|
||||
"""TerrainSystem::visualToPhysicalX/Z with a zero terrain origin."""
|
||||
px = vx + WORLD_SIZE * 0.5
|
||||
page = math.floor((vz + WORLD_SIZE * 0.5) / WORLD_SIZE)
|
||||
pz = 2.0 * page * WORLD_SIZE + WORLD_SIZE * 0.5 - vz
|
||||
return px, pz
|
||||
|
||||
|
||||
def main():
|
||||
out_path = sys.argv[1] if len(sys.argv) > 1 else "heightmap.bin"
|
||||
span = 3.0 * WORLD_SIZE
|
||||
origin = -WORLD_SIZE
|
||||
|
||||
grid = []
|
||||
for iz in range(RES):
|
||||
row = []
|
||||
pz = origin + iz * span / RES
|
||||
for ix in range(RES):
|
||||
px = origin + ix * span / RES
|
||||
vx, vz = physical_to_visual(px, pz)
|
||||
row.append(profile(math.hypot(vx, vz)))
|
||||
grid.append(row)
|
||||
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(struct.pack("<I", RES))
|
||||
for row in grid:
|
||||
f.write(struct.pack("<%df" % RES, *row))
|
||||
|
||||
# Verify: re-sample the grid exactly like TerrainSystem::sampleBaseLocked
|
||||
# (bilinear, physical coords) at a few visual positions.
|
||||
def sample(vx, vz):
|
||||
px, pz = visual_to_physical(vx, vz)
|
||||
fx = (px - origin) / span * RES
|
||||
fz = (pz - origin) / span * RES
|
||||
x0 = max(0, min(int(math.floor(fx)), RES - 1))
|
||||
z0 = max(0, min(int(math.floor(fz)), RES - 1))
|
||||
x1 = max(0, min(x0 + 1, RES - 1))
|
||||
z1 = max(0, min(z0 + 1, RES - 1))
|
||||
tx = fx - int(math.floor(fx))
|
||||
tz = fz - int(math.floor(fz))
|
||||
h00 = grid[z0][x0]
|
||||
h10 = grid[z0][x1]
|
||||
h01 = grid[z1][x0]
|
||||
h11 = grid[z1][x1]
|
||||
return ((1 - tx) * (1 - tz) * h00 + tx * (1 - tz) * h10 +
|
||||
(1 - tx) * tz * h01 + tx * tz * h11)
|
||||
|
||||
checks = [
|
||||
("arrival (0,24)", 0.0, 24.0),
|
||||
("origin (0,0)", 0.0, 0.0),
|
||||
("building corner (4,30)", 4.0, 30.0),
|
||||
("flat edge (55,0)", 55.0, 0.0),
|
||||
("mid slope (110,0)", 110.0, 0.0),
|
||||
("lake (200,0)", 200.0, 0.0),
|
||||
("lake (-200,200)", -200.0, 200.0),
|
||||
]
|
||||
ok = True
|
||||
for label, vx, vz in checks:
|
||||
h = sample(vx, vz)
|
||||
print("%-24s visual (%7.1f,%7.1f) -> height %8.3f"
|
||||
% (label, vx, vz, h))
|
||||
for vx, vz in [(0.0, 24.0), (0.0, 0.0), (4.0, 30.0), (55.0, 0.0)]:
|
||||
if abs(sample(vx, vz)) > 1e-4:
|
||||
print("ERROR: playable area not flat at", vx, vz)
|
||||
ok = False
|
||||
for vx, vz in [(200.0, 0.0), (-200.0, 200.0)]:
|
||||
if sample(vx, vz) > -4.9:
|
||||
print("ERROR: lake area not deep enough at", vx, vz)
|
||||
ok = False
|
||||
if not ok:
|
||||
sys.exit(1)
|
||||
print("heightmap written to %s (%dx%d), all checks passed"
|
||||
% (out_path, RES, RES))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
@@ -0,0 +1,131 @@
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Entity Hierarchy]
|
||||
Pos=0,0
|
||||
Size=300,1043
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Property Editor]
|
||||
Pos=1570,0
|
||||
Size=350,1043
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Load Scene]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Save Scene]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][StartupMenu]
|
||||
Pos=0,0
|
||||
Size=1920,1043
|
||||
|
||||
[Window][Prefab Browser]
|
||||
Pos=300,300
|
||||
Size=250,400
|
||||
|
||||
[Window][Create Prefab]
|
||||
Pos=655,364
|
||||
Size=305,77
|
||||
|
||||
[Window][3D Cursor]
|
||||
Pos=300,100
|
||||
Size=280,350
|
||||
|
||||
[Window][Mesh Browser]
|
||||
Pos=533,240
|
||||
Size=416,406
|
||||
|
||||
[Window][Action Database (Singleton)]
|
||||
Pos=326,33
|
||||
Size=404,694
|
||||
|
||||
[Window][Dialogue Settings]
|
||||
Pos=300,100
|
||||
Size=400,500
|
||||
|
||||
[Window][DialogueBox]
|
||||
Pos=0,681
|
||||
Size=1682,227
|
||||
|
||||
[Window][Character Class Database]
|
||||
Pos=303,109
|
||||
Size=600,600
|
||||
|
||||
[Window][Character Registry]
|
||||
Pos=476,258
|
||||
Size=903,429
|
||||
|
||||
[Window][Delete Prefab]
|
||||
Pos=797,467
|
||||
Size=325,109
|
||||
|
||||
[Window][PauseMenu]
|
||||
Pos=0,0
|
||||
Size=2490,1536
|
||||
LastUsed=20260905
|
||||
|
||||
[Window][Item Registry]
|
||||
Pos=60,60
|
||||
Size=600,500
|
||||
|
||||
[Window][Inventory Dialog Config]
|
||||
Pos=300,100
|
||||
Size=350,200
|
||||
|
||||
[Window][Character Sheet]
|
||||
Pos=0,0
|
||||
Size=1920,1043
|
||||
|
||||
[Window][Load Game]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
|
||||
[Window][Save Game]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
|
||||
[Window][Animation Tree Registry]
|
||||
Pos=60,60
|
||||
Size=900,600
|
||||
|
||||
[Window][Confirm Blend Map Resolution Change]
|
||||
Pos=815,477
|
||||
Size=290,105
|
||||
|
||||
[Window][Confirm Heightmap Resolution Change]
|
||||
Pos=815,477
|
||||
Size=290,105
|
||||
|
||||
[Window][Road Graph Invalid]
|
||||
Pos=892,217
|
||||
Size=136,127
|
||||
LastUsed=20260731
|
||||
|
||||
[Window][Wedge Geometry Debug]
|
||||
Pos=10,10
|
||||
Size=420,520
|
||||
LastUsed=20260814
|
||||
|
||||
[Window][Road Graph Valid]
|
||||
Pos=882,486
|
||||
Size=156,71
|
||||
LastUsed=20260821
|
||||
|
||||
[Window][Switch Scene]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
LastUsed=20260906
|
||||
|
||||
[Window][Open Project]
|
||||
Pos=300,100
|
||||
Size=639,377
|
||||
LastUsed=20260908
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"fontPath": "Jupiteroid-Bold.ttf",
|
||||
"fontSize": 16.0
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
/* Generated by CMake from project.json - do not edit.
|
||||
* Embeds the project parameters into the release binary so it runs its
|
||||
* project with no --project flag (see GameFeatures202609.md, F8). */
|
||||
#define EDITSCENE_PROJECT_APP_NAME "@PROJECT_APP_NAME@"
|
||||
#define EDITSCENE_PROJECT_START_SCENE "@PROJECT_START_SCENE@"
|
||||
#define EDITSCENE_PROJECT_GAME_MODE @PROJECT_GAME_MODE_VALUE@
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"appName": "demo-interior-exterior-dynamics",
|
||||
"startScene": "demo_scene_interior.json",
|
||||
"gameMode": true
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Stage everything demoInteriorExteriorDynamics needs into its own directory so it
|
||||
# runs standalone from
|
||||
# <build>/src/features/editScene/demos/demo-interior-exterior-dynamics.
|
||||
#
|
||||
# Expected -D arguments: DEMO_DIR, EDITSCENE_BIN, EDITSCENE_SRC, SRC_DIR.
|
||||
#
|
||||
# Small demo-owned files are COPIED, except the demo scenes, which are
|
||||
# SYMLINKED to the source tree so scene edits are visible to the demo
|
||||
# without a rebuild (the demo never saves scenes, so nothing writes through
|
||||
# the links); the big pre-staged runtime directories
|
||||
# are SYMLINKED from the editScene binary directory (Ogre FileSystem
|
||||
# locations follow symlinks, and copying would duplicate hundreds of MB on
|
||||
# every build). The symlink targets are populated by the editSceneEditor
|
||||
# staging, so editSceneEditor must have been built (and run its POST_BUILD
|
||||
# staging) at least once.
|
||||
|
||||
file(COPY "${EDITSCENE_SRC}/resources.cfg" DESTINATION "${DEMO_DIR}")
|
||||
file(COPY "${SRC_DIR}/project.json" DESTINATION "${DEMO_DIR}")
|
||||
|
||||
foreach(f demo_scene_interior.json demo_scene_exterior.json)
|
||||
set(link "${DEMO_DIR}/${f}")
|
||||
if(EXISTS "${link}" OR IS_SYMLINK "${link}")
|
||||
file(REMOVE "${link}")
|
||||
endif()
|
||||
file(CREATE_LINK "${SRC_DIR}/${f}" "${link}" SYMBOLIC)
|
||||
endforeach()
|
||||
|
||||
# Terrain heightmap: staged at configure time by a configure_file COPYONLY
|
||||
# in CMakeLists.txt (to heightmaps/4242424300000001/), which re-copies it
|
||||
# automatically when the source file changes. Not symlinked: the fixup
|
||||
# layer and save path write next to it.
|
||||
|
||||
# Character prefab for the spawner's registry entry (registryId 2). It is
|
||||
# written by a previous editor/game run (CharacterRegistry::savePrefab...),
|
||||
# not part of the source tree, so copy it from the editScene binary
|
||||
# directory when present.
|
||||
file(MAKE_DIRECTORY "${DEMO_DIR}/prefabs")
|
||||
foreach(f char_2.json)
|
||||
if(EXISTS "${EDITSCENE_BIN}/prefabs/${f}")
|
||||
file(COPY "${EDITSCENE_BIN}/prefabs/${f}"
|
||||
DESTINATION "${DEMO_DIR}/prefabs")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(dir resources characters lua-scripts)
|
||||
set(link "${DEMO_DIR}/${dir}")
|
||||
if(EXISTS "${link}" OR IS_SYMLINK "${link}")
|
||||
file(REMOVE_RECURSE "${link}")
|
||||
endif()
|
||||
file(CREATE_LINK "${EDITSCENE_BIN}/${dir}" "${link}" SYMBOLIC)
|
||||
endforeach()
|
||||
|
||||
# Runtime config JSONs loaded at startup relative to the CWD (game mode
|
||||
# reads startup_menu.json, the registries read the rest — the demo needs
|
||||
# character_registry.json for the spawner's registryId 2 and
|
||||
# animation_tree.json for the character's "male1_6" animation tree). They
|
||||
# only exist after the editor/game has run once; copy them when present so
|
||||
# the demo behaves the same as when run from the editor binary directory.
|
||||
foreach(f startup_menu.json character_registry.json character_class.json
|
||||
items.json item_state.json inventory_config.json
|
||||
animation_tree.json)
|
||||
if(EXISTS "${EDITSCENE_BIN}/${f}")
|
||||
file(COPY "${EDITSCENE_BIN}/${f}" DESTINATION "${DEMO_DIR}")
|
||||
endif()
|
||||
endforeach()
|
||||
@@ -3,7 +3,15 @@ vertex_program SkyboxVP glsl glsles glslang hlsl
|
||||
source skybox.vert
|
||||
default_params
|
||||
{
|
||||
param_named_auto worldViewProj worldviewproj_matrix
|
||||
// All vertex parameters are set manually every frame by
|
||||
// EditorSkyboxSystem::updateShaderParams (auto constants did
|
||||
// not reach the vertex program reliably, and manual Matrix4
|
||||
// params did not land in the uniform block on all render
|
||||
// systems, so only vector types are used).
|
||||
param_named camForward float3 0.0 0.0 -1.0
|
||||
param_named camRight float3 1.0 0.0 0.0
|
||||
param_named camUp float3 0.0 1.0 0.0
|
||||
param_named tanHalfFov float2 1.0 1.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ OGRE_NATIVE_GLSL_VERSION_DIRECTIVE
|
||||
#include <OgreUnifiedShader.h>
|
||||
|
||||
OGRE_UNIFORMS(
|
||||
uniform mat4 worldViewProj;
|
||||
uniform vec3 camForward;
|
||||
uniform vec3 camRight;
|
||||
uniform vec3 camUp;
|
||||
uniform vec2 tanHalfFov;
|
||||
)
|
||||
|
||||
OUT(vec3 vWorldPos, TEXCOORD0)
|
||||
@@ -11,6 +14,13 @@ MAIN_PARAMETERS
|
||||
IN(vec4 vertex, POSITION)
|
||||
MAIN_DECLARATION
|
||||
{
|
||||
gl_Position = worldViewProj * vertex;
|
||||
vWorldPos = vertex.xyz;
|
||||
// The sky mesh is a fullscreen triangle in NDC (see
|
||||
// EditorSkyboxSystem::createSkyTriangle). Reconstruct the view
|
||||
// ray from the camera basis instead of interpolating cube vertex
|
||||
// positions: cube faces behind the camera rasterised with a
|
||||
// degenerate (zero) interpolated position, which painted
|
||||
// horizon-glow "circles" on the sky.
|
||||
vWorldPos = camForward + camRight * (vertex.x * tanHalfFov.x) +
|
||||
camUp * (vertex.y * tanHalfFov.y);
|
||||
gl_Position = vec4(vertex.xy, 0.999999, 1.0);
|
||||
}
|
||||
|
||||
@@ -516,6 +516,13 @@ void CharacterSystem::update(float deltaTime)
|
||||
}
|
||||
state.character->SetLinearVelocity(finalVel);
|
||||
|
||||
/* Publish the ground contact state so gameplay code
|
||||
* (e.g. swim vs wade animation selection) can tell
|
||||
* floating apart from standing on the bottom. The
|
||||
* ground state itself is refreshed by PostSimulation()
|
||||
* after each physics step, so this lags one frame. */
|
||||
cc.isSupported = state.character->IsSupported();
|
||||
|
||||
|
||||
/* Floor detection: raycast downward to find ground */
|
||||
if (cc.useGravity && !cc.hasFloor) {
|
||||
|
||||
@@ -36,8 +36,10 @@ void EditorSkyboxSystem::update(Ogre::Camera *camera)
|
||||
if (!skybox.sceneNode || !camera)
|
||||
return;
|
||||
|
||||
// Follow camera position
|
||||
skybox.sceneNode->setPosition(camera->getDerivedPosition());
|
||||
// The sky mesh is a fullscreen NDC triangle, so there is no
|
||||
// cube to keep glued to the camera; the camera argument only
|
||||
// gates the shader parameter update above.
|
||||
skybox.sceneNode->setVisible(true);
|
||||
|
||||
// Get sun data from same entity or use defaults
|
||||
float sunElev = 1.0f;
|
||||
@@ -57,7 +59,7 @@ void EditorSkyboxSystem::update(Ogre::Camera *camera)
|
||||
}
|
||||
}
|
||||
|
||||
updateShaderParams(skybox, sunElev, sunDir);
|
||||
updateShaderParams(skybox, sunElev, sunDir, camera);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,15 +71,16 @@ void EditorSkyboxSystem::rebuildSkybox(flecs::entity entity,
|
||||
Ogre::String baseName = "Skybox_" +
|
||||
Ogre::StringConverter::toString(entity.id());
|
||||
|
||||
// Create scene node at origin (will follow camera)
|
||||
// Create scene node at origin (the mesh is a fullscreen NDC
|
||||
// triangle, so the node transform does not affect rendering)
|
||||
skybox.sceneNode =
|
||||
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
|
||||
baseName + "Node");
|
||||
|
||||
// Create manual object cube
|
||||
// Create manual object fullscreen triangle
|
||||
skybox.manualObject =
|
||||
m_sceneMgr->createManualObject(baseName + "Mesh");
|
||||
createCube(skybox.manualObject, skybox.size);
|
||||
createSkyTriangle(skybox.manualObject);
|
||||
|
||||
// Get material instance
|
||||
Ogre::MaterialPtr material =
|
||||
@@ -97,8 +100,7 @@ void EditorSkyboxSystem::rebuildSkybox(flecs::entity entity,
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"SkyboxSystem: Created skybox for entity " +
|
||||
Ogre::StringConverter::toString(entity.id()) +
|
||||
", size=" + Ogre::StringConverter::toString(skybox.size));
|
||||
Ogre::StringConverter::toString(entity.id()));
|
||||
}
|
||||
|
||||
void EditorSkyboxSystem::cleanupSkybox(SkyboxComponent &skybox)
|
||||
@@ -116,67 +118,34 @@ void EditorSkyboxSystem::cleanupSkybox(SkyboxComponent &skybox)
|
||||
}
|
||||
}
|
||||
|
||||
void EditorSkyboxSystem::createCube(Ogre::ManualObject *obj, float size)
|
||||
void EditorSkyboxSystem::createSkyTriangle(Ogre::ManualObject *obj)
|
||||
{
|
||||
float h = size * 0.5f;
|
||||
|
||||
// Fullscreen triangle in clip space (NDC). The vertex shader
|
||||
// reconstructs the per-pixel view ray from the inverse
|
||||
// view-projection matrix, so no sky geometry ever sits behind the
|
||||
// camera. (The old camera-following cube had faces behind the
|
||||
// camera; those rasterised with a degenerate interpolated position
|
||||
// and painted horizon-glow "circles" around the view direction.)
|
||||
obj->begin("Skybox/Dynamic",
|
||||
Ogre::RenderOperation::OT_TRIANGLE_LIST);
|
||||
|
||||
// Front face (+Z)
|
||||
obj->position(-h, -h, h);
|
||||
obj->position(h, -h, h);
|
||||
obj->position(h, h, h);
|
||||
obj->position(-h, h, h);
|
||||
obj->triangle(0, 2, 1);
|
||||
obj->triangle(0, 3, 2);
|
||||
|
||||
// Back face (-Z)
|
||||
obj->position(h, -h, -h);
|
||||
obj->position(-h, -h, -h);
|
||||
obj->position(-h, h, -h);
|
||||
obj->position(h, h, -h);
|
||||
obj->triangle(4, 6, 5);
|
||||
obj->triangle(4, 7, 6);
|
||||
|
||||
// Left face (-X)
|
||||
obj->position(-h, -h, -h);
|
||||
obj->position(-h, -h, h);
|
||||
obj->position(-h, h, h);
|
||||
obj->position(-h, h, -h);
|
||||
obj->triangle(8, 10, 9);
|
||||
obj->triangle(8, 11, 10);
|
||||
|
||||
// Right face (+X)
|
||||
obj->position(h, -h, h);
|
||||
obj->position(h, -h, -h);
|
||||
obj->position(h, h, -h);
|
||||
obj->position(h, h, h);
|
||||
obj->triangle(12, 14, 13);
|
||||
obj->triangle(12, 15, 14);
|
||||
|
||||
// Top face (+Y)
|
||||
obj->position(-h, h, h);
|
||||
obj->position(h, h, h);
|
||||
obj->position(h, h, -h);
|
||||
obj->position(-h, h, -h);
|
||||
obj->triangle(16, 18, 17);
|
||||
obj->triangle(16, 19, 18);
|
||||
|
||||
// Bottom face (-Y)
|
||||
obj->position(-h, -h, -h);
|
||||
obj->position(h, -h, -h);
|
||||
obj->position(h, -h, h);
|
||||
obj->position(-h, -h, h);
|
||||
obj->triangle(20, 22, 21);
|
||||
obj->triangle(20, 23, 22);
|
||||
obj->position(-1.0f, -1.0f, 0.0f);
|
||||
obj->position(3.0f, -1.0f, 0.0f);
|
||||
obj->position(-1.0f, 3.0f, 0.0f);
|
||||
obj->triangle(0, 1, 2);
|
||||
|
||||
obj->end();
|
||||
|
||||
// The vertices are NDC, so the computed AABB is a tiny box at the
|
||||
// world origin and the sky would be frustum-culled from anywhere
|
||||
// else; give it an infinite bounds instead.
|
||||
obj->setBoundingBox(Ogre::AxisAlignedBox::BOX_INFINITE);
|
||||
}
|
||||
|
||||
void EditorSkyboxSystem::updateShaderParams(SkyboxComponent &skybox,
|
||||
float sunElev,
|
||||
const Ogre::Vector3 &sunDir)
|
||||
const Ogre::Vector3 &sunDir,
|
||||
Ogre::Camera *camera)
|
||||
{
|
||||
if (!skybox.manualObject)
|
||||
return;
|
||||
@@ -190,6 +159,32 @@ void EditorSkyboxSystem::updateShaderParams(SkyboxComponent &skybox,
|
||||
return;
|
||||
|
||||
Ogre::Pass *pass = material->getTechnique(0)->getPass(0);
|
||||
|
||||
// Vertex parameters. Set them manually rather than relying on
|
||||
// param_named_auto: the auto constants did not reach the vertex
|
||||
// program on all render systems (the sky rendered with a zero
|
||||
// direction). Vector types only - a manual Matrix4 param did not
|
||||
// land in the uniform block either. The values are for the main
|
||||
// camera; the water reflection/refraction passes reuse them, which
|
||||
// is close enough for a sky.
|
||||
if (camera) {
|
||||
Ogre::GpuProgramParametersSharedPtr vpParams =
|
||||
pass->getVertexProgramParameters();
|
||||
if (vpParams) {
|
||||
Ogre::Radian fovY = camera->getFOVy();
|
||||
float tanY = Ogre::Math::Tan(fovY * 0.5f);
|
||||
float tanX = tanY * camera->getAspectRatio();
|
||||
vpParams->setNamedConstant(
|
||||
"camForward", camera->getDerivedDirection());
|
||||
vpParams->setNamedConstant(
|
||||
"camRight", camera->getDerivedRight());
|
||||
vpParams->setNamedConstant(
|
||||
"camUp", camera->getDerivedUp());
|
||||
vpParams->setNamedConstant(
|
||||
"tanHalfFov", Ogre::Vector2(tanX, tanY));
|
||||
}
|
||||
}
|
||||
|
||||
Ogre::GpuProgramParametersSharedPtr fpParams =
|
||||
pass->getFragmentProgramParameters();
|
||||
if (!fpParams)
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Skybox system - creates and updates a procedural skybox cube
|
||||
* that follows the camera and renders dynamic sky colors.
|
||||
* Skybox system - creates and updates a procedural sky rendered as a
|
||||
* fullscreen triangle and shaded from the per-pixel view ray, so the
|
||||
* sky is correct from any camera (including the water reflection and
|
||||
* refraction cameras).
|
||||
*/
|
||||
class EditorSkyboxSystem {
|
||||
public:
|
||||
@@ -17,13 +19,18 @@ public:
|
||||
// Update skybox position and shader parameters
|
||||
void update(Ogre::Camera *camera);
|
||||
|
||||
// Destroy the manual object and its scene node (parented to the
|
||||
// root node, so it survives entity deletion unless cleaned up).
|
||||
// Public so EditorUISystem::deleteEntity can release them.
|
||||
void cleanupSkybox(struct SkyboxComponent &skybox);
|
||||
|
||||
private:
|
||||
void rebuildSkybox(flecs::entity entity,
|
||||
struct SkyboxComponent &skybox);
|
||||
void cleanupSkybox(SkyboxComponent &skybox);
|
||||
void createCube(Ogre::ManualObject *obj, float size);
|
||||
void createSkyTriangle(Ogre::ManualObject *obj);
|
||||
void updateShaderParams(SkyboxComponent &skybox,
|
||||
float sunElev, const Ogre::Vector3 &sunDir);
|
||||
float sunElev, const Ogre::Vector3 &sunDir,
|
||||
Ogre::Camera *camera);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
|
||||
@@ -15,7 +15,7 @@ EditorSunSystem::EditorSunSystem(flecs::world &world,
|
||||
|
||||
EditorSunSystem::~EditorSunSystem() = default;
|
||||
|
||||
void EditorSunSystem::update(float deltaTime)
|
||||
void EditorSunSystem::update(float deltaTime, Ogre::Camera *camera)
|
||||
{
|
||||
m_query.each([&](flecs::entity entity, SunComponent &sun,
|
||||
TransformComponent &transform) {
|
||||
@@ -105,20 +105,34 @@ void EditorSunSystem::update(float deltaTime)
|
||||
}
|
||||
m_sceneMgr->setAmbientLight(ambient);
|
||||
|
||||
// Update sphere positions
|
||||
// Update sphere positions. The sphere nodes are parented
|
||||
// to the root node; orbit them around the camera (render
|
||||
// space) when one is available so they always stay inside
|
||||
// the camera-following skybox and line up with the shader
|
||||
// sun/moon discs regardless of where the entity sits.
|
||||
// Without a camera, orbit the entity transform node.
|
||||
float orbitRadius = 200.0f;
|
||||
Ogre::Vector3 sunPos = -lightDir * orbitRadius;
|
||||
Ogre::Vector3 moonPos = lightDir * orbitRadius;
|
||||
Ogre::Vector3 orbitCenter = Ogre::Vector3::ZERO;
|
||||
if (camera) {
|
||||
orbitCenter = camera->getDerivedPosition();
|
||||
} else if (transform.node) {
|
||||
orbitCenter = transform.node->_getDerivedPosition();
|
||||
}
|
||||
Ogre::Vector3 sunPos = orbitCenter - lightDir * orbitRadius;
|
||||
Ogre::Vector3 moonPos = orbitCenter + lightDir * orbitRadius;
|
||||
|
||||
if (sun.sunSphereNode) {
|
||||
sun.sunSphereNode->setPosition(sunPos);
|
||||
// Hide the sphere once it dips below the horizon
|
||||
// (sphere angular radius is ~0.025 at this orbit)
|
||||
// so it cannot shine through water or terrain.
|
||||
sun.sunSphereNode->setVisible(
|
||||
sun.showSunSphere && sunElev > -0.2f);
|
||||
sun.showSunSphere && sunElev > -0.03f);
|
||||
}
|
||||
if (sun.moonSphereNode) {
|
||||
sun.moonSphereNode->setPosition(moonPos);
|
||||
sun.moonSphereNode->setVisible(
|
||||
sun.showMoonSphere && sunElev < 0.2f);
|
||||
sun.showMoonSphere && sunElev < 0.03f);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -151,7 +165,8 @@ void EditorSunSystem::rebuildSun(flecs::entity entity, SunComponent &sun,
|
||||
lightName + "Node");
|
||||
sun.lightNode->attachObject(sun.light);
|
||||
|
||||
// Create sun sphere
|
||||
// Create sun sphere (root-node child: the spheres orbit the
|
||||
// camera in update(), not the entity)
|
||||
if (sun.showSunSphere) {
|
||||
Ogre::String sunName = "SunSphere_" +
|
||||
Ogre::StringConverter::toString(
|
||||
@@ -160,7 +175,8 @@ void EditorSunSystem::rebuildSun(flecs::entity entity, SunComponent &sun,
|
||||
createSphere(sun.sunSphere, sun.sunSphereSize,
|
||||
Ogre::ColourValue(1.0f, 0.95f, 0.8f));
|
||||
sun.sunSphereNode =
|
||||
transform.node->createChildSceneNode(sunName + "Node");
|
||||
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
|
||||
sunName + "Node");
|
||||
sun.sunSphereNode->attachObject(sun.sunSphere);
|
||||
}
|
||||
|
||||
@@ -173,7 +189,8 @@ void EditorSunSystem::rebuildSun(flecs::entity entity, SunComponent &sun,
|
||||
createSphere(sun.moonSphere, sun.moonSphereSize,
|
||||
Ogre::ColourValue(0.8f, 0.8f, 0.9f));
|
||||
sun.moonSphereNode =
|
||||
transform.node->createChildSceneNode(moonName + "Node");
|
||||
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
|
||||
moonName + "Node");
|
||||
sun.moonSphereNode->attachObject(sun.moonSphere);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,21 @@ public:
|
||||
EditorSunSystem(flecs::world &world, Ogre::SceneManager *sceneMgr);
|
||||
~EditorSunSystem();
|
||||
|
||||
// Update sun position, light, and colors
|
||||
void update(float deltaTime);
|
||||
// Update sun position, light, and colors. When camera is not
|
||||
// null the sun/moon spheres orbit the camera (render space) so
|
||||
// they always stay inside the camera-following skybox and line
|
||||
// up with the shader sun/moon discs; otherwise they orbit the
|
||||
// entity transform node as before.
|
||||
void update(float deltaTime, Ogre::Camera *camera);
|
||||
|
||||
// Destroy the light, sphere meshes and their scene nodes.
|
||||
// Public so EditorUISystem::deleteEntity can release them before
|
||||
// destroying the entity's transform node.
|
||||
void cleanupSun(struct SunComponent &sun);
|
||||
|
||||
private:
|
||||
void rebuildSun(flecs::entity entity, struct SunComponent &sun,
|
||||
struct TransformComponent &transform);
|
||||
void cleanupSun(SunComponent &sun);
|
||||
void createSphere(Ogre::ManualObject *obj, float radius,
|
||||
const Ogre::ColourValue &color);
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "PhysicsSystem.hpp"
|
||||
#include "BuoyancySystem.hpp"
|
||||
#include "EditorSunSystem.hpp"
|
||||
#include "EditorSkyboxSystem.hpp"
|
||||
#include "NavMeshSystem.hpp"
|
||||
#include "NormalDebugSystem.hpp"
|
||||
#include "SceneScriptSystem.hpp"
|
||||
@@ -1249,6 +1251,20 @@ void EditorUISystem::deleteEntity(flecs::entity entity)
|
||||
}
|
||||
}
|
||||
|
||||
/* Clean up the sun/skybox runtime objects before the transform
|
||||
* node: the sun's directional light would leak detached (it keeps
|
||||
* illuminating the scene), and the skybox scene node is parented
|
||||
* to the root node, so it would survive the entity entirely -
|
||||
* both showed up as stale extra suns/skies after scene switches. */
|
||||
if (entity.has<SunComponent>() && m_sunSystem) {
|
||||
auto &sun = entity.get_mut<SunComponent>();
|
||||
m_sunSystem->cleanupSun(sun);
|
||||
}
|
||||
if (entity.has<SkyboxComponent>() && m_skyboxSystem) {
|
||||
auto &skybox = entity.get_mut<SkyboxComponent>();
|
||||
m_skyboxSystem->cleanupSkybox(skybox);
|
||||
}
|
||||
|
||||
// Clean up transform node
|
||||
if (entity.has<TransformComponent>()) {
|
||||
auto &transform = entity.get_mut<TransformComponent>();
|
||||
|
||||
@@ -26,6 +26,8 @@ class BuoyancySystem;
|
||||
class NormalDebugSystem;
|
||||
class EditorCamera;
|
||||
class EditorApp;
|
||||
class EditorSunSystem;
|
||||
class EditorSkyboxSystem;
|
||||
|
||||
namespace Ogre
|
||||
{
|
||||
@@ -163,6 +165,20 @@ public:
|
||||
m_buoyancySystem = buoyancy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sun/skybox systems so deleteEntity can release their
|
||||
* runtime objects (light, spheres, skybox node) before the
|
||||
* entity's transform node is destroyed.
|
||||
*/
|
||||
void setSunSystem(EditorSunSystem *sys)
|
||||
{
|
||||
m_sunSystem = sys;
|
||||
}
|
||||
void setSkyboxSystem(EditorSkyboxSystem *sys)
|
||||
{
|
||||
m_skyboxSystem = sys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set normal debug system for toggle
|
||||
*/
|
||||
@@ -296,6 +312,10 @@ private:
|
||||
// Buoyancy system reference (for configuration)
|
||||
BuoyancySystem *m_buoyancySystem = nullptr;
|
||||
|
||||
// Sun/skybox system references (for deleteEntity cleanup)
|
||||
EditorSunSystem *m_sunSystem = nullptr;
|
||||
EditorSkyboxSystem *m_skyboxSystem = nullptr;
|
||||
|
||||
// Normal debug system reference (for toggle)
|
||||
NormalDebugSystem *m_normalDebugSystem = nullptr;
|
||||
|
||||
|
||||
@@ -53,6 +53,15 @@ void PlayerControllerSystem::resetControllers()
|
||||
m_states.clear();
|
||||
}
|
||||
|
||||
void PlayerControllerSystem::setControllerYawPitch(float yawDeg,
|
||||
float pitchDeg)
|
||||
{
|
||||
for (auto &pair : m_states) {
|
||||
pair.second.yaw = yawDeg;
|
||||
pair.second.pitch = pitchDeg;
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerControllerSystem::shutdownController(ControllerState &state)
|
||||
{
|
||||
if (state.targetEntity.is_alive())
|
||||
@@ -448,7 +457,16 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc,
|
||||
if (!ats)
|
||||
return;
|
||||
|
||||
bool inWater = state.targetEntity.has<InWater>();
|
||||
/* Swim states only when actually floating: InWater fires as soon
|
||||
* as the body centre dips below the surface (~chest-deep), but in
|
||||
* shallow water the character still stands on the bottom (Jolt
|
||||
* reports it as supported) and buoyancy cannot lift it. Playing
|
||||
* the swim animation then would show an upright body sticking out
|
||||
* of the water, so wading keeps the land states. */
|
||||
const CharacterComponent &cc =
|
||||
state.targetEntity.get<CharacterComponent>();
|
||||
bool inWater =
|
||||
state.targetEntity.has<InWater>() && !cc.isSupported;
|
||||
Ogre::String animState;
|
||||
if (inWater) {
|
||||
if (!isMoving) {
|
||||
|
||||
@@ -51,6 +51,13 @@ public:
|
||||
*/
|
||||
void resetControllers();
|
||||
|
||||
/**
|
||||
* Override the camera yaw/pitch (degrees) of every active controller.
|
||||
* Debug/testing helper used by demo screenshot capture; the next
|
||||
* updateTPSCamera call re-applies it to the camera.
|
||||
*/
|
||||
void setControllerYawPitch(float yawDeg, float pitchDeg);
|
||||
|
||||
private:
|
||||
struct ControllerState {
|
||||
flecs::entity targetEntity = flecs::entity::null();
|
||||
|
||||
@@ -23,11 +23,12 @@ public:
|
||||
component.markDirty();
|
||||
}
|
||||
|
||||
if (ImGui::SliderFloat("Box Size", &component.size, 100.0f, 2000.0f,
|
||||
"%.0f")) {
|
||||
changed = true;
|
||||
component.markDirty();
|
||||
}
|
||||
// The sky is a fullscreen triangle now; the legacy cube size
|
||||
// is loaded from old scenes but has no visual effect.
|
||||
ImGui::BeginDisabled();
|
||||
ImGui::SliderFloat("Box Size (legacy)", &component.size, 100.0f,
|
||||
2000.0f, "%.0f");
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Day Colors");
|
||||
|
||||
Reference in New Issue
Block a user