Compare commits

..

13 Commits

Author SHA1 Message Date
Vladislav Khorev
fe01a5d1b2 Working on audio in web part 2 2026-04-17 17:14:44 +03:00
Vladislav Khorev
b6e0422d71 Working on audio in web part 1 2026-04-17 17:13:55 +03:00
Vladislav Khorev
3778a603f2 merge 2026-04-17 16:30:09 +03:00
Vladislav Khorev
e310822462 Working on web version 2026-04-17 16:13:56 +03:00
Vladislav Khorev
3d3c3b2a36 merge 2026-04-17 13:42:45 +03:00
Vlad
b5bcd3a812 added music 2026-04-17 14:23:56 +06:00
16e7580418 Merge branch 'witcher001-cutscene' of gitea.fishrungames.com:salmon-engine-projects/space-game001 into witcher001-cutscene 2026-04-17 02:33:56 +06:00
be4dcd4cf9 fix: make choice options the only clickable area in dialogue choices 2026-04-17 02:04:36 +06:00
Vladislav Khorev
a1dda3fd50 Major refactoring 2026-04-16 20:24:27 +03:00
Vladislav Khorev
92ba3f2b60 Some refactoring 2026-04-16 20:06:03 +03:00
Vladislav Khorev
f2f56125f0 Refactoring 2026-04-16 18:07:22 +03:00
Vladislav Khorev
2ef2e456f7 GPU skinning refactoring 2026-04-16 17:48:23 +03:00
Vladislav Khorev
83cceecccd Refactoring 2026-04-16 15:37:48 +03:00
50 changed files with 1926 additions and 5790 deletions

3
.gitattributes vendored
View File

@ -2,3 +2,6 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.anim filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text

BIN
audio/background.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,111 @@
import bpy
import bmesh
import mathutils
import random
import math
class SolidTreeGenerator:
def __init__(self, levels=5, length=3.0, radius=0.3):
self.levels = levels
self.base_length = length
self.base_radius = radius
# Хранилище для данных: (start_pos, end_pos, radius_start, radius_end)
self.branches_data = []
def calculate_tree(self, start_pos, direction, length, radius, level):
if level <= 0 or length < 0.1:
return
end_pos = start_pos + direction * length
# Сохраняем данные сегмента
self.branches_data.append({
'start': start_pos.copy(),
'end': end_pos.copy(),
'r_start': radius,
'r_end': radius * 0.7
})
# 1. Основной ствол (продолжение)
trunk_dir = (direction + self.get_random_vector(0.1)).normalized()
self.calculate_tree(end_pos, trunk_dir, length * 0.8, radius * 0.7, level - 1)
# 2. Боковые ветки (ветвление)
if level > 1:
num_sides = random.randint(2, 3) # Минимум 2 ветки для видимости
for _ in range(num_sides):
# Создаем вектор, сильно отклоненный от ствола (30-60 градусов)
axis = self.get_random_vector(1.0).normalized()
angle = math.radians(random.uniform(30, 60))
side_dir = direction.copy()
side_dir.rotate(mathutils.Quaternion(axis, angle))
# Боковые ветки короче
self.calculate_tree(end_pos, side_dir, length * 0.6, radius * 0.5, level - 1)
def get_random_vector(self, intensity):
return mathutils.Vector((
random.uniform(-intensity, intensity),
random.uniform(-intensity, intensity),
random.uniform(-intensity, intensity)
))
def build_mesh(self):
mesh = bpy.data.meshes.new("TreeMesh")
obj = bpy.data.objects.new("Tree", mesh)
bpy.context.collection.objects.link(obj)
bm = bmesh.new()
skin_layer = bm.verts.layers.skin.verify()
# Словарь для предотвращения дублирования вершин в одной точке
# Ключ - кортеж координат, Значение - объект вершины BMesh
vert_map = {}
for b in self.branches_data:
# Превращаем координаты в кортежи для словаря
s_key = tuple(round(v, 4) for v in b['start'])
e_key = tuple(round(v, 4) for v in b['end'])
# Получаем или создаем начальную вершину
if s_key not in vert_map:
v_start = bm.verts.new(b['start'])
v_start[skin_layer].radius = (b['r_start'], b['r_start'])
vert_map[s_key] = v_start
else:
v_start = vert_map[s_key]
# Получаем или создаем конечную вершину
if e_key not in vert_map:
v_end = bm.verts.new(b['end'])
v_end[skin_layer].radius = (b['r_end'], b['r_end'])
vert_map[e_key] = v_end
else:
v_end = vert_map[e_key]
# Создаем ребро, если его еще нет
if not bm.edges.get((v_start, v_end)):
bm.edges.new((v_start, v_end))
# Находим корень (самую нижнюю точку) и помечаем его
root_v = min(bm.verts, key=lambda v: v.co.z)
root_v[skin_layer].use_root = True
bm.to_mesh(mesh)
bm.free()
# Модификаторы
obj.modifiers.new(name="Skin", type='SKIN')
sub = obj.modifiers.new(name="Subdiv", type='SUBSURF')
sub.levels = 1 # Для начала 1, чтобы не тормозило
# Очистка сцены
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Запуск
generator = SolidTreeGenerator(levels=5, length=3.0, radius=0.4)
generator.calculate_tree(mathutils.Vector((0,0,0)), mathutils.Vector((0,0,1)), 3.0, 0.4, 5)
generator.build_mesh()

View File

@ -53,3 +53,6 @@ check_and_download("https://github.com/lua/lua/archive/refs/tags/v5.4.8.zip" "lu
# 10) sol2 (header-only C++ bindings for Lua)
check_and_download("https://github.com/ThePhD/sol2/archive/refs/tags/v3.3.0.zip" "sol2-v3.3.0.zip" "sol2-3.3.0" "include/sol/sol.hpp")
# 11) SDL2_mixer
check_and_download("https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.8.0.zip" "SDL_mixer-release-2.8.0.zip" "SDL_mixer-release-2.8.0" "CMakeLists.txt")

View File

@ -608,3 +608,121 @@ if(NOT TARGET sol2_external_lib)
target_include_directories(sol2_external_lib INTERFACE "${SOL2_SRC_DIR}/include")
target_link_libraries(sol2_external_lib INTERFACE lua_static)
endif()
# ===========================================
# 11) SDL2_mixer (2.8.0) сборка из исходников
# ===========================================
set(SDL2MIXER_SRC_DIR "${THIRDPARTY_DIR}/SDL_mixer-release-2.8.0")
set(SDL2MIXER_BASE_DIR "${SDL2MIXER_SRC_DIR}/install")
set(SDL2MIXER_BASE_DIR "${SDL2MIXER_BASE_DIR}" CACHE PATH "SDL2_mixer install base directory" FORCE)
set(_have_sdl2mixer TRUE)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(NOT EXISTS "${SDL2MIXER_BASE_DIR}-${cfg}/lib/SDL2_mixer.lib" AND
NOT EXISTS "${SDL2MIXER_BASE_DIR}-${cfg}/lib/SDL2_mixerd.lib")
set(_have_sdl2mixer FALSE)
endif()
endforeach()
if(NOT _have_sdl2mixer)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(cfg STREQUAL "Debug")
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2d.lib")
else()
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2.lib")
endif()
log("Configuring SDL2_mixer (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${SDL2MIXER_SRC_DIR}"
-B "${SDL2MIXER_SRC_DIR}/build-${cfg}"
-DCMAKE_INSTALL_PREFIX=${SDL2MIXER_BASE_DIR}-${cfg}
-DCMAKE_PREFIX_PATH=${SDL2_INSTALL_DIR}
-DSDL2_LIBRARY=${_SDL2_LIB}
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
-DSDL2MIXER_DEPS_SHARED=OFF
-DSDL2MIXER_VENDORED=ON
-DSDL2MIXER_SAMPLES=OFF
-DSDL2MIXER_MUSIC_CMD=OFF
-DSDL2MIXER_MOD=OFF
-DSDL2MIXER_MIDI=OFF
-DSDL2MIXER_OPUS=OFF
-DSDL2MIXER_WAVPACK=OFF
-DSDL2MIXER_MP3_MPG123=OFF
-DSDL2MIXER_MP3_DRMP3=ON
-DSDL2MIXER_FLAC_DRFLAC=ON
-DSDL2MIXER_OGG_STB=ON
-DCMAKE_DISABLE_FIND_PACKAGE_OGG=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_Vorbis=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_FLAC=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_MPG123=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_LibModPlug=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_FluidLite=TRUE
RESULT_VARIABLE _mixer_cfg_res
OUTPUT_VARIABLE _mixer_cfg_out
ERROR_VARIABLE _mixer_cfg_err
)
if(NOT _mixer_cfg_res EQUAL 0)
message(STATUS "SDL2_mixer configure stdout: ${_mixer_cfg_out}")
message(STATUS "SDL2_mixer configure stderr: ${_mixer_cfg_err}")
message(FATAL_ERROR "SDL2_mixer configure failed for ${cfg}")
endif()
log("Building SDL2_mixer (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${SDL2MIXER_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _mixer_build_res
)
if(NOT _mixer_build_res EQUAL 0)
message(FATAL_ERROR "SDL2_mixer build failed for ${cfg}")
endif()
log("Installing SDL2_mixer (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${SDL2MIXER_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _mixer_inst_res
)
if(NOT _mixer_inst_res EQUAL 0)
message(FATAL_ERROR "SDL2_mixer install failed for ${cfg}")
endif()
endforeach()
endif()
set(_mixer_debug_lib "")
foreach(cand
"${SDL2MIXER_BASE_DIR}-Debug/lib/SDL2_mixerd.lib"
"${SDL2MIXER_BASE_DIR}-Debug/lib/SDL2_mixer.lib"
)
if(EXISTS "${cand}")
set(_mixer_debug_lib "${cand}")
break()
endif()
endforeach()
set(_mixer_release_lib "")
foreach(cand
"${SDL2MIXER_BASE_DIR}-Release/lib/SDL2_mixer.lib"
)
if(EXISTS "${cand}")
set(_mixer_release_lib "${cand}")
break()
endif()
endforeach()
if(_mixer_debug_lib STREQUAL "" OR _mixer_release_lib STREQUAL "")
message(FATAL_ERROR "SDL2_mixer libs not found in ${SDL2MIXER_BASE_DIR}-Debug/Release")
endif()
add_library(SDL2_mixer_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2_mixer_external_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "${_mixer_debug_lib}"
IMPORTED_LOCATION_RELEASE "${_mixer_release_lib}"
INTERFACE_INCLUDE_DIRECTORIES
"$<IF:$<CONFIG:Debug>,${SDL2MIXER_BASE_DIR}-Debug/include,${SDL2MIXER_BASE_DIR}-Release/include>"
INTERFACE_LINK_LIBRARIES
"SDL2_external_lib"
)

View File

@ -1,156 +0,0 @@
{
"root": {
"type": "FrameLayout",
"x": 0,
"y": 0,
"width": 1280,
"height": 720,
"children": [
{
"type": "FrameLayout",
"name": "leftPanel",
"x": 100,
"y": 100,
"width": 320,
"height": 400,
"children": [
{
"type": "LinearLayout",
"name": "mainButtons",
"orientation": "vertical",
"spacing": 10,
"x": 0,
"y": 0,
"width": 300,
"height": 300,
"children": [
{
"type": "Button",
"name": "playButton",
"x": 100,
"y": 300,
"width": 200,
"height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
}
},
"textures": {
"normal": "./resources/button.png",
"hover": "./resources/sand.png",
"pressed": "./resources/button.png"
}
},
{
"type": "Button",
"name": "settingsButton",
"x": 100,
"y": 200,
"width": 200,
"height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "wait",
"duration": 0.5
},
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
}
},
"textures": {
"normal": "./resources/sand.png",
"hover": "./resources/button.png",
"pressed": "./resources/sand.png"
}
},
{
"type": "Button",
"name": "exitButton",
"x": 100,
"y": 100,
"width": 200,
"height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "wait",
"duration": 1.0
},
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
},
"bgScroll": {
"repeat": true,
"steps": [
{
"type": "move",
"to": [
1280,
0
],
"duration": 5.0,
"easing": "linear"
}
]
}
},
"textures": {
"normal": "./resources/rock.png",
"hover": "./resources/button.png",
"pressed": "./resources/rock.png"
}
}
]
}
]
},
{
"type": "Slider",
"name": "musicVolumeSlider",
"x": 1140,
"y": 100,
"width": 10,
"height": 500,
"value": 0.5,
"orientation": "vertical",
"textures": {
"track": "./resources/musicVolumeBarTexture.png",
"knob": "./resources/musicVolumeBarButton.png"
}
}
]
}
}

View File

