feat: complete native client rendering parity updates
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
find_package(Vulkan REQUIRED)
|
||||
find_package(SDL3 CONFIG REQUIRED)
|
||||
find_program(GLSLC glslc REQUIRED)
|
||||
|
||||
set(MT_NATIVE_VERT_SPV "${CMAKE_CURRENT_BINARY_DIR}/native.vert.spv")
|
||||
set(MT_NATIVE_FRAG_SPV "${CMAKE_CURRENT_BINARY_DIR}/native.frag.spv")
|
||||
add_custom_command(OUTPUT "${MT_NATIVE_VERT_SPV}"
|
||||
COMMAND "${GLSLC}" -fshader-stage=vert "${CMAKE_CURRENT_SOURCE_DIR}/native.vert" -o "${MT_NATIVE_VERT_SPV}"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/native.vert")
|
||||
add_custom_command(OUTPUT "${MT_NATIVE_FRAG_SPV}"
|
||||
COMMAND "${GLSLC}" -fshader-stage=frag "${CMAKE_CURRENT_SOURCE_DIR}/native.frag" -o "${MT_NATIVE_FRAG_SPV}"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/native.frag")
|
||||
add_custom_target(mt_native_shaders DEPENDS "${MT_NATIVE_VERT_SPV}" "${MT_NATIVE_FRAG_SPV}")
|
||||
|
||||
set(MT_NATIVE_RENDER_SOURCES
|
||||
main.cpp
|
||||
stb_image_impl.cpp
|
||||
"${PROJECT_SOURCE_DIR}/extension/src/dxt.cpp"
|
||||
)
|
||||
if(TARGET port_platform AND TARGET mtpython)
|
||||
list(APPEND MT_NATIVE_RENDER_SOURCES
|
||||
"${PROJECT_SOURCE_DIR}/extension/tests/port_login_flow_server.cpp"
|
||||
"${PROJECT_SOURCE_DIR}/extension/src/net/classic/classic_cipher.cpp"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
# SDLActivity loads libmain.so; Android does not launch a native executable.
|
||||
set(MT_NATIVE_TARGET main)
|
||||
add_library(${MT_NATIVE_TARGET} SHARED ${MT_NATIVE_RENDER_SOURCES})
|
||||
else()
|
||||
set(MT_NATIVE_TARGET mt_native_render)
|
||||
add_executable(${MT_NATIVE_TARGET} ${MT_NATIVE_RENDER_SOURCES})
|
||||
endif()
|
||||
if(APPLE)
|
||||
set_target_properties(${MT_NATIVE_TARGET} PROPERTIES
|
||||
OSX_ARCHITECTURES "${CMAKE_HOST_SYSTEM_PROCESSOR}"
|
||||
)
|
||||
endif()
|
||||
add_dependencies(${MT_NATIVE_TARGET} mt_native_shaders)
|
||||
target_include_directories(${MT_NATIVE_TARGET} PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/third_party"
|
||||
"${PROJECT_SOURCE_DIR}/extension/src"
|
||||
"${PROJECT_SOURCE_DIR}/extension/src/platform/EterLib")
|
||||
if(NOT ANDROID)
|
||||
add_custom_command(TARGET ${MT_NATIVE_TARGET} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MT_NATIVE_VERT_SPV}" "$<TARGET_FILE_DIR:${MT_NATIVE_TARGET}>/native.vert.spv"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MT_NATIVE_FRAG_SPV}" "$<TARGET_FILE_DIR:${MT_NATIVE_TARGET}>/native.frag.spv")
|
||||
endif()
|
||||
target_link_libraries(${MT_NATIVE_TARGET} PRIVATE Vulkan::Vulkan SDL3::SDL3)
|
||||
if(APPLE)
|
||||
target_link_libraries(${MT_NATIVE_TARGET} PRIVATE
|
||||
"-framework AudioToolbox")
|
||||
endif()
|
||||
if(TARGET port_platform AND TARGET mtpython)
|
||||
target_compile_definitions(${MT_NATIVE_TARGET} PRIVATE MT_NATIVE_HAS_LIVE_CLIENT=1)
|
||||
target_link_libraries(${MT_NATIVE_TARGET} PRIVATE port_platform)
|
||||
if(TARGET mtpython_stdlib)
|
||||
add_dependencies(${MT_NATIVE_TARGET} mtpython_stdlib)
|
||||
if(NOT ANDROID)
|
||||
add_custom_command(TARGET ${MT_NATIVE_TARGET} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MT_PYTHON_STDLIB_ZIP}" "$<TARGET_FILE_DIR:${MT_NATIVE_TARGET}>/python27.zip")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
# Add this directory to the SDL Android app's assets.srcDirs.
|
||||
set(MT_NATIVE_ANDROID_ASSETS "${CMAKE_CURRENT_BINARY_DIR}/android-assets")
|
||||
add_custom_target(mt_native_android_assets ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${MT_NATIVE_ANDROID_ASSETS}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${MT_NATIVE_VERT_SPV}" "${MT_NATIVE_ANDROID_ASSETS}/native.vert.spv"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${MT_NATIVE_FRAG_SPV}" "${MT_NATIVE_ANDROID_ASSETS}/native.frag.spv"
|
||||
DEPENDS mt_native_shaders)
|
||||
if(TARGET mtpython_stdlib)
|
||||
add_dependencies(mt_native_android_assets mtpython_stdlib)
|
||||
add_custom_command(TARGET mt_native_android_assets POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MT_PYTHON_STDLIB_ZIP}" "${MT_NATIVE_ANDROID_ASSETS}/python27.zip")
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,153 @@
|
||||
# Native Vulkan renderer prototype
|
||||
|
||||
This is the standalone renderer path for the 40250 `Render3DDraw` and `UIRenderCommand` command streams, plus direct `--live-client` execution of the ported 40250 client (`PythonBoot`). SDL3 owns the window and input forwarding; Vulkan owns the swapchain (`FIFO` or `IMMEDIATE`/`MAILBOX`), `VK_FORMAT_D32_SFLOAT` depth buffer, `VK_QUERY_TYPE_TIMESTAMP` hardware GPU timer, key-indexed persistent vertex/index buffers, GPU skeletal skinning bone palette SSBO (`set = 1, binding = 0`), DDS/TGA/`mem:` glyph-page texture sampler descriptors, fixed-function 3D + 2D UI state pipelines, and draw submission. On macOS, the Vulkan loader uses MoltenVK over Metal. The existing Godot client remains the default playable path.
|
||||
|
||||
## Build and run on macOS
|
||||
|
||||
Install Vulkan headers/loader, MoltenVK, SDL3 and `glslc` (shaderc). With Homebrew:
|
||||
|
||||
```sh
|
||||
brew install vulkan-headers vulkan-loader molten-vk sdl3 shaderc
|
||||
|
||||
# 1. Standalone replay build (without port_platform)
|
||||
cmake -S . -B build-native-render -DMTGODOT_BUILD_EXTENSION=OFF \
|
||||
-DMT_BUILD_NATIVE_RENDER_PROTOTYPE=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
|
||||
cmake --build build-native-render --target mt_native_render -j8
|
||||
|
||||
# 2. Full live-client build (links port_platform + mtpython + FakeLoginServer)
|
||||
cmake -S . -B build -DMT_BUILD_NATIVE_RENDER_PROTOTYPE=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
|
||||
cmake --build build --target mtgodot mt_native_render port_fake_login_server -j8
|
||||
|
||||
# 3. Optimized Release (-O3) live-client build
|
||||
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release -DMT_BUILD_NATIVE_RENDER_PROTOTYPE=ON \
|
||||
-DMTGODOT_EMBED_PYTHON=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
|
||||
cmake --build build-release --target mt_native_render -j8
|
||||
```
|
||||
|
||||
On macOS, `mt_native_render` automatically detects `/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json` (or `/usr/local/etc/vulkan/icd.d/MoltenVK_icd.json`) when `VK_ICD_FILENAMES` is not set in the environment.
|
||||
The build copies `native.vert.spv`, `native.frag.spv`, and, for the live client,
|
||||
`python27.zip` beside the executable. Keep these files together when moving the
|
||||
binary. In a macOS `.app`, put them in `Contents/Resources` (the directory
|
||||
returned by SDL's `SDL_GetBasePath`). `MT_PYTHON_STDLIB` can override the zip path.
|
||||
|
||||
## Android integration
|
||||
|
||||
With an Android toolchain and SDL3 Android AAR/Prefab available to CMake, the
|
||||
same option builds `libmain.so` for SDLActivity. The `mt_native_android_assets`
|
||||
target prepares `native.vert.spv`, `native.frag.spv`, and `python27.zip` under
|
||||
`<build>/native_render/android-assets` (`python27.zip` is included when the
|
||||
embedded Python target is enabled); include that directory in the SDL app's
|
||||
`assets.srcDirs`. The app must allow network access for login. The 40250 `Client`
|
||||
directory must be placed at `SDL_GetPrefPath("mtgodot", "native-render")/Client`,
|
||||
with `pack/Index` present, before live mode starts. The Godot APK is a separate
|
||||
application and does not launch this SDL renderer.
|
||||
|
||||
The standalone `arm64-v8a` renderer cross-builds at Android API 24. The full
|
||||
live-client cross-build currently stops in `extension/src/port/common/Win32Crt.cpp`:
|
||||
the NDK exposes `<iconv.h>` at API 24 but does not declare `iconv` until API 28.
|
||||
This is a port-runtime prerequisite for an Android live-client APK; raising the
|
||||
minimum API level is not assumed here.
|
||||
|
||||
The Vulkan portability enumeration extension is selected only when advertised
|
||||
by the loader. DDS, TGA, and memory textures retain their existing decoders;
|
||||
JPEG, PNG, and BMP use the same portable decoder on macOS and Android. The
|
||||
vendored `stb_image.h` is upstream v2.30 (SHA-256
|
||||
`594c2fe35d49488b4382dbfaec8f98366defca819d916ac95becf3e75f4200b3`).
|
||||
|
||||
### Interactive Playable Modes
|
||||
|
||||
Run the full 40250 client interactively (infinite frame loop until window close, resizable SDL3 window with automatic Vulkan swapchain recreation and `PythonBoot::SetUISize` sync, full keyboard/IME text input, SDL hardware cursor built from the original cursor images, and SDL3 + `AudioToolbox` `.wav`/`.mp3` audio):
|
||||
|
||||
```sh
|
||||
# Interactive outdoor map session (auto-login via loopback FakeLoginServer)
|
||||
./build-release/native_render/mt_native_render \
|
||||
--live-client "/path/to/40250/Server Client TMP4/Client" \
|
||||
--interactive --fake-mobs 24 --width 1280 --height 800
|
||||
|
||||
# Interactive login screen (stops at introLogin.LoginWindow for manual typing/login)
|
||||
./build-release/native_render/mt_native_render \
|
||||
--live-client "/path/to/40250/Server Client TMP4/Client" \
|
||||
--login-screen --width 1024 --height 768
|
||||
|
||||
# Connect to an external 40250 Auth + Game server
|
||||
./build-release/native_render/mt_native_render \
|
||||
--live-client "/path/to/40250/Server Client TMP4/Client" \
|
||||
--live-server 127.0.0.1:11002:13000 --login-screen
|
||||
```
|
||||
|
||||
### Synthetic benchmark
|
||||
|
||||
```sh
|
||||
./build-release/native_render/mt_native_render --frames 60 --draws 64 --triangles-per-draw 333 --no-vsync
|
||||
```
|
||||
|
||||
Timing metrics printed by `mt_native_render`:
|
||||
- `p95_frame_ms`, `p99_frame_ms`, `max_frame_ms`: wall-clock time for each measured update/render iteration, excluding startup and final GPU drain. These include vsync wait when enabled; use them alongside `gpu_ms` and the CPU breakdown.
|
||||
- `game_update_ms`: mean CPU time spent in `PythonBoot::UIUpdate()`, `PythonBoot::UIRender()`, and audio command draining per frame in `--live-client` mode.
|
||||
- `prepare_ms`: mean CPU draw-preparation time across all frames (including frame 0 cold-start geometry/texture uploads and pipeline creation).
|
||||
- `steady_prepare_ms`: mean CPU draw-preparation time on frames after frame 0 (bone palette copy, UI quad batching, command recording).
|
||||
- `submit_ms`: host CPU time around `vkQueueSubmit` (in `FIFO` mode this includes swapchain backpressure; pass `--no-vsync` to switch to `IMMEDIATE`/`MAILBOX`).
|
||||
- `gpu_ms`: true hardware GPU execution time between top-of-pipe `vkCmdBeginRenderPass` and bottom-of-pipe `vkCmdEndRenderPass` measured via `VK_QUERY_TYPE_TIMESTAMP`.
|
||||
|
||||
### macOS real-server acceptance
|
||||
|
||||
Build the Release live-client target above, then start the evidence runner from the repository root:
|
||||
|
||||
```sh
|
||||
# First verify the runner and renderer using the local fake server.
|
||||
node script/native_mac_acceptance.mjs --fake --frames 180
|
||||
|
||||
# Use the real server's shared host, auth port, and game channel port.
|
||||
node script/native_mac_acceptance.mjs --server HOST:AUTH_PORT:GAME_PORT
|
||||
```
|
||||
|
||||
Real-server mode checks both ports before starting, opens the native login screen,
|
||||
and collects a redacted client log, one-second process RSS samples, and a JSON
|
||||
report under `build/native-acceptance/`. Enter credentials in the app, then
|
||||
exercise login, character selection, movement, combat, map changes, inventory,
|
||||
chat, window resize/focus, and visual comparison with the current client. Close
|
||||
the window after at least 30 minutes. The report records whether that minimum
|
||||
was met, but remains `NEEDS_MANUAL_REVIEW` until those actions and visual results
|
||||
are checked by a person. It does not assert that RSS alone proves no GPU leak.
|
||||
|
||||
`--live-server` currently accepts one shared host for the auth and game ports.
|
||||
If those endpoints use different hosts, update the native connection setup
|
||||
before claiming a real-server pass. Credentials are entered in the client UI;
|
||||
the runner never puts them in arguments or the report.
|
||||
|
||||
## Run the real 40250 client benchmark in native Vulkan (`--live-client`)
|
||||
|
||||
When built with `port_platform`, `mt_native_render` boots `system.py`, logs in via loopback `FakeLoginServer`, enters the outdoor map with 1..64 monsters, renders the complete 3D scene (40250 hardware-transform terrain splats, animated water patches, gradient skybox & scrolling clouds, SpeedTree forest bark/leaf geometry, GPU-skinned characters with stage-1 specular sphere-maps, and 2D UI/minimap/text-tails/software cursor), and optionally writes a Version 5 `.mtdr` capture:
|
||||
|
||||
```sh
|
||||
./build-release/native_render/mt_native_render \
|
||||
--live-client "/path/to/40250/Server Client TMP4/Client" \
|
||||
--fake-mobs 64 --frames 180 --gpu-skinning --no-vsync \
|
||||
--capture-out /tmp/mt_full_64mobs.mtdr
|
||||
```
|
||||
|
||||
Compare against CPU skinning (`GrannyDeformVertices` on CPU + per-frame vertex buffer re-uploads) with `--no-gpu-skinning`:
|
||||
|
||||
```sh
|
||||
./build-release/native_render/mt_native_render \
|
||||
--live-client "/path/to/40250/Server Client TMP4/Client" \
|
||||
--fake-mobs 64 --frames 180 --no-gpu-skinning --no-vsync
|
||||
```
|
||||
|
||||
## Replay a captured frame (`.mtdr` v1 / v2 / v3 / v4 / v5)
|
||||
|
||||
```sh
|
||||
./build-release/native_render/mt_native_render \
|
||||
--capture /tmp/mt_full_64mobs.mtdr --frames 180 --animate-bones --no-vsync
|
||||
```
|
||||
|
||||
- Pass `--animate-bones` to animate the bone palette SSBO each frame without re-uploading any vertex buffers (`uploads` stays equal to unique static geometries uploaded on frame 0).
|
||||
- Pass `--animate-first-draw` to increment the first draw's `geometry_revision` each frame after frame 0 and verify incremental GPU buffer re-uploads.
|
||||
|
||||
Capture format Version 5 (backward-compatible with Versions 1–4) stores:
|
||||
1. 3D draws (`Render3DDraw`): `geometry_key`, `geometry_revision`, matrices, positions, normals, UVs, diffuse colors, indices, D3D8 fixed-function states, `texture0` / `texture1` names, and GPU skinning data (`bone_indices`, `bone_weights`, `bone_matrices`).
|
||||
2. Self-contained textures: `.dds`, `.tga`, `.jpg`, `.png`, and `.bmp` pack bytes plus `"MTRA"` raw RGBA memory textures (`mem:<id>@<revision>` font glyph pages).
|
||||
3. 2D UI stream (`UIRenderCommand`): canvas size (`ui_width`, `ui_height`) and all `Bar`, `GradientBar`, `Line`, and `Image` commands (including `behind_3d`, clip rects, minimap mask UV coordinates, and software mouse cursor quads).
|
||||
4. Per-draw fog color, vertex/table mode, range flag, start/end distance and density. Older captures omit these fields and replay without fog.
|
||||
|
||||
The native shader now evaluates recorded D3D8 stage 0/1 color and alpha operations, including the original cloud operation (`D3DTOP_MODULATEINVALPHA_ADDCOLOR = 20`), and applies the captured linear or exponential fog. Expanded UI image modes use the 40250 blend factors for screen/color-dodge and modulate. These state fixes do not by themselves establish pixel parity with a Windows 40250 screenshot; compare the same map, time, camera and UI state before treating a color difference as resolved.
|
||||
@@ -0,0 +1,378 @@
|
||||
#pragma once
|
||||
|
||||
#include "../extension/src/platform/EterLib/RenderCommands3D.h"
|
||||
#include "../extension/src/platform/EterLib/UIRenderCommands.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// Diagnostic, little-endian arm64 format:
|
||||
// - Version 1: matrices, positions, diffuse, indices.
|
||||
// - Version 2: adds stable geometry_key and geometry_revision.
|
||||
// - Version 3: adds normals, UVs, D3D8 fixed-function states, and embedded pack texture bytes.
|
||||
// - Version 4: adds GPU skeletal skinning streams (bone_indices, bone_weights, bone_matrices)
|
||||
// and 2D UIRenderCommand stream + memory textures ("MTRA").
|
||||
// - Version 5: adds per-draw fog color, mode, range and distance/density state.
|
||||
// - Version 6: adds texture address and filtering state for both stages.
|
||||
// - Version 7: adds two fixed-function lights and material color sources.
|
||||
// Capture is opt-in and never runs in the normal game path.
|
||||
namespace native_draw_capture {
|
||||
|
||||
struct Capture {
|
||||
std::uint32_t version = 7;
|
||||
std::vector<Render3DDraw> draws;
|
||||
std::unordered_map<std::string, std::vector<std::uint8_t>> textures;
|
||||
std::uint32_t ui_width = 960;
|
||||
std::uint32_t ui_height = 640;
|
||||
std::vector<UIRenderCommand> ui_commands;
|
||||
};
|
||||
|
||||
inline std::vector<std::uint8_t> encode_raw_argb_as_mtra(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
const std::uint32_t* argb) {
|
||||
std::vector<std::uint8_t> out;
|
||||
if (!width || !height || !argb)
|
||||
return out;
|
||||
const std::size_t pixel_count = std::size_t(width) * std::size_t(height);
|
||||
out.resize(12 + pixel_count * 4);
|
||||
out[0] = 'M'; out[1] = 'T'; out[2] = 'R'; out[3] = 'A';
|
||||
std::memcpy(out.data() + 4, &width, 4);
|
||||
std::memcpy(out.data() + 8, &height, 4);
|
||||
std::uint8_t* dst = out.data() + 12;
|
||||
for (std::size_t i = 0; i < pixel_count; ++i) {
|
||||
const std::uint32_t c = argb[i];
|
||||
dst[i * 4 + 0] = static_cast<std::uint8_t>((c >> 16) & 0xffu);
|
||||
dst[i * 4 + 1] = static_cast<std::uint8_t>((c >> 8) & 0xffu);
|
||||
dst[i * 4 + 2] = static_cast<std::uint8_t>(c & 0xffu);
|
||||
dst[i * 4 + 3] = static_cast<std::uint8_t>((c >> 24) & 0xffu);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename T> void write_scalar(std::ofstream& file, const T& value) {
|
||||
file.write(reinterpret_cast<const char*>(&value), sizeof(value));
|
||||
}
|
||||
|
||||
template <typename T> void read_scalar(std::ifstream& file, T& value) {
|
||||
file.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
if (!file) throw std::runtime_error("truncated native draw capture");
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void write_vector(std::ofstream& file, const std::vector<T>& values, std::size_t max_count = 4'000'000) {
|
||||
if (values.size() > max_count) throw std::runtime_error("native draw capture array too large");
|
||||
const auto count = static_cast<std::uint32_t>(values.size());
|
||||
write_scalar(file, count);
|
||||
if (count) file.write(reinterpret_cast<const char*>(values.data()), count * sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void read_vector(std::ifstream& file, std::vector<T>& values, std::size_t max_count = 4'000'000) {
|
||||
std::uint32_t count = 0;
|
||||
read_scalar(file, count);
|
||||
if (count > max_count) throw std::runtime_error("native draw capture array too large");
|
||||
values.resize(count);
|
||||
if (count) file.read(reinterpret_cast<char*>(values.data()), count * sizeof(T));
|
||||
if (!file) throw std::runtime_error("truncated native draw capture array");
|
||||
}
|
||||
|
||||
inline void write_string(std::ofstream& file, const std::string& value) {
|
||||
if (value.size() > 4096) throw std::runtime_error("native draw capture string too large");
|
||||
const auto size = static_cast<std::uint32_t>(value.size());
|
||||
write_scalar(file, size);
|
||||
if (size) file.write(value.data(), size);
|
||||
}
|
||||
|
||||
inline void read_string(std::ifstream& file, std::string& value) {
|
||||
std::uint32_t size = 0;
|
||||
read_scalar(file, size);
|
||||
if (size > 4096) throw std::runtime_error("native draw capture string too large");
|
||||
value.resize(size);
|
||||
if (size) file.read(value.data(), size);
|
||||
if (!file) throw std::runtime_error("truncated native draw capture string");
|
||||
}
|
||||
|
||||
inline void write(
|
||||
const std::string& path,
|
||||
const std::vector<Render3DDraw>& draws,
|
||||
const std::unordered_map<std::string, std::vector<std::uint8_t>>& textures = {},
|
||||
std::uint32_t ui_width = 960,
|
||||
std::uint32_t ui_height = 640,
|
||||
const std::vector<UIRenderCommand>& ui_commands = {}) {
|
||||
if (draws.size() > 10'000 || textures.size() > 8'192 || ui_commands.size() > 100'000)
|
||||
throw std::runtime_error("native draw capture has too many draws, textures, or UI commands");
|
||||
std::ofstream file(path, std::ios::binary | std::ios::trunc);
|
||||
if (!file) throw std::runtime_error("cannot create native draw capture: " + path);
|
||||
const std::uint32_t magic = 0x4d544452; // MTDR
|
||||
const std::uint32_t version = 7;
|
||||
const auto count = static_cast<std::uint32_t>(draws.size());
|
||||
write_scalar(file, magic); write_scalar(file, version); write_scalar(file, count);
|
||||
for (const auto& draw : draws) {
|
||||
file.write(reinterpret_cast<const char*>(draw.world), sizeof(draw.world));
|
||||
file.write(reinterpret_cast<const char*>(draw.view), sizeof(draw.view));
|
||||
file.write(reinterpret_cast<const char*>(draw.proj), sizeof(draw.proj));
|
||||
write_scalar(file, draw.geometry_key);
|
||||
write_scalar(file, draw.geometry_revision);
|
||||
const std::uint32_t flags =
|
||||
(draw.lines ? 1u : 0u) | (draw.pretransformed ? 2u : 0u) | (draw.light0 ? 4u : 0u);
|
||||
write_scalar(file, flags);
|
||||
write_vector(file, draw.positions);
|
||||
write_vector(file, draw.diffuse);
|
||||
write_vector(file, draw.indices);
|
||||
file.write(reinterpret_cast<const char*>(draw.viewport), sizeof(draw.viewport));
|
||||
write_string(file, draw.texture0);
|
||||
write_string(file, draw.texture1);
|
||||
write_vector(file, draw.rhw);
|
||||
write_vector(file, draw.normals);
|
||||
write_vector(file, draw.uv0);
|
||||
write_vector(file, draw.uv1);
|
||||
write_scalar(file, draw.alpha_blend);
|
||||
write_scalar(file, draw.src_blend);
|
||||
write_scalar(file, draw.dest_blend);
|
||||
write_scalar(file, draw.alpha_test);
|
||||
write_scalar(file, draw.alpha_ref);
|
||||
write_scalar(file, draw.alpha_func);
|
||||
write_scalar(file, draw.cull_mode);
|
||||
write_scalar(file, draw.z_enable);
|
||||
write_scalar(file, draw.z_write);
|
||||
write_scalar(file, draw.z_func);
|
||||
write_scalar(file, draw.lighting);
|
||||
write_scalar(file, draw.texture_factor);
|
||||
write_scalar(file, draw.fog_enable);
|
||||
file.write(reinterpret_cast<const char*>(draw.color_op), sizeof(draw.color_op));
|
||||
file.write(reinterpret_cast<const char*>(draw.color_arg1), sizeof(draw.color_arg1));
|
||||
file.write(reinterpret_cast<const char*>(draw.color_arg2), sizeof(draw.color_arg2));
|
||||
file.write(reinterpret_cast<const char*>(draw.alpha_op), sizeof(draw.alpha_op));
|
||||
file.write(reinterpret_cast<const char*>(draw.alpha_arg1), sizeof(draw.alpha_arg1));
|
||||
file.write(reinterpret_cast<const char*>(draw.alpha_arg2), sizeof(draw.alpha_arg2));
|
||||
file.write(reinterpret_cast<const char*>(draw.material_diffuse), sizeof(draw.material_diffuse));
|
||||
file.write(reinterpret_cast<const char*>(draw.material_ambient), sizeof(draw.material_ambient));
|
||||
file.write(reinterpret_cast<const char*>(draw.material_emissive), sizeof(draw.material_emissive));
|
||||
file.write(reinterpret_cast<const char*>(draw.light0_direction), sizeof(draw.light0_direction));
|
||||
file.write(reinterpret_cast<const char*>(draw.light0_diffuse), sizeof(draw.light0_diffuse));
|
||||
file.write(reinterpret_cast<const char*>(draw.light0_ambient), sizeof(draw.light0_ambient));
|
||||
write_scalar(file, draw.ambient);
|
||||
write_scalar(file, draw.fog_color);
|
||||
write_scalar(file, draw.fog_vertex_mode);
|
||||
write_scalar(file, draw.fog_table_mode);
|
||||
write_scalar(file, draw.fog_range_enable);
|
||||
write_scalar(file, draw.fog_start);
|
||||
write_scalar(file, draw.fog_end);
|
||||
write_scalar(file, draw.fog_density);
|
||||
file.write(reinterpret_cast<const char*>(draw.address_u), sizeof(draw.address_u));
|
||||
file.write(reinterpret_cast<const char*>(draw.address_v), sizeof(draw.address_v));
|
||||
file.write(reinterpret_cast<const char*>(draw.min_filter), sizeof(draw.min_filter));
|
||||
file.write(reinterpret_cast<const char*>(draw.mag_filter), sizeof(draw.mag_filter));
|
||||
file.write(reinterpret_cast<const char*>(draw.mip_filter), sizeof(draw.mip_filter));
|
||||
file.write(reinterpret_cast<const char*>(draw.lights), sizeof(draw.lights));
|
||||
write_scalar(file, draw.diffuse_material_source);
|
||||
write_scalar(file, draw.ambient_material_source);
|
||||
write_scalar(file, draw.color_vertex);
|
||||
write_vector(file, draw.bone_indices);
|
||||
write_vector(file, draw.bone_weights);
|
||||
write_vector(file, draw.bone_matrices);
|
||||
}
|
||||
const auto texture_count = static_cast<std::uint32_t>(textures.size());
|
||||
write_scalar(file, texture_count);
|
||||
for (const auto& [name, bytes] : textures) {
|
||||
write_string(file, name);
|
||||
write_vector(file, bytes, 16'777'216);
|
||||
}
|
||||
write_scalar(file, ui_width);
|
||||
write_scalar(file, ui_height);
|
||||
const auto ui_count = static_cast<std::uint32_t>(ui_commands.size());
|
||||
write_scalar(file, ui_count);
|
||||
for (const auto& cmd : ui_commands) {
|
||||
const auto kind = static_cast<std::uint32_t>(cmd.kind);
|
||||
write_scalar(file, kind);
|
||||
write_scalar(file, cmd.x1);
|
||||
write_scalar(file, cmd.y1);
|
||||
write_scalar(file, cmd.x2);
|
||||
write_scalar(file, cmd.y2);
|
||||
write_scalar(file, cmd.argb);
|
||||
write_scalar(file, cmd.end_argb);
|
||||
write_scalar(file, cmd.clip_x1);
|
||||
write_scalar(file, cmd.clip_y1);
|
||||
write_scalar(file, cmd.clip_x2);
|
||||
write_scalar(file, cmd.clip_y2);
|
||||
write_string(file, cmd.text);
|
||||
const std::uint32_t uiflags = (cmd.quad ? 1u : 0u) | (cmd.behind_3d ? 2u : 0u);
|
||||
write_scalar(file, uiflags);
|
||||
file.write(reinterpret_cast<const char*>(cmd.qx), sizeof(cmd.qx));
|
||||
file.write(reinterpret_cast<const char*>(cmd.qy), sizeof(cmd.qy));
|
||||
write_scalar(file, cmd.su);
|
||||
write_scalar(file, cmd.sv);
|
||||
write_scalar(file, cmd.eu);
|
||||
write_scalar(file, cmd.ev);
|
||||
write_scalar(file, cmd.blend);
|
||||
write_string(file, cmd.mask);
|
||||
file.write(reinterpret_cast<const char*>(cmd.mu), sizeof(cmd.mu));
|
||||
file.write(reinterpret_cast<const char*>(cmd.mv), sizeof(cmd.mv));
|
||||
}
|
||||
if (!file) throw std::runtime_error("failed to write native draw capture: " + path);
|
||||
}
|
||||
|
||||
inline Capture read_capture(const std::string& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) throw std::runtime_error("cannot open native draw capture: " + path);
|
||||
std::uint32_t magic = 0, version = 0, count = 0;
|
||||
read_scalar(file, magic); read_scalar(file, version); read_scalar(file, count);
|
||||
if (magic != 0x4d544452 || (version < 1 || version > 7) || count > 10'000)
|
||||
throw std::runtime_error("unsupported native draw capture format");
|
||||
Capture capture;
|
||||
capture.version = version;
|
||||
capture.draws.resize(count);
|
||||
for (auto& draw : capture.draws) {
|
||||
file.read(reinterpret_cast<char*>(draw.world), sizeof(draw.world));
|
||||
file.read(reinterpret_cast<char*>(draw.view), sizeof(draw.view));
|
||||
file.read(reinterpret_cast<char*>(draw.proj), sizeof(draw.proj));
|
||||
if (!file) throw std::runtime_error("truncated native draw capture matrices");
|
||||
if (version >= 2) {
|
||||
read_scalar(file, draw.geometry_key);
|
||||
read_scalar(file, draw.geometry_revision);
|
||||
}
|
||||
std::uint32_t flags = 0;
|
||||
read_scalar(file, flags);
|
||||
draw.lines = (flags & 1u) != 0;
|
||||
draw.pretransformed = (flags & 2u) != 0;
|
||||
draw.light0 = (flags & 4u) != 0;
|
||||
read_vector(file, draw.positions);
|
||||
read_vector(file, draw.diffuse);
|
||||
read_vector(file, draw.indices);
|
||||
if (version >= 3) {
|
||||
file.read(reinterpret_cast<char*>(draw.viewport), sizeof(draw.viewport));
|
||||
read_string(file, draw.texture0);
|
||||
read_string(file, draw.texture1);
|
||||
read_vector(file, draw.rhw);
|
||||
read_vector(file, draw.normals);
|
||||
read_vector(file, draw.uv0);
|
||||
read_vector(file, draw.uv1);
|
||||
read_scalar(file, draw.alpha_blend);
|
||||
read_scalar(file, draw.src_blend);
|
||||
read_scalar(file, draw.dest_blend);
|
||||
read_scalar(file, draw.alpha_test);
|
||||
read_scalar(file, draw.alpha_ref);
|
||||
read_scalar(file, draw.alpha_func);
|
||||
read_scalar(file, draw.cull_mode);
|
||||
read_scalar(file, draw.z_enable);
|
||||
read_scalar(file, draw.z_write);
|
||||
read_scalar(file, draw.z_func);
|
||||
read_scalar(file, draw.lighting);
|
||||
read_scalar(file, draw.texture_factor);
|
||||
read_scalar(file, draw.fog_enable);
|
||||
file.read(reinterpret_cast<char*>(draw.color_op), sizeof(draw.color_op));
|
||||
file.read(reinterpret_cast<char*>(draw.color_arg1), sizeof(draw.color_arg1));
|
||||
file.read(reinterpret_cast<char*>(draw.color_arg2), sizeof(draw.color_arg2));
|
||||
file.read(reinterpret_cast<char*>(draw.alpha_op), sizeof(draw.alpha_op));
|
||||
file.read(reinterpret_cast<char*>(draw.alpha_arg1), sizeof(draw.alpha_arg1));
|
||||
file.read(reinterpret_cast<char*>(draw.alpha_arg2), sizeof(draw.alpha_arg2));
|
||||
file.read(reinterpret_cast<char*>(draw.material_diffuse), sizeof(draw.material_diffuse));
|
||||
file.read(reinterpret_cast<char*>(draw.material_ambient), sizeof(draw.material_ambient));
|
||||
file.read(reinterpret_cast<char*>(draw.material_emissive), sizeof(draw.material_emissive));
|
||||
file.read(reinterpret_cast<char*>(draw.light0_direction), sizeof(draw.light0_direction));
|
||||
file.read(reinterpret_cast<char*>(draw.light0_diffuse), sizeof(draw.light0_diffuse));
|
||||
file.read(reinterpret_cast<char*>(draw.light0_ambient), sizeof(draw.light0_ambient));
|
||||
read_scalar(file, draw.ambient);
|
||||
if (version >= 5) {
|
||||
read_scalar(file, draw.fog_color);
|
||||
read_scalar(file, draw.fog_vertex_mode);
|
||||
read_scalar(file, draw.fog_table_mode);
|
||||
read_scalar(file, draw.fog_range_enable);
|
||||
read_scalar(file, draw.fog_start);
|
||||
read_scalar(file, draw.fog_end);
|
||||
read_scalar(file, draw.fog_density);
|
||||
}
|
||||
if (version >= 6) {
|
||||
file.read(reinterpret_cast<char*>(draw.address_u), sizeof(draw.address_u));
|
||||
file.read(reinterpret_cast<char*>(draw.address_v), sizeof(draw.address_v));
|
||||
file.read(reinterpret_cast<char*>(draw.min_filter), sizeof(draw.min_filter));
|
||||
file.read(reinterpret_cast<char*>(draw.mag_filter), sizeof(draw.mag_filter));
|
||||
file.read(reinterpret_cast<char*>(draw.mip_filter), sizeof(draw.mip_filter));
|
||||
}
|
||||
if (version >= 7) {
|
||||
file.read(reinterpret_cast<char*>(draw.lights), sizeof(draw.lights));
|
||||
read_scalar(file, draw.diffuse_material_source);
|
||||
read_scalar(file, draw.ambient_material_source);
|
||||
read_scalar(file, draw.color_vertex);
|
||||
} else if (draw.light0) {
|
||||
auto& light = draw.lights[0];
|
||||
light.type = 3;
|
||||
std::memcpy(light.direction, draw.light0_direction, sizeof(light.direction));
|
||||
std::memcpy(light.diffuse, draw.light0_diffuse, sizeof(light.diffuse));
|
||||
std::memcpy(light.ambient, draw.light0_ambient, sizeof(light.ambient));
|
||||
}
|
||||
if (!file) throw std::runtime_error("truncated native draw capture state");
|
||||
} else {
|
||||
draw.z_enable = 1;
|
||||
draw.z_write = 1;
|
||||
}
|
||||
if (version >= 4) {
|
||||
read_vector(file, draw.bone_indices);
|
||||
read_vector(file, draw.bone_weights);
|
||||
read_vector(file, draw.bone_matrices);
|
||||
}
|
||||
}
|
||||
if (version >= 3) {
|
||||
std::uint32_t texture_count = 0;
|
||||
read_scalar(file, texture_count);
|
||||
if (texture_count > 8'192) throw std::runtime_error("native draw capture has too many textures");
|
||||
for (std::uint32_t i = 0; i < texture_count; ++i) {
|
||||
std::string name;
|
||||
std::vector<std::uint8_t> bytes;
|
||||
read_string(file, name);
|
||||
read_vector(file, bytes, 16'777'216);
|
||||
capture.textures.emplace(std::move(name), std::move(bytes));
|
||||
}
|
||||
}
|
||||
if (version >= 4) {
|
||||
read_scalar(file, capture.ui_width);
|
||||
read_scalar(file, capture.ui_height);
|
||||
std::uint32_t ui_count = 0;
|
||||
read_scalar(file, ui_count);
|
||||
if (ui_count > 100'000) throw std::runtime_error("native draw capture has too many UI commands");
|
||||
capture.ui_commands.resize(ui_count);
|
||||
for (auto& cmd : capture.ui_commands) {
|
||||
std::uint32_t kind = 0, uiflags = 0;
|
||||
read_scalar(file, kind);
|
||||
cmd.kind = static_cast<UIRenderCommand::Kind>(kind);
|
||||
read_scalar(file, cmd.x1);
|
||||
read_scalar(file, cmd.y1);
|
||||
read_scalar(file, cmd.x2);
|
||||
read_scalar(file, cmd.y2);
|
||||
read_scalar(file, cmd.argb);
|
||||
read_scalar(file, cmd.end_argb);
|
||||
read_scalar(file, cmd.clip_x1);
|
||||
read_scalar(file, cmd.clip_y1);
|
||||
read_scalar(file, cmd.clip_x2);
|
||||
read_scalar(file, cmd.clip_y2);
|
||||
read_string(file, cmd.text);
|
||||
read_scalar(file, uiflags);
|
||||
cmd.quad = (uiflags & 1u) != 0;
|
||||
cmd.behind_3d = (uiflags & 2u) != 0;
|
||||
file.read(reinterpret_cast<char*>(cmd.qx), sizeof(cmd.qx));
|
||||
file.read(reinterpret_cast<char*>(cmd.qy), sizeof(cmd.qy));
|
||||
read_scalar(file, cmd.su);
|
||||
read_scalar(file, cmd.sv);
|
||||
read_scalar(file, cmd.eu);
|
||||
read_scalar(file, cmd.ev);
|
||||
read_scalar(file, cmd.blend);
|
||||
read_string(file, cmd.mask);
|
||||
file.read(reinterpret_cast<char*>(cmd.mu), sizeof(cmd.mu));
|
||||
file.read(reinterpret_cast<char*>(cmd.mv), sizeof(cmd.mv));
|
||||
if (!file) throw std::runtime_error("truncated native draw capture UI command");
|
||||
}
|
||||
}
|
||||
return capture;
|
||||
}
|
||||
|
||||
inline std::vector<Render3DDraw> read(const std::string& path) {
|
||||
return read_capture(path).draws;
|
||||
}
|
||||
|
||||
} // namespace native_draw_capture
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
#version 450
|
||||
layout(location = 0) in vec4 in_color;
|
||||
layout(location = 1) in vec2 in_uv;
|
||||
layout(location = 2) in vec2 in_mask_uv;
|
||||
layout(location = 3) in float in_fog;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
layout(set = 0, binding = 0) uniform sampler2D tex_sampler;
|
||||
layout(set = 0, binding = 1) uniform sampler2D mask_sampler;
|
||||
|
||||
layout(push_constant) uniform DrawConstants {
|
||||
mat4 mvp;
|
||||
vec4 tint_color;
|
||||
vec4 ambient_emissive;
|
||||
vec4 light_dir;
|
||||
vec4 light_diffuse;
|
||||
} draw;
|
||||
|
||||
layout(set = 1, binding = 1, std430) readonly buffer FixedFunctionState {
|
||||
uvec4 stage0_color;
|
||||
uvec4 stage0_alpha;
|
||||
uvec4 stage1_color;
|
||||
uvec4 stage1_alpha;
|
||||
vec4 fog_color;
|
||||
vec4 fog_params;
|
||||
vec4 texture_factor;
|
||||
mat4 world_view;
|
||||
uvec4 flags;
|
||||
} fixed_state;
|
||||
|
||||
vec4 stage_arg(uint selector, vec4 diffuse, vec4 current, vec4 texel) {
|
||||
uint source = selector & 15u;
|
||||
vec4 value = source == 0u ? diffuse :
|
||||
source == 1u ? current :
|
||||
source == 2u ? texel :
|
||||
source == 3u ? fixed_state.texture_factor : vec4(1.0);
|
||||
if ((selector & 16u) != 0u) value = vec4(1.0) - value;
|
||||
if ((selector & 32u) != 0u) value.rgb = vec3(value.a);
|
||||
return value;
|
||||
}
|
||||
|
||||
vec4 apply_op(uint op, vec4 a, vec4 b, vec4 current, vec4 diffuse, vec4 texel) {
|
||||
if (op == 2u) return a; // SELECTARG1
|
||||
if (op == 3u) return b; // SELECTARG2
|
||||
if (op == 4u) return a * b; // MODULATE
|
||||
if (op == 5u) return 2.0 * a * b; // MODULATE2X
|
||||
if (op == 6u) return 4.0 * a * b; // MODULATE4X
|
||||
if (op == 7u) return a + b; // ADD
|
||||
if (op == 8u) return a + b - vec4(0.5); // ADDSIGNED
|
||||
if (op == 9u) return 2.0 * (a + b - vec4(0.5)); // ADDSIGNED2X
|
||||
if (op == 10u) return a - b; // SUBTRACT
|
||||
if (op == 11u) return a + b - a * b; // ADDSMOOTH
|
||||
if (op == 12u) return mix(b, a, diffuse.a);
|
||||
if (op == 13u) return mix(b, a, texel.a);
|
||||
if (op == 14u) return mix(b, a, fixed_state.texture_factor.a);
|
||||
if (op == 15u) return a + b * (1.0 - texel.a);
|
||||
if (op == 16u) return mix(b, a, current.a);
|
||||
if (op == 18u) return vec4(a.rgb + a.a * b.rgb, a.a);
|
||||
if (op == 19u) return vec4(a.rgb * b.rgb + vec3(a.a), a.a);
|
||||
if (op == 20u) return vec4((1.0 - a.a) * b.rgb + a.rgb, a.a);
|
||||
if (op == 21u) return vec4((vec3(1.0) - a.rgb) * b.rgb + vec3(a.a), a.a);
|
||||
return current;
|
||||
}
|
||||
|
||||
vec4 apply_stage(uvec4 color_state, uvec4 alpha_state,
|
||||
vec4 diffuse, vec4 current, vec4 texel) {
|
||||
if (color_state.x <= 1u) return current;
|
||||
vec4 color_arg1 = stage_arg(color_state.y, diffuse, current, texel);
|
||||
vec4 color_arg2 = stage_arg(color_state.z, diffuse, current, texel);
|
||||
vec4 result = apply_op(color_state.x, color_arg1, color_arg2, current, diffuse, texel);
|
||||
if (alpha_state.x > 1u) {
|
||||
vec4 alpha_arg1 = stage_arg(alpha_state.y, diffuse, current, texel);
|
||||
vec4 alpha_arg2 = stage_arg(alpha_state.z, diffuse, current, texel);
|
||||
result.a = apply_op(alpha_state.x, alpha_arg1, alpha_arg2, current, diffuse, texel).a;
|
||||
} else if (alpha_state.x == 1u) {
|
||||
result.a = current.a;
|
||||
}
|
||||
return clamp(result, 0.0, 1.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 tex0 = texture(tex_sampler, in_uv);
|
||||
if (fixed_state.flags.x == 2u) {
|
||||
vec3 shadow = tex0.rgb;
|
||||
if (fixed_state.flags.z != 0u && fixed_state.stage1_color.x == 4u)
|
||||
shadow *= texture(mask_sampler, in_mask_uv).rgb;
|
||||
shadow = mix(fixed_state.fog_color.rgb, shadow, in_fog);
|
||||
if (all(greaterThanEqual(shadow, vec3(0.997)))) discard;
|
||||
out_color = vec4(shadow, 1.0);
|
||||
return;
|
||||
}
|
||||
if (fixed_state.flags.x != 0u) {
|
||||
vec4 diffuse = in_color;
|
||||
vec4 color = apply_stage(fixed_state.stage0_color, fixed_state.stage0_alpha,
|
||||
diffuse, diffuse, tex0);
|
||||
if (fixed_state.flags.z != 0u && fixed_state.stage1_color.x > 1u) {
|
||||
vec4 tex1 = texture(mask_sampler, in_mask_uv);
|
||||
color = apply_stage(fixed_state.stage1_color, fixed_state.stage1_alpha,
|
||||
diffuse, color, tex1);
|
||||
}
|
||||
if (fixed_state.stage1_alpha.w != 0u) {
|
||||
float ref = draw.ambient_emissive.a;
|
||||
uint func = fixed_state.stage0_alpha.w;
|
||||
bool passes = func == 1u ? false :
|
||||
func == 2u ? color.a < ref :
|
||||
func == 3u ? abs(color.a - ref) < (0.5 / 255.0) :
|
||||
func == 4u ? color.a <= ref :
|
||||
func == 5u ? color.a > ref :
|
||||
func == 6u ? abs(color.a - ref) >= (0.5 / 255.0) :
|
||||
func == 7u ? color.a >= ref : true;
|
||||
if (!passes) discard;
|
||||
}
|
||||
color.rgb = mix(fixed_state.fog_color.rgb, color.rgb, in_fog);
|
||||
out_color = color;
|
||||
return;
|
||||
}
|
||||
vec4 color = in_color * tex0;
|
||||
if (draw.light_dir.w < -0.4) {
|
||||
vec4 mask_val = texture(mask_sampler, in_mask_uv);
|
||||
if (draw.light_dir.w < -0.8) {
|
||||
if (in_mask_uv.x < 0.0 || in_mask_uv.x > 1.0 || in_mask_uv.y < 0.0 || in_mask_uv.y > 1.0) {
|
||||
discard;
|
||||
}
|
||||
color.rgb *= mask_val.rgb;
|
||||
color.a = mask_val.a * in_color.a;
|
||||
} else {
|
||||
color.a = mask_val.a * in_color.a;
|
||||
if (color.a <= 0.003) discard;
|
||||
}
|
||||
} else if (draw.light_dir.w > 1.001) {
|
||||
// Specular sphere-map stage 1 (D3DTOP_MODULATEALPHA_ADDCOLOR)
|
||||
float spec_power = draw.light_dir.w - 1.0;
|
||||
vec4 spec_map = texture(mask_sampler, in_mask_uv);
|
||||
color.rgb = min(color.rgb + (tex0.a * spec_power) * spec_map.rgb, vec3(1.5));
|
||||
color.a = 1.0;
|
||||
}
|
||||
if (color.a <= draw.ambient_emissive.a) discard;
|
||||
out_color = color;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
#version 450
|
||||
layout(location = 0) in vec3 in_position;
|
||||
layout(location = 1) in vec3 in_normal;
|
||||
layout(location = 2) in vec2 in_uv;
|
||||
layout(location = 3) in vec4 in_color;
|
||||
layout(location = 4) in uvec4 in_joints;
|
||||
layout(location = 5) in vec4 in_weights;
|
||||
layout(location = 6) in vec2 in_mask_uv;
|
||||
layout(location = 7) in float in_rhw;
|
||||
|
||||
layout(location = 0) out vec4 out_color;
|
||||
layout(location = 1) out vec2 out_uv;
|
||||
layout(location = 2) out vec2 out_mask_uv;
|
||||
layout(location = 3) out float out_fog;
|
||||
|
||||
layout(set = 1, binding = 0, std430) readonly buffer BonePalette {
|
||||
mat4 bones[];
|
||||
} bone_palette;
|
||||
|
||||
layout(push_constant) uniform DrawConstants {
|
||||
mat4 mvp;
|
||||
vec4 tint_color;
|
||||
vec4 ambient_emissive;
|
||||
vec4 light_dir;
|
||||
vec4 light_diffuse;
|
||||
} draw;
|
||||
|
||||
layout(set = 1, binding = 1, std430) readonly buffer FixedFunctionState {
|
||||
uvec4 stage0_color;
|
||||
uvec4 stage0_alpha;
|
||||
uvec4 stage1_color;
|
||||
uvec4 stage1_alpha;
|
||||
vec4 fog_color;
|
||||
vec4 fog_params;
|
||||
vec4 texture_factor;
|
||||
mat4 world_view;
|
||||
uvec4 flags;
|
||||
mat4 world;
|
||||
uvec4 lighting_flags;
|
||||
vec4 material_ambient;
|
||||
vec4 material_emissive;
|
||||
vec4 global_ambient;
|
||||
vec4 light_position_type[2];
|
||||
vec4 light_direction_range[2];
|
||||
vec4 light_diffuse[2];
|
||||
vec4 light_ambient[2];
|
||||
vec4 light_attenuation[2];
|
||||
vec4 light_spot[2];
|
||||
} fixed_state;
|
||||
|
||||
void main() {
|
||||
vec3 pos = in_position;
|
||||
vec3 nrm = in_normal;
|
||||
if (draw.light_diffuse.w > 0.5) {
|
||||
uint bone_base = uint(draw.light_diffuse.w - 0.5);
|
||||
vec3 skinned_pos = vec3(0.0);
|
||||
vec3 skinned_nrm = vec3(0.0);
|
||||
float total_w = 0.0;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
float w = in_weights[k];
|
||||
if (w > 0.0) {
|
||||
mat4 B = bone_palette.bones[bone_base + in_joints[k]];
|
||||
skinned_pos += w * (B * vec4(pos, 1.0)).xyz;
|
||||
skinned_nrm += w * (mat3(B) * nrm);
|
||||
total_w += w;
|
||||
}
|
||||
}
|
||||
if (total_w > 0.0) {
|
||||
pos = skinned_pos;
|
||||
nrm = skinned_nrm;
|
||||
}
|
||||
}
|
||||
// D3D row-vector matrices are uploaded row-major. GLSL reads the bytes as their transpose.
|
||||
gl_Position = draw.mvp * vec4(pos, 1.0);
|
||||
if (draw.light_diffuse.w < -0.5) {
|
||||
float clip_w = in_rhw > 0.0 ? 1.0 / in_rhw : 1.0;
|
||||
gl_Position.xyz *= clip_w;
|
||||
gl_Position.w = clip_w;
|
||||
}
|
||||
gl_Position.y = -gl_Position.y;
|
||||
vec3 n = length(nrm) > 1e-4 ? normalize(nrm) : vec3(0.0, 0.0, 1.0);
|
||||
out_color = in_color;
|
||||
if (fixed_state.lighting_flags.x != 0u) {
|
||||
vec3 world_pos = (fixed_state.world * vec4(pos, 1.0)).xyz;
|
||||
vec3 world_normal = normalize(mat3(fixed_state.world) * n);
|
||||
vec3 mat_diffuse = (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.w == 1u)
|
||||
? in_color.rgb : draw.tint_color.rgb;
|
||||
vec3 mat_ambient = (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.z == 1u)
|
||||
? in_color.rgb : fixed_state.material_ambient.rgb;
|
||||
vec3 ambient_sum = fixed_state.global_ambient.rgb;
|
||||
vec3 diffuse_sum = vec3(0.0);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
int type = int(fixed_state.light_position_type[i].w + 0.5);
|
||||
if (type == 0) continue;
|
||||
vec3 L;
|
||||
float strength = 1.0;
|
||||
if (type == 3) {
|
||||
L = normalize(-fixed_state.light_direction_range[i].xyz);
|
||||
} else {
|
||||
vec3 to_light = fixed_state.light_position_type[i].xyz - world_pos;
|
||||
float distance_to_light = length(to_light);
|
||||
if (distance_to_light > fixed_state.light_direction_range[i].w || distance_to_light < 0.0001)
|
||||
continue;
|
||||
L = to_light / distance_to_light;
|
||||
vec4 attenuation = fixed_state.light_attenuation[i];
|
||||
float denominator = attenuation.x + attenuation.y * distance_to_light +
|
||||
attenuation.z * distance_to_light * distance_to_light;
|
||||
strength = denominator > 0.0001 ? min(1.0 / denominator, 1.0) : 1.0;
|
||||
if (type == 2) {
|
||||
vec3 spot_dir = normalize(fixed_state.light_direction_range[i].xyz);
|
||||
float cosine = dot(-L, spot_dir);
|
||||
float inner = cos(fixed_state.light_spot[i].x * 0.5);
|
||||
float outer = cos(fixed_state.light_spot[i].y * 0.5);
|
||||
float cone = clamp((cosine - outer) / max(inner - outer, 0.0001), 0.0, 1.0);
|
||||
strength *= pow(cone, max(attenuation.w, 0.0001));
|
||||
}
|
||||
}
|
||||
ambient_sum += fixed_state.light_ambient[i].rgb * strength;
|
||||
diffuse_sum += fixed_state.light_diffuse[i].rgb * max(dot(world_normal, L), 0.0) * strength;
|
||||
}
|
||||
vec3 lit = fixed_state.material_emissive.rgb + mat_ambient * ambient_sum + mat_diffuse * diffuse_sum;
|
||||
float alpha = (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.w == 1u)
|
||||
? in_color.a : draw.tint_color.a;
|
||||
out_color = vec4(clamp(lit, 0.0, 1.0), alpha);
|
||||
}
|
||||
out_uv = in_uv;
|
||||
if (draw.light_dir.w > 1.001) {
|
||||
vec3 view_normal = normalize(mat3(fixed_state.world_view) * n);
|
||||
vec3 view_pos = (fixed_state.world_view * vec4(pos, 1.0)).xyz;
|
||||
vec3 view_dir = normalize(-view_pos);
|
||||
vec3 reflected = reflect(-view_dir, view_normal);
|
||||
out_mask_uv = reflected.xy * vec2(0.5, -0.5) + vec2(0.5);
|
||||
} else {
|
||||
out_mask_uv = in_mask_uv;
|
||||
}
|
||||
out_fog = 1.0;
|
||||
if (fixed_state.flags.x != 0u && fixed_state.flags.w != 0u && draw.light_diffuse.w >= -0.5) {
|
||||
vec3 eye = (fixed_state.world_view * vec4(pos, 1.0)).xyz;
|
||||
float distance_to_eye = fixed_state.fog_params.w > 0.5 ? length(eye) : abs(eye.z);
|
||||
if (fixed_state.flags.w == 3u) {
|
||||
float span = fixed_state.fog_params.y - fixed_state.fog_params.x;
|
||||
if (span > 0.0001)
|
||||
out_fog = clamp((fixed_state.fog_params.y - distance_to_eye) / span, 0.0, 1.0);
|
||||
} else if (fixed_state.flags.w == 1u) {
|
||||
out_fog = clamp(exp(-fixed_state.fog_params.z * distance_to_eye), 0.0, 1.0);
|
||||
} else if (fixed_state.flags.w == 2u) {
|
||||
float fog_distance = fixed_state.fog_params.z * distance_to_eye;
|
||||
out_fog = clamp(exp(-fog_distance * fog_distance), 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#define STBI_NO_STDIO
|
||||
#define STBI_ONLY_JPEG
|
||||
#define STBI_ONLY_PNG
|
||||
#define STBI_ONLY_BMP
|
||||
#define STBI_MAX_DIMENSIONS 4096
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "stb_image.h"
|
||||
+7988
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,732 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "UIRenderCommands.h"
|
||||
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
#include "platform/ScriptLib/PythonBoot.h"
|
||||
#endif
|
||||
|
||||
class TouchController {
|
||||
public:
|
||||
struct ButtonDef {
|
||||
int id; // 1 = ATK, 2 = S1, 3 = S2, 4 = S3, 5 = POT, 6 = PICK
|
||||
// 99 = MENU (toggle drawer)
|
||||
// 10 = BAG (DIK_I), 11 = CHAR (DIK_C), 12 = SKILL (DIK_V), 13 = QUEST (DIK_N), 14 = COMM (DIK_M), 15 = SET (DIK_ESCAPE)
|
||||
int dik;
|
||||
float x, y, radius;
|
||||
const char* label;
|
||||
uint32_t color_idle;
|
||||
uint32_t color_pressed;
|
||||
bool pressed = false;
|
||||
int64_t finger_id = -1;
|
||||
};
|
||||
|
||||
TouchController() {
|
||||
init_buttons();
|
||||
}
|
||||
|
||||
void set_enabled(bool enabled) { enabled_ = enabled; }
|
||||
bool is_enabled() const { return enabled_; }
|
||||
|
||||
void update_screen_size(int width, int height) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
screen_w_ = width;
|
||||
screen_h_ = height;
|
||||
// Position joystick on lower-left
|
||||
joystick_base_x_ = std::max(90.0f, float(width) * 0.12f);
|
||||
joystick_base_y_ = float(height) - std::max(90.0f, float(height) * 0.22f);
|
||||
if (!joystick_active_) {
|
||||
joystick_knob_x_ = joystick_base_x_;
|
||||
joystick_knob_y_ = joystick_base_y_;
|
||||
}
|
||||
layout_buttons();
|
||||
}
|
||||
|
||||
// Called once per frame to maintain continuous analog movement
|
||||
void update() {
|
||||
if (!enabled_) return;
|
||||
if (joystick_active_) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(move_angle_, true);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Handles finger touch down (norm_x, norm_y in 0.0 .. 1.0)
|
||||
bool on_finger_down(int64_t finger_id, float norm_x, float norm_y) {
|
||||
if (!enabled_) return false;
|
||||
const float px = norm_x * float(screen_w_);
|
||||
const float py = norm_y * float(screen_h_);
|
||||
|
||||
// 1. Check HUD buttons (Action cluster & Drawer menu)
|
||||
int btn_idx = find_button(px, py);
|
||||
if (btn_idx >= 0) {
|
||||
auto& btn = buttons_[btn_idx];
|
||||
if (btn.id == 99) { // MENU toggle button
|
||||
drawer_open_ = !drawer_open_;
|
||||
layout_buttons();
|
||||
return true;
|
||||
}
|
||||
btn.pressed = true;
|
||||
btn.finger_id = finger_id;
|
||||
trigger_button(btn.dik, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Intelligent UI Hit-Testing: check if touch lands inside an open in-game UI window
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (PythonBoot::IsPointInsideActiveUI(int(px), int(py))) {
|
||||
ui_touch_finger_id_ = finger_id;
|
||||
PythonBoot::UIMouseMove(int(px), int(py));
|
||||
PythonBoot::UIMouseButton(1, true, int(px), int(py));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// 3. Left side: virtual joystick (lower-left quadrant)
|
||||
if (norm_x < 0.40f && norm_y > 0.35f && !joystick_active_) {
|
||||
joystick_active_ = true;
|
||||
joystick_finger_id_ = finger_id;
|
||||
joystick_base_x_ = px;
|
||||
joystick_base_y_ = py;
|
||||
joystick_knob_x_ = px;
|
||||
joystick_knob_y_ = py;
|
||||
update_joystick_motion(px, py);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. 3D Game World: camera rotation, pinch zoom, or tap to target
|
||||
if (camera_finger_id_ < 0) {
|
||||
camera_finger_id_ = finger_id;
|
||||
camera_last_x_ = px;
|
||||
camera_last_y_ = py;
|
||||
camera_start_x_ = px;
|
||||
camera_start_y_ = py;
|
||||
camera_dragged_ = false;
|
||||
camera_down_time_ = get_time_sec();
|
||||
return true;
|
||||
} else if (pinch_finger2_ < 0) {
|
||||
pinch_finger1_ = camera_finger_id_;
|
||||
pinch_finger2_ = finger_id;
|
||||
pinch_last_dist_ = std::hypot(px - camera_last_x_, py - camera_last_y_);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handles finger motion
|
||||
bool on_finger_motion(int64_t finger_id, float norm_x, float norm_y) {
|
||||
if (!enabled_) return false;
|
||||
const float px = norm_x * float(screen_w_);
|
||||
const float py = norm_y * float(screen_h_);
|
||||
|
||||
// 1. UI Touch dragging (e.g. dragging item in inventory or scrollbar)
|
||||
if (ui_touch_finger_id_ == finger_id) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::UIMouseMove(int(px), int(py));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Virtual Joystick finger
|
||||
if (joystick_active_ && finger_id == joystick_finger_id_) {
|
||||
update_joystick_motion(px, py);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Button drag tracking (check if finger slid off)
|
||||
for (auto& btn : buttons_) {
|
||||
if (btn.finger_id == finger_id) {
|
||||
const float dist = std::hypot(px - btn.x, py - btn.y);
|
||||
if (dist > btn.radius * 1.5f && btn.pressed) {
|
||||
btn.pressed = false;
|
||||
trigger_button(btn.dik, false);
|
||||
} else if (dist <= btn.radius * 1.5f && !btn.pressed) {
|
||||
btn.pressed = true;
|
||||
trigger_button(btn.dik, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Two-finger pinch zoom
|
||||
if (pinch_finger1_ >= 0 && pinch_finger2_ >= 0 &&
|
||||
(finger_id == pinch_finger1_ || finger_id == pinch_finger2_)) {
|
||||
const float cur_dist = std::hypot(px - camera_last_x_, py - camera_last_y_);
|
||||
if (pinch_last_dist_ > 1.0f && cur_dist > 1.0f) {
|
||||
const float delta_d = cur_dist - pinch_last_dist_;
|
||||
if (std::abs(delta_d) > 2.0f) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::UIMouseWheel(int(delta_d * 8.0f));
|
||||
#endif
|
||||
pinch_last_dist_ = cur_dist;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. Single-finger camera drag
|
||||
if (finger_id == camera_finger_id_) {
|
||||
const float total_dist = std::hypot(px - camera_start_x_, py - camera_start_y_);
|
||||
if (total_dist > 6.0f) {
|
||||
if (!camera_dragged_) {
|
||||
camera_dragged_ = true;
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::CameraBeginDrag(int(camera_start_x_), int(camera_start_y_));
|
||||
#endif
|
||||
}
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::CameraDrag(int(px), int(py));
|
||||
#endif
|
||||
}
|
||||
camera_last_x_ = px;
|
||||
camera_last_y_ = py;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handles finger touch up
|
||||
bool on_finger_up(int64_t finger_id, float norm_x, float norm_y) {
|
||||
if (!enabled_) return false;
|
||||
const float px = norm_x * float(screen_w_);
|
||||
const float py = norm_y * float(screen_h_);
|
||||
|
||||
// 1. UI Touch release (e.g. dropped item in inventory or clicked button)
|
||||
if (ui_touch_finger_id_ == finger_id) {
|
||||
ui_touch_finger_id_ = -1;
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::UIMouseButton(1, false, int(px), int(py));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Joystick release
|
||||
if (joystick_active_ && finger_id == joystick_finger_id_) {
|
||||
joystick_active_ = false;
|
||||
joystick_finger_id_ = -1;
|
||||
joystick_knob_x_ = joystick_base_x_;
|
||||
joystick_knob_y_ = joystick_base_y_;
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(0.0f, false);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Button release
|
||||
for (auto& btn : buttons_) {
|
||||
if (btn.finger_id == finger_id) {
|
||||
if (btn.pressed) {
|
||||
btn.pressed = false;
|
||||
trigger_button(btn.dik, false);
|
||||
}
|
||||
btn.finger_id = -1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Pinch end
|
||||
if (finger_id == pinch_finger1_ || finger_id == pinch_finger2_) {
|
||||
pinch_finger1_ = -1;
|
||||
pinch_finger2_ = -1;
|
||||
pinch_last_dist_ = 0.0f;
|
||||
if (finger_id == camera_finger_id_) camera_finger_id_ = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. Camera finger release
|
||||
if (finger_id == camera_finger_id_) {
|
||||
camera_finger_id_ = -1;
|
||||
if (camera_dragged_) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::CameraEndDrag();
|
||||
#endif
|
||||
} else if ((get_time_sec() - camera_down_time_) < 0.35) {
|
||||
// Short tap on 3D world: select target (mob, NPC, ground)
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::UIMouseMove(int(px), int(py));
|
||||
PythonBoot::UIMouseButton(1, true, int(px), int(py));
|
||||
PythonBoot::UIMouseButton(1, false, int(px), int(py));
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Render virtual joystick, buttons, drawer menu, and player status bar
|
||||
void append_ui_commands(std::vector<UIRenderCommand>& commands) const {
|
||||
if (!enabled_) return;
|
||||
|
||||
const float W = float(screen_w_);
|
||||
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
// --- 1. Top-Left Player Status Bar (Mobile HUD) ---
|
||||
const auto status = PythonBoot::GetPlayerStatusInfo();
|
||||
if (status.max_hp > 0) {
|
||||
const float bar_x = 20.0f;
|
||||
const float bar_y = 16.0f;
|
||||
const float bar_w = 205.0f;
|
||||
const float bar_h = 44.0f;
|
||||
|
||||
// Background plate
|
||||
draw_rect_bar(commands, bar_x, bar_y, bar_x + bar_w, bar_y + bar_h, 0x90101824);
|
||||
draw_rect_lines(commands, bar_x, bar_y, bar_x + bar_w, bar_y + bar_h, 0x60405870);
|
||||
|
||||
// Avatar circle badge
|
||||
const float av_cx = bar_x + 22.0f;
|
||||
const float av_cy = bar_y + 22.0f;
|
||||
const float av_r = 15.0f;
|
||||
draw_filled_disc(commands, av_cx, av_cy, av_r, 0xB0203040);
|
||||
draw_circle(commands, av_cx, av_cy, av_r, 0xE0FFD700, 16);
|
||||
// "Lv" tick inside avatar
|
||||
draw_line(commands, av_cx - 4.0f, av_cy - 4.0f, av_cx - 4.0f, av_cy + 3.0f, 0xFFFFFFFF);
|
||||
draw_line(commands, av_cx - 4.0f, av_cy + 3.0f, av_cx - 1.0f, av_cy + 3.0f, 0xFFFFFFFF);
|
||||
draw_line(commands, av_cx + 1.0f, av_cy - 4.0f, av_cx + 3.0f, av_cy + 3.0f, 0xFFFFFFFF);
|
||||
draw_line(commands, av_cx + 5.0f, av_cy - 4.0f, av_cx + 3.0f, av_cy + 3.0f, 0xFFFFFFFF);
|
||||
|
||||
// Gauges
|
||||
const float gx1 = bar_x + 44.0f;
|
||||
const float gx2 = bar_x + bar_w - 10.0f;
|
||||
const float gw = gx2 - gx1;
|
||||
|
||||
// HP Gauge
|
||||
const float hp_y1 = bar_y + 10.0f;
|
||||
const float hp_y2 = hp_y1 + 10.0f;
|
||||
const float hp_ratio = std::clamp(float(status.hp) / float(status.max_hp), 0.0f, 1.0f);
|
||||
draw_rect_bar(commands, gx1, hp_y1, gx2, hp_y2, 0x70301010);
|
||||
draw_rect_bar(commands, gx1, hp_y1, gx1 + gw * hp_ratio, hp_y2, 0xE5D82424);
|
||||
draw_line(commands, gx1, hp_y1, gx1 + gw * hp_ratio, hp_y1, 0x60FFFFFF); // Gloss line
|
||||
draw_rect_lines(commands, gx1, hp_y1, gx2, hp_y2, 0x80502020);
|
||||
|
||||
// MP Gauge
|
||||
const float mp_y1 = bar_y + 24.0f;
|
||||
const float mp_y2 = mp_y1 + 8.0f;
|
||||
const float mp_ratio = status.max_sp > 0 ? std::clamp(float(status.sp) / float(status.max_sp), 0.0f, 1.0f) : 0.0f;
|
||||
draw_rect_bar(commands, gx1, mp_y1, gx2, mp_y2, 0x70102038);
|
||||
draw_rect_bar(commands, gx1, mp_y1, gx1 + gw * mp_ratio, mp_y2, 0xE52068E0);
|
||||
draw_line(commands, gx1, mp_y1, gx1 + gw * mp_ratio, mp_y1, 0x60FFFFFF); // Gloss line
|
||||
draw_rect_lines(commands, gx1, mp_y1, gx2, mp_y2, 0x80204060);
|
||||
|
||||
// EXP bar (bottom trim line)
|
||||
if (status.max_exp > 0) {
|
||||
const float exp_ratio = std::clamp(float(status.exp) / float(status.max_exp), 0.0f, 1.0f);
|
||||
draw_rect_bar(commands, bar_x, bar_y + bar_h - 2.0f, bar_x + bar_w * exp_ratio, bar_y + bar_h, 0xD0E0B020);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- 2. Top-Right Drawer Menu Bar ---
|
||||
if (drawer_open_) {
|
||||
// Background capsule tray behind drawer buttons
|
||||
draw_rect_bar(commands, W - 468.0f, 16.0f, W - 198.0f, 54.0f, 0x90101824);
|
||||
draw_rect_lines(commands, W - 468.0f, 16.0f, W - 198.0f, 54.0f, 0x60506880);
|
||||
}
|
||||
|
||||
// --- 3. Buttons Rendering (Combat wheel & Drawer items) ---
|
||||
for (const auto& btn : buttons_) {
|
||||
if (btn.id >= 10 && btn.id <= 15 && !drawer_open_)
|
||||
continue;
|
||||
|
||||
const uint32_t col = btn.pressed ? btn.color_pressed : btn.color_idle;
|
||||
draw_filled_disc(commands, btn.x, btn.y, btn.radius, col);
|
||||
draw_circle(commands, btn.x, btn.y, btn.radius * 0.85f, btn.pressed ? 0xFFFFFFFF : 0x70FFFFFF, 16);
|
||||
|
||||
const uint32_t icon_col = btn.pressed ? 0xFFFFFFFF : 0xDDFFFFFF;
|
||||
const float r = btn.radius;
|
||||
|
||||
switch (btn.id) {
|
||||
case 1: { // ATK: crossed swords
|
||||
const float s = r * 0.35f;
|
||||
draw_line(commands, btn.x - s, btn.y - s, btn.x + s, btn.y + s, icon_col);
|
||||
draw_line(commands, btn.x + s, btn.y - s, btn.x - s, btn.y + s, icon_col);
|
||||
const float g = s * 0.35f;
|
||||
draw_line(commands, btn.x - s*0.4f - g, btn.y - s*0.4f + g, btn.x - s*0.4f + g, btn.y - s*0.4f - g, icon_col);
|
||||
draw_line(commands, btn.x + s*0.4f - g, btn.y - s*0.4f - g, btn.x + s*0.4f + g, btn.y - s*0.4f + g, icon_col);
|
||||
break;
|
||||
}
|
||||
case 2: { // S1: I
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x, btn.y - h, btn.x, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x - 4.0f, btn.y - h, btn.x + 4.0f, btn.y - h, icon_col);
|
||||
draw_line(commands, btn.x - 4.0f, btn.y + h, btn.x + 4.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 3: { // S2: II
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x - 4.0f, btn.y - h, btn.x - 4.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 4.0f, btn.y - h, btn.x + 4.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 4: { // S3: III
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x - 6.0f, btn.y - h, btn.x - 6.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - h, btn.x, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 6.0f, btn.y - h, btn.x + 6.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 5: { // POT: +
|
||||
const float p = r * 0.4f;
|
||||
draw_line(commands, btn.x - p, btn.y, btn.x + p, btn.y, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - p, btn.x, btn.y + p, icon_col);
|
||||
break;
|
||||
}
|
||||
case 6: { // PICK: Downward arrow
|
||||
const float a = r * 0.35f;
|
||||
draw_line(commands, btn.x - a, btn.y - a * 0.3f, btn.x, btn.y + a * 0.6f, icon_col);
|
||||
draw_line(commands, btn.x + a, btn.y - a * 0.3f, btn.x, btn.y + a * 0.6f, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - a * 0.7f, btn.x, btn.y + a * 0.6f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 99: { // MENU: ☰ hamburger icon
|
||||
draw_line(commands, btn.x - 7.0f, btn.y - 5.0f, btn.x + 7.0f, btn.y - 5.0f, icon_col);
|
||||
draw_line(commands, btn.x - 7.0f, btn.y, btn.x + 7.0f, btn.y, icon_col);
|
||||
draw_line(commands, btn.x - 7.0f, btn.y + 5.0f, btn.x + 7.0f, btn.y + 5.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 10: { // BAG: Backpack
|
||||
draw_rect_lines(commands, btn.x - 6.0f, btn.y - 4.0f, btn.x + 6.0f, btn.y + 6.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y - 4.0f, btn.x, btn.y - 7.0f, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - 7.0f, btn.x + 3.0f, btn.y - 4.0f, icon_col);
|
||||
draw_line(commands, btn.x - 6.0f, btn.y, btn.x + 6.0f, btn.y, icon_col);
|
||||
break;
|
||||
}
|
||||
case 11: { // CHAR: Head + Shoulders
|
||||
draw_circle(commands, btn.x, btn.y - 3.0f, 4.0f, icon_col, 12);
|
||||
draw_line(commands, btn.x - 6.0f, btn.y + 6.0f, btn.x - 3.0f, btn.y + 2.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y + 2.0f, btn.x + 3.0f, btn.y + 2.0f, icon_col);
|
||||
draw_line(commands, btn.x + 3.0f, btn.y + 2.0f, btn.x + 6.0f, btn.y + 6.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 12: { // SKILL: Lightning
|
||||
draw_line(commands, btn.x + 2.0f, btn.y - 7.0f, btn.x - 3.0f, btn.y - 1.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y - 1.0f, btn.x + 1.0f, btn.y - 1.0f, icon_col);
|
||||
draw_line(commands, btn.x + 1.0f, btn.y - 1.0f, btn.x - 2.0f, btn.y + 7.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 13: { // QUEST: Scroll
|
||||
draw_rect_lines(commands, btn.x - 5.0f, btn.y - 6.0f, btn.x + 5.0f, btn.y + 6.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y - 2.0f, btn.x + 3.0f, btn.y - 2.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y + 2.0f, btn.x + 1.0f, btn.y + 2.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 14: { // COMM: Chat bubble
|
||||
draw_rect_lines(commands, btn.x - 6.0f, btn.y - 5.0f, btn.x + 6.0f, btn.y + 3.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y + 3.0f, btn.x - 5.0f, btn.y + 6.0f, icon_col);
|
||||
draw_line(commands, btn.x - 5.0f, btn.y + 6.0f, btn.x, btn.y + 3.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 15: { // SET: Gear / Close
|
||||
draw_circle(commands, btn.x, btn.y, 4.0f, icon_col, 10);
|
||||
draw_line(commands, btn.x - 7.0f, btn.y, btn.x + 7.0f, btn.y, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - 7.0f, btn.x, btn.y + 7.0f, icon_col);
|
||||
draw_line(commands, btn.x - 5.0f, btn.y - 5.0f, btn.x + 5.0f, btn.y + 5.0f, icon_col);
|
||||
draw_line(commands, btn.x - 5.0f, btn.y + 5.0f, btn.x + 5.0f, btn.y - 5.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Virtual Joystick ---
|
||||
draw_circle(commands, joystick_base_x_, joystick_base_y_, joystick_radius_, 0x8080C0FF, 24);
|
||||
draw_circle(commands, joystick_base_x_, joystick_base_y_, joystick_radius_ * 0.45f, 0x4080C0FF, 16);
|
||||
draw_line(commands, joystick_base_x_ - joystick_radius_, joystick_base_y_,
|
||||
joystick_base_x_ - joystick_radius_ + 8.0f, joystick_base_y_, 0x90FFFFFF);
|
||||
draw_line(commands, joystick_base_x_ + joystick_radius_ - 8.0f, joystick_base_y_,
|
||||
joystick_base_x_ + joystick_radius_, joystick_base_y_, 0x90FFFFFF);
|
||||
draw_line(commands, joystick_base_x_, joystick_base_y_ - joystick_radius_,
|
||||
joystick_base_x_, joystick_base_y_ - joystick_radius_ + 8.0f, 0x90FFFFFF);
|
||||
draw_line(commands, joystick_base_x_, joystick_base_y_ + joystick_radius_ - 8.0f,
|
||||
joystick_base_x_, joystick_base_y_ + joystick_radius_, 0x90FFFFFF);
|
||||
|
||||
if (joystick_active_) {
|
||||
draw_line(commands, joystick_base_x_, joystick_base_y_, joystick_knob_x_, joystick_knob_y_, 0xB000FFFF);
|
||||
}
|
||||
const uint32_t knob_color = joystick_active_ ? 0xB040A0FF : 0x6040A0FF;
|
||||
draw_filled_disc(commands, joystick_knob_x_, joystick_knob_y_, joystick_knob_radius_, knob_color);
|
||||
draw_circle(commands, joystick_knob_x_, joystick_knob_y_, joystick_knob_radius_ * 0.5f, 0x80FFFFFF, 12);
|
||||
}
|
||||
|
||||
// Desktop mouse testing simulation
|
||||
bool on_mouse_button(int button, bool pressed, int x, int y) {
|
||||
if (!enabled_) return false;
|
||||
const float norm_x = float(x) / float(screen_w_);
|
||||
const float norm_y = float(y) / float(screen_h_);
|
||||
if (pressed) {
|
||||
if (button == 1) {
|
||||
return on_finger_down(101, norm_x, norm_y);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
if (button == 1) {
|
||||
return on_finger_up(101, norm_x, norm_y);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool on_mouse_motion(int x, int y) {
|
||||
if (!enabled_) return false;
|
||||
const float norm_x = float(x) / float(screen_w_);
|
||||
const float norm_y = float(y) / float(screen_h_);
|
||||
bool handled = false;
|
||||
if (ui_touch_finger_id_ == 101) {
|
||||
handled |= on_finger_motion(101, norm_x, norm_y);
|
||||
}
|
||||
if (joystick_active_ && joystick_finger_id_ == 101) {
|
||||
handled |= on_finger_motion(101, norm_x, norm_y);
|
||||
}
|
||||
if (camera_finger_id_ == 101) {
|
||||
handled |= on_finger_motion(101, norm_x, norm_y);
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
private:
|
||||
void init_buttons() {
|
||||
buttons_.clear();
|
||||
// Combat Action Wheel (Lower-Right)
|
||||
buttons_.push_back({1, 0x39, 0, 0, 42.0f, "ATK", 0x80D48820, 0xD0FFB040});
|
||||
buttons_.push_back({2, 0x02, 0, 0, 26.0f, "S1", 0x803060C0, 0xD05080FF});
|
||||
buttons_.push_back({3, 0x03, 0, 0, 26.0f, "S2", 0x80903090, 0xD0D050D0});
|
||||
buttons_.push_back({4, 0x04, 0, 0, 26.0f, "S3", 0x80309060, 0xD050D080});
|
||||
buttons_.push_back({5, 0x05, 0, 0, 22.0f, "POT", 0x90A03030, 0xD0FF5050});
|
||||
buttons_.push_back({6, 0x2c, 0, 0, 22.0f, "PICK", 0x80208080, 0xD040B0B0});
|
||||
|
||||
// Top-Right Drawer Menu Toggle
|
||||
buttons_.push_back({99, 0, 0, 0, 20.0f, "MENU", 0x90283848, 0xD0FFB040});
|
||||
|
||||
// Drawer Menu Buttons
|
||||
buttons_.push_back({10, 0x17, 0, 0, 18.0f, "BAG", 0x90D09020, 0xD0FFB040}); // DIK_I
|
||||
buttons_.push_back({11, 0x2e, 0, 0, 18.0f, "CHAR", 0x903060B0, 0xD05080FF}); // DIK_C
|
||||
buttons_.push_back({12, 0x2f, 0, 0, 18.0f, "SKILL", 0x90803090, 0xD0D050D0}); // DIK_V
|
||||
buttons_.push_back({13, 0x31, 0, 0, 18.0f, "QUEST", 0x90308050, 0xD050D070}); // DIK_N
|
||||
buttons_.push_back({14, 0x32, 0, 0, 18.0f, "COMM", 0x90905020, 0xD0E07030}); // DIK_M
|
||||
buttons_.push_back({15, 0x01, 0, 0, 18.0f, "SET", 0x90506070, 0xD08090A0}); // DIK_ESCAPE
|
||||
}
|
||||
|
||||
void layout_buttons() {
|
||||
const float W = float(screen_w_);
|
||||
const float H = float(screen_h_);
|
||||
for (auto& btn : buttons_) {
|
||||
switch (btn.id) {
|
||||
case 1:
|
||||
btn.x = W - 85.0f;
|
||||
btn.y = H - 85.0f;
|
||||
btn.radius = std::min(46.0f, H * 0.12f);
|
||||
break;
|
||||
case 2:
|
||||
btn.x = W - 165.0f;
|
||||
btn.y = H - 85.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 3:
|
||||
btn.x = W - 145.0f;
|
||||
btn.y = H - 155.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 4:
|
||||
btn.x = W - 85.0f;
|
||||
btn.y = H - 175.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 5:
|
||||
btn.x = W - 225.0f;
|
||||
btn.y = H - 75.0f;
|
||||
btn.radius = std::min(24.0f, H * 0.065f);
|
||||
break;
|
||||
case 6:
|
||||
btn.x = W - 85.0f;
|
||||
btn.y = H - 235.0f;
|
||||
btn.radius = std::min(24.0f, H * 0.065f);
|
||||
break;
|
||||
case 99: // MENU toggle button (to the left of MiniMap)
|
||||
btn.x = W - 170.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 20.0f;
|
||||
break;
|
||||
case 10: // BAG
|
||||
btn.x = W - 220.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
case 11: // CHAR
|
||||
btn.x = W - 265.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
case 12: // SKILL
|
||||
btn.x = W - 310.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
case 13: // QUEST
|
||||
btn.x = W - 355.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
case 14: // COMM
|
||||
btn.x = W - 400.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
case 15: // SET
|
||||
btn.x = W - 445.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int find_button(float x, float y) {
|
||||
for (size_t i = 0; i < buttons_.size(); ++i) {
|
||||
const auto& btn = buttons_[i];
|
||||
if (btn.id >= 10 && btn.id <= 15 && !drawer_open_)
|
||||
continue;
|
||||
const float dist = std::hypot(x - btn.x, y - btn.y);
|
||||
if (dist <= btn.radius * 1.25f) {
|
||||
return int(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void trigger_button(int dik, bool pressed) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (dik == 0x39) {
|
||||
PythonBoot::SetAttackKey(pressed);
|
||||
}
|
||||
if (dik != 0) {
|
||||
PythonBoot::UIKey(dik, pressed);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void update_joystick_motion(float px, float py) {
|
||||
const float dx = px - joystick_base_x_;
|
||||
const float dy = py - joystick_base_y_;
|
||||
const float dist = std::hypot(dx, dy);
|
||||
|
||||
if (dist <= joystick_radius_) {
|
||||
joystick_knob_x_ = px;
|
||||
joystick_knob_y_ = py;
|
||||
} else if (dist > 0.0f) {
|
||||
joystick_knob_x_ = joystick_base_x_ + (dx / dist) * joystick_radius_;
|
||||
joystick_knob_y_ = joystick_base_y_ + (dy / dist) * joystick_radius_;
|
||||
}
|
||||
|
||||
if (dist > 8.0f) {
|
||||
const float rad = std::atan2(-dx, -dy);
|
||||
move_angle_ = rad * 180.0f / 3.14159265358979323846f;
|
||||
if (move_angle_ < 0.0f) move_angle_ += 360.0f;
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(move_angle_, true);
|
||||
#endif
|
||||
} else {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(0.0f, false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_line(std::vector<UIRenderCommand>& commands,
|
||||
float x1, float y1, float x2, float y2, uint32_t argb) {
|
||||
UIRenderCommand cmd{};
|
||||
cmd.kind = UIRenderCommand::Line;
|
||||
cmd.x1 = x1; cmd.y1 = y1;
|
||||
cmd.x2 = x2; cmd.y2 = y2;
|
||||
cmd.argb = argb;
|
||||
commands.push_back(cmd);
|
||||
}
|
||||
|
||||
static void draw_rect_bar(std::vector<UIRenderCommand>& commands,
|
||||
float x1, float y1, float x2, float y2, uint32_t argb) {
|
||||
UIRenderCommand bar{};
|
||||
bar.kind = UIRenderCommand::Bar;
|
||||
bar.x1 = x1; bar.y1 = y1;
|
||||
bar.x2 = x2; bar.y2 = y2;
|
||||
bar.argb = argb;
|
||||
commands.push_back(bar);
|
||||
}
|
||||
|
||||
static void draw_rect_lines(std::vector<UIRenderCommand>& commands,
|
||||
float x1, float y1, float x2, float y2, uint32_t argb) {
|
||||
draw_line(commands, x1, y1, x2, y1, argb);
|
||||
draw_line(commands, x2, y1, x2, y2, argb);
|
||||
draw_line(commands, x2, y2, x1, y2, argb);
|
||||
draw_line(commands, x1, y2, x1, y1, argb);
|
||||
}
|
||||
|
||||
static void draw_circle(std::vector<UIRenderCommand>& commands,
|
||||
float cx, float cy, float radius, uint32_t argb, int segments = 16) {
|
||||
const float step = 2.0f * 3.14159265f / float(segments);
|
||||
for (int i = 0; i < segments; ++i) {
|
||||
const float a1 = float(i) * step;
|
||||
const float a2 = float(i + 1) * step;
|
||||
draw_line(commands,
|
||||
cx + std::cos(a1) * radius, cy + std::sin(a1) * radius,
|
||||
cx + std::cos(a2) * radius, cy + std::sin(a2) * radius,
|
||||
argb);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_filled_disc(std::vector<UIRenderCommand>& commands,
|
||||
float cx, float cy, float radius, uint32_t argb) {
|
||||
// Base rectangular fill
|
||||
draw_rect_bar(commands, cx - radius * 0.65f, cy - radius * 0.65f, cx + radius * 0.65f, cy + radius * 0.65f, (argb & 0x00FFFFFF) | 0x55000000);
|
||||
// Cross fills for roundness
|
||||
draw_rect_bar(commands, cx - radius * 0.85f, cy - radius * 0.35f, cx + radius * 0.85f, cy + radius * 0.35f, (argb & 0x00FFFFFF) | 0x55000000);
|
||||
draw_rect_bar(commands, cx - radius * 0.35f, cy - radius * 0.85f, cx + radius * 0.35f, cy + radius * 0.85f, (argb & 0x00FFFFFF) | 0x55000000);
|
||||
// Border rings
|
||||
draw_circle(commands, cx, cy, radius, argb, 20);
|
||||
draw_circle(commands, cx, cy, radius - 1.0f, (argb & 0x00FFFFFF) | 0x40000000, 20);
|
||||
}
|
||||
|
||||
static double get_time_sec() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<duration<double>>(steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
bool enabled_ = false;
|
||||
int screen_w_ = 1280;
|
||||
int screen_h_ = 720;
|
||||
|
||||
bool drawer_open_ = false;
|
||||
int64_t ui_touch_finger_id_ = -1;
|
||||
|
||||
bool joystick_active_ = false;
|
||||
int64_t joystick_finger_id_ = -1;
|
||||
float joystick_base_x_ = 140.0f;
|
||||
float joystick_base_y_ = 580.0f;
|
||||
float joystick_knob_x_ = 140.0f;
|
||||
float joystick_knob_y_ = 580.0f;
|
||||
float joystick_radius_ = 65.0f;
|
||||
float joystick_knob_radius_ = 28.0f;
|
||||
float move_angle_ = 0.0f;
|
||||
|
||||
int64_t camera_finger_id_ = -1;
|
||||
float camera_start_x_ = 0.0f;
|
||||
float camera_start_y_ = 0.0f;
|
||||
float camera_last_x_ = 0.0f;
|
||||
float camera_last_y_ = 0.0f;
|
||||
double camera_down_time_ = 0.0;
|
||||
bool camera_dragged_ = false;
|
||||
|
||||
int64_t pinch_finger1_ = -1;
|
||||
int64_t pinch_finger2_ = -1;
|
||||
float pinch_last_dist_ = 0.0f;
|
||||
|
||||
std::vector<ButtonDef> buttons_;
|
||||
};
|
||||
Reference in New Issue
Block a user