@ -8,7 +8,7 @@ if(NOT CMAKE_MAKE_PROGRAM AND WIN32)
endif()
endif()
project(space-game001 LANGUAGES C CXX)
project(bishkek-witcher LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -83,49 +83,49 @@ set(SOURCES
../src/utils/Utils.h
../src/SparkEmitter.cpp
../src/SparkEmitter.h
# ../src/planet/PlanetObject.cpp
# ../src/planet/PlanetObject.h
# ../src/planet/PlanetData.cpp
# ../src/planet/PlanetData.h
../src/utils/Perlin.cpp
../src/utils/Perlin.h
../src/utils/TaskManager.cpp
../src/utils/TaskManager.h
# ../src/planet/StoneObject.cpp
# ../src/planet/StoneObject.h
../src/render/FrameBuffer.cpp
../src/render/FrameBuffer.h
../src/render/ShadowMap.cpp
../src/render/ShadowMap.h
../src/UiManager.cpp
../src/UiManager.h
../src/Projectile.h
../src/Projectile.cpp
# ../src/network/NetworkInterface.h
# ../src/network/LocalClient.h
# ../src/network/LocalClient.cpp
# ../src/network/ClientState.h
# ../src/network/ClientState.cpp
# ../src/network/WebSocketClientBase.h
# ../src/network/WebSocketClientBase.cpp
# ../src/network/WebSocketClientEmscripten.h
# ../src/network/WebSocketClientEmscripten.cpp
../src/render/TextRenderer.h
../src/render/TextRenderer.cpp
../src/MenuManager.h
../src/MenuManager.cpp
# ../src/Space.h
# ../src/Space.cpp
../src/Location.h
../src/Location.cpp
../src/GameConstants.h
../src/GameConstants.cpp
../src/ScriptEngine.h
../src/ScriptEngine.cpp
../src/navigation/PathFinder.h
../src/navigation/PathFinder.cpp
../src/items/GameObjectLoader.h
../src/items/GameObjectLoader.cpp
../src/items/Item.h
../src/items/Item.cpp
../src/items/InteractiveObject.h
../src/items/InteractiveObject.cpp
../src/dialogue/DialogueTypes.h
../src/dialogue/DialogueDatabase.h
../src/dialogue/DialogueDatabase.cpp
../src/dialogue/DialogueRuntime.h
../src/dialogue/DialogueRuntime.cpp
../src/dialogue/DialogueOverlay.h
../src/dialogue/DialogueOverlay.cpp
../src/dialogue/DialogueSystem.h
../src/dialogue/DialogueSystem.cpp
)
add_executable(space-game001 ${SOURCES})
add_executable(bishkek-witcher ${SOURCES})
# Настройка путей к инклудам (используем скачанные исходники)
target_include_directories(space-game001 PRIVATE
target_include_directories(bishkek-witcher PRIVATE
../src
../thirdparty/eigen-5.0.0
../thirdparty/boost_1_90_0
@ -140,7 +140,7 @@ set(ENABLE_COMMONCRYPTO OFF CACHE BOOL "" FORCE)
add_subdirectory("../thirdparty/libzip-1.11.4" libzip-build)
target_link_libraries(space-game001 PRIVATE zip z lua_static websocket.js)
target_link_libraries(bishkek-witcher PRIVATE zip z lua_static websocket.js)
# Эмскриптен-флаги
set(EMSCRIPTEN_FLAGS
@ -149,13 +149,15 @@ set(EMSCRIPTEN_FLAGS
"-sUSE_LIBPNG=1"
"-sUSE_ZLIB=1"
"-sUSE_SDL_TTF=2"
"-sUSE_SDL_MIXER=2"
"-sSDL2_MIXER_FORMATS=[ogg,mp3]"
#"-pthread"
#"-sUSE_PTHREADS=1"
"-fexceptions"
"-DNETWORK"
)
target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
target_compile_options(bishkek-witcher PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
# Only loading.png and the shaders used before resources.zip is ready are preloaded.
# resources.zip is downloaded asynchronously at runtime and served as a separate file.
@ -168,14 +170,15 @@ set(EMSCRIPTEN_LINK_FLAGS
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loading.png@resources/loading.png"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/shaders@resources/shaders"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start.lua@resources/start.lua"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../audio@audio"
)
# Применяем настройки линковки
target_link_options(space-game001 PRIVATE ${EMSCRIPTEN_LINK_FLAGS})
target_link_options(bishkek-witcher PRIVATE ${EMSCRIPTEN_LINK_FLAGS})
# Для совместимости со старыми версиями CMake, если target_link_options недостаточно
string(REPLACE ";" " " EMSCRIPTEN_LINK_FLAGS_STR "${EMSCRIPTEN_LINK_FLAGS}")
set_target_properties(space-game001 PROPERTIES
set_target_properties(bishkek-witcher PROPERTIES
LINK_FLAGS "${EMSCRIPTEN_LINK_FLAGS_STR}"
SUFFIX ".html"
)
@ -193,33 +196,33 @@ add_custom_command(
)
add_custom_target(pack_resources DEPENDS "${RESOURCES_ZIP}")
add_dependencies(space-game001 pack_resources)
add_dependencies(bishkek-witcher pack_resources)
# Определяем путь к директории установки (относительно папки билда)
set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/public")
# 1. Устанавливаем основной HTML файл
install(TARGETS space-game001
install(TARGETS bishkek-witcher
RUNTIME DESTINATION .
)
# 2. Устанавливаем сопутствующие файлы (JS, WASM и сгенерированный архив данных)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/space-game001.js"
"${CMAKE_CURRENT_BINARY_DIR}/space-game001.wasm"
"${CMAKE_CURRENT_BINARY_DIR}/space-game001.data"
"${CMAKE_CURRENT_BINARY_DIR}/bishkek-witcher.js"
"${CMAKE_CURRENT_BINARY_DIR}/bishkek-witcher.wasm"
"${CMAKE_CURRENT_BINARY_DIR}/bishkek-witcher.data"
DESTINATION .
)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/space-game001plain.html"
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/index.html"
DESTINATION .
)
# resources.zip is served separately and downloaded asynchronously at runtime
install(FILES "${RESOURCES_ZIP}" DESTINATION .)
add_custom_command(TARGET space-game001 POST_BUILD
add_custom_command(TARGET bishkek-witcher POST_BUILD
COMMAND ${CMAKE_COMMAND} --install .
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
COMMENT "Automatically deploying to public directory..."

82
proj-web/index.html Normal file
View File

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Bishkek Witcher</title>
<style>
body, html {
margin: 0; padding: 0; width: 100%; height: 100%;
overflow: hidden; background-color: #000;
position: fixed;
}
#canvas {
display: block;
position: absolute;
top: 0; left: 0;
width: 100vw; height: 100vh;
border: none;
}
#fs-button {
position: absolute;
top: 10px; right: 10px;
padding: 10px;
z-index: 10;
background: rgba(255,255,255,0.3);
color: white; border: 1px solid white;
cursor: pointer;
font-family: sans-serif;
border-radius: 5px;
}
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
</style>
</head>
<body>
<button id="fs-button">Fullscreen</button>
<div id="status">Downloading...</div>
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
<script>
function prepareModuleEnvironment() {
window.Module = window.Module || {};
var canvasEl = document.getElementById('canvas');
window.Module.canvas = canvasEl;
window.Module.setStatus = window.Module.setStatus || function (text) {
var statusElement = document.getElementById("status");
statusElement.innerHTML = text;
statusElement.style.display = text ? 'block' : 'none';
};
}
function loadGameScript() {
var s = document.createElement('script');
s.src = 'bishkek-witcher.js';
s.async = true;
document.body.appendChild(s);
}
document.getElementById('fs-button').addEventListener('click', function() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(function(e) {
console.error('Fullscreen error: ' + e.message);
});
} else {
document.exitFullscreen();
}
});
document.addEventListener('DOMContentLoaded', function() {
prepareModuleEnvironment();
loadGameScript();
});
window.addEventListener("orientationchange", function() {
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 200);
});
</script>
</body>
</html>

View File

@ -36,41 +36,22 @@ add_executable(space-game001
../src/utils/Utils.h
../src/SparkEmitter.cpp
../src/SparkEmitter.h
# ../src/planet/PlanetObject.cpp
# ../src/planet/PlanetObject.h
# ../src/planet/PlanetData.cpp
# ../src/planet/PlanetData.h
../src/utils/Perlin.cpp
../src/utils/Perlin.h
../src/utils/TaskManager.cpp
../src/utils/TaskManager.h
# ../src/planet/StoneObject.cpp
# ../src/planet/StoneObject.h
../src/render/FrameBuffer.cpp
../src/render/FrameBuffer.h
../src/render/ShadowMap.cpp
../src/render/ShadowMap.h
../src/UiManager.cpp
../src/UiManager.h
../src/Projectile.h
../src/Projectile.cpp
# ../src/network/NetworkInterface.h
# ../src/network/LocalClient.h
# ../src/network/LocalClient.cpp
# ../src/network/ClientState.h
# ../src/network/ClientState.cpp
# ../src/network/WebSocketClient.h
# ../src/network/WebSocketClient.cpp
# ../src/network/WebSocketClientBase.h
# ../src/network/WebSocketClientBase.cpp
# ../src/network/WebSocketClientEmscripten.h
# ../src/network/WebSocketClientEmscripten.cpp
../src/render/TextRenderer.h
../src/render/TextRenderer.cpp
../src/MenuManager.h
../src/MenuManager.cpp
# ../src/Space.h
# ../src/Space.cpp
../src/Location.h
../src/Location.cpp
../src/GameConstants.h
../src/GameConstants.cpp
../src/ScriptEngine.h
@ -115,8 +96,6 @@ target_compile_definitions(space-game001 PRIVATE
SDL_MAIN_HANDLED
# DEBUG_LIGHT
# SHOW_PATH
# NETWORK
# SIMPLIFIED
)
# Линкуем с SDL2main, если он вообще установлен
@ -133,6 +112,7 @@ target_link_libraries(space-game001 PRIVATE
eigen_external_lib
boost_external_lib
sol2_external_lib
SDL2_mixer_external_lib
)
# Линкуем OpenGL (Windows)
@ -157,6 +137,7 @@ if (WIN32)
set(SDL2TTF_DLL_SRC "$<IF:$<CONFIG:Debug>,${SDL2TTF_BASE_DIR}-Debug/bin/SDL2_ttfd.dll,${SDL2TTF_BASE_DIR}-Release/bin/SDL2_ttf.dll>")
set(SDL2MIXER_DLL_SRC "$<IF:$<CONFIG:Debug>,${SDL2MIXER_BASE_DIR}-Debug/bin/SDL2_mixerd.dll,${SDL2MIXER_BASE_DIR}-Release/bin/SDL2_mixer.dll>")
add_custom_command(TARGET space-game001 POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Copying DLLs to output folder..."
@ -178,6 +159,10 @@ if (WIN32)
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2TTF_DLL_SRC}"
"$<TARGET_FILE_DIR:space-game001>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/SDL_mixer-release-2.8.0/install-$<CONFIG>/bin/SDL2_mixer$<$<CONFIG:Debug>:d>.dll"
"$<TARGET_FILE_DIR:space-game001>/SDL2_mixer$<$<CONFIG:Debug>:d>.dll"
)
endif()
@ -188,6 +173,7 @@ endif()
# Какие папки с ресурсами нужно копировать
set(RUNTIME_RESOURCE_DIRS
"resources"
"audio"
)
# Копируем ресурсы и шейдеры в папку exe и в корень build/

View File

@ -59,7 +59,7 @@
"type": "Choice",
"speaker": "Hero",
"portrait": "resources/hero.png",
"text": "Choose your answer.",
"text": "",
"choices": [
{
"id": "main_1",

View File

@ -2,6 +2,7 @@
-- NPC PATROL WAYPOINTS
-- ============================================
local function step3()
game_api.npc_walk_to(0, 0.0, 0.0, -30.0, step1)
end
@ -32,7 +33,6 @@ end
function on_npc_interact(npc_index)
print("[Lua] NPC interaction! Index: " .. tostring(npc_index))
if npc_index == 1 then
game_api.start_dialogue("test_line_dialogue")
else

View File

@ -1,93 +1,207 @@
#ifdef AUDIO
#include "AudioPlayerAsync.h"
#include <iostream>
#ifdef __EMSCRIPTEN__
AudioPlayerAsync::AudioPlayerAsync() {}
#else
AudioPlayerAsync::AudioPlayerAsync() : worker(&AudioPlayerAsync::workerThread, this) {}
#endif
AudioPlayerAsync::~AudioPlayerAsync() {
#ifndef __EMSCRIPTEN__
{
std::unique_lock<std::mutex> lock(mtx);
stop = true;
cv.notify_all();
}
if (worker.joinable())
worker.join();
#endif
shutdown();
}
void AudioPlayerAsync::stopAsync() {
bool AudioPlayerAsync::init() {
if (initialized) return true;
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
std::cerr << "SDL_InitSubSystem(SDL_INIT_AUDIO) failed: " << SDL_GetError() << std::endl;
return false;
}
if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
std::cerr << "Mix_OpenAudio failed: " << Mix_GetError() << std::endl;
return false;
}
Mix_AllocateChannels(16);
initialized = true;
std::cout << "AudioPlayerAsync initialized with SDL2_mixer" << std::endl;
return true;
}
void AudioPlayerAsync::shutdown() {
if (!initialized) return;
{
std::lock_guard<std::mutex> lock(soundCacheMutex);
for (auto& pair : soundCache) {
Mix_FreeChunk(pair.second);
}
soundCache.clear();
}
Mix_CloseAudio();
SDL_QuitSubSystem(SDL_INIT_AUDIO);
initialized = false;
std::cout << "AudioPlayerAsync shutdown" << std::endl;
}
void AudioPlayerAsync::playSoundAsync(const std::string& filePath, int loops, int channel) {
if (!initialized) {
std::cerr << "AudioPlayerAsync not initialized" << std::endl;
return;
}
auto task = [this, filePath, loops, channel]() {
Mix_Chunk* sound = nullptr;
{
std::lock_guard<std::mutex> cacheLock(soundCacheMutex);
auto it = soundCache.find(filePath);
if (it != soundCache.end()) {
sound = it->second;
}
}
if (!sound) {
sound = Mix_LoadWAV(filePath.c_str());
if (!sound) {
std::cerr << "Failed to load sound " << filePath << ": " << Mix_GetError() << std::endl;
return;
}
std::lock_guard<std::mutex> cacheLock(soundCacheMutex);
soundCache[filePath] = sound;
}
int result = Mix_PlayChannel(channel, sound, loops);
if (result == -1) {
std::cerr << "Mix_PlayChannel failed: " << Mix_GetError() << std::endl;
}
};
#ifdef __EMSCRIPTEN__
task();
#else
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push([this]() {
//audioPlayerMutex.lock();
audioPlayer->stop();
std::this_thread::sleep_for(std::chrono::seconds(1));
//audioPlayerMutex.unlock();
taskQueue.push(std::move(task));
cv.notify_one();
#endif
}
void AudioPlayerAsync::playMusicAsync(const std::string& filePath, int loops) {
if (!initialized) return;
auto task = [filePath, loops]() {
Mix_Music* music = Mix_LoadMUS(filePath.c_str());
if (!music) {
std::cerr << "Failed to load music " << filePath << ": " << Mix_GetError() << std::endl;
return;
}
if (Mix_PlayMusic(music, loops) == -1) {
std::cerr << "Mix_PlayMusic failed: " << Mix_GetError() << std::endl;
Mix_FreeMusic(music);
}
};
#ifdef __EMSCRIPTEN__
task();
#else
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push(std::move(task));
cv.notify_one();
#endif
}
void AudioPlayerAsync::stopMusicAsync() {
if (!initialized) return;
#ifdef __EMSCRIPTEN__
Mix_HaltMusic();
#else
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push([]() {
Mix_HaltMusic();
});
cv.notify_one();
#endif
}
void AudioPlayerAsync::resetAsync() {
void AudioPlayerAsync::pauseMusicAsync() {
if (!initialized) return;
#ifdef __EMSCRIPTEN__
Mix_PauseMusic();
#else
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push([this]() {
//audioPlayerMutex.lock();
audioPlayer.reset();
audioPlayer = std::make_unique<AudioPlayer>();
//audioPlayerMutex.unlock();
taskQueue.push([]() {
Mix_PauseMusic();
});
cv.notify_one();
#endif
}
void AudioPlayerAsync::playSoundAsync(std::string soundName) {
soundNameMutex.lock();
latestSoundName = soundName;
soundNameMutex.unlock();
void AudioPlayerAsync::resumeMusicAsync() {
if (!initialized) return;
#ifdef __EMSCRIPTEN__
Mix_ResumeMusic();
#else
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push([this]() {
//audioPlayerMutex.lock();
if (audioPlayer) {
audioPlayer->playSound(latestSoundName);
}
//audioPlayerMutex.unlock();
taskQueue.push([]() {
Mix_ResumeMusic();
});
cv.notify_one();
#endif
}
void AudioPlayerAsync::playMusicAsync(std::string musicName) {
musicNameMutex.lock();
latestMusicName = musicName;
musicNameMutex.unlock();
void AudioPlayerAsync::setMusicVolume(int volume) {
if (!initialized) return;
volume = std::max(0, std::min(128, volume));
#ifdef __EMSCRIPTEN__
Mix_VolumeMusic(volume);
#else
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push([this]() {
//audioPlayerMutex.lock();
if (audioPlayer) {
audioPlayer->playMusic(latestMusicName);
}
//audioPlayerMutex.unlock();
taskQueue.push([volume]() {
Mix_VolumeMusic(volume);
});
cv.notify_one();
#endif
}
void AudioPlayerAsync::setSoundVolume(int volume) {
if (!initialized) return;
volume = std::max(0, std::min(128, volume));
#ifdef __EMSCRIPTEN__
Mix_Volume(-1, volume);
#else
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push([volume]() {
Mix_Volume(-1, volume); // все каналы
});
cv.notify_one();
#endif
}
#ifndef __EMSCRIPTEN__
void AudioPlayerAsync::workerThread() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]() { return !taskQueue.empty() || stop; });
if (stop && taskQueue.empty()) {
cv.wait(lock, [this] { return !taskQueue.empty() || stop; });
if (stop && taskQueue.empty())
break;
}
task = taskQueue.front();
task = std::move(taskQueue.front());
taskQueue.pop();
}
task();
}
}
#endif

View File

@ -1,53 +1,50 @@
#pragma once
#ifdef AUDIO
#include <iostream>
#include <thread>
#include <SDL2/SDL_mixer.h>
#include <string>
#include <unordered_map>
#include <memory>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include "cmakeaudioplayer/include/AudioPlayer.hpp"
#include <SDL2/SDL.h>
#ifndef __EMSCRIPTEN__
#include <queue>
#include <thread>
#include <condition_variable>
#include <functional>
#endif
class AudioPlayerAsync {
public:
AudioPlayerAsync();
~AudioPlayerAsync();
void resetAsync();
bool init();
void shutdown();
void playSoundAsync(std::string soundName);
void playSoundAsync(const std::string& filePath, int loops = 0, int channel = -1);
void playMusicAsync(const std::string& filePath, int loops = -1);
void stopMusicAsync();
void pauseMusicAsync();
void resumeMusicAsync();
void setMusicVolume(int volume); // 0..128
void setSoundVolume(int volume); // 0..128
void playMusicAsync(std::string musicName);
void stopAsync();
void exit()
{
stop = true;
}
std::thread worker;
void exit() { stop = true; }
private:
std::unique_ptr<AudioPlayer> audioPlayer;
//std::mutex audioPlayerMutex;
std::mutex soundNameMutex;
std::mutex musicNameMutex;
std::string latestSoundName;
std::string latestMusicName;
#ifndef __EMSCRIPTEN__
void workerThread();
std::thread worker;
std::mutex mtx;
std::condition_variable cv;
std::queue<std::function<void()>> taskQueue;
#endif
bool stop = false;
void workerThread();
std::unordered_map<std::string, Mix_Chunk*> soundCache;
std::mutex soundCacheMutex;
bool initialized = false;
};
#endif

View File

@ -8,6 +8,10 @@
namespace ZL
{
#ifdef EMSCRIPTEN
using std::min;
using std::max;
#endif
int getIndexByValue(const std::string& name, const std::vector<std::string>& words)
{
@ -757,70 +761,6 @@ namespace ZL
}
prepared = true;
/*
if (startBones.size() > MAX_GPU_BONES)
{
std::cout << "Warning: model has " << startBones.size()
<< " bones, exceeding GPU skinning limit of " << MAX_GPU_BONES << std::endl;
}*/
}
void BoneSystem::ComputeSkinningMatrices(int frame, std::vector<Matrix4f>& outMatrices) const
{
int startingKeyFrame = -1;
for (size_t i = 0; i < animations[0].keyFrames.size() - 1; i++)
{
int oldFrame = animations[0].keyFrames[i].frame;
int nextFrame = animations[0].keyFrames[i + 1].frame;
if (frame >= oldFrame && frame < nextFrame)
{
startingKeyFrame = static_cast<int>(i);
break;
}
}
if (startingKeyFrame == -1)
{
outMatrices.resize(startBones.size());
for (auto& m : outMatrices) m = Matrix4f::Identity();
return;
}
int modifiedFrameNumber = frame - animations[0].keyFrames[startingKeyFrame].frame;
int diffFrames = animations[0].keyFrames[startingKeyFrame + 1].frame - animations[0].keyFrames[startingKeyFrame].frame;
float t = (modifiedFrameNumber + 0.f) / diffFrames;
const std::vector<Bone>& oneFrameBones = animations[0].keyFrames[startingKeyFrame].bones;
const std::vector<Bone>& nextFrameBones = animations[0].keyFrames[startingKeyFrame + 1].bones;
outMatrices.resize(startBones.size());
for (size_t i = 0; i < startBones.size(); i++)
{
Vector3f interpPos;
interpPos(0) = oneFrameBones[i].boneStartWorld(0) + t * (nextFrameBones[i].boneStartWorld(0) - oneFrameBones[i].boneStartWorld(0));
interpPos(1) = oneFrameBones[i].boneStartWorld(1) + t * (nextFrameBones[i].boneStartWorld(1) - oneFrameBones[i].boneStartWorld(1));
interpPos(2) = oneFrameBones[i].boneStartWorld(2) + t * (nextFrameBones[i].boneStartWorld(2) - oneFrameBones[i].boneStartWorld(2));
Matrix3f oneFrameBonesMatrix = oneFrameBones[i].boneMatrixWorld.block<3, 3>(0, 0);
Matrix3f nextFrameBonesMatrix = nextFrameBones[i].boneMatrixWorld.block<3, 3>(0, 0);
Eigen::Quaternionf q1 = Eigen::Quaternionf(oneFrameBonesMatrix).normalized();
Eigen::Quaternionf q2 = Eigen::Quaternionf(nextFrameBonesMatrix).normalized();
Eigen::Quaternionf result = q1.slerp(t, q2);
Matrix3f boneMatrixWorld3 = result.toRotationMatrix();
Matrix4f currentBoneMatrixWorld4 = Eigen::Matrix4f::Identity();
currentBoneMatrixWorld4.block<3, 3>(0, 0) = boneMatrixWorld3;
currentBoneMatrixWorld4.block<3, 1>(0, 3) = interpPos;
Matrix4f startBoneMatrixWorld4 = animations[0].keyFrames[0].bones[i].boneMatrixWorld;
Matrix4f invertedStartBoneMatrixWorld4 = startBoneMatrixWorld4.inverse();
outMatrices[i] = currentBoneMatrixWorld4 * invertedStartBoneMatrixWorld4;
}
}
void BoneSystem::Interpolate(int frame)
@ -897,13 +837,6 @@ namespace ZL
}
/*std::cout << "m=" << skinningMatrixForEachBone[5] << std::endl;
std::cout << "Start Mesh data " << std::endl;
std::cout << startMesh.PositionData[0] << std::endl;
std::cout << startMesh.PositionData[10] << std::endl;
std::cout << startMesh.PositionData[100] << std::endl;
*/
for (int i = 0; i < mesh.PositionData.size(); i++)
{
Vector4f originalPos = {
@ -927,29 +860,9 @@ namespace ZL
vMoved = true;
finalPos = finalPos + (skinningMatrixForEachBone[verticesBoneWeight[i][j].boneIndex] * originalPos) * verticesBoneWeight[i][j].weight;
/*if (i == 100)
{
std::cout << "bone index=" << verticesBoneWeight[i][j].boneIndex << std::endl;
std::cout << "weight=" << verticesBoneWeight[i][j].weight << std::endl;
std::cout << "skinning matrix=" << skinningMatrixForEachBone[verticesBoneWeight[i][j].boneIndex] << std::endl;
std::cout << "original pos=" << originalPos.transpose() << std::endl;
std::cout << "final pos=" << finalPos.transpose() << std::endl;
std::cout << "-----------------" << std::endl;
}*/
}
}
/*if (i == 100)
{
std::cout << originalPos << std::endl;
std::cout << finalPos << std::endl;
}*/
/*if (abs(finalPos(0) - originalPos(0)) > 1 || abs(finalPos(1) - originalPos(1)) > 1 || abs(finalPos(2) - originalPos(2)) > 1)
{
}*/
if (!vMoved)
{
finalPos = originalPos;
@ -962,5 +875,148 @@ namespace ZL
}
void GpuSkinningShaderData::prepareGpuSkinningVBOs(BoneSystem& model) {
if (gpuSkinningPrepared)
{
return;
}
gpuBoneData.PrepareGpuSkinningData(model.verticesBoneWeight);
// Upload bind-pose mesh (static, done once)
bindPoseMutable.AssignFrom(model.startMesh);
auto& gpu = gpuBoneData;
boneIndices0VBO = std::make_shared<VBOHolder>();
glBindBuffer(GL_ARRAY_BUFFER, boneIndices0VBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, gpu.boneIndices0.size() * sizeof(Eigen::Vector4f),
gpu.boneIndices0.data(), GL_STATIC_DRAW);
boneIndices1VBO = std::make_shared<VBOHolder>();
glBindBuffer(GL_ARRAY_BUFFER, boneIndices1VBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, gpu.boneIndices1.size() * sizeof(Eigen::Vector2f),
gpu.boneIndices1.data(), GL_STATIC_DRAW);
boneWeights0VBO = std::make_shared<VBOHolder>();
glBindBuffer(GL_ARRAY_BUFFER, boneWeights0VBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, gpu.boneWeights0.size() * sizeof(Eigen::Vector4f),
gpu.boneWeights0.data(), GL_STATIC_DRAW);
boneWeights1VBO = std::make_shared<VBOHolder>();
glBindBuffer(GL_ARRAY_BUFFER, boneWeights1VBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, gpu.boneWeights1.size() * sizeof(Eigen::Vector2f),
gpu.boneWeights1.data(), GL_STATIC_DRAW);
gpuSkinningPrepared = true;
}
void GpuSkinningShaderData::RenderVBO(Renderer& renderer)
{
CheckGlError(__FILE__, __LINE__);
// Bind position and texcoord VBOs
glBindBuffer(GL_ARRAY_BUFFER, bindPoseMutable.positionVBO->getBuffer());
renderer.VertexAttribPointer3fv("vPosition", 0, NULL);
CheckGlError(__FILE__, __LINE__);
if (bindPoseMutable.texCoordVBO) {
glBindBuffer(GL_ARRAY_BUFFER, bindPoseMutable.texCoordVBO->getBuffer());
renderer.VertexAttribPointer2fv("vTexCoord", 0, NULL);
CheckGlError(__FILE__, __LINE__);
}
CheckGlError(__FILE__, __LINE__);
if (bindPoseMutable.normalVBO) {
glBindBuffer(GL_ARRAY_BUFFER, bindPoseMutable.normalVBO->getBuffer());
renderer.VertexAttribPointer3fv("vNormal", 0, NULL);
CheckGlError(__FILE__, __LINE__);
} else {
renderer.DisableVertexAttribArray("vNormal");
CheckGlError(__FILE__, __LINE__);
}
CheckGlError(__FILE__, __LINE__);
// Bind bone index VBOs
glBindBuffer(GL_ARRAY_BUFFER, boneIndices0VBO->getBuffer());
renderer.VertexAttribPointer4fv("aBoneIndices0", 0, NULL);
CheckGlError(__FILE__, __LINE__);
glBindBuffer(GL_ARRAY_BUFFER, boneIndices1VBO->getBuffer());
renderer.VertexAttribPointer2fv("aBoneIndices1", 0, NULL);
CheckGlError(__FILE__, __LINE__);
// Bind bone weight VBOs
glBindBuffer(GL_ARRAY_BUFFER, boneWeights0VBO->getBuffer());
renderer.VertexAttribPointer4fv("aBoneWeights0", 0, NULL);
CheckGlError(__FILE__, __LINE__);
glBindBuffer(GL_ARRAY_BUFFER, boneWeights1VBO->getBuffer());
renderer.VertexAttribPointer2fv("aBoneWeights1", 0, NULL);
CheckGlError(__FILE__, __LINE__);
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(bindPoseMutable.data.PositionData.size()));
}
void GpuSkinningShaderData::ComputeSkinningMatrices(const std::vector<Bone>& startBones, const std::vector<AnimationKeyFrame>& keyFrames, int frame)
{
int startingKeyFrame = -1;
for (size_t i = 0; i < keyFrames.size() - 1; i++)
{
int oldFrame = keyFrames[i].frame;
int nextFrame = keyFrames[i + 1].frame;
if (frame >= oldFrame && frame < nextFrame)
{
startingKeyFrame = static_cast<int>(i);
break;
}
}
skinningMatrices.resize(startBones.size());
if (startingKeyFrame == -1)
{
for (auto& m : skinningMatrices) m = Matrix4f::Identity();
return;
}
int modifiedFrameNumber = frame - keyFrames[startingKeyFrame].frame;
int diffFrames = keyFrames[startingKeyFrame + 1].frame - keyFrames[startingKeyFrame].frame;
float t = (modifiedFrameNumber + 0.f) / diffFrames;
const std::vector<Bone>& oneFrameBones = keyFrames[startingKeyFrame].bones;
const std::vector<Bone>& nextFrameBones = keyFrames[startingKeyFrame + 1].bones;
for (size_t i = 0; i < startBones.size(); i++)
{
Vector3f interpPos;
interpPos(0) = oneFrameBones[i].boneStartWorld(0) + t * (nextFrameBones[i].boneStartWorld(0) - oneFrameBones[i].boneStartWorld(0));
interpPos(1) = oneFrameBones[i].boneStartWorld(1) + t * (nextFrameBones[i].boneStartWorld(1) - oneFrameBones[i].boneStartWorld(1));
interpPos(2) = oneFrameBones[i].boneStartWorld(2) + t * (nextFrameBones[i].boneStartWorld(2) - oneFrameBones[i].boneStartWorld(2));
Matrix3f oneFrameBonesMatrix = oneFrameBones[i].boneMatrixWorld.block<3, 3>(0, 0);
Matrix3f nextFrameBonesMatrix = nextFrameBones[i].boneMatrixWorld.block<3, 3>(0, 0);
Eigen::Quaternionf q1 = Eigen::Quaternionf(oneFrameBonesMatrix).normalized();
Eigen::Quaternionf q2 = Eigen::Quaternionf(nextFrameBonesMatrix).normalized();
Eigen::Quaternionf result = q1.slerp(t, q2);
Matrix3f boneMatrixWorld3 = result.toRotationMatrix();
Matrix4f currentBoneMatrixWorld4 = Eigen::Matrix4f::Identity();
currentBoneMatrixWorld4.block<3, 3>(0, 0) = boneMatrixWorld3;
currentBoneMatrixWorld4.block<3, 1>(0, 3) = interpPos;
Matrix4f startBoneMatrixWorld4 = keyFrames[0].bones[i].boneMatrixWorld;
Matrix4f invertedStartBoneMatrixWorld4 = startBoneMatrixWorld4.inverse();
skinningMatrices[i] = currentBoneMatrixWorld4 * invertedStartBoneMatrixWorld4;
}
}
}

View File

@ -12,8 +12,6 @@ namespace ZL
Vector3f boneStartWorld;
float boneLength;
Matrix4f boneMatrixWorld;
// boneVector = boneLength * (0, 1, 0) <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// Then multiply by boneMatrixWorld <20> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>
int parent;
std::vector<int> children;
@ -59,22 +57,18 @@ namespace ZL
std::vector<Animation> animations;
int startingFrame = 0;
GpuBoneData gpuBoneData;
void LoadFromFile(const std::string& fileName, const std::string& ZIPFileName = "");
void LoadFromBinaryFile(const std::string& fileName, const std::string& ZIPFileName = "");
void Interpolate(int frame);
// GPU skinning: compute skinning matrices without modifying the mesh
void ComputeSkinningMatrices(int frame, std::vector<Matrix4f>& outMatrices) const;
//void ComputeSkinningMatrices(int frame, std::vector<Matrix4f>& outMatrices) const;
};
struct BoneAnimationData {
BoneSystem model;
float currentFrame = 0.f;
int lastFrame = -1;
int totalFrames = 1;
struct GpuSkinningShaderData {
GpuBoneData gpuBoneData;
// GPU skinning data (lazily initialized)
VertexRenderStruct bindPoseMutable;
@ -84,9 +78,23 @@ namespace ZL
std::shared_ptr<VBOHolder> boneWeights1VBO;
bool gpuSkinningPrepared = false;
std::vector<Eigen::Matrix4f> skinningMatrices;
void prepareGpuSkinningVBOs(BoneSystem& model);
void RenderVBO(Renderer& renderer);
void ComputeSkinningMatrices(const std::vector<Bone>& startBones, const std::vector<AnimationKeyFrame>& keyFrames, int frame);
};
struct BoneAnimationData {
BoneSystem model;
float currentFrame = 0.f;
int lastFrame = -1;
int totalFrames = 1;
GpuSkinningShaderData gpuSkinningShaderData;
};
};

View File

@ -27,9 +27,6 @@ void Character::loadBinaryAnimation(AnimationState state, const std::string& fil
data.totalFrames = 1;
}
}
/*void Character::setTexture(std::shared_ptr<Texture> tex) {
texture = tex;
}*/
void Character::setTarget(const Eigen::Vector3f& target,
std::function<void()> onArrived) {
@ -259,7 +256,7 @@ void Character::update(int64_t deltaMs) {
if (static_cast<int>(anim.currentFrame) != anim.lastFrame) {
if (useGpuSkinning) {
anim.model.ComputeSkinningMatrices(static_cast<int>(anim.currentFrame), anim.skinningMatrices);
anim.gpuSkinningShaderData.ComputeSkinningMatrices(anim.model.startBones, anim.model.animations[0].keyFrames, static_cast<int>(anim.currentFrame));
} else {
anim.model.Interpolate(static_cast<int>(anim.currentFrame));
}
@ -301,43 +298,6 @@ void Character::draw(Renderer& renderer) {
renderer.shaderManager.PopShader();
}
void Character::prepareGpuSkinningVBOs(BoneAnimationData& anim) {
if (anim.gpuSkinningPrepared)
{
return;
}
anim.model.gpuBoneData.PrepareGpuSkinningData(anim.model.verticesBoneWeight);
// Upload bind-pose mesh (static, done once)
anim.bindPoseMutable.AssignFrom(anim.model.startMesh);
auto& gpu = anim.model.gpuBoneData;
anim.boneIndices0VBO = std::make_shared<VBOHolder>();
glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices0VBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, gpu.boneIndices0.size() * sizeof(Eigen::Vector4f),
gpu.boneIndices0.data(), GL_STATIC_DRAW);
anim.boneIndices1VBO = std::make_shared<VBOHolder>();
glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices1VBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, gpu.boneIndices1.size() * sizeof(Eigen::Vector2f),
gpu.boneIndices1.data(), GL_STATIC_DRAW);
anim.boneWeights0VBO = std::make_shared<VBOHolder>();
glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights0VBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, gpu.boneWeights0.size() * sizeof(Eigen::Vector4f),
gpu.boneWeights0.data(), GL_STATIC_DRAW);
anim.boneWeights1VBO = std::make_shared<VBOHolder>();
glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights1VBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, gpu.boneWeights1.size() * sizeof(Eigen::Vector2f),
gpu.boneWeights1.data(), GL_STATIC_DRAW);
anim.gpuSkinningPrepared = true;
}
void Character::drawGpuSkinning(Renderer& renderer) {
AnimationState drawState = resolveActiveState();
auto it = animations.find(drawState);
@ -347,14 +307,15 @@ void Character::drawGpuSkinning(Renderer& renderer) {
}
auto& anim = it->second;
prepareGpuSkinningVBOs(anim);
anim.gpuSkinningShaderData.prepareGpuSkinningVBOs(anim.model);
if (anim.skinningMatrices.empty())
if (anim.gpuSkinningShaderData.skinningMatrices.empty())
{
if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty()) return;
anim.model.ComputeSkinningMatrices(
anim.model.animations[0].keyFrames[0].frame, anim.skinningMatrices);
if (anim.skinningMatrices.empty()) return;
anim.gpuSkinningShaderData.ComputeSkinningMatrices(anim.model.startBones, anim.model.animations[0].keyFrames, static_cast<int>(anim.currentFrame));
if (anim.gpuSkinningShaderData.skinningMatrices.empty()) return;
}
static const std::string skinningShaderName = "skinning";
static const std::string boneMatricesUniform = "uBoneMatrices[0]";
@ -374,51 +335,56 @@ void Character::drawGpuSkinning(Renderer& renderer) {
// Upload bone skinning matrices
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
static_cast<int>(anim.skinningMatrices.size()), false,
anim.skinningMatrices[0].data());
static_cast<int>(anim.gpuSkinningShaderData.skinningMatrices.size()), false,
anim.gpuSkinningShaderData.skinningMatrices[0].data());
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
// Bind VAO (desktop only)
#ifndef EMSCRIPTEN
#ifndef __ANDROID__
if (anim.bindPoseMutable.vao) {
glBindVertexArray(anim.bindPoseMutable.vao->getBuffer());
if (anim.gpuSkinningShaderData.bindPoseMutable.vao) {
glBindVertexArray(anim.gpuSkinningShaderData.bindPoseMutable.vao->getBuffer());
renderer.shaderManager.EnableVertexAttribArrays();
}
#endif
#endif
// Bind position and texcoord VBOs
glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.positionVBO->getBuffer());
renderer.VertexAttribPointer3fv("vPosition", 0, NULL);
if (anim.bindPoseMutable.texCoordVBO) {
glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.texCoordVBO->getBuffer());
renderer.VertexAttribPointer2fv("vTexCoord", 0, NULL);
}
// Bind bone index VBOs
glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices0VBO->getBuffer());
renderer.VertexAttribPointer4fv("aBoneIndices0", 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices1VBO->getBuffer());
renderer.VertexAttribPointer2fv("aBoneIndices1", 0, NULL);
// Bind bone weight VBOs
glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights0VBO->getBuffer());
renderer.VertexAttribPointer4fv("aBoneWeights0", 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights1VBO->getBuffer());
renderer.VertexAttribPointer2fv("aBoneWeights1", 0, NULL);
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(anim.bindPoseMutable.data.PositionData.size()));
anim.gpuSkinningShaderData.RenderVBO(renderer);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
}
bool Character::prepareGpuSkinning()
{
AnimationState drawState = resolveActiveState();
auto it = animations.find(drawState);
if (it == animations.end())
{
return false;
}
auto& anim = it->second;
anim.gpuSkinningShaderData.prepareGpuSkinningVBOs(anim.model);
if (anim.gpuSkinningShaderData.skinningMatrices.empty()) {
if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty())
{
return false;
}
anim.gpuSkinningShaderData.ComputeSkinningMatrices(anim.model.startBones, anim.model.animations[0].keyFrames, static_cast<int>(anim.currentFrame));
if (anim.gpuSkinningShaderData.skinningMatrices.empty())
{
return false;
}
}
return true;
}
// ==================== Shadow depth pass ====================
void Character::drawShadowDepth(Renderer& renderer) {
@ -451,18 +417,17 @@ void Character::drawShadowDepthCpu(Renderer& renderer) {
}
void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
CheckGlError(__FILE__, __LINE__);
AnimationState drawState = resolveActiveState();
auto it = animations.find(drawState);
if (it == animations.end()) return;
auto& anim = it->second;
prepareGpuSkinningVBOs(anim);
if (anim.skinningMatrices.empty()) {
if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty()) return;
anim.model.ComputeSkinningMatrices(
anim.model.animations[0].keyFrames[0].frame, anim.skinningMatrices);
if (anim.skinningMatrices.empty()) return;
}
CheckGlError(__FILE__, __LINE__);
if (!prepareGpuSkinning()) return;
CheckGlError(__FILE__, __LINE__);
static const std::string shadowSkinningShader = "shadow_depth_skinning";
static const std::string boneMatricesUniform = "uBoneMatrices[0]";
@ -476,40 +441,19 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
renderer.ScaleMatrix(modelScale);
renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());
CheckGlError(__FILE__, __LINE__);
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
static_cast<int>(anim.skinningMatrices.size()), false,
anim.skinningMatrices[0].data());
static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
it->second.gpuSkinningShaderData.skinningMatrices[0].data());
#ifndef EMSCRIPTEN
#ifndef __ANDROID__
if (anim.bindPoseMutable.vao) {
glBindVertexArray(anim.bindPoseMutable.vao->getBuffer());
renderer.shaderManager.EnableVertexAttribArrays();
}
#endif
#endif
glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.positionVBO->getBuffer());
renderer.VertexAttribPointer3fv("vPosition", 0, NULL);
if (anim.bindPoseMutable.texCoordVBO) {
glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.texCoordVBO->getBuffer());
renderer.VertexAttribPointer2fv("vTexCoord", 0, NULL);
}
glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices0VBO->getBuffer());
renderer.VertexAttribPointer4fv("aBoneIndices0", 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices1VBO->getBuffer());
renderer.VertexAttribPointer2fv("aBoneIndices1", 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights0VBO->getBuffer());
renderer.VertexAttribPointer4fv("aBoneWeights0", 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights1VBO->getBuffer());
renderer.VertexAttribPointer2fv("aBoneWeights1", 0, NULL);
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(anim.bindPoseMutable.data.PositionData.size()));
CheckGlError(__FILE__, __LINE__);
it->second.gpuSkinningShaderData.RenderVBO(renderer);
CheckGlError(__FILE__, __LINE__);
renderer.PopMatrix();
renderer.shaderManager.PopShader();
CheckGlError(__FILE__, __LINE__);
}
// ==================== Main pass with shadows ====================
@ -565,14 +509,11 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
auto it = animations.find(drawState);
if (it == animations.end() || !texture) return;
auto& anim = it->second;
prepareGpuSkinningVBOs(anim);
if (anim.skinningMatrices.empty()) {
if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty()) return;
anim.model.ComputeSkinningMatrices(
anim.model.animations[0].keyFrames[0].frame, anim.skinningMatrices);
if (anim.skinningMatrices.empty()) return;
}
CheckGlError(__FILE__, __LINE__);
if (!prepareGpuSkinning()) return;
CheckGlError(__FILE__, __LINE__);
static const std::string skinningShadowShader = "skinning_shadow";
static const std::string boneMatricesUniform = "uBoneMatrices[0]";
@ -583,10 +524,14 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
renderer.RenderUniform3fv("uLightDir", lightDirCamera.data());
CheckGlError(__FILE__, __LINE__);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, shadowMapTex);
glActiveTexture(GL_TEXTURE0);
CheckGlError(__FILE__, __LINE__);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
@ -597,49 +542,24 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
renderer.ScaleMatrix(modelScale);
renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());
CheckGlError(__FILE__, __LINE__);
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
static_cast<int>(anim.skinningMatrices.size()), false,
anim.skinningMatrices[0].data());
static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
it->second.gpuSkinningShaderData.skinningMatrices[0].data());
CheckGlError(__FILE__, __LINE__);
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
#ifndef EMSCRIPTEN
#ifndef __ANDROID__
if (anim.bindPoseMutable.vao) {
glBindVertexArray(anim.bindPoseMutable.vao->getBuffer());
renderer.shaderManager.EnableVertexAttribArrays();
}
#endif
#endif
CheckGlError(__FILE__, __LINE__);
it->second.gpuSkinningShaderData.RenderVBO(renderer);
glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.positionVBO->getBuffer());
renderer.VertexAttribPointer3fv("vPosition", 0, NULL);
if (anim.bindPoseMutable.texCoordVBO) {
glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.texCoordVBO->getBuffer());
renderer.VertexAttribPointer2fv("vTexCoord", 0, NULL);
}
// Bind normals for diffuse lighting (skinning_shadow shader skins them)
if (anim.bindPoseMutable.normalVBO) {
glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.normalVBO->getBuffer());
renderer.VertexAttribPointer3fv("vNormal", 0, NULL);
}
glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices0VBO->getBuffer());
renderer.VertexAttribPointer4fv("aBoneIndices0", 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices1VBO->getBuffer());
renderer.VertexAttribPointer2fv("aBoneIndices1", 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights0VBO->getBuffer());
renderer.VertexAttribPointer4fv("aBoneWeights0", 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights1VBO->getBuffer());
renderer.VertexAttribPointer2fv("aBoneWeights1", 0, NULL);
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(anim.bindPoseMutable.data.PositionData.size()));
CheckGlError(__FILE__, __LINE__);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError(__FILE__, __LINE__);
}
} // namespace ZL

View File

@ -97,8 +97,8 @@ private:
// if the requested state has no loaded animation.
AnimationState resolveActiveState() const;
// GPU skinning: prepare per-animation VBOs (called lazily on first draw)
void prepareGpuSkinningVBOs(BoneAnimationData& anim);
bool prepareGpuSkinning();
// GPU skinning: draw using shader-based skinning
void drawGpuSkinning(Renderer& renderer);
// Shadow: draw into depth map (no texture, no projection push)

View File

@ -1,7 +1,6 @@
#include "Game.h"
#include "AnimatedModel.h"
#include "BoneAnimatedModel.h"
#include "planet/PlanetData.h"
#include "utils/Utils.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
@ -56,6 +55,7 @@ namespace ZL
: newTickCount(0)
, lastTickCount(0)
, menuManager(renderer)
, audioPlayer(std::make_unique<AudioPlayerAsync>())
{
}
@ -73,7 +73,7 @@ namespace ZL
Environment::computeProjectionDimensions();
ZL::BindOpenGlFunctions();
ZL::CheckGlError();
ZL::CheckGlError(__FILE__, __LINE__);
renderer.InitOpenGL();
#ifdef EMSCRIPTEN
@ -144,111 +144,12 @@ namespace ZL
std::cout << "Load resurces step 4" << std::endl;
roomTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/w/room005.png", CONST_ZIP_FILE));
roomMesh.data = LoadFromTextFile02("resources/w/room001.txt", CONST_ZIP_FILE);
roomMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitY())).toRotationMatrix());
roomMesh.RefreshVBO();
currentLocation = std::make_unique<Location>(renderer, inventory);
currentLocation->setup();
std::cout << "Load resurces step 5" << std::endl;
// Load static game objects
gameObjects = GameObjectLoader::loadAndCreateGameObjects("resources/config2/gameobjects.json", renderer, CONST_ZIP_FILE);
// Load interactive objects
interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects("resources/config2/gameobjects.json", renderer, CONST_ZIP_FILE);
auto violaTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE));
// Player (Viola)
player = std::make_unique<Character>();
/*
player->loadAnimation(AnimationState::STAND, "resources/w/gg/gg_stand_idle001.txt", CONST_ZIP_FILE);
player->loadAnimation(AnimationState::WALK, "resources/w/gg/gg_walking001.txt", CONST_ZIP_FILE);
player->loadAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/gg_stand_to_action002.txt", CONST_ZIP_FILE);
player->loadAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/gg_action_attack001.txt", CONST_ZIP_FILE);
player->loadAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/gg_action_idle001.txt", CONST_ZIP_FILE);
player->loadAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/gg_action_to_stand001.txt", CONST_ZIP_FILE);
*/
player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/gg_stand_idle001.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/gg_walking001.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/gg_stand_to_action002.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/gg_action_attack001.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/gg_action_idle001.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/gg_action_to_stand001.anim", CONST_ZIP_FILE);
player->setTexture(violaTexture);
player->walkSpeed = 3.0f;
player->rotationSpeed = 8.0f;
player->modelScale = 1.f;
player->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
//Eigen::Quaternionf::Identity();
/*Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5f, Eigen::Vector3f::UnitX())) * */
/*Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitZ()));*/
player->canAttack = true;
player->isPlayer = true;
std::cout << "Load resurces step 9" << std::endl;
// Load NPCs from JSON
npcs = GameObjectLoader::loadAndCreateNpcs("resources/config2/npcs.json", CONST_ZIP_FILE);
/*
auto defaultTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/default_skin001.png", CONST_ZIP_FILE));
auto npc01 = std::make_unique<Character>();
npc01->loadAnimation(AnimationState::STAND, "resources/w/default_idle002.txt", CONST_ZIP_FILE);
npc01->loadAnimation(AnimationState::WALK, "resources/w/default_walk001.txt", CONST_ZIP_FILE);
//npc01->loadAnimation(AnimationState::STAND, "resources/idleviola_uv010.txt", CONST_ZIP_FILE);
//npc01->loadAnimation(AnimationState::WALK, "resources/walkviola_uv010.txt", CONST_ZIP_FILE);
npc01->setTexture(defaultTexture);
npc01->walkSpeed = 1.5f;
npc01->rotationSpeed = 8.0f;
npc01->modelScale = 0.01f;
//npc01->modelScale = 0.1f;
npc01->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
std::cout << "Load resurces step 10" << std::endl;
npc01->position = Eigen::Vector3f(0.f, 0.f, -10.f);
npc01->setTarget(npc01->position);
npcs.push_back(std::move(npc01));
*/
auto ghostTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/ghost_skin001.png", CONST_ZIP_FILE));
std::cout << "Load resurces step 11" << std::endl;
auto npc02 = std::make_unique<Character>();
/*
npc02->loadAnimation(AnimationState::STAND, "resources/w/default_float001.txt", CONST_ZIP_FILE);
npc02->loadAnimation(AnimationState::WALK, "resources/w/default_float001.txt", CONST_ZIP_FILE);
npc02->loadAnimation(AnimationState::ACTION_IDLE, "resources/w/float_attack003_cut.txt", CONST_ZIP_FILE);
npc02->loadAnimation(AnimationState::ACTION_ATTACK, "resources/w/float_attack003.txt", CONST_ZIP_FILE);
npc02->loadAnimation(AnimationState::STAND_TO_ACTION, "resources/w/default_float001_cut.txt", CONST_ZIP_FILE);
npc02->loadAnimation(AnimationState::ACTION_TO_STAND, "resources/w/default_float001_cut.txt", CONST_ZIP_FILE);
*/
npc02->loadBinaryAnimation(AnimationState::STAND, "resources/w/default_float001.anim", CONST_ZIP_FILE);
npc02->loadBinaryAnimation(AnimationState::WALK, "resources/w/default_float001.anim", CONST_ZIP_FILE);
npc02->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/float_attack003_cut.anim", CONST_ZIP_FILE);
npc02->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/float_attack003.anim", CONST_ZIP_FILE);
npc02->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/default_float001_cut.anim", CONST_ZIP_FILE);
npc02->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/default_float001_cut.anim", CONST_ZIP_FILE);
//npc02->loadAnimation(AnimationState::STAND, "resources/w/float_attack003.txt", CONST_ZIP_FILE);
//npc02->loadAnimation(AnimationState::STAND, "resources/idleviola_uv010.txt", CONST_ZIP_FILE);
npc02->setTexture(ghostTexture);
npc02->walkSpeed = 1.5f;
npc02->rotationSpeed = 8.0f;
npc02->modelScale = 0.01f;
//npc02->modelScale = 0.1f;
npc02->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
npc02->position = Eigen::Vector3f(0.f, 0.f, -20.f);
npc02->setTarget(npc02->position);
npc02->canAttack = true;
npc02->attackTarget = player.get();
npcs.push_back(std::move(npc02));
std::cout << "Load resurces step 12" << std::endl;
// Shadow mapping shaders
@ -264,27 +165,6 @@ namespace ZL
renderer.shaderManager.AddShaderFromFiles("skinning_shadow", "resources/shaders/skinning_shadow.vertex", "resources/shaders/default_shadow_desktop.fragment", CONST_ZIP_FILE);
#endif
// Create shadow map (2048x2048, ortho size 40, near 0.1, far 100)
shadowMap = std::make_unique<ShadowMap>(2048, 40.0f, 0.1f, 100.0f);
shadowMap->setLightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f));
std::cout << "Shadow map initialized" << std::endl;
setupNavigation();
loadingCompleted = true;
dialogueSystem.init(renderer, CONST_ZIP_FILE);
dialogueSystem.loadDatabase("resources/dialogue/sample_dialogues.json");
/*dialogueSystem.addTriggerZone({
"ghost_room_trigger",
"test_line_dialogue",
Eigen::Vector3f(0.0f, 0.0f, -8.5f),
2.0f,
true,
false
});*/
scriptEngine.init(this);
std::cout << "Load resurces step 13" << std::endl;
@ -298,10 +178,25 @@ namespace ZL
menuManager.uiManager.setTextButtonCallback("inventory_button", [this](const std::string& name) {
std::cout << "[UI] Inventory button clicked" << std::endl;
//scriptEngine.callItemPickupCallback("toggle_inventory");
scriptEngine.showInventory(this);
//scriptEngine
//scriptEngine
this->menuManager.uiManager.setNodeVisible("inventory_items_panel", true);
this->menuManager.uiManager.setNodeVisible("close_inventory_button", true);
this->inventoryOpen = true;
// Update UI with current items
const auto& items = this->inventory.getItems();
std::string itemText;
if (items.empty()) {
itemText = "Inventory (Empty)";
}
else {
itemText = "Inventory (" + std::to_string(items.size()) + " items)\n\n";
for (size_t i = 0; i < items.size(); ++i) {
itemText += std::to_string(i + 1) + ". " + items[i].name + "\n";
}
}
this->menuManager.uiManager.setText("inventory_items_text", itemText);
});
menuManager.uiManager.setTextButtonCallback("close_inventory_button", [this](const std::string& name) {
@ -314,152 +209,18 @@ namespace ZL
catch (const std::exception& e) {
std::cerr << "Failed to load UI: " << e.what() << std::endl;
}
loadingCompleted = true;
if (audioPlayer->init()) {
audioPlayer->setMusicVolume(100);
audioPlayer->setSoundVolume(80);
std::cout << "Audio initialized successfully" << std::endl;
}
else {
std::cout << "Audio initialization failed" << std::endl;
}
void Game::setupNavigation()
{
std::vector<PathFinder::ObstacleMesh> obstacles;
obstacles.reserve(gameObjects.size() + interactiveObjects.size());
for (const auto& item : gameObjects) {
const LoadedGameObject& gameObj = item.second;
obstacles.push_back({ &gameObj.mesh.data, Eigen::Vector3f::Zero() });
}
for (const InteractiveObject& intObj : interactiveObjects) {
if (!intObj.isActive) {
continue;
}
obstacles.push_back({ &intObj.mesh.data, intObj.position });
}
navigation.build(obstacles, "resources/config2/navigation.json", CONST_ZIP_FILE);
#ifdef SHOW_PATH
buildDebugNavMeshes();
#endif
auto planner = [this](const Eigen::Vector3f& start, const Eigen::Vector3f& end) {
return navigation.findPath(start, end);
};
if (player) {
player->setPathPlanner(planner);
}
for (auto& npc : npcs) {
if (npc) {
npc->setPathPlanner(planner);
}
}
}
#ifdef SHOW_PATH
void Game::buildDebugNavMeshes()
{
debugNavMeshes.clear();
const auto& areas = navigation.getAreas();
float y = navigation.getFloorY() + 0.02f;
Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
for (const auto& area : areas) {
if (area.polygon.size() < 3) continue;
VertexRenderStruct mesh;
mesh.data = CreatePolygonFloor(area.polygon, y, red);
mesh.RefreshVBO();
debugNavMeshes.push_back(std::move(mesh));
}
}
void Game::drawDebugNavigation()
{
renderer.shaderManager.PushShader("defaultColor");
renderer.SetMatrix();
for (const auto& mesh : debugNavMeshes) {
renderer.DrawVertexRenderStruct(mesh);
}
renderer.shaderManager.PopShader();
renderer.SetMatrix();
}
#endif
InteractiveObject* Game::raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir) {
if (interactiveObjects.empty()) {
std::cout << "[RAYCAST] No interactive objects to check" << std::endl;
return nullptr;
}
std::cout << "[RAYCAST] Starting raycast with " << interactiveObjects.size() << " objects" << std::endl;
std::cout << "[RAYCAST] Ray origin: (" << rayOrigin.x() << ", " << rayOrigin.y() << ", " << rayOrigin.z() << ")" << std::endl;
std::cout << "[RAYCAST] Ray dir: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl;
float closestDistance = FLT_MAX;
InteractiveObject* closestObject = nullptr;
for (auto& intObj : interactiveObjects) {
std::cout << "[RAYCAST] Checking object: " << intObj.name << " (active: " << intObj.isActive << ")" << std::endl;
if (!intObj.isActive) {
std::cout << "[RAYCAST] -> Object inactive, skipping" << std::endl;
continue;
}
std::cout << "[RAYCAST] Position: (" << intObj.position.x() << ", " << intObj.position.y() << ", "
<< intObj.position.z() << "), Radius: " << intObj.interactionRadius << std::endl;
Eigen::Vector3f toObject = intObj.position - rayOrigin;
std::cout << "[RAYCAST] Vector to object: (" << toObject.x() << ", " << toObject.y() << ", " << toObject.z() << ")" << std::endl;
float distanceAlongRay = toObject.dot(rayDir);
std::cout << "[RAYCAST] Distance along ray: " << distanceAlongRay << std::endl;
if (distanceAlongRay < 0.1f) {
std::cout << "[RAYCAST] -> Object behind camera, skipping" << std::endl;
continue;
}
Eigen::Vector3f closestPointOnRay = rayOrigin + rayDir * distanceAlongRay;
float distToObject = (closestPointOnRay - intObj.position).norm();
std::cout << "[RAYCAST] Distance to object: " << distToObject
<< " (interaction radius: " << intObj.interactionRadius << ")" << std::endl;
if (distToObject <= intObj.interactionRadius && distanceAlongRay < closestDistance) {
std::cout << "[RAYCAST] *** HIT DETECTED! ***" << std::endl;
closestDistance = distanceAlongRay;
closestObject = &intObj;
}
}
if (closestObject) {
std::cout << "[RAYCAST] *** RAYCAST SUCCESS: Found object " << closestObject->name << " ***" << std::endl;
} else {
std::cout << "[RAYCAST] No objects hit" << std::endl;
}
return closestObject;
}
Character* Game::raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance) {
Character* closestNpc = nullptr;
float closestDist = maxDistance;
for (auto& npc : npcs) {
Eigen::Vector3f toNpc = npc->position - rayOrigin;
float distAlongRay = toNpc.dot(rayDir);
if (distAlongRay < 0.1f) continue;
Eigen::Vector3f closestPoint = rayOrigin + rayDir * distAlongRay;
float distToNpc = (closestPoint - npc->position).norm();
float radius = npc->modelScale * 50.0f;
if (distToNpc <= radius && distAlongRay < closestDist) {
closestDist = distAlongRay;
closestNpc = npc.get();
}
}
return closestNpc;
}
void Game::drawUI()
@ -474,7 +235,10 @@ namespace ZL
glEnable(GL_BLEND);
menuManager.uiManager.draw(renderer);
dialogueSystem.draw(renderer);
if (currentLocation)
{
currentLocation->dialogueSystem.draw(renderer);
}
glDisable(GL_BLEND);
renderer.shaderManager.PopShader();
@ -482,222 +246,7 @@ namespace ZL
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
CheckGlError();
}
void Game::drawGame()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
//renderer.TranslateMatrix({ 0, -6.f, 0 });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID());
renderer.DrawVertexRenderStruct(roomMesh);
for (auto& [name, gameObj] : gameObjects) {
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
/*
glBindTexture(GL_TEXTURE_2D, fireboxTexture->getTexID());
renderer.DrawVertexRenderStruct(fireboxMesh);
glBindTexture(GL_TEXTURE_2D, inaiTexture->getTexID());
renderer.DrawVertexRenderStruct(inaiMesh);
glBindTexture(GL_TEXTURE_2D, benchTexture->getTexID());
renderer.DrawVertexRenderStruct(benchMesh);
*/
for (auto& intObj : interactiveObjects) {
if (intObj.isActive) {
intObj.draw(renderer);
}
}
if (player) player->draw(renderer);
for (auto& npc : npcs) npc->draw(renderer);
#ifdef SHOW_PATH
drawDebugNavigation();
#endif
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
}
void Game::drawShadowDepthPass()
{
if (!shadowMap) return;
const Eigen::Vector3f& sceneCenter = player ? player->position : Eigen::Vector3f::Zero();
shadowMap->updateLightSpaceMatrix(sceneCenter);
shadowMap->bind();
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
// Use front-face culling during depth pass to reduce shadow acne on lit faces
//glCullFace(GL_FRONT);
glEnable(GL_CULL_FACE);
renderer.shaderManager.PushShader("shadow_depth");
// Set up light's orthographic projection
const Eigen::Matrix4f& lightProj = shadowMap->getLightProjectionMatrix();
const Eigen::Matrix4f& lightView = shadowMap->getLightViewMatrix();
// Push the light's projection matrix via the 6-param ortho overload won't
// match our pre-computed matrix. Instead, use the raw stack approach:
// push a dummy projection then overwrite via PushSpecialMatrix-style.
// Simpler: push ortho then push the light view as modelview.
renderer.PushProjectionMatrix(
-40.0f, 40.0f,
-40.0f, 40.0f,
0.1f, 100.0f);
const Eigen::Vector3f& lightDir = shadowMap->getLightDirection();
Eigen::Vector3f lightPos = sceneCenter - lightDir * 50.0f;
Eigen::Vector3f up(0.0f, 1.0f, 0.0f);
if (std::abs(lightDir.dot(up)) > 0.99f) {
up = Eigen::Vector3f(0.0f, 0.0f, 1.0f);
}
// Build the light view matrix and push it
renderer.PushSpecialMatrix(lightView);
// Draw static geometry
renderer.DrawVertexRenderStruct(roomMesh);
for (auto& [name, gameObj] : gameObjects) {
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
for (auto& intObj : interactiveObjects) {
if (intObj.isActive && intObj.texture) {
renderer.PushMatrix();
renderer.TranslateMatrix(intObj.position);
renderer.DrawVertexRenderStruct(intObj.mesh);
renderer.PopMatrix();
}
}
// Draw characters (they handle their own skinning shader switch internally)
if (player) player->drawShadowDepth(renderer);
for (auto& npc : npcs) npc->drawShadowDepth(renderer);
renderer.PopMatrix(); // light view
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
//glCullFace(GL_BACK);
glDisable(GL_CULL_FACE);
shadowMap->unbind();
}
void Game::drawGameWithShadows()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
#ifdef DEBUG_LIGHT
// Debug mode: render from the light's point of view using the plain
// textured shader so we can see what the shadow map "sees".
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushProjectionMatrix(
-40.0f, 40.0f,
-40.0f, 40.0f,
0.1f, 1000.0f);
renderer.PushSpecialMatrix(shadowMap->getLightViewMatrix());
#else
static const std::string shadowShaderName = "default_shadow";
renderer.shaderManager.PushShader(shadowShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform1i("uShadowMap", 1);
// Bind shadow map texture to unit 1
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, shadowMap->getDepthTexture());
glActiveTexture(GL_TEXTURE0);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
// Capture the camera view matrix and compute uLightFromCamera
cameraViewMatrix = renderer.GetCurrentModelViewMatrix();
Eigen::Matrix4f cameraViewInverse = cameraViewMatrix.inverse();
Eigen::Matrix4f lightFromCamera = shadowMap->getLightSpaceMatrix() * cameraViewInverse;
renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
// Light direction in camera space for diffuse lighting
Eigen::Vector3f lightDirCamera = cameraViewMatrix.block<3,3>(0,0) * shadowMap->getLightDirection();
renderer.RenderUniform3fv("uLightDir", lightDirCamera.data());
#endif
glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID());
renderer.DrawVertexRenderStruct(roomMesh);
for (auto& [name, gameObj] : gameObjects) {
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
for (auto& intObj : interactiveObjects) {
if (intObj.isActive) {
intObj.draw(renderer);
}
}
#ifdef DEBUG_LIGHT
// In debug-light mode characters use the plain shaders (draw normally
// but from the light's viewpoint — projection/view already on stack).
if (player) player->draw(renderer);
for (auto& npc : npcs) npc->draw(renderer);
#else
// Characters use their own shadow-aware shaders
if (player) player->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
for (auto& npc : npcs) npc->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
#endif
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError(__FILE__, __LINE__);
}
void Game::drawScene() {
@ -707,15 +256,27 @@ namespace ZL
}
else
{
if (shadowMap) {
drawShadowDepthPass();
drawGameWithShadows();
} else {
drawGame();
if (currentLocation)
{
if (currentLocation->shadowMap) {
CheckGlError(__FILE__, __LINE__);
currentLocation->drawShadowDepthPass();
CheckGlError(__FILE__, __LINE__);
currentLocation->drawGameWithShadows();
CheckGlError(__FILE__, __LINE__);
}
else {
currentLocation->drawGame();
CheckGlError(__FILE__, __LINE__);
}
}
else
{
// ??? Main menu???
}
drawUI();
}
CheckGlError();
CheckGlError(__FILE__, __LINE__);
}
void Game::drawLoading()
@ -742,7 +303,7 @@ namespace ZL
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError();
CheckGlError(__FILE__, __LINE__);
}
@ -773,46 +334,16 @@ namespace ZL
lastTickCount = newTickCount;
if (player) {
player->update(delta);
dialogueSystem.update(static_cast<int>(delta), player->position);
}
for (auto& npc : npcs) npc->update(delta);
// Check if player reached target interactive object
if (targetInteractiveObject && player) {
float distToObject = (player->position - targetInteractiveObject->position).norm();
// If player is close enough to pick up the item
if (distToObject <= targetInteractiveObject->interactionRadius + 1.0f) {
std::cout << "[PICKUP] Player reached object! Distance: " << distToObject << std::endl;
std::cout << "[PICKUP] Calling Lua callback for: " << targetInteractiveObject->id << std::endl;
// Call custom activate function if specified, otherwise use fallback
try {
if (!targetInteractiveObject->activateFunctionName.empty()) {
std::cout << "[PICKUP] Using custom function: " << targetInteractiveObject->activateFunctionName << std::endl;
scriptEngine.callActivateFunction(targetInteractiveObject->activateFunctionName);
}
else {
std::cout << "[PICKUP] Using fallback callback" << std::endl;
scriptEngine.callItemPickupCallback(targetInteractiveObject->id);
}
}
catch (const std::exception& e) {
std::cerr << "[PICKUP] Error calling function: " << e.what() << std::endl;
if (currentLocation)
{
currentLocation->update(delta);
}
targetInteractiveObject = nullptr;
}
}
}
}
void Game::render() {
ZL::CheckGlError();
ZL::CheckGlError(__FILE__, __LINE__);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
@ -895,97 +426,16 @@ namespace ZL
std::cout << "\n========== MOUSE DOWN EVENT ==========" << std::endl;
handleDown(ZL::UiManager::MOUSE_FINGER_ID, mx, my);
player->attackTarget = nullptr;
if (menuManager.uiManager.isUiInteractionForFinger(ZL::UiManager::MOUSE_FINGER_ID)) {
std::cout << "[CLICK] UI handled, skipping character movement" << std::endl;
continue;
}
// Calculate ray for picking
if (dialogueSystem.blocksGameplayInput()) {
dialogueSystem.handlePointerReleased(static_cast<float>(mx), Environment::projectionHeight - static_cast<float>(my));
continue;
}
// Unproject click to ground plane (y=0) for Viola's walk target
float ndcX = 2.0f * event.button.x / Environment::width - 1.0f;
float ndcY = 1.0f - 2.0f * event.button.y / Environment::height;
float aspect = (float)Environment::width / (float)Environment::height;
float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f);
float cosAzim = cos(cameraAzimuth), sinAzim = sin(cameraAzimuth);
float cosIncl = cos(cameraInclination), sinIncl = sin(cameraInclination);
Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim);
Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl);
Eigen::Vector3f camUp(sinAzim * sinIncl, cosIncl, -cosAzim * sinIncl);
const Eigen::Vector3f& playerPos = player ? player->position : Eigen::Vector3f::Zero();
Eigen::Vector3f camPos = playerPos + Eigen::Vector3f(-sinAzim * cosIncl, sinIncl, cosAzim * cosIncl) * Environment::zoom;
Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized();
std::cout << "[CLICK] Camera position: (" << camPos.x() << ", " << camPos.y() << ", " << camPos.z() << ")" << std::endl;
std::cout << "[CLICK] Ray direction: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl;
// First check if we clicked on interactive object
InteractiveObject* clickedObject = raycastInteractiveObjects(camPos, rayDir);
if (clickedObject && player && clickedObject->isActive) {
std::cout << "[CLICK] *** SUCCESS: Clicked on interactive object: " << clickedObject->name << " ***" << std::endl;
std::cout << "[CLICK] Object position: (" << clickedObject->position.x() << ", "
<< clickedObject->position.y() << ", " << clickedObject->position.z() << ")" << std::endl;
std::cout << "[CLICK] Player position: (" << player->position.x() << ", "
<< player->position.y() << ", " << player->position.z() << ")" << std::endl;
targetInteractiveObject = clickedObject;
player->setTarget(clickedObject->position);
std::cout << "[CLICK] Player moving to object..." << std::endl;
}
else {
// Check if we clicked on an NPC
Character* clickedNpc = raycastNpcs(camPos, rayDir);
if (clickedNpc && player) {
float distance = (player->position - clickedNpc->position).norm();
int npcIndex = -1;
for (size_t i = 0; i < npcs.size(); ++i) {
if (npcs[i].get() == clickedNpc) {
npcIndex = static_cast<int>(i);
break;
}
}
if (npcIndex != -1) {
if (distance <= clickedNpc->interactionRadius) {
std::cout << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " ***" << std::endl;
scriptEngine.callNpcInteractCallback(npcIndex);
}
else {
std::cout << "[CLICK] Too far from NPC (distance " << distance
<< " > " << clickedNpc->interactionRadius << ")" << std::endl;
}
if (clickedNpc->canAttack)
if (currentLocation)
{
player->attackTarget = clickedNpc;
currentLocation->handleDown(ZL::UiManager::MOUSE_FINGER_ID, event.button.x, event.button.y, mx, my);
}
}
}
else if (rayDir.y() < -0.001f && player) {
// Otherwise, unproject click to ground plane for Viola's walk target
float t = -camPos.y() / rayDir.y();
Eigen::Vector3f hit = camPos + rayDir * t;
std::cout << "[CLICK] Clicked on ground at: (" << hit.x() << ", " << hit.z() << ")" << std::endl;
if (player->currentState == AnimationState::STAND || player->currentState == AnimationState::WALK)
{
player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z()));
}
}
else {
std::cout << "[CLICK] No valid target found" << std::endl;
}
}
std::cout << "========================================\n" << std::endl;
///.....
} else {
handleUp(ZL::UiManager::MOUSE_FINGER_ID, mx, my);
}
@ -995,9 +445,19 @@ namespace ZL
rightMouseDown = true;
lastMouseX = event.button.x;
lastMouseY = event.button.y;
if (currentLocation)
{
currentLocation->rightMouseDown = true;
currentLocation->lastMouseX = event.button.x;
currentLocation->lastMouseY = event.button.y;
}
}
else {
rightMouseDown = false;
if (currentLocation)
{
currentLocation->rightMouseDown = false;
}
}
}
}
@ -1005,20 +465,9 @@ namespace ZL
int mx = static_cast<int>((float)event.motion.x / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((float)event.motion.y / Environment::height * Environment::projectionHeight);
handleMotion(ZL::UiManager::MOUSE_FINGER_ID, mx, my);
if (rightMouseDown) {
int dx = event.motion.x - lastMouseX;
int dy = event.motion.y - lastMouseY;
lastMouseX = event.motion.x;
lastMouseY = event.motion.y;
const float sensitivity = 0.005f;
cameraAzimuth += dx * sensitivity;
cameraInclination += dy * sensitivity;
const float minInclination = M_PI * 30.f / 180.f;
const float maxInclination = M_PI * 0.5f;
cameraInclination = max(minInclination, min(maxInclination, cameraInclination));
if (currentLocation)
{
currentLocation->handleMotion(ZL::UiManager::MOUSE_FINGER_ID, event.motion.x, event.motion.y, mx, my);
}
}
@ -1035,36 +484,29 @@ namespace ZL
}
}
if (event.type == SDL_KEYDOWN && dialogueSystem.handleKeyDown(event.key.keysym.sym)) {
continue;
}
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
switch (event.key.keysym.sym) {
case SDLK_1:
if (audioPlayer) audioPlayer->playSoundAsync("audio/background.wav");
break;
case SDLK_2:
if (audioPlayer) audioPlayer->playMusicAsync("audio/lullaby-music-vol20-186394--online-audio-convert.com.ogg");
break;
case SDLK_3:
if (audioPlayer) audioPlayer->stopMusicAsync();
break;
case SDLK_f:
dialogueSystem.startDialogue("test_cutscene_pan_dialogue_silent");
currentLocation->dialogueSystem.startDialogue("test_choice_dialogue");
break;
case SDLK_e:
dialogueSystem.startDialogue("test_cutscene_pan_dialogue");
currentLocation->dialogueSystem.startDialogue("test_cutscene_pan_dialogue");
break;
case SDLK_p:
if (player->battle_state == 0)
{
player->battle_state = 1;
}
else
{
player->battle_state = 0;
}
break;
case SDLK_l:
if (player->attack == 0)
{
player->attack = 1;
}
break;
case SDLK_RETURN:
@ -1094,21 +536,7 @@ namespace ZL
}
if (event.type == SDL_KEYUP) {
/*benchMesh.data.Move({-x, -y, 0});
if (event.key.keysym.sym == SDLK_a) {
x = x - 0.1;
}
if (event.key.keysym.sym == SDLK_d) {
x = x + 0.1;
}
if (event.key.keysym.sym == SDLK_w) {
y = y - 0.1;
}
if (event.key.keysym.sym == SDLK_s) {
y = y + 0.1;
}
benchMesh.data.Move({ x, y, 0 });
benchMesh.RefreshVBO();*/
}
#endif
}
@ -1153,25 +581,4 @@ namespace ZL
}
bool Game::requestDialogueStart(const std::string& dialogueId)
{
return dialogueSystem.startDialogue(dialogueId);
}
void Game::setDialogueFlag(const std::string& flag, int value)
{
dialogueSystem.setFlag(flag, value);
}
int Game::getDialogueFlag(const std::string& flag) const
{
return dialogueSystem.getFlag(flag);
}
bool Game::setNavigationAreaAvailable(const std::string& areaName, bool available)
{
return navigation.setAreaAvailable(areaName, available);
}
} // namespace ZL

View File

@ -5,9 +5,7 @@
#include "Environment.h"
#include "render/TextureManager.h"
#include "SparkEmitter.h"
#include "planet/PlanetObject.h"
#include "UiManager.h"
#include "Projectile.h"
#include "utils/TaskManager.h"
#include "items/GameObjectLoader.h"
#include "items/Item.h"
@ -19,12 +17,10 @@
#include <cstdint>
#include <render/TextRenderer.h>
#include "MenuManager.h"
#include "ScriptEngine.h"
#include <unordered_map>
#include "dialogue/DialogueSystem.h"
#include "render/ShadowMap.h"
#include "navigation/PathFinder.h"
#include <unordered_set>
#include "Location.h"
#include "AudioPlayerAsync.h"
namespace ZL {
@ -39,86 +35,40 @@ namespace ZL {
void render();
bool shouldExit() const { return Environment::exitGameLoop; }
bool requestDialogueStart(const std::string& dialogueId);
void setDialogueFlag(const std::string& flag, int value);
int getDialogueFlag(const std::string& flag) const;
bool setNavigationAreaAvailable(const std::string& areaName, bool available);
Renderer renderer;
TaskManager taskManager;
MainThreadHandler mainThreadHandler;
//std::unique_ptr<INetworkClient> networkClient;
std::shared_ptr<Texture> loadingTexture;
VertexRenderStruct loadingMesh;
bool loadingCompleted = false;
std::shared_ptr<Location> currentLocation;
std::shared_ptr<Texture> roomTexture;
VertexRenderStruct roomMesh;
std::unordered_map<std::string, LoadedGameObject> gameObjects;
/*
std::shared_ptr<Texture> fireboxTexture;
VertexRenderStruct fireboxMesh;
std::shared_ptr<Texture> inaiTexture;
VertexRenderStruct inaiMesh;
std::shared_ptr<Texture> benchTexture;
VertexRenderStruct benchMesh;
*/
std::vector<InteractiveObject> interactiveObjects;
Inventory inventory;
InteractiveObject* pickedUpObject = nullptr;
// Interactive object targeting
InteractiveObject* targetInteractiveObject = nullptr;
bool inventoryOpen = false;
std::unique_ptr<Character> player;
std::vector<std::unique_ptr<Character>> npcs;
float cameraAzimuth = 0.0f;
float cameraInclination = M_PI * 30.f / 180.f;
// Public access for ScriptEngine
MenuManager menuManager;
ScriptEngine scriptEngine;
PathFinder navigation;
#ifdef SHOW_PATH
std::vector<VertexRenderStruct> debugNavMeshes;
#endif
private:
bool rightMouseDown = false;
int lastMouseX = 0;
int lastMouseY = 0;
static constexpr float CAMERA_FOV_Y = 1.0f / 1.5f;
std::unique_ptr<AudioPlayerAsync> audioPlayer;
int64_t getSyncTimeMs();
void processTickCount();
void drawScene();
void drawUI();
void drawGame();
void drawLoading();
void drawShadowDepthPass();
void drawGameWithShadows();
void setupNavigation();
void handleDown(int64_t fingerId, int mx, int my);
void handleUp(int64_t fingerId, int mx, int my);
void handleMotion(int64_t fingerId, int mx, int my);
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
#ifdef SHOW_PATH
void buildDebugNavMeshes();
void drawDebugNavigation();
#endif
#ifdef EMSCRIPTEN
static Game* s_instance;
@ -128,18 +78,9 @@ namespace ZL {
int64_t newTickCount;
int64_t lastTickCount;
uint32_t connectingStartTicks = 0;
static constexpr uint32_t CONNECTING_TIMEOUT_MS = 10000;
static const size_t CONST_TIMER_INTERVAL = 10;
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
//MenuManager menuManager;
Dialogue::DialogueSystem dialogueSystem;
//ScriptEngine scriptEngine;
std::unique_ptr<ShadowMap> shadowMap;
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
};

670
src/Location.cpp Normal file
View File

@ -0,0 +1,670 @@
#include "Location.h"
#include "utils/Utils.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#include "render/TextureManager.h"
#include "TextModel.h"
#include <random>
#include <cmath>
#include <algorithm>
#include <functional>
#include <memory>
#include <cfloat>
#include "GameConstants.h"
namespace ZL
{
extern const char* CONST_ZIP_FILE;
static constexpr float CAMERA_FOV_Y = 1.0f / 1.5f;
Location::Location(Renderer& iRenderer, Inventory& iInventory)
: renderer(iRenderer)
, inventory(iInventory)
{
}
void Location::setup()
{
roomTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/w/room005.png", CONST_ZIP_FILE));
roomMesh.data = LoadFromTextFile02("resources/w/room001.txt", CONST_ZIP_FILE);
roomMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitY())).toRotationMatrix());
roomMesh.RefreshVBO();
// Load static game objects
gameObjects = GameObjectLoader::loadAndCreateGameObjects("resources/config2/gameobjects.json", renderer, CONST_ZIP_FILE);
// Load interactive objects
interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects("resources/config2/gameobjects.json", renderer, CONST_ZIP_FILE);
auto playerTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE));
player = std::make_unique<Character>();
player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/gg_stand_idle001.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/gg_walking001.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/gg_stand_to_action002.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/gg_action_attack001.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/gg_action_idle001.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/gg_action_to_stand001.anim", CONST_ZIP_FILE);
player->setTexture(playerTexture);
player->walkSpeed = 3.0f;
player->rotationSpeed = 8.0f;
player->modelScale = 1.f;
player->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
player->canAttack = true;
player->isPlayer = true;
std::cout << "Load resurces step 9" << std::endl;
// Load NPCs from JSON
npcs = GameObjectLoader::loadAndCreateNpcs("resources/config2/npcs.json", CONST_ZIP_FILE);
auto ghostTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/ghost_skin001.png", CONST_ZIP_FILE));
std::cout << "Load resurces step 11" << std::endl;
auto npc02 = std::make_unique<Character>();
npc02->loadBinaryAnimation(AnimationState::STAND, "resources/w/default_float001.anim", CONST_ZIP_FILE);
npc02->loadBinaryAnimation(AnimationState::WALK, "resources/w/default_float001.anim", CONST_ZIP_FILE);
npc02->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/float_attack003_cut.anim", CONST_ZIP_FILE);
npc02->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/float_attack003.anim", CONST_ZIP_FILE);
npc02->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/default_float001_cut.anim", CONST_ZIP_FILE);
npc02->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/default_float001_cut.anim", CONST_ZIP_FILE);
npc02->setTexture(ghostTexture);
npc02->walkSpeed = 1.5f;
npc02->rotationSpeed = 8.0f;
npc02->modelScale = 0.01f;
//npc02->modelScale = 0.1f;
npc02->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
npc02->position = Eigen::Vector3f(0.f, 0.f, -20.f);
npc02->setTarget(npc02->position);
npc02->canAttack = true;
npc02->attackTarget = player.get();
npcs.push_back(std::move(npc02));
// Create shadow map (2048x2048, ortho size 40, near 0.1, far 100)
shadowMap = std::make_unique<ShadowMap>(2048, 40.0f, 0.1f, 100.0f);
shadowMap->setLightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f));
std::cout << "Shadow map initialized" << std::endl;
setupNavigation();
scriptEngine.init(this, &inventory);
dialogueSystem.init(renderer, CONST_ZIP_FILE);
dialogueSystem.loadDatabase("resources/dialogue/sample_dialogues.json");
/*dialogueSystem.addTriggerZone({
"ghost_room_trigger",
"test_line_dialogue",
Eigen::Vector3f(0.0f, 0.0f, -8.5f),
2.0f,
true,
false
});*/
}
void Location::setupNavigation()
{
std::vector<PathFinder::ObstacleMesh> obstacles;
obstacles.reserve(gameObjects.size() + interactiveObjects.size());
for (const auto& item : gameObjects) {
const LoadedGameObject& gameObj = item.second;
obstacles.push_back({ &gameObj.mesh.data, Eigen::Vector3f::Zero() });
}
for (const InteractiveObject& intObj : interactiveObjects) {
if (!intObj.isActive) {
continue;
}
obstacles.push_back({ &intObj.mesh.data, intObj.position });
}
navigation.build(obstacles, "resources/config2/navigation.json", CONST_ZIP_FILE);
#ifdef SHOW_PATH
buildDebugNavMeshes();
#endif
auto planner = [this](const Eigen::Vector3f& start, const Eigen::Vector3f& end) {
return navigation.findPath(start, end);
};
if (player) {
player->setPathPlanner(planner);
}
for (auto& npc : npcs) {
if (npc) {
npc->setPathPlanner(planner);
}
}
}
#ifdef SHOW_PATH
void Location::buildDebugNavMeshes()
{
debugNavMeshes.clear();
const auto& areas = navigation.getAreas();
float y = navigation.getFloorY() + 0.02f;
Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
for (const auto& area : areas) {
if (area.polygon.size() < 3) continue;
VertexRenderStruct mesh;
mesh.data = CreatePolygonFloor(area.polygon, y, red);
mesh.RefreshVBO();
debugNavMeshes.push_back(std::move(mesh));
}
}
void Location::drawDebugNavigation()
{
renderer.shaderManager.PushShader("defaultColor");
renderer.SetMatrix();
for (const auto& mesh : debugNavMeshes) {
renderer.DrawVertexRenderStruct(mesh);
}
renderer.shaderManager.PopShader();
renderer.SetMatrix();
}
#endif
InteractiveObject* Location::raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir) {
if (interactiveObjects.empty()) {
std::cout << "[RAYCAST] No interactive objects to check" << std::endl;
return nullptr;
}
std::cout << "[RAYCAST] Starting raycast with " << interactiveObjects.size() << " objects" << std::endl;
std::cout << "[RAYCAST] Ray origin: (" << rayOrigin.x() << ", " << rayOrigin.y() << ", " << rayOrigin.z() << ")" << std::endl;
std::cout << "[RAYCAST] Ray dir: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl;
float closestDistance = FLT_MAX;
InteractiveObject* closestObject = nullptr;
for (auto& intObj : interactiveObjects) {
std::cout << "[RAYCAST] Checking object: " << intObj.name << " (active: " << intObj.isActive << ")" << std::endl;
if (!intObj.isActive) {
std::cout << "[RAYCAST] -> Object inactive, skipping" << std::endl;
continue;
}
std::cout << "[RAYCAST] Position: (" << intObj.position.x() << ", " << intObj.position.y() << ", "
<< intObj.position.z() << "), Radius: " << intObj.interactionRadius << std::endl;
Eigen::Vector3f toObject = intObj.position - rayOrigin;
std::cout << "[RAYCAST] Vector to object: (" << toObject.x() << ", " << toObject.y() << ", " << toObject.z() << ")" << std::endl;
float distanceAlongRay = toObject.dot(rayDir);
std::cout << "[RAYCAST] Distance along ray: " << distanceAlongRay << std::endl;
if (distanceAlongRay < 0.1f) {
std::cout << "[RAYCAST] -> Object behind camera, skipping" << std::endl;
continue;
}
Eigen::Vector3f closestPointOnRay = rayOrigin + rayDir * distanceAlongRay;
float distToObject = (closestPointOnRay - intObj.position).norm();
std::cout << "[RAYCAST] Distance to object: " << distToObject
<< " (interaction radius: " << intObj.interactionRadius << ")" << std::endl;
if (distToObject <= intObj.interactionRadius && distanceAlongRay < closestDistance) {
std::cout << "[RAYCAST] *** HIT DETECTED! ***" << std::endl;
closestDistance = distanceAlongRay;
closestObject = &intObj;
}
}
if (closestObject) {
std::cout << "[RAYCAST] *** RAYCAST SUCCESS: Found object " << closestObject->name << " ***" << std::endl;
}
else {
std::cout << "[RAYCAST] No objects hit" << std::endl;
}
return closestObject;
}
Character* Location::raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance) {
Character* closestNpc = nullptr;
float closestDist = maxDistance;
for (auto& npc : npcs) {
Eigen::Vector3f toNpc = npc->position - rayOrigin;
float distAlongRay = toNpc.dot(rayDir);
if (distAlongRay < 0.1f) continue;
Eigen::Vector3f closestPoint = rayOrigin + rayDir * distAlongRay;
float distToNpc = (closestPoint - npc->position).norm();
float radius = npc->modelScale * 50.0f;
if (distToNpc <= radius && distAlongRay < closestDist) {
closestDist = distAlongRay;
closestNpc = npc.get();
}
}
return closestNpc;
}
void Location::drawGame()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
//renderer.TranslateMatrix({ 0, -6.f, 0 });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID());
renderer.DrawVertexRenderStruct(roomMesh);
for (auto& [name, gameObj] : gameObjects) {
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
/*
glBindTexture(GL_TEXTURE_2D, fireboxTexture->getTexID());
renderer.DrawVertexRenderStruct(fireboxMesh);
glBindTexture(GL_TEXTURE_2D, inaiTexture->getTexID());
renderer.DrawVertexRenderStruct(inaiMesh);
glBindTexture(GL_TEXTURE_2D, benchTexture->getTexID());
renderer.DrawVertexRenderStruct(benchMesh);
*/
for (auto& intObj : interactiveObjects) {
if (intObj.isActive) {
intObj.draw(renderer);
}
}
if (player) player->draw(renderer);
for (auto& npc : npcs) npc->draw(renderer);
#ifdef SHOW_PATH
drawDebugNavigation();
#endif
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
}
void Location::drawShadowDepthPass()
{
if (!shadowMap) return;
const Eigen::Vector3f& sceneCenter = player ? player->position : Eigen::Vector3f::Zero();
shadowMap->updateLightSpaceMatrix(sceneCenter);
shadowMap->bind();
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
// Use front-face culling during depth pass to reduce shadow acne on lit faces
//glCullFace(GL_FRONT);
glEnable(GL_CULL_FACE);
renderer.shaderManager.PushShader("shadow_depth");
// Set up light's orthographic projection
const Eigen::Matrix4f& lightProj = shadowMap->getLightProjectionMatrix();
const Eigen::Matrix4f& lightView = shadowMap->getLightViewMatrix();
// Push the light's projection matrix via the 6-param ortho overload won't
// match our pre-computed matrix. Instead, use the raw stack approach:
// push a dummy projection then overwrite via PushSpecialMatrix-style.
// Simpler: push ortho then push the light view as modelview.
renderer.PushProjectionMatrix(
-40.0f, 40.0f,
-40.0f, 40.0f,
0.1f, 100.0f);
const Eigen::Vector3f& lightDir = shadowMap->getLightDirection();
Eigen::Vector3f lightPos = sceneCenter - lightDir * 50.0f;
Eigen::Vector3f up(0.0f, 1.0f, 0.0f);
if (std::abs(lightDir.dot(up)) > 0.99f) {
up = Eigen::Vector3f(0.0f, 0.0f, 1.0f);
}
// Build the light view matrix and push it
renderer.PushSpecialMatrix(lightView);
// Draw static geometry
renderer.DrawVertexRenderStruct(roomMesh);
for (auto& [name, gameObj] : gameObjects) {
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
for (auto& intObj : interactiveObjects) {
if (intObj.isActive && intObj.texture) {
renderer.PushMatrix();
renderer.TranslateMatrix(intObj.position);
renderer.DrawVertexRenderStruct(intObj.mesh);
renderer.PopMatrix();
}
}
// Draw characters (they handle their own skinning shader switch internally)
if (player) player->drawShadowDepth(renderer);
for (auto& npc : npcs) npc->drawShadowDepth(renderer);
renderer.PopMatrix(); // light view
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
//glCullFace(GL_BACK);
glDisable(GL_CULL_FACE);
shadowMap->unbind();
}
void Location::drawGameWithShadows()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
#ifdef DEBUG_LIGHT
// Debug mode: render from the light's point of view using the plain
// textured shader so we can see what the shadow map "sees".
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushProjectionMatrix(
-40.0f, 40.0f,
-40.0f, 40.0f,
0.1f, 1000.0f);
renderer.PushSpecialMatrix(shadowMap->getLightViewMatrix());
#else
static const std::string shadowShaderName = "default_shadow";
renderer.shaderManager.PushShader(shadowShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform1i("uShadowMap", 1);
// Bind shadow map texture to unit 1
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, shadowMap->getDepthTexture());
glActiveTexture(GL_TEXTURE0);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
// Capture the camera view matrix and compute uLightFromCamera
cameraViewMatrix = renderer.GetCurrentModelViewMatrix();
Eigen::Matrix4f cameraViewInverse = cameraViewMatrix.inverse();
Eigen::Matrix4f lightFromCamera = shadowMap->getLightSpaceMatrix() * cameraViewInverse;
renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
// Light direction in camera space for diffuse lighting
Eigen::Vector3f lightDirCamera = cameraViewMatrix.block<3, 3>(0, 0) * shadowMap->getLightDirection();
renderer.RenderUniform3fv("uLightDir", lightDirCamera.data());
#endif
CheckGlError(__FILE__, __LINE__);
glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID());
renderer.DrawVertexRenderStruct(roomMesh);
CheckGlError(__FILE__, __LINE__);
for (auto& [name, gameObj] : gameObjects) {
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
CheckGlError(__FILE__, __LINE__);
for (auto& intObj : interactiveObjects) {
if (intObj.isActive) {
intObj.draw(renderer);
}
}
CheckGlError(__FILE__, __LINE__);
#ifdef DEBUG_LIGHT
// In debug-light mode characters use the plain shaders (draw normally
// but from the light's viewpoint — projection/view already on stack).
if (player) player->draw(renderer);
for (auto& npc : npcs) npc->draw(renderer);
#else
// Characters use their own shadow-aware shaders
CheckGlError(__FILE__, __LINE__);
if (player) player->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
CheckGlError(__FILE__, __LINE__);
for (auto& npc : npcs) npc->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
#endif
CheckGlError(__FILE__, __LINE__);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
}
bool Location::setNavigationAreaAvailable(const std::string& areaName, bool available)
{
return navigation.setAreaAvailable(areaName, available);
}
void Location::update(int64_t delta)
{
if (player) {
player->update(delta);
dialogueSystem.update(static_cast<int>(delta), player->position);
}
for (auto& npc : npcs) npc->update(delta);
// Check if player reached target interactive object
if (targetInteractiveObject && player) {
float distToObject = (player->position - targetInteractiveObject->position).norm();
// If player is close enough to pick up the item
if (distToObject <= targetInteractiveObject->interactionRadius + 1.0f) {
std::cout << "[PICKUP] Player reached object! Distance: " << distToObject << std::endl;
std::cout << "[PICKUP] Calling Lua callback for: " << targetInteractiveObject->id << std::endl;
// Call custom activate function if specified, otherwise use fallback
try {
if (!targetInteractiveObject->activateFunctionName.empty()) {
std::cout << "[PICKUP] Using custom function: " << targetInteractiveObject->activateFunctionName << std::endl;
scriptEngine.callActivateFunction(targetInteractiveObject->activateFunctionName);
}
else {
std::cout << "[PICKUP] Using fallback callback" << std::endl;
scriptEngine.callItemPickupCallback(targetInteractiveObject->id);
}
}
catch (const std::exception& e) {
std::cerr << "[PICKUP] Error calling function: " << e.what() << std::endl;
}
targetInteractiveObject = nullptr;
}
}
}
void Location::handleDown(int64_t fingerId, int eventX, int eventY, int mx, int my)
{
// Calculate ray for picking
if (dialogueSystem.blocksGameplayInput()) {
dialogueSystem.handlePointerReleased(static_cast<float>(mx), Environment::projectionHeight - static_cast<float>(my));
return;
}
player->attackTarget = nullptr;
// Unproject click to ground plane (y=0) for Viola's walk target
float ndcX = 2.0f * eventX / Environment::width - 1.0f;
float ndcY = 1.0f - 2.0f * eventY / Environment::height;
float aspect = (float)Environment::width / (float)Environment::height;
float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f);
float cosAzim = cos(cameraAzimuth), sinAzim = sin(cameraAzimuth);
float cosIncl = cos(cameraInclination), sinIncl = sin(cameraInclination);
Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim);
Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl);
Eigen::Vector3f camUp(sinAzim * sinIncl, cosIncl, -cosAzim * sinIncl);
const Eigen::Vector3f& playerPos = player ? player->position : Eigen::Vector3f::Zero();
Eigen::Vector3f camPos = playerPos + Eigen::Vector3f(-sinAzim * cosIncl, sinIncl, cosAzim * cosIncl) * Environment::zoom;
Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized();
std::cout << "[CLICK] Camera position: (" << camPos.x() << ", " << camPos.y() << ", " << camPos.z() << ")" << std::endl;
std::cout << "[CLICK] Ray direction: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl;
// First check if we clicked on interactive object
InteractiveObject* clickedObject = raycastInteractiveObjects(camPos, rayDir);
if (clickedObject && player && clickedObject->isActive) {
std::cout << "[CLICK] *** SUCCESS: Clicked on interactive object: " << clickedObject->name << " ***" << std::endl;
std::cout << "[CLICK] Object position: (" << clickedObject->position.x() << ", "
<< clickedObject->position.y() << ", " << clickedObject->position.z() << ")" << std::endl;
std::cout << "[CLICK] Player position: (" << player->position.x() << ", "
<< player->position.y() << ", " << player->position.z() << ")" << std::endl;
targetInteractiveObject = clickedObject;
player->setTarget(clickedObject->position);
std::cout << "[CLICK] Player moving to object..." << std::endl;
}
else {
// Check if we clicked on an NPC
Character* clickedNpc = raycastNpcs(camPos, rayDir);
if (clickedNpc && player) {
float distance = (player->position - clickedNpc->position).norm();
int npcIndex = -1;
for (size_t i = 0; i < npcs.size(); ++i) {
if (npcs[i].get() == clickedNpc) {
npcIndex = static_cast<int>(i);
break;
}
}
if (npcIndex != -1) {
if (distance <= clickedNpc->interactionRadius) {
std::cout << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " ***" << std::endl;
scriptEngine.callNpcInteractCallback(npcIndex);
}
else {
std::cout << "[CLICK] Too far from NPC (distance " << distance
<< " > " << clickedNpc->interactionRadius << ")" << std::endl;
}
if (clickedNpc->canAttack)
{
player->attackTarget = clickedNpc;
}
}
}
else if (rayDir.y() < -0.001f && player) {
// Otherwise, unproject click to ground plane for Viola's walk target
float t = -camPos.y() / rayDir.y();
Eigen::Vector3f hit = camPos + rayDir * t;
std::cout << "[CLICK] Clicked on ground at: (" << hit.x() << ", " << hit.z() << ")" << std::endl;
if (player->currentState == AnimationState::STAND || player->currentState == AnimationState::WALK)
{
player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z()));
}
}
else {
std::cout << "[CLICK] No valid target found" << std::endl;
}
}
std::cout << "========================================\n" << std::endl;
}
void Location::handleUp(int64_t fingerId, int mx, int my)
{
}
void Location::handleMotion(int64_t fingerId, int eventX, int eventY, int mx, int my)
{
if (dialogueSystem.blocksGameplayInput()) {
dialogueSystem.handlePointerMoved(
static_cast<float>(mx),
Environment::projectionHeight - static_cast<float>(my)
);
}
if (rightMouseDown) {
int dx = eventX - lastMouseX;
int dy = eventY - lastMouseY;
lastMouseX = eventX;
lastMouseY = eventY;
const float sensitivity = 0.005f;
cameraAzimuth += dx * sensitivity;
cameraInclination += dy * sensitivity;
const float minInclination = M_PI * 30.f / 180.f;
const float maxInclination = M_PI * 0.5f;
cameraInclination = max(minInclination, min(maxInclination, cameraInclination));
}
}
bool Location::requestDialogueStart(const std::string& dialogueId)
{
return dialogueSystem.startDialogue(dialogueId);
}
void Location::setDialogueFlag(const std::string& flag, int value)
{
dialogueSystem.setFlag(flag, value);
}
int Location::getDialogueFlag(const std::string& flag) const
{
return dialogueSystem.getFlag(flag);
}
} // namespace ZL

81
src/Location.h Normal file
View File

@ -0,0 +1,81 @@
#pragma once
#include "render/Renderer.h"
#include "Environment.h"
#include "render/TextureManager.h"
#include "items/GameObjectLoader.h"
#include "items/InteractiveObject.h"
#include "navigation/PathFinder.h"
#include "render/ShadowMap.h"
#include "ScriptEngine.h"
#include "dialogue/DialogueSystem.h"
namespace ZL
{
class Location
{
public:
Location(Renderer& iRenderer, Inventory& iInventory);
std::shared_ptr<Texture> roomTexture;
VertexRenderStruct roomMesh;
std::unordered_map<std::string, LoadedGameObject> gameObjects;
std::vector<InteractiveObject> interactiveObjects;
std::unique_ptr<Character> player;
std::vector<std::unique_ptr<Character>> npcs;
float cameraAzimuth = 0.0f;
float cameraInclination = M_PI * 30.f / 180.f;
PathFinder navigation;
std::unique_ptr<ShadowMap> shadowMap;
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
InteractiveObject* targetInteractiveObject = nullptr;
ScriptEngine scriptEngine;
Dialogue::DialogueSystem dialogueSystem;
#ifdef SHOW_PATH
std::vector<VertexRenderStruct> debugNavMeshes;
void buildDebugNavMeshes();
void drawDebugNavigation();
#endif
bool rightMouseDown = false;
int lastMouseX = 0;
int lastMouseY = 0;
void setup();
void setupNavigation();
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
void drawGame();
void drawShadowDepthPass();
void drawGameWithShadows();
bool setNavigationAreaAvailable(const std::string& areaName, bool available);
void update(int64_t deltaMs);
void handleDown(int64_t fingerId, int eventX, int eventY, int mx, int my);
void handleUp(int64_t fingerId, int mx, int my);
void handleMotion(int64_t fingerId, int eventX, int eventY, int mx, int my);
bool requestDialogueStart(const std::string& dialogueId);
void setDialogueFlag(const std::string& flag, int value);
int getDialogueFlag(const std::string& flag) const;
protected:
Renderer& renderer;
Inventory& inventory;
};
} // namespace ZL

View File

@ -1,71 +0,0 @@
#include "Projectile.h"
namespace ZL {
Projectile::Projectile()
: pos({ 0,0,0 })
, vel({ 0,0,0 })
, life(0.0f)
, maxLife(0.0f)
, active(false)
, size(0.5f)
, texture(nullptr)
{
}
void Projectile::init(const Vector3f& startPos, const Vector3f& startVel, float lifeMs, float s, std::shared_ptr<Texture> tex, Renderer& renderer) {
pos = startPos;
vel = startVel;
life = 0.0f;
maxLife = lifeMs;
size = s;
texture = tex;
active = true;
rebuildMesh(renderer);
mesh.RefreshVBO();
}
void Projectile::update(float deltaMs, Renderer& renderer) {
if (!active) return;
pos = pos + vel * (deltaMs / 1000.0f);
life += deltaMs;
if (life >= maxLife) {
active = false;
return;
}
rebuildMesh(renderer);
mesh.RefreshVBO();
}
void Projectile::rebuildMesh(Renderer&) {
float half = 10 * size * 0.5f;
mesh.data.PositionData.clear();
mesh.data.TexCoordData.clear();
mesh.data.PositionData.push_back({ pos(0) - half, pos(1) - half, pos(2) });
mesh.data.PositionData.push_back({ pos(0) - half, pos(1) + half, pos(2) });
mesh.data.PositionData.push_back({ pos(0) + half, pos(1) + half, pos(2) });
mesh.data.PositionData.push_back({ pos(0) - half, pos(1) - half, pos(2) });
mesh.data.PositionData.push_back({ pos(0) + half, pos(1) + half, pos(2) });
mesh.data.PositionData.push_back({ pos(0) + half, pos(1) - half, pos(2) });
mesh.data.TexCoordData.push_back({ 0.0f, 0.0f });
mesh.data.TexCoordData.push_back({ 0.0f, 1.0f });
mesh.data.TexCoordData.push_back({ 1.0f, 1.0f });
mesh.data.TexCoordData.push_back({ 0.0f, 0.0f });
mesh.data.TexCoordData.push_back({ 1.0f, 1.0f });
mesh.data.TexCoordData.push_back({ 1.0f, 0.0f });
}
void Projectile::draw(Renderer& renderer) const {
if (!active || !texture) return;
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
renderer.DrawVertexRenderStruct(mesh);
}
} // namespace ZL

View File

@ -1,38 +0,0 @@
#pragma once
#include "render/Renderer.h"
#include "render/TextureManager.h"
#include <memory>
#include "SparkEmitter.h"
namespace ZL {
class Projectile {
public:
Projectile();
~Projectile() = default;
void init(const Vector3f& startPos, const Vector3f& startVel, float lifeMs, float size, std::shared_ptr<Texture> tex, Renderer& renderer);
void update(float deltaMs, Renderer& renderer);
void draw(Renderer& renderer) const;
bool isActive() const { return active; }
Vector3f getPosition() const { return pos; }
void deactivate() { active = false; }
SparkEmitter projectileEmitter;
private:
Vector3f pos;
Vector3f vel;
float life;
float maxLife;
bool active;
float size;
VertexRenderStruct mesh;
std::shared_ptr<Texture> texture;
void rebuildMesh(Renderer& renderer);
};
} // namespace ZL

View File

@ -2,6 +2,7 @@
#include "Game.h"
#include <iostream>
#include <stdexcept>
#include "Location.h"
#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
@ -15,7 +16,7 @@ namespace ZL {
ScriptEngine::ScriptEngine() = default;
ScriptEngine::~ScriptEngine() = default;
void ScriptEngine::init(Game* game) {
void ScriptEngine::init(Location* game, Inventory* inventory) {
impl = std::make_unique<Impl>();
sol::state& lua = impl->lua;
@ -49,13 +50,15 @@ namespace ZL {
});
// pickup_item(object_name)
api.set_function("pickup_item", [game](const std::string& objectName) {
api.set_function("pickup_item", [game, inventory](const std::string& objectName) {
std::cout << "[script] pickup_item: " << objectName << std::endl;
for (auto& intObj : game->interactiveObjects) {
if (intObj.id == objectName && intObj.isActive) {
// Add item to inventory
game->inventory.addItem(intObj.dropItem);
inventory->addItem(intObj.dropItem);
// Deactivate object
intObj.isActive = false;
@ -68,51 +71,18 @@ namespace ZL {
});
// show_inventory()
api.set_function("show_inventory", [game]() {
std::cout << "[script] show_inventory called" << std::endl;
game->menuManager.uiManager.setNodeVisible("inventory_items_panel", true);
game->menuManager.uiManager.setNodeVisible("close_inventory_button", true);
game->inventoryOpen = true;
// Update UI with current items
const auto& items = game->inventory.getItems();
std::string itemText;
if (items.empty()) {
itemText = "Inventory (Empty)";
}
else {
itemText = "Inventory (" + std::to_string(items.size()) + " items)\n\n";
for (size_t i = 0; i < items.size(); ++i) {
itemText += std::to_string(i + 1) + ". " + items[i].name + "\n";
}
}
game->menuManager.uiManager.setText("inventory_items_text", itemText);
});
// hide_inventory()
api.set_function("hide_inventory", [game]() {
std::cout << "[script] hide_inventory called" << std::endl;
game->menuManager.uiManager.setNodeVisible("inventory_items_panel", false);
game->menuManager.uiManager.setNodeVisible("close_inventory_button", false);
game->inventoryOpen = false;
});
// add_item(item_id, name, description, icon)
api.set_function("add_item", [game](const std::string& id, const std::string& name,
api.set_function("add_item", [game, inventory](const std::string& id, const std::string& name,
const std::string& description, const std::string& icon) {
std::cout << "[script] add_item: " << name << std::endl;
Item newItem(id, name, description, icon);
game->inventory.addItem(newItem);
inventory->addItem(newItem);
});
// remove_item(item_id)
api.set_function("remove_item", [game](const std::string& id) {
api.set_function("remove_item", [game, inventory](const std::string& id) {
std::cout << "[script] remove_item: " << id << std::endl;
game->inventory.removeItem(id);
inventory->removeItem(id);
});
// deactivate_interactive_object(object_name)
@ -129,13 +99,13 @@ namespace ZL {
});
// get_inventory_count()
api.set_function("get_inventory_count", [game]() {
return game->inventory.getCount();
api.set_function("get_inventory_count", [inventory]() {
return inventory->getCount();
});
// has_item(item_id)
api.set_function("has_item", [game](const std::string& id) {
return game->inventory.hasItem(id);
api.set_function("has_item", [inventory](const std::string& id) {
return inventory->hasItem(id);
});
api.set_function("start_dialogue",
@ -164,7 +134,7 @@ namespace ZL {
});
// receive_npc_gift(npc_index)
api.set_function("receive_npc_gift", [game](int npcIndex) {
api.set_function("receive_npc_gift", [game, inventory](int npcIndex) {
std::cout << "[script] receive_npc_gift: npc index " << npcIndex << std::endl;
auto& npcs = game->npcs;
@ -188,7 +158,7 @@ namespace ZL {
return;
}
game->inventory.addItem(npc->giftItem);
inventory->addItem(npc->giftItem);
npc->giftReceived = true;
std::cout << "[script] Received gift from " << npc->npcName << ": "
@ -284,31 +254,4 @@ namespace ZL {
}
}
void ScriptEngine::showInventory(Game* game) {
std::cout << "[script] toggle_inventory called" << std::endl;
bool isVisible = game->menuManager.uiManager.getNodeVisible("inventory_items_panel");
game->menuManager.uiManager.setNodeVisible("inventory_items_panel", !isVisible);
game->menuManager.uiManager.setNodeVisible("close_inventory_button", !isVisible);
game->inventoryOpen = !isVisible;
if (!isVisible) {
const auto& items = game->inventory.getItems();
std::string itemText;
if (items.empty()) {
itemText = "Inventory (Empty)";
}
else {
itemText = "Inventory (" + std::to_string(items.size()) + " items)\n\n";
for (size_t i = 0; i < items.size(); ++i) {
itemText += std::to_string(i + 1) + ". " + items[i].name + "\n";
}
}
game->menuManager.uiManager.setText("inventory_items_text", itemText);
}
}
} // namespace ZL

View File

@ -4,7 +4,8 @@
namespace ZL {
class Game;
class Location;
class Inventory;
class ScriptEngine {
public:
@ -12,7 +13,7 @@ public:
~ScriptEngine();
// Must be called once, after the Game's NPCs are ready.
void init(Game* game);
void init(Location* game, Inventory* inventory);
// Execute a Lua script file. Logs errors to stderr.
void runScript(const std::string& path);
@ -21,7 +22,6 @@ public:
void callActivateFunction(const std::string& functionName);
void showInventory(Game* game);
void callNpcInteractCallback(int npcIndex);
private:

File diff suppressed because it is too large Load Diff

View File

@ -1,204 +0,0 @@
#pragma once
#include "render/Renderer.h"
#include "Environment.h"
#include "render/TextureManager.h"
#include "SparkEmitter.h"
#include "planet/PlanetObject.h"
#include "UiManager.h"
#include "Projectile.h"
#include "utils/TaskManager.h"
#include "network/NetworkInterface.h"
#include <queue>
#include <vector>
#include <string>
#include <memory>
#include <render/TextRenderer.h>
#include "MenuManager.h"
#include <unordered_set>
namespace ZL {
struct BoxCoords
{
Vector3f pos;
Matrix3f m;
};
class Space {
public:
Space(Renderer& iRenderer, TaskManager& iTaskManager, MainThreadHandler& iMainThreadHandler, std::unique_ptr<INetworkClient>& iNetworkClient, MenuManager& iMenuManager);
~Space();
void setup();
void update();
Renderer& renderer;
TaskManager& taskManager;
MainThreadHandler& mainThreadHandler;
std::unique_ptr<INetworkClient>& networkClient;
MenuManager& menuManager;
public:
void processTickCount(int64_t newTickCount, int64_t delta);
void drawScene();
void drawCubemap(float skyPercent);
void drawShip();
void drawBoxes();
void drawBoxesLabels();
void drawRemoteShips();
void drawRemoteShipsLabels();
void fireProjectiles();
void handleDown(int mx, int my);
void handleUp(int mx, int my);
void handleMotion(int mx, int my);
std::vector<BoxCoords> boxCoordsArr;
std::vector<VertexRenderStruct> boxRenderArr;
std::vector<std::string> boxLabels;
std::unique_ptr<TextRenderer> textRenderer;
std::unordered_map<int, ClientState> remotePlayerStates;
std::unordered_map<int, SparkEmitter> remoteShipSparkEmitters;
float newShipVelocity = 0;
static const size_t CONST_TIMER_INTERVAL = 10;
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
std::shared_ptr<Texture> sparkTexture;
std::shared_ptr<Texture> spaceshipTexture;
std::shared_ptr<Texture> cubemapTexture;
VertexDataStruct spaceshipBase;
VertexRenderStruct spaceship;
std::shared_ptr<Texture> cargoTexture;
VertexDataStruct cargoBase;
VertexRenderStruct cargo;
VertexRenderStruct cubemap;
std::shared_ptr<Texture> boxTexture;
VertexDataStruct boxBase;
SparkEmitter sparkEmitter;
SparkEmitter sparkEmitterCargo;
SparkEmitter projectileEmitter;
SparkEmitter explosionEmitter;
PlanetObject planetObject;
std::vector<std::unique_ptr<Projectile>> projectiles;
std::shared_ptr<Texture> projectileTexture;
float projectileCooldownMs = 500.0f;
int64_t lastProjectileFireTime = 0;
int maxProjectiles = 500;
//std::vector<Vector3f> shipLocalEmissionPoints;
bool shipAlive = true;
bool gameOver = false;
bool firePressed = false;
std::vector<bool> boxAlive;
float shipCollisionRadius = 15.0f;
float boxCollisionRadius = 2.0f;
bool showExplosion = false;
uint64_t lastExplosionTime = 0;
const uint64_t explosionDurationMs = 500;
bool serverBoxesApplied = false;
bool nearPickupBox = false;
static constexpr float MAX_DIST_SQ = 10000.f * 10000.f;
static constexpr float FADE_START = 6000.f;
static constexpr float FADE_RANGE = 4000.f;
static constexpr float BASE_SCALE = 140.f;
static constexpr float PERSPECTIVE_K = 0.05f; // Tune
static constexpr float MIN_SCALE = 0.4f;
static constexpr float MAX_SCALE = 0.8f;
static constexpr float CLOSE_DIST = 600.0f;
std::unordered_set<int> deadRemotePlayers;
int playerScore = 0;
int prevPlayerScore = 0;
bool wasConnectedToServer = false;
bool playerListVisible = false;
int manualTrackedTargetId = -1;
static constexpr float TARGET_MAX_DIST = 50000.0f;
static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST;
// --- Target HUD (brackets + offscreen arrow) ---
int trackedTargetId = -1;
bool targetWasVisible = false;
float targetAcquireAnim = 0.0f; // 0..1 схлопывание (0 = далеко, 1 = на месте)
// временный меш для HUD (будем перезаливать VBO маленькими порциями)
VertexRenderStruct hudTempMesh;
// helpers
void drawTargetHud(); // рисует рамку или стрелку
int pickTargetId() const; // ???????? ???? (????: ????????? ????? ????????? ?????)
void resetPlayerState();
void clearTextRendererCache();
void updateShowPlayersButtonState();
bool showPlayersButtonEnabled = false;
// Player list overlay
void buildAndShowPlayerList();
void closePlayerList();
void rebuildPlayerListIfVisible();
void clearPlayerListIfVisible();
std::shared_ptr<UiNode> buildPlayerListRoot();
void updateSparkEmitters(float deltaMs);
void prepareSparkEmittersForDraw();
void drawShipSparkEmitters();
// Crosshair HUD
struct CrosshairConfig {
bool enabled = true;
int refW = 1280;
int refH = 720;
float scaleMul = 1.0f;
Eigen::Vector3f color = { 1.f, 1.f, 1.f };
float alpha = 1.0f; // cl_crosshairalpha
float thicknessPx = 2.0f; // cl_crosshairthickness
float gapPx = 10.0f;
float topLenPx = 14.0f;
float topAngleDeg = 90.0f;
struct Arm { float lenPx; float angleDeg; };
std::vector<Arm> arms;
};
CrosshairConfig crosshairCfg;
bool crosshairCfgLoaded = false;
// кеш геометрии
VertexRenderStruct crosshairMesh;
bool crosshairMeshValid = false;
int crosshairLastW = 0, crosshairLastH = 0;
float crosshairLastAlpha = -1.0f;
float crosshairLastThickness = -1.0f;
float crosshairLastGap = -1.0f;
float crosshairLastScaleMul = -1.0f;
bool loadCrosshairConfig(const std::string& path);
void rebuildCrosshairMeshIfNeeded();
void drawCrosshair();
};
} // namespace ZL

View File

@ -3,6 +3,11 @@
#include "utils/Utils.h"
#include <iostream>
namespace ZL
{
extern const char* CONST_ZIP_FILE;
}
namespace ZL::Dialogue {
NodeType DialogueDatabase::parseNodeType(const std::string& value) {
@ -202,10 +207,23 @@ bool DialogueDatabase::loadFromFile(const std::string& path) {
dialogues.clear();
cutscenes.clear();
const std::string raw = ZL::readTextFile(path);
if (raw.empty()) {
std::cerr << "[dialogue] Failed to read file: " << path << "\n";
return false;
std::string raw;
try {
if (strlen(CONST_ZIP_FILE) == 0) {
raw = readTextFile(path);
}
else {
auto buf = readFileFromZIP(path, CONST_ZIP_FILE);
if (buf.empty()) {
std::cerr << "UiManager: failed to read " << path << " from zip " << CONST_ZIP_FILE << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
raw.assign(buf.begin(), buf.end());
}
}
catch (const std::exception& e) {
std::cerr << "UiManager: failed to open " << path << " : " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
json root;

View File

@ -84,6 +84,12 @@ bool DialogueOverlay::init(Renderer& renderer, const std::string& zipFile) {
return ok;
}
void DialogueOverlay::update(const PresentationModel& model, int deltaMs) {
if (model.mode != PresentationMode::Choice) {
hoveredChoiceIndex = -1;
}
}
void DialogueOverlay::draw(Renderer& renderer, const PresentationModel& model) {
if (model.mode == PresentationMode::Hidden) {
lastChoiceRects.clear();
@ -170,12 +176,9 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
lastChoiceRects.push_back(rect);
choiceQuads[i].rebuild(rect);
const bool isSelected = static_cast<int>(i) == model.selectedChoice;
std::shared_ptr<Texture> choiceTexture = choiceMainTexture;
if (model.choices[i].kind == ChoiceKind::Optional) {
choiceTexture = choiceOptionalTexture;
}
if (isSelected) {
const bool isHighlighted = static_cast<int>(i) == hoveredChoiceIndex || static_cast<int>(i) == model.selectedChoice;
std::shared_ptr<Texture> choiceTexture = (model.choices[i].kind == ChoiceKind::Optional) ? choiceOptionalTexture : choiceMainTexture;
if (isHighlighted) {
choiceTexture = choiceSelectedTexture;
}
drawQuad(renderer, choiceQuads[i], choiceTexture);
@ -187,7 +190,7 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
for (size_t i = 0; i < model.choices.size(); ++i) {
const UiRect& rect = lastChoiceRects[i];
const bool isSelected = static_cast<int>(i) == model.selectedChoice;
const bool isHighlighted = static_cast<int>(i) == hoveredChoiceIndex || static_cast<int>(i) == model.selectedChoice;
const std::array<float, 4> color = (model.choices[i].kind == ChoiceKind::Optional)
? std::array<float, 4>{0.82f, 0.82f, 0.82f, 1.0f}
: std::array<float, 4>{ 1.0f, 0.93f, 0.65f, 1.0f };
@ -198,7 +201,7 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
rect.y + 9.0f,
1.0f,
false,
isSelected ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
isHighlighted ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
);
}
}
@ -417,21 +420,48 @@ void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel&
glDisable(GL_BLEND);
}
bool DialogueOverlay::handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex) const {
void DialogueOverlay::handlePointerDown(float x, float y, const PresentationModel& model) {
if (model.mode == PresentationMode::Choice) {
handlePointerMoved(x, y, model);
return;
}
}
void DialogueOverlay::handlePointerMoved(float x, float y, const PresentationModel& model) {
if (model.mode == PresentationMode::Choice) {
hoveredChoiceIndex = -1;
for (size_t i = 0; i < lastChoiceRects.size(); ++i) {
if (rectContains(lastChoiceRects[i], x, y)) {
hoveredChoiceIndex = static_cast<int>(i);
break;
}
}
return;
}
hoveredChoiceIndex = -1;
}
bool DialogueOverlay::handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex, bool& outAdvanceDialogue) {
outChoiceIndex = -1;
outAdvanceDialogue = false;
if (model.mode == PresentationMode::Choice) {
for (size_t i = 0; i < lastChoiceRects.size(); ++i) {
if (lastChoiceRects[i].contains(x, y)) {
if (rectContains(lastChoiceRects[i], x, y)) {
outChoiceIndex = static_cast<int>(i);
return true;
}
}
return lastDialogueAdvanceRect.contains(x, y);
return false;
}
if (model.mode == PresentationMode::Dialogue) {
return lastDialogueAdvanceRect.contains(x, y);
if (lastDialogueAdvanceRect.contains(x, y)) {
outAdvanceDialogue = true;
return true;
}
return false;
}
if (model.mode == PresentationMode::Cutscene) {
@ -494,4 +524,8 @@ std::string DialogueOverlay::wrapText(const std::string& input, size_t maxLineLe
return output;
}
bool DialogueOverlay::rectContains(const UiRect& rect, float x, float y) {
return x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
}
} // namespace ZL::Dialogue

View File

@ -15,11 +15,12 @@ namespace ZL::Dialogue {
class DialogueOverlay {
public:
bool init(Renderer& renderer, const std::string& zipFile = "");
void update(const PresentationModel& model, int deltaMs);
void draw(Renderer& renderer, const PresentationModel& model);
// Coordinates are expected in the game's UI projection space
// Returns true only when the click should advance/select.
bool handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex) const;
void handlePointerDown(float x, float y, const PresentationModel& model);
void handlePointerMoved(float x, float y, const PresentationModel& model);
bool handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex, bool& outAdvanceDialogue);
private:
struct TexturedQuad {
@ -60,6 +61,8 @@ private:
mutable UiRect lastCutsceneAdvanceRect{};
mutable bool cutsceneAdvanceEnabled = false;
int hoveredChoiceIndex = -1;
std::unique_ptr<TextRenderer> nameRenderer;
std::unique_ptr<TextRenderer> bodyRenderer;
std::unique_ptr<TextRenderer> choiceRenderer;
@ -80,6 +83,7 @@ private:
void drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const;
static std::string wrapText(const std::string& input, size_t maxLineLength);
static bool rectContains(const UiRect& rect, float x, float y);
static float lerpFloat(float a, float b, float t);
static ResolvedViewport resolveViewportPose(

View File

@ -185,10 +185,26 @@ void DialogueRuntime::moveSelection(int delta) {
}
const int count = static_cast<int>(visibleChoices.size());
if (selectedChoice < 0 || selectedChoice >= count) {
selectedChoice = (delta >= 0) ? 0 : (count - 1);
}
else {
selectedChoice = (selectedChoice + delta) % count;
if (selectedChoice < 0) {
selectedChoice += count;
}
}
presentation.selectedChoice = selectedChoice;
}
void DialogueRuntime::selectChoice(int index) {
if (mode != Mode::WaitingForChoice || visibleChoices.empty()) {
return;
}
if (index < 0 || index >= static_cast<int>(visibleChoices.size())) {
return;
}
selectedChoice = index;
presentation.selectedChoice = selectedChoice;
}
@ -346,14 +362,14 @@ void DialogueRuntime::presentChoices(const Node& node) {
}
mode = Mode::WaitingForChoice;
selectedChoice = 0;
selectedChoice = -1;
presentation.mode = PresentationMode::Choice;
presentation.speaker = node.speaker;
presentation.fullText = node.text;
presentation.visibleText = node.text;
presentation.portraitPath = node.portrait;
presentation.backgroundPath.clear();
presentation.selectedChoice = 0;
presentation.selectedChoice = -1;
presentation.revealCompleted = true;
presentation.showCutsceneSubtitle = false;
presentation.cutsceneCamera = {};

View File

@ -26,6 +26,7 @@ public:
void confirmAdvance();
void moveSelection(int delta);
void selectChoice(int index);
const PresentationModel& getPresentation() const { return presentation; }

View File

@ -66,27 +66,46 @@ bool DialogueSystem::handleKeyDown(SDL_Keycode key) {
}
}
void DialogueSystem::handlePointerDown(float x, float y) {
if (!runtime.isActive()) {
return;
}
overlay.handlePointerDown(x, y, runtime.getPresentation());
}
void DialogueSystem::handlePointerMoved(float x, float y) {
if (!runtime.isActive()) {
return;
}
overlay.handlePointerMoved(x, y, runtime.getPresentation());
}
bool DialogueSystem::handlePointerReleased(float x, float y) {
if (!runtime.isActive()) {
return false;
}
int choiceIndex = -1;
bool advanceDialogue = false;
const PresentationModel& model = runtime.getPresentation();
if (!overlay.handlePointerReleased(x, y, model, choiceIndex)) {
if (!overlay.handlePointerReleased(x, y, model, choiceIndex, advanceDialogue)) {
return false;
}
if (choiceIndex >= 0) {
while (runtime.getPresentation().selectedChoice != choiceIndex) {
runtime.moveSelection(1);
}
runtime.selectChoice(choiceIndex);
runtime.confirmAdvance();
return true;
}
if (advanceDialogue) {
runtime.confirmAdvance();
return true;
}
return true;
}
bool DialogueSystem::startDialogue(const std::string& dialogueId) {
return runtime.startDialogue(dialogueId);
}

View File

@ -27,6 +27,8 @@ public:
void draw(Renderer& renderer);
bool handleKeyDown(SDL_Keycode key);
void handlePointerDown(float x, float y);
void handlePointerMoved(float x, float y);
bool handlePointerReleased(float x, float y);
bool startDialogue(const std::string& dialogueId);

View File

@ -174,7 +174,7 @@ struct PresentationModel {
std::string portraitPath;
std::string backgroundPath;
std::vector<PresentedChoice> choices;
int selectedChoice = 0;
int selectedChoice = -1;
bool revealCompleted = true;
bool showCutsceneSubtitle = false;

View File

@ -14,10 +14,40 @@ namespace ZL {
std::vector<GameObjectData> GameObjectLoader::loadFromJson(const std::string& jsonPath, const std::string& zipPath)
{
std::vector<GameObjectData> objects;
std::string content;
try {
if (zipPath.empty()) {
content = readTextFile(jsonPath);
}
else {
auto buf = readFileFromZIP(jsonPath, zipPath);
if (buf.empty()) {
std::cerr << "UiManager: failed to read " << jsonPath << " from zip " << zipPath << std::endl;
throw std::runtime_error("Failed to load UI file: " + jsonPath);
}
content.assign(buf.begin(), buf.end());
}
}
catch (const std::exception& e) {
std::cerr << "UiManager: failed to open " << jsonPath << " : " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + jsonPath);
}
json j;
try {
j = json::parse(content);
}
catch (const std::exception& e) {
std::cerr << "UiManager: json parse error: " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + jsonPath);
}
//json j;
try {
std::ifstream file(jsonPath);
/*std::ifstream file(jsonPath);
if (!file.is_open()) {
throw std::runtime_error("Could not open file: " + jsonPath);
}
@ -26,7 +56,7 @@ namespace ZL {
throw std::runtime_error("JSON file is empty: " + jsonPath);
}
file >> j;
file >> j;*/
if (!j.contains("objects") || !j["objects"].is_array()) {
std::cerr << "Warning: 'objects' array not found in " << jsonPath << std::endl;
@ -255,10 +285,40 @@ namespace ZL {
std::vector<NpcData> GameObjectLoader::loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath)
{
std::vector<NpcData> npcs;
std::string content;
try {
if (zipPath.empty()) {
content = readTextFile(jsonPath);
}
else {
auto buf = readFileFromZIP(jsonPath, zipPath);
if (buf.empty()) {
std::cerr << "UiManager: failed to read " << jsonPath << " from zip " << zipPath << std::endl;
throw std::runtime_error("Failed to load UI file: " + jsonPath);
}
content.assign(buf.begin(), buf.end());
}
}
catch (const std::exception& e) {
std::cerr << "UiManager: failed to open " << jsonPath << " : " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + jsonPath);
}
json j;
try {
j = json::parse(content);
}
catch (const std::exception& e) {
std::cerr << "UiManager: json parse error: " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + jsonPath);
}
//json j;
try {
std::ifstream file(jsonPath);
/*std::ifstream file(jsonPath);
if (!file.is_open()) {
throw std::runtime_error("Could not open file: " + jsonPath);
}
@ -267,7 +327,7 @@ namespace ZL {
throw std::runtime_error("JSON file is empty: " + jsonPath);
}
file >> j;
file >> j;*/
if (!j.contains("npcs") || !j["npcs"].is_array()) {
std::cerr << "Warning: 'npcs' array not found in " << jsonPath << std::endl;

View File

@ -12,7 +12,7 @@ namespace ZL {
void InteractiveObject::draw(Renderer& renderer) const {
if (!isActive || !texture) return;
/*
std::cout << "[DRAW] InteractiveObject::draw() called" << std::endl;
std::cout << "[DRAW] Object: " << name << std::endl;
std::cout << "[DRAW] Position: (" << position.x() << ", " << position.y() << ", " << position.z() << ")" << std::endl;
@ -22,7 +22,7 @@ namespace ZL {
std::cout << "[DRAW] First vertex: (" << mesh.data.PositionData[0].x() << ", "
<< mesh.data.PositionData[0].y() << ", " << mesh.data.PositionData[0].z() << ")" << std::endl;
}
*/
// Apply position transformation
renderer.PushMatrix();
renderer.TranslateMatrix(position);

View File

@ -1,455 +0,0 @@
#include "PlanetData.h"
#include <iostream>
#include <numeric>
#include <cmath>
#include <algorithm>
namespace ZL {
Matrix3f GetRotationForTriangle(const Triangle& tri);
const float PlanetData::PLANET_RADIUS = 20000.f;
const Vector3f PlanetData::PLANET_CENTER_OFFSET = Vector3f{ 0.f, 0.f, 0.0f };
// --- Константы диапазонов (перенесены из PlanetObject.cpp) ---
VertexID generateEdgeID(const VertexID& id1, const VertexID& id2) {
return id1 < id2 ? id1 + "_" + id2 : id2 + "_" + id1;
}
// Вспомогательная функция для проекции (локальная)
static Vector3f projectPointOnPlane(const Vector3f& P, const Vector3f& A, const Vector3f& B, const Vector3f& C) {
Vector3f AB = B + A * (-1.0f);
Vector3f AC = C + A * (-1.0f);
Vector3f N = AB.cross(AC).normalized();
Vector3f AP = P + A * (-1.0f);
float distance = N.dot(AP);
return P + N * (-distance);
}
PlanetData::PlanetData()
: perlin(77777)
, colorPerlin(123123)
//, currentLod(0)
{
// currentLod = planetMeshLods.size() - 1; // Start with max LOD
/*
initialVertexMap = {
{{ 0.0f, 1.0f, 0.0f}, "A"},
{{ 0.0f, -1.0f, 0.0f}, "B"},
{{ 1.0f, 0.0f, 0.0f}, "C"},
{{-1.0f, 0.0f, 0.0f}, "D"},
{{ 0.0f, 0.0f, 1.0f}, "E"},
{{ 0.0f, 0.0f, -1.0f}, "F"}
};*/
}
void PlanetData::init() {
for (int i = 0; i < planetMeshLods.size(); i++) {
//planetMeshLods[i] = generateSphere(i, 0.01f);
planetMeshLods[i] = generateSphere(i, 0.f);
planetMeshLods[i].Scale(PLANET_RADIUS);
planetMeshLods[i].Move(PLANET_CENTER_OFFSET);
}
planetAtmosphereLod = generateSphere(5, 0);
planetAtmosphereLod.Scale(PLANET_RADIUS * 1.03);
planetAtmosphereLod.Move(PLANET_CENTER_OFFSET);
const auto& lodLevel = getLodLevel();
for (size_t i = 0; i < lodLevel.triangles.size(); i++)
{
if (i % 100 == 0)
{
PlanetCampObject campObject;
campObject.position = (lodLevel.triangles[i].data[0] +
lodLevel.triangles[i].data[1] +
lodLevel.triangles[i].data[2]) / 3.0f;
auto newM = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitX())).toRotationMatrix();
campObject.rotation = GetRotationForTriangle(lodLevel.triangles[i]).inverse() * newM;
campObjects.push_back(campObject);
}
}
}
const LodLevel& PlanetData::getLodLevel() const {
return planetMeshLods.at(MAX_LOD_LEVELS-1);
}
const LodLevel& PlanetData::getAtmosphereLod() const {
return planetAtmosphereLod;
}
/*
int PlanetData::getCurrentLodIndex() const {
return currentLod;
}
int PlanetData::getMaxLodIndex() const {
return static_cast<int>(planetMeshLods.size() - 1);
}*/
std::pair<float, float> PlanetData::calculateZRange(float dToPlanetSurface) {
float currentZNear;
float currentZFar;
float alpha;
if (dToPlanetSurface > 2000)
{
currentZNear = 1000;
currentZFar = currentZNear * 100;
}
else if (dToPlanetSurface > 1200)
{
currentZNear = 500;
currentZFar = currentZNear * 100;
}
else if (dToPlanetSurface > 650)
{
currentZNear = 250;
currentZFar = currentZNear * 100;
}
else if (dToPlanetSurface > 160)
{
currentZNear = 125;
currentZFar = currentZNear * 150;
}
else if (dToPlanetSurface > 100)
{
currentZNear = 65;
currentZFar = currentZNear * 170;
}
else if (dToPlanetSurface > 40)
{
currentZNear = 32;
currentZFar = 10000.f;
}
else if (dToPlanetSurface > 20)
{
currentZNear = 16;
currentZFar = 5000.f;
}
else if (dToPlanetSurface > 5)
{
currentZNear = 8;
currentZFar = 2500.f;
}
else
{
currentZNear = 4;
currentZFar = 1250.f;
}
return { currentZNear, currentZFar };
}
float PlanetData::distanceToPlanetSurfaceFast(const Vector3f& viewerPosition)
{
Vector3f shipLocalPosition = viewerPosition - PLANET_CENTER_OFFSET;
return sqrt(shipLocalPosition.squaredNorm()) - PLANET_RADIUS;
}
std::vector<int> PlanetData::getBestTriangleUnderCamera(const Vector3f& viewerPosition) {
const LodLevel& finalLod = planetMeshLods[MAX_LOD_LEVELS - 1]; // Работаем с текущим активным LOD
Vector3f targetDir = (viewerPosition - PLANET_CENTER_OFFSET).normalized();
int bestTriangle = -1;
float maxDot = -1.0f;
// Шаг 1: Быстрый поиск ближайшего треугольника по "центроиду"
// Чтобы не проверять все, можно проверять каждый N-й или использовать
// предварительно вычисленные центры для LOD0, чтобы сузить круг.
// Но для надежности пройдемся по массиву (для 5-6 подразделений это быстро)
for (int i = 0; i < (int)finalLod.triangles.size(); ++i) {
// Вычисляем примерное направление на треугольник
Vector3f triDir = (finalLod.triangles[i].data[0] +
finalLod.triangles[i].data[1] +
finalLod.triangles[i].data[2]).normalized();
float dot = targetDir.dot(triDir);
if (dot > maxDot) {
maxDot = dot;
bestTriangle = i;
}
}
if (bestTriangle == -1) return {};
return { bestTriangle };
}
std::vector<int> PlanetData::getTrianglesUnderCameraNew2(const Vector3f& viewerPosition) {
const LodLevel& finalLod = planetMeshLods[MAX_LOD_LEVELS - 1];
Vector3f shipLocal = viewerPosition - PLANET_CENTER_OFFSET;
float currentDist = shipLocal.norm();
Vector3f targetDir = shipLocal.normalized();
// Желаемый радиус покрытия на поверхности планеты (в метрах/единицах движка)
// Подбери это значение так, чтобы камни вокруг корабля всегда были видны.
const float desiredCoverageRadius = 3000.0f;
// Вычисляем порог косинуса на основе желаемого радиуса и текущего расстояния.
// Чем мы дальше (currentDist больше), тем меньше должен быть угол отклонения
// от нормали, чтобы захватить ту же площадь.
float angle = atan2(desiredCoverageRadius, currentDist);
float searchThreshold = cos(angle);
// Ограничитель, чтобы не захватить всю планету или вообще ничего
searchThreshold = std::clamp(searchThreshold, 0.90f, 0.9999f);
std::vector<int> result;
for (int i = 0; i < (int)finalLod.triangles.size(); ++i) {
// Используем центроид (можно кэшировать в LodLevel для скорости)
Vector3f triDir = (finalLod.triangles[i].data[0] +
finalLod.triangles[i].data[1] +
finalLod.triangles[i].data[2]).normalized();
if (targetDir.dot(triDir) > searchThreshold) {
result.push_back(i);
}
}
if (result.empty()) return getBestTriangleUnderCamera(viewerPosition);
return result;
}
std::vector<Triangle> PlanetData::subdivideTriangles(const std::vector<Triangle>& input, float noiseCoeff) {
std::vector<Triangle> output;
for (const auto& t : input) {
// Вершины и их ID
const Vector3f& a = t.data[0];
const Vector3f& b = t.data[1];
const Vector3f& c = t.data[2];
const VertexID& id_a = t.ids[0];
const VertexID& id_b = t.ids[1];
const VertexID& id_c = t.ids[2];
// 1. Вычисляем середины (координаты)
Vector3f m_ab = ((a + b) * 0.5f).normalized();
Vector3f m_bc = ((b + c) * 0.5f).normalized();
Vector3f m_ac = ((a + c) * 0.5f).normalized();
//Vector3f pm_ab = m_ab * perlin.getSurfaceHeight(m_ab, noiseCoeff);
//Vector3f pm_bc = m_bc * perlin.getSurfaceHeight(m_bc, noiseCoeff);
//Vector3f pm_ac = m_ac * perlin.getSurfaceHeight(m_ac, noiseCoeff);
Vector3f pm_ab = m_ab;
Vector3f pm_bc = m_bc;
Vector3f pm_ac = m_ac;
// 2. Вычисляем ID новых вершин
VertexID id_mab = generateEdgeID(id_a, id_b);
VertexID id_mbc = generateEdgeID(id_b, id_c);
VertexID id_mac = generateEdgeID(id_a, id_c);
// 3. Формируем 4 новых треугольника
output.emplace_back(Triangle{ {a, pm_ab, pm_ac}, {id_a, id_mab, id_mac} }); // 0
output.emplace_back(Triangle{ {pm_ab, b, pm_bc}, {id_mab, id_b, id_mbc} }); // 1
output.emplace_back(Triangle{ {pm_ac, pm_bc, c}, {id_mac, id_mbc, id_c} }); // 2
output.emplace_back(Triangle{ {pm_ab, pm_bc, pm_ac}, {id_mab, id_mbc, id_mac} }); // 3
}
return output;
}
LodLevel PlanetData::createLodLevel(const std::vector<Triangle>& geometry) {
LodLevel result;
result.triangles = geometry;
size_t vertexCount = geometry.size() * 3;
result.VertexIDs.reserve(vertexCount);
for (const auto& t : geometry) {
for (int i = 0; i < 3; ++i) {
result.VertexIDs.push_back(t.ids[i]);
}
}
return result;
}
void PlanetData::recalculateMeshAttributes(LodLevel& lod)
{
size_t vertexCount = lod.triangles.size() * 3;
lod.vertexData.PositionData.clear();
lod.vertexData.NormalData.clear();
lod.vertexData.TexCoordData.clear();
lod.vertexData.TangentData.clear();
lod.vertexData.BinormalData.clear();
lod.vertexData.ColorData.clear();
lod.vertexData.PositionData.reserve(vertexCount);
lod.vertexData.NormalData.reserve(vertexCount);
lod.vertexData.TexCoordData.reserve(vertexCount);
lod.vertexData.TangentData.reserve(vertexCount);
lod.vertexData.BinormalData.reserve(vertexCount);
lod.vertexData.ColorData.reserve(vertexCount);
const std::array<Vector2f, 3> triangleUVs = {
Vector2f(0.5f, 1.0f),
Vector2f(0.0f, 0.0f),
Vector2f(1.0f, 0.0f)
};
const Vector3f colorPinkish = { 1.0f, 0.8f, 0.82f }; // Слегка розоватый
const Vector3f colorYellowish = { 1.0f, 1.0f, 0.75f }; // Слегка желтоватый
const float colorFrequency = 4.0f; // Масштаб пятен
for (const auto& t : lod.triangles) {
// --- Вычисляем локальный базис треугольника (как в GetRotationForTriangle) ---
Vector3f vA = t.data[0];
Vector3f vB = t.data[1];
Vector3f vC = t.data[2];
Vector3f x_axis = (vC - vB).normalized(); // Направление U
Vector3f edge1 = vB - vA;
Vector3f edge2 = vC - vA;
Vector3f z_axis = edge1.cross(edge2).normalized(); // Нормаль плоскости
// Проверка направления нормали наружу (от центра планеты)
Vector3f centerToTri = (vA + vB + vC).normalized();
if (z_axis.dot(centerToTri) < 0) {
z_axis = z_axis * -1.0f;
}
Vector3f y_axis = z_axis.cross(x_axis).normalized(); // Направление V
for (int i = 0; i < 3; ++i) {
lod.vertexData.PositionData.push_back(t.data[i]);
lod.vertexData.NormalData.push_back(z_axis);
lod.vertexData.TexCoordData.push_back(triangleUVs[i]);
lod.vertexData.TangentData.push_back(x_axis);
lod.vertexData.BinormalData.push_back(y_axis);
// Используем один шум для выбора между розовым и желтым
Vector3f dir = t.data[i].normalized();
float blendFactor = colorPerlin.noise(
dir(0) * colorFrequency,
dir(1) * colorFrequency,
dir(2) * colorFrequency
);
// Приводим шум из диапазона [-1, 1] в [0, 1]
blendFactor = blendFactor * 0.5f + 0.5f;
// Линейная интерполяция между двумя цветами
Vector3f finalColor;
finalColor = colorPinkish + blendFactor * (colorYellowish - colorPinkish);
lod.vertexData.ColorData.push_back(finalColor);
}
}
}
LodLevel PlanetData::generateSphere(int subdivisions, float noiseCoeff) {
const float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
// 12 базовых вершин икосаэдра
std::vector<Vector3f> icosaVertices = {
{-1, t, 0}, { 1, t, 0}, {-1, -t, 0}, { 1, -t, 0},
{ 0, -1, t}, { 0, 1, t}, { 0, -1, -t}, { 0, 1, -t},
{ t, 0, -1}, { t, 0, 1}, {-t, 0, -1}, {-t, 0, 1}
};
// Нормализуем вершины
for (auto& v : icosaVertices) v = v.normalized();
// 20 граней икосаэдра
struct IndexedTri { int v1, v2, v3; };
std::vector<IndexedTri> faces = {
{0, 11, 5}, {0, 5, 1}, {0, 1, 7}, {0, 7, 10}, {0, 10, 11},
{1, 5, 9}, {5, 11, 4}, {11, 10, 2}, {10, 7, 6}, {7, 1, 8},
{3, 9, 4}, {3, 4, 2}, {3, 2, 6}, {3, 6, 8}, {3, 8, 9},
{4, 9, 5}, {2, 4, 11}, {6, 2, 10}, {8, 6, 7}, {9, 8, 1}
};
std::vector<Triangle> geometry;
for (auto& f : faces) {
Triangle tri;
tri.data[0] = icosaVertices[f.v1];
tri.data[1] = icosaVertices[f.v2];
tri.data[2] = icosaVertices[f.v3];
// Генерируем ID для базовых вершин (можно использовать их координаты)
for (int i = 0; i < 3; ++i) {
tri.ids[i] = std::to_string(tri.data[i](0)) + "_" +
std::to_string(tri.data[i](1)) + "_" +
std::to_string(tri.data[i](2));
}
geometry.push_back(tri);
}
// 3. Разбиваем N раз
for (int i = 0; i < subdivisions; i++) {
geometry = subdivideTriangles(geometry, 0.0f); // Шум пока игнорируем
}
// 4. Создаем LodLevel и заполняем топологию (v2tMap)
LodLevel lodLevel = createLodLevel(geometry);
// Пересобираем v2tMap (она критична для релаксации)
lodLevel.v2tMap.clear();
for (size_t i = 0; i < geometry.size(); ++i) {
for (int j = 0; j < 3; ++j) {
lodLevel.v2tMap[geometry[i].ids[j]].push_back((int)i);
}
}
// 5. Применяем итеративную релаксацию (Lloyd-like)
// 5-10 итераций достаточно для отличной сетки
applySphericalRelaxation(lodLevel, 8);
// 6. Накладываем шум и обновляем атрибуты
// ... (твой код наложения шума через Perlin)
recalculateMeshAttributes(lodLevel);
return lodLevel;
}
void PlanetData::applySphericalRelaxation(LodLevel& lod, int iterations) {
for (int iter = 0; iter < iterations; ++iter) {
std::map<VertexID, Vector3f> newPositions;
for (auto const& [vID, connectedTris] : lod.v2tMap) {
Vector3f centroid(0, 0, 0);
// Находим среднюю точку среди центров всех соседних треугольников
for (int triIdx : connectedTris) {
const auto& tri = lod.triangles[triIdx];
Vector3f faceCenter = (tri.data[0] + tri.data[1] + tri.data[2]) * (1.0f / 3.0f);
centroid = centroid + faceCenter;
}
centroid = centroid * (1.0f / (float)connectedTris.size());
// Проецируем обратно на единичную сферу
newPositions[vID] = centroid.normalized();
}
// Синхронизируем данные в треугольниках
for (auto& tri : lod.triangles) {
for (int i = 0; i < 3; ++i) {
tri.data[i] = newPositions[tri.ids[i]];
}
}
}
}
}

View File

@ -1,134 +0,0 @@
#pragma once
#include "utils/Perlin.h"
#include "render/Renderer.h"
#include <vector>
#include <array>
#include <string>
#include <map>
#include <set>
namespace ZL {
using VertexID = std::string;
using V2TMap = std::map<VertexID, std::vector<int>>;
struct Vector3fComparator {
bool operator()(const Eigen::Vector3f& a, const Eigen::Vector3f& b) const {
// Лексикографическое сравнение (x, затем y, затем z)
if (a.x() != b.x()) return a.x() < b.x();
if (a.y() != b.y()) return a.y() < b.y();
return a.z() < b.z();
}
};
VertexID generateEdgeID(const VertexID& id1, const VertexID& id2);
constexpr static int MAX_LOD_LEVELS = 6;
struct Triangle
{
std::array<Vector3f, 3> data;
std::array<VertexID, 3> ids;
Triangle()
{
}
Triangle(Vector3f p1, Vector3f p2, Vector3f p3)
: data{ p1, p2, p3 }
{
}
Triangle(std::array<Vector3f, 3> idata, std::array<VertexID, 3> iids)
: data{ idata }
, ids{ iids }
{
}
};
struct LodLevel
{
std::vector<Triangle> triangles;
VertexDataStruct vertexData;
std::vector<VertexID> VertexIDs;
V2TMap v2tMap;
void Scale(float s)
{
vertexData.Scale(s);
for (auto& t : triangles) {
for (int i = 0; i < 3; i++) {
t.data[i] = t.data[i] * s;
}
}
}
void Move(Vector3f pos)
{
vertexData.Move(pos);
for (auto& t : triangles) {
for (int i = 0; i < 3; i++) {
t.data[i] = t.data[i] + pos;
}
}
}
};
struct PlanetCampObject
{
Vector3f position;
Matrix3f rotation;
std::array<Vector3f, 5> platformPos = {
Vector3f{ 0.f, 0.f,-38.f },
Vector3f{ 20.f, 0.f,-18.f },
Vector3f{ 20.f, 0.f,-58.f },
Vector3f{ -20.f, 0.f,-58.f },
Vector3f{ -20.f, 0.f,-18.f }
};
};
class PlanetData {
public:
static const float PLANET_RADIUS;
static const Vector3f PLANET_CENTER_OFFSET;
private:
PerlinNoise perlin;
PerlinNoise colorPerlin;
std::array<LodLevel, MAX_LOD_LEVELS> planetMeshLods;
LodLevel planetAtmosphereLod;
//int currentLod; // Логический текущий уровень детализации
//std::map<Vector3f, VertexID, Vector3fComparator> initialVertexMap;
// Внутренние методы генерации
std::vector<Triangle> subdivideTriangles(const std::vector<Triangle>& inputTriangles, float noiseCoeff);
LodLevel createLodLevel(const std::vector<Triangle>& triangles);
void recalculateMeshAttributes(LodLevel& lod);
LodLevel generateSphere(int subdivisions, float noiseCoeff);
public:
PlanetData();
void init();
const LodLevel& getLodLevel() const;
const LodLevel& getAtmosphereLod() const;
// Логика
std::pair<float, float> calculateZRange(float distanceToSurface);
float distanceToPlanetSurfaceFast(const Vector3f& viewerPosition);
// Возвращает индексы треугольников, видимых камерой
std::vector<int> getBestTriangleUnderCamera(const Vector3f& viewerPosition);
std::vector<int> getTrianglesUnderCameraNew2(const Vector3f& viewerPosition);
void applySphericalRelaxation(LodLevel& lod, int iterations);
std::vector<PlanetCampObject> campObjects;
};
} // namespace ZL

View File

@ -1,611 +0,0 @@
#include "PlanetObject.h"
#include <random>
#include <cmath>
#include "render/OpenGlExtensions.h"
#include "Environment.h"
#include "StoneObject.h"
#include "utils/TaskManager.h"
#include "TextModel.h"
#include "GameConstants.h"
namespace ZL {
extern float x;
#if defined EMSCRIPTEN || defined __ANDROID__
using std::min;
using std::max;
#endif
extern const char* CONST_ZIP_FILE;
Matrix3f GetRotationForTriangle(const Triangle& tri) {
Vector3f vA = tri.data[0];
Vector3f vB = tri.data[1];
Vector3f vC = tri.data[2];
// 1. Вычисляем ось X (горизонталь).
Vector3f x_axis = (vC - vB).normalized();
// 2. Вычисляем нормаль (ось Z).
// Порядок cross product (AB x AC) определит "лицевую" сторону.
Vector3f edge1 = vB - vA;
Vector3f edge2 = vC - vA;
Vector3f z_axis = edge1.cross(edge2).normalized();
// 3. Вычисляем ось Y (вертикаль).
// В ортонормированном базисе Y всегда перпендикулярна Z и X.
Vector3f y_axis = z_axis.cross(x_axis).normalized();
// 4. Формируем прямую матрицу поворота (Rotation/World Matrix).
Matrix3f rot;
// Столбец 0: Ось X
rot.data()[0] = x_axis.data()[0];
rot.data()[3] = x_axis.data()[1];
rot.data()[6] = x_axis.data()[2];
// Столбец 1: Ось Y
rot.data()[1] = y_axis.data()[0];
rot.data()[4] = y_axis.data()[1];
rot.data()[7] = y_axis.data()[2];
// Столбец 2: Ось Z
rot.data()[2] = z_axis.data()[0];
rot.data()[5] = z_axis.data()[1];
rot.data()[8] = z_axis.data()[2];
return rot;
}
PlanetObject::PlanetObject(Renderer& iRenderer, TaskManager& iTaskManager, MainThreadHandler& iMainThreadHandler)
: renderer(iRenderer),
taskManager(iTaskManager),
mainThreadHandler(iMainThreadHandler)
{
}
void PlanetObject::init() {
// 1. Инициализируем данные (генерация мешей)
planetData.init();
// 2. Забираем данные для VBO
// Берем максимальный LOD для начальной отрисовки
planetRenderStruct.data = planetData.getLodLevel().vertexData;
//planetRenderStruct.data.PositionData.resize(9);
planetRenderStruct.RefreshVBO();
sandTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/sand2.png", CONST_ZIP_FILE));
stoneTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/rockdark3.png", CONST_ZIP_FILE));
// Атмосфера
planetAtmosphereRenderStruct.data = planetData.getAtmosphereLod().vertexData;
if (planetAtmosphereRenderStruct.data.PositionData.size() > 0)
{
planetAtmosphereRenderStruct.RefreshVBO();
}
planetStones = CreateStoneGroupData(778, planetData.getLodLevel());
//stonesToRender = planetStones.inflate(planetStones.allInstances.size());
stonesToRender.resize(planetStones.allInstances.size());
planetStones.initStatuses();
stoneToBake = planetStones.inflateOne(0, 0.75);
/*
campPlatform.data = LoadFromTextFile02("resources/platform1.txt", CONST_ZIP_FILE);
campPlatform.RefreshVBO();
campPlatformTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/platform_base.png", CONST_ZIP_FILE));
*/
}
void PlanetObject::update(float deltaTimeMs) {
// 1. Проверка порога движения (оптимизация из текущего кода)
float movementThreshold = 1.0f;
if ((Environment::shipState.position - lastUpdatePos).squaredNorm() < movementThreshold * movementThreshold
&& !triangleIndicesToDraw.empty()) {
//processMainThreadTasks(); // Все равно обрабатываем очередь OpenGL задач
return;
}
lastUpdatePos = Environment::shipState.position;
// 2. Получаем список видимых треугольников
auto newIndices = planetData.getTrianglesUnderCameraNew2(Environment::shipState.position);
std::sort(newIndices.begin(), newIndices.end());
// 3. Анализируем, что нужно загрузить
for (int triIdx : newIndices) {
if (planetStones.statuses[triIdx] == ChunkStatus::Empty) {
// Помечаем, чтобы не спамить задачами
planetStones.statuses[triIdx] = ChunkStatus::Generating;
// Отправляем тяжелую математику в TaskManager
taskManager.EnqueueBackgroundTask([this, triIdx]() {
// Выполняется в фоновом потоке: только генерация геометрии в RAM
float scaleModifier = 1.0f;
VertexDataStruct generatedData = planetStones.inflateOneDataOnly(triIdx, scaleModifier);
// Передаем задачу на загрузку в GPU в очередь главного потока
this->mainThreadHandler.EnqueueMainThreadTask([this, triIdx, data = std::move(generatedData)]() mutable {
// Проверяем актуальность: если треугольник всё еще в списке видимых
auto it = std::find(triangleIndicesToDraw.begin(), triangleIndicesToDraw.end(), triIdx);
if (it != triangleIndicesToDraw.end()) {
stonesToRender[triIdx].data = std::move(data);
stonesToRender[triIdx].RefreshVBO(); // OpenGL вызов
planetStones.statuses[triIdx] = ChunkStatus::Live;
}
else {
// Если уже не нужен — просто сбрасываем статус
planetStones.statuses[triIdx] = ChunkStatus::Empty;
}
});
});
}
}
// 4. Очистка (Unload)
// Если статус Live, но треугольника нет в новых индексах — освобождаем GPU память
for (size_t i = 0; i < planetStones.statuses.size(); ++i) {
if (planetStones.statuses[i] == ChunkStatus::Live) {
bool isVisible = std::binary_search(newIndices.begin(), newIndices.end(), (int)i);
if (!isVisible) {
// Очищаем данные и VBO (деструкторы shared_ptr в VertexRenderStruct сделают glDeleteBuffers)
stonesToRender[i].data.PositionData.clear();
stonesToRender[i].vao.reset();
stonesToRender[i].positionVBO.reset();
stonesToRender[i].normalVBO.reset();
stonesToRender[i].tangentVBO.reset();
stonesToRender[i].binormalVBO.reset();
stonesToRender[i].colorVBO.reset();
stonesToRender[i].texCoordVBO.reset();
planetStones.statuses[i] = ChunkStatus::Empty;
}
}
}
triangleIndicesToDraw = std::move(newIndices);
// 5. Выполняем одну задачу из очереди OpenGL (RefreshVBO и т.д.)
//processMainThreadTasks();
}
void PlanetObject::bakeStoneTexture(Renderer& renderer) {
glViewport(0, 0, 512, 512);
glClearColor(0,0,0, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
static const std::string planetBakeShaderName = "planetBake";
renderer.shaderManager.PushShader(planetBakeShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
Triangle tr = planetData.getLodLevel().triangles[0];
// 1. Получаем матрицу вращения (оси в столбцах)
Matrix3f mr = GetRotationForTriangle(tr);
// 2. Трансформируем вершины в локальное пространство экрана, чтобы найти габариты
// Используем MultMatrixVector(Matrix, Vector).
// Если ваша функция считает V * M, то передайте Inverse(mr).
Vector3f rA = mr * tr.data[0];
Vector3f rB = mr * tr.data[1];
Vector3f rC = mr * tr.data[2];
// 3. Вычисляем реальные границы треугольника после поворота
float minX = min(rA(0), min(rB(0), rC(0)));
float maxX = max(rA(0), max(rB(0), rC(0)));
float minY = min(rA(1), min(rB(1), rC(1)));
float maxY = max(rA(1), max(rB(1), rC(1)));
// Находим центр и размеры
float width = maxX - minX;
float height = maxY - minY;
float centerX = (minX + maxX) * 0.5f;
float centerY = (minY + maxY) * 0.5f;
//width = width * 0.995;
//height = height * 0.995;
renderer.PushProjectionMatrix(
centerX - width*0.5, centerX + width * 0.5,
centerY - height * 0.5, centerY + height * 0.5,
150, 200000);
renderer.PushMatrix();
renderer.LoadIdentity();
// Сдвигаем камеру по Z
renderer.TranslateMatrix(Vector3f{ 0, 0, -45000 });
// Применяем вращение
renderer.RotateMatrix(mr);
// Извлекаем нормаль треугольника (это 3-й столбец нашей матрицы вращения)
Vector3f planeNormal = mr.col(2);
renderer.RenderUniform3fv("uPlanePoint", tr.data[0].data());
renderer.RenderUniform3fv("uPlaneNormal", planeNormal.data());
renderer.RenderUniform1f("uMaxHeight", StoneParams::BASE_SCALE * 1.1f); // Соответствует BASE_SCALE + perturbation
glActiveTexture(GL_TEXTURE0);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK); // Отсекаем задние грани
if (stoneToBake.data.PositionData.size() > 0)
{
glBindTexture(GL_TEXTURE_2D, stoneTexture->getTexID());
renderer.DrawVertexRenderStruct(stoneToBake);
CheckGlError();
}
glDisable(GL_CULL_FACE); // Не забываем выключить, чтобы не сломать остальной рендер
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError();
}
void PlanetObject::draw(Renderer& renderer) {
{
if (stoneMapFB == nullptr)
{
//stoneMapFB = std::make_unique<FrameBuffer>(512, 512, true);
stoneMapFB = std::make_unique<FrameBuffer>(512, 512);
}
stoneMapFB->Bind();
bakeStoneTexture(renderer);
stoneMapFB->Unbind();
stoneMapFB->GenerateMipmaps();
}
glViewport(0, 0, Environment::width, Environment::height);
//--------------------------
drawPlanet(renderer);
drawStones(renderer);
//drawCamp(renderer);
glClear(GL_DEPTH_BUFFER_BIT);
drawAtmosphere(renderer);
}
void PlanetObject::drawPlanet(Renderer& renderer)
{
static const std::string planetLandShaderName = "planetLand";
static const std::string textureUniformName = "Texture";
renderer.shaderManager.PushShader(planetLandShaderName);
float dist = planetData.distanceToPlanetSurfaceFast(Environment::shipState.position);
auto zRange = planetData.calculateZRange(dist);
const float currentZNear = zRange.first;
const float currentZFar = zRange.second;
// 2. Применяем динамическую матрицу проекции
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
currentZNear, currentZFar);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
const Matrix4f viewMatrix = renderer.GetCurrentModelViewMatrix();
renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform1i("BakedTexture", 1);
Triangle tr = planetData.getLodLevel().triangles[0]; // Берем базовый треугольник
Matrix3f mr = GetRotationForTriangle(tr); // Та же матрица, что и при запекании
// Позиция камеры (корабля) в мире
renderer.RenderUniform3fv("uViewPos", Environment::shipState.position.data());
//renderer.RenderUniform1f("uHeightScale", 0.08f);
renderer.RenderUniform1f("uHeightScale", 0.0f);
renderer.RenderUniform1f("uDistanceToPlanetSurface", dist);
renderer.RenderUniform1f("uCurrentZFar", currentZFar);
// Направление на солнце в мировом пространстве
Vector3f sunDirWorld = Vector3f(1.0f, -1.0f, -1.0f).normalized();
renderer.RenderUniform3fv("uLightDirWorld", sunDirWorld.data());
// Направление от центра планеты к игроку для расчета дня/ночи
Vector3f playerDirWorld = Environment::shipState.position.normalized();
renderer.RenderUniform3fv("uPlayerDirWorld", playerDirWorld.data());
// Тот же фактор освещенности игрока
float playerLightFactor = playerDirWorld.dot(-sunDirWorld);
playerLightFactor = max(0.0f, (playerLightFactor + 0.2f) / 1.2f);
renderer.RenderUniform1f("uPlayerLightFactor", playerLightFactor);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, stoneMapFB->getTextureID());
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, sandTexture->getTexID());
//glBindTexture(GL_TEXTURE_2D, stoneMapFB->getTextureID());
renderer.DrawVertexRenderStruct(planetRenderStruct);
CheckGlError();
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError();
}
void PlanetObject::drawStones(Renderer& renderer)
{
static const std::string planetStoneShaderName = "planetStone";
renderer.shaderManager.PushShader(planetStoneShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
float dist = planetData.distanceToPlanetSurfaceFast(Environment::shipState.position);
auto zRange = planetData.calculateZRange(dist);
const float currentZNear = zRange.first;
const float currentZFar = zRange.second;
// 2. Применяем динамическую матрицу проекции
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
currentZNear, currentZFar);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
renderer.RenderUniform1f("uDistanceToPlanetSurface", dist);
renderer.RenderUniform1f("uCurrentZFar", currentZFar);
renderer.RenderUniform3fv("uViewPos", Environment::shipState.position.data());
//std::cout << "uViewPos" << Environment::shipState.position << std::endl;
// PlanetObject.cpp, метод drawStones
Vector3f sunDirWorld = Vector3f(1.0f, -1.0f, -1.0f).normalized();
renderer.RenderUniform3fv("uLightDirWorld", sunDirWorld.data());
Vector3f playerDirWorld = Environment::shipState.position.normalized();
float playerLightFactor = max(0.0f, (playerDirWorld.dot(-sunDirWorld) + 0.2f) / 1.2f);
renderer.RenderUniform1f("uPlayerLightFactor", playerLightFactor);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBindTexture(GL_TEXTURE_2D, stoneTexture->getTexID());
/*
for (int i : triangleIndicesToDraw)
//for (int i = 0; i < stonesToRender.size(); i++)
{
if (stonesToRender[i].data.PositionData.size() > 0)
{
renderer.DrawVertexRenderStruct(stonesToRender[i]);
}
}*/
for (int i : triangleIndicesToDraw) {
// КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ:
// Проверяем, что данные не просто существуют, а загружены в GPU
if (planetStones.statuses[i] == ChunkStatus::Live) {
// Дополнительная проверка на наличие данных (на всякий случай)
if (stonesToRender[i].data.PositionData.size() > 0) {
renderer.DrawVertexRenderStruct(stonesToRender[i]);
}
}
// Если статус Generating или Empty — мы просто пропускаем этот индекс.
// Камни появятся на экране плавно, как только отработает TaskManager и RefreshVBO.
}
CheckGlError();
glDisable(GL_BLEND);
glDisable(GL_CULL_FACE);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError();
}
void PlanetObject::drawAtmosphere(Renderer& renderer) {
static const std::string defaultShaderName = "defaultAtmosphere";
//glClear(GL_DEPTH_BUFFER_BIT);
glDepthMask(GL_FALSE);
renderer.shaderManager.PushShader(defaultShaderName);
float dist = planetData.distanceToPlanetSurfaceFast(Environment::shipState.position);
auto zRange = planetData.calculateZRange(dist);
float currentZNear = zRange.first;
float currentZFar = zRange.second;
if (currentZNear < 200)
{
currentZNear = 200;
currentZFar = currentZNear * 100;
}
// 2. Применяем динамическую матрицу проекции
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
currentZNear, currentZFar);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
const Matrix4f viewMatrix = renderer.GetCurrentModelViewMatrix();
Vector3f color = { 0.0, 0.5, 1.0 };
renderer.RenderUniform3fv("uColor", color.data());
renderer.RenderUniformMatrix4fv("ModelViewMatrix", false, viewMatrix.data());
renderer.RenderUniform1f("uDistanceToPlanetSurface", dist);
// В начале drawAtmosphere или как uniform
//float time = SDL_GetTicks() / 1000.0f;
//renderer.RenderUniform1f("uTime", time);
// В методе PlanetObject::drawAtmosphere
Vector3f worldLightDir = Vector3f(1.0f, -1.0f, -1.0f).normalized();
// Получаем текущую матрицу вида (ModelView без трансляции объекта)
// В вашем движке это Environment::inverseShipMatrix
Matrix3f viewMatrix2 = Environment::inverseShipMatrix;
// Трансформируем вектор света в пространство вида
Vector3f viewLightDir = viewMatrix2 * worldLightDir;
// Передаем инвертированный вектор (направление НА источник)
Vector3f lightToSource = -viewLightDir.normalized();
renderer.RenderUniform3fv("uLightDirView", lightToSource.data());
// В методе Game::drawCubemap
//renderer.RenderUniform1f("uTime", SDL_GetTicks() / 1000.0f);
// Направление света в мировом пространстве для освещения облаков
//Vector3f worldLightDir = Vector3f(1.0f, -1.0f, -1.0f).normalized();
renderer.RenderUniform3fv("uWorldLightDir", worldLightDir.data());
// 1. Рассчитываем uPlayerLightFactor (как в Game.cpp)
Vector3f playerDirWorld = Environment::shipState.position.normalized();
Vector3f sunDirWorld = Vector3f(1.0f, -1.0f, -1.0f).normalized();
// Насколько игрок на свету
float playerLightFactor = playerDirWorld.dot(-sunDirWorld);
playerLightFactor = max(0.0f, (playerLightFactor + 0.2f) / 1.2f); // Мягкий порог
// 2. ОБЯЗАТЕЛЬНО передаем в шейдер
renderer.RenderUniform1f("uPlayerLightFactor", playerLightFactor);
// 3. Убедитесь, что uSkyColor тоже передан (в коде выше его не было)
renderer.RenderUniform3fv("uSkyColor", color.data());
//Vector3f playerDirWorld = Environment::shipState.position.normalized();
renderer.RenderUniform3fv("uPlayerDirWorld", playerDirWorld.data());
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);// Аддитивное смешивание для эффекта свечения
renderer.DrawVertexRenderStruct(planetAtmosphereRenderStruct);
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError();
}
void PlanetObject::drawCamp(Renderer& renderer)
{
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
float dist = planetData.distanceToPlanetSurfaceFast(Environment::shipState.position);
auto zRange = planetData.calculateZRange(dist);
const float currentZNear = zRange.first;
const float currentZFar = zRange.second;
// 2. Применяем динамическую матрицу проекции
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
currentZNear, currentZFar);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
renderer.RenderUniform1f("uDistanceToPlanetSurface", dist);
renderer.RenderUniform1f("uCurrentZFar", currentZFar);
renderer.RenderUniform3fv("uViewPos", Environment::shipState.position.data());
//std::cout << "uViewPos" << Environment::shipState.position << std::endl;
// PlanetObject.cpp, метод drawStones
Vector3f sunDirWorld = Vector3f(1.0f, -1.0f, -1.0f).normalized();
renderer.RenderUniform3fv("uLightDirWorld", sunDirWorld.data());
Vector3f playerDirWorld = Environment::shipState.position.normalized();
float playerLightFactor = max(0.0f, (playerDirWorld.dot(-sunDirWorld) + 0.2f) / 1.2f);
renderer.RenderUniform1f("uPlayerLightFactor", playerLightFactor);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (int i = 0; i < planetData.campObjects.size(); i++)
{
renderer.PushMatrix();
for (int j = 0; j < 5; j++)
{
renderer.PushMatrix();
renderer.TranslateMatrix(planetData.campObjects[i].position);
renderer.RotateMatrix(planetData.campObjects[i].rotation);
renderer.ScaleMatrix(Vector3f{ 2.0f, 2.0f, 2.0f });
renderer.TranslateMatrix(planetData.campObjects[i].platformPos[j]);
glBindTexture(GL_TEXTURE_2D, campPlatformTexture->getTexID());
renderer.DrawVertexRenderStruct(campPlatform);
renderer.PopMatrix();
}
renderer.PopMatrix();
}
CheckGlError();
glDisable(GL_BLEND);
glDisable(GL_CULL_FACE);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
CheckGlError();
glClear(GL_DEPTH_BUFFER_BIT);
}
float PlanetObject::distanceToPlanetSurface(const Vector3f& viewerPosition)
{
return planetData.distanceToPlanetSurfaceFast(viewerPosition);
}
} // namespace ZL

View File

@ -1,69 +0,0 @@
#pragma once
#include "render/Renderer.h"
#include "render/TextureManager.h"
#include <vector>
#include <chrono>
#include <iostream>
#include <string>
#include <array>
#include <numeric>
#include <random>
#include <algorithm>
#include <map>
#include <set>
#include "utils/Perlin.h"
#include "PlanetData.h"
#include "StoneObject.h"
#include "render/FrameBuffer.h"
#include <queue>
#include <mutex>
namespace ZL {
class TaskManager;
class MainThreadHandler;
class PlanetObject {
public:
PlanetData planetData;
// Данные только для рендеринга (OpenGL specific)
VertexRenderStruct planetRenderStruct;
VertexRenderStruct planetAtmosphereRenderStruct;
StoneGroup planetStones;
std::vector<VertexRenderStruct> stonesToRender;
VertexRenderStruct stoneToBake;
std::vector<int> triangleIndicesToDraw;
std::shared_ptr<Texture> sandTexture;
std::shared_ptr<Texture> stoneTexture;
std::unique_ptr<FrameBuffer> stoneMapFB;
VertexRenderStruct campPlatform;
std::shared_ptr<Texture> campPlatformTexture;
Vector3f lastUpdatePos;
// External items, set outside
Renderer& renderer;
TaskManager& taskManager;
MainThreadHandler& mainThreadHandler;
public:
PlanetObject(Renderer& iRenderer, TaskManager& iTaskManager, MainThreadHandler& iMainThreadHandler);
void init();
void update(float deltaTimeMs);
void bakeStoneTexture(Renderer& renderer);
void draw(Renderer& renderer);
void drawStones(Renderer& renderer);
void drawPlanet(Renderer& renderer);
void drawAtmosphere(Renderer& renderer);
void drawCamp(Renderer& renderer);
float distanceToPlanetSurface(const Vector3f& viewerPosition);
};
} // namespace ZL

View File

@ -1,354 +0,0 @@
#include "StoneObject.h"
#include "utils/Utils.h"
#include <random>
#include <cmath>
#include "render/Renderer.h"
#include "PlanetData.h"
namespace ZL {
#if defined EMSCRIPTEN || defined __ANDROID__
using std::min;
using std::max;
#endif
const float StoneParams::BASE_SCALE = 10.0f; // Общий размер камня
const float StoneParams::MIN_AXIS_SCALE = 1.0f; // Минимальное растяжение/сжатие по оси
const float StoneParams::MAX_AXIS_SCALE = 1.0f; // Максимальное растяжение/сжатие по оси
const float StoneParams::MIN_PERTURBATION = 0.0f; // Минимальное радиальное возмущение вершины
const float StoneParams::MAX_PERTURBATION = 0.0f; // Максимальное радиальное возмущение вершины
const int StoneParams::STONES_PER_TRIANGLE = 40;
// Вспомогательная функция для получения случайного числа в диапазоне [min, max]
float getRandomFloat(std::mt19937& gen, float min, float max) {
std::uniform_real_distribution<> distrib(min, max);
return static_cast<float>(distrib(gen));
}
// Вспомогательная функция для генерации случайной точки на треугольнике
// Использует барицентрические координаты
Vector3f GetRandomPointOnTriangle(const Triangle& t, std::mt19937& engine) {
std::uniform_real_distribution<> distrib(0.0f, 1.0f);
float r1 = getRandomFloat(engine, 0.0f, 1.0f);
float r2 = getRandomFloat(engine, 0.0f, 1.0f);
// Преобразование r1, r2 для получения равномерного распределения
float a = 1.0f - std::sqrt(r1);
float b = std::sqrt(r1) * r2;
float c = 1.0f - a - b; // c = sqrt(r1) * (1 - r2)
// Барицентрические координаты
// P = a*p1 + b*p2 + c*p3
Vector3f p1_term = t.data[0] * a;
Vector3f p2_term = t.data[1] * b;
Vector3f p3_term = t.data[2] * c;
return p1_term + p2_term + p3_term;
}
// Икосаэдр (на основе золотого сечения phi)
// Координаты могут быть вычислены заранее для константного икосаэдра.
// Здесь только объявление, чтобы показать идею.
VertexDataStruct CreateBaseConvexPolyhedron(uint64_t seed) {
// const size_t SUBDIVISION_LEVEL = 1; // Уровень подразделения (для более круглого камня, пока опустим)
std::mt19937 engine(static_cast<unsigned int>(seed));
// Золотое сечение
const float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
// 12 вершин икосаэдра
std::vector<Vector3f> initialVertices = {
{ -1, t, 0 }, { 1, t, 0 }, { -1, -t, 0 }, { 1, -t, 0 },
{ 0, -1, t }, { 0, 1, t }, { 0, -1, -t }, { 0, 1, -t },
{ t, 0, -1 }, { t, 0, 1 }, { -t, 0, -1 }, { -t, 0, 1 }
};
// 20 треугольных граней (индексы вершин)
std::vector<std::array<int, 3>> faces = {
// 5 треугольников вокруг вершины 0
{0, 11, 5}, {0, 5, 1}, {0, 1, 7}, {0, 7, 10}, {0, 10, 11},
// 5 смежных полос
{1, 5, 9}, {5, 11, 4}, {11, 10, 2}, {10, 7, 6}, {7, 1, 8},
// 5 треугольников вокруг вершины 3
{3, 9, 4}, {3, 4, 2}, {3, 2, 6}, {3, 6, 8}, {3, 8, 9},
// 5 смежных полос
{4, 9, 5}, {2, 4, 11}, {6, 2, 10}, {8, 6, 7}, {9, 8, 1}
};
// 1. Нормализация и Возмущение (Perturbation)
for (Vector3f& v : initialVertices) {
v = v.normalized() * StoneParams::BASE_SCALE; // Нормализация к сфере радиуса BASE_SCALE
// Радиальное возмущение:
float perturbation = getRandomFloat(engine, StoneParams::MIN_PERTURBATION, StoneParams::MAX_PERTURBATION);
v = v * (1.0f + perturbation);
}
// 2. Трансформация (Масштабирование и Поворот)
// Случайные масштабы по осям
Vector3f scaleFactors = {
getRandomFloat(engine, StoneParams::MIN_AXIS_SCALE, StoneParams::MAX_AXIS_SCALE),
getRandomFloat(engine, StoneParams::MIN_AXIS_SCALE, StoneParams::MAX_AXIS_SCALE),
getRandomFloat(engine, StoneParams::MIN_AXIS_SCALE, StoneParams::MAX_AXIS_SCALE)
};
// Применяем масштабирование
for (Vector3f& v : initialVertices) {
v(0) *= scaleFactors(0);
v(1) *= scaleFactors(1);
v(2) *= scaleFactors(2);
}
/*
// Случайный поворот (например, вокруг трех осей)
Vector4f qx = Eigen::Quaternionf(Eigen::AngleAxisf(getRandomFloat(engine, 0.0f, 360.0f), Eigen::Vector3f::UnitX()));//QuatFromRotateAroundX(getRandomFloat(engine, 0.0f, 360.0f));
Vector4f qy = Eigen::Quaternionf(Eigen::AngleAxisf(getRandomFloat(engine, 0.0f, 360.0f), Eigen::Vector3f::UnitY()));//QuatFromRotateAroundY(getRandomFloat(engine, 0.0f, 360.0f));
Vector4f qz = Eigen::Quaternionf(Eigen::AngleAxisf(getRandomFloat(engine, 0.0f, 360.0f), Eigen::Vector3f::UnitZ()));//QuatFromRotateAroundZ(getRandomFloat(engine, 0.0f, 360.0f));
Vector4f qFinal = slerp(qx, qy, 0.5f); // Простой пример комбинирования
qFinal = slerp(qFinal, qz, 0.5f).normalized();
Matrix3f rotationMatrix = QuatToMatrix(qFinal);
for (Vector3f& v : initialVertices) {
v = MultMatrixVector(rotationMatrix, v);
}*/
// 3. Сглаженные Нормали и Формирование Mesh
VertexDataStruct result;
// Карта для накопления нормалей по уникальным позициям вершин
// (Требует определенного оператора < для Vector3f в ZLMath.h, который у вас есть)
std::map<Vector3f, Vector3f, Vector3fComparator> smoothNormalsMap;
// Предварительное заполнение карты нормалями
for (const auto& face : faces) {
Vector3f p1 = initialVertices[face[0]];
Vector3f p2 = initialVertices[face[1]];
Vector3f p3 = initialVertices[face[2]];
// Нормаль грани: (p2 - p1) x (p3 - p1)
Vector3f faceNormal = (p2 - p1).cross(p3 - p1).normalized();
smoothNormalsMap[p1] = smoothNormalsMap[p1] + faceNormal;
smoothNormalsMap[p2] = smoothNormalsMap[p2] + faceNormal;
smoothNormalsMap[p3] = smoothNormalsMap[p3] + faceNormal;
}
// Нормализация накопленных нормалей
for (auto& pair : smoothNormalsMap) {
pair.second = pair.second.normalized();
}
// 4. Финальное заполнение VertexDataStruct и Текстурные Координаты
for (const auto& face : faces) {
Vector3f p1 = initialVertices[face[0]];
Vector3f p2 = initialVertices[face[1]];
Vector3f p3 = initialVertices[face[2]];
// Позиции
result.PositionData.push_back(p1);
result.PositionData.push_back(p2);
result.PositionData.push_back(p3);
// Сглаженные Нормали (из карты)
result.NormalData.push_back(smoothNormalsMap[p1]);
result.NormalData.push_back(smoothNormalsMap[p2]);
result.NormalData.push_back(smoothNormalsMap[p3]);
// Текстурные Координаты (Планарная проекция на плоскость грани)
// p1 -> (0, 0), p2 -> (L_12, 0), p3 -> (L_13 * cos(angle), L_13 * sin(angle))
// Где L_xy - длина вектора, angle - угол между p2-p1 и p3-p1
Vector3f uAxis = (p2 - p1).normalized();
Vector3f vRaw = p3 - p1;
// Проекция vRaw на uAxis
float uProjLen = vRaw.dot(uAxis);
// Вектор V перпендикулярный U: vRaw - uProj
Vector3f vAxisRaw = vRaw - (uAxis * uProjLen);
// Длина оси V
float vLen = vAxisRaw.norm();
// Нормализованная ось V
Vector3f vAxis = vAxisRaw.normalized();
// Координаты (u, v) для p1, p2, p3 относительно p1
Vector2f uv1 = { 0.0f, 0.0f };
Vector2f uv2 = { (p2 - p1).norm(), 0.0f }; // p2-p1 вдоль оси U
Vector2f uv3 = { uProjLen, vLen }; // p3-p1: u-компонента = uProjLen, v-компонента = vLen
// Находим максимальный размер, чтобы масштабировать в [0, 1]
float maxUV = max(uv2(0), max(uv3(0), uv3(1)));
if (maxUV > 0.000001f) {
// Масштабируем:
result.TexCoordData.push_back(uv1);
result.TexCoordData.push_back(uv2 * (1.f / maxUV));
result.TexCoordData.push_back(uv3 * (1.f / maxUV));
}
else {
// Предотвращение деления на ноль для вырожденных граней
result.TexCoordData.push_back({ 0.0f, 0.0f });
result.TexCoordData.push_back({ 0.0f, 0.0f });
result.TexCoordData.push_back({ 0.0f, 0.0f });
}
}
return result;
}
Triangle createLocalTriangle(const Triangle& sampleTri)
{
// Находим центр в 3D
Vector3f center = (sampleTri.data[0] + sampleTri.data[1] + sampleTri.data[2]) * (1.0f / 3.0f);
// Строим базис самого треугольника
// vY направляем на 0-ю вершину (как в вашем Special расчете)
Vector3f vY = (sampleTri.data[0] - center).normalized();
// Временный X для расчета нормали
Vector3f vX_temp = (sampleTri.data[1] - sampleTri.data[2]).normalized();
// Чистая нормаль
Vector3f vZ = vX_temp.cross(vY).normalized();
// Чистый X, перпендикулярный Y и Z
Vector3f vX = vY.cross(vZ).normalized();
// Переводим 3D точки в этот 2D базис (Z зануляется сам собой)
auto toLocal = [&](const Vector3f& p) {
Vector3f d = p - center;
return Vector3f{ d.dot(vX), d.dot(vY), 0.0f };
};
Triangle local;
local.data[0] = toLocal(sampleTri.data[0]);
local.data[1] = toLocal(sampleTri.data[1]);
local.data[2] = toLocal(sampleTri.data[2]);
return local;
}
StoneGroup CreateStoneGroupData(uint64_t globalSeed, const LodLevel& planetLodLevel) {
StoneGroup group;
group.allInstances.resize(planetLodLevel.triangles.size());
for (size_t tIdx = 0; tIdx < planetLodLevel.triangles.size(); ++tIdx) {
const Triangle& tri = planetLodLevel.triangles[tIdx];
std::mt19937 engine(static_cast<unsigned int>(globalSeed));
for (int i = 0; i < StoneParams::STONES_PER_TRIANGLE; ++i) {
StoneInstance instance;
instance.seed = globalSeed;// + tIdx * 1000 + i;
instance.position = GetRandomPointOnTriangle(tri, engine);
float SCALE_MIN = 0.75f;
float SCALE_MAX = 2.5f;
instance.scale = {
getRandomFloat(engine, SCALE_MIN, SCALE_MAX),
getRandomFloat(engine, SCALE_MIN, SCALE_MAX),
getRandomFloat(engine, SCALE_MIN, SCALE_MAX)
};
/*
if (tIdx == 0)
{
instance.scale = instance.scale * 0.7f;
}*/
/*
Vector4f qx = QuatFromRotateAroundX(getRandomFloat(engine, 0.0f, 360.0f));
Vector4f qy = QuatFromRotateAroundY(getRandomFloat(engine, 0.0f, 360.0f));
Vector4f qz = QuatFromRotateAroundZ(getRandomFloat(engine, 0.0f, 360.0f));
instance.rotation = slerp(slerp(qx, qy, 0.5f), qz, 0.5f).normalized();
*/
instance.rotation = Vector4f(0.f, 0.f, 0.f, 1.f);
group.allInstances[tIdx].push_back(instance);
}
}
return group;
}
std::vector<VertexRenderStruct> StoneGroup::inflate(int count)
{
std::vector<VertexRenderStruct> result;
result.resize(count);
for (int tIdx = 0; tIdx < count; tIdx++)
{
result[tIdx] = inflateOne(tIdx, 1.0f);
}
return result;
}
VertexRenderStruct StoneGroup::inflateOne(int index, float scaleModifier)
{
static VertexDataStruct baseStone = CreateBaseConvexPolyhedron(1337);
VertexRenderStruct result;
for (const auto& inst : allInstances[index]) {
Matrix3f rotMat = inst.rotation.toRotationMatrix();
for (size_t j = 0; j < baseStone.PositionData.size(); ++j) {
Vector3f p = baseStone.PositionData[j];
Vector3f n = baseStone.NormalData[j];
p(0) *= inst.scale(0) * scaleModifier;
p(1) *= inst.scale(1) * scaleModifier;
p(2) *= inst.scale(2) * scaleModifier;
result.data.PositionData.push_back(rotMat * p + inst.position);
result.data.NormalData.push_back(rotMat * n);
result.data.TexCoordData.push_back(baseStone.TexCoordData[j]);
}
result.RefreshVBO();
}
return result;
}
VertexDataStruct StoneGroup::inflateOneDataOnly(int index, float scaleModifier)
{
static VertexDataStruct baseStone = CreateBaseConvexPolyhedron(1337);
VertexDataStruct result;
for (const auto& inst : allInstances[index]) {
Matrix3f rotMat = inst.rotation.toRotationMatrix();
for (size_t j = 0; j < baseStone.PositionData.size(); ++j) {
Vector3f p = baseStone.PositionData[j];
Vector3f n = baseStone.NormalData[j];
p(0) *= inst.scale(0) * scaleModifier;
p(1) *= inst.scale(1) * scaleModifier;
p(2) *= inst.scale(2) * scaleModifier;
result.PositionData.push_back(rotMat * p + inst.position);
result.NormalData.push_back(rotMat * n);
result.TexCoordData.push_back(baseStone.TexCoordData[j]);
}
}
return result;
}
} // namespace ZL

View File

@ -1,57 +0,0 @@
#pragma once
#include "render/Renderer.h"
#include "PlanetData.h"
namespace ZL {
struct StoneParams
{
static const float BASE_SCALE; // Общий размер камня
static const float MIN_AXIS_SCALE; // Минимальное растяжение/сжатие по оси
static const float MAX_AXIS_SCALE; // Максимальное растяжение/сжатие по оси
static const float MIN_PERTURBATION; // Минимальное радиальное возмущение вершины
static const float MAX_PERTURBATION; // Максимальное радиальное возмущение вершины
static const int STONES_PER_TRIANGLE;
};
struct StoneInstance {
uint64_t seed;
Vector3f position;
Vector3f scale;
Eigen::Quaternionf rotation;
};
enum class ChunkStatus {
Empty, // Данных нет
Generating, // Задача в TaskManager (CPU)
ReadyToUpload, // Данные в памяти, ждут очереди в главный поток
Live // Загружено в GPU и готово к отрисовке
};
struct StoneGroup {
// mesh.PositionData и прочие будут заполняться в inflate()
VertexDataStruct mesh;
std::vector<std::vector<StoneInstance>> allInstances;
// Очищает старую геометрию и генерирует новую для указанных индексов
std::vector<VertexRenderStruct> inflate(int count);
VertexRenderStruct inflateOne(int index, float scaleModifier);
VertexDataStruct inflateOneDataOnly(int index, float scaleModifier);
std::vector<ChunkStatus> statuses;
// Инициализация статусов при создании группы
void initStatuses() {
statuses.assign(allInstances.size(), ChunkStatus::Empty);
}
};
// Теперь возвращает заготовку со всеми параметрами, но без тяжелого меша
StoneGroup CreateStoneGroupData(uint64_t globalSeed, const LodLevel& lodLevel);
Triangle createLocalTriangle(const Triangle& sampleTri);
} // namespace ZL

View File

@ -329,6 +329,16 @@ namespace ZL {
size_t error = glGetError();
if (error != GL_NO_ERROR)
{
std::cout << "OpenGL error: " << error << std::endl;
throw std::runtime_error("Gl error");
}
}
void CheckGlError(const char* file, int line)
{
size_t error = glGetError();
if (error != GL_NO_ERROR)
{
std::cout << "OpenGL error: " << error << " happened in file: " << file << " at line: " << line << std::endl;
throw std::runtime_error("Gl error");
}
}

View File

@ -161,4 +161,6 @@ namespace ZL {
bool BindOpenGlFunctions();
void CheckGlError();
void CheckGlError(const char* file, int line);
}

View File

@ -536,7 +536,7 @@ namespace ZL {
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDepthFunc(GL_LEQUAL);
CheckGlError();
CheckGlError(__FILE__, __LINE__);
}
void Renderer::PushProjectionMatrix(float width, float height, float zNear, float zFar)

View File

@ -56,6 +56,22 @@ namespace ZL {
{
glGenFramebuffers(1, &fbo);
#ifdef EMSCRIPTEN
glGenTextures(1, &depthTexture);
glBindTexture(GL_TEXTURE_2D, depthTexture);
// Используем GL_DEPTH_COMPONENT24 для WebGL 2
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24,
resolution, resolution, 0,
GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL);
// ВАЖНО: Используем GL_NEAREST, так как GL_LINEAR часто ломает FBO в WebGL
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
#else
glGenTextures(1, &depthTexture);
glBindTexture(GL_TEXTURE_2D, depthTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
@ -65,17 +81,24 @@ namespace ZL {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
#endif
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_TEXTURE_2D, depthTexture, 0);
// No color buffer for this FBO — depth only.
#ifdef EMSCRIPTEN
GLenum drawBuffers[] = { GL_NONE };
glDrawBuffers(1, drawBuffers);
#else
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
#endif
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "Error: Shadow map framebuffer is not complete!" << std::endl;
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "Error: Shadow map framebuffer is not complete! Status: 0x"
<< std::hex << status << std::endl;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);

View File

@ -54,13 +54,13 @@ bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixe
zipfilename
);
#endif
ZL::CheckGlError();
ZL::CheckGlError(__FILE__, __LINE__);
if (!loadGlyphs(ttfPath, pixelSize, zipfilename)) return false;
ZL::CheckGlError();
ZL::CheckGlError(__FILE__, __LINE__);
textMesh.data.PositionData.resize(6, Eigen::Vector3f(0, 0, 0));
textMesh.RefreshVBO();
ZL::CheckGlError();
ZL::CheckGlError(__FILE__, __LINE__);
return true;
}

View File

@ -54,7 +54,7 @@ namespace ZL
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat,
static_cast<GLsizei>(width), static_cast<GLsizei>(height),
0, externalFormat, GL_UNSIGNED_BYTE, texData.data.data());
CheckGlError();
CheckGlError(__FILE__, __LINE__);
// 3. Фильтрация и Мип-мапы
// ВНИМАНИЕ: Для шрифтов (NPOT) в WebGL glGenerateMipmap работать НЕ будет!
@ -67,7 +67,7 @@ namespace ZL
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
CheckGlError();
CheckGlError(__FILE__, __LINE__);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
}
@ -91,7 +91,7 @@ namespace ZL
glBindTexture(GL_TEXTURE_CUBE_MAP, texID);
CheckGlError();
CheckGlError(__FILE__, __LINE__);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
@ -106,7 +106,7 @@ namespace ZL
#endif
#endif
CheckGlError();
CheckGlError(__FILE__, __LINE__);
for (int i = 0; i < 6; ++i)
{
@ -136,7 +136,7 @@ namespace ZL
GL_UNSIGNED_BYTE,
texDataArray[i].data.data()
);
CheckGlError();
CheckGlError(__FILE__, __LINE__);
}
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);