feat: complete native client rendering parity updates

This commit is contained in:
shen
2026-09-27 19:44:41 -07:00
parent 9a9dc6d224
commit 25dd65f2ce
99 changed files with 21121 additions and 921 deletions
+5
View File
@@ -23,6 +23,7 @@ option(MTGODOT_BUILD_EXTENSION "Build the Godot GDExtension (pulls in godot-cpp)
option(MTGODOT_BUILD_TOOLS "Build libgr2 validation tools (gr2dump/gr2fuzz/oracle_diff)" OFF)
option(MTGODOT_BUILD_NET_TOOLS "Build host-only protocol probes without Godot" OFF)
option(MTGODOT_BUILD_PORT "Build only port_logic (with MTGODOT_BUILD_EXTENSION=OFF), e.g. for Linux/Windows" OFF)
option(MT_BUILD_NATIVE_RENDER_PROTOTYPE "Build the standalone Vulkan draw-command prototype" OFF)
# --- shared, cross-platform core (no godot-cpp, no third-party) ---
add_subdirectory(libgr2)
@@ -81,3 +82,7 @@ if(MTGODOT_BUILD_PORT AND NOT MTGODOT_BUILD_EXTENSION)
endif()
add_subdirectory(extension/src/port)
endif()
if(MT_BUILD_NATIVE_RENDER_PROTOTYPE)
add_subdirectory(native_render)
endif()
+1
View File
@@ -0,0 +1 @@
2 3
+2
View File
@@ -555,3 +555,5 @@
{"time": "2026-09-25T03:00:00Z", "event": "route_cleanup", "unit": "client_main legacy elimination", "detail": "Removed legacy AppFlow route, preloads (AppFlow, LiveSmokeTest, PlayableLiveTest, NetTrace), and MT_PORT_PATH fallback from project/client_main.gd. client_main.tscn now unconditionally boots the native 40250 port route (PythonUISurface + Python3DSurface -> system.py). Passing MT_PORT_PATH=legacy emits a deprecation warning and continues on port route.", "tests": ["client_main.tscn verified on port route and legacy warning", "script/port_gate.sh macos PASS (19/19 suites)", "port_map.py check: 0 errors"]}
{"time": "2026-09-24T07:07:19Z", "event": "port_verification_fix", "unit": "Client/root/*.py + UserInterface/PythonApplication platform adapter + MilesLib/SoundManager platform adapter", "detail": "Corrected 74 RUN_AS_IS mappings from unrelated assets/root to 40250 root.epk, added per-script byte/hash verification and stricter port-map validation. Restored GC_TIME server clock semantics and routed packed sound bytes to Godot without applying volume twice; replaced deleted legacy asset-gate tests with active host/audio tests. Qualified MT_PLATFORM_STUB_TRACE exposed 93 called stubs in offline login_flow; SetGlobalCenterPosition was implemented from the original call order, and remaining reached groups were recorded in audit/reports/platform-runtime-trace.md. Android port-only gate exposed the pre-existing Win32Crt.cpp glob/iconv API-24 build blocker after a missing <ctype.h> PCH dependency in Japanese.cpp was repaired.", "tests": ["script/verify_root_pack.py PASS (74/74)", "port_map.py check: 0 errors", "script/port_gate.sh macos PASS", "script/run_40250_asset_gate.sh PASS", "script/python_game_render_test.sh PASS", "MT_PLATFORM_STUB_TRACE=1 port.login_flow PASS (93 called stubs identified)", "android port_gate FAIL: Win32Crt.cpp glob/iconv declarations under NDK API 24"]}
{"time": "2026-09-24T07:31:08Z", "event": "platform_adapter_fix", "unit": "EterLib/GrpScreen.cpp + MilesLib/SoundManager.cpp", "detail": "Implemented reached 40250 UI box and vertical gradient command bridge; audio bridge now preserves named three-slot music fades, frame-step fade speed and limit, indexed 3D volume, 3D play counts, and SaveVolume/RestoreVolume mute state. Remaining sky/water/shadow/culling/IME adapters stay TODO.", "evidence": ["./script/port_gate.sh macos", "./script/python_game_render_test.sh", "godot --headless --path project -s res://audio_driver_test.gd", "port_map.py check"]}
{"date": "2026-09-28T01:31:59+00:00", "unit": "GameLib/MapOutdoorRenderHTP.cpp + GameLib/MapOutdoorCharacterShadow.cpp", "action": "native 40250 render parity: terrain near/mid/far LOD/fog/splat cap, character shadow RTT CPU raster, per-draw depth/sampler states and two-light fixed-function shading", "evidence": "mt_native_render fake-server game 90 frames; 178 3D draws; shadow mem texture 205 nonwhite pixels; matched Windows screenshot pending"}
{"date": "2026-09-28T01:35:06+00:00", "unit": "GameLib/MapOutdoorRenderHTP.cpp", "action": "match 40250 lighting state in terrain splat passes: unlit base textures, temporary lit shadow pass, white fog color only for shadow pass", "evidence": "reference MapOutdoorRenderHTP.cpp non-WORLD_EDITOR branch; native fake-server scene rerun pending"}
+2 -2
View File
@@ -4049,11 +4049,11 @@
"project/gamescene_test.gd",
"project/netbridge_test.gd"
],
"last_test_result": "PARTIAL: msenv/skybox/weather tests, xmas_snow native command, GameScene map filtering and M2Client signal pass; SnowEnvironment pool, DayMode, filter/lens flare/wind and linear fog remain open"
"last_test_result": "PARTIAL: native fake-server 1024x768/24-mob render built and ran 90 frames; terrain and R5G6B5 character shadow target produced scene draws and 204 nonwhite shadow pixels. Same-scene Windows color parity, SnowEnvironment blur, DayMode, filter/lens flare/wind and fog timing remain open."
},
"remaining": [
"实现或明确 screen filter、lens flare 和 SpeedTree/树木风更新,不以 metadata 代替运行时效果",
"修正/验证 40250 线性 fog near/far/color 与 HTP/STP/terrain 的参数和时序",
"原生 HTP 已恢复近/中/远景分段、LOD、splat 上限及远景雾色;继续用 Windows 同场景截图核对颜色、STP 和 fog 时序",
"补齐 DayMode/PRESERVE_DayMode 正式环境切换;WeatherDayNightSystem 的自动地图预设继续隔离,不能替代服务器 xmas_snow 命令",
"对天空 transition、云 plane、SnowEnvironment 池和真实渲染顺序增加参数/screenshot oracle",
"增加重复加载、快速切图、暂停恢复、资源缺失和环境/粒子清理测试",
@@ -9,7 +9,7 @@
"impl": [
"extension/src/platform/GameLib/MapOutdoorCharacterShadow.cpp:CMapOutdoor::SetShadowTextureSize"
],
"note": "40250 body verbatim, kept in the platform file beside the render-to-texture pass it serves (Begin/End are platform no-ops).",
"note": "40250 shadow texture lifecycle in the platform file; Begin/End now run the light-view offscreen pass through the RecordingDevice CPU R5G6B5 target.",
"test": "extension/tests/port_login_flow_test.cpp"
},
"CMapOutdoor::CreateCharacterShadowTexture": {
@@ -17,7 +17,7 @@
"impl": [
"extension/src/platform/GameLib/MapOutdoorCharacterShadow.cpp:CMapOutdoor::CreateCharacterShadowTexture"
],
"note": "40250 body verbatim, kept in the platform file beside the render-to-texture pass it serves (Begin/End are platform no-ops).",
"note": "40250 shadow texture creation retained; the RecordingDevice now rasterizes its R5G6B5 render target and exposes the result as a memory texture.",
"test": "extension/tests/port_login_flow_test.cpp"
},
"CMapOutdoor::ReleaseCharacterShadowTexture": {
@@ -25,22 +25,22 @@
"impl": [
"extension/src/platform/GameLib/MapOutdoorCharacterShadow.cpp:CMapOutdoor::ReleaseCharacterShadowTexture"
],
"note": "40250 body verbatim, kept in the platform file beside the render-to-texture pass it serves (Begin/End are platform no-ops).",
"note": "40250 shadow texture release retained for the native offscreen pass.",
"test": "extension/tests/port_login_flow_test.cpp"
},
"CMapOutdoor::BeginRenderCharacterShadowToTexture": {
"status": "DIVERGENT",
"status": "NEEDS_LIVE",
"impl": [
"extension/src/platform/GameLib/MapOutdoorCharacterShadow.cpp:CMapOutdoor::BeginRenderCharacterShadowToTexture"
],
"note": "Returns false: the render-to-texture character shadow pass is not drawn (RecordingDevice drops off-screen render targets); callers skip the pass as when 40250 fails to begin it."
"note": "Restores 40250 light view, target, viewport and render states; the small R5G6B5 target is rasterized on CPU. Native fake-server capture contains nonwhite shadow pixels; matched Windows screenshot parity remains open."
},
"CMapOutdoor::EndRenderCharacterShadowToTexture": {
"status": "DIVERGENT",
"status": "NEEDS_LIVE",
"impl": [
"extension/src/platform/GameLib/MapOutdoorCharacterShadow.cpp:CMapOutdoor::EndRenderCharacterShadowToTexture"
],
"note": "No-op, paired with BeginRenderCharacterShadowToTexture returning false."
"note": "Restores the 40250 viewport, render target, light state and transforms after the shadow pass; matched Windows screenshot parity remains open."
}
}
}
@@ -9,17 +9,21 @@
"impl": [
"extension/src/platform/GameLib/MapOutdoorRenderHTP.cpp:CMapOutdoor::__RenderTerrain_RenderHardwareTransformPatch"
],
"note": "No-op: the Godot terrain adapter draws the height-field (PORT-PLAN §3); the D3D8 texture-stage splat passes have no recording-device counterpart."
"note": "Native path now follows 40250 near/mid/far patch distance bands, LOD 0/1/2, splat limit and fog-color far fill; Godot path stays adapted. Same-scene Windows color and image comparison remains open."
},
"CMapOutdoor::__HardwareTransformPatch_RenderPatchSplat": {
"status": "DIVERGENT",
"impl": [],
"note": "Not built: only called from __RenderTerrain_RenderHardwareTransformPatch, which is a no-op (Godot draws the terrain, PORT-PLAN §3)."
"impl": [
"extension/src/platform/GameLib/MapOutdoorRenderHTP.cpp:CMapOutdoor::__HardwareTransformPatch_RenderPatchSplat"
],
"note": "Native path draws 40250 terrain splat layers and static/character shadows; texture-0 fallback and state sequencing still differ from the reference and need matched image verification."
},
"CMapOutdoor::__HardwareTransformPatch_RenderPatchNone": {
"status": "DIVERGENT",
"impl": [],
"note": "Not built: only called from __RenderTerrain_RenderHardwareTransformPatch, which is a no-op (Godot draws the terrain, PORT-PLAN §3)."
"status": "NEEDS_LIVE",
"impl": [
"extension/src/platform/GameLib/MapOutdoorRenderHTP.cpp:CMapOutdoor::__HardwareTransformPatch_RenderPatchNone"
],
"note": "Native path now draws the far patch with the 40250 fog texture factor; matched Windows image comparison remains open."
}
}
}
+298
View File
@@ -0,0 +1,298 @@
# 客户端运行流畅度与性能瓶颈调查报告
> **调查日期**:2026-09-24
> **调查目标**:定位 macOS 客户端在运行、移动与战斗过程中出现不流畅、微卡顿(micro-stutter)与跳帧感的根本原因,提出针对性优化方案。
> **状态**:已完成调查,尚未引入代码改动。
---
## 一、 核心结论概述
经链路排查,客户端运行不流畅的**根本原因不在于 40250 原版游戏逻辑本身的性能**,而在于当前 **Godot 与 40250 C++ 桥接管线中存在的 6 大性能与同步瓶颈**:
1. **显存颠簸(致命瓶颈)**:每一帧都在销毁并重新创建数十到上百个 GPU 网格缓冲(`ArrayMesh.new()`),macOS Metal 驱动层发生每秒数千次的显存分配与释放(Memory Allocator Thrashing)。
2. **跨语言数据巨量重复封包与无意义消耗**:每帧将数万顶点、法线、矩阵打包为 Godot Dictionary,且在 UI 表面仅仅为了判断 `has_3d` 就把全量顶点数据封包一次并立即丢弃。
3. **Mac 120Hz ProMotion 与 40250 60 FPS 逻辑时钟失步**:未设置帧率上限导致以 120Hz 刷新,每帧时间在 8ms 与 16ms 之间跳跃,不仅引发明显的视觉顿挫感,还导致所有 CPU/GPU 颠簸开销翻倍。
4. **GDScript 解释执行高频 CPU 计算**:脚本层三重循环执行 4x4 矩阵乘法、逐 UI 图元无条件执行复杂多边形布尔裁剪。
5. **双线程每帧 Ping-Pong 等待**:Godot 主线程与 Python Script 线程通过互斥锁与条件变量每帧往返切换,受 macOS 大小核调度抖动影响。
6. **Retina 屏上的重型渲染器配置**:默认启用了 `Forward+` 与 `MSAA 2x`,在高 DPI 屏幕上带来了不必要的渲染计算与显存带宽压力。
---
## 二、 关键瓶颈深度剖析
### 1. 致命瓶颈:每帧反复创建并销毁 `ArrayMesh`(显存颠簸)
* **代码位置**:`project/python_3d_surface.gd:121, 342-356`
* **实现逻辑**:
```gdscript
func update_frame() -> void:
...
instance.mesh = _build_mesh(draw) # 每帧对每个 3D 绘制对象调用
func _build_mesh(draw: Dictionary) -> ArrayMesh:
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = draw["positions"]
...
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
mesh.surface_set_material(0, _material(draw))
return mesh
```
* **机制危害**:
- 在 Godot 架构中,`ArrayMesh.new()` 配合 `add_surface_from_arrays` 会在底层图形 API(macOS Metal / Vulkan)中**新申请显存分配顶点缓冲区(VBO)和索引缓冲区(IBO)**。
- 场景中包含角色各部位(身体、武器、头发)、怪物、NPC、特效等数十至上百个 draw call。下一帧到来时,上一帧的所有 `ArrayMesh` 被丢弃进 GC 释放显存。
- **以 60 FPS 计,每秒发生 3,000 ~ 6,000 次 GPU 显存分配和释放**;若在 120Hz 屏幕上更达 **6,000 ~ 12,000 次/秒**。
- 这会引发 macOS Metal 驱动内存分配器严重颠簸(Memory Allocator Thrashing)和驱动锁争用,是掉帧与微卡顿的最大源头。
---
### 2. C++ 到 GDScript 巨量跨语言数据封包与重复调用
* **代码位置**:
1. `project/python_ui_surface.gd:192-194`
2. `project/python_3d_surface.gd:89, 136`
3. `extension/src/python_host_node.cpp:169-220`
* **机制危害**:
- `Metin2PythonHost::render3d_draws()` 每次调用都会把所有 3D draw call 的 16 维矩阵、几万个顶点、法线、UV、颜色、索引逐个转换成 `PackedVector3Array`、`PackedFloat32Array` 并封装成带有 20~30 个键值对的庞大 Godot `Dictionary` 数组。
- **严重重复调用**:
在 `python_ui_surface.gd:192-194` 中:
```gdscript
var draws_3d: Array = Metin2PythonHost.render3d_draws()
var has_3d: bool = not draws_3d.is_empty()
```
这里**仅仅为了检查 3D 绘制队列是否为空**,就把整个场景的几何数据完整跨语言序列化了一遍,然后**立即全部丢弃进垃圾回收**!
紧接着在 `python_3d_surface.gd:89` 又完整调用了一遍 `render3d_draws()` 真正用于绘制。
- 此外,`Metin2PythonHost.ui_render_commands()` 每一帧也在 `python_ui_surface.gd:195` 与 `python_3d_surface.gd:136`(`_update_background()`)中**被调用了两次**。
---
### 3. 屏幕刷新率(ProMotion 120Hz)与 40250 逻辑时钟(60 FPS)失步
* **代码位置**:
- `project/project.godot`(未配置 `max_fps`)
- `extension/src/port/EterBase/Timer.cpp:109-116`
* **机制危害**:
- MacBook 屏幕通常为 120Hz ProMotion。当前 `project.godot` 没有设置帧率上限,Godot 默认以 120 FPS 运行 `_process`。
- 这导致上述所有 C++ 内存封包、显存频繁重建、线程唤醒**以每秒 120 次的频率翻倍运行**。
- 更关键的是,40250 原版内部逻辑是以 60 FPS(约 16.6ms)的时钟基准设计的。在 120Hz 下,传递给 40250 的单帧 delta 时间约为 8.3ms,导致游戏内部插值在 8ms 与 16ms 之间来回跳动,产生肉眼可见的“画面抽动 / 跳帧”感。
---
### 4. GDScript 解释器高频数学与几何计算
* **代码位置**:
1. `project/python_3d_surface.gd:77-86`:
```gdscript
static func multiply(a: PackedFloat32Array, b: PackedFloat32Array) -> PackedFloat32Array:
var out := PackedFloat32Array()
out.resize(16)
for r in 4:
for c in 4:
var sum := 0.0
for k in 4:
sum += a[r * 4 + k] * b[k * 4 + c]
out[r * 4 + c] = sum
return out
```
每一帧在 GDScript 解释器层面用三重循环对每一个 3D 模型进行 4x4 矩阵乘法,并频繁分配 16 个元素的数组。
2. `project/python_ui_surface.gd:341-355`:
`_clip_quad` 对界面上每个图元都无条件调用 `Geometry2D.intersect_polygons(...)` 进行多边形相交求值,即使 95% 以上的 UI 元素完全位于屏幕范围内未发生任何裁剪。
---
### 5. 双线程每帧条件变量往返切换(Ping-Pong Jitter)
* **代码位置**:`extension/src/platform/ScriptLib/PythonBoot.cpp:597, 612-616`
* **机制危害**:
Godot 主线程与 Python Script 线程通过 `g_fiber.cv.notify_all()` 与 `cv.wait()` 互斥锁交替执行。主线程渲染完必须挂起等待 Script 线程完成 `Process()`,若 macOS 系统调度器将其中一个线程调度到了能效核(E-Core)或与其他系统后台任务竞争,会造成帧间隔不均匀(Frame Time Jitter)。
---
### 6. 渲染器配置过重(Forward+ & MSAA 2x)
* **代码位置**:`project/project.godot:40-41`
```ini
renderer/rendering_method="forward_plus"
anti_aliasing/quality/msaa_3d=2
```
* **机制危害**:
Metin2 模型是典型的 2004 年代 Direct3D 8 单方向光、无复杂 PBR 的固定管线模型。在 Mac Retina 高分辨率(例如 3K/4K 物理分辨率)下,Godot 的 `Forward+` 会执行完整的光照聚类裁剪(Clustered Shading Compute Shaders),且 `MSAA 2x` 在高分辨率下会产生巨大的 Resolve 显存带宽负担。
---
## 三、 优化实施方案与优先级建议
| 阶段 | 优化措施 | 涉及文件 | 预期成效 |
| :--- | :--- | :--- | :--- |
| **P0** | **锁定 60 FPS 帧率**<br>在 `project.godot` 配置 `application/run/max_fps=60` | `project/project.godot` | 彻底消除 120Hz/60FPS 帧率抖动与跳帧感,立竿见影减少一半 CPU/GPU 压力与发热 |
| **P0** | **消除跨语言重复调用**<br>1. C++ 暴露轻量 `Metin2PythonHost.has_3d_draws()`,UI 表面不再调用 `render3d_draws()` 判空。<br>2. 缓存或单次提取 `ui_render_commands()`,避免同帧重复调用。 | `extension/src/python_host_node.cpp`<br>`project/python_ui_surface.gd`<br>`project/python_3d_surface.gd` | 消除每帧数万个无用 Variant 的内存分配与 GC 压力 |
| **P1** | **网格缓存与显存复用**<br>对动态 mesh 进行池化或直接利用动态更新,避免每帧 `ArrayMesh.new()` 导致 GPU VBO/IBO 频繁申请与释放。 | `project/python_3d_surface.gd` | **彻底根治 Metal 显存分配器颠簸**,使画面运行保持平滑稳定 |
| **P1** | **UI 裁剪短路优化**<br>若图元 quad 的外接矩形完全在屏幕与 clip 矩形之内,直接跳过 `Geometry2D.intersect_polygons` 计算。 | `project/python_ui_surface.gd` | 显著减轻 GDScript 解释器的 CPU 计算负担 |
| **P2** | **渲染器与抗锯齿轻量化**<br>评估切换为 `mobile` 渲染模式,在高 PPI Retina 屏上关闭 MSAA 2x 或改用开销更低的抗锯齿方案。 | `project/project.godot` | 大幅度降低 GPU 填充率与显存带宽消耗 |
---
## 四、 已实施优化与复测对比(2026-09-25)
### 优化轮次:`ui_draw` 专项优化
1. **C++ 端预分配与 StringName 优化**:在 `extension/src/python_host_node.cpp` 中预分配 Command Array 空间,使用静态 `StringName` 替换所有字典字符串键,避免每帧数百次重复字符串分配与动态哈希运算。
2. **纹理元数据与 Atlas 缓存**:在 `project/python_ui_surface.gd` 建立 `_tex_info_cache`,缓存纹理 RID、Atlas UV 缩放/偏移与全图默认 UV 数组,将 Atlas 区域与大小计算彻底移出每帧绘制主循环。
3. **图元包围盒快速裁剪**:基于预计算的 `x1, y1, x2, y2` 与裁剪视口 `clip_x1, clip_y1, clip_x2, clip_y2` 执行 4 次浮点比较:
- 完全在视口外的图元直接跳过;
- 完全在视口内的图元(占比 >95%)走无裁切快路径,直接组装多边形顶点,完全跳过多边形求交计算与中间数组构造。
4. **颜色数组与画布清理池化**:缓存高频单色数组,避免 `PackedColorArray([color])` 的动态堆分配;仅清除当帧实际使用过的 CanvasItem 节点。
### 复测性能对比(稳态场景 240 帧采样)
| 性能指标 | 优化前 | 优化后 | 改善幅度 |
| :--- | :--- | :--- | :--- |
| **`ui_draw` 耗时 (P50)** | **9.267 ms** | **1.798 ms** | **-80.6%(减少 7.47 ms)** |
| **单帧平均耗时 (Mean)** | 21.126 ms (47.3 FPS) | **16.666 ms (60.0 FPS)** | **-21.1%(稳定跑满 60 帧)** |
| **单帧中位数耗时 (P50)** | 20.964 ms | **16.667 ms** | **-20.5%** |
| **95分位耗时 (P95)** | 22.391 ms | **16.920 ms** | **-24.4%(抖动几乎消除)** |
| **测试门禁状态** | PASS | **PASS (12/12 全绿)** | 画面与逻辑 100% 保持一致 |
### 多怪战斗复测与字形批处理(2026-09-25)
用户已在 40250 原版客户端实测相同怪物量运行流畅。因此,多怪场景的卡顿应优先排查移植层增加的开销,不能仅归因于原版逐字绘制或怪物数量。
`MT_FAKE_MOB_COUNT=24` 场景中,原始 UI 命令约 1000 条,其中约 900 条是字体图集 `mem:1@51` 的字形。Godot 端逐条将这些字形转换为画布多边形,是移植后额外的每帧开销。现在 C++ 在不改变命令顺序的前提下,把同纹理、无遮挡裁剪、无蒙版的相邻字形合并为一次三角形数组提交;其他 UI 图元沿用原路径。3D 背景提取也复用同帧批处理命令。
| 24 怪场景指标 | 批处理前 | 批处理后 |
| :--- | ---: | ---: |
| 空闲帧 P50 | 27.424 ms | 17.790 ms |
| 空闲帧 P95 | 28.788 ms | 19.221 ms |
| 空闲帧 UI 绘制 P50 | 7.253 ms | 1.086 ms |
| 战斗帧 P50 | 22.429 ms | 16.632 ms |
| 战斗帧 UI 绘制 P50 | 6.470 ms | 1.063 ms |
批处理后的一帧诊断中,约 1000 条原始 UI 命令缩减为 345 条提交命令,其中 7 个批次容纳 666 个字形。单怪场景的空闲与战斗帧 P50 均约 16.66 ms。24 怪场景的登录、进图、贴地、贴图、光照、截图及击杀检查通过。空闲帧 P50 仍约 17.8 ms,`ui_update` P50 约 11.6 ms,是后续分析重点。以上数字来自本地自动化场景,不能当作原版与移植版在同一机器上的直接性能对比。
原始和复测数据分别在 `build/rendering/python-game-20260925-064436-59271/report.json` 与 `build/rendering/python-game-20260925-072043-60686/report.json`。
### 64 怪压力测试(2026-09-25)
`MT_FAKE_MOB_COUNT=64` 的本地测试通过登录、进图、画面及击杀检查;截图检查时视野内记录到 38 个角色。无额外原生计时器时,空闲 240 帧的帧间隔 P50 为 **27.028 ms**、P95 为 **28.785 ms**(约 37 FPS);战斗采样 P50 为 **16.732 ms**、P95 为 **21.449 ms**。战斗段的可见内容和角色数量会变化,因此不能凭其 P50 宣称 64 怪稳定 60 FPS。数据见 `build/rendering/python-game-20260925-072444-60992/report.json`。
空闲帧里,Godot 记录的 `ui_update` P50 为 17.146 ms、3D 表面更新 P50 为 6.292 ms、UI 画布绘制 P50 为 1.737 ms。`ui_update` 这个字段包含脚本线程执行完整的原生 `CPythonApplication::Process()`,并不等于纯界面更新。临时细分计时表明原生窗口树 `OnUIUpdate()` 只有约 0.3 ms,原生渲染录制约 16 ms;其中 `RenderGame()` 约 11 ms,角色 `Deform()` 约 5 ms,角色 `Render()` 约 5 ms,背景约 0.8 ms。这些是连续 120 帧的平均值,计时探针本身会增加少量开销;探针已移除,日志保存在 `build/rendering/python-game-20260925-072854-61643/godot.log`。
这一场景的原始 UI 图像命令为 1747 条,其中字体图集字形 1619 条;批处理后的命令总量 538 条,13 个字形批次覆盖 1229 个字形。后续优先分析角色变形和原生绘制录制的逐怪线性开销,再分析 Godot 3D 几何提交;继续优化 Godot UI 画布预计收益较小。任何角色动画降频、剔除或 LOD 都要先与 40250 原版可见行为对照。
### 渲染转换第一轮(2026-09-25)
先试过对动画 `ArrayMesh` 原地更新;单怪测试中 3D 更新 P50 升至约 12 ms。临时细分计时发现每帧顶点打包和拓扑哈希使跨语言命令提取耗时约 14 ms,而且网格原地更新没有命中。该试验已撤回,没有保留在运行路径。
随后改为在 Godot 单精度构建中,把原生连续 `float` 顶点、法线和 UV 批量复制到对应 Packed 数组;非单精度构建保留逐元素转换。64 怪场景在相同临时计时下,命令提取 P50 从 **2.583 ms** 降至 **1.571 ms**,3D 表面更新 P50 从 **6.857 ms** 降至 **5.280 ms**,空闲帧 P50 从 **28.109 ms** 降至 **25.603 ms**;战斗 P50 仍约 16.7 ms。数据见 `build/rendering/python-game-20260925-075500-62940/report.json` 和 `build/rendering/python-game-20260925-075707-63151/report.json`。单怪测试空闲帧 P50 为 16.667 ms,24 怪两次复测 P50 为 18.639/18.547 ms;三档测试的画面和击杀检查均通过。24 怪整体帧时间比更早的 17.790 ms 测量略高,而本次测得的 3D 表面更新时间更低,差异主要出现在原生帧阶段,仍需重复采样判断。
移除临时细分计时器后,最终 64 怪复测的空闲帧 P50/P95 为 **25.591/27.372 ms**,3D 表面更新 P50 为 **5.240 ms**,战斗 P50/P95 为 **16.718/21.065 ms**,画面和击杀检查通过;见 `build/rendering/python-game-20260925-080029-63326/report.json`。这仍未达到 64 怪稳定 60 FPS。下一步若改用 C++ RenderingServer 或 GPU 蒙皮,应分别验证稳定网格身份与拓扑复用、骨骼权重和绑定姿态的导出,以及逐帧顶点/GPU 同步成本;不能把底层 API 本身当作零拷贝或固定收益保证。
### 批量转换与几何缓存完整验证(2026-09-25 最新复测)
对当前未提交代码(含 C++ 连续内存批量 `memcpy` 复制、静态网格 `geometry_key` 缓存复用、UI 批处理、以及输入与拾取射线同步)进行了完整的 1 怪、24 怪、64 怪多密度验证:
| 测试场景 | 空闲帧 P50 | 空闲帧 P95 | 3D 表面更新 P50 | UI 绘制 P50 | 战斗帧 P50 | 战斗 3D 更新 P50 | 门禁与击杀检查 |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| **1 怪 (单体基准)** | **16.679 ms** (60 FPS) | 16.825 ms | **1.928 ms** | 0.653 ms | 16.670 ms | 1.664 ms | PASS (已击杀) |
| **24 怪 (中等密度)** | **17.611 ms** | 18.078 ms | **3.346 ms** | 1.061 ms | 16.655 ms | 2.499 ms | PASS (已击杀) |
| **64 怪 (极限压力)** | **25.619 ms** (约 39 FPS) | 26.493 ms | **5.252 ms** | 1.664 ms | 16.710 ms | 2.926 ms | PASS (已击杀) |
**结论与评估**:
1. **优化切实有效且无低密度回退**:
- 1 怪场景的 3D 表面更新由最初的 2.3 ms 降至 1.93 ms,空闲与战斗均稳定在 60 FPS(16.67 ms),证明没有像第一版那样出现小场景负优化(之前第一版原地更新曾劣化到 11.9 ms)。
- 24 怪场景空闲帧 P50 取得 17.611 ms、P95 取得 18.078 ms,创下历史最佳稳定性;3D 表面更新仅 3.35 ms,战斗全程维持 60 FPS(16.66 ms)。
- 64 怪极端场景下,3D 表面更新由优化前基准的 6.857 ms 降至 5.252 ms(**减少 1.605 ms / -23.4%**);空闲帧由 28.109 ms 降至 25.619 ms(**减少 2.49 ms / -8.9%**)。
2. **正确性完全保证**:
- 1 怪、24 怪、64 怪所有用例的地面贴地、材质贴图、光照、屏幕对照度分析、目标锁定与鼠标左键普攻击杀(HP 递减为 [100, 66, 33, 0])均 100% 通过。
- 报告数据分别保存在:
- 1 怪:`build/rendering/python-game-20260925-090440-78310/report.json`
- 24 怪:`build/rendering/python-game-20260925-090521-78357/report.json`
- 64 怪:`build/rendering/python-game-20260925-090617-78378/report.json`
### 独立 Vulkan/MoltenVK 原型(2026-09-25)
新增 `native_render/`:SDL3 创建独立窗口,Vulkan 创建交换链、顶点缓冲、管线与逐 draw 提交;macOS 通过 MoltenVK 映射到 Metal。它直接读取录制设备的 `Render3DDraw` 结构,也能回放 Godot 客户端显式捕获的一帧。构建和回放命令见 `native_render/README.md`。现有 Godot 客户端运行路径未切换。
#### 第一阶段:基础提交与每帧 CPU 展开基线
Apple M4 上,合成的 64 draw × 333 三角形(63,936 个展开顶点)运行 60 帧,平均帧间隔 16.64 ms,CPU 几何准备 6.80 ms。真实 64 怪测试捕获的一帧(`64-mob.mtdr`,v1)为 51 draw、51,231 个索引;回放 60 帧,平均帧间隔 16.81 ms,CPU 几何准备 5.96 ms。
#### 第二阶段:按 `geometry_key` 持久化 GPU 顶点/索引缓冲 + 着色器矩阵变换
1. **持久化 GPU 缓冲与按修订号增量上传**:以 `GeometryId{geometry_key, signature}` 作为 GPU 缓冲缓存键(而非 draw 数组下标,避免视锥裁剪导致 draw 顺序变化时整表失效),仅在新几何出现或 `geometry_revision` 变化时上传顶点与索引缓冲。
2. **着色器 `world * view * proj` 变换**:将每帧 CPU 顶点变换移入 `native.vert` Push Constants,索引缓冲直接走 `vkCmdDrawIndexed`(消除每帧 CPU 三角形展开)。
#### 第三阶段:深度缓冲、DDS 贴图、法线/UV 与 D3D8 固定管线状态(Version 3 捕获)
1. **深度与固定管线状态**:新增 `VK_FORMAT_D32_SFLOAT` 深度附件,按 `(cull_mode, z_enable/z_write, alpha_blend/dest_blend)` 缓存 `VkPipeline`,在 `native.vert` / `native.frag`(128 字节 Push Constants,满足桌面与 Android Vulkan 最小保证)中实现 D3D8 方向光(`light0` + 环境光 + 自发光)、`texture_factor` 调制与 `alpha_test` 裁剪。
2. **自包含 DDS 贴图回放**:捕获格式升级为 Version 3(向下兼容 v1/v2),在 `MT_NATIVE_CAPTURE_PATH` 触发时自动从 40250 pack 提取被引用的 `.dds` 原始字节写入 `.mtdr`;`mt_native_render` 复用 `extension/src/dxt.cpp` 在首帧软解 DXT1/3/5 并上传至 `VK_IMAGE_TILING_OPTIMAL` 纹理与描述符集。
3. **修复无 `MT_PROFILE_FRAME` 时 64 怪测试 `dog_screen=(-1,-1)` 失败的根因**:
- 根因一:`CPythonNetworkStream::GamePhase()` 每帧最多消费 3 个网络包,64 怪场景的进图突发包(约 147 个包,含末尾 `GC_PING`)需要约 49 帧排空;此前未开启 `MT_PROFILE_FRAME` 时仅等待 30 帧就点击地面移动,导致客户端在回复 `CG_PONG`(254)前先发出了 `CG_CHARACTER_MOVE`(7),伪造服务端报 `expected CG header 254, got 7` 并主动断开连接清空角色。
- 根因二:`settled` 闭包未检查 `now.x >= 0.0`,连续两帧 `(-1, -1)` 距离为 `0.0 < 0.5` 会提前退出等待。
- 修复后,`MT_FAKE_MOB_COUNT=64` 在不开启 `MT_PROFILE_FRAME` 的捕获运行中也 100% 通过所有贴图、光照、投影与普攻击杀检查(`build/native-render/capture-64mob-v3/report.json`)。
#### Apple M4 原生 Vulkan 60 帧回放实测对比
| 测试用例(60 帧,FIFO 垂直同步) | Draw 数 | 顶点数 | 索引数 | 60 帧总几何上传 | 贴图上传 | 全帧平均 `prepare_ms` | **稳态 `steady_prepare_ms`(第 2~60 帧)** | `submit_ms` | `mean_frame_ms` |
| :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| **合成 64 draw(阶段一:每帧 CPU 展开)** | 64 | 63,936 | — | 3,840 次 | 0 | 6.80 ms | 6.80 ms | 8.21 ms | 16.64 ms |
| **真实 64 怪 v1(阶段一:每帧 CPU 展开)** | 51 | 51,231 | — | 3,060 次 | 0 | 5.96 ms | 5.96 ms | 9.33 ms | 16.81 ms |
| **合成 64 draw(阶段二/三:持久 GPU 缓冲)** | 64 | 63,936 | 63,936 | **64 次** (3.32 MB) | 0 | 0.17 ms | **0.13 ms** | 13.88 ms | 16.60 ms |
| **真实 64 怪 v2(51 draw,持久 GPU 缓冲)** | 51 | 216,384 | 51,231 | **51 次** (10.59 MB) | 0 | 0.21 ms | **0.11 ms** | 14.13 ms | 16.35 ms |
| **真实 64 怪 v3(90 draw,含 17 张 DDS 贴图 + 光照 + 深度)** | 90 | 244,520 | 157,647 | **90 次** (12.37 MB) | **17 次** (18.37 MB) | 2.48 ms(含首帧冷启动解纹理) | **0.23 ms** | 13.57 ms | 18.75 ms |
| **真实 64 怪 v3 + 首 draw 每帧动画修订(`--animate-first-draw`)** | 90 | 244,520 | 157,647 | **149 次** (90 + 59) | **17 次** (18.37 MB) | 2.66 ms(含首帧冷启动解纹理) | **0.43 ms** | 13.45 ms | 18.82 ms |
#### 第四阶段:硬件 GPU 时间戳、40250 地形 HTP、2D UI/字形/小地图遮罩、GPU 骨骼蒙皮与实时游戏主循环接入(Version 4 捕获)
1. **分离真实 GPU 执行耗时(`gpu_ms`)与垂直同步等待(`submit_ms`)**:
- 在 `native_render/main.cpp` 中接入 `VkQueryPool`(`VK_QUERY_TYPE_TIMESTAMP`),在 `vkCmdBeginRenderPass`(`TOP_OF_PIPE`)与 `vkCmdEndRenderPass`(`BOTTOM_OF_PIPE`)记录硬件时间戳并换算为毫秒级 `gpu_ms`。
- 新增 `--no-vsync`(优先选用 `VK_PRESENT_MODE_IMMEDIATE_KHR` / `MAILBOX_KHR`),将交换链垂直同步等待从 `submit_ms` 中剥离。
2. **40250 原生硬件变换地形块(HTP)渲染**:
- 在 `extension/src/platform/GameLib/MapOutdoorRenderHTP.cpp` 中实现 `CMapOutdoor::__RenderTerrain_RenderHardwareTransformPatch()` 与 `__HardwareTransformPatch_RenderPatchSplat(...)`(由 `SetNativeTerrainRenderEnabled(true)` 或 `MT_NATIVE_TERRAIN=1` 启用,不影响 Godot 默认测试路径)。
- 在 `extension/src/platform/EterLib/RecordingDevice.cpp` 中支持 D3D8 `D3DTSS_TCI_CAMERASPACEPOSITION` 相机空间纹理矩阵生成地形基础层与 Alpha Splat 层 UV,并将 `m_textures[0]` 混入 `geometry_key` 实现跨帧持久化缓存。
3. **2D `UIRenderCommand` 全量渲染(含字形图集与小地图圆角遮罩)**:
- 支持 `behind_3d` 与前景两趟正交 2D 渲染,涵盖 `Bar`、`GradientBar`、`Line`、`Image`、裁剪矩形(`clip_x1..y2`)、`mem:<id>@<revision>` 动态字形图集(`"MTRA"` 原始 RGBA 编码)、`.tga`(含未压缩与 RLE)解码,以及 `CPythonMiniMap` 的双纹理(`tex0` + `mask_tex`)圆角遮罩混合。
- 相邻同状态、同纹理的 UI 图元与字形自动合并为单次 `vkCmdDrawIndexed` 批次提交(64 怪场景 1,597 个 UI/字形四边形成批为 31 个 Vulkan 批次)。
4. **GPU 线性混合骨骼蒙皮(GPU Skeletal Skinning)**:
- 在 `extension/src/platform/EterGrnLib/GrannyRuntime.cpp` 中,当启用 GPU 蒙皮(`--gpu-skinning` 或 `MT_GPU_SKINNING=1`)时,`GrannyDeformVertices` 不再逐帧在 CPU 上对每个顶点做矩阵加权变形,而是按不可变的源网格指针(`SourceVertices`)提取一次静态绑定姿态(Bind-Pose)顶点、法线、4 骨骼索引(`bone_indices`)与 4 骨骼权重(`bone_weights`),仅将当前实例的骨骼矩阵调色板(`bone_count * 16` 个 `float`)写入切片表。
- 在 `RecordingDevice.cpp` 中,所有共享同一 `.gr2` 源网格的怪物/角色实例共享同一个 `geometry_key` 且 `geometry_revision` 恒为 `1`;`native.vert` 通过 `set = 1, binding = 0` 的 `BonePalette` SSBO 在顶点着色器中完成 4 骨骼线性混合蒙皮。
5. **实时游戏主循环直接接入(`--live-client`)**:
- 当 `mt_native_render` 在主构建目录 `build/` 下构建时,直接链接 `port_platform`、`mtpython` 与 `FakeLoginServer`,通过 `--live-client <ClientDir>` 在原生 SDL3 + Vulkan 窗口内直接启动 40250 `system.py`,完成登录、选人、进图、刷出 1~64 只怪物、转发 SDL3 键鼠事件,并可导出包含 3D 骨骼权重、地形、2D UI 与字形页的 Version 4 `.mtdr` 捕获文件。
#### Apple M4 第四阶段实测数据(实时客户端 `--live-client` 与 v4 回放,60 帧)
| 运行模式(Apple M4,60 帧) | 呈现模式 | 3D Draw (含地形) | GPU 蒙皮 Draw | UI 批次 / 四边形 | 顶点数 / 索引数 | **60 帧动态几何重传** | **`game_update_ms` (40250 逻辑)** | **稳态 `steady_prepare_ms`** | **`submit_ms`** | **硬件 `gpu_ms`** | **`mean_frame_ms`** |
| :--- | :--- | ---: | ---: | ---: | ---: | :--- | ---: | ---: | ---: | ---: | ---: |
| **实时 1 怪 (`--live-client --gpu-skinning --no-vsync`)** | `IMMEDIATE` | 74 | 10 | 31 / 220 | 225,355 / 91,119 | 30 次 (9.8 KB) | 5.55 ms | **0.62 ms** | 2.22 ms | **0.36 ms** | **8.48 ms** (~118 FPS) |
| **实时 64 怪・CPU 蒙皮 (`--live-client --no-gpu-skinning --no-vsync`)** | `IMMEDIATE` | 116 | 0 | 31 / 1,597 | 253,999 / 207,543 | **3,178 次 (205.37 MB)** | 19.93 ms | 3.42 ms | 0.16 ms | **0.83 ms** | 23.62 ms (~42 FPS) |
| **实时 64 怪・GPU 蒙皮 (`--live-client --gpu-skinning --no-vsync`)** | `IMMEDIATE` | 117 | **52** | 31 / 1,597 | 254,003 / 207,549 | **28 次 (9.18 KB)** | **10.51 ms** | **0.78 ms** | **0.10 ms** | **0.79 ms** | **11.46 ms (~87 FPS)** |
| **实时 64 怪・GPU 蒙皮 (`--live-client --gpu-skinning`,默认 FIFO)** | `FIFO` | 116 | **52** | 31 / 1,597 | 253,999 / 207,543 | **19 次 (6.23 KB)** | **10.42 ms** | **0.79 ms** | **0.10 ms** | **0.88 ms** | **11.37 ms** |
| **v4 捕获回放 + 骨骼动画 (`--capture 64-mob-v4.mtdr --animate-bones --no-vsync`)** | `IMMEDIATE` | 116 | **52** | 31 / 1,597 | 253,999 / 207,543 | **首帧 74 次,第 2~60 帧 0 次** | — | **1.11 ms** | 5.19 ms | **1.08 ms** | 12.25 ms |
**关键验证结论**:
1. **真实 GPU 耗时(`gpu_ms`)极低**:通过 Vulkan 硬件时间戳确认,Apple M4 执行完整 64 怪场景(117 个 3D Draw 含地形多级 Splat、52 个 GPU 骨骼蒙皮网格、31 个 UI 批次共 1,597 个 UI/字形四边形、约 25.4 万顶点)的单帧真实 GPU 耗时仅为 **0.79 ~ 0.88 ms**,此前 `submit_ms` 的 ~13.5 ms 完全来自 `FIFO` 垂直同步等待。
2. **GPU 骨骼蒙皮消除 99.995% 动态顶点带宽并减半 CPU 帧耗时**:在 64 怪实时客户端中,开启 `--gpu-skinning` 后:
- 60 帧动态几何上传量从 CPU 蒙皮的 **3,178 次 / 205.37 MB** 降至 **28 次 / 9.18 KB**(所有同模型怪物实例共享同一份静态绑定姿态 GPU 缓冲);
- 40250 游戏帧更新 `game_update_ms` 从 **19.93 ms** 降至 **10.51 ms**(每帧节省 **9.42 ms**);
- 渲染准备 `steady_prepare_ms` 从 **3.42 ms** 降至 **0.78 ms**(每帧节省 **2.64 ms**);
- 端到端单帧总耗时 `mean_frame_ms` 从 **23.62 ms(~42 FPS)** 降至 **11.46 ms(~87 FPS)**,在 Debug 构建下即已稳定跑进 60 FPS(16.67 ms)预算以内。
#### 第五阶段:完整可玩原生客户端补齐(水面/天空盒/云层/SpeedTree/球面环境高光、音频、全键鼠/IME/软件光标、交互启动与 Release `-O3` 构建)
按路线图顺序完成全部 5 项剩余原生特性(通过 `IsNativeTerrainRenderEnabled()` 门控新 3D 要素,保持 Godot 默认路径 32/32 回归检查 100% 通过):
1. **交互启动与窗口/贴图扩展**:
- 新增 `--interactive`(无限帧循环直至关闭窗口)、`--login-screen`(停留在 `introLogin.LoginWindow` 供手动输入账号密码)、`--live-server HOST:AUTH_PORT:GAME_PORT`(直连外部 40250 服务端)与 `--width W --height H`。
- 支持 `SDL_WINDOW_RESIZABLE` 与 `VK_ERROR_OUT_OF_DATE_KHR` / `VK_SUBOPTIMAL_KHR` 交换链自动重建,并同步调用 `PythonBoot::SetUISize`。
- 接入 macOS `ImageIO` 解码器,支持从 40250 pack 直接解码 `.jpg` / `.png` / `.bmp` 登录背景与 UI 贴图;并在未显式设置 `VK_ICD_FILENAMES` 时自动探测 Homebrew `MoltenVK_icd.json`。
2. **补齐剩余 3D 场景视觉要素**:
- **水面(Water)**:移植 `extension/src/platform/GameLib/MapOutdoorWater.cpp`(`CMapOutdoor::RenderWater` 与 `DrawWater`),支持 30 帧循环水面贴图、相机空间水面变换矩阵与基于高度差的顶点 Alpha 混合。
- **天空盒与动态云层(Skybox & Clouds)**:移植 `extension/src/platform/EterLib/SkyBox.cpp`(`CSkyObjectQuad`、`CSkyObject`、`CSkyBox::Render`、`CSkyBox::RenderCloud`),并在 `RecordingDevice.cpp` 中支持 Stage 0 二维纹理矩阵变换(`D3DTTFF_COUNT2`)与 `D3DTOP_MODULATEINVALPHA_ADDCOLOR`(19)云层着色模式。
- **SpeedTree 森林与树木(SpeedTreeLib)**:实现 `SpeedTreeForest.cpp`、`SpeedTreeForestDirectX8.cpp` 与 `SpeedTreeWrapper.cpp`,读取 40250 `.spt` 属性与 `"TreeSize"` 参数生成带树皮贴图与树叶交叉十字网格的 Z-up 顶点/索引缓冲并参与渲染。
- **多级纹理高光(Specular Sphere-Map)与完整混合因子**:在 `PythonApplication.cpp` 中实现 `CGrannyMaterial::CreateSphereMap` 与 `TranslateSpecularMatrix`,在 `native.vert` / `native.frag` 中实现球面环境映射 UV 生成与 `D3DTOP_MODULATEALPHA_ADDCOLOR`(18)高光叠加,并补齐完整 D3D8 `src_blend` / `dest_blend` 因子映射。
- **修复退出纯虚函数崩溃**:移除基类析构函数 `CSkyObject::~CSkyObject()` 对纯虚函数 `Destroy()` 的调用,以及 `CSpeedTreeForest::~CSpeedTreeForest()` 对纯虚函数 `Clear()` 的调用(移入派生类 `CSpeedTreeForestDirectX8::~CSpeedTreeForestDirectX8()`),解决客户端退出时 `libc++abi: Pure virtual function called!` 崩溃。
3. **音频播放接入(SDL3 Audio + AudioToolbox)**:
- 在 `native_render/main.cpp` 中实现 `NativeAudioEngine`,每帧排空 `DrainAudioCommands()`,通过 macOS `AudioToolbox`(`AudioFileOpenWithCallbacks` + `ExtAudioFileRead`)直接从内存解包 `.wav` / `.mp3` 为 44.1 kHz 双声道浮点 PCM,送入 `SDL_AudioStream` 完成 2D/3D 音效混音与 BGM 淡入淡出循环播放。
4. **完整键盘/IME 文本输入与原生软件光标**:
- 补齐 `F1`–`F12`、`Tab`、`Backspace`、`Return`、`Delete`、`Shift`/`Ctrl`/`Alt`、方向键、数字小键盘及符号键的 `SDL_Scancode -> DIK_*` 与 `VK_*`(`PythonBoot::UIIMEKeyDown`)映射,接入 `SDL_EVENT_TEXT_INPUT` 转发 UTF-8 字符至 `PythonBoot::UIChar`。
- 启用 40250 `CURSOR_MODE_SOFTWARE`(`OnMouseUpdate` / `OnMouseRender`),当游戏软件光标激活时自动隐藏系统光标并渲染 40250 原生彩色游戏光标。
5. **Release (`-O3`) 构建与实测对比(Apple M4,180 帧,`--no-vsync`)**:
| 场景与构建配置(Apple M4,180 帧) | 3D Draw | GPU 蒙皮 Draw | UI 批次 / 四边形 | 顶点数 / 索引数 | **`game_update_ms` (逻辑+UI+音频)** | **稳态 `steady_prepare_ms`** | **硬件 `gpu_ms`** | **`mean_frame_ms`** |
| :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| **完整场景 1 怪 · Debug 构建** | 97 | 10 | 32 / 221 | 225,447 / 91,257 | 5.82 ms | 0.62 ms | 0.44 ms | 8.27 ms (~121 FPS) |
| **完整场景 1 怪 · Release (`-O3`) 构建** | 96 | 10 | 32 / 221 | 225,443 / 91,251 | **1.36 ms** (**4.3x 加速**) | **0.79 ms** | **0.67 ms** | **8.33 ms** (~120 FPS) |
| **完整场景 64 怪 · Debug 构建** | 140 | 52 | 32 / 1,598 | 254,095 / 207,687 | 9.89 ms | 0.87 ms | 0.77 ms | 13.45 ms (~74 FPS) |
| **完整场景 64 怪 · Release (`-O3`) 构建** | **140** | **52** | **32 / 1,598** | **254,095 / 207,687** | **1.62 ms** (**6.1x 加速**) | **0.77 ms** | **1.00 ms** | **8.24 ms** (**~121 FPS**) |
| **完整场景 64 怪 `.mtdr` 回放 · Release (`-O3`)** | 140 | 52 | 31 / 1,597 | 254,095 / 207,687 | — | **0.19 ms** | **1.02 ms** | 8.62 ms |
+27
View File
@@ -369,6 +369,9 @@ const Metin2World::Chunk *Metin2World::chunk_at(int tx, int ty) const {
}
bool Metin2World::build_chunk(int tx, int ty) {
const bool profile_map = std::getenv("MT_PROFILE_MAP") != nullptr;
const auto ticks = [] { return Time::get_singleton()->get_ticks_usec(); };
const double profile_start = profile_map ? ticks() : 0;
const std::string dir =
std::string(map_dir().utf8().get_data()) + "/" + fmt::m2coord::tile_dir(tx, ty);
auto hm = std::make_shared<fmt::HeightMap>();
@@ -378,12 +381,15 @@ bool Metin2World::build_chunk(int tx, int ty) {
++chunks_failed;
return false;
}
const double profile_height = profile_map ? ticks() : 0;
auto am = std::make_shared<fmt::AttrMap>();
if (!fmt::load_attr_map(dir + "/attr.atr", *am, &err))
am.reset(); // 非致命:attr 缺失 -> 该区块无阻挡
const double profile_attr = profile_map ? ticks() : 0;
fmt::TerrainMesh tmesh;
fmt::build_terrain_mesh(*hm, tx, ty, setting.height_scale, tmesh);
const double profile_terrain = profile_map ? ticks() : 0;
Ref<Material> mat;
{
@@ -394,19 +400,26 @@ bool Metin2World::build_chunk(int tx, int ty) {
}
bool splatted = false;
double profile_tile_ms = 0, profile_alpha_ms = 0, profile_material_ms = 0;
if (splat_ready && resolver) {
fmt::TileMap tile;
std::string e2;
const double tile_start = profile_map ? ticks() : 0;
if (fmt::load_tile_map(dir + "/tile.raw", tile, &e2)) {
if (profile_map) profile_tile_ms = (ticks() - tile_start) / 1000.0;
fmt::SplatSet ss;
const double alpha_start = profile_map ? ticks() : 0;
fmt::build_splat(tile, texture_set.runtime_count(), ss);
if (profile_map) profile_alpha_ms = (ticks() - alpha_start) / 1000.0;
if (!ss.layers.empty()) {
String smpath;
String sm = String(dir.c_str()) + "/shadowmap.dds";
if (mtgodot::file_exists(sm))
smpath = sm;
const double material_start = profile_map ? ticks() : 0;
Ref<ShaderMaterial> tm = build_chunk_terrain_material(
ss, texture_set, *resolver, smpath);
if (profile_map) profile_material_ms = (ticks() - material_start) / 1000.0;
if (tm.is_valid()) {
mat = tm;
splatted = true;
@@ -416,6 +429,7 @@ bool Metin2World::build_chunk(int tx, int ty) {
}
if (splatted)
++chunks_splatted;
const double profile_splat = profile_map ? ticks() : 0;
// 该区块的场景根 —— terrain / water / 对象 / 树都挂它下面,卸载 = free 它
Node3D *croot = memnew(Node3D);
@@ -483,6 +497,7 @@ bool Metin2World::build_chunk(int tx, int ty) {
}
}
++chunks_built;
const double profile_godot_mesh = profile_map ? ticks() : 0;
// 地形碰撞(W1 item 6):HeightMapShape3D,129×129,格距 = CELL_M(缩放承载)
if (collision_enabled) {
@@ -545,6 +560,18 @@ bool Metin2World::build_chunk(int tx, int ty) {
objects_placed += ck.objects;
trees_placed += ck.trees;
chunks.push_back(std::move(ck));
if (profile_map) {
const double profile_end = ticks();
UtilityFunctions::print(vformat("MAP_PROFILE %d,%d height=%.3f attr=%.3f terrain_cpu=%.3f splat=%.3f tile=%.3f alpha=%.3f material=%.3f godot_mesh=%.3f rest=%.3f total=%.3f",
tx, ty, (profile_height - profile_start) / 1000.0,
(profile_attr - profile_height) / 1000.0,
(profile_terrain - profile_attr) / 1000.0,
(profile_splat - profile_terrain) / 1000.0,
profile_tile_ms, profile_alpha_ms, profile_material_ms,
(profile_godot_mesh - profile_splat) / 1000.0,
(profile_end - profile_godot_mesh) / 1000.0,
(profile_end - profile_start) / 1000.0));
}
return true;
}
@@ -13,15 +13,21 @@
#include <gr2/gr2.h>
#include "granny.h"
#include "../EterLib/RenderCommands3D.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace
{
using Mat4 = gr2::Mat4;
@@ -815,16 +821,179 @@ int read_influences(const Layout& l, int m, const uint8_t* v, float* out, bool w
}
return n;
}
struct StaticSkinMesh
{
std::uint64_t source_mesh_key = 0;
std::uint32_t vertex_count = 0;
std::uint32_t out_stride = 0;
std::uint32_t bone_count = 1;
std::vector<std::uint8_t> bind_vertices;
std::vector<std::uint8_t> bone_indices; // vertex_count * 4
std::vector<float> bone_weights; // vertex_count * 4
};
struct DestSkinSlice
{
const std::uint8_t* dest_begin = nullptr;
const std::uint8_t* dest_end = nullptr;
std::uint32_t stride = 0;
std::uint32_t vertex_count = 0;
const StaticSkinMesh* source = nullptr;
std::vector<float> bone_matrices; // bone_count * 16
};
std::mutex g_skin_mutex;
int g_gpu_skinning_override = -1;
std::unordered_map<const void*, std::unique_ptr<StaticSkinMesh>> g_static_skin_meshes;
std::map<const std::uint8_t*, DestSkinSlice> g_dest_skin_slices;
} // namespace
void SetGpuSkinningEnabled(bool enabled)
{
std::lock_guard<std::mutex> lock(g_skin_mutex);
g_gpu_skinning_override = enabled ? 1 : 0;
if (!enabled)
g_dest_skin_slices.clear();
}
bool IsGpuSkinningEnabled()
{
if (g_gpu_skinning_override >= 0)
return g_gpu_skinning_override != 0;
static const bool env_on = [] {
const char* v = std::getenv("MT_GPU_SKINNING");
return v && (*v == '1' || *v == 't' || *v == 'T' || *v == 'y' || *v == 'Y');
}();
return env_on;
}
bool LookupGpuSkinSubrange(const void* vertex_buffer_base, std::uint32_t stride,
std::uint32_t lo_vertex, std::uint32_t hi_vertex,
GpuSkinSubrangeView* out_view)
{
if (!IsGpuSkinningEnabled() || !vertex_buffer_base || !stride || !out_view || hi_vertex < lo_vertex)
return false;
std::lock_guard<std::mutex> lock(g_skin_mutex);
const auto* base = static_cast<const std::uint8_t*>(vertex_buffer_base);
const auto* q_begin = base + std::size_t(lo_vertex) * stride;
const auto* q_end = base + std::size_t(hi_vertex + 1) * stride;
auto it = g_dest_skin_slices.upper_bound(q_begin);
if (it == g_dest_skin_slices.begin())
return false;
--it;
const DestSkinSlice& slice = it->second;
if (!slice.source || slice.stride != stride || q_begin < slice.dest_begin || q_end > slice.dest_end || slice.dest_begin < base)
return false;
const std::size_t byte_offset = static_cast<std::size_t>(slice.dest_begin - base);
if (byte_offset % stride != 0)
return false;
out_view->source_mesh_key = slice.source->source_mesh_key;
out_view->mesh_base_vertex = static_cast<std::uint32_t>(byte_offset / stride);
out_view->mesh_vertex_count = slice.vertex_count;
out_view->bone_indices = slice.source->bone_indices.data();
out_view->bone_weights = slice.source->bone_weights.data();
out_view->bone_matrices = slice.bone_matrices.data();
out_view->bone_count = slice.source->bone_count;
return true;
}
void GrannyDeformVertices(granny_mesh_deformer const* Deformer, granny_int32x const* MatrixIndices,
granny_real32 const* MatrixBuffer4x4, granny_int32x VertexCount,
void const* SourceVertices, void* DestVertices)
{
if (!Deformer || !MatrixBuffer4x4 || !SourceVertices || !DestVertices)
if (!Deformer || !MatrixBuffer4x4 || !SourceVertices || !DestVertices || VertexCount <= 0)
return;
const granny_mesh_deformer& d = *Deformer;
const bool normals = d.type != GrannyDeformPosition && d.in_normal >= 0 && d.out_normal >= 0;
if (IsGpuSkinningEnabled())
{
std::lock_guard<std::mutex> lock(g_skin_mutex);
auto& mesh_ptr = g_static_skin_meshes[SourceVertices];
if (!mesh_ptr || mesh_ptr->vertex_count != std::uint32_t(VertexCount) || mesh_ptr->out_stride != std::uint32_t(d.out.size))
{
mesh_ptr = std::make_unique<StaticSkinMesh>();
mesh_ptr->source_mesh_key = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(SourceVertices));
mesh_ptr->vertex_count = static_cast<std::uint32_t>(VertexCount);
mesh_ptr->out_stride = static_cast<std::uint32_t>(d.out.size);
mesh_ptr->bind_vertices.resize(std::size_t(VertexCount) * d.out.size, 0);
mesh_ptr->bone_indices.resize(std::size_t(VertexCount) * 4, 0);
mesh_ptr->bone_weights.resize(std::size_t(VertexCount) * 4, 0.0f);
int max_local_bone = 0;
for (int v = 0; v < VertexCount; ++v)
{
const uint8_t* src = static_cast<const uint8_t*>(SourceVertices) + std::size_t(v) * d.in.size;
uint8_t* dst = mesh_ptr->bind_vertices.data() + std::size_t(v) * d.out.size;
run_conversion(d.tail, src, dst);
float p[3] = {0, 0, 0}, nrm[3] = {0, 0, 0};
read_vec(d.in, d.in_position, src, p, 3);
write_vec(d.out, d.out_position, dst, p, 3);
if (normals)
{
read_vec(d.in, d.in_normal, src, nrm, 3);
write_vec(d.out, d.out_normal, dst, nrm, 3);
}
float weights[4] = {1, 0, 0, 0}, indices[4] = {0, 0, 0, 0};
int wn = 1, in = 1;
if (d.in_weights >= 0)
wn = read_influences(d.in, d.in_weights, src, weights, true);
if (d.in_indices >= 0)
in = read_influences(d.in, d.in_indices, src, indices, false);
const int n = std::min(wn, in);
for (int k = 0; k < n; ++k)
{
const float w = weights[k];
if (w <= 0.0f)
continue;
const int local = std::max(0, std::min(127, int(indices[k])));
mesh_ptr->bone_indices[std::size_t(v) * 4 + k] = static_cast<std::uint8_t>(local);
mesh_ptr->bone_weights[std::size_t(v) * 4 + k] = w;
if (local > max_local_bone)
max_local_bone = local;
}
}
mesh_ptr->bone_count = static_cast<std::uint32_t>(max_local_bone + 1);
}
const StaticSkinMesh* mesh = mesh_ptr.get();
const auto* dest_begin = static_cast<const std::uint8_t*>(DestVertices);
const auto* dest_end = dest_begin + mesh->bind_vertices.size();
// Remove any stale overlapping slice from a previous model using the same buffer.
auto ov = g_dest_skin_slices.lower_bound(dest_begin);
if (ov != g_dest_skin_slices.begin())
{
auto prev = std::prev(ov);
if (prev->second.dest_end > dest_begin)
ov = prev;
}
while (ov != g_dest_skin_slices.end() && ov->second.dest_begin < dest_end)
{
if (ov->first != dest_begin)
ov = g_dest_skin_slices.erase(ov);
else
++ov;
}
DestSkinSlice& slice = g_dest_skin_slices[dest_begin];
if (slice.source != mesh || slice.vertex_count != mesh->vertex_count)
{
std::memcpy(DestVertices, mesh->bind_vertices.data(), mesh->bind_vertices.size());
slice.dest_begin = dest_begin;
slice.dest_end = dest_end;
slice.stride = mesh->out_stride;
slice.vertex_count = mesh->vertex_count;
slice.source = mesh;
}
slice.bone_matrices.resize(std::size_t(mesh->bone_count) * 16);
for (std::uint32_t b = 0; b < mesh->bone_count; ++b)
{
const int bone = MatrixIndices ? MatrixIndices[b] : int(b);
std::memcpy(&slice.bone_matrices[std::size_t(b) * 16], MatrixBuffer4x4 + std::size_t(bone) * 16, 16 * sizeof(float));
}
return;
}
for (int v = 0; v < VertexCount; ++v)
{
const uint8_t* src = static_cast<const uint8_t*>(SourceVertices) + size_t(v) * d.in.size;
@@ -863,6 +1032,7 @@ void GrannyDeformVertices(granny_mesh_deformer const* Deformer, granny_int32x co
}
}
// ── Model instances and controls ───────────────────────────────────────────────────────────────
struct granny_model_instance
+9 -2
View File
@@ -5,10 +5,15 @@
#include "EterLib/StdAfx.h"
#include <cstdint>
#include <atomic>
#include <vector>
inline std::atomic<std::uint64_t> mt_next_cpu_buffer_id{1};
struct MtCpuVertexBuffer : IDirect3DVertexBuffer8
{
const std::uint64_t id = mt_next_cpu_buffer_id.fetch_add(1, std::memory_order_relaxed);
std::uint64_t revision = 0;
std::vector<uint8_t> bytes;
DWORD fvf = 0;
@@ -19,11 +24,13 @@ struct MtCpuVertexBuffer : IDirect3DVertexBuffer8
*data = bytes.data() + offset;
return S_OK;
}
HRESULT Unlock() override { return S_OK; }
HRESULT Unlock() override { ++revision; return S_OK; }
};
struct MtCpuIndexBuffer : IDirect3DIndexBuffer8
{
const std::uint64_t id = mt_next_cpu_buffer_id.fetch_add(1, std::memory_order_relaxed);
std::uint64_t revision = 0;
std::vector<uint8_t> bytes;
D3DFORMAT format = D3DFMT_INDEX16;
@@ -34,7 +41,7 @@ struct MtCpuIndexBuffer : IDirect3DIndexBuffer8
*data = bytes.data() + offset;
return S_OK;
}
HRESULT Unlock() override { return S_OK; }
HRESULT Unlock() override { ++revision; return S_OK; }
};
// D3DXGetFVFVertexSize.
@@ -1,79 +0,0 @@
// Platform skeleton for EterLib/CullingManager.h (40250 EterLib/CullingManager.cpp), generated by platform_stub.py.
// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation.
#include "EterLib/StdAfx.h"
#include "EterLib/CullingManager.h"
#include "../PlatformStub.h"
CCullingManager::CCullingManager()
{
MT_PLATFORM_STUB();
}
CCullingManager::~CCullingManager()
{
MT_PLATFORM_STUB();
}
auto CCullingManager::RayTraceCallback(const Vector3d &, const Vector3d &, float, const Vector3d &, SpherePack *) -> void
{
MT_PLATFORM_STUB();
}
auto CCullingManager::VisibilityCallback(const Frustum &, SpherePack *, ViewState) -> void
{
MT_PLATFORM_STUB();
}
auto CCullingManager::RangeTestCallback(const Vector3d &, float, SpherePack *, ViewState) -> void
{
MT_PLATFORM_STUB();
}
auto CCullingManager::Reset() -> void
{
MT_PLATFORM_STUB();
}
auto CCullingManager::Update() -> void
{
MT_PLATFORM_STUB();
}
// 40250 CullingManager.cpp:105. RenderGame calls it after SetPerspective; it hands the camera's view and
// the projection to CStateManager for the scene.
// PORT: BuildViewFrustum and m_Factory->FrustumTest follow in 40250; the sphere-pack culling (SphereLib)
// is not ported, so every registered object stays visible.
void CCullingManager::Process()
{
//DWORD time = ELTimer_GetMSec();
//Frustum f;
UpdateViewMatrix();
UpdateProjMatrix();
}
auto CCullingManager::FindRange(const Vector3d &, float) -> void
{
MT_PLATFORM_STUB();
}
auto CCullingManager::FindRay(const Vector3d &, const Vector3d &) -> void
{
MT_PLATFORM_STUB();
}
auto CCullingManager::FindRayDistance(const Vector3d &, const Vector3d &, float) -> void
{
MT_PLATFORM_STUB();
}
auto CCullingManager::Register(CGraphicObjectInstance *) -> CullingHandle
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<CullingHandle>();
}
auto CCullingManager::Unregister(CullingHandle) -> void
{
MT_PLATFORM_STUB();
}
@@ -98,6 +98,8 @@ int CGraphicDevice::Create(HWND hWnd, int iHres, int iVres, bool Windowed, int /
D3DXMatrixIdentity(&ms_matProj);
D3DXMatrixIdentity(&ms_matInverseView);
D3DXMatrixIdentity(&ms_matInverseViewYAxis);
D3DXMatrixIdentity(&ms_matWorld);
D3DXMatrixIdentity(&ms_matWorldView);
D3DXMatrixIdentity(&ms_matScreen0);
D3DXMatrixIdentity(&ms_matScreen1);
D3DXMatrixIdentity(&ms_matScreen2);
@@ -137,16 +137,22 @@ std::string UIRenderTextureNameFromHandle(const IDirect3DBaseTexture8* handle)
{
if (!handle) return {};
const auto* texture = static_cast<const IDirect3DTexture8*>(handle);
std::lock_guard<std::mutex> lock(g_memory_mutex);
if (const MemoryTexture* memory = live_memory_texture(texture))
return "mem:" + std::to_string(memory->id) + "@" + std::to_string(memory->revision);
const auto* file = static_cast<const FileTexture*>(texture);
return g_file_textures.count(file) ? file->name : std::string();
{
std::lock_guard<std::mutex> lock(g_memory_mutex);
if (const MemoryTexture* memory = live_memory_texture(texture))
return "mem:" + std::to_string(memory->id) + "@" + std::to_string(memory->revision);
const auto* file = static_cast<const FileTexture*>(texture);
if (g_file_textures.count(file))
return file->name;
}
return MtCpuTextureNameFromHandle(handle);
}
bool UIRenderMemoryTexture(const std::string& name, UIMemoryTexture* out)
{
if (name.compare(0, 4, "mem:") != 0 || !out) return false;
if (name.compare(0, 8, "mem:cpu_") == 0)
return MtCpuMemoryTexture(name, out);
const unsigned id = unsigned(std::strtoul(name.c_str() + 4, nullptr, 10));
std::lock_guard<std::mutex> lock(g_memory_mutex);
auto it = g_memory.find(id);
@@ -34,6 +34,7 @@ bool CGraphicIndexBuffer::Lock(void** pretIndices) const
void CGraphicIndexBuffer::Unlock() const
{
assert(m_lpd3dIdxBuf!=NULL);
static_cast<MtCpuIndexBuffer*>(m_lpd3dIdxBuf)->revision++;
}
bool CGraphicIndexBuffer::Lock(void** pretIndices)
@@ -47,6 +48,7 @@ bool CGraphicIndexBuffer::Lock(void** pretIndices)
void CGraphicIndexBuffer::Unlock()
{
assert(m_lpd3dIdxBuf!=NULL);
static_cast<MtCpuIndexBuffer*>(m_lpd3dIdxBuf)->revision++;
}
bool CGraphicIndexBuffer::Copy(int bufSize, const void* srcIndices)
@@ -54,6 +56,7 @@ bool CGraphicIndexBuffer::Copy(int bufSize, const void* srcIndices)
assert(m_lpd3dIdxBuf!=NULL);
memcpy(index_bytes(m_lpd3dIdxBuf), srcIndices, bufSize);
static_cast<MtCpuIndexBuffer*>(m_lpd3dIdxBuf)->revision++;
return true;
}
@@ -73,6 +76,7 @@ bool CGraphicIndexBuffer::Create(int faceCount, TFace* faces)
dstIndices[1]=curFace->indices[1];
dstIndices[2]=curFace->indices[2];
}
static_cast<MtCpuIndexBuffer*>(m_lpd3dIdxBuf)->revision++;
return true;
}
+77 -16
View File
@@ -3,6 +3,7 @@
#include "EterLib/StdAfx.h"
#include "EterLib/GrpScreen.h"
#include "EterLib/StateManager.h"
#include "EterLib/Camera.h"
#include "../PlatformStub.h"
#include "UIRenderCommands.h"
@@ -285,27 +286,75 @@ void CScreen::SetCursorPosition(int x, int y, int hres, int vres)
ms_Ray.SetDirection(-ms_vtPickRayDir, 51200.0f);
}
auto CScreen::GetCursorPosition(float *, float *, float *) -> bool
auto CScreen::GetCursorPosition(float * px, float * py, float * pz) -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
if (!GetCursorXYPosition(px, py)) return false;
if (!GetCursorZPosition(pz)) return false;
return true;
}
auto CScreen::GetCursorXYPosition(float *, float *) -> bool
auto CScreen::GetCursorXYPosition(float * px, float * py) -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
D3DXVECTOR3 v3Eye = CCameraManager::Instance().GetCurrentCamera()->GetEye();
TPosition posVertices[4];
posVertices[0] = TPosition(v3Eye.x - 90000000.0f, v3Eye.y + 90000000.0f, 0.0f);
posVertices[1] = TPosition(v3Eye.x - 90000000.0f, v3Eye.y - 90000000.0f, 0.0f);
posVertices[2] = TPosition(v3Eye.x + 90000000.0f, v3Eye.y + 90000000.0f, 0.0f);
posVertices[3] = TPosition(v3Eye.x + 90000000.0f, v3Eye.y - 90000000.0f, 0.0f);
static const WORD sc_awFillRectIndices[6] = { 0, 2, 1, 2, 3, 1, };
float u, v, t;
for (int i = 0; i < 2; ++i)
{
if (IntersectTriangle(ms_vtPickRayOrig, ms_vtPickRayDir,
posVertices[sc_awFillRectIndices[i * 3]],
posVertices[sc_awFillRectIndices[i * 3 + 1]],
posVertices[sc_awFillRectIndices[i * 3 + 2]],
&u, &v, &t))
{
*px = u;
*py = v;
return true;
}
}
return false;
}
auto CScreen::GetCursorZPosition(float *) -> bool
auto CScreen::GetCursorZPosition(float * pz) -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
D3DXVECTOR3 v3Eye = CCameraManager::Instance().GetCurrentCamera()->GetEye();
TPosition posVertices[4];
posVertices[0] = TPosition(v3Eye.x - 90000000.0f, 0.0f, v3Eye.z + 90000000.0f);
posVertices[1] = TPosition(v3Eye.x - 90000000.0f, 0.0f, v3Eye.z - 90000000.0f);
posVertices[2] = TPosition(v3Eye.x + 90000000.0f, 0.0f, v3Eye.z + 90000000.0f);
posVertices[3] = TPosition(v3Eye.x + 90000000.0f, 0.0f, v3Eye.z - 90000000.0f);
static const WORD sc_awFillRectIndices[6] = { 0, 2, 1, 2, 3, 1, };
float u, v, t;
for (int i = 0; i < 2; ++i)
{
if (IntersectTriangle(ms_vtPickRayOrig, ms_vtPickRayDir,
posVertices[sc_awFillRectIndices[i * 3]],
posVertices[sc_awFillRectIndices[i * 3 + 1]],
posVertices[sc_awFillRectIndices[i * 3 + 2]],
&u, &v, &t))
{
*pz = t;
return true;
}
}
return false;
}
auto CScreen::GetPickingPosition(float, float *, float *, float *) -> void
auto CScreen::GetPickingPosition(float t, float * x, float * y, float * z) -> void
{
MT_PLATFORM_STUB();
*x = ms_vtPickRayOrig.x + ms_vtPickRayDir.x * t;
*y = ms_vtPickRayOrig.y + ms_vtPickRayDir.y * t;
*z = ms_vtPickRayOrig.z + ms_vtPickRayDir.z * t;
}
// 40250 GrpScreen.cpp:749-779.
@@ -313,7 +362,7 @@ void CScreen::ProjectPosition(float x, float y, float z, float * pfX, float * pf
{
D3DXVECTOR3 Input(x, y, z);
D3DXVECTOR3 Output;
D3DXVec3Project(&Output, &Input, &ms_Viewport, &ms_matProj, &ms_matView, &ms_matWorld);
D3DXVec3Project(&Output, &Input, &ms_Viewport, &ms_matProj, &ms_matView, &ms_matIdentity);
*pfX = Output.x;
*pfY = Output.y;
@@ -323,7 +372,7 @@ void CScreen::ProjectPosition(float x, float y, float z, float * pfX, float * pf
{
D3DXVECTOR3 Input(x, y, z);
D3DXVECTOR3 Output;
D3DXVec3Project(&Output, &Input, &ms_Viewport, &ms_matProj, &ms_matView, &ms_matWorld);
D3DXVec3Project(&Output, &Input, &ms_Viewport, &ms_matProj, &ms_matView, &ms_matIdentity);
*pfX = Output.x;
*pfY = Output.y;
@@ -334,7 +383,7 @@ void CScreen::UnprojectPosition(float x, float y, float z, float * pfX, float *
{
D3DXVECTOR3 Input(x, y, z);
D3DXVECTOR3 Output;
D3DXVec3Unproject(&Output, &Input, &ms_Viewport, &ms_matProj, &ms_matView, &ms_matWorld);
D3DXVec3Unproject(&Output, &Input, &ms_Viewport, &ms_matProj, &ms_matView, &ms_matIdentity);
*pfX = Output.x;
*pfY = Output.y;
@@ -355,12 +404,24 @@ auto CScreen::RestoreDevice() -> BOOL
auto CScreen::BuildViewFrustum() -> void
{
MT_PLATFORM_STUB();
CCamera* pkCamera = CCameraManager::Instance().GetCurrentCamera();
if (!pkCamera)
return;
const D3DXVECTOR3& c_rv3Eye = pkCamera->GetEye();
const D3DXVECTOR3& c_rv3View = pkCamera->GetView();
auto vv = ms_matView * ms_matProj;
ms_frustum.BuildViewFrustum2(
vv,
ms_fNearY,
ms_fFarY,
ms_fFieldOfView,
ms_fAspect,
c_rv3Eye, c_rv3View);
}
auto CScreen::Identity() -> void
{
MT_PLATFORM_STUB();
STATEMANAGER.SetTransform(D3DTS_WORLD, &ms_matIdentity);
}
decltype(CScreen::ms_diffuseColor) CScreen::ms_diffuseColor{};
@@ -49,6 +49,7 @@ bool CGraphicVertexBuffer::Unlock() const
{
if (!m_lpd3dVB)
return false;
static_cast<MtCpuVertexBuffer*>(m_lpd3dVB)->revision++;
return true;
}
@@ -83,6 +84,7 @@ bool CGraphicVertexBuffer::Unlock()
{
if (!m_lpd3dVB)
return false;
static_cast<MtCpuVertexBuffer*>(m_lpd3dVB)->revision++;
return true;
}
+20 -11
View File
@@ -35,34 +35,43 @@ CInputKeyboard::~CInputKeyboard()
auto CInputKeyboard::InitializeKeyboard(HWND) -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
ResetKeyboard();
return true;
}
auto CInputKeyboard::UpdateKeyboard() -> void
{
MT_PLATFORM_STUB();
}
auto CInputKeyboard::ResetKeyboard() -> void
{
MT_PLATFORM_STUB();
memset(ms_bPressedKey, 0, sizeof(ms_bPressedKey));
memset(ms_diks, 0, sizeof(ms_diks));
}
auto CInputKeyboard::IsPressed(int) -> bool
auto CInputKeyboard::IsPressed(int iIndex) -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
if (iIndex >= 0 && iIndex < 256)
return (ms_diks[iIndex] & 0x80) != 0;
return false;
}
auto CInputKeyboard::KeyDown(int) -> void
auto CInputKeyboard::KeyDown(int iIndex) -> void
{
MT_PLATFORM_STUB();
if (iIndex >= 0 && iIndex < 256)
{
ms_bPressedKey[iIndex] = true;
ms_diks[iIndex] = (char)0x80;
}
}
auto CInputKeyboard::KeyUp(int) -> void
auto CInputKeyboard::KeyUp(int iIndex) -> void
{
MT_PLATFORM_STUB();
if (iIndex >= 0 && iIndex < 256)
{
ms_bPressedKey[iIndex] = false;
ms_diks[iIndex] = 0;
}
}
decltype(CInputKeyboard::ms_lpKeyboard) CInputKeyboard::ms_lpKeyboard{};
@@ -5,12 +5,17 @@
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <iterator>
#include <mutex>
#include <unordered_map>
namespace {
std::mutex g_draws_mutex;
std::vector<Render3DDraw> g_draws;
int g_native_terrain_override = -1;
struct VertexLayout {
unsigned stride = 0;
@@ -69,9 +74,8 @@ int format_bytes(D3DFORMAT format)
}
// PORT: device-created resources live in CPU memory with D3D reference counting. Textures made by
// CreateTexture (CTerrain's splat alpha maps and attribute marks, CSnowEnvironment's blur targets)
// keep their mip levels as bytes; nothing samples them because terrain and offscreen passes are
// drawn by the Godot side.
// CreateTexture resources keep their mip levels as bytes. Terrain alpha maps and the character
// shadow render target are sampled by the native renderer through MtCpuMemoryTexture.
struct CpuTexture;
struct CpuSurface final : IDirect3DSurface8
@@ -89,13 +93,32 @@ struct CpuSurface final : IDirect3DSurface8
HRESULT UnlockRect() override { return S_OK; }
};
struct CpuTexture;
std::mutex g_cpu_tex_mutex;
std::unordered_map<unsigned, CpuTexture*> g_cpu_textures;
unsigned g_next_cpu_tex_id = 1;
struct CpuTexture final : IDirect3DTexture8
{
struct Level { D3DSURFACE_DESC desc; std::vector<uint8_t> bytes; };
unsigned id = 0;
std::uint32_t revision = 1;
ULONG refs = 1;
std::vector<Level> levels;
CpuTexture()
{
std::lock_guard<std::mutex> lock(g_cpu_tex_mutex);
id = g_next_cpu_tex_id++;
g_cpu_textures[id] = this;
}
~CpuTexture()
{
std::lock_guard<std::mutex> lock(g_cpu_tex_mutex);
g_cpu_textures.erase(id);
}
ULONG AddRef() override { return ++refs; }
ULONG Release() override
{
@@ -132,7 +155,14 @@ struct CpuTexture final : IDirect3DTexture8
locked->pBits = levels[level].bytes.data();
return S_OK;
}
HRESULT UnlockRect(UINT level) override { return level < levels.size() ? S_OK : E_FAIL; }
HRESULT UnlockRect(UINT level) override
{
if (level >= levels.size())
return E_FAIL;
if (level == 0)
++revision;
return S_OK;
}
};
ULONG CpuSurface::Release()
@@ -217,6 +247,22 @@ public:
m_renderTarget->AddRef();
m_depthStencil = m_depthBuffer;
m_depthStencil->AddRef();
m_viewport = { 0, 0, (DWORD)width, (DWORD)height, 0.0f, 1.0f };
m_renderStates[D3DRS_TEXTUREFACTOR] = 0xFFFFFFFF;
m_renderStates[D3DRS_ZENABLE] = TRUE;
m_renderStates[D3DRS_ZWRITEENABLE] = TRUE;
m_renderStates[D3DRS_CULLMODE] = D3DCULL_CCW;
m_renderStates[D3DRS_LIGHTING] = TRUE;
m_renderStates[D3DRS_SRCBLEND] = D3DBLEND_ONE;
m_renderStates[D3DRS_DESTBLEND] = D3DBLEND_ZERO;
m_renderStates[D3DRS_ALPHAFUNC] = D3DCMP_ALWAYS;
m_stageStates[0][D3DTSS_COLOROP] = D3DTOP_MODULATE;
m_stageStates[0][D3DTSS_COLORARG1] = D3DTA_TEXTURE;
m_stageStates[0][D3DTSS_COLORARG2] = D3DTA_CURRENT;
m_stageStates[0][D3DTSS_ALPHAOP] = D3DTOP_SELECTARG1;
m_stageStates[0][D3DTSS_ALPHAARG1] = D3DTA_TEXTURE;
m_stageStates[1][D3DTSS_COLOROP] = D3DTOP_DISABLE;
m_stageStates[1][D3DTSS_ALPHAOP] = D3DTOP_DISABLE;
}
~RecordingDevice() override
{
@@ -254,7 +300,14 @@ public:
}
HRESULT GetViewport(D3DVIEWPORT8* viewport) override
{
*viewport = { 0, 0, DWORD(m_width), DWORD(m_height), 0.0f, 1.0f };
if (viewport)
*viewport = m_viewport;
return S_OK;
}
HRESULT SetViewport(const D3DVIEWPORT8* viewport) override
{
if (viewport)
m_viewport = *viewport;
return S_OK;
}
// PORT: CPU memory has no texture budget; report the 64MB of a mid-range 2004 card.
@@ -340,8 +393,8 @@ public:
*out = m_depthStencil;
return S_OK;
}
// PORT: an offscreen target (CSnowEnvironment's blur pass) swallows its draws; only the back
// buffer's draws are recorded for the Godot renderer.
// Character shadow targets are rasterized below; other offscreen targets still need a
// separate effect-specific implementation (snow blur is disabled by default).
HRESULT SetRenderTarget(IDirect3DSurface8* target, IDirect3DSurface8* depth) override
{
if (target)
@@ -358,7 +411,28 @@ public:
}
return S_OK;
}
HRESULT Clear(DWORD, const D3DRECT*, DWORD, D3DCOLOR, float, DWORD) override { return S_OK; }
HRESULT Clear(DWORD, const D3DRECT*, DWORD flags, D3DCOLOR color, float depth, DWORD) override
{
if (m_renderTarget == m_backBuffer)
return S_OK;
auto* surface = static_cast<CpuSurface*>(m_renderTarget);
if (!surface->parent || surface->level != 0 || surface->desc.Format != D3DFMT_R5G6B5)
return S_OK;
const size_t pixels = size_t(surface->desc.Width) * surface->desc.Height;
if (flags & D3DCLEAR_TARGET)
{
const std::uint16_t rgb565 = std::uint16_t((((color >> 16) & 255u) >> 3) << 11 |
(((color >> 8) & 255u) >> 2) << 5 | ((color & 255u) >> 3));
auto& bytes = surface->parent->levels[0].bytes;
bytes.resize(pixels * 2);
for (size_t i = 0; i < pixels; ++i)
std::memcpy(bytes.data() + i * 2, &rgb565, 2);
++surface->parent->revision;
}
if (flags & D3DCLEAR_ZBUFFER)
m_offscreenDepth.assign(pixels, depth);
return S_OK;
}
HRESULT SetRenderState(D3DRENDERSTATETYPE state, DWORD value) override
{
if (unsigned(state) < kRenderStates)
@@ -406,7 +480,8 @@ public:
std::vector<std::uint32_t> indices(count);
for (UINT i = 0; i < count; ++i)
indices[i] = startVertex + i;
record(type, primitiveCount, m_stream->bytes.data(), m_stream->bytes.size(), m_streamStride, indices);
record(type, primitiveCount, m_stream->bytes.data(), m_stream->bytes.size(), m_streamStride, indices,
m_stream, nullptr, startVertex);
return S_OK;
}
HRESULT DrawIndexedPrimitive(D3DPRIMITIVETYPE type, UINT, UINT, UINT startIndex, UINT primitiveCount) override
@@ -417,7 +492,8 @@ public:
if (!read_indices(m_indices->bytes.data(), m_indices->bytes.size(), m_indices->format, startIndex,
index_count(type, primitiveCount), m_baseVertex, &indices))
return E_FAIL;
record(type, primitiveCount, m_stream->bytes.data(), m_stream->bytes.size(), m_streamStride, indices);
record(type, primitiveCount, m_stream->bytes.data(), m_stream->bytes.size(), m_streamStride, indices,
m_stream, m_indices, startIndex);
return S_OK;
}
HRESULT DrawPrimitiveUP(D3DPRIMITIVETYPE type, UINT primitiveCount, const void* vertices, UINT stride) override
@@ -488,6 +564,92 @@ private:
transform_point(&a[r * 4], b, &out[r * 4]);
}
// The 40250 shadow pass renders a flat grey silhouette into an R5G6B5 target.
// Rasterizing that small target here keeps the D3D render-target sequence and lets
// the normal memory-texture path upload the result for terrain/object projection.
void rasterize_shadow(const Render3DDraw& draw)
{
auto* surface = static_cast<CpuSurface*>(m_renderTarget);
if (!surface->parent || surface->level != 0 || surface->desc.Format != D3DFMT_R5G6B5 || draw.lines)
return;
const int width = int(surface->desc.Width), height = int(surface->desc.Height);
if (width <= 0 || height <= 0 || draw.indices.size() < 3)
return;
auto& bytes = surface->parent->levels[0].bytes;
if (bytes.size() < size_t(width) * height * 2)
return;
if (m_offscreenDepth.size() != size_t(width) * height)
m_offscreenDepth.assign(size_t(width) * height, 1.0f);
float worldView[16], mvp[16];
multiply(draw.world, draw.view, worldView);
multiply(worldView, draw.proj, mvp);
struct Point { float x, y, z; bool valid; };
std::vector<Point> points(draw.positions.size() / 3);
for (size_t i = 0; i < points.size(); ++i)
{
float p[4] = { draw.positions[i * 3], draw.positions[i * 3 + 1], draw.positions[i * 3 + 2], 1.0f };
if (!draw.bone_matrices.empty() && draw.bone_weights.size() >= (i + 1) * 4 &&
draw.bone_indices.size() >= (i + 1) * 4)
{
float skinned[4] = {};
float total = 0.0f;
for (int b = 0; b < 4; ++b)
{
const float weight = draw.bone_weights[i * 4 + b];
const size_t bone = draw.bone_indices[i * 4 + b];
if (weight <= 0.0f || (bone + 1) * 16 > draw.bone_matrices.size()) continue;
float transformed[4];
transform_point(p, draw.bone_matrices.data() + bone * 16, transformed);
for (int c = 0; c < 4; ++c) skinned[c] += transformed[c] * weight;
total += weight;
}
if (total > 0.0f) { p[0] = skinned[0]; p[1] = skinned[1]; p[2] = skinned[2]; }
}
float clip[4];
transform_point(p, mvp, clip);
const bool valid = clip[3] > 0.00001f;
const float invW = valid ? 1.0f / clip[3] : 0.0f;
points[i] = { draw.viewport[0] + (clip[0] * invW * 0.5f + 0.5f) * draw.viewport[2],
draw.viewport[1] + (0.5f - clip[1] * invW * 0.5f) * draw.viewport[3],
clip[2] * invW, valid };
}
const std::uint32_t color = m_renderStates[D3DRS_TEXTUREFACTOR];
const std::uint16_t rgb565 = std::uint16_t((((color >> 16) & 255u) >> 3) << 11 |
(((color >> 8) & 255u) >> 2) << 5 | ((color & 255u) >> 3));
auto edge = [](const Point& a, const Point& b, float x, float y) {
return (x - a.x) * (b.y - a.y) - (y - a.y) * (b.x - a.x);
};
for (size_t i = 0; i + 2 < draw.indices.size(); i += 3)
{
if (draw.indices[i] >= points.size() || draw.indices[i + 1] >= points.size() ||
draw.indices[i + 2] >= points.size()) continue;
const Point& a = points[draw.indices[i]];
const Point& b = points[draw.indices[i + 1]];
const Point& c = points[draw.indices[i + 2]];
if (!a.valid || !b.valid || !c.valid) continue;
const float area = edge(a, b, c.x, c.y);
if (std::fabs(area) < 0.00001f) continue;
const int x0 = std::max(0, int(std::floor(std::min({a.x, b.x, c.x}))));
const int y0 = std::max(0, int(std::floor(std::min({a.y, b.y, c.y}))));
const int x1 = std::min(width - 1, int(std::ceil(std::max({a.x, b.x, c.x}))));
const int y1 = std::min(height - 1, int(std::ceil(std::max({a.y, b.y, c.y}))));
for (int y = y0; y <= y1; ++y)
for (int x = x0; x <= x1; ++x)
{
const float px = float(x) + 0.5f, py = float(y) + 0.5f;
const float wa = edge(b, c, px, py) / area;
const float wb = edge(c, a, px, py) / area;
const float wc = 1.0f - wa - wb;
if (wa < 0.0f || wb < 0.0f || wc < 0.0f) continue;
const float z = wa * a.z + wb * b.z + wc * c.z;
const size_t pixel = size_t(y) * width + x;
if (z < 0.0f || z > 1.0f || z > m_offscreenDepth[pixel]) continue;
m_offscreenDepth[pixel] = z;
std::memcpy(bytes.data() + pixel * 2, &rgb565, 2);
}
}
}
// Each two-triangle quad of a UI-space draw becomes an image command: its screen corners through
// world * view * projection, its stage-0 texture (or D3DTA_TFACTOR colour when stage 0 selects it)
// and, when stage 1 generates coordinates from the camera-space position through D3DTS_TEXTURE1
@@ -585,9 +747,10 @@ private:
}
void record(D3DPRIMITIVETYPE type, UINT primitives, const uint8_t* vertices, size_t vertexBytes, UINT stride,
const std::vector<std::uint32_t>& indices)
const std::vector<std::uint32_t>& indices, const MtCpuVertexBuffer* source_vertex = nullptr,
const MtCpuIndexBuffer* source_index = nullptr, UINT source_first_index = 0)
{
if (type == D3DPT_POINTLIST || indices.empty() || !vertices || m_renderTarget != m_backBuffer)
if (type == D3DPT_POINTLIST || indices.empty() || !vertices)
return;
VertexLayout layout = fvf_layout(m_fvf);
if (!stride)
@@ -602,7 +765,7 @@ private:
// An orthographic projection over untransformed vertices is a UI-space draw (CPythonMiniMap's
// terrain tiles under CPythonGraphic::SetOrtho2D): it joins the UI command stream in order.
if (!layout.rhw && m_transforms[D3DTS_PROJECTION][11] == 0.0f)
if (m_renderTarget == m_backBuffer && !layout.rhw && m_transforms[D3DTS_PROJECTION][11] == 0.0f)
{
record_ui_quads(type, vertices, vertexBytes, stride, layout, indices);
return;
@@ -614,6 +777,10 @@ private:
std::memcpy(draw.proj, m_transforms[D3DTS_PROJECTION], sizeof(draw.proj));
draw.texture0 = UIRenderTextureNameFromHandle(m_textures[0]);
draw.texture1 = UIRenderTextureNameFromHandle(m_textures[1]);
draw.viewport[0] = float(m_viewport.X);
draw.viewport[1] = float(m_viewport.Y);
draw.viewport[2] = float(m_viewport.Width);
draw.viewport[3] = float(m_viewport.Height);
draw.pretransformed = layout.rhw;
draw.lines = type == D3DPT_LINELIST || type == D3DPT_LINESTRIP;
@@ -627,47 +794,221 @@ private:
if ((size_t(hi) + 1) * stride > vertexBytes)
return;
const size_t count = size_t(hi - lo) + 1;
draw.positions.resize(count * 3);
if (layout.rhw) draw.rhw.resize(count);
if (layout.normal >= 0) draw.normals.resize(count * 3);
if (layout.uv0 >= 0) draw.uv0.resize(count * 2);
if (layout.uv1 >= 0) draw.uv1.resize(count * 2);
if (layout.diffuse >= 0) draw.diffuse.resize(count);
for (size_t i = 0; i < count; ++i)
const bool texgen_cam_pos0 = layout.uv0 < 0 && m_textures[0] != nullptr &&
(m_stageStates[0][D3DTSS_TEXCOORDINDEX] & 0xFFFF0000u) == D3DTSS_TCI_CAMERASPACEPOSITION &&
m_stageStates[0][D3DTSS_TEXTURETRANSFORMFLAGS] == D3DTTFF_COUNT2;
const bool texgen_cam_pos1 = layout.uv1 < 0 && m_textures[1] != nullptr &&
(m_stageStates[1][D3DTSS_TEXCOORDINDEX] & 0xFFFF0000u) == D3DTSS_TCI_CAMERASPACEPOSITION &&
m_stageStates[1][D3DTSS_TEXTURETRANSFORMFLAGS] == D3DTTFF_COUNT2;
const bool tex_xform0 = layout.uv0 >= 0 && m_textures[0] != nullptr &&
(m_stageStates[0][D3DTSS_TEXCOORDINDEX] & 0xFFFF0000u) == 0 &&
m_stageStates[0][D3DTSS_TEXTURETRANSFORMFLAGS] == D3DTTFF_COUNT2;
GpuSkinSubrangeView skin_view;
const bool gpu_skinned = source_vertex != nullptr &&
LookupGpuSkinSubrange(vertices, stride, lo, hi, &skin_view);
if (source_vertex)
{
const uint8_t* v = vertices + (lo + i) * stride;
std::memcpy(&draw.positions[i * 3], v, 12);
if (layout.rhw) std::memcpy(&draw.rhw[i], v + 12, 4);
if (layout.normal >= 0) std::memcpy(&draw.normals[i * 3], v + layout.normal, 12);
if (layout.uv0 >= 0) std::memcpy(&draw.uv0[i * 2], v + layout.uv0, 8);
if (layout.uv1 >= 0) std::memcpy(&draw.uv1[i * 2], v + layout.uv1, 8);
if (layout.diffuse >= 0) std::memcpy(&draw.diffuse[i], v + layout.diffuse, 4);
// The original buffers outlive individual draw calls. Keep their identity separate from
// Unlock revisions so a static surface can keep its Godot mesh across frames.
auto mix = [](std::uint64_t hash, std::uint64_t value) {
return (hash ^ value) * 1099511628211ull;
};
std::uint64_t key = 14695981039346656037ull;
if (gpu_skinned)
{
key = mix(key, skin_view.source_mesh_key);
key = mix(key, source_index ? source_index->id : 0);
key = mix(key, source_first_index);
key = mix(key, lo - skin_view.mesh_base_vertex);
key = mix(key, hi - skin_view.mesh_base_vertex);
key = mix(key, primitives);
key = mix(key, type);
key = mix(key, stride);
key = mix(key, m_fvf);
draw.geometry_key = (key & 0x7FFFFFFFFFFFFFFFull) | 1ull;
draw.geometry_revision = 1ull;
}
else
{
key = mix(key, source_vertex->id);
key = mix(key, source_index ? source_index->id : 0);
key = mix(key, source_first_index);
key = mix(key, lo);
key = mix(key, hi);
key = mix(key, primitives);
key = mix(key, type);
key = mix(key, stride);
key = mix(key, m_fvf);
if (texgen_cam_pos0)
key = mix(key, static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(m_textures[0])));
if (texgen_cam_pos1)
key = mix(key, static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(m_textures[1])));
draw.geometry_key = (key & 0x7FFFFFFFFFFFFFFFull) | 1ull;
std::uint64_t revision = mix(14695981039346656037ull, source_vertex->revision);
revision = mix(revision, source_index ? source_index->revision : 0);
if (tex_xform0)
{
const float* m = m_transforms[D3DTS_TEXTURE0];
for (int mi : { 0, 1, 4, 5, 8, 9, 12, 13 })
{
std::uint32_t bits = 0;
std::memcpy(&bits, &m[mi], sizeof(bits));
revision = mix(revision, bits);
}
}
// Camera-space texture coordinates are baked into the captured geometry.
// The terrain splat and character-shadow matrices can change while the
// vertex/index buffers remain unchanged, so they are part of its revision.
for (int stage = 0; stage < 2; ++stage)
{
if ((stage == 0 && !texgen_cam_pos0) || (stage == 1 && !texgen_cam_pos1))
continue;
float worldView[16], worldTexture[16];
multiply(m_transforms[256], m_transforms[D3DTS_VIEW], worldView);
multiply(worldView, m_transforms[stage == 0 ? D3DTS_TEXTURE0 : D3DTS_TEXTURE1], worldTexture);
for (int mi : { 0, 1, 4, 5, 8, 9, 12, 13 })
{
std::uint32_t bits = 0;
std::memcpy(&bits, &worldTexture[mi], sizeof(bits));
revision = mix(revision, bits);
}
}
draw.geometry_revision = revision & 0x7FFFFFFFFFFFFFFFull;
}
}
const std::uint64_t frame_id = UIRenderFrameId();
auto cached = draw.geometry_key ? m_geometry_cache.find(draw.geometry_key) : m_geometry_cache.end();
const bool reuse = cached != m_geometry_cache.end() && !cached->second.changing &&
cached->second.revision == draw.geometry_revision;
if (reuse)
{
cached->second.last_used = frame_id;
const Render3DDraw& prior = cached->second.geometry;
draw.positions = prior.positions;
draw.rhw = prior.rhw;
draw.normals = prior.normals;
draw.uv0 = prior.uv0;
draw.uv1 = prior.uv1;
draw.diffuse = prior.diffuse;
draw.indices = prior.indices;
draw.bone_indices = prior.bone_indices;
draw.bone_weights = prior.bone_weights;
}
else
{
draw.positions.resize(count * 3);
if (layout.rhw) draw.rhw.resize(count);
if (layout.normal >= 0) draw.normals.resize(count * 3);
if (layout.uv0 >= 0) draw.uv0.resize(count * 2);
if (layout.uv1 >= 0) draw.uv1.resize(count * 2);
if (layout.diffuse >= 0) draw.diffuse.resize(count);
for (size_t i = 0; i < count; ++i)
{
const uint8_t* v = vertices + (lo + i) * stride;
std::memcpy(&draw.positions[i * 3], v, 12);
if (layout.rhw) std::memcpy(&draw.rhw[i], v + 12, 4);
if (layout.normal >= 0) std::memcpy(&draw.normals[i * 3], v + layout.normal, 12);
if (layout.uv0 >= 0) std::memcpy(&draw.uv0[i * 2], v + layout.uv0, 8);
if (layout.uv1 >= 0) std::memcpy(&draw.uv1[i * 2], v + layout.uv1, 8);
if (layout.diffuse >= 0) std::memcpy(&draw.diffuse[i], v + layout.diffuse, 4);
}
if (texgen_cam_pos0)
{
float worldView[16], worldTex0[16];
multiply(m_transforms[256], m_transforms[D3DTS_VIEW], worldView);
multiply(worldView, m_transforms[D3DTS_TEXTURE0], worldTex0);
draw.uv0.resize(count * 2);
const bool clamp0_u = m_stageStates[0][D3DTSS_ADDRESSU] == D3DTADDRESS_CLAMP;
const bool clamp0_v = m_stageStates[0][D3DTSS_ADDRESSV] == D3DTADDRESS_CLAMP;
for (size_t i = 0; i < count; ++i)
{
const float pos[4] = { draw.positions[i * 3 + 0], draw.positions[i * 3 + 1], draw.positions[i * 3 + 2], 1.0f };
float tc[4];
transform_point(pos, worldTex0, tc);
draw.uv0[i * 2 + 0] = clamp0_u ? std::clamp(tc[0], 0.5f / 256.0f, 255.5f / 256.0f) : tc[0];
draw.uv0[i * 2 + 1] = clamp0_v ? std::clamp(tc[1], 0.5f / 256.0f, 255.5f / 256.0f) : tc[1];
}
}
else if (tex_xform0)
{
const float* m = m_transforms[D3DTS_TEXTURE0];
for (size_t i = 0; i < count; ++i)
{
const float u = draw.uv0[i * 2 + 0];
const float v = draw.uv0[i * 2 + 1];
draw.uv0[i * 2 + 0] = u * m[0] + v * m[4] + m[8] + m[12];
draw.uv0[i * 2 + 1] = u * m[1] + v * m[5] + m[9] + m[13];
}
}
if (texgen_cam_pos1)
{
float worldView[16], worldTex1[16];
multiply(m_transforms[256], m_transforms[D3DTS_VIEW], worldView);
multiply(worldView, m_transforms[D3DTS_TEXTURE1], worldTex1);
draw.uv1.resize(count * 2);
for (size_t i = 0; i < count; ++i)
{
const float pos[4] = { draw.positions[i * 3 + 0], draw.positions[i * 3 + 1], draw.positions[i * 3 + 2], 1.0f };
float tc[4];
transform_point(pos, worldTex1, tc);
draw.uv1[i * 2 + 0] = tc[0];
draw.uv1[i * 2 + 1] = tc[1];
}
}
if (gpu_skinned)
{
const std::size_t rel_lo = std::size_t(lo - skin_view.mesh_base_vertex);
draw.bone_indices.resize(count * 4);
draw.bone_weights.resize(count * 4);
std::memcpy(draw.bone_indices.data(), skin_view.bone_indices + rel_lo * 4, count * 4 * sizeof(std::uint8_t));
std::memcpy(draw.bone_weights.data(), skin_view.bone_weights + rel_lo * 4, count * 4 * sizeof(float));
}
// Strips and fans become lists; D3D flips the winding of every odd strip triangle.
switch (type)
{
case D3DPT_TRIANGLESTRIP:
for (UINT i = 0; i < primitives; ++i)
{
const std::uint32_t a = indices[i] - lo, b = indices[i + 1] - lo, c = indices[i + 2] - lo;
if (i & 1) draw.indices.insert(draw.indices.end(), { b, a, c });
else draw.indices.insert(draw.indices.end(), { a, b, c });
}
break;
case D3DPT_TRIANGLEFAN:
for (UINT i = 0; i < primitives; ++i)
draw.indices.insert(draw.indices.end(), { indices[0] - lo, indices[i + 1] - lo, indices[i + 2] - lo });
break;
case D3DPT_LINESTRIP:
for (UINT i = 0; i < primitives; ++i)
draw.indices.insert(draw.indices.end(), { indices[i] - lo, indices[i + 1] - lo });
break;
default:
for (std::uint32_t index : indices)
draw.indices.push_back(index - lo);
break;
}
if (cached != m_geometry_cache.end())
{
cached->second.last_used = frame_id;
if (!cached->second.changing && cached->second.revision != draw.geometry_revision)
{
cached->second.changing = true;
cached->second.geometry = Render3DDraw();
}
}
else if (draw.geometry_key && cached == m_geometry_cache.end())
m_geometry_cache.emplace(draw.geometry_key, GeometryCacheEntry{draw.geometry_revision, frame_id, false, draw});
}
if (gpu_skinned && skin_view.bone_matrices && skin_view.bone_count > 0)
draw.bone_matrices.assign(skin_view.bone_matrices, skin_view.bone_matrices + std::size_t(skin_view.bone_count) * 16);
if (frame_id >= m_cache_pruned_frame + 120)
{
m_cache_pruned_frame = frame_id;
for (auto it = m_geometry_cache.begin(); it != m_geometry_cache.end();)
it = frame_id - it->second.last_used > 120 ? m_geometry_cache.erase(it) : std::next(it);
}
// Strips and fans become lists; D3D flips the winding of every odd strip triangle.
switch (type)
{
case D3DPT_TRIANGLESTRIP:
for (UINT i = 0; i < primitives; ++i)
{
const std::uint32_t a = indices[i] - lo, b = indices[i + 1] - lo, c = indices[i + 2] - lo;
if (i & 1) draw.indices.insert(draw.indices.end(), { b, a, c });
else draw.indices.insert(draw.indices.end(), { a, b, c });
}
break;
case D3DPT_TRIANGLEFAN:
for (UINT i = 0; i < primitives; ++i)
draw.indices.insert(draw.indices.end(), { indices[0] - lo, indices[i + 1] - lo, indices[i + 2] - lo });
break;
case D3DPT_LINESTRIP:
for (UINT i = 0; i < primitives; ++i)
draw.indices.insert(draw.indices.end(), { indices[i] - lo, indices[i + 1] - lo });
break;
default:
for (std::uint32_t index : indices)
draw.indices.push_back(index - lo);
break;
}
draw.alpha_blend = m_renderStates[D3DRS_ALPHABLENDENABLE];
draw.src_blend = m_renderStates[D3DRS_SRCBLEND];
@@ -682,7 +1023,17 @@ private:
draw.lighting = m_renderStates[D3DRS_LIGHTING];
draw.texture_factor = m_renderStates[D3DRS_TEXTUREFACTOR];
draw.fog_enable = m_renderStates[D3DRS_FOGENABLE];
draw.fog_color = m_renderStates[D3DRS_FOGCOLOR];
draw.fog_vertex_mode = m_renderStates[D3DRS_FOGVERTEXMODE];
draw.fog_table_mode = m_renderStates[D3DRS_FOGTABLEMODE];
draw.fog_range_enable = m_renderStates[D3DRS_RANGEFOGENABLE];
std::memcpy(&draw.fog_start, &m_renderStates[D3DRS_FOGSTART], sizeof(float));
std::memcpy(&draw.fog_end, &m_renderStates[D3DRS_FOGEND], sizeof(float));
std::memcpy(&draw.fog_density, &m_renderStates[D3DRS_FOGDENSITY], sizeof(float));
draw.ambient = m_renderStates[D3DRS_AMBIENT];
draw.diffuse_material_source = m_renderStates[D3DRS_DIFFUSEMATERIALSOURCE];
draw.ambient_material_source = m_renderStates[D3DRS_AMBIENTMATERIALSOURCE];
draw.color_vertex = m_renderStates[D3DRS_COLORVERTEX];
for (int stage = 0; stage < 2; ++stage)
{
draw.color_op[stage] = m_stageStates[stage][D3DTSS_COLOROP];
@@ -691,11 +1042,16 @@ private:
draw.alpha_op[stage] = m_stageStates[stage][D3DTSS_ALPHAOP];
draw.alpha_arg1[stage] = m_stageStates[stage][D3DTSS_ALPHAARG1];
draw.alpha_arg2[stage] = m_stageStates[stage][D3DTSS_ALPHAARG2];
draw.address_u[stage] = m_stageStates[stage][D3DTSS_ADDRESSU];
draw.address_v[stage] = m_stageStates[stage][D3DTSS_ADDRESSV];
draw.min_filter[stage] = m_stageStates[stage][D3DTSS_MINFILTER];
draw.mag_filter[stage] = m_stageStates[stage][D3DTSS_MAGFILTER];
draw.mip_filter[stage] = m_stageStates[stage][D3DTSS_MIPFILTER];
}
copy_color(draw.material_diffuse, m_material.Diffuse);
copy_color(draw.material_ambient, m_material.Ambient);
copy_color(draw.material_emissive, m_material.Emissive);
if (m_lightEnabled[0] && m_lights[0].Type == D3DLIGHT_DIRECTIONAL)
if (m_lightEnabled[0] && (m_lights[0].Type == D3DLIGHT_DIRECTIONAL || m_lights[0].Type == D3DLIGHT_SPOT))
{
draw.light0 = true;
draw.light0_direction[0] = m_lights[0].Direction.x;
@@ -704,11 +1060,37 @@ private:
copy_color(draw.light0_diffuse, m_lights[0].Diffuse);
copy_color(draw.light0_ambient, m_lights[0].Ambient);
}
Render3DAdd(std::move(draw));
for (int i = 0; i < 2; ++i)
{
if (!m_lightEnabled[i]) continue;
const D3DLIGHT8& source = m_lights[i];
auto& light = draw.lights[i];
light.type = source.Type;
light.position[0] = source.Position.x;
light.position[1] = source.Position.y;
light.position[2] = source.Position.z;
light.direction[0] = source.Direction.x;
light.direction[1] = source.Direction.y;
light.direction[2] = source.Direction.z;
copy_color(light.diffuse, source.Diffuse);
copy_color(light.ambient, source.Ambient);
light.attenuation[0] = source.Attenuation0;
light.attenuation[1] = source.Attenuation1;
light.attenuation[2] = source.Attenuation2;
light.range = source.Range;
light.theta = source.Theta;
light.phi = source.Phi;
light.falloff = source.Falloff;
}
if (m_renderTarget == m_backBuffer)
Render3DAdd(std::move(draw));
else
rasterize_shadow(draw);
}
ULONG m_refs = 1;
int m_width, m_height;
D3DVIEWPORT8 m_viewport = {};
float m_transforms[kTransforms][16];
DWORD m_renderStates[kRenderStates] = {};
DWORD m_stageStates[kStages][kStageStates] = {};
@@ -725,11 +1107,103 @@ private:
UINT m_streamStride = 0;
MtCpuIndexBuffer* m_indices = nullptr;
UINT m_baseVertex = 0;
struct GeometryCacheEntry {
std::uint64_t revision;
std::uint64_t last_used;
bool changing;
Render3DDraw geometry;
};
std::unordered_map<std::uint64_t, GeometryCacheEntry> m_geometry_cache;
std::uint64_t m_cache_pruned_frame = 0;
std::vector<float> m_offscreenDepth;
};
}
std::string MtCpuTextureNameFromHandle(const IDirect3DBaseTexture8* handle)
{
if (!handle) return {};
std::lock_guard<std::mutex> lock(g_cpu_tex_mutex);
for (const auto& entry : g_cpu_textures)
{
if (entry.second == handle && !entry.second->levels.empty())
return "mem:cpu_" + std::to_string(entry.first) + "@" + std::to_string(entry.second->revision);
}
return {};
}
bool MtCpuMemoryTexture(const std::string& name, UIMemoryTexture* out)
{
if (name.compare(0, 8, "mem:cpu_") != 0 || !out) return false;
const unsigned id = unsigned(std::strtoul(name.c_str() + 8, nullptr, 10));
std::lock_guard<std::mutex> lock(g_cpu_tex_mutex);
auto it = g_cpu_textures.find(id);
if (it == g_cpu_textures.end() || it->second->levels.empty()) return false;
const auto& lv0 = it->second->levels[0];
const UINT w = lv0.desc.Width;
const UINT h = lv0.desc.Height;
if (!w || !h) return false;
out->width = static_cast<int>(w);
out->height = static_cast<int>(h);
out->revision = it->second->revision;
out->argb.resize(std::size_t(w) * std::size_t(h));
const uint8_t* bytes = lv0.bytes.data();
const int bpp = format_bytes(lv0.desc.Format);
for (std::size_t i = 0; i < out->argb.size(); ++i)
{
if (bpp == 4)
{
std::uint32_t px = 0;
std::memcpy(&px, bytes + i * 4, 4);
// CTerrain::PutImage32 packs alpha into bits 31..24 with RGB == 0.
// Set RGB to white so stage-1 modulation preserves the base texture's RGB.
if ((px & 0x00FFFFFFu) == 0)
px |= 0x00FFFFFFu;
out->argb[i] = px;
}
else if (lv0.desc.Format == D3DFMT_R5G6B5)
{
std::uint16_t word = 0;
std::memcpy(&word, bytes + i * 2, 2);
const std::uint32_t r = ((word >> 11) & 31u) * 255u / 31u;
const std::uint32_t g = ((word >> 5) & 63u) * 255u / 63u;
const std::uint32_t b = (word & 31u) * 255u / 31u;
out->argb[i] = 0xFF000000u | (r << 16) | (g << 8) | b;
}
else if (bpp == 2)
{
std::uint16_t word = 0;
std::memcpy(&word, bytes + i * 2, 2);
// CTerrain::PutImage16 writes `src[x] << 8`, storing the full 8-bit alpha in the high byte.
const std::uint32_t a = (word >> 8) & 0xFFu;
out->argb[i] = (a << 24) | 0x00FFFFFFu;
}
else
{
const std::uint32_t a = bytes[i];
out->argb[i] = (a << 24) | 0x00FFFFFFu;
}
}
return true;
}
IDirect3DDevice8* MtCreateRecordingDevice(int width, int height) { return new RecordingDevice(width, height); }
void SetNativeTerrainRenderEnabled(bool enabled)
{
g_native_terrain_override = enabled ? 1 : 0;
}
bool IsNativeTerrainRenderEnabled()
{
if (g_native_terrain_override >= 0)
return g_native_terrain_override != 0;
static const bool env_on = [] {
const char* v = std::getenv("MT_NATIVE_TERRAIN");
return v && (*v == '1' || *v == 't' || *v == 'T' || *v == 'y' || *v == 'Y');
}();
return env_on;
}
void Render3DBeginFrame()
{
std::lock_guard<std::mutex> lock(g_draws_mutex);
@@ -10,9 +10,14 @@
//
// Matrices are D3D8's row-vector layout (translation in elements 12..14), exactly as 40250 set them.
struct Render3DDraw {
// Stable source-buffer identity and content version. Zero key means an immediate-mode draw
// without a reusable D3D buffer. Geometry may be reused only while both values match.
std::uint64_t geometry_key = 0;
std::uint64_t geometry_revision = 0;
float world[16];
float view[16];
float proj[16];
float viewport[4] = {}; // x, y, width, height
// Stage 0/1 textures, named like UIRenderTextureName: the pack path of a file texture or
// "mem:<id>@<revision>"; empty when the stage has no texture.
@@ -30,6 +35,9 @@ struct Render3DDraw {
std::vector<float> uv1; // u, v
std::vector<std::uint32_t> diffuse; // 0xAARRGGBB
std::vector<std::uint32_t> indices; // triangle list (strips and fans are expanded), or line list
std::vector<std::uint8_t> bone_indices; // 4 uint8 per vertex (mesh-local bone palette index)
std::vector<float> bone_weights; // 4 floats per vertex
std::vector<float> bone_matrices; // 16 floats per bone in the mesh's palette (D3D row-vector layout)
bool lines = false;
// D3DRS_* / D3DTSS_* values in effect for the draw.
@@ -37,8 +45,12 @@ struct Render3DDraw {
std::uint32_t alpha_test = 0, alpha_ref = 0, alpha_func = 0;
std::uint32_t cull_mode = 0, z_enable = 0, z_write = 0, z_func = 0;
std::uint32_t lighting = 0, texture_factor = 0, fog_enable = 0;
std::uint32_t fog_color = 0, fog_vertex_mode = 0, fog_table_mode = 0, fog_range_enable = 0;
float fog_start = 0, fog_end = 0, fog_density = 0;
std::uint32_t color_op[2] = {}, color_arg1[2] = {}, color_arg2[2] = {};
std::uint32_t alpha_op[2] = {}, alpha_arg1[2] = {}, alpha_arg2[2] = {};
std::uint32_t address_u[2] = {}, address_v[2] = {};
std::uint32_t min_filter[2] = {}, mag_filter[2] = {}, mip_filter[2] = {};
// D3DMATERIAL8 diffuse/ambient/emissive (r, g, b, a) and light 0 when enabled.
float material_diffuse[4] = {1, 1, 1, 1};
float material_ambient[4] = {};
@@ -48,8 +60,40 @@ struct Render3DDraw {
float light0_diffuse[4] = {};
float light0_ambient[4] = {};
std::uint32_t ambient = 0; // D3DRS_AMBIENT
struct Light {
std::uint32_t type = 0; // zero when disabled; D3DLIGHT_POINT/SPOT/DIRECTIONAL otherwise
float position[3] = {};
float direction[3] = {};
float diffuse[4] = {};
float ambient[4] = {};
float attenuation[3] = {};
float range = 0;
float theta = 0, phi = 0, falloff = 0;
} lights[2];
std::uint32_t diffuse_material_source = 0;
std::uint32_t ambient_material_source = 0;
std::uint32_t color_vertex = 0;
};
struct GpuSkinSubrangeView {
std::uint64_t source_mesh_key = 0;
std::uint32_t mesh_base_vertex = 0;
std::uint32_t mesh_vertex_count = 0;
const std::uint8_t* bone_indices = nullptr; // mesh_vertex_count * 4
const float* bone_weights = nullptr; // mesh_vertex_count * 4
const float* bone_matrices = nullptr; // bone_count * 16
std::uint32_t bone_count = 0;
};
void SetNativeTerrainRenderEnabled(bool enabled);
bool IsNativeTerrainRenderEnabled();
void SetGpuSkinningEnabled(bool enabled);
bool IsGpuSkinningEnabled();
bool LookupGpuSkinSubrange(const void* vertex_buffer_base, std::uint32_t stride,
std::uint32_t lo_vertex, std::uint32_t hi_vertex,
GpuSkinSubrangeView* out_view);
void Render3DBeginFrame();
void Render3DAdd(Render3DDraw draw);
const std::vector<Render3DDraw>& Render3DDraws();
+666 -78
View File
@@ -1,199 +1,787 @@
// Platform skeleton for EterLib/SkyBox.h (40250 EterLib/SkyBox.cpp), generated by platform_stub.py.
// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation.
// Platform implementation of EterLib/SkyBox.cpp (40250 EterLib/SkyBox.cpp).
#include "EterLib/StdAfx.h"
#include "EterLib/SkyBox.h"
#include "../PlatformStub.h"
#include "EterLib/Camera.h"
#include "EterLib/StateManager.h"
#include "EterLib/ResourceManager.h"
#include "EterBase/Timer.h"
#include "RenderCommands3D.h"
CSkyObjectQuad::CSkyObjectQuad()
{
MT_PLATFORM_STUB();
m_Indices[0] = 0;
m_Indices[1] = 2;
m_Indices[2] = 1;
m_Indices[3] = 3;
for (unsigned char uci = 0; uci < 4; ++uci)
{
memset(&m_Vertex[uci], 0, sizeof(TPDTVertex));
}
}
CSkyObjectQuad::~CSkyObjectQuad()
{
MT_PLATFORM_STUB();
}
auto CSkyObjectQuad::Clear(const unsigned char &, const float &, const float &, const float &, const float &) -> void
void CSkyObjectQuad::Clear(const unsigned char & c_rucNumVertex,
const float & c_rfRed,
const float & c_rfGreen,
const float & c_rfBlue,
const float & c_rfAlpha)
{
MT_PLATFORM_STUB();
if (c_rucNumVertex > 3)
return;
m_Helper[c_rucNumVertex].Clear(c_rfRed, c_rfGreen, c_rfBlue, c_rfAlpha);
}
auto CSkyObjectQuad::SetSrcColor(const unsigned char &, const float &, const float &, const float &, const float &) -> void
void CSkyObjectQuad::SetSrcColor(const unsigned char & c_rucNumVertex,
const float & c_rfRed,
const float & c_rfGreen,
const float & c_rfBlue,
const float & c_rfAlpha)
{
MT_PLATFORM_STUB();
if (c_rucNumVertex > 3)
return;
m_Helper[c_rucNumVertex].SetSrcColor(c_rfRed, c_rfGreen, c_rfBlue, c_rfAlpha);
}
auto CSkyObjectQuad::SetTransition(const unsigned char &, const float &, const float &, const float &, const float &, DWORD) -> void
void CSkyObjectQuad::SetTransition(const unsigned char & c_rucNumVertex,
const float & c_rfRed,
const float & c_rfGreen,
const float & c_rfBlue,
const float & c_rfAlpha,
DWORD dwDuration)
{
MT_PLATFORM_STUB();
if (c_rucNumVertex > 3)
return;
m_Helper[c_rucNumVertex].SetTransition(c_rfRed, c_rfGreen, c_rfBlue, c_rfAlpha, dwDuration);
}
auto CSkyObjectQuad::SetVertex(const unsigned char &, const TPDTVertex &) -> void
void CSkyObjectQuad::SetVertex(const unsigned char & c_rucNumVertex, const TPDTVertex & c_rPDTVertex)
{
MT_PLATFORM_STUB();
if (c_rucNumVertex > 3)
return;
memcpy(&m_Vertex[m_Indices[c_rucNumVertex]], &c_rPDTVertex, sizeof(TPDTVertex));
}
auto CSkyObjectQuad::StartTransition() -> void
void CSkyObjectQuad::StartTransition()
{
MT_PLATFORM_STUB();
for (unsigned char uci = 0; uci < 4; ++uci)
{
m_Helper[uci].StartTransition();
}
}
auto CSkyObjectQuad::Update() -> bool
bool CSkyObjectQuad::Update()
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
bool bResult = false;
for (unsigned char uci = 0; uci < 4; ++uci)
{
bResult = m_Helper[uci].Update() || bResult;
m_Vertex[m_Indices[uci]].diffuse = m_Helper[uci].GetCurColor();
}
return bResult;
}
auto CSkyObjectQuad::Render() -> void
void CSkyObjectQuad::Render()
{
MT_PLATFORM_STUB();
if (CGraphicBase::SetPDTStream(m_Vertex, 4))
STATEMANAGER.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
}
CSkyObject::CSkyObject()
CSkyObject::CSkyObject() :
m_v3Position(0.0f, 0.0f, 0.0f),
m_fScaleX(1.0f),
m_fScaleY(1.0f),
m_fScaleZ(1.0f)
{
MT_PLATFORM_STUB();
D3DXMatrixIdentity(&m_matWorld);
D3DXMatrixIdentity(&m_matTranslation);
D3DXMatrixIdentity(&m_matWorldCloud);
D3DXMatrixIdentity(&m_matTranslationCloud);
D3DXMatrixIdentity(&m_matTextureCloud);
m_dwlastTime = CTimer::Instance().GetCurrentMillisecond();
m_fCloudPositionU = 0.0f;
m_fCloudPositionV = 0.0f;
m_fCloudScaleX = 1.0f;
m_fCloudScaleY = 1.0f;
m_fCloudHeight = 0.0f;
m_fCloudTextureScaleX = 1.0f;
m_fCloudTextureScaleY = 1.0f;
m_fCloudScrollSpeedU = 0.0f;
m_fCloudScrollSpeedV = 0.0f;
m_ucRenderMode = SKY_RENDER_MODE_DEFAULT;
m_bTransitionStarted = false;
m_bSkyMatrixUpdated = false;
}
CSkyObject::~CSkyObject()
{
MT_PLATFORM_STUB();
}
auto CSkyObject::StartTransition() -> void
void CSkyObject::Destroy()
{
MT_PLATFORM_STUB();
}
auto CSkyObject::GenerateTexture(const char *) -> CGraphicImageInstance *
void CSkyObject::Update()
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<CGraphicImageInstance *>();
CCamera* pCamera = CCameraManager::Instance().GetCurrentCamera();
if (!pCamera)
return;
D3DXVECTOR3 v3Eye = pCamera->GetEye();
if (m_v3Position == v3Eye)
if (m_bSkyMatrixUpdated == false)
return;
m_v3Position = v3Eye;
m_matWorld._41 = m_v3Position.x;
m_matWorld._42 = m_v3Position.y;
m_matWorld._43 = m_v3Position.z;
m_matWorldCloud._41 = m_v3Position.x;
m_matWorldCloud._42 = m_v3Position.y;
m_matWorldCloud._43 = m_v3Position.z + m_fCloudHeight;
if (m_bSkyMatrixUpdated)
m_bSkyMatrixUpdated = false;
}
auto CSkyObject::DeleteTexture(CGraphicImageInstance *) -> void
void CSkyObject::Render()
{
MT_PLATFORM_STUB();
}
auto CSkyObject::CSkyBox::StartTransition() -> void
void CSkyObject::StartTransition()
{
MT_PLATFORM_STUB();
}
auto CSkyObject::CSkyBox::Update() -> bool
CGraphicImageInstance * CSkyObject::GenerateTexture(const char * szfilename)
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
if (!szfilename || !*szfilename)
return NULL;
CResource * pResource = CResourceManager::Instance().GetResourcePointer(szfilename);
if (!pResource || !pResource->IsType(CGraphicImage::Type()))
return NULL;
CGraphicImageInstance * pImageInstance = CGraphicImageInstance::New();
pImageInstance->SetImagePointer(static_cast<CGraphicImage *>(pResource));
return pImageInstance;
}
auto CSkyObject::CSkyBox::Render() -> void
void CSkyObject::DeleteTexture(CGraphicImageInstance * pImageInstance)
{
MT_PLATFORM_STUB();
if (pImageInstance)
CGraphicImageInstance::Delete(pImageInstance);
}
void CSkyObject::TSkyObjectFace::StartTransition()
{
for (unsigned char uci = 0; uci < m_SkyObjectQuadVector.size(); ++uci)
{
m_SkyObjectQuadVector[uci].StartTransition();
}
}
bool CSkyObject::TSkyObjectFace::Update()
{
bool bResult = false;
for (DWORD dwi = 0; dwi < m_SkyObjectQuadVector.size(); ++dwi)
bResult = m_SkyObjectQuadVector[dwi].Update() || bResult;
return bResult;
}
void CSkyObject::TSkyObjectFace::Render()
{
for (unsigned char uci = 0; uci < m_SkyObjectQuadVector.size(); ++uci)
{
m_SkyObjectQuadVector[uci].Render();
}
}
CSkyBox::CSkyBox()
{
MT_PLATFORM_STUB();
m_ucVirticalGradientLevelUpper = 0;
m_ucVirticalGradientLevelLower = 0;
}
CSkyBox::~CSkyBox()
{
MT_PLATFORM_STUB();
Destroy();
}
auto CSkyBox::Update() -> void
void CSkyBox::Destroy()
{
MT_PLATFORM_STUB();
Unload();
}
auto CSkyBox::Render() -> void
void CSkyBox::Unload()
{
MT_PLATFORM_STUB();
TGraphicImageInstanceMap::iterator itor = m_GraphicImageInstanceMap.begin();
while (itor != m_GraphicImageInstanceMap.end())
{
DeleteTexture(itor->second);
++itor;
}
m_GraphicImageInstanceMap.clear();
}
auto CSkyBox::RenderCloud() -> void
void CSkyBox::SetSkyBoxScale(const D3DXVECTOR3 & c_rv3Scale)
{
MT_PLATFORM_STUB();
m_fScaleX = c_rv3Scale.x;
m_fScaleY = c_rv3Scale.y;
m_fScaleZ = c_rv3Scale.z;
m_bSkyMatrixUpdated = true;
D3DXMatrixScaling(&m_matWorld, m_fScaleX, m_fScaleY, m_fScaleZ);
}
auto CSkyBox::Destroy() -> void
void CSkyBox::SetGradientLevel(BYTE byUpper, BYTE byLower)
{
MT_PLATFORM_STUB();
m_ucVirticalGradientLevelUpper = byUpper;
m_ucVirticalGradientLevelLower = byLower;
}
auto CSkyBox::Unload() -> void
void CSkyBox::SetFaceTexture(const char* c_szFileName, int iFaceIndex)
{
MT_PLATFORM_STUB();
if (iFaceIndex < 0 || iFaceIndex > 5 || !c_szFileName || !*c_szFileName)
return;
TGraphicImageInstanceMap::iterator itor = m_GraphicImageInstanceMap.find(c_szFileName);
if (m_GraphicImageInstanceMap.end() != itor)
return;
m_Faces[iFaceIndex].m_strFaceTextureFileName = c_szFileName;
CGraphicImageInstance * pGraphicImageInstance = GenerateTexture(c_szFileName);
m_GraphicImageInstanceMap.insert(TGraphicImageInstanceMap::value_type(c_szFileName, pGraphicImageInstance));
}
auto CSkyBox::SetSkyBoxScale(const D3DXVECTOR3 &) -> void
void CSkyBox::SetCloudTexture(const char * c_szFileName)
{
MT_PLATFORM_STUB();
if (!c_szFileName || !*c_szFileName)
return;
TGraphicImageInstanceMap::iterator itor = m_GraphicImageInstanceMap.find(c_szFileName);
if (m_GraphicImageInstanceMap.end() != itor)
return;
m_FaceCloud.m_strfacename = c_szFileName;
CGraphicImageInstance * pGraphicImageInstance = GenerateTexture(c_szFileName);
m_GraphicImageInstanceMap.insert(TGraphicImageInstanceMap::value_type(m_FaceCloud.m_strfacename, pGraphicImageInstance));
}
auto CSkyBox::SetGradientLevel(BYTE, BYTE) -> void
void CSkyBox::SetCloudScale(const D3DXVECTOR2 & c_rv2CloudScale)
{
MT_PLATFORM_STUB();
m_fCloudScaleX = c_rv2CloudScale.x;
m_fCloudScaleY = c_rv2CloudScale.y;
D3DXMatrixScaling(&m_matWorldCloud, m_fCloudScaleX, m_fCloudScaleY, 1.0f);
}
auto CSkyBox::SetFaceTexture(const char *, int) -> void
void CSkyBox::SetCloudHeight(float fHeight)
{
MT_PLATFORM_STUB();
m_fCloudHeight = fHeight;
}
auto CSkyBox::SetCloudTexture(const char *) -> void
void CSkyBox::SetCloudTextureScale(const D3DXVECTOR2 & c_rv2CloudTextureScale)
{
MT_PLATFORM_STUB();
m_fCloudTextureScaleX = c_rv2CloudTextureScale.x;
m_fCloudTextureScaleY = c_rv2CloudTextureScale.y;
m_matTextureCloud._11 = m_fCloudTextureScaleX;
m_matTextureCloud._22 = m_fCloudTextureScaleY;
}
auto CSkyBox::SetCloudScale(const D3DXVECTOR2 &) -> void
void CSkyBox::SetCloudScrollSpeed(const D3DXVECTOR2 & c_rv2CloudScrollSpeed)
{
MT_PLATFORM_STUB();
m_fCloudScrollSpeedU = c_rv2CloudScrollSpeed.x;
m_fCloudScrollSpeedV = c_rv2CloudScrollSpeed.y;
}
auto CSkyBox::SetCloudHeight(float) -> void
void CSkyBox::SetSkyObjectQuadVertical(TSkyObjectQuadVector * pSkyObjectQuadVector, const D3DXVECTOR2 * c_pv2QuadPoints)
{
MT_PLATFORM_STUB();
TPDTVertex aPDTVertex;
DWORD dwIndex = 0;
pSkyObjectQuadVector->clear();
pSkyObjectQuadVector->resize(m_ucVirticalGradientLevelUpper + m_ucVirticalGradientLevelLower);
unsigned char ucY;
for (ucY = 0; ucY < m_ucVirticalGradientLevelUpper; ++ucY)
{
CSkyObjectQuad & rSkyObjectQuad = pSkyObjectQuadVector->at(dwIndex++);
aPDTVertex.position.x = c_pv2QuadPoints[0].x;
aPDTVertex.position.y = c_pv2QuadPoints[0].y;
aPDTVertex.position.z = 1.0f - (float)(ucY + 1) / (float)(m_ucVirticalGradientLevelUpper);
aPDTVertex.texCoord.x = 0.0f;
aPDTVertex.texCoord.y = (float)(ucY + 1) / (float)(m_ucVirticalGradientLevelUpper) * 0.5f;
rSkyObjectQuad.SetVertex(0, aPDTVertex);
aPDTVertex.position.x = c_pv2QuadPoints[0].x;
aPDTVertex.position.y = c_pv2QuadPoints[0].y;
aPDTVertex.position.z = 1.0f - (float)(ucY) / (float)(m_ucVirticalGradientLevelUpper);
aPDTVertex.texCoord.x = 0.0f;
aPDTVertex.texCoord.y = (float)(ucY) / (float)(m_ucVirticalGradientLevelUpper) * 0.5f;
rSkyObjectQuad.SetVertex(1, aPDTVertex);
aPDTVertex.position.x = c_pv2QuadPoints[1].x;
aPDTVertex.position.y = c_pv2QuadPoints[1].y;
aPDTVertex.position.z = 1.0f - (float)(ucY + 1) / (float)(m_ucVirticalGradientLevelUpper);
aPDTVertex.texCoord.x = 1.0f;
aPDTVertex.texCoord.y = (float)(ucY + 1) / (float)(m_ucVirticalGradientLevelUpper) * 0.5f;
rSkyObjectQuad.SetVertex(2, aPDTVertex);
aPDTVertex.position.x = c_pv2QuadPoints[1].x;
aPDTVertex.position.y = c_pv2QuadPoints[1].y;
aPDTVertex.position.z = 1.0f - (float)(ucY) / (float)(m_ucVirticalGradientLevelUpper);
aPDTVertex.texCoord.x = 1.0f;
aPDTVertex.texCoord.y = (float)(ucY) / (float)(m_ucVirticalGradientLevelUpper) * 0.5f;
rSkyObjectQuad.SetVertex(3, aPDTVertex);
}
for (ucY = 0; ucY < m_ucVirticalGradientLevelLower; ++ucY)
{
CSkyObjectQuad & rSkyObjectQuad = pSkyObjectQuadVector->at(dwIndex++);
aPDTVertex.position.x = c_pv2QuadPoints[0].x;
aPDTVertex.position.y = c_pv2QuadPoints[0].y;
aPDTVertex.position.z = -(float)(ucY + 1) / (float)(m_ucVirticalGradientLevelLower);
aPDTVertex.texCoord.x = 0.0f;
aPDTVertex.texCoord.y = 0.5f + (float)(ucY + 1) / (float)(m_ucVirticalGradientLevelUpper) * 0.5f;
rSkyObjectQuad.SetVertex(0, aPDTVertex);
aPDTVertex.position.x = c_pv2QuadPoints[0].x;
aPDTVertex.position.y = c_pv2QuadPoints[0].y;
aPDTVertex.position.z = -(float)(ucY) / (float)(m_ucVirticalGradientLevelLower);
aPDTVertex.texCoord.x = 0.0f;
aPDTVertex.texCoord.y = 0.5f + (float)(ucY) / (float)(m_ucVirticalGradientLevelUpper) * 0.5f;
rSkyObjectQuad.SetVertex(1, aPDTVertex);
aPDTVertex.position.x = c_pv2QuadPoints[1].x;
aPDTVertex.position.y = c_pv2QuadPoints[1].y;
aPDTVertex.position.z = -(float)(ucY + 1) / (float)(m_ucVirticalGradientLevelLower);
aPDTVertex.texCoord.x = 1.0f;
aPDTVertex.texCoord.y = 0.5f + (float)(ucY + 1) / (float)(m_ucVirticalGradientLevelUpper) * 0.5f;
rSkyObjectQuad.SetVertex(2, aPDTVertex);
aPDTVertex.position.x = c_pv2QuadPoints[1].x;
aPDTVertex.position.y = c_pv2QuadPoints[1].y;
aPDTVertex.position.z = -(float)(ucY) / (float)(m_ucVirticalGradientLevelLower);
aPDTVertex.texCoord.x = 1.0f;
aPDTVertex.texCoord.y = 0.5f + (float)(ucY) / (float)(m_ucVirticalGradientLevelUpper) * 0.5f;
rSkyObjectQuad.SetVertex(3, aPDTVertex);
}
}
auto CSkyBox::SetCloudTextureScale(const D3DXVECTOR2 &) -> void
void CSkyBox::SetSkyObjectQuadHorizon(TSkyObjectQuadVector * pSkyObjectQuadVector, const D3DXVECTOR3 * c_pv3QuadPoints)
{
MT_PLATFORM_STUB();
pSkyObjectQuadVector->clear();
pSkyObjectQuadVector->resize(1);
CSkyObjectQuad & rSkyObjectQuad = pSkyObjectQuadVector->at(0);
TPDTVertex aPDTVertex{};
aPDTVertex.position = c_pv3QuadPoints[0];
aPDTVertex.texCoord.x = 0.0f;
aPDTVertex.texCoord.y = 1.0f;
rSkyObjectQuad.SetVertex(0, aPDTVertex);
aPDTVertex.position = c_pv3QuadPoints[1];
aPDTVertex.texCoord.x = 0.0f;
aPDTVertex.texCoord.y = 0.0f;
rSkyObjectQuad.SetVertex(1, aPDTVertex);
aPDTVertex.position = c_pv3QuadPoints[2];
aPDTVertex.texCoord.x = 1.0f;
aPDTVertex.texCoord.y = 1.0f;
rSkyObjectQuad.SetVertex(2, aPDTVertex);
aPDTVertex.position = c_pv3QuadPoints[3];
aPDTVertex.texCoord.x = 1.0f;
aPDTVertex.texCoord.y = 0.0f;
rSkyObjectQuad.SetVertex(3, aPDTVertex);
}
auto CSkyBox::SetCloudScrollSpeed(const D3DXVECTOR2 &) -> void
void CSkyBox::Refresh()
{
MT_PLATFORM_STUB();
D3DXVECTOR3 v3QuadPoints[4];
if (m_ucRenderMode == CSkyObject::SKY_RENDER_MODE_DEFAULT || m_ucRenderMode == CSkyObject::SKY_RENDER_MODE_DIFFUSE)
{
if (m_ucVirticalGradientLevelUpper + m_ucVirticalGradientLevelLower <= 0)
return;
D3DXVECTOR2 v2QuadPoints[2];
v2QuadPoints[0] = D3DXVECTOR2(1.0f, -1.0f);
v2QuadPoints[1] = D3DXVECTOR2(-1.0f, -1.0f);
SetSkyObjectQuadVertical(&m_Faces[0].m_SkyObjectQuadVector, v2QuadPoints);
m_Faces[0].m_strfacename = "front";
v2QuadPoints[0] = D3DXVECTOR2(-1.0f, 1.0f);
v2QuadPoints[1] = D3DXVECTOR2(1.0f, 1.0f);
SetSkyObjectQuadVertical(&m_Faces[1].m_SkyObjectQuadVector, v2QuadPoints);
m_Faces[1].m_strfacename = "back";
v2QuadPoints[0] = D3DXVECTOR2(-1.0f, -1.0f);
v2QuadPoints[1] = D3DXVECTOR2(-1.0f, 1.0f);
SetSkyObjectQuadVertical(&m_Faces[2].m_SkyObjectQuadVector, v2QuadPoints);
m_Faces[2].m_strfacename = "left";
v2QuadPoints[0] = D3DXVECTOR2(1.0f, 1.0f);
v2QuadPoints[1] = D3DXVECTOR2(1.0f, -1.0f);
SetSkyObjectQuadVertical(&m_Faces[3].m_SkyObjectQuadVector, v2QuadPoints);
m_Faces[3].m_strfacename = "right";
v3QuadPoints[0] = D3DXVECTOR3(1.0f, 1.0f, 1.0f);
v3QuadPoints[1] = D3DXVECTOR3(-1.0f, 1.0f, 1.0f);
v3QuadPoints[2] = D3DXVECTOR3(1.0f, -1.0f, 1.0f);
v3QuadPoints[3] = D3DXVECTOR3(-1.0f, -1.0f, 1.0f);
SetSkyObjectQuadHorizon(&m_Faces[4].m_SkyObjectQuadVector, v3QuadPoints);
m_Faces[4].m_strfacename = "top";
v3QuadPoints[0] = D3DXVECTOR3(-1.0f, 1.0f, -1.0f);
v3QuadPoints[1] = D3DXVECTOR3(1.0f, 1.0f, -1.0f);
v3QuadPoints[2] = D3DXVECTOR3(-1.0f, -1.0f, -1.0f);
v3QuadPoints[3] = D3DXVECTOR3(1.0f, -1.0f, -1.0f);
SetSkyObjectQuadHorizon(&m_Faces[5].m_SkyObjectQuadVector, v3QuadPoints);
m_Faces[5].m_strfacename = "bottom";
}
else if (m_ucRenderMode == CSkyObject::SKY_RENDER_MODE_TEXTURE)
{
v3QuadPoints[0] = D3DXVECTOR3(1.0f, -1.0f, -1.0f);
v3QuadPoints[1] = D3DXVECTOR3(1.0f, -1.0f, 1.0f);
v3QuadPoints[2] = D3DXVECTOR3(-1.0f, -1.0f, -1.0f);
v3QuadPoints[3] = D3DXVECTOR3(-1.0f, -1.0f, 1.0f);
SetSkyObjectQuadHorizon(&m_Faces[0].m_SkyObjectQuadVector, v3QuadPoints);
m_Faces[0].m_strfacename = "front";
v3QuadPoints[0] = D3DXVECTOR3(-1.0f, 1.0f, -1.0f);
v3QuadPoints[1] = D3DXVECTOR3(-1.0f, 1.0f, 1.0f);
v3QuadPoints[2] = D3DXVECTOR3(1.0f, 1.0f, -1.0f);
v3QuadPoints[3] = D3DXVECTOR3(1.0f, 1.0f, 1.0f);
SetSkyObjectQuadHorizon(&m_Faces[1].m_SkyObjectQuadVector, v3QuadPoints);
m_Faces[1].m_strfacename = "back";
v3QuadPoints[0] = D3DXVECTOR3(1.0f, 1.0f, -1.0f);
v3QuadPoints[1] = D3DXVECTOR3(1.0f, 1.0f, 1.0f);
v3QuadPoints[2] = D3DXVECTOR3(1.0f, -1.0f, -1.0f);
v3QuadPoints[3] = D3DXVECTOR3(1.0f, -1.0f, 1.0f);
SetSkyObjectQuadHorizon(&m_Faces[2].m_SkyObjectQuadVector, v3QuadPoints);
m_Faces[2].m_strfacename = "left";
v3QuadPoints[0] = D3DXVECTOR3(-1.0f, -1.0f, -1.0f);
v3QuadPoints[1] = D3DXVECTOR3(-1.0f, -1.0f, 1.0f);
v3QuadPoints[2] = D3DXVECTOR3(-1.0f, 1.0f, -1.0f);
v3QuadPoints[3] = D3DXVECTOR3(-1.0f, 1.0f, 1.0f);
SetSkyObjectQuadHorizon(&m_Faces[3].m_SkyObjectQuadVector, v3QuadPoints);
m_Faces[3].m_strfacename = "right";
v3QuadPoints[0] = D3DXVECTOR3(1.0f, -1.0f, 1.0f);
v3QuadPoints[1] = D3DXVECTOR3(1.0f, 1.0f, 1.0f);
v3QuadPoints[2] = D3DXVECTOR3(-1.0f, -1.0f, 1.0f);
v3QuadPoints[3] = D3DXVECTOR3(-1.0f, 1.0f, 1.0f);
SetSkyObjectQuadHorizon(&m_Faces[4].m_SkyObjectQuadVector, v3QuadPoints);
m_Faces[4].m_strfacename = "top";
v3QuadPoints[0] = D3DXVECTOR3(1.0f, -1.0f, -1.0f);
v3QuadPoints[1] = D3DXVECTOR3(1.0f, 1.0f, -1.0f);
v3QuadPoints[2] = D3DXVECTOR3(-1.0f, -1.0f, -1.0f);
v3QuadPoints[3] = D3DXVECTOR3(-1.0f, 1.0f, -1.0f);
SetSkyObjectQuadHorizon(&m_Faces[5].m_SkyObjectQuadVector, v3QuadPoints);
m_Faces[5].m_strfacename = "bottom";
}
v3QuadPoints[0] = D3DXVECTOR3(1.0f, 1.0f, 0.0f);
v3QuadPoints[1] = D3DXVECTOR3(-1.0f, 1.0f, 0.0f);
v3QuadPoints[2] = D3DXVECTOR3(1.0f, -1.0f, 0.0f);
v3QuadPoints[3] = D3DXVECTOR3(-1.0f, -1.0f, 0.0f);
SetSkyObjectQuadHorizon(&m_FaceCloud.m_SkyObjectQuadVector, v3QuadPoints);
}
auto CSkyBox::SetCloudColor(const TGradientColor &, const TGradientColor &, const DWORD &) -> void
void CSkyBox::SetCloudColor(const TGradientColor & c_rColor, const TGradientColor & c_rNextColor, const DWORD & dwTransitionTime)
{
MT_PLATFORM_STUB();
TSkyObjectFace & aFaceCloud = m_FaceCloud;
for (DWORD dwk = 0; dwk < aFaceCloud.m_SkyObjectQuadVector.size(); ++dwk)
{
CSkyObjectQuad & aSkyObjectQuad = aFaceCloud.m_SkyObjectQuadVector[dwk];
for (unsigned char v = 0; v < 4; ++v)
{
aSkyObjectQuad.SetSrcColor(v,
c_rColor.m_FirstColor.r,
c_rColor.m_FirstColor.g,
c_rColor.m_FirstColor.b,
c_rColor.m_FirstColor.a);
aSkyObjectQuad.SetTransition(v,
c_rNextColor.m_FirstColor.r,
c_rNextColor.m_FirstColor.g,
c_rNextColor.m_FirstColor.b,
c_rNextColor.m_FirstColor.a,
dwTransitionTime);
}
}
}
auto CSkyBox::Refresh() -> void
void CSkyBox::SetSkyColor(const TVectorGradientColor & c_rColorVector, const TVectorGradientColor & c_rNextColorVector, long lTransitionTime)
{
MT_PLATFORM_STUB();
if (c_rColorVector.empty() || c_rNextColorVector.empty())
return;
unsigned long ulVectorGradientColornum = 0;
unsigned long uck;
for (unsigned char ucj = 0; ucj < 4; ++ucj)
{
TSkyObjectFace & aFace = m_Faces[ucj];
ulVectorGradientColornum = 0;
for (uck = 0; uck < aFace.m_SkyObjectQuadVector.size(); ++uck)
{
if (ulVectorGradientColornum >= c_rColorVector.size() || ulVectorGradientColornum >= c_rNextColorVector.size())
break;
CSkyObjectQuad & aSkyObjectQuad = aFace.m_SkyObjectQuadVector[uck];
aSkyObjectQuad.SetSrcColor(0,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.r,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.g,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.b,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.a);
aSkyObjectQuad.SetTransition(0,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.r,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.g,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.b,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.a,
lTransitionTime);
aSkyObjectQuad.SetSrcColor(1,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.r,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.g,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.b,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.a);
aSkyObjectQuad.SetTransition(1,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.r,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.g,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.b,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.a,
lTransitionTime);
aSkyObjectQuad.SetSrcColor(2,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.r,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.g,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.b,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.a);
aSkyObjectQuad.SetTransition(2,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.r,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.g,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.b,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.a,
lTransitionTime);
aSkyObjectQuad.SetSrcColor(3,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.r,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.g,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.b,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.a);
aSkyObjectQuad.SetTransition(3,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.r,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.g,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.b,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.a,
lTransitionTime);
ulVectorGradientColornum++;
}
}
TSkyObjectFace & aFaceTop = m_Faces[4];
ulVectorGradientColornum = 0;
for (uck = 0; uck < aFaceTop.m_SkyObjectQuadVector.size(); ++uck)
{
CSkyObjectQuad & aSkyObjectQuad = aFaceTop.m_SkyObjectQuadVector[uck];
for (unsigned char v = 0; v < 4; ++v)
{
aSkyObjectQuad.SetSrcColor(v,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.r,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.g,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.b,
c_rColorVector[ulVectorGradientColornum].m_FirstColor.a);
aSkyObjectQuad.SetTransition(v,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.r,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.g,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.b,
c_rNextColorVector[ulVectorGradientColornum].m_FirstColor.a,
lTransitionTime);
}
}
TSkyObjectFace & aFaceBottom = m_Faces[5];
ulVectorGradientColornum = c_rColorVector.size() - 1;
for (uck = 0; uck < aFaceBottom.m_SkyObjectQuadVector.size(); ++uck)
{
CSkyObjectQuad & aSkyObjectQuad = aFaceBottom.m_SkyObjectQuadVector[uck];
for (unsigned char v = 0; v < 4; ++v)
{
aSkyObjectQuad.SetSrcColor(v,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.r,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.g,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.b,
c_rColorVector[ulVectorGradientColornum].m_SecondColor.a);
aSkyObjectQuad.SetTransition(v,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.r,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.g,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.b,
c_rNextColorVector[ulVectorGradientColornum].m_SecondColor.a,
lTransitionTime);
}
}
}
auto CSkyBox::SetSkyColor(const TVectorGradientColor &, const TVectorGradientColor &, long) -> void
void CSkyBox::StartTransition()
{
MT_PLATFORM_STUB();
m_bTransitionStarted = true;
for (unsigned char ucj = 0; ucj < 6; ++ucj)
m_Faces[ucj].StartTransition();
m_FaceCloud.StartTransition();
}
auto CSkyBox::StartTransition() -> void
void CSkyBox::Update()
{
MT_PLATFORM_STUB();
CSkyObject::Update();
if (!m_bTransitionStarted)
return;
bool bResult = false;
for (unsigned char uci = 0; uci < 6; ++uci)
bResult = m_Faces[uci].Update() || bResult;
bResult = m_FaceCloud.Update() || bResult;
m_bTransitionStarted = bResult;
}
auto CSkyBox::SetSkyObjectQuadVertical(TSkyObjectQuadVector *, const D3DXVECTOR2 *) -> void
void CSkyBox::Render()
{
MT_PLATFORM_STUB();
if (!IsNativeTerrainRenderEnabled())
return;
STATEMANAGER.SaveRenderState(D3DRS_ZENABLE, TRUE);
STATEMANAGER.SaveRenderState(D3DRS_ZWRITEENABLE, FALSE);
STATEMANAGER.SaveRenderState(D3DRS_LIGHTING, FALSE);
STATEMANAGER.SaveRenderState(D3DRS_FOGENABLE, FALSE);
STATEMANAGER.SaveRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
STATEMANAGER.SaveRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG2);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
STATEMANAGER.SetTexture(1, NULL);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
STATEMANAGER.SetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1);
STATEMANAGER.SetTransform(D3DTS_WORLD, &m_matWorld);
if (m_ucRenderMode == CSkyObject::SKY_RENDER_MODE_TEXTURE)
{
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP);
for (unsigned int i = 0; i < 6; ++i)
{
CGraphicImageInstance * pFaceImageInstance = m_GraphicImageInstanceMap[m_Faces[i].m_strFaceTextureFileName];
if (!pFaceImageInstance)
break;
STATEMANAGER.SetTexture(0, pFaceImageInstance->GetTextureReference().GetD3DTexture());
m_Faces[i].Render();
}
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_ADDRESSU);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_ADDRESSV);
}
else
{
STATEMANAGER.SetTexture(0, NULL);
for (unsigned int i = 0; i < 6; ++i)
{
m_Faces[i].Render();
}
}
STATEMANAGER.RestoreRenderState(D3DRS_CULLMODE);
STATEMANAGER.RestoreRenderState(D3DRS_LIGHTING);
STATEMANAGER.RestoreRenderState(D3DRS_ZENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_ZWRITEENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_FOGENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_ALPHABLENDENABLE);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_COLOROP);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_COLORARG1);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_COLORARG2);
}
auto CSkyBox::SetSkyObjectQuadHorizon(TSkyObjectQuadVector *, const D3DXVECTOR3 *) -> void
void CSkyBox::RenderCloud()
{
MT_PLATFORM_STUB();
if (!IsNativeTerrainRenderEnabled())
return;
CGraphicImageInstance * pCloudGraphicImageInstance = m_GraphicImageInstanceMap[m_FaceCloud.m_strfacename];
if (!pCloudGraphicImageInstance || pCloudGraphicImageInstance->IsEmpty() || !pCloudGraphicImageInstance->GetTexturePointer())
return;
STATEMANAGER.SaveRenderState(D3DRS_ZENABLE, TRUE);
STATEMANAGER.SaveRenderState(D3DRS_ZWRITEENABLE, FALSE);
STATEMANAGER.SaveRenderState(D3DRS_LIGHTING, FALSE);
STATEMANAGER.SaveRenderState(D3DRS_FOGENABLE, FALSE);
STATEMANAGER.SaveRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
STATEMANAGER.SaveRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
STATEMANAGER.SaveRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCCOLOR);
STATEMANAGER.SaveRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
m_matTextureCloud._31 = m_fCloudPositionU;
m_matTextureCloud._32 = m_fCloudPositionV;
DWORD dwCurTime = CTimer::Instance().GetCurrentMillisecond();
m_fCloudPositionU += m_fCloudScrollSpeedU * (float)(dwCurTime - m_dwlastTime) * 0.001f;
if (m_fCloudPositionU >= 1.0f)
m_fCloudPositionU = 0.0f;
m_fCloudPositionV += m_fCloudScrollSpeedV * (float)(dwCurTime - m_dwlastTime) * 0.001f;
if (m_fCloudPositionV >= 1.0f)
m_fCloudPositionV = 0.0f;
m_dwlastTime = dwCurTime;
STATEMANAGER.SaveTransform(D3DTS_TEXTURE0, &m_matTextureCloud);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATEINVALPHA_ADDCOLOR);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
D3DXMATRIX matProjCloud;
D3DXMatrixPerspectiveFovRH(&matProjCloud, D3DX_PI * 0.25f, 1.33333f, 50.0f, 999999.0f);
STATEMANAGER.SetTransform(D3DTS_WORLD, &m_matWorldCloud);
STATEMANAGER.SaveTransform(D3DTS_PROJECTION, &matProjCloud);
STATEMANAGER.SetTexture(0, pCloudGraphicImageInstance->GetTexturePointer()->GetD3DTexture());
m_FaceCloud.Render();
STATEMANAGER.RestoreTransform(D3DTS_PROJECTION);
STATEMANAGER.RestoreTransform(D3DTS_TEXTURE0);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS);
STATEMANAGER.RestoreRenderState(D3DRS_CULLMODE);
STATEMANAGER.RestoreRenderState(D3DRS_LIGHTING);
STATEMANAGER.RestoreRenderState(D3DRS_ZENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_ZWRITEENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_FOGENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_ALPHABLENDENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_SRCBLEND);
STATEMANAGER.RestoreRenderState(D3DRS_DESTBLEND);
}
@@ -1,11 +1,13 @@
#include "EterLib/StdAfx.h"
#include "UIRenderCommands.h"
#include "RenderCommands3D.h"
#include "EterLib/StateManager.h"
#include <algorithm>
namespace {
std::vector<UIRenderCommand> commands;
std::uint64_t frame_id = 0;
int canvas_width = 0;
int canvas_height = 0;
float clip_x1 = 0, clip_y1 = 0, clip_x2 = 0, clip_y2 = 0;
@@ -18,14 +20,17 @@ void UIRenderGetSize(unsigned* width, unsigned* height) {
if (height) *height = canvas_height;
}
void UIRenderBeginFrame() {
++frame_id;
commands.clear();
clip_x1 = clip_y1 = 0;
clip_x2 = canvas_width;
clip_y2 = canvas_height;
}
std::uint64_t UIRenderFrameId() { return frame_id; }
void UIRenderAdd(UIRenderCommand command) {
command.clip_x1 = clip_x1; command.clip_y1 = clip_y1;
command.clip_x2 = clip_x2; command.clip_y2 = clip_y2;
command.behind_3d = Render3DDraws().empty();
commands.push_back(command);
}
const std::vector<UIRenderCommand>& UIRenderCommands() { return commands; }
@@ -23,6 +23,7 @@ struct UIRenderCommand {
// circular minimap_image_filter): the mask's texture coordinates at the quad's four corners.
std::string mask;
float mu[4] = {}, mv[4] = {};
bool behind_3d = false;
};
// D3DXCOLOR (0..1 floats) -> the 0xAARRGGBB the commands carry.
@@ -54,6 +55,8 @@ struct IDirect3DBaseTexture8;
void UIRenderReleaseMemoryTexture(IDirect3DTexture8* texture);
// The name (as UIRenderTextureName) of the texture a D3D handle belongs to; "" for null or unknown.
std::string UIRenderTextureNameFromHandle(const IDirect3DBaseTexture8* handle);
std::string MtCpuTextureNameFromHandle(const IDirect3DBaseTexture8* handle);
bool MtCpuMemoryTexture(const std::string& name, UIMemoryTexture* out);
// A textured quad from 40250's TPDTVertex[4] (TL, TR, BL, BR) positions and texture coordinates.
// PORT: D3D8 puts pixel centres on integers, which is why 40250 subtracts 0.5 from every vertex; the
@@ -64,6 +67,7 @@ void UIRenderAddImage(const CGraphicTexture* texture, const float x[4], const fl
void UIRenderSetSize(int width, int height);
void UIRenderGetSize(unsigned* width, unsigned* height);
void UIRenderBeginFrame();
std::uint64_t UIRenderFrameId();
void UIRenderAdd(UIRenderCommand command);
const std::vector<UIRenderCommand>& UIRenderCommands();
void UIRenderSetClip(float x, float y, float width, float height);
@@ -78,11 +78,19 @@ LPDIRECT3D8 CPythonGraphic::GetD3D()
void CPythonGraphic::SetViewport(float x, float y, float width, float height)
{
if (ms_lpd3dDevice)
{
ms_lpd3dDevice->GetViewport(&m_backupViewport);
D3DVIEWPORT8 vp = { (DWORD)x, (DWORD)y, (DWORD)width, (DWORD)height, 0.0f, 1.0f };
ms_lpd3dDevice->SetViewport(&vp);
}
UIRenderSetClip(x, y, width, height);
}
void CPythonGraphic::RestoreViewport()
{
if (ms_lpd3dDevice)
ms_lpd3dDevice->SetViewport(&m_backupViewport);
UIRenderRestoreClip();
}
void CPythonGraphic::SetOmniLight()
@@ -1,7 +1,4 @@
// Platform implementation of 40250 GameLib/MapOutdoorCharacterShadow.cpp. The render-target lifecycle is
// the 40250 code as is; the shadow-map pass is not run (BeginRenderCharacterShadowToTexture reports it
// cannot render) because the terrain that samples the map is drawn by Godot, which casts its own
// character shadows (PORT-PLAN §3).
// Platform implementation of 40250 GameLib/MapOutdoorCharacterShadow.cpp.
#include "GameLib/StdAfx.h"
#include "EterLib/StateManager.h"
#include "EterLib/Camera.h"
@@ -66,14 +63,63 @@ void CMapOutdoor::ReleaseCharacterShadowTexture()
SAFE_RELEASE(m_lpCharacterShadowMapTexture);
}
// PORT: no shadow-map pass (see the file comment); CPythonBackground::RenderCharacterShadowToTexture
// then skips the shadow instances.
static DWORD dwLightEnable = FALSE;
static bool shadowPassBegun = false;
bool CMapOutdoor::BeginRenderCharacterShadowToTexture()
{
return false;
CCamera* camera = CCameraManager::Instance().GetCurrentCamera();
if (!camera)
return false;
if (recreate)
{
CreateCharacterShadowTexture();
recreate = false;
}
if (!m_lpCharacterShadowMapRenderTargetSurface || !m_lpCharacterShadowMapDepthSurface)
return false;
shadowPassBegun = true;
const D3DXVECTOR3 target = camera->GetTarget();
const D3DXVECTOR3 eye(target.x - 1.732f * 1250.0f,
target.y - 1250.0f, target.z + 2.0f * 1.732f * 1250.0f);
const D3DXVECTOR3 up(0.0f, 0.0f, 1.0f);
D3DXMATRIX lightView, lightProj;
D3DXMatrixLookAtRH(&lightView, &eye, &target, &up);
D3DXMatrixOrthoRH(&lightProj, 2550.0f, 2550.0f, 1.0f, 15000.0f);
STATEMANAGER.SaveTransform(D3DTS_VIEW, &lightView);
STATEMANAGER.SaveTransform(D3DTS_PROJECTION, &lightProj);
dwLightEnable = STATEMANAGER.GetRenderState(D3DRS_LIGHTING);
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, FALSE);
STATEMANAGER.SaveRenderState(D3DRS_TEXTUREFACTOR, 0xFF808080);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
bool success = SUCCEEDED(ms_lpd3dDevice->GetRenderTarget(&m_lpBackupRenderTargetSurface));
success = SUCCEEDED(ms_lpd3dDevice->GetDepthStencilSurface(&m_lpBackupDepthSurface)) && success;
success = SUCCEEDED(ms_lpd3dDevice->SetRenderTarget(m_lpCharacterShadowMapRenderTargetSurface,
m_lpCharacterShadowMapDepthSurface)) && success;
success = SUCCEEDED(ms_lpd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
0xFFFFFFFFu, 1.0f, 0)) && success;
success = SUCCEEDED(ms_lpd3dDevice->GetViewport(&m_BackupViewport)) && success;
success = SUCCEEDED(ms_lpd3dDevice->SetViewport(&m_ShadowMapViewport)) && success;
return success;
}
// PORT: nothing to restore when the pass did not begin.
void CMapOutdoor::EndRenderCharacterShadowToTexture()
{
if (!shadowPassBegun)
return;
shadowPassBegun = false;
ms_lpd3dDevice->SetViewport(&m_BackupViewport);
if (m_lpBackupRenderTargetSurface && m_lpBackupDepthSurface)
ms_lpd3dDevice->SetRenderTarget(m_lpBackupRenderTargetSurface, m_lpBackupDepthSurface);
SAFE_RELEASE(m_lpBackupRenderTargetSurface);
SAFE_RELEASE(m_lpBackupDepthSurface);
STATEMANAGER.RestoreTransform(D3DTS_VIEW);
STATEMANAGER.RestoreTransform(D3DTS_PROJECTION);
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, dwLightEnable);
STATEMANAGER.RestoreRenderState(D3DRS_TEXTUREFACTOR);
}
@@ -1,10 +1,315 @@
// Platform implementation of 40250 GameLib/MapOutdoorRenderHTP.cpp (hardware-transform terrain patches).
// PORT: the Godot Metin2World adapter draws the terrain from the same .raw/.tga/tile data (PORT-PLAN §3):
// the recorder has no texture-coordinate generation or texture transforms, so the patch splat passes are
// not replayed through the D3D8 device.
// PORT: the Godot Metin2World adapter draws the terrain from the same .raw/.tga/tile data (PORT-PLAN §3).
// When IsNativeTerrainRenderEnabled() is true (MT_NATIVE_TERRAIN=1 or standalone native renderer),
// terrain patches are emitted through CStateManager with D3DTSS_TCI_CAMERASPACEPOSITION texture transforms.
#include "GameLib/StdAfx.h"
#include "GameLib/MapOutdoor.h"
#include "GameLib/TerrainPatch.h"
#include "GameLib/AreaTerrain.h"
#include "EterLib/StateManager.h"
#include "../EterLib/RenderCommands3D.h"
#include <algorithm>
void CMapOutdoor::__RenderTerrain_RenderHardwareTransformPatch()
{
if (!IsNativeTerrainRenderEnabled())
return;
const DWORD fogColor = mc_pEnvironmentData ? DWORD(mc_pEnvironmentData->FogColor) : 0xffffffff;
const float fogNear = mc_pEnvironmentData ? mc_pEnvironmentData->GetFogNearDistance() : 5000.0f;
const float fogFar = mc_pEnvironmentData ? mc_pEnvironmentData->GetFogFarDistance() : 10000.0f;
m_matWorldForCommonUse._41 = 0.0f;
m_matWorldForCommonUse._42 = 0.0f;
STATEMANAGER.SetTransform(D3DTS_WORLD, &m_matWorldForCommonUse);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_CAMERASPACEPOSITION);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_COLORARG2, D3DTA_CURRENT);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_ADDRESSU, D3DTADDRESS_WRAP);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_ADDRESSV, D3DTADDRESS_WRAP);
STATEMANAGER.SaveTextureStageState(1, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_CAMERASPACEPOSITION);
STATEMANAGER.SaveTextureStageState(1, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
STATEMANAGER.SaveTextureStageState(1, D3DTSS_COLORARG1, D3DTA_CURRENT);
STATEMANAGER.SaveTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
STATEMANAGER.SaveTextureStageState(1, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
STATEMANAGER.SaveTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
STATEMANAGER.SaveTextureStageState(1, D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP);
STATEMANAGER.SaveTextureStageState(1, D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP);
STATEMANAGER.SetTexture(1, NULL);
STATEMANAGER.SaveRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
STATEMANAGER.SaveRenderState(D3DRS_ALPHATESTENABLE, TRUE);
STATEMANAGER.SaveRenderState(D3DRS_ALPHAREF, 0);
STATEMANAGER.SaveRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATER);
STATEMANAGER.SaveRenderState(D3DRS_TEXTUREFACTOR, fogColor);
STATEMANAGER.SaveRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
STATEMANAGER.SaveRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
STATEMANAGER.SaveRenderState(D3DRS_ZWRITEENABLE, TRUE);
STATEMANAGER.SetVertexShader(D3DFVF_XYZ | D3DFVF_NORMAL);
m_iRenderedSplatNumSqSum = 0;
m_iRenderedPatchNum = 0;
m_iRenderedSplatNum = 0;
m_RenderedTextureNumVector.clear();
WORD wPrimitiveCount = 0;
D3DPRIMITIVETYPE ePrimitiveType = D3DPT_TRIANGLESTRIP;
SelectIndexBuffer(0, &wPrimitiveCount, &ePrimitiveType);
const auto nearIt = std::upper_bound(m_PatchVector.begin(), m_PatchVector.end(),
std::pair<float, long>(fogNear - 3200.0f, 0));
const auto farIt = std::upper_bound(m_PatchVector.begin(), m_PatchVector.end(),
std::pair<float, long>(fogFar + 1600.0f, 0));
const float lod1 = __GetNoFogDistance();
const float lod2 = __GetFogDistance();
BYTE lod = 0;
auto updateLod = [&](float distance) {
if (lod == 0 && lod1 <= distance) {
lod = 1;
SelectIndexBuffer(1, &wPrimitiveCount, &ePrimitiveType);
} else if (lod == 1 && lod2 <= distance) {
lod = 2;
SelectIndexBuffer(2, &wPrimitiveCount, &ePrimitiveType);
}
};
const DWORD fogEnabled = STATEMANAGER.GetRenderState(D3DRS_FOGENABLE);
STATEMANAGER.SetRenderState(D3DRS_FOGENABLE, FALSE);
for (auto it = m_PatchVector.begin(); it != nearIt; ++it) {
updateLod(it->first);
__HardwareTransformPatch_RenderPatchSplat(it->second, wPrimitiveCount, ePrimitiveType);
if (m_iRenderedSplatNum >= m_iSplatLimit) break;
if (m_bDrawWireFrame) DrawWireFrame(it->second, wPrimitiveCount, ePrimitiveType);
}
STATEMANAGER.SetRenderState(D3DRS_FOGENABLE, fogEnabled);
if (m_iRenderedSplatNum < m_iSplatLimit) {
for (auto it = nearIt; it != farIt; ++it) {
updateLod(it->first);
__HardwareTransformPatch_RenderPatchSplat(it->second, wPrimitiveCount, ePrimitiveType);
if (m_iRenderedSplatNum >= m_iSplatLimit) break;
if (m_bDrawWireFrame) DrawWireFrame(it->second, wPrimitiveCount, ePrimitiveType);
}
}
STATEMANAGER.SetRenderState(D3DRS_FOGENABLE, FALSE);
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, FALSE);
STATEMANAGER.SetTexture(0, NULL);
STATEMANAGER.SetTexture(1, NULL);
STATEMANAGER.SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, FALSE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_TEXTURETRANSFORMFLAGS, FALSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
if (m_iRenderedSplatNum < m_iSplatLimit) {
for (auto it = farIt; it != m_PatchVector.end(); ++it) {
updateLod(it->first);
__HardwareTransformPatch_RenderPatchNone(it->second, wPrimitiveCount, ePrimitiveType);
if (m_iRenderedSplatNum >= m_iSplatLimit) break;
if (m_bDrawWireFrame) DrawWireFrame(it->second, wPrimitiveCount, ePrimitiveType);
}
}
STATEMANAGER.SetRenderState(D3DRS_FOGENABLE, fogEnabled);
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, TRUE);
std::sort(m_RenderedTextureNumVector.begin(), m_RenderedTextureNumVector.end());
m_matWorldForCommonUse._41 = 0.0f;
m_matWorldForCommonUse._42 = 0.0f;
STATEMANAGER.SetTexture(1, NULL);
STATEMANAGER.RestoreRenderState(D3DRS_ZWRITEENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_DESTBLEND);
STATEMANAGER.RestoreRenderState(D3DRS_SRCBLEND);
STATEMANAGER.RestoreRenderState(D3DRS_ALPHATESTENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_ALPHAREF);
STATEMANAGER.RestoreRenderState(D3DRS_ALPHAFUNC);
STATEMANAGER.RestoreRenderState(D3DRS_TEXTUREFACTOR);
STATEMANAGER.RestoreRenderState(D3DRS_ALPHABLENDENABLE);
STATEMANAGER.RestoreTextureStageState(1, D3DTSS_ADDRESSV);
STATEMANAGER.RestoreTextureStageState(1, D3DTSS_ADDRESSU);
STATEMANAGER.RestoreTextureStageState(1, D3DTSS_ALPHAOP);
STATEMANAGER.RestoreTextureStageState(1, D3DTSS_ALPHAARG1);
STATEMANAGER.RestoreTextureStageState(1, D3DTSS_COLOROP);
STATEMANAGER.RestoreTextureStageState(1, D3DTSS_COLORARG1);
STATEMANAGER.RestoreTextureStageState(1, D3DTSS_TEXTURETRANSFORMFLAGS);
STATEMANAGER.RestoreTextureStageState(1, D3DTSS_TEXCOORDINDEX);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_ADDRESSV);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_ADDRESSU);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_ALPHAOP);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_ALPHAARG1);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_COLOROP);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_COLORARG2);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_COLORARG1);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_TEXCOORDINDEX);
}
void CMapOutdoor::__HardwareTransformPatch_RenderPatchNone(long patchnum, WORD wPrimitiveCount, D3DPRIMITIVETYPE ePrimitiveType)
{
assert(NULL != m_pTerrainPatchProxyList);
CTerrainPatchProxy* patch = &m_pTerrainPatchProxyList[patchnum];
if (!patch->isUsed()) return;
CGraphicVertexBuffer* vb = patch->HardwareTransformPatch_GetVertexBufferPtr();
if (!vb) return;
STATEMANAGER.SetStreamSource(0, vb->GetD3DVertexBuffer(), m_iPatchTerrainVertexSize);
STATEMANAGER.DrawIndexedPrimitive(ePrimitiveType, 0, m_iPatchTerrainVertexCount, 0, wPrimitiveCount);
}
void CMapOutdoor::__HardwareTransformPatch_RenderPatchSplat(long patchnum, WORD wPrimitiveCount, D3DPRIMITIVETYPE ePrimitiveType)
{
assert(NULL != m_pTerrainPatchProxyList && "__HardwareTransformPatch_RenderPatchSplat");
CTerrainPatchProxy * pTerrainPatchProxy = &m_pTerrainPatchProxyList[patchnum];
if (!pTerrainPatchProxy->isUsed())
return;
long sPatchNum = pTerrainPatchProxy->GetPatchNum();
if (sPatchNum < 0)
return;
BYTE ucTerrainNum = pTerrainPatchProxy->GetTerrainNum();
if (0xFF == ucTerrainNum)
return;
CTerrain * pTerrain;
if (!GetTerrainPointer(ucTerrainNum, &pTerrain))
return;
CGraphicVertexBuffer* pkVB = pTerrainPatchProxy->HardwareTransformPatch_GetVertexBufferPtr();
if (!pkVB)
return;
WORD wCoordX, wCoordY;
pTerrain->GetCoordinate(&wCoordX, &wCoordY);
m_matWorldForCommonUse._41 = -(float)(wCoordX * CTerrainImpl::XSIZE * CTerrainImpl::CELLSCALE);
m_matWorldForCommonUse._42 = (float)(wCoordY * CTerrainImpl::YSIZE * CTerrainImpl::CELLSCALE);
D3DXMATRIX matTerrainTexTransform, matSplatAlphaTexTransform;
D3DXMatrixMultiply(&matTerrainTexTransform, &m_matViewInverse, &m_matWorldForCommonUse);
D3DXMatrixMultiply(&matSplatAlphaTexTransform, &matTerrainTexTransform, &m_matSplatAlpha);
STATEMANAGER.SetTransform(D3DTS_TEXTURE1, &matSplatAlphaTexTransform);
STATEMANAGER.SetStreamSource(0, pkVB->GetD3DVertexBuffer(), m_iPatchTerrainVertexSize);
// 40250 renders the terrain splats unlit; only its shadow pass enables lighting.
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, FALSE);
TTerrainSplatPatch & rTerrainSplatPatch = pTerrain->GetTerrainSplatPatch();
const DWORD texCount = m_TextureSet.GetTextureCount();
bool isFirst = true;
int renderedSplatCount = 0;
for (DWORD j = 1; j < texCount; ++j)
{
TTerainSplat & rSplat = rTerrainSplatPatch.Splats[j];
if (!rSplat.Active || rTerrainSplatPatch.PatchTileCount[sPatchNum][j] == 0)
continue;
const TTerrainTexture & rTexture = m_TextureSet.GetTexture(j);
if (!rTexture.pd3dTexture)
continue;
D3DXMATRIX matTexTransform;
D3DXMatrixMultiply(&matTexTransform, &m_matViewInverse, &rTexture.m_matTransform);
STATEMANAGER.SetTransform(D3DTS_TEXTURE0, &matTexTransform);
STATEMANAGER.SetTexture(0, rTexture.pd3dTexture);
if (isFirst || !rSplat.pd3dTexture)
{
STATEMANAGER.SetTexture(1, NULL);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
STATEMANAGER.SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
STATEMANAGER.SetRenderState(D3DRS_ZWRITEENABLE, TRUE);
isFirst = false;
}
else
{
STATEMANAGER.SetTexture(1, rSplat.pd3dTexture);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
STATEMANAGER.SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
STATEMANAGER.SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
}
STATEMANAGER.DrawIndexedPrimitive(ePrimitiveType, 0, m_iPatchTerrainVertexCount, 0, wPrimitiveCount);
++renderedSplatCount;
++m_iRenderedSplatNum;
if (std::find(m_RenderedTextureNumVector.begin(), m_RenderedTextureNumVector.end(), int(j)) == m_RenderedTextureNumVector.end())
m_RenderedTextureNumVector.push_back(int(j));
if (m_iRenderedSplatNum >= m_iSplatLimit)
break;
}
if (renderedSplatCount == 0 && texCount > 1)
{
const TTerrainTexture & rTexture = m_TextureSet.GetTexture(1);
if (rTexture.pd3dTexture)
{
D3DXMATRIX matTexTransform;
D3DXMatrixMultiply(&matTexTransform, &m_matViewInverse, &rTexture.m_matTransform);
STATEMANAGER.SetTransform(D3DTS_TEXTURE0, &matTexTransform);
STATEMANAGER.SetTexture(0, rTexture.pd3dTexture);
STATEMANAGER.SetTexture(1, NULL);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
STATEMANAGER.SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
STATEMANAGER.SetRenderState(D3DRS_ZWRITEENABLE, TRUE);
STATEMANAGER.DrawIndexedPrimitive(ePrimitiveType, 0, m_iPatchTerrainVertexCount, 0, wPrimitiveCount);
renderedSplatCount = 1;
++m_iRenderedSplatNum;
}
}
if (m_bDrawShadow && pTerrain->GetShadowTexture())
{
const DWORD previousFogColor = STATEMANAGER.GetRenderState(D3DRS_FOGCOLOR);
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, TRUE);
STATEMANAGER.SetRenderState(D3DRS_FOGCOLOR, 0xFFFFFFFF);
D3DXMATRIX matShadowTexTransform;
D3DXMatrixMultiply(&matShadowTexTransform, &matTerrainTexTransform, &m_matStaticShadow);
STATEMANAGER.SetTransform(D3DTS_TEXTURE0, &matShadowTexTransform);
STATEMANAGER.SetTexture(0, pTerrain->GetShadowTexture());
STATEMANAGER.SetTextureStageState(0, D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
STATEMANAGER.SetTexture(1, NULL);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
if (m_bDrawChrShadow && m_lpCharacterShadowMapTexture)
{
STATEMANAGER.SetTransform(D3DTS_TEXTURE1, &m_matDynamicShadow);
STATEMANAGER.SetTexture(1, m_lpCharacterShadowMapTexture);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_MODULATE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP);
}
STATEMANAGER.SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
STATEMANAGER.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);
STATEMANAGER.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR);
STATEMANAGER.SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
STATEMANAGER.DrawIndexedPrimitive(ePrimitiveType, 0, m_iPatchTerrainVertexCount, 0, wPrimitiveCount);
STATEMANAGER.SetTexture(1, NULL);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP);
STATEMANAGER.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
STATEMANAGER.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
STATEMANAGER.SetRenderState(D3DRS_FOGCOLOR, previousFogColor);
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, FALSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ADDRESSU, D3DTADDRESS_WRAP);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ADDRESSV, D3DTADDRESS_WRAP);
++renderedSplatCount;
++m_iRenderedSplatNum;
}
++m_iRenderedPatchNum;
m_iRenderedSplatNumSqSum += renderedSplatCount * renderedSplatCount;
}
@@ -3,6 +3,8 @@
#include "GameLib/StdAfx.h"
#include "EterLib/StateManager.h"
#include "EterLib/ResourceManager.h"
#include "EterBase/Timer.h"
#include "../EterLib/RenderCommands3D.h"
#include "GameLib/MapOutdoor.h"
#include "GameLib/TerrainPatch.h"
@@ -24,7 +26,133 @@ void CMapOutdoor::UnloadWaterTexture()
m_WaterInstances[i].Destroy();
}
// PORT: the water patches are drawn by the Godot side.
void CMapOutdoor::RenderWater()
{
if (!IsNativeTerrainRenderEnabled())
return;
if (m_PatchVector.empty())
return;
if (!IsVisiblePart(PART_WATER))
return;
D3DXMATRIX matTexTransformWater;
STATEMANAGER.SaveRenderState(D3DRS_ZWRITEENABLE, FALSE);
STATEMANAGER.SaveRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
STATEMANAGER.SaveRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
STATEMANAGER.SaveRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
STATEMANAGER.SaveRenderState(D3DRS_COLORVERTEX, TRUE);
STATEMANAGER.SaveRenderState(D3DRS_LIGHTING, FALSE);
CGraphicImageInstance& rkWaterInst = m_WaterInstances[((ELTimer_GetMSec() / 70) % 30)];
if (!rkWaterInst.IsEmpty() && rkWaterInst.GetTexturePointer())
STATEMANAGER.SetTexture(0, rkWaterInst.GetTexturePointer()->GetD3DTexture());
else
STATEMANAGER.SetTexture(0, NULL);
D3DXMatrixScaling(&matTexTransformWater, m_fWaterTexCoordBase, -m_fWaterTexCoordBase, 0.0f);
D3DXMatrixMultiply(&matTexTransformWater, &m_matViewInverse, &matTexTransformWater);
STATEMANAGER.SaveTransform(D3DTS_TEXTURE0, &matTexTransformWater);
STATEMANAGER.SaveVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_CAMERASPACEPOSITION);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_MINFILTER, D3DTEXF_LINEAR);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_MIPFILTER, D3DTEXF_LINEAR);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_ADDRESSU, D3DTADDRESS_WRAP);
STATEMANAGER.SaveTextureStageState(0, D3DTSS_ADDRESSV, D3DTADDRESS_WRAP);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTexture(1, NULL);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
static float s_fWaterHeightCurrent = 0;
static float s_fWaterHeightBegin = 0;
static float s_fWaterHeightEnd = 0;
static DWORD s_dwLastHeightChangeTime = CTimer::Instance().GetCurrentMillisecond();
static DWORD s_dwBlendtime = 300;
if ((CTimer::Instance().GetCurrentMillisecond() - s_dwLastHeightChangeTime) > s_dwBlendtime)
{
s_dwBlendtime = random_range(1000, 3000);
if (s_fWaterHeightEnd == 0)
s_fWaterHeightEnd = -static_cast<float>(random_range(0, 15));
else
s_fWaterHeightEnd = 0;
s_fWaterHeightBegin = s_fWaterHeightCurrent;
s_dwLastHeightChangeTime = CTimer::Instance().GetCurrentMillisecond();
}
s_fWaterHeightCurrent = s_fWaterHeightBegin + (s_fWaterHeightEnd - s_fWaterHeightBegin) * (float)((CTimer::Instance().GetCurrentMillisecond() - s_dwLastHeightChangeTime) / (float)s_dwBlendtime);
m_matWorldForCommonUse._43 = s_fWaterHeightCurrent;
m_matWorldForCommonUse._41 = 0.0f;
m_matWorldForCommonUse._42 = 0.0f;
STATEMANAGER.SetTransform(D3DTS_WORLD, &m_matWorldForCommonUse);
for (auto i = m_PatchVector.begin(); i != m_PatchVector.end(); ++i)
{
DrawWater(i->second);
}
m_matWorldForCommonUse._43 = 0.0f;
STATEMANAGER.RestoreVertexShader();
STATEMANAGER.RestoreTransform(D3DTS_TEXTURE0);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_MINFILTER);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_MAGFILTER);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_MIPFILTER);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_ADDRESSU);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_ADDRESSV);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_TEXCOORDINDEX);
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS);
STATEMANAGER.RestoreRenderState(D3DRS_LIGHTING);
STATEMANAGER.RestoreRenderState(D3DRS_DIFFUSEMATERIALSOURCE);
STATEMANAGER.RestoreRenderState(D3DRS_COLORVERTEX);
STATEMANAGER.RestoreRenderState(D3DRS_ZWRITEENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_ALPHABLENDENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_CULLMODE);
}
void CMapOutdoor::DrawWater(long patchnum)
{
assert(NULL != m_pTerrainPatchProxyList);
if (!m_pTerrainPatchProxyList)
return;
CTerrainPatchProxy& rkTerrainPatchProxy = m_pTerrainPatchProxyList[patchnum];
if (!rkTerrainPatchProxy.isUsed())
return;
if (!rkTerrainPatchProxy.isWaterExists())
return;
CGraphicVertexBuffer* pkVB = rkTerrainPatchProxy.GetWaterVertexBufferPointer();
if (!pkVB)
return;
if (!pkVB->GetD3DVertexBuffer())
return;
UINT uPriCount = rkTerrainPatchProxy.GetWaterFaceCount();
if (!uPriCount)
return;
STATEMANAGER.SetStreamSource(0, pkVB->GetD3DVertexBuffer(), sizeof(SWaterVertex));
STATEMANAGER.DrawPrimitive(D3DPT_TRIANGLELIST, 0, uPriCount);
ms_faceCount += uPriCount;
}
+190 -5
View File
@@ -8,6 +8,7 @@
#include "EterBase/Timer.h"
#include "EterLib/Util.h"
#include "EterLib/GrpBase.h"
#include "EterLib/Camera.h"
#include "../EterLib/HostCursor.h"
#include "UserInterface/PythonNetworkStream.h"
#include "UserInterface/AccountConnector.h"
@@ -86,7 +87,11 @@ std::unique_ptr<CEffectManager> g_effect_manager;
class GameApplicationAdapter final : public IAbstractApplication
{
public:
void GetMousePosition(POINT* point) override { if (point) *point = {}; }
void GetMousePosition(POINT* point) override
{
if (point)
CPythonApplication::Instance().GetMousePosition(point);
}
float GetGlobalTime() override { return CTimer::Instance().GetCurrentSecond(); }
float GetGlobalElapsedTime() override { return CTimer::Instance().GetElapsedSecond(); }
void SkipRenderBuffering(DWORD) override {}
@@ -144,6 +149,7 @@ struct GameSingletons
CPythonSystem pySystem; // last CPythonApplication member (m_pySystem)
};
std::unique_ptr<GameSingletons> g_game_singletons;
bool g_host_hardware_cursor_enabled = false;
bool fail(std::string* error, const std::string& text)
{
@@ -229,6 +235,26 @@ void init_2v0_gameplay_stubs();
namespace PythonBoot
{
void SetHostHardwareCursorEnabled(bool enabled)
{
g_host_hardware_cursor_enabled = enabled;
}
bool HostHardwareCursorEnabled()
{
return g_host_hardware_cursor_enabled;
}
int CursorShape()
{
return g_game_singletons ? CPythonApplication::Instance().GetCursorNum() : 0;
}
bool CursorVisible()
{
return !g_game_singletons || CPythonApplication::Instance().GetCursorVisible();
}
std::string CurrentMapName()
{
if (!g_game_singletons)
@@ -255,9 +281,43 @@ void SetUISize(int width, int height)
}
}
void update_3d_pick_ray()
{
if (!g_game_singletons || !g_window_manager)
return;
if (!CPythonBackground::Instance().IsMapReady())
return;
long lx = 0, ly = 0;
g_window_manager->GetMousePosition(lx, ly);
const long sw = g_window_manager->GetScreenWidth();
const long sh = g_window_manager->GetScreenHeight();
if (sw <= 0 || sh <= 0)
return;
CScreen s;
s.SetPerspective(30.0f, g_window_manager->GetAspect(), 100.0f, CPythonBackground::Instance().GetFarClip());
s.UpdateViewMatrix();
s.BuildViewFrustum();
s.SetCursorPosition(lx, ly, sw, sh);
POINT ptMouse{static_cast<LONG>(lx), static_cast<LONG>(ly)};
CPythonItem::Instance().Update(ptMouse);
CPythonCharacterManager::Instance().Pick();
}
void UIMouseMove(int x, int y)
{
MtHostSetCursor(x, y);
if (g_game_singletons)
{
CPythonApplication::Instance().OnMouseMove(x, y);
if (g_window_manager)
{
long lx = x, ly = y;
g_window_manager->GetMousePosition(lx, ly);
MtHostSetCursor(static_cast<int>(lx), static_cast<int>(ly));
}
update_3d_pick_ray();
return;
}
if (g_window_manager) g_window_manager->RunMouseMove(x, y);
}
@@ -265,16 +325,55 @@ void UIMouseButton(int button, bool pressed, int x, int y)
{
MtHostSetCursor(x, y);
if (!g_window_manager) return;
g_window_manager->RunMouseMove(x, y);
if (g_game_singletons)
CPythonApplication::Instance().OnMouseMove(x, y);
else
g_window_manager->RunMouseMove(x, y);
update_3d_pick_ray();
switch (button)
{
case 1: if (pressed) g_window_manager->RunMouseLeftButtonDown(x, y); else g_window_manager->RunMouseLeftButtonUp(x, y); break;
case 2: if (pressed) g_window_manager->RunMouseRightButtonDown(x, y); else g_window_manager->RunMouseRightButtonUp(x, y); break;
case 3: if (pressed) g_window_manager->RunMouseMiddleButtonDown(x, y); else g_window_manager->RunMouseMiddleButtonUp(x, y); break;
case 1:
if (pressed)
g_window_manager->RunMouseLeftButtonDown(x, y);
else
g_window_manager->RunMouseLeftButtonUp(x, y);
break;
case 2:
if (pressed)
{
g_window_manager->RunMouseRightButtonDown(x, y);
}
else
{
g_window_manager->RunMouseRightButtonUp(x, y);
if (g_game_singletons)
{
CCamera* pkCmrCur = CCameraManager::Instance().GetCurrentCamera();
if (pkCmrCur && pkCmrCur->IsDraging())
CPythonApplication::Instance().OnMouseMiddleButtonUp(x, y);
}
}
break;
case 3:
if (g_game_singletons)
{
if (pressed) CPythonApplication::Instance().OnMouseMiddleButtonDown(x, y);
else CPythonApplication::Instance().OnMouseMiddleButtonUp(x, y);
}
if (pressed) g_window_manager->RunMouseMiddleButtonDown(x, y); else g_window_manager->RunMouseMiddleButtonUp(x, y);
break;
default: break;
}
}
void UIMouseWheel(int nLen)
{
CCameraManager& rkCmrMgr = CCameraManager::Instance();
CCamera* pkCmrCur = rkCmrMgr.GetCurrentCamera();
if (pkCmrCur)
pkCmrCur->Wheel(nLen);
}
// 40250 CPythonApplication::OnKeyDown/OnKeyUp (PythonApplicationEvent.cpp:130-146): ESC first goes
// to RunPressEscapeKey (the OnPressEscapeKey chain every dialog closes on), then to RunKeyDown.
void UIKey(int key, bool pressed)
@@ -282,12 +381,80 @@ void UIKey(int key, bool pressed)
if (!g_window_manager) return;
if (pressed)
{
CPythonApplication::Instance().KeyDown(key);
if (DIK_ESCAPE == key)
g_window_manager->RunPressEscapeKey();
g_window_manager->RunKeyDown(key);
}
else
{
CPythonApplication::Instance().KeyUp(key);
g_window_manager->RunKeyUp(key);
}
}
void SetMoveDirection(float angleDeg, bool moving)
{
if (!g_game_singletons) return;
if (moving)
CPythonPlayer::Instance().NEW_MoveToDirection(angleDeg);
else
CPythonPlayer::Instance().NEW_Stop();
}
void SetAttackKey(bool pressed)
{
if (!g_game_singletons) return;
CPythonPlayer::Instance().SetAttackKeyState(pressed);
}
void CameraBeginDrag(int x, int y)
{
CCameraManager& rkCmrMgr = CCameraManager::Instance();
CCamera* pkCmrCur = rkCmrMgr.GetCurrentCamera();
if (pkCmrCur)
pkCmrCur->BeginDrag(x, y);
}
void CameraDrag(int x, int y)
{
CCameraManager& rkCmrMgr = CCameraManager::Instance();
CCamera* pkCmrCur = rkCmrMgr.GetCurrentCamera();
if (pkCmrCur)
{
POINT pt;
pkCmrCur->Drag(x, y, &pt);
}
}
void CameraEndDrag()
{
CCameraManager& rkCmrMgr = CCameraManager::Instance();
CCamera* pkCmrCur = rkCmrMgr.GetCurrentCamera();
if (pkCmrCur)
pkCmrCur->EndDrag();
}
bool IsPointInsideActiveUI(int x, int y)
{
if (!g_window_manager) return false;
return g_window_manager->IsPointInsideActiveUI(x, y);
}
PlayerStatusInfo GetPlayerStatusInfo()
{
PlayerStatusInfo info{};
if (!g_game_singletons) return info;
info.level = CPythonPlayer::Instance().GetStatus(POINT_LEVEL);
info.hp = CPythonPlayer::Instance().GetStatus(POINT_HP);
info.max_hp = CPythonPlayer::Instance().GetStatus(POINT_MAX_HP);
info.sp = CPythonPlayer::Instance().GetStatus(POINT_SP);
info.max_sp = CPythonPlayer::Instance().GetStatus(POINT_MAX_SP);
info.exp = CPythonPlayer::Instance().GetStatus(POINT_EXP);
info.max_exp = CPythonPlayer::Instance().GetStatus(POINT_NEXT_EXP);
const char* szName = CPythonPlayer::Instance().GetName();
if (szName) info.name = szName;
return info;
}
// 40250 CPythonApplication::WindowProcedure (PythonApplicationProcedure.cpp:111-117): WM_CHAR goes to
@@ -346,6 +513,14 @@ void UIRender()
if (g_window_manager) g_window_manager->Render();
}
bool IsSoftwareCursorVisible()
{
if (!g_game_singletons)
return false;
return CPythonApplication::Instance().GetCursorMode() == CPythonApplication::CURSOR_MODE_SOFTWARE &&
CPythonApplication::Instance().GetCursorVisible();
}
bool Start(const char* stdlib_path, std::string* error)
{
if (g_launcher)
@@ -647,6 +822,16 @@ void Stop()
g_fiber.finished = false;
// 40250 Main(): pyLauncher.Clear() runs before app->Destroy()/delete app, so the window manager
// (a CPythonApplication member) is still alive while Py_Finalize runs the windows' __del__.
if (Py_IsInitialized())
{
PyGILState_STATE gil = PyGILState_Ensure();
PyRun_SimpleString(
"import sys\n"
"_mm = sys.modules.get('mouseModule')\n"
"if _mm and hasattr(_mm, 'mouseController'):\n"
" getattr(_mm.mouseController, 'cursorDict', {}).clear()\n");
PyGILState_Release(gil);
}
g_launcher->Clear();
g_game_singletons.reset();
g_game_application.reset();
@@ -24,6 +24,11 @@ namespace PythonBoot
// before the launcher exists.
bool Start(const char* stdlib_path, std::string* error);
bool IsRunning();
// The SDL native client supplies an OS cursor; the Godot host keeps its software cursor.
void SetHostHardwareCursorEnabled(bool enabled);
bool HostHardwareCursorEnabled();
int CursorShape();
bool CursorVisible();
// 40250: RunMainScript(CPythonLauncher&, const char*) — the module initializers, __DEBUG__,
// __COMMAND_LINE__ and RunFile("system.py"). 2V0-e registers temporary observable gameplay modules;
@@ -56,13 +61,34 @@ bool Evaluate(const char* expression, std::string* result, std::string* error);
void SetUISize(int width, int height);
void UIMouseMove(int x, int y);
void UIMouseButton(int button, bool pressed, int x, int y);
void UIMouseWheel(int nLen);
void UIKey(int key, bool pressed);
// Mobile touch & joystick controls
void SetMoveDirection(float angleDeg, bool moving);
void SetAttackKey(bool pressed);
void CameraBeginDrag(int x, int y);
void CameraDrag(int x, int y);
void CameraEndDrag();
bool IsPointInsideActiveUI(int x, int y);
struct PlayerStatusInfo {
int level = 1;
int hp = 0;
int max_hp = 0;
int sp = 0;
int max_sp = 0;
int exp = 0;
int max_exp = 0;
std::string name;
};
PlayerStatusInfo GetPlayerStatusInfo();
// WM_CHAR (a Unicode code point) and WM_KEYDOWN (a Win32 VK code) for CPythonIME and OnIMEKeyDown.
void UIChar(unsigned codepoint);
void UIIMEKeyDown(int vkey);
// While IsAppLooping(), UIUpdate is AppFrame() and UIRender keeps the commands that Process() drew.
void UIUpdate();
void UIRender();
bool IsSoftwareCursorVisible();
// 40250 Main() calls Clear() and lets the launcher leave scope.
void Stop();
@@ -1,84 +1,241 @@
// Platform skeleton for SpeedTreeLib/SpeedTreeForest.h (40250 SpeedTreeLib/SpeedTreeForest.cpp), generated by platform_stub.py.
// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation.
#include "SpeedTreeLib/StdAfx.h"
#include "SpeedTreeLib/SpeedTreeForest.h"
#include "../PlatformStub.h"
#include "EterPack/EterPackManager.h"
#include "GameLib/Property.h"
#include "GameLib/PropertyManager.h"
#include "../EterLib/RenderCommands3D.h"
#include <algorithm>
#include <cfloat>
#include <cmath>
#include <cstdlib>
#include <cstring>
CSpeedTreeForest::CSpeedTreeForest()
: m_fWindStrength(0.2f)
, m_fAccumTime(0.0f)
{
MT_PLATFORM_STUB();
std::memset(m_afLighting, 0, sizeof(m_afLighting));
std::memset(m_afFog, 0, sizeof(m_afFog));
m_afForestExtents[0] = m_afForestExtents[1] = m_afForestExtents[2] = FLT_MAX;
m_afForestExtents[3] = m_afForestExtents[4] = m_afForestExtents[5] = -FLT_MAX;
}
CSpeedTreeForest::~CSpeedTreeForest()
{
MT_PLATFORM_STUB();
}
auto CSpeedTreeForest::ClearMainTree() -> void
{
MT_PLATFORM_STUB();
Clear();
}
auto CSpeedTreeForest::GetMainTree(DWORD, CSpeedTreeWrapper **, const char *) -> BOOL
auto CSpeedTreeForest::GetMainTree(DWORD dwCRC, CSpeedTreeWrapper ** ppMainTree, const char * c_pszFileName) -> BOOL
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<BOOL>();
if (!ppMainTree)
return FALSE;
*ppMainTree = NULL;
if (!IsNativeTerrainRenderEnabled())
return FALSE;
TTreeMap::iterator itor = m_pMainTreeMap.find(dwCRC);
CSpeedTreeWrapper * pTree = NULL;
if (itor != m_pMainTreeMap.end())
{
pTree = itor->second;
}
else
{
if (!c_pszFileName || !c_pszFileName[0])
return FALSE;
CMappedFile file;
LPCVOID c_pvData = NULL;
if (!CEterPackManager::Instance().Get(file, c_pszFileName, &c_pvData))
return FALSE;
float fSize = 1000.0f;
float fVariance = 0.0f;
CProperty * pProperty = NULL;
if (CPropertyManager::InstancePtr() && CPropertyManager::Instance().Get(dwCRC, &pProperty) && pProperty)
{
const char * c_pszTreeSize = NULL;
const char * c_pszTreeVariance = NULL;
if (pProperty->GetString("TreeSize", &c_pszTreeSize) && c_pszTreeSize)
fSize = static_cast<float>( std::atof(c_pszTreeSize));
if (pProperty->GetString("TreeVariance", &c_pszTreeVariance) && c_pszTreeVariance)
fVariance = static_cast<float>(std::atof(c_pszTreeVariance));
}
pTree = new CSpeedTreeWrapper;
if (!pTree->LoadTree(c_pszFileName, static_cast<const BYTE *>(c_pvData), file.Size(), 1, fSize, fVariance))
{
delete pTree;
return FALSE;
}
m_pMainTreeMap.insert(TTreeMap::value_type(dwCRC, pTree));
file.Destroy();
}
*ppMainTree = pTree;
return TRUE;
}
auto CSpeedTreeForest::GetMainTree(DWORD) -> CSpeedTreeWrapper *
auto CSpeedTreeForest::GetMainTree(DWORD dwCRC) -> CSpeedTreeWrapper *
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<CSpeedTreeWrapper *>();
TTreeMap::iterator itor = m_pMainTreeMap.find(dwCRC);
if (itor == m_pMainTreeMap.end())
return NULL;
return itor->second;
}
auto CSpeedTreeForest::DeleteMainTree(DWORD) -> void
auto CSpeedTreeForest::DeleteMainTree(DWORD dwCRC) -> void
{
MT_PLATFORM_STUB();
TTreeMap::iterator itor = m_pMainTreeMap.find(dwCRC);
if (itor == m_pMainTreeMap.end())
return;
CSpeedTreeWrapper * pMainTree = itor->second;
UINT uiCount = 0;
CSpeedTreeWrapper ** ppInstances = pMainTree->GetInstances(uiCount);
for (UINT i = 0; i < uiCount; ++i)
delete ppInstances[i];
delete pMainTree;
m_pMainTreeMap.erase(itor);
}
auto CSpeedTreeForest::CreateInstance(float, float, float, DWORD, const char *) -> CSpeedTreeWrapper *
auto CSpeedTreeForest::CreateInstance(float x, float y, float z, DWORD dwTreeCRC, const char * c_pszTreeName) -> CSpeedTreeWrapper *
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<CSpeedTreeWrapper *>();
if (!IsNativeTerrainRenderEnabled())
return NULL;
CSpeedTreeWrapper * pMainTree = NULL;
if (!GetMainTree(dwTreeCRC, &pMainTree, c_pszTreeName) || !pMainTree)
return NULL;
CSpeedTreeWrapper * pTreeInst = pMainTree->MakeInstance();
if (!pTreeInst)
return NULL;
pTreeInst->SetPosition(x, y, z);
pTreeInst->RegisterBoundingSphere();
AdjustExtents(x, y, z);
return pTreeInst;
}
auto CSpeedTreeForest::DeleteInstance(CSpeedTreeWrapper *) -> void
auto CSpeedTreeForest::DeleteInstance(CSpeedTreeWrapper * pInstance) -> void
{
MT_PLATFORM_STUB();
if (!pInstance)
return;
CSpeedTreeWrapper * pParentTree = pInstance->InstanceOf();
if (!pParentTree)
return;
pParentTree->DeleteInstance(pInstance);
}
auto CSpeedTreeForest::UpdateSystem(float) -> void
auto CSpeedTreeForest::UpdateSystem(float fCurrentTime) -> void
{
MT_PLATFORM_STUB();
static float fLastTime = fCurrentTime;
float fElapsedTime = fCurrentTime - fLastTime;
fLastTime = fCurrentTime;
if (fElapsedTime > 0.0f && fElapsedTime < 10.0f)
m_fAccumTime += fElapsedTime;
SetupWindMatrices(m_fAccumTime);
}
auto CSpeedTreeForest::Clear() -> void
{
MT_PLATFORM_STUB();
TTreeMap::iterator itor = m_pMainTreeMap.begin();
UINT uiCount = 0;
while (itor != m_pMainTreeMap.end())
{
CSpeedTreeWrapper * pMainTree = (itor++)->second;
if (!pMainTree)
continue;
CSpeedTreeWrapper ** ppInstances = pMainTree->GetInstances(uiCount);
for (UINT i = 0; i < uiCount; ++i)
delete ppInstances[i];
delete pMainTree;
}
m_pMainTreeMap.clear();
}
auto CSpeedTreeForest::SetLight(const float *, const float *, const float *) -> void
auto CSpeedTreeForest::SetLight(const float * afDirection, const float * afAmbient, const float * afDiffuse) -> void
{
MT_PLATFORM_STUB();
if (!afDirection || !afAmbient || !afDiffuse)
return;
m_afLighting[0] = afDirection[0];
m_afLighting[1] = afDirection[1];
m_afLighting[2] = afDirection[2];
m_afLighting[3] = 1.0f;
m_afLighting[4] = afAmbient[0];
m_afLighting[5] = afAmbient[1];
m_afLighting[6] = afAmbient[2];
m_afLighting[7] = afAmbient[3];
m_afLighting[8] = afDiffuse[0];
m_afLighting[9] = afDiffuse[1];
m_afLighting[10] = afDiffuse[2];
m_afLighting[11] = afDiffuse[3];
}
auto CSpeedTreeForest::SetFog(float, float) -> void
auto CSpeedTreeForest::SetFog(float fFogNear, float fFogFar) -> void
{
MT_PLATFORM_STUB();
const float denom = (fFogFar - fFogNear);
const float c_fFogLinearScale = (std::fabs(denom) > 1e-4f) ? (1.0f / denom) : 0.0f;
m_afFog[0] = fFogNear;
m_afFog[1] = fFogFar;
m_afFog[2] = c_fFogLinearScale;
m_afFog[3] = 0.0f;
}
auto CSpeedTreeForest::SetWindStrength(float) -> void
auto CSpeedTreeForest::SetWindStrength(float fStrength) -> void
{
MT_PLATFORM_STUB();
if (m_fWindStrength == fStrength)
return;
m_fWindStrength = fStrength;
TTreeMap::iterator itor = m_pMainTreeMap.begin();
UINT uiCount = 0;
while (itor != m_pMainTreeMap.end())
{
CSpeedTreeWrapper * pMainTree = (itor++)->second;
if (!pMainTree)
continue;
CSpeedTreeWrapper ** ppInstances = pMainTree->GetInstances(uiCount);
for (UINT i = 0; i < uiCount; ++i)
{
if (ppInstances[i] && ppInstances[i]->GetSpeedTree())
ppInstances[i]->GetSpeedTree()->SetWindStrength(m_fWindStrength);
}
}
}
auto CSpeedTreeForest::SetupWindMatrices(float) -> void
auto CSpeedTreeForest::SetupWindMatrices(float /*fTimeInSecs*/) -> void
{
MT_PLATFORM_STUB();
}
auto CSpeedTreeForest::AdjustExtents(float, float, float) -> void
auto CSpeedTreeForest::AdjustExtents(float x, float y, float z) -> void
{
MT_PLATFORM_STUB();
m_afForestExtents[0] = std::min(m_afForestExtents[0], x);
m_afForestExtents[1] = std::min(m_afForestExtents[1], y);
m_afForestExtents[2] = std::min(m_afForestExtents[2], z);
m_afForestExtents[3] = std::max(m_afForestExtents[3], x);
m_afForestExtents[4] = std::max(m_afForestExtents[4], y);
m_afForestExtents[5] = std::max(m_afForestExtents[5], z);
}
@@ -1,43 +1,163 @@
// Platform skeleton for SpeedTreeLib/SpeedTreeForestDirectX8.h (40250 SpeedTreeLib/SpeedTreeForestDirectX8.cpp), generated by platform_stub.py.
// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation.
#include "SpeedTreeLib/StdAfx.h"
#include "SpeedTreeLib/SpeedTreeForestDirectX8.h"
#include "../PlatformStub.h"
#include "EterBase/Timer.h"
#include "EterLib/Camera.h"
#include "EterLib/StateManager.h"
#include "../EterLib/RenderCommands3D.h"
CSpeedTreeForestDirectX8::CSpeedTreeForestDirectX8()
: m_pDx(NULL)
, m_dwBranchVertexShader(D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX1)
, m_dwLeafVertexShader(D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX1)
{
MT_PLATFORM_STUB();
}
CSpeedTreeForestDirectX8::~CSpeedTreeForestDirectX8()
{
MT_PLATFORM_STUB();
Clear();
}
auto CSpeedTreeForestDirectX8::UploadWindMatrix(unsigned int, const float *) const -> void
auto CSpeedTreeForestDirectX8::UploadWindMatrix(unsigned int uiLocation, const float * pMatrix) const -> void
{
MT_PLATFORM_STUB();
if (pMatrix)
STATEMANAGER.SetVertexShaderConstant(uiLocation, pMatrix, 4);
}
auto CSpeedTreeForestDirectX8::UpdateCompundMatrix(const D3DXVECTOR3 &, const D3DXMATRIX &, const D3DXMATRIX &) -> void
auto CSpeedTreeForestDirectX8::UpdateCompundMatrix(const D3DXVECTOR3 & /*c_rEyeVec*/, const D3DXMATRIX & c_rmatView, const D3DXMATRIX & c_rmatProj) -> void
{
MT_PLATFORM_STUB();
D3DXMATRIX matBlendShader;
D3DXMatrixMultiply(&matBlendShader, &c_rmatView, &c_rmatProj);
D3DXMatrixTranspose(&matBlendShader, &matBlendShader);
STATEMANAGER.SetVertexShaderConstant(0, &matBlendShader, 4);
}
auto CSpeedTreeForestDirectX8::Render(unsigned long) -> void
auto CSpeedTreeForestDirectX8::Render(unsigned long ulRenderBitVector) -> void
{
MT_PLATFORM_STUB();
if (!IsNativeTerrainRenderEnabled())
return;
UpdateSystem(CTimer::Instance().GetCurrentSecond());
if (m_pMainTreeMap.empty())
return;
if (!(ulRenderBitVector & Forest_RenderToShadow) && !(ulRenderBitVector & Forest_RenderToMiniMap))
{
CCamera * pCamera = CCameraManager::Instance().GetCurrentCamera();
if (pCamera)
UpdateCompundMatrix(pCamera->GetEye(), ms_matView, ms_matProj);
}
DWORD dwLightState = STATEMANAGER.GetRenderState(D3DRS_LIGHTING);
DWORD dwColorVertexState = STATEMANAGER.GetRenderState(D3DRS_COLORVERTEX);
DWORD dwFogVertexMode = STATEMANAGER.GetRenderState(D3DRS_FOGVERTEXMODE);
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, TRUE);
STATEMANAGER.SetRenderState(D3DRS_COLORVERTEX, TRUE);
TTreeMap::iterator itor = m_pMainTreeMap.begin();
UINT uiCount = 0;
while (itor != m_pMainTreeMap.end())
{
CSpeedTreeWrapper * pMainTree = (itor++)->second;
if (!pMainTree)
continue;
CSpeedTreeWrapper ** ppInstances = pMainTree->GetInstances(uiCount);
for (UINT i = 0; i < uiCount; ++i)
{
if (ppInstances[i])
ppInstances[i]->Advance();
}
}
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_MINFILTER, D3DTEXF_LINEAR);
STATEMANAGER.SetTextureStageState(0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR);
STATEMANAGER.SetTextureStageState(0, D3DTSS_MIPFILTER, D3DTEXF_LINEAR);
STATEMANAGER.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
STATEMANAGER.SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
STATEMANAGER.SaveRenderState(D3DRS_ALPHATESTENABLE, FALSE);
STATEMANAGER.SaveRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATER);
STATEMANAGER.SaveRenderState(D3DRS_ALPHAREF, 0x00000060);
STATEMANAGER.SaveRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
STATEMANAGER.SetVertexShader(m_dwBranchVertexShader);
// Render branches
if (ulRenderBitVector & Forest_RenderBranches)
{
STATEMANAGER.SetRenderState(D3DRS_ALPHATESTENABLE, FALSE);
itor = m_pMainTreeMap.begin();
while (itor != m_pMainTreeMap.end())
{
CSpeedTreeWrapper * pMainTree = (itor++)->second;
if (!pMainTree)
continue;
CSpeedTreeWrapper ** ppInstances = pMainTree->GetInstances(uiCount);
pMainTree->SetupBranchForTreeType();
for (UINT i = 0; i < uiCount; ++i)
{
if (ppInstances[i] && ppInstances[i]->isShow())
ppInstances[i]->RenderBranches();
}
}
}
// Render leaves
if (ulRenderBitVector & Forest_RenderLeaves)
{
STATEMANAGER.SetVertexShader(m_dwLeafVertexShader);
STATEMANAGER.SetRenderState(D3DRS_ALPHATESTENABLE, TRUE);
STATEMANAGER.SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATER);
STATEMANAGER.SetRenderState(D3DRS_ALPHAREF, 0x00000060);
STATEMANAGER.SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
itor = m_pMainTreeMap.begin();
while (itor != m_pMainTreeMap.end())
{
CSpeedTreeWrapper * pMainTree = (itor++)->second;
if (!pMainTree)
continue;
CSpeedTreeWrapper ** ppInstances = pMainTree->GetInstances(uiCount);
pMainTree->SetupLeafForTreeType();
for (UINT i = 0; i < uiCount; ++i)
{
if (ppInstances[i] && ppInstances[i]->isShow())
ppInstances[i]->RenderLeaves();
}
pMainTree->EndLeafForTreeType();
}
}
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, dwLightState);
STATEMANAGER.SetRenderState(D3DRS_COLORVERTEX, dwColorVertexState);
STATEMANAGER.SetRenderState(D3DRS_FOGVERTEXMODE, dwFogVertexMode);
STATEMANAGER.RestoreRenderState(D3DRS_ALPHATESTENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_ALPHAFUNC);
STATEMANAGER.RestoreRenderState(D3DRS_ALPHAREF);
STATEMANAGER.RestoreRenderState(D3DRS_CULLMODE);
}
auto CSpeedTreeForestDirectX8::SetRenderingDevice(LPDIRECT3DDEVICE8) -> bool
auto CSpeedTreeForestDirectX8::SetRenderingDevice(LPDIRECT3DDEVICE8 pDevice) -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
m_pDx = pDevice;
return InitVertexShaders();
}
auto CSpeedTreeForestDirectX8::InitVertexShaders() -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
m_dwBranchVertexShader = D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX1;
m_dwLeafVertexShader = D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX1;
CSpeedTreeWrapper::SetVertexShaders(m_dwBranchVertexShader, m_dwLeafVertexShader);
return true;
}
File diff suppressed because it is too large Load Diff
@@ -1,22 +0,0 @@
// Platform skeleton for SphereLib/frustum.h (40250 SphereLib/frustum.cpp), generated by platform_stub.py.
// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation.
#include "SphereLib/StdAfx.h"
#include "SphereLib/frustum.h"
#include "../PlatformStub.h"
auto Frustum::BuildViewFrustum(D3DXMATRIX &) -> void
{
MT_PLATFORM_STUB();
}
auto Frustum::BuildViewFrustum2(D3DXMATRIX &, float, float, float, float, const D3DXVECTOR3 &, const D3DXVECTOR3 &) -> void
{
MT_PLATFORM_STUB();
}
auto Frustum::ViewVolumeTest(const Vector3d &, const float) const -> ViewState
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<ViewState>();
}
@@ -1,49 +0,0 @@
// Platform skeleton for SphereLib/sphere.h (40250 SphereLib/sphere.cpp), generated by platform_stub.py.
// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation.
#include "SphereLib/StdAfx.h"
#include "SphereLib/sphere.h"
#include "../PlatformStub.h"
SphereInterface::SphereInterface()
{
MT_PLATFORM_STUB();
}
SphereInterface::~SphereInterface()
{
MT_PLATFORM_STUB();
}
auto Sphere::Set(const Vector3d &, float) -> void
{
MT_PLATFORM_STUB();
}
auto Sphere::Compute(const SphereInterface &) -> void
{
MT_PLATFORM_STUB();
}
auto Sphere::RayIntersection(const Vector3d &, const Vector3d &, float, Vector3d *) -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
}
auto Sphere::RayIntersection(const Vector3d &, const Vector3d &, Vector3d *) -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
}
auto Sphere::RayIntersectionInFront(const Vector3d &, const Vector3d &, Vector3d *) -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
}
auto Sphere::Report() -> void
{
MT_PLATFORM_STUB();
}
@@ -1,138 +0,0 @@
// Platform skeleton for SphereLib/spherepack.h (40250 SphereLib/spherepack.cpp), generated by platform_stub.py.
// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation.
#include "SphereLib/StdAfx.h"
#include "SphereLib/spherepack.h"
#include "../PlatformStub.h"
auto SpherePack::LostChild(SpherePack *) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePack::Render(unsigned int) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePack::Recompute(float) -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
}
auto SpherePack::VisibilityTest(const Frustum &, SpherePackCallback *, ViewState) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePack::RayTrace(const Vector3d &, const Vector3d &, float, SpherePackCallback *) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePack::RangeTest(const Vector3d &, float, SpherePackCallback *, ViewState) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePack::PointTest2d(const Vector3d &, SpherePackCallback *, ViewState) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePack::Reset() -> void
{
MT_PLATFORM_STUB();
}
SpherePackFactory::SpherePackFactory(int, float, float, float)
{
MT_PLATFORM_STUB();
}
SpherePackFactory::~SpherePackFactory()
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::Process() -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::AddSphere_(const Vector3d &, float, void *, bool, int) -> SpherePack *
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<SpherePack *>();
}
auto SpherePackFactory::AddIntegrate(SpherePack *) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::AddRecompute(SpherePack *) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::Integrate(SpherePack *, SpherePack *, float) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::Render() -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::Remove(SpherePack *) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::FrustumTest(const Frustum &, SpherePackCallback *) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::RayTrace(const Vector3d &, const Vector3d &, SpherePackCallback *) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::RangeTest(const Vector3d &, float, SpherePackCallback *) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::PointTest2d(const Vector3d &, SpherePackCallback *) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::RayTraceCallback(const Vector3d &, const Vector3d &, float, const Vector3d &, SpherePack *) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::RangeTestCallback(const Vector3d &, float, SpherePack *, ViewState) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::PointTest2dCallback(const Vector3d &, SpherePack *, ViewState) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::VisibilityCallback(const Frustum &, SpherePack *, ViewState) -> void
{
MT_PLATFORM_STUB();
}
auto SpherePackFactory::Reset() -> void
{
MT_PLATFORM_STUB();
}
@@ -1,12 +0,0 @@
// Platform skeleton for SphereLib/vector.h, generated by platform_stub.py.
// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation.
#include "SphereLib/StdAfx.h"
#include "SphereLib/vector.h"
#include "../PlatformStub.h"
auto Vector3d::IsInStaticRange() const -> bool
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<bool>();
}
@@ -25,8 +25,9 @@
//
// PORT: there is no CPythonApplication object yet (its members are the whole game: background,
// network stream, player, ...), so these functions only use the singletons PythonBoot owns
// (UI::CWindowManager, CTimer, CResourceManager) and never touch a data member.
CPythonApplication* CPythonApplication::ms_pInstance = nullptr;
alignas(alignof(CPythonApplication)) static char s_app_storage[sizeof(CPythonApplication)] = {};
CPythonApplication* CPythonApplication::ms_pInstance = reinterpret_cast<CPythonApplication*>(s_app_storage);
extern double g_specularSpd;
void CPythonApplication::ShowWebPage(const char*, const RECT&) { MT_PLATFORM_STUB(); }
void CPythonApplication::MoveWebPage(const RECT&) { MT_PLATFORM_STUB(); }
@@ -134,6 +135,16 @@ bool CPythonApplication::Create(PyObject*, const char*, int, int, int)
// 40250 PythonApplication.cpp:1260.
CPythonTextTail::Instance().Initialize();
if (IsNativeTerrainRenderEnabled())
{
const bool use_hardware_cursor = PythonBoot::HostHardwareCursorEnabled();
if (CPythonSystem::InstancePtr() && CPythonSystem::Instance().GetConfig())
CPythonSystem::Instance().GetConfig()->is_software_cursor = !use_hardware_cursor;
SetCursorMode(use_hardware_cursor ? CURSOR_MODE_HARDWARE : CURSOR_MODE_SOFTWARE);
CGrannyMaterial::CreateSphereMap(0, "d:/ymir work/special/spheremap.jpg");
CGrannyMaterial::CreateSphereMap(1, "d:/ymir work/special/spheremap01.jpg");
}
return true;
}
// 40250 PythonApplication.cpp:105
@@ -216,6 +227,7 @@ void CPythonApplication::UpdateGame()
s.SetPerspective(30.0f,fAspect, 100.0f, fFarClip);
s.BuildViewFrustum();
s.SetCursorPosition(ptMouse.x, ptMouse.y, UI::CWindowManager::Instance().GetScreenWidth(), UI::CWindowManager::Instance().GetScreenHeight());
}
TPixelPosition kPPosMainActor;
@@ -262,8 +274,13 @@ bool CPythonApplication::Process()
CResourceManager::Instance().Update();
OnCameraUpdate();
if (IsNativeTerrainRenderEnabled())
OnMouseUpdate();
OnUIUpdate();
if (IsNativeTerrainRenderEnabled())
CGrannyMaterial::TranslateSpecularMatrix(g_specularSpd, g_specularSpd, 0.0f);
// PORT: the render block without the lost-device restore, ClearDepthBuffer, Show and the render-time
// statistics (Godot clears and presents the frame). The frame's UI and 3D command lists restart here.
CCullingManager::Instance().Update();
@@ -275,6 +292,14 @@ bool CPythonApplication::Process()
rkGraphic.SetInterfaceRenderState();
OnUIRender();
if (IsNativeTerrainRenderEnabled())
{
rkGraphic.SetInterfaceRenderState();
unsigned cw = 0, ch = 0;
UIRenderGetSize(&cw, &ch);
UIRenderSetClip(0.0f, 0.0f, float(cw), float(ch));
OnMouseRender();
}
rkGraphic.End();
}
@@ -11,7 +11,10 @@ void CPythonApplication::OnCameraUpdate()
{
CCamera* pkCameraMgr = CCameraManager::Instance().GetCurrentCamera();
if (pkCameraMgr)
{
pkCameraMgr->Update();
CPythonGraphic::Instance().UpdateViewMatrix();
}
}
}
@@ -9,31 +9,41 @@ void CGrannyModelInstance::MakeBoundBox(TBoundBox* pBoundBox,
D3DXVECTOR3* vtMin,
D3DXVECTOR3* vtMax)
{
pBoundBox->sx = OBBMin[0] * mat[0] + OBBMin[1] * mat[4] + OBBMin[2] * mat[8] + mat[12];
pBoundBox->sy = OBBMin[0] * mat[1] + OBBMin[1] * mat[5] + OBBMin[2] * mat[9] + mat[13];
pBoundBox->sz = OBBMin[0] * mat[2] + OBBMin[1] * mat[6] + OBBMin[2] * mat[10] + mat[14];
pBoundBox->sx = +10000000.0f;
pBoundBox->sy = +10000000.0f;
pBoundBox->sz = +10000000.0f;
pBoundBox->ex = -10000000.0f;
pBoundBox->ey = -10000000.0f;
pBoundBox->ez = -10000000.0f;
pBoundBox->ex = OBBMax[0] * mat[0] + OBBMax[1] * mat[4] + OBBMax[2] * mat[8] + mat[12];
pBoundBox->ey = OBBMax[0] * mat[1] + OBBMax[1] * mat[5] + OBBMax[2] * mat[9] + mat[13];
pBoundBox->ez = OBBMax[0] * mat[2] + OBBMax[1] * mat[6] + OBBMax[2] * mat[10] + mat[14];
for (int c = 0; c < 8; ++c)
{
const float ox = (c & 1) ? OBBMax[0] : OBBMin[0];
const float oy = (c & 2) ? OBBMax[1] : OBBMin[1];
const float oz = (c & 4) ? OBBMax[2] : OBBMin[2];
const float tx = ox * mat[0] + oy * mat[4] + oz * mat[8] + mat[12];
const float ty = ox * mat[1] + oy * mat[5] + oz * mat[9] + mat[13];
const float tz = ox * mat[2] + oy * mat[6] + oz * mat[10] + mat[14];
pBoundBox->sx = min(pBoundBox->sx, tx);
pBoundBox->sy = min(pBoundBox->sy, ty);
pBoundBox->sz = min(pBoundBox->sz, tz);
pBoundBox->ex = max(pBoundBox->ex, tx);
pBoundBox->ey = max(pBoundBox->ey, ty);
pBoundBox->ez = max(pBoundBox->ez, tz);
}
vtMin->x = min(vtMin->x, pBoundBox->sx);
vtMin->x = min(vtMin->x, pBoundBox->ex);
vtMin->y = min(vtMin->y, pBoundBox->sy);
vtMin->y = min(vtMin->y, pBoundBox->ey);
vtMin->z = min(vtMin->z, pBoundBox->sz);
vtMin->z = min(vtMin->z, pBoundBox->ez);
vtMax->x = max(vtMax->x, pBoundBox->sx);
vtMax->x = max(vtMax->x, pBoundBox->ex);
vtMax->y = max(vtMax->y, pBoundBox->sy);
vtMax->y = max(vtMax->y, pBoundBox->ey);
vtMax->z = max(vtMax->z, pBoundBox->sz);
vtMax->z = max(vtMax->z, pBoundBox->ez);
}
bool CGrannyModelInstance::Intersect(const D3DXMATRIX * c_pMatrix,
float * /*pu*/, float * /*pv*/, float * pt)
float * pu, float * pv, float * pt)
{
if (!m_pgrnModelInstance)
return false;
@@ -88,7 +98,51 @@ bool CGrannyModelInstance::Intersect(const D3DXMATRIX * c_pMatrix,
return ret;
}
return true;
bool hasVolumeBone = false;
for (int m = 0; m < meshCount; ++m)
{
const granny_mesh * pgrnMesh = m_pModel->GetGrannyModelPointer()->MeshBindings[m].Mesh;
int * boneIndices = __GetMeshBoneIndices(m);
for (int b = 0; b < pgrnMesh->BoneBindingCount; ++b)
{
const granny_bone_binding& rgrnBoneBinding = pgrnMesh->BoneBindings[b];
if (rgrnBoneBinding.OBBMin[0] >= rgrnBoneBinding.OBBMax[0] &&
rgrnBoneBinding.OBBMin[1] >= rgrnBoneBinding.OBBMax[1] &&
rgrnBoneBinding.OBBMin[2] >= rgrnBoneBinding.OBBMax[2])
continue;
hasVolumeBone = true;
const D3DXMATRIX * pBoneMat = (const D3DXMATRIX *)GrannyGetWorldPose4x4(__GetWorldPosePtr(), boneIndices[b]);
D3DXMATRIX matBoneWorld;
if (c_pMatrix)
D3DXMatrixMultiply(&matBoneWorld, pBoneMat, c_pMatrix);
else
matBoneWorld = *pBoneMat;
if (IntersectCube(&matBoneWorld,
rgrnBoneBinding.OBBMin[0], rgrnBoneBinding.OBBMin[1], rgrnBoneBinding.OBBMin[2],
rgrnBoneBinding.OBBMax[0], rgrnBoneBinding.OBBMax[1], rgrnBoneBinding.OBBMax[2],
ms_vtPickRayOrig, ms_vtPickRayDir,
&u, &v, &t))
{
if (pu) *pu = u;
if (pv) *pv = v;
if (pt) *pt = t;
return true;
}
}
}
if (!hasVolumeBone)
{
if (pu) *pu = u;
if (pv) *pv = v;
if (pt) *pt = t;
return true;
}
return false;
/*
TBoundBox* boundBoxs = s_boundBoxPool.base();
+12 -1
View File
@@ -92,7 +92,16 @@ void CCamera::Wheel(int nLen)
if (IsLock())
return;
m_v3AngularVelocity.y = (float)(nLen) * m_fResistance;
float fAdd = (float)(nLen) * m_fResistance;
if ((m_v3AngularVelocity.y > 0.0f && fAdd < 0.0f) || (m_v3AngularVelocity.y < 0.0f && fAdd > 0.0f))
m_v3AngularVelocity.y = fAdd;
else
m_v3AngularVelocity.y += fAdd;
if (m_v3AngularVelocity.y > 500.0f)
m_v3AngularVelocity.y = 500.0f;
else if (m_v3AngularVelocity.y < -500.0f)
m_v3AngularVelocity.y = -500.0f;
}
void CCamera::BeginDrag(int nMouseX, int nMouseY)
@@ -167,6 +176,8 @@ bool CCamera::Drag(int nMouseX, int nMouseY, LPPOINT lpReturnPoint)
m_v3AngularVelocity.x = fNewRotationVelocity;
m_v3AngularVelocity.z = fNewPitchVelocity;
m_lMousePosX = lMouseX;
m_lMousePosY = lMouseY;
lpReturnPoint->x = m_lMousePosX;
lpReturnPoint->y = m_lMousePosY;
return true;
@@ -0,0 +1,176 @@
#include "StdAfx.h"
#include "CullingManager.h"
#include "GrpObjectInstance.h"
//#define COUNT_SHOWING_SPHERE
#ifdef COUNT_SHOWING_SPHERE
int showingcount = 0;
#endif
void CCullingManager::RayTraceCallback(const Vector3d &/*p1*/, // source pos of ray
const Vector3d &/*dir*/, // dest pos of ray
float distance,
const Vector3d &/*sect*/,
SpherePack *sphere)
{
//if (state!=VS_OUTSIDE)
//{
if (m_RayFarDistance<=0.0f || m_RayFarDistance>=distance)
{
#ifdef SPHERELIB_STRICT
if (sphere->IS_SPHERE)
puts("CCullingManager::RayTraceCallback");
#endif
m_list.push_back((CGraphicObjectInstance *)sphere->GetUserData());
}
//f((CGraphicObjectInstance *)sphere->GetUserData());
//}
}
void CCullingManager::VisibilityCallback(const Frustum &/*f*/,SpherePack *sphere,ViewState state)
{
#ifdef SPHERELIB_STRICT
if (sphere->IS_SPHERE)
puts("CCullingManager::VisibilityCallback");
#endif
CGraphicObjectInstance * pInstance = (CGraphicObjectInstance*)sphere->GetUserData();
/*if (state == VS_PARTIAL)
{
Vector3d v;
float r;
pInstance->GetBoundingSphere(v,r);
state = f.ViewVolumeTest(v,r);
}*/
if (state == VS_OUTSIDE)
{
#ifdef COUNT_SHOWING_SPHERE
if (pInstance->isShow())
{
Tracef("SH : %p ",sphere->GetUserData());
showingcount--;
Tracef("show size : %5d\n",showingcount);
}
#endif
pInstance->Hide();
}
else
{
#ifdef COUNT_SHOWING_SPHERE
if (!pInstance->isShow())
{
Tracef("HS : %p ",sphere->GetUserData());
showingcount++;
Tracef("show size : %5d\n",showingcount);
}
#endif
pInstance->Show();
}
}
void CCullingManager::RangeTestCallback(const Vector3d &/*p*/,float /*distance*/,SpherePack *sphere,ViewState state)
{
#ifdef SPHERELIB_STRICT
if (sphere->IS_SPHERE)
puts("CCullingManager::RangeTestCallback");
#endif
if (state!=VS_OUTSIDE)
{
m_list.push_back((CGraphicObjectInstance *)sphere->GetUserData());
//f((CGraphicObjectInstance *)sphere->GetUserData());
}
//assert(false && "NOT REACHED");
}
void CCullingManager::Reset()
{
m_Factory->Reset();
}
void CCullingManager::Update()
{
// TODO : update each object
// ÇÏÁö¸»°í °¢ÀÚ ÇÏ°Ô ÇØº¸ÀÚ
//DWORD time = ELTimer_GetMSec();
//Reset();
m_Factory->Process();
//Tracef("cull update : %3d ",ELTimer_GetMSec()-time);
}
void CCullingManager::Process()
{
//DWORD time = ELTimer_GetMSec();
//Frustum f;
UpdateViewMatrix();
UpdateProjMatrix();
BuildViewFrustum();
m_Factory->FrustumTest(GetFrustum(), this);
//Tracef("cull process : %3d ",ELTimer_GetMSec()-time);
}
CCullingManager::CullingHandle CCullingManager::Register(CGraphicObjectInstance * obj)
{
assert(obj);
#ifdef COUNT_SHOWING_SPHERE
Tracef("CR : %p ",obj);
showingcount++;
Tracef("show size : %5d\n",showingcount);
#endif
Vector3d center;
float radius;
obj->GetBoundingSphere(center,radius);
return m_Factory->AddSphere_(center,radius,obj, false);
}
void CCullingManager::Unregister(CullingHandle h)
{
#ifdef COUNT_SHOWING_SPHERE
if (((CGraphicObjectInstance*)h->GetUserData())->isShow())
{
Tracef("DE : %p ",h->GetUserData());
showingcount--;
Tracef("show size : %5d\n",showingcount);
}
#endif
m_Factory->Remove(h);
}
CCullingManager::CCullingManager()
{
m_Factory = new SpherePackFactory(
10000, // maximum count
6400, // root radius
1600, // leaf radius
400 // extra radius
);
}
CCullingManager::~CCullingManager()
{
delete m_Factory;
}
void CCullingManager::FindRange(const Vector3d &p, float radius)
{
m_list.clear();
m_Factory->RangeTest(p, radius, this);
}
void CCullingManager::FindRay(const Vector3d &p1, const Vector3d &dir)
{
m_RayFarDistance = -1;
m_list.clear();
m_Factory->RayTrace(p1,dir,this);
}
void CCullingManager::FindRayDistance(const Vector3d &p1, const Vector3d &dir, float distance)
{
m_RayFarDistance = distance;
m_list.clear();
m_Factory->RayTrace(p1,dir,this);
}
+4
View File
@@ -139,6 +139,10 @@ void CGraphicBase::SetBackBufferSize(UINT uWidth, UINT uHeight)
{
ms_d3dPresentParameter.BackBufferWidth = uWidth;
ms_d3dPresentParameter.BackBufferHeight = uHeight;
ms_iWidth = uWidth;
ms_iHeight = uHeight;
ms_Viewport.Width = uWidth;
ms_Viewport.Height = uHeight;
}
void CGraphicBase::SetDefaultIndexBuffer(UINT eDefIB)
@@ -723,13 +723,32 @@ namespace UI
CWindow * pLayer = *ritor;
CWindow * pPickedWindow = pLayer->PickWindow(x, y);
if (pPickedWindow != pLayer)
if (pPickedWindow != pLayer) {
return pPickedWindow;
}
}
return NULL;
}
bool CWindowManager::IsPointInsideActiveUI(long x, long y)
{
CWindow * pPicked = __PickWindow(x, y);
if (!pPicked || pPicked == m_pRootWindow)
return false;
CWindow * pCur = pPicked;
while (pCur->GetParent() && pCur->GetParent() != m_pRootWindow)
{
pCur = pCur->GetParent();
}
if (pCur && (0 == strcmp(pCur->GetName(), "GAME") || pCur == m_pRootWindow))
return false;
return true;
}
void CWindowManager::SetMousePosition(long x, long y)
{
if (m_iHres==0)
@@ -760,6 +779,13 @@ namespace UI
SetMousePosition(x, y);
CWindow * pPointWindow = __PickWindow(m_lMouseX, m_lMouseY);
if (x == 470 && y == 380) {
std::printf(">>> C++ RunMouseMove(470, 380): m_lMouse=(%ld, %ld), pPointWindow=%s (type=%s, is_show=%d)\n",
m_lMouseX, m_lMouseY,
pPointWindow ? pPointWindow->GetName() : "NULL",
pPointWindow ? typeid(*pPointWindow).name() : "none",
pPointWindow ? pPointWindow->IsShow() : 0);
}
if (g_bShowOverInWindowName)
{
@@ -103,6 +103,9 @@ namespace UI
void ActivateWindow(CWindow * pWin);
void DeactivateWindow();
CWindow * GetActivateWindow();
bool IsPointInsideActiveUI(long x, long y);
CWindow * PickWindow(long x, long y) { return __PickWindow(x, y); }
void SetTop(CWindow * pWin);
void SetTopUIWindow();
void ResetCapture();
+1 -1
View File
@@ -250,7 +250,7 @@ bool CActorInstance::IsHandMode()
bool CActorInstance::IsTwoHandMode()
{
if (CRaceMotionData::MODE_TWOHAND_SWORD==GetMotionMode())
if (CRaceMotionData::MODE_TWOHAND_SWORD==GetMotionMode() || CRaceMotionData::MODE_HORSE_TWOHAND_SWORD==GetMotionMode())
return true;
return false;
+6
View File
@@ -0,0 +1,6 @@
// stdafx.cpp : source file that includes just the standard includes
// SphereLib.pch will be the pre-compiled header
// stdafx.obj will contain the pre-compiled type information
#include "StdAfx.h"
+93
View File
@@ -0,0 +1,93 @@
/* Copyright (C) John W. Ratcliff, 2001.
* All rights reserved worldwide.
*
* This software is provided "as is" without express or implied
* warranties. You may freely copy and compile this source into
* applications you distribute provided that the copyright text
* below is included in the resulting source code, for example:
* "Portions Copyright (C) John W. Ratcliff, 2001"
*/
#include "StdAfx.h"
#include "frustum.h"
//#include "frustum.h"
/*void Frustum::Set(int x1,int y1,int x2,int y2)
{
mX1 = x1;
mY1 = y1;
mX2 = x2;
mY2 = y2;
}
*/
ViewState Frustum::ViewVolumeTest(const Vector3d &c_v3Center,const float c_fRadius) const
{
if (m_bUsingSphere)
{
D3DXVECTOR3 v(
c_v3Center.x-m_v3Center.x,
c_v3Center.y-m_v3Center.y,
c_v3Center.z-m_v3Center.z);
if ((c_fRadius + m_fRadius) * (c_fRadius + m_fRadius) < D3DXVec3LengthSq(&v))
{
return VS_OUTSIDE;
}
}
const int count=6;
D3DXVECTOR3 center = c_v3Center;
//center.y *=-1;
int i;
float distance[count];
for(i=0;i<count;i++)
{
distance[i] = D3DXPlaneDotCoord(&m_plane[i],&center);
if (distance[i]<=-c_fRadius)
return VS_OUTSIDE;
}
//return VS_INSIDE;
for(i=0;i<count;i++)
{
if (distance[i]<=c_fRadius)
return VS_PARTIAL;
}
return VS_INSIDE;
}
void Frustum::BuildViewFrustum(D3DXMATRIX & mat)
{
m_bUsingSphere = false;
m_plane[0] = D3DXPLANE( mat._13, mat._23, mat._33, mat._43);
m_plane[1] = D3DXPLANE(mat._14 - mat._13, mat._24 - mat._23, mat._34 - mat._33, mat._44 - mat._43);
//m_plane[0] = D3DXPLANE(mat._14 + mat._13, mat._24 + mat._23, mat._34 + mat._33, mat._44 + mat._43);
m_plane[2] = D3DXPLANE(mat._14 + mat._11, mat._24 + mat._21, mat._34 + mat._31, mat._44 + mat._41);
m_plane[3] = D3DXPLANE(mat._14 - mat._11, mat._24 - mat._21, mat._34 - mat._31, mat._44 - mat._41);
m_plane[4] = D3DXPLANE(mat._14 + mat._12, mat._24 + mat._22, mat._34 + mat._32, mat._44 + mat._42);
m_plane[5] = D3DXPLANE(mat._14 - mat._12, mat._24 - mat._22, mat._34 - mat._32, mat._44 - mat._42);
for(int i=0;i<6;i++)
D3DXPlaneNormalize(&m_plane[i],&m_plane[i]);
}
void Frustum::BuildViewFrustum2(D3DXMATRIX & mat, float fNear, float fFar, float fFov, float fAspect, const D3DXVECTOR3 & vCamera, const D3DXVECTOR3 & vLook)
{
float fViewLen = fFar-fNear;
float fH = fViewLen * tan(fFov*0.5f);
float fW = fH*fAspect;
D3DXVECTOR3 P(0.0f, 0.0f, fNear+fViewLen*0.5f);
D3DXVECTOR3 Q(fW, fH, fViewLen);
D3DXVECTOR3 PQ = P-Q;
m_fRadius = D3DXVec3Length(&PQ);
m_v3Center = vCamera + vLook * (fNear+fViewLen*0.5f);
BuildViewFrustum(mat);
m_bUsingSphere = true;
}
+248
View File
@@ -0,0 +1,248 @@
/* Copyright (C) John W. Ratcliff, 2001.
* All rights reserved worldwide.
*
* This software is provided "as is" without express or implied
* warranties. You may freely copy and compile this source into
* applications you distribute provided that the copyright text
* below is included in the resulting source code, for example:
* "Portions Copyright (C) John W. Ratcliff, 2001"
*/
#include "StdAfx.h"
#include "sphere.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
bool Vector3d::IsInStaticRange() const
{
const float LIMIT = 3276700.0f;
if (x<LIMIT && x>-LIMIT)
if (y<LIMIT && y>-LIMIT)
if (z<LIMIT && z>-LIMIT)
return true;
return false;
}
void Sphere::Set(const Vector3d &center, float radius)
{
#ifdef __STATIC_RANGE__
assert(center.IsInStaticRange());
#endif
mCenter = center;
mRadius = radius;
mRadius2 = radius*radius;
}
//ray-sphere intersection test from Graphics Gems p.388
// **NOTE** There is a bug in this Graphics Gem. If the origin
// of the ray is *inside* the sphere being tested, it reports the
// wrong intersection location. This code has a fix for the bug.
bool Sphere::RayIntersection(const Vector3d &rayOrigin,
const Vector3d &dir,
Vector3d *intersect)
{
//notation:
//point E = rayOrigin
//point O = sphere center
Vector3d EO = mCenter - rayOrigin;
Vector3d V = dir;
float dist2 = EO.x*EO.x + EO.y*EO.y + EO.z * EO.z;
// Bug Fix For Gem, if origin is *inside* the sphere, invert the
// direction vector so that we get a valid intersection location.
if ( dist2 < mRadius2 ) V*=-1;
float v = EO.Dot(V);
float disc = mRadius2 - (EO.Length2() - v*v);
if (disc > 0.0f)
{
if ( intersect )
{
float d = sqrtf(disc);
//float dist2 = rayOrigin.DistanceSq(mCenter);
*intersect = rayOrigin + V*(v-d);
}
return true;
}
return false;
}
//
bool Sphere::RayIntersection(const Vector3d &rayOrigin,
const Vector3d &V,
float distance,
Vector3d *intersect)
{
Vector3d sect;
bool hit = RayIntersectionInFront(rayOrigin,V,&sect);
if ( hit )
{
float d = rayOrigin.DistanceSq(sect);
if ( d > (distance*distance) ) return false;
if ( intersect ) *intersect = sect;
return true;
}
return false;
}
bool Sphere::RayIntersectionInFront(const Vector3d &rayOrigin,
const Vector3d &V,
Vector3d *intersect)
{
Vector3d sect;
bool hit = RayIntersection(rayOrigin,V,&sect);
if ( hit )
{
Vector3d dir = sect - rayOrigin;
float dot = dir.Dot(V);
if ( dot >= 0 ) // then it's in front!
{
if ( intersect ) *intersect = sect;
return true;
}
}
return false;
}
void Sphere::Report(void)
{
}
/*
An Efficient Bounding Sphere
by Jack Ritter
from "Graphics Gems", Academic Press, 1990
*/
/* Routine to calculate tight bounding sphere over */
/* a set of points in 3D */
/* This contains the routine find_bounding_sphere(), */
/* the struct definition, and the globals used for parameters. */
/* The abs() of all coordinates must be < BIGNUMBER */
/* Code written by Jack Ritter and Lyle Rains. */
#define BIGNUMBER 100000000.0 /* hundred million */
void Sphere::Compute(const SphereInterface &source)
{
Vector3d xmin,xmax,ymin,ymax,zmin,zmax,dia1,dia2;
/* FIRST PASS: find 6 minima/maxima points */
xmin.Set(BIGNUMBER,BIGNUMBER,BIGNUMBER);
xmax.Set(-BIGNUMBER,-BIGNUMBER,-BIGNUMBER);
ymin.Set(BIGNUMBER,BIGNUMBER,BIGNUMBER);
ymax.Set(-BIGNUMBER,-BIGNUMBER,-BIGNUMBER);
zmin.Set(BIGNUMBER,BIGNUMBER,BIGNUMBER);
zmax.Set(-BIGNUMBER,-BIGNUMBER,-BIGNUMBER);
int count = source.GetVertexCount();
for (int i=0; i<count; i++)
{
Vector3d caller_p;
source.GetVertex(i,caller_p);
if (caller_p.GetX()<xmin.GetX()) xmin = caller_p; /* New xminimum point */
if (caller_p.GetX()>xmax.GetX()) xmax = caller_p;
if (caller_p.GetY()<ymin.GetY()) ymin = caller_p;
if (caller_p.GetY()>ymax.GetY()) ymax = caller_p;
if (caller_p.GetZ()<zmin.GetZ()) zmin = caller_p;
if (caller_p.GetZ()>zmax.GetZ()) zmax = caller_p;
}
/* Set xspan = distance between the 2 points xmin & xmax (squared) */
float dx = xmax.GetX() - xmin.GetX();
float dy = xmax.GetY() - xmin.GetY();
float dz = xmax.GetZ() - xmin.GetZ();
float xspan = dx*dx + dy*dy + dz*dz;
/* Same for y & z spans */
dx = ymax.GetX() - ymin.GetX();
dy = ymax.GetY() - ymin.GetY();
dz = ymax.GetZ() - ymin.GetZ();
float yspan = dx*dx + dy*dy + dz*dz;
dx = zmax.GetX() - zmin.GetX();
dy = zmax.GetY() - zmin.GetY();
dz = zmax.GetZ() - zmin.GetZ();
float zspan = dx*dx + dy*dy + dz*dz;
/* Set points dia1 & dia2 to the maximally separated pair */
dia1 = xmin;
dia2 = xmax; /* assume xspan biggest */
float maxspan = xspan;
if (yspan>maxspan)
{
maxspan = yspan;
dia1 = ymin;
dia2 = ymax;
}
if (zspan>maxspan)
{
dia1 = zmin;
dia2 = zmax;
}
/* dia1,dia2 is a diameter of initial sphere */
/* calc initial center */
mCenter.SetX( (dia1.GetX()+dia2.GetX())*0.5f );
mCenter.SetY( (dia1.GetY()+dia2.GetY())*0.5f );
mCenter.SetZ( (dia1.GetZ()+dia2.GetZ())*0.5f );
/* calculate initial radius**2 and radius */
dx = dia2.GetX()-mCenter.GetX(); /* x component of radius vector */
dy = dia2.GetY()-mCenter.GetY(); /* y component of radius vector */
dz = dia2.GetZ()-mCenter.GetZ(); /* z component of radius vector */
mRadius2 = dx*dx + dy*dy + dz*dz;
mRadius = float(sqrt(mRadius2));
/* SECOND PASS: increment current sphere */
for (int j=0; j<count; j++)
{
Vector3d caller_p;
source.GetVertex(j,caller_p);
dx = caller_p.GetX()-mCenter.GetX();
dy = caller_p.GetY()-mCenter.GetY();
dz = caller_p.GetZ()-mCenter.GetZ();
float old_to_p_sq = dx*dx + dy*dy + dz*dz;
if (old_to_p_sq > mRadius2) /* do r**2 test first */
{ /* this point is outside of current sphere */
float old_to_p = float(sqrt(old_to_p_sq));
/* calc radius of new sphere */
mRadius = (mRadius + old_to_p) * 0.5f;
mRadius2 = mRadius*mRadius; /* for next r**2 compare */
float old_to_new = old_to_p - mRadius;
/* calc center of new sphere */
float recip = 1.0f /old_to_p;
float cx = (mRadius*mCenter.GetX() + old_to_new*caller_p.GetX()) * recip;
float cy = (mRadius*mCenter.GetY() + old_to_new*caller_p.GetY()) * recip;
float cz = (mRadius*mCenter.GetZ() + old_to_new*caller_p.GetZ()) * recip;
mCenter.Set(cx,cy,cz);
}
}
}
+878
View File
@@ -0,0 +1,878 @@
/* Copyright (C) John W. Ratcliff, 2001.
* All rights reserved worldwide.
*
* This software is provided "as is" without express or implied
* warranties. You may freely copy and compile this source into
* applications you distribute provided that the copyright text
* below is included in the resulting source code, for example:
* "Portions Copyright (C) John W. Ratcliff, 2001"
*/
#include "StdAfx.h"
#include "spherepack.h"
#if DEMO
int PrintText(int x, int y, int color, char* output, ...);
int DrawLine(int x1, int y1, int x2, int y2, int color);
int DrawCircle(int locx, int locy, int radius, int color);
#endif
SpherePackFactory::SpherePackFactory(int maxspheres, float rootsize, float leafsize, float gravy)
{
NANOBEGIN
maxspheres *= 4; // include room for both trees, the root node and leaf node tree, and the superspheres
mMaxRootSize = rootsize;
mMaxLeafSize = leafsize;
mSuperSphereGravy = gravy;
mIntegrate = new SpherePackFifo(maxspheres);
mRecompute = new SpherePackFifo(maxspheres);
mSpheres.Set(maxspheres); // init pool to hold all possible SpherePack instances.
Vector3d p(0,0,0);
mRoot = mSpheres.GetFreeLink(); // initially empty
mRoot->Init(this,p,6553600,0, false);
mRoot->SetSpherePackFlag(SpherePackFlag(SPF_SUPERSPHERE | SPF_ROOTNODE | SPF_ROOT_TREE));
#if DEMO
mRoot->SetColor(0x00FFFFFF);
#endif
mLeaf = mSpheres.GetFreeLink();; // initially empty
mLeaf->Init(this,p,1638400,0,false);
mLeaf->SetSpherePackFlag(SpherePackFlag(SPF_SUPERSPHERE | SPF_ROOTNODE | SPF_LEAF_TREE));
#if DEMO
mLeaf->SetColor(0x00FFFFFF);
mColorCount = 0;
mColors[0] = 0x00FF0000;
mColors[1] = 0x0000FF00;
mColors[2] = 0x000000FF;
mColors[3] = 0x00FFFF00;
mColors[4] = 0x00FF00FF;
mColors[5] = 0x0000FFFF;
mColors[6] = 0x00FF8080;
mColors[7] = 0x0000FF80;
mColors[8] = 0x000080FF;
mColors[9] = 0x00FFFF80;
mColors[10] = 0x00FF80FF;
mColors[11] = 0x0080FFFF;
#endif
NANOEND
}
SpherePackFactory::~SpherePackFactory(void)
{
delete mIntegrate; // free up integration fifo
delete mRecompute; // free up recomputation fifo.
}
void SpherePackFactory::Process(void)
{
{
// First recompute anybody that needs to be recomputed!!
// When leaf node spheres exit their parent sphere, then the parent sphere needs to be rebalanced. In fact,it may now be empty and
// need to be removed.
// This is the location where (n) number of spheres in the recomputation FIFO are allowed to be rebalanced in the tree.
int maxrecompute = mRecompute->GetCount();
for (int i = 0; i < maxrecompute; ++i)
{
SpherePack * pack = mRecompute->Pop();
if (!pack) break;
pack->SetFifo1(0); // no longer on the fifo!!
bool kill = pack->Recompute(mSuperSphereGravy);
if (kill) Remove(pack);
}
}
{
// Now, process the integration step.
int maxintegrate = mIntegrate->GetCount();
for (int i = 0; i < maxintegrate; ++i)
{
SpherePack * pack = mIntegrate->Pop();
if (!pack)
break;
pack->SetFifo2(0);
if (pack->HasSpherePackFlag(SPF_ROOT_TREE))
Integrate(pack,mRoot,mMaxRootSize); // integrate this one single dude against the root node.
else
Integrate(pack,mLeaf,mMaxLeafSize); // integrate this one single dude against the root node.
}
}
}
SpherePack * SpherePackFactory::AddSphere_(const Vector3d &pos,
float radius,
void *userdata,
bool isSphere,
int flags)
{
SpherePack *pack = mSpheres.GetFreeLink();
assert(pack);
if (pack)
{
if (flags & SPF_ROOT_TREE)
{
pack->Init(this,pos,radius,userdata, isSphere);
pack->SetSpherePackFlag(SPF_ROOT_TREE); // member of the leaf node tree!
AddIntegrate(pack); // add to integration list.
}
else
{
pack->Init(this,pos,radius,userdata, isSphere);
pack->SetSpherePackFlag(SPF_LEAF_TREE); // member of the leaf node tree!
AddIntegrate(pack); // add to integration list.
}
}
return pack;
}
void SpherePackFactory::AddIntegrate(SpherePack *pack)
{
if (pack->HasSpherePackFlag(SPF_ROOT_TREE))
mRoot->AddChild(pack);
else
mLeaf->AddChild(pack);
pack->SetSpherePackFlag(SPF_INTEGRATE); // still needs to be integrated!
SpherePack **fifo = mIntegrate->Push(pack); // add it to the integration stack.
pack->SetFifo2(fifo);
}
void SpherePackFactory::AddRecompute(SpherePack *recompute)
{
if (!recompute->HasSpherePackFlag(SPF_RECOMPUTE))
{
if (recompute->GetChildCount())
{
recompute->SetSpherePackFlag(SPF_RECOMPUTE); // needs to be recalculated!
SpherePack **fifo = mRecompute->Push(recompute);
recompute->SetFifo1(fifo);
}
else
{
Remove(recompute);
}
}
}
void SpherePackFactory::Render(void)
{
#if DEMO
mRoot->Render(mRoot->GetColor());
mLeaf->Render(mLeaf->GetColor());
#endif
}
void SpherePack::Render(unsigned int /*color*/)
{
#if DEMO
if (!HasSpherePackFlag(SPF_ROOTNODE))
{
if (HasSpherePackFlag(SPF_SUPERSPHERE))
{
color = mColor;
}
else
{
if (mParent->HasSpherePackFlag(SPF_ROOTNODE)) color = 0x00FFFFFF;
}
#if DEMO
DrawCircle(int(mCenter.x), int(mCenter.y),int(GetRadius()),color);
#endif
if (HasSpherePackFlag(SPF_SUPERSPHERE))
{
if (HasSpherePackFlag(SPF_LEAF_TREE))
{
#if DEMO
DrawCircle(int(mCenter.x), int(mCenter.y),int(GetRadius()),color);
#endif
#ifdef SPHERELIB_STRICT
if (!sphere->IS_SPHERE)
puts("SpherePack::Render");
#endif
SpherePack *link = (SpherePack *) GetUserData();
link = link->GetParent();
if (link && !link->HasSpherePackFlag(SPF_ROOTNODE))
{
DrawLine(int(mCenter.x), int(mCenter.y),
int(link->mCenter.x), int(link->mCenter.y),
link->GetColor());
}
}
else
{
#if DEMO
DrawCircle(int(mCenter.x), int(mCenter.y),int(GetRadius())+3,color);
#endif
}
}
}
if (mChildren)
{
SpherePack *pack = mChildren;
while (pack)
{
pack->Render(color);
pack = pack->_GetNextSibling();
}
}
#endif
}
bool SpherePack::Recompute(float gravy)
{
if (!mChildren) return true; // kill it!
if (HasSpherePackFlag(SPF_ROOTNODE)) return false; // don't recompute root nodes!
#if 1
// recompute bounding sphere!
Vector3d total(0,0,0);
int count=0;
SpherePack *pack = mChildren;
while (pack)
{
total+=pack->mCenter;
count++;
pack = pack->_GetNextSibling();
}
if (count)
{
float recip = 1.0f / float(count);
total*=recip;
Vector3d oldpos = mCenter;
#ifdef __STATIC_RANGE__
assert(total.IsInStaticRange());
#endif
mCenter = total; // new origin!
float maxradius = 0;
pack = mChildren;
while (pack)
{
float dist = DistanceSquared(pack);
float radius = sqrtf(dist) + pack->GetRadius();
if (radius > maxradius)
{
maxradius = radius;
if ((maxradius+gravy) >= GetRadius())
{
#ifdef __STATIC_RANGE__
assert(oldpos.IsInStaticRange());
#endif
mCenter = oldpos;
ClearSpherePackFlag(SPF_RECOMPUTE);
return false;
}
}
pack = pack->_GetNextSibling();
}
maxradius+=gravy;
SetRadius(maxradius);
// now all children have to recompute binding distance!!
pack = mChildren;
while (pack)
{
pack->ComputeBindingDistance(this);
pack = pack->_GetNextSibling();
}
}
#endif
ClearSpherePackFlag(SPF_RECOMPUTE);
return false;
}
void SpherePack::LostChild(SpherePack *t)
{
assert(mChildCount);
assert(mChildren);
#ifdef _DEBUG // debug validation code.
SpherePack *pack = mChildren;
bool found = false;
while (pack)
{
if (pack == t)
{
assert(!found);
found = true;
}
pack = pack->_GetNextSibling();
}
assert(found);
#endif
// first patch old linked list.. his previous now points to his next
SpherePack *prev = t->_GetPrevSibling();
if (prev)
{
SpherePack *next = t->_GetNextSibling();
prev->SetNextSibling(next); // my previous now points to my next
if (next) next->SetPrevSibling(prev);
// list is patched!
}
else
{
SpherePack *next = t->_GetNextSibling();
mChildren = next;
if (mChildren) mChildren->SetPrevSibling(0);
}
mChildCount--;
if (!mChildCount && HasSpherePackFlag(SPF_SUPERSPHERE))
{
mFactory->Remove(this);
}
}
void SpherePackFactory::Remove(SpherePack*pack)
{
if (pack->HasSpherePackFlag(SPF_ROOTNODE)) return; // CAN NEVER REMOVE THE ROOT NODE EVER!!!
if (pack->HasSpherePackFlag(SPF_SUPERSPHERE) && pack->HasSpherePackFlag(SPF_LEAF_TREE))
{
#ifdef SPHERELIB_STRICT
if (!pack->IS_SPHERE)
puts("SpherePackFactory::Remove");
#endif
SpherePack *link = (SpherePack *) pack->GetUserData();
Remove(link);
}
pack->Unlink();
mSpheres.Release(pack);
}
#if DEMO
unsigned int SpherePackFactory::GetColor(void)
{
unsigned int ret = mColors[mColorCount];
mColorCount++;
if (mColorCount == MAXCOLORS) mColorCount = 0;
return ret;
}
#endif
void SpherePackFactory::Integrate(SpherePack *pack,
SpherePack *supersphere,
float node_size)
{
// ok..time to integrate this sphere with the tree
// first find which supersphere we are closest to the center of
SpherePack *search = supersphere->GetChildren();
SpherePack *nearest1 = 0; // nearest supersphere we are completely
float neardist1 = 1e38f; // enclosed within
SpherePack *nearest2 = 0; // supersphere we must grow the least to
float neardist2 = 1e38f; // add ourselves to.
//int scount = 1;
while (search)
{
if (search->HasSpherePackFlag(SPF_SUPERSPHERE) && !search->HasSpherePackFlag(SPF_ROOTNODE) && search->GetChildCount())
{
float dist = pack->DistanceSquared(search);
if (nearest1)
{
if (dist < neardist1)
{
float d = sqrtf(dist)+pack->GetRadius();
if (d <= search->GetRadius())
{
neardist1 = dist;
nearest1 = search;
}
}
}
else
{
float d = (sqrtf(dist) + pack->GetRadius())-search->GetRadius();
if (d < neardist2)
{
if (d < 0)
{
neardist1 = dist;
nearest1 = search;
}
else
{
neardist2 = d;
nearest2 = search;
}
}
}
}
search = search->_GetNextSibling();
}
// ok...now..on exit let's see what we got.
if (nearest1)
{
// if we are inside an existing supersphere, we are all good!
// we need to detach item from wherever it is, and then add it to
// this supersphere as a child.
pack->Unlink();
nearest1->AddChild(pack);
pack->ComputeBindingDistance(nearest1);
nearest1->Recompute(mSuperSphereGravy);
if (nearest1->HasSpherePackFlag(SPF_LEAF_TREE))
{
#ifdef SPHERELIB_STRICT
if (!nearest1->IS_SPHERE)
puts("SpherePackFactory::Integrate1");
#endif
SpherePack *link = (SpherePack *) nearest1->GetUserData();
link->NewPosRadius(nearest1->GetPos(), nearest1->GetRadius());
}
}
else
{
bool newsphere = true;
if (nearest2)
{
float newsize = neardist2 + nearest2->GetRadius() + mSuperSphereGravy;
if (newsize <= node_size)
{
pack->Unlink();
nearest2->SetRadius(newsize);
nearest2->AddChild(pack);
nearest2->Recompute(mSuperSphereGravy);
pack->ComputeBindingDistance(nearest2);
if (nearest2->HasSpherePackFlag(SPF_LEAF_TREE))
{
#ifdef SPHERELIB_STRICT
if (!nearest2->IS_SPHERE)
puts("SpherePackFactory::Integrate2");
#endif
SpherePack *link = (SpherePack *) nearest2->GetUserData();
link->NewPosRadius(nearest2->GetPos(), nearest2->GetRadius());
}
newsphere = false;
}
}
if (newsphere)
{
assert(supersphere->HasSpherePackFlag(SPF_ROOTNODE));
// we are going to create a new superesphere around this guy!
pack->Unlink();
SpherePack *parent = mSpheres.GetFreeLink();
assert(parent);
parent->Init(this, pack->GetPos(), pack->GetRadius()+mSuperSphereGravy, 0, false);
if (supersphere->HasSpherePackFlag(SPF_ROOT_TREE))
parent->SetSpherePackFlag(SPF_ROOT_TREE);
else
parent->SetSpherePackFlag(SPF_LEAF_TREE);
parent->SetSpherePackFlag(SPF_SUPERSPHERE);
#if DEMO
parent->SetColor(GetColor());
#endif
parent->AddChild(pack);
supersphere->AddChild(parent);
parent->Recompute(mSuperSphereGravy);
pack->ComputeBindingDistance(parent);
if (parent->HasSpherePackFlag(SPF_LEAF_TREE))
{
// need to create parent association!
SpherePack *link = AddSphere_(parent->GetPos(), parent->GetRadius(), parent, true, SPF_ROOT_TREE);
parent->SetUserData(link, true); // hook him up!!
}
}
}
pack->ClearSpherePackFlag(SPF_INTEGRATE); // we've been integrated!
}
void SpherePackFactory::FrustumTest(const Frustum &f,SpherePackCallback *callback)
{
// test case here, just traverse children.
mCallback = callback;
mRoot->VisibilityTest(f,this,VS_PARTIAL);
}
void SpherePack::VisibilityTest(const Frustum &f,SpherePackCallback *callback,ViewState state)
{
if (state == VS_PARTIAL)
{
state = f.ViewVolumeTest(mCenter, GetRadius());
#if DEMO
if (state != VS_OUTSIDE)
{
DrawCircle(int(mCenter.x), int(mCenter.y), int(GetRadius()), 0x404040);
}
#endif
}
if (HasSpherePackFlag(SPF_SUPERSPHERE))
{
if (state == VS_OUTSIDE)
{
if (HasSpherePackFlag(SPF_HIDDEN)) return; // no state change
ClearSpherePackFlag(SpherePackFlag(SPF_INSIDE | SPF_PARTIAL));
SetSpherePackFlag(SPF_HIDDEN);
}
else
{
if (state == VS_INSIDE)
{
if (HasSpherePackFlag(SPF_INSIDE)) return; // no state change
ClearSpherePackFlag(SpherePackFlag(SPF_PARTIAL | SPF_HIDDEN));
SetSpherePackFlag(SPF_INSIDE);
}
else
{
ClearSpherePackFlag(SpherePackFlag(SPF_HIDDEN | SPF_INSIDE));
SetSpherePackFlag(SPF_PARTIAL);
}
}
SpherePack *pack = mChildren;
while (pack)
{
pack->VisibilityTest(f,callback,state);
pack = pack->_GetNextSibling();
}
}
else
{
switch (state)
{
case VS_INSIDE:
if (!HasSpherePackFlag(SPF_INSIDE))
{
ClearSpherePackFlag(SpherePackFlag(SPF_HIDDEN | SPF_PARTIAL));
SetSpherePackFlag(SPF_INSIDE);
callback->VisibilityCallback(f,this,state);
}
break;
case VS_OUTSIDE:
if (!HasSpherePackFlag(SPF_HIDDEN))
{
ClearSpherePackFlag(SpherePackFlag(SPF_INSIDE | SPF_PARTIAL));
SetSpherePackFlag(SPF_HIDDEN);
callback->VisibilityCallback(f,this,state);
}
break;
case VS_PARTIAL:
//if (!HasSpherePackFlag(SPF_PARTIAL))
{
ClearSpherePackFlag(SpherePackFlag(SPF_INSIDE | SPF_HIDDEN));
SetSpherePackFlag(SPF_PARTIAL);
callback->VisibilityCallback(f,this,state);
}
break;
}
}
}
void SpherePackFactory::RayTrace(const Vector3d &p1,
const Vector3d &p2,
SpherePackCallback *callback)
{
// test case here, just traverse children.
Vector3d dir = p2;
float dist = dir.Normalize();
mCallback = callback;
mRoot->RayTrace(p1,dir,dist,this);
}
#include "../EterBase/Debug.h"
void SpherePackFactory::RangeTest(const Vector3d &center,float radius,SpherePackCallback *callback)
{
#ifdef __STATIC_RANGE__
if (!center.IsInStaticRange())
{
TraceError("SpherePackFactory::RangeTest - RANGE ERROR %f, %f, %f",
center.x, center.y, center.z);
assert("SpherePackFactory::RangeTest - RANGE ERROR");
return;
}
#endif
mCallback = callback;
mRoot->RangeTest(center,radius,this,VS_PARTIAL);
}
void SpherePackFactory::PointTest2d(const Vector3d &center, SpherePackCallback *callback)
{
#ifdef __STATIC_RANGE__
if (!center.IsInStaticRange())
{
TraceError("SpherePackFactory::RangeTest2d - RANGE ERROR %f, %f, %f",
center.x, center.y, center.z);
assert("SpherePackFactory::RangeTest2d - RANGE ERROR");
return;
}
#endif
mCallback = callback;
#ifdef SPHERELIB_STRICT
mRoot->PointTest2d(center, this,VS_PARTIAL);
extern bool MAPOUTDOOR_GET_HEIGHT_TRACE;
if (MAPOUTDOOR_GET_HEIGHT_TRACE)
puts("================================================");
#else
mRoot->PointTest2d(center, this,VS_PARTIAL);
#endif
}
void SpherePack::RangeTest(const Vector3d &p,
float distance,
SpherePackCallback *callback,
ViewState state)
{
if (state == VS_PARTIAL)
{
float d = p.Distance(mCenter);
if ((d-distance) > GetRadius()) return;;
if ((GetRadius()+d) < distance) state = VS_INSIDE;
}
if (HasSpherePackFlag(SPF_SUPERSPHERE))
{
#if DEMO
if (state == VS_PARTIAL)
{
DrawCircle(int(mCenter.x), int(mCenter.y), int(GetRadius()), 0x404040);
}
#endif
SpherePack *pack = mChildren;
while (pack)
{
pack->RangeTest(p,distance,callback,state);
pack = pack->_GetNextSibling();
}
}
else
{
callback->RangeTestCallback(p,distance,this,state);
}
}
void SpherePack::PointTest2d(const Vector3d &p,
SpherePackCallback *callback,
ViewState state)
{
if (state == VS_PARTIAL)
{
float dx=p.x-mCenter.x;
float dy=p.y-mCenter.y;
float distSquare = (dx*dx)+(dy*dy);
if (distSquare > GetRadius2()) return;;
if (GetRadius2() < -distSquare) state = VS_INSIDE;
}
if (HasSpherePackFlag(SPF_SUPERSPHERE))
{
#if DEMO
if (state == VS_PARTIAL)
{
DrawCircle(int(mCenter.x), int(mCenter.y), int(GetRadius()), 0x404040);
}
#endif
SpherePack *pack = mChildren;
while (pack)
{
pack->PointTest2d(p, callback, state);
pack = pack->_GetNextSibling();
}
}
else
{
#ifdef SPHERELIB_STRICT
extern bool MAPOUTDOOR_GET_HEIGHT_TRACE;
if (MAPOUTDOOR_GET_HEIGHT_TRACE)
{
float dx=p.x-mCenter.x;
float dy=p.y-mCenter.y;
float distSquare = (dx*dx)+(dy*dy);
printf("--- (%f, %f) dist %f radius %f isSphere %d\n", mCenter.x, mCenter.y, distSquare, GetRadius(), IS_SPHERE);
}
#endif
callback->PointTest2dCallback(p, this, state);
}
}
void SpherePackFactory::RangeTestCallback(const Vector3d &p,float distance,SpherePack *sphere,ViewState state)
{
#ifdef SPHERELIB_STRICT
if (!sphere->IS_SPHERE)
puts("SpherePackFactory::RangeTestCallback");
#endif
SpherePack *link = (SpherePack *) sphere->GetUserData();
if (link) link->RangeTest(p,distance,mCallback,state);
};
void SpherePackFactory::PointTest2dCallback(const Vector3d &p, SpherePack *sphere,ViewState state)
{
#ifdef SPHERELIB_STRICT
if (!sphere->IS_SPHERE)
puts("SpherePackFactory::PointTest2dCallback");
#endif
SpherePack *link = (SpherePack *) sphere->GetUserData();
if (link) link->PointTest2d(p, mCallback,state);
};
void SpherePack::RayTrace(const Vector3d &p1,
const Vector3d &dir,
float distance,
SpherePackCallback *callback)
{
bool hit = false;
if (HasSpherePackFlag(SPF_SUPERSPHERE))
{
hit = RayIntersectionInFront(p1,dir,0);
if (hit)
{
#if DEMO
DrawCircle(int(mCenter.x), int(mCenter.y), int(GetRadius()), 0x404040);
#endif
SpherePack *pack = mChildren;
while (pack)
{
pack->RayTrace(p1,dir,distance,callback);
pack = pack->_GetNextSibling();
}
}
}
else
{
Vector3d sect;
hit = RayIntersection(p1,dir,distance,&sect);
if (hit)
{
callback->RayTraceCallback(p1,dir,distance,sect,this);
}
}
}
void SpherePackFactory::RayTraceCallback(const Vector3d &p1, // source pos of ray
const Vector3d &dir, // direction of ray
float distance, // distance of ray
const Vector3d &/*sect*/, // intersection location
SpherePack *sphere)
{
#ifdef SPHERELIB_STRICT
if (!sphere->IS_SPHERE)
puts("SpherePackFactory::RayTraceCallback");
#endif
SpherePack *link = (SpherePack *) sphere->GetUserData();
if (link) link->RayTrace(p1,dir,distance,mCallback);
};
void SpherePackFactory::Reset(void)
{
mRoot->Reset();
mLeaf->Reset();
}
void SpherePack::Reset(void)
{
ClearSpherePackFlag(SpherePackFlag(SPF_HIDDEN | SPF_PARTIAL | SPF_INSIDE));
SpherePack *pack = mChildren;
while (pack)
{
pack->Reset();
pack = pack->_GetNextSibling();
}
}
void SpherePackFactory::VisibilityCallback(const Frustum &f,SpherePack *sphere,ViewState state)
{
#ifdef SPHERELIB_STRICT
if (!sphere->IS_SPHERE)
puts("SpherePackFactory::VisibilityCallback");
#endif
SpherePack *link = (SpherePack *) sphere->GetUserData();
if (link) link->VisibilityTest(f,mCallback,state);
}
@@ -2775,6 +2775,9 @@ void CInstanceBase::ChangeWeapon(DWORD eWeapon)
if (SetWeapon(eWeapon))
RefreshState(CRaceMotionData::NAME_WAIT, true);
if (IsAffect(AFFECT_GEOMGYEONG))
__Warrior_SetGeomgyeongAffect(true);
}
bool CInstanceBase::ChangeArmor(DWORD dwArmor)
@@ -3028,6 +3031,7 @@ void CInstanceBase::__Warrior_Initialize()
void CInstanceBase::__Initialize()
{
__Warrior_Initialize();
memset(m_byAffectGrade, 0, sizeof(m_byAffectGrade));
__StoneSmoke_Inialize();
__EffectContainer_Initialize();
__InitializeRotationSpeed();
@@ -436,6 +436,7 @@ class CInstanceBase
// 스크립트용 테스트 함수. 나중에 없에자
void SCRIPT_SetAffect(UINT eAffect, bool isVisible);
void __Warrior_SetGeomgyeongAffect(bool isVisible);
float CalculateDistanceSq3d(const TPixelPosition& c_rkPPosDst);
@@ -803,7 +804,7 @@ class CInstanceBase
void __ClearMainInstance();
void __Shaman_SetParalysis(bool isParalysis);
void __Warrior_SetGeomgyeongAffect(bool isVisible);
BYTE __GetAffectGrade(UINT eAffect);
void __Assassin_SetEunhyeongAffect(bool isVisible);
void __SetReviveInvisibilityAffect(bool isVisible);
@@ -1045,6 +1046,7 @@ class CInstanceBase
};
SWarrior m_kWarrior;
BYTE m_byAffectGrade[AFFECT_NUM];
void __Warrior_Initialize();
@@ -340,6 +340,66 @@ bool CInstanceBase::NEW_UseSkill(UINT uSkill, UINT uMot, UINT uMotLoopCount, boo
m_GraphicThingInstance.__OnUseSkill(uMot, uMotLoopCount, isMovingSkill);
BYTE bGrade = (uMot >= 25) ? (uMot / 25) : 0;
if (bGrade > 3)
bGrade = 3;
if (uSkill != 0)
{
switch (uSkill)
{
case 3: m_byAffectGrade[AFFECT_JEONGWI] = bGrade; break;
case 4: m_byAffectGrade[AFFECT_GEOMGYEONG] = bGrade; break;
case 19: m_byAffectGrade[AFFECT_CHEONGEUN] = bGrade; break;
case 34: m_byAffectGrade[AFFECT_EUNHYEONG] = bGrade; break;
case 49: m_byAffectGrade[AFFECT_GYEONGGONG] = bGrade; break;
case 63: m_byAffectGrade[AFFECT_GWIGEOM] = bGrade; break;
case 64: m_byAffectGrade[AFFECT_GONGPO] = bGrade; break;
case 65: m_byAffectGrade[AFFECT_JUMAGAP] = bGrade; break;
case 78: m_byAffectGrade[AFFECT_MUYEONG] = bGrade; break;
case 79: m_byAffectGrade[AFFECT_HEUKSIN] = bGrade; break;
case 94: m_byAffectGrade[AFFECT_HOSIN] = bGrade; break;
case 95: m_byAffectGrade[AFFECT_BOHO] = bGrade; break;
case 96: m_byAffectGrade[AFFECT_GICHEON] = bGrade; break;
case 110: m_byAffectGrade[AFFECT_KWAESOK] = bGrade; break;
case 111: m_byAffectGrade[AFFECT_JEUNGRYEOK] = bGrade; break;
}
}
else
{
UINT uMotSub = uMot % 25;
int iJob = RaceToJob(GetRace());
switch (iJob)
{
case NRaceData::JOB_WARRIOR:
if (uMotSub == 3) m_byAffectGrade[AFFECT_JEONGWI] = bGrade;
else if (uMotSub == 4) m_byAffectGrade[AFFECT_GEOMGYEONG] = bGrade;
else if (uMotSub == 19) m_byAffectGrade[AFFECT_CHEONGEUN] = bGrade;
break;
case NRaceData::JOB_ASSASSIN:
if (uMotSub == 4) m_byAffectGrade[AFFECT_EUNHYEONG] = bGrade;
else if (uMotSub == 19) m_byAffectGrade[AFFECT_GYEONGGONG] = bGrade;
break;
case NRaceData::JOB_SURA:
if (uMotSub == 3) m_byAffectGrade[AFFECT_GWIGEOM] = bGrade;
else if (uMotSub == 4) m_byAffectGrade[AFFECT_GONGPO] = bGrade;
else if (uMotSub == 5) m_byAffectGrade[AFFECT_JUMAGAP] = bGrade;
else if (uMotSub == 18) m_byAffectGrade[AFFECT_MUYEONG] = bGrade;
else if (uMotSub == 19) m_byAffectGrade[AFFECT_HEUKSIN] = bGrade;
break;
case NRaceData::JOB_SHAMAN:
if (uMotSub == 4) m_byAffectGrade[AFFECT_HOSIN] = bGrade;
else if (uMotSub == 5) m_byAffectGrade[AFFECT_BOHO] = bGrade;
else if (uMotSub == 6) m_byAffectGrade[AFFECT_GICHEON] = bGrade;
else if (uMotSub == 20) m_byAffectGrade[AFFECT_KWAESOK] = bGrade;
else if (uMotSub == 21) m_byAffectGrade[AFFECT_JEUNGRYEOK] = bGrade;
break;
}
}
if (IsAffect(AFFECT_GEOMGYEONG))
__Warrior_SetGeomgyeongAffect(true);
if (uMotLoopCount > 0)
m_GraphicThingInstance.SetMotionLoopCount(uMotLoopCount);
@@ -825,6 +825,29 @@ void CInstanceBase::__Shaman_SetParalysis(bool isParalysis)
BYTE CInstanceBase::__GetAffectGrade(UINT eAffect)
{
if (__IsMainInstance())
{
DWORD dwSkillIndex = 0;
if (CPythonPlayer::Instance().AffectIndexToSkillIndex(eAffect, &dwSkillIndex))
{
DWORD dwSkillSlotIndex = 0;
if (CPythonPlayer::Instance().GetSkillSlotIndex(dwSkillIndex, &dwSkillSlotIndex))
{
int iGrade = CPythonPlayer::Instance().GetSkillGrade(dwSkillSlotIndex);
if (iGrade >= 0 && iGrade < 4)
return (BYTE)iGrade;
}
}
}
if (eAffect < AFFECT_NUM)
return m_byAffectGrade[eAffect];
return 0;
}
void CInstanceBase::__Warrior_SetGeomgyeongAffect(bool isVisible)
{
if (isVisible)
@@ -836,10 +859,38 @@ void CInstanceBase::__Warrior_SetGeomgyeongAffect(bool isVisible)
__DetachEffect(m_kWarrior.m_dwGeomgyeongEffect);
m_GraphicThingInstance.SetReachScale(1.5f);
if (m_GraphicThingInstance.IsTwoHandMode())
m_kWarrior.m_dwGeomgyeongEffect=__AttachEffect(EFFECT_WEAPON+WEAPON_TWOHAND);
BYTE bGrade = __GetAffectGrade(AFFECT_GEOMGYEONG);
if (bGrade >= 4)
bGrade = 3;
static const char * c_szGeomSpearFiles[4] = {
"d:/ymir work/pc/warrior/effect/geom_spear_loop.mse",
"d:/ymir work/pc/warrior/effect/geom_2_spear_loop.mse",
"d:/ymir work/pc/warrior/effect/geom_3_spear_loop.mse",
"d:/ymir work/pc/warrior/effect/geom_4_spear_loop.mse"
};
static const char * c_szGeomSwordFiles[4] = {
"d:/ymir work/pc/warrior/effect/geom_sword_loop.mse",
"d:/ymir work/pc/warrior/effect/geom_2_sword_loop.mse",
"d:/ymir work/pc/warrior/effect/geom_3_sword_loop.mse",
"d:/ymir work/pc/warrior/effect/geom_4_sword_loop.mse"
};
const char * c_szFileName = m_GraphicThingInstance.IsTwoHandMode() ? c_szGeomSpearFiles[bGrade] : c_szGeomSwordFiles[bGrade];
DWORD dwEffectCRC = 0;
if (CEffectManager::Instance().RegisterEffect2(c_szFileName, &dwEffectCRC, true))
{
m_kWarrior.m_dwGeomgyeongEffect = m_GraphicThingInstance.AttachEffectByID(0, "equip_right_hand", dwEffectCRC);
}
else
m_kWarrior.m_dwGeomgyeongEffect=__AttachEffect(EFFECT_WEAPON+WEAPON_ONEHAND);
{
if (m_GraphicThingInstance.IsTwoHandMode())
m_kWarrior.m_dwGeomgyeongEffect = __AttachEffect(EFFECT_WEAPON + WEAPON_TWOHAND);
else
m_kWarrior.m_dwGeomgyeongEffect = __AttachEffect(EFFECT_WEAPON + WEAPON_ONEHAND);
}
}
else
{
@@ -1033,6 +1084,129 @@ DWORD CInstanceBase::__AttachEffect(UINT eEftType)
if (eEftType>=EFFECT_NUM)
return 0;
if (eEftType >= EFFECT_AFFECT && eEftType < EFFECT_AFFECT_END)
{
UINT eAffect = eEftType - EFFECT_AFFECT;
BYTE bGrade = __GetAffectGrade(eAffect);
if (bGrade >= 4)
bGrade = 3;
const char * c_szFileName = NULL;
const char * c_szBoneName = NULL;
switch (eAffect)
{
case AFFECT_GWIGEOM:
{
static const char * c_szGwigeomFiles[4] = {
"d:/ymir work/pc/sura/effect/gwigeom_loop.mse",
"d:/ymir work/pc/sura/effect/gwigeom_2_loop.mse",
"d:/ymir work/pc/sura/effect/gwigeom_3_loop.mse",
"d:/ymir work/pc/sura/effect/gwigeom_4_loop.mse"
};
c_szFileName = c_szGwigeomFiles[bGrade];
c_szBoneName = "Bip01 R Finger2";
break;
}
case AFFECT_GONGPO:
{
static const char * c_szFearFiles[4] = {
"d:/ymir work/pc/sura/effect/fear_loop.mse",
"d:/ymir work/pc/sura/effect/fear_2_loop.mse",
"d:/ymir work/pc/sura/effect/fear_3_loop.mse",
"d:/ymir work/pc/sura/effect/fear_3_loop.mse"
};
c_szFileName = c_szFearFiles[bGrade];
c_szBoneName = "";
break;
}
case AFFECT_JUMAGAP:
{
static const char * c_szJumagapFiles[4] = {
"d:/ymir work/pc/sura/effect/jumagap_loop.mse",
"d:/ymir work/pc/sura/effect/jumagap_2_loop.mse",
"d:/ymir work/pc/sura/effect/jumagap_3_loop.mse",
"d:/ymir work/pc/sura/effect/jumagap_4_loop.mse"
};
c_szFileName = c_szJumagapFiles[bGrade];
c_szBoneName = "";
break;
}
case AFFECT_HEUKSIN:
{
static const char * c_szHeuksinFiles[4] = {
"d:/ymir work/pc/sura/effect/heuksin_loop.mse",
"d:/ymir work/pc/sura/effect/heuksin_2_loop.mse",
"d:/ymir work/pc/sura/effect/heuksin_3_loop.mse",
"d:/ymir work/pc/sura/effect/heuksin_4_loop.mse"
};
c_szFileName = c_szHeuksinFiles[bGrade];
c_szBoneName = "";
break;
}
case AFFECT_HOSIN:
{
c_szFileName = (bGrade >= 3) ? "d:/ymir work/pc/shaman/effect/3hosin_loop_4.mse" : "d:/ymir work/pc/shaman/effect/3hosin_loop.mse";
c_szBoneName = "";
break;
}
case AFFECT_BOHO:
{
c_szFileName = (bGrade >= 3) ? "d:/ymir work/pc/shaman/effect/boho_loop_4.mse" : "d:/ymir work/pc/shaman/effect/boho_loop.mse";
c_szBoneName = "";
break;
}
case AFFECT_GYEONGGONG:
{
static const char * c_szGyeonggongFiles[4] = {
"d:/ymir work/pc/assassin/effect/gyeonggong_loop.mse",
"d:/ymir work/pc/assassin/effect/gyeonggong_2_loop.mse",
"d:/ymir work/pc/assassin/effect/gyeonggong_3_loop.mse",
"d:/ymir work/pc/assassin/effect/gyeonggong_4_loop.mse"
};
c_szFileName = c_szGyeonggongFiles[bGrade];
c_szBoneName = "";
break;
}
case AFFECT_CHEONGEUN:
{
static const char * c_szCheongeunFiles[4] = {
"d:/ymir work/pc/warrior/effect/gyeokgongjang_loop.mse",
"d:/ymir work/pc/warrior/effect/gyeokgongjang_2_loop.mse",
"d:/ymir work/pc/warrior/effect/gyeokgongjang_3_loop.mse",
"d:/ymir work/pc/warrior/effect/gyeokgongjang_3_loop.mse"
};
c_szFileName = c_szCheongeunFiles[bGrade];
c_szBoneName = "";
break;
}
case AFFECT_GICHEON:
{
c_szFileName = (bGrade >= 3) ? "d:/ymir work/pc/shaman/effect/6gicheon_hand_4.mse" : "d:/ymir work/pc/shaman/effect/6gicheon_hand.mse";
c_szBoneName = "Bip01 R Hand";
break;
}
case AFFECT_JEUNGRYEOK:
{
c_szFileName = (bGrade >= 3) ? "d:/ymir work/pc/shaman/effect/jeungryeok_hand_4.mse" : "d:/ymir work/pc/shaman/effect/jeungryeok_hand.mse";
c_szBoneName = "Bip01 L Hand";
break;
}
default:
break;
}
if (c_szFileName)
{
DWORD dwEffectCRC = 0;
if (CEffectManager::Instance().RegisterEffect2(c_szFileName, &dwEffectCRC, true))
{
const char * bone = (c_szBoneName && c_szBoneName[0]) ? c_szBoneName : NULL;
return m_GraphicThingInstance.AttachEffectByID(0, bone, dwEffectCRC);
}
}
}
if (ms_astAffectEffectAttachBone[eEftType].empty())
{
return m_GraphicThingInstance.AttachEffectByID(0, NULL, ms_adwCRCAffectEffect[eEftType]);
@@ -274,17 +274,7 @@ class CPythonApplication final : public CMSApplication, public CInputKeyboard, p
int m_nLeft, m_nRight, m_nTop, m_nBottom;
protected:
LRESULT WindowProcedure(HWND hWnd, UINT uiMsg, WPARAM wParam, LPARAM lParam);
void OnCameraUpdate();
void OnUIUpdate();
void OnUIRender();
void OnMouseUpdate();
void OnMouseRender();
public:
void OnMouseWheel(int nLen);
void OnMouseMove(int x, int y);
void OnMouseMiddleButtonDown(int x, int y);
@@ -299,6 +289,17 @@ class CPythonApplication final : public CMSApplication, public CInputKeyboard, p
void OnKeyUp(int iIndex);
void OnIMEKeyDown(int iIndex);
protected:
LRESULT WindowProcedure(HWND hWnd, UINT uiMsg, WPARAM wParam, LPARAM lParam);
void OnCameraUpdate();
void OnUIUpdate();
void OnUIRender();
void OnMouseUpdate();
void OnMouseRender();
int CheckDeviceState();
BOOL __IsContinuousChangeTypeCursor(int iCursorNum);
@@ -254,10 +254,34 @@ void CPythonBackground::__CreateProperty()
{
// The exported client runs with player settings as CWD; its read-only property pack
// lives beside the other bundled 40250 packs. Use the same root registered by PackBackend.
std::string property_pack = "pack/property";
std::vector<std::string> candidates;
if (const char* client = getenv("MT_40250_CLIENT"))
property_pack = std::string(client) + "/pack/Property";
m_PropertyManager.Initialize(property_pack.c_str());
{
candidates.push_back(std::string(client) + "/pack/Property");
candidates.push_back(std::string(client) + "/pack/property");
}
candidates.push_back("pack/Property");
candidates.push_back("pack/property");
candidates.push_back("Client/pack/Property");
candidates.push_back("Client/pack/property");
bool initialized = false;
for (const auto& path : candidates)
{
std::string eix = path + ".eix";
if (_access(eix.c_str(), 0) == 0)
{
if (m_PropertyManager.Initialize(path.c_str()))
{
initialized = true;
break;
}
}
}
if (!initialized && !candidates.empty())
{
m_PropertyManager.Initialize(candidates.front().c_str());
}
}
}
@@ -735,6 +735,11 @@ void CPythonCharacterManager::__SortPickedActorList()
std::sort(m_kVct_pkInstPicked.begin(), m_kVct_pkInstPicked.end(), kLess);
}
void CPythonCharacterManager::Pick()
{
__NEW_Pick();
}
void CPythonCharacterManager::__NEW_Pick()
{
__UpdateSortPickedActorList();
@@ -773,31 +778,10 @@ void CPythonCharacterManager::__NEW_Pick()
}
}
// 못찾겠으면 걍 순서대로
{
std::vector<CInstanceBase*>::iterator f;
for (f=m_kVct_pkInstPicked.begin(); f!=m_kVct_pkInstPicked.end(); ++f)
{
CInstanceBase* pkInstEach=*f;
if (pkInstEach!=pkInstMain)
{
if (m_pkInstPick)
if (m_pkInstPick!=pkInstEach)
m_pkInstPick->OnUnselected();
if (pkInstEach->CanPickInstance())
{
m_pkInstPick = pkInstEach;
m_pkInstPick->OnSelected();
return;
}
}
}
}
if (pkInstMain)
if (pkInstMain->CanPickInstance())
if (m_kVct_pkInstPicked.end() != std::find(m_kVct_pkInstPicked.begin(), m_kVct_pkInstPicked.end(), pkInstMain))
if (pkInstMain->IntersectBoundingBox())
{
if (m_pkInstPick)
if (m_pkInstPick!=pkInstMain)
@@ -85,6 +85,7 @@ class CPythonCharacterManager : public CSingleton<CPythonCharacterManager>, publ
CInstanceBase * GetInstancePtrByName(const char *name);
// Pick
void Pick();
int PickAll();
CInstanceBase * GetCloseInstance(CInstanceBase * pInstance);
@@ -1037,6 +1037,25 @@ void CPythonPlayer::SetSkillLevel_(DWORD dwSkillIndex, DWORD dwSkillGrade, DWORD
m_playerStatus.aSkill[dwSlotIndex].fcurEfficientPercentage = LocaleService_GetSkillPower(dwSkillLevel)/100.0f;
m_playerStatus.aSkill[dwSlotIndex].fnextEfficientPercentage = LocaleService_GetSkillPower(dwSkillLevel+1)/100.0f;
CInstanceBase * pkInstMain = NEW_GetMainActorPtr();
if (pkInstMain)
{
if (dwSkillIndex == 4 && pkInstMain->IsAffect(CInstanceBase::AFFECT_GEOMGYEONG))
{
pkInstMain->__Warrior_SetGeomgyeongAffect(true);
}
else
{
for (std::map<DWORD, DWORD>::const_iterator it = m_kMap_dwAffectIndexToSkillIndex.begin(); it != m_kMap_dwAffectIndexToSkillIndex.end(); ++it)
{
if (it->second == dwSkillIndex && pkInstMain->IsAffect(it->first))
{
pkInstMain->SCRIPT_SetAffect(it->first, false);
pkInstMain->SCRIPT_SetAffect(it->first, true);
}
}
}
}
}
void CPythonPlayer::SetSkillCoolTime(DWORD dwSkillIndex)
@@ -12,7 +12,7 @@ std::map<std::string, DWORD> CPythonSkill::SSkillData::ms_NewMinStatusNameMap;
std::map<std::string, DWORD> CPythonSkill::SSkillData::ms_NewMaxStatusNameMap;
DWORD CPythonSkill::SSkillData::ms_dwTimeIncreaseSkillNumber = 0;
BOOL SKILL_EFFECT_UPGRADE_ENABLE = FALSE;
BOOL SKILL_EFFECT_UPGRADE_ENABLE = TRUE;
int SplitLine(const char * c_szText, CTokenVector* pstTokenVector, const char * c_szDelimeter)
{
+1
View File
@@ -757,6 +757,7 @@ struct IDirect3DDevice8
virtual ULONG Release() = 0;
virtual HRESULT GetDeviceCaps(D3DCAPS8* pCaps) = 0;
virtual HRESULT GetViewport(D3DVIEWPORT8* pViewport) = 0;
virtual HRESULT SetViewport(const D3DVIEWPORT8* pViewport) = 0;
virtual UINT GetAvailableTextureMem() = 0;
virtual HRESULT BeginScene() = 0;
virtual HRESULT EndScene() = 0;
+369 -70
View File
@@ -12,7 +12,13 @@
#include "platform/EterLib/UIRenderCommands.h"
#include "platform/EterLib/RenderCommands3D.h"
#include "platform/MilesLib/AudioCommands.h"
#include "platform/PackBackend.h"
#include "../../native_render/draw_capture.h"
#include <cstdlib>
#include <cstring>
#include <string>
#include <type_traits>
#include <unordered_map>
#endif
#include <godot_cpp/classes/image.hpp>
@@ -23,6 +29,8 @@
#include <godot_cpp/variant/packed_int32_array.hpp>
#include <godot_cpp/variant/packed_vector2_array.hpp>
#include <godot_cpp/variant/packed_vector3_array.hpp>
#include <godot_cpp/variant/string_name.hpp>
using namespace godot;
@@ -82,53 +90,188 @@ void Metin2PythonHost::ui_mouse_move(int x, int y) { PythonBoot::UIMouseMove(x,
void Metin2PythonHost::ui_mouse_button(int button, bool pressed, int x, int y) {
PythonBoot::UIMouseButton(button, pressed, x, y);
}
void Metin2PythonHost::ui_mouse_wheel(int delta) { PythonBoot::UIMouseWheel(delta); }
void Metin2PythonHost::ui_key(int key, bool pressed) { PythonBoot::UIKey(key, pressed); }
void Metin2PythonHost::ui_char(int codepoint) { PythonBoot::UIChar(unsigned(codepoint)); }
void Metin2PythonHost::ui_ime_key(int vkey) { PythonBoot::UIIMEKeyDown(vkey); }
void Metin2PythonHost::ui_update() { PythonBoot::UIUpdate(); }
namespace {
Dictionary ui_command_item(const UIRenderCommand &command) {
static const StringName s_kind("kind");
static const StringName s_x1("x1");
static const StringName s_y1("y1");
static const StringName s_x2("x2");
static const StringName s_y2("y2");
static const StringName s_argb("argb");
static const StringName s_end_argb("end_argb");
static const StringName s_clip_x1("clip_x1");
static const StringName s_clip_y1("clip_y1");
static const StringName s_clip_x2("clip_x2");
static const StringName s_clip_y2("clip_y2");
static const StringName s_text("text");
static const StringName s_quad("quad");
static const StringName s_uv("uv");
static const StringName s_blend("blend");
static const StringName s_mask("mask");
static const StringName s_mask_uv("mask_uv");
static const StringName s_behind_3d("behind_3d");
static const StringName s_val_bar("bar");
static const StringName s_val_gradient_bar("gradient_bar");
static const StringName s_val_line("line");
static const StringName s_val_image("image");
static const StringName s_val_text("text");
Dictionary item;
item[s_kind] = command.kind == UIRenderCommand::GradientBar ? s_val_gradient_bar :
(command.kind == UIRenderCommand::Bar ? s_val_bar :
(command.kind == UIRenderCommand::Line ? s_val_line :
(command.kind == UIRenderCommand::Image ? s_val_image : s_val_text)));
item[s_x1] = command.x1;
item[s_y1] = command.y1;
item[s_x2] = command.x2;
item[s_y2] = command.y2;
item[s_argb] = static_cast<int64_t>(command.argb);
if (command.kind == UIRenderCommand::GradientBar)
item[s_end_argb] = static_cast<int64_t>(command.end_argb);
item[s_clip_x1] = command.clip_x1;
item[s_clip_y1] = command.clip_y1;
item[s_clip_x2] = command.clip_x2;
item[s_clip_y2] = command.clip_y2;
if (command.kind == UIRenderCommand::Text || command.kind == UIRenderCommand::Image)
item[s_text] = String::utf8(command.text.c_str());
if (command.quad) {
PackedVector2Array quad;
quad.resize(4);
Vector2 *qptr = quad.ptrw();
for (int i = 0; i < 4; ++i)
qptr[i] = Vector2(command.qx[i], command.qy[i]);
item[s_quad] = quad;
PackedVector2Array uv;
uv.resize(4);
Vector2 *uvptr = uv.ptrw();
uvptr[0] = Vector2(command.su, command.sv);
uvptr[1] = Vector2(command.eu, command.sv);
uvptr[2] = Vector2(command.su, command.ev);
uvptr[3] = Vector2(command.eu, command.ev);
item[s_uv] = uv;
item[s_blend] = command.blend;
if (!command.mask.empty()) {
item[s_mask] = String::utf8(command.mask.c_str());
PackedVector2Array mask_uv;
mask_uv.resize(4);
Vector2 *mptr = mask_uv.ptrw();
for (int i = 0; i < 4; ++i)
mptr[i] = Vector2(command.mu[i], command.mv[i]);
item[s_mask_uv] = mask_uv;
}
}
item[s_behind_3d] = command.behind_3d;
return item;
}
} // namespace
Array Metin2PythonHost::ui_render_commands() {
PythonBoot::UIRender();
static std::uint64_t cached_frame_id = UINT64_MAX;
static Array cached_commands;
const std::uint64_t frame_id = UIRenderFrameId();
if (cached_frame_id == frame_id)
return cached_commands;
const auto &commands = UIRenderCommands();
Array out;
for (const auto &command : UIRenderCommands()) {
Dictionary item;
item["kind"] = command.kind == UIRenderCommand::GradientBar ? "gradient_bar" :
(command.kind == UIRenderCommand::Bar ? "bar" :
(command.kind == UIRenderCommand::Line ? "line" :
(command.kind == UIRenderCommand::Image ? "image" : "text")));
item["x1"] = command.x1;
item["y1"] = command.y1;
item["x2"] = command.x2;
item["y2"] = command.y2;
item["argb"] = static_cast<int64_t>(command.argb);
if (command.kind == UIRenderCommand::GradientBar)
item["end_argb"] = static_cast<int64_t>(command.end_argb);
item["clip_x1"] = command.clip_x1;
item["clip_y1"] = command.clip_y1;
item["clip_x2"] = command.clip_x2;
item["clip_y2"] = command.clip_y2;
if (command.kind == UIRenderCommand::Text || command.kind == UIRenderCommand::Image)
item["text"] = String::utf8(command.text.c_str());
if (command.quad) {
PackedVector2Array quad;
for (int i = 0; i < 4; ++i)
quad.push_back(Vector2(command.qx[i], command.qy[i]));
item["quad"] = quad;
item["uv"] = PackedVector2Array({Vector2(command.su, command.sv), Vector2(command.eu, command.sv),
Vector2(command.su, command.ev), Vector2(command.eu, command.ev)});
item["blend"] = command.blend;
if (!command.mask.empty()) {
item["mask"] = String::utf8(command.mask.c_str());
PackedVector2Array mask_uv;
for (int i = 0; i < 4; ++i)
mask_uv.push_back(Vector2(command.mu[i], command.mv[i]));
item["mask_uv"] = mask_uv;
}
}
out.push_back(item);
}
out.resize(static_cast<int64_t>(commands.size()));
for (size_t idx = 0; idx < commands.size(); ++idx)
out[static_cast<int64_t>(idx)] = ui_command_item(commands[idx]);
cached_frame_id = frame_id;
cached_commands = out;
return out;
}
Array Metin2PythonHost::ui_render_commands_batched() {
PythonBoot::UIRender();
static std::uint64_t cached_frame_id = UINT64_MAX;
static Array cached_commands;
const std::uint64_t frame_id = UIRenderFrameId();
if (cached_frame_id == frame_id)
return cached_commands;
const auto &commands = UIRenderCommands();
const auto batchable = [](const UIRenderCommand &c) {
return c.kind == UIRenderCommand::Image && c.quad && c.text.rfind("mem:", 0) == 0 &&
c.mask.empty() && c.blend == 0 && c.x1 >= c.clip_x1 && c.y1 >= c.clip_y1 &&
c.x2 <= c.clip_x2 && c.y2 <= c.clip_y2;
};
Array out;
for (size_t i = 0; i < commands.size();) {
const UIRenderCommand &first = commands[i];
if (!batchable(first)) {
out.push_back(ui_command_item(first));
++i;
continue;
}
size_t end = i + 1;
while (end < commands.size() && batchable(commands[end]) &&
commands[end].text == first.text && commands[end].behind_3d == first.behind_3d)
++end;
if (end == i + 1) {
out.push_back(ui_command_item(first));
i = end;
continue;
}
const int64_t count = static_cast<int64_t>(end - i);
PackedVector2Array points, uvs;
PackedColorArray colors;
PackedInt32Array indices;
points.resize(count * 4);
uvs.resize(count * 4);
colors.resize(count * 4);
indices.resize(count * 6);
Vector2 *point_data = points.ptrw(), *uv_data = uvs.ptrw();
Color *color_data = colors.ptrw();
int32_t *index_data = indices.ptrw();
for (int64_t j = 0; j < count; ++j) {
const UIRenderCommand &c = commands[i + static_cast<size_t>(j)];
const int64_t v = j * 4, t = j * 6;
point_data[v] = Vector2(c.qx[0], c.qy[0]);
point_data[v + 1] = Vector2(c.qx[1], c.qy[1]);
point_data[v + 2] = Vector2(c.qx[3], c.qy[3]);
point_data[v + 3] = Vector2(c.qx[2], c.qy[2]);
uv_data[v] = Vector2(c.su, c.sv);
uv_data[v + 1] = Vector2(c.eu, c.sv);
uv_data[v + 2] = Vector2(c.eu, c.ev);
uv_data[v + 3] = Vector2(c.su, c.ev);
const Color color(((c.argb >> 16) & 255) / 255.0f, ((c.argb >> 8) & 255) / 255.0f,
(c.argb & 255) / 255.0f, ((c.argb >> 24) & 255) / 255.0f);
for (int k = 0; k < 4; ++k)
color_data[v + k] = color;
const int32_t base = static_cast<int32_t>(v);
index_data[t] = base;
index_data[t + 1] = base + 1;
index_data[t + 2] = base + 2;
index_data[t + 3] = base;
index_data[t + 4] = base + 2;
index_data[t + 5] = base + 3;
}
Dictionary item;
item["kind"] = StringName("glyph_batch");
item["text"] = String::utf8(first.text.c_str());
item["behind_3d"] = first.behind_3d;
item["points"] = points;
item["uvs"] = uvs;
item["colors"] = colors;
item["indices"] = indices;
item["glyph_count"] = count;
out.push_back(item);
i = end;
}
cached_frame_id = frame_id;
cached_commands = out;
return out;
}
bool Metin2PythonHost::has_3d_draws() { return !Render3DDraws().empty(); }
Ref<Image> Metin2PythonHost::memory_texture(const String &name, int64_t known_revision) {
UIMemoryTexture texture;
if (!UIRenderMemoryTexture(name.utf8().get_data(), &texture) || texture.width <= 0 || texture.height <= 0)
@@ -162,12 +305,107 @@ Color argb_color(std::uint32_t argb) {
((argb >> 24) & 255) / 255.0f);
}
bool read_pack_texture(const std::string &vpath, std::vector<std::uint8_t> &bytes) {
if (vpath.empty() || !mtpack40250::ready())
return false;
std::string norm = vpath;
for (char &ch : norm)
if (ch == '\\')
ch = '/';
std::string stripped = norm;
if (stripped.size() >= 2 && stripped[1] == ':')
stripped = stripped.substr(2);
while (!stripped.empty() && stripped.front() == '/')
stripped.erase(stripped.begin());
auto lower = [](std::string s) {
for (char &ch : s)
if (ch >= 'A' && ch <= 'Z')
ch = static_cast<char>(ch - 'A' + 'a');
return s;
};
for (const std::string &candidate : {
norm, stripped, "d:/" + stripped,
lower(norm), lower(stripped), lower("d:/" + stripped)}) {
if (mtpack40250::read(candidate, bytes) && !bytes.empty())
return true;
}
return false;
}
} // namespace
Array Metin2PythonHost::render3d_draws() {
struct CachedGeometry {
std::uint64_t revision;
std::uint64_t last_used;
Dictionary arrays;
};
static std::unordered_map<std::uint64_t, CachedGeometry> geometry_cache;
static std::uint64_t extraction = 0;
++extraction;
const auto &draws = Render3DDraws();
static bool capture_written = false;
if (!capture_written) {
const char *capture_path = std::getenv("MT_NATIVE_CAPTURE_PATH");
const char *minimum_text = std::getenv("MT_NATIVE_CAPTURE_MIN_DRAWS");
const unsigned long minimum = minimum_text ? std::strtoul(minimum_text, nullptr, 10) : 100UL;
if (capture_path && *capture_path && draws.size() >= minimum) {
capture_written = true;
try {
std::unordered_map<std::string, std::vector<std::uint8_t>> textures;
auto add_texture = [&](const std::string &name) {
if (name.empty() || textures.count(name))
return;
if (name.rfind("mem:", 0) == 0) {
UIMemoryTexture mem_tex;
if (UIRenderMemoryTexture(name, &mem_tex) && mem_tex.width > 0 && mem_tex.height > 0) {
auto mtra = native_draw_capture::encode_raw_argb_as_mtra(
static_cast<std::uint32_t>(mem_tex.width),
static_cast<std::uint32_t>(mem_tex.height),
mem_tex.argb.data());
if (!mtra.empty())
textures.emplace(name, std::move(mtra));
}
return;
}
std::vector<std::uint8_t> bytes;
if (read_pack_texture(name, bytes))
textures.emplace(name, std::move(bytes));
};
for (const Render3DDraw &draw : draws) {
add_texture(draw.texture0);
add_texture(draw.texture1);
}
const auto &ui_commands = UIRenderCommands();
for (const UIRenderCommand &cmd : ui_commands) {
if (cmd.kind == UIRenderCommand::Image) {
add_texture(cmd.text);
add_texture(cmd.mask);
}
}
unsigned ui_w = 0, ui_h = 0;
UIRenderGetSize(&ui_w, &ui_h);
native_draw_capture::write(
capture_path, draws, textures,
ui_w ? ui_w : 960u, ui_h ? ui_h : 640u, ui_commands);
UtilityFunctions::print(
"native draw capture: ", capture_path,
" draws=", static_cast<int64_t>(draws.size()),
" ui_commands=", static_cast<int64_t>(ui_commands.size()),
" textures=", static_cast<int64_t>(textures.size()));
} catch (const std::exception &error) {
UtilityFunctions::printerr("native draw capture failed: ", error.what());
}
}
}
Array out;
for (const Render3DDraw &draw : Render3DDraws()) {
out.resize(static_cast<int64_t>(draws.size()));
int64_t draw_index = 0;
for (const Render3DDraw &draw : draws) {
Dictionary item;
item["geometry_key"] = static_cast<int64_t>(draw.geometry_key);
item["geometry_revision"] = static_cast<int64_t>(draw.geometry_revision);
item["world"] = floats(draw.world, 16);
item["view"] = floats(draw.view, 16);
item["proj"] = floats(draw.proj, 16);
@@ -176,43 +414,73 @@ Array Metin2PythonHost::render3d_draws() {
item["pretransformed"] = draw.pretransformed;
item["lines"] = draw.lines;
PackedVector3Array positions;
positions.resize(static_cast<int64_t>(draw.positions.size() / 3));
for (int64_t i = 0; i < positions.size(); ++i)
positions[i] = Vector3(draw.positions[i * 3], draw.positions[i * 3 + 1], draw.positions[i * 3 + 2]);
item["positions"] = positions;
if (!draw.rhw.empty())
item["rhw"] = floats(draw.rhw.data(), static_cast<int>(draw.rhw.size()));
if (!draw.normals.empty()) {
PackedVector3Array normals;
normals.resize(static_cast<int64_t>(draw.normals.size() / 3));
for (int64_t i = 0; i < normals.size(); ++i)
normals[i] = Vector3(draw.normals[i * 3], draw.normals[i * 3 + 1], draw.normals[i * 3 + 2]);
item["normals"] = normals;
Dictionary geometry;
if (draw.geometry_key != 0) {
auto cached = geometry_cache.find(draw.geometry_key);
if (cached != geometry_cache.end() && cached->second.revision == draw.geometry_revision) {
cached->second.last_used = extraction;
geometry = cached->second.arrays;
}
}
auto uvs = [](const std::vector<float> &values) {
if (geometry.is_empty()) {
PackedVector3Array positions;
positions.resize(static_cast<int64_t>(draw.positions.size() / 3));
if constexpr (sizeof(Vector3) == sizeof(float) * 3 && std::is_trivially_copyable_v<Vector3>) {
if (!draw.positions.empty())
std::memcpy(positions.ptrw(), draw.positions.data(), draw.positions.size() * sizeof(float));
} else {
Vector3 *dst = positions.ptrw();
for (int64_t i = 0; i < positions.size(); ++i)
dst[i] = Vector3(draw.positions[i * 3], draw.positions[i * 3 + 1], draw.positions[i * 3 + 2]);
}
geometry["positions"] = positions;
if (!draw.rhw.empty())
geometry["rhw"] = floats(draw.rhw.data(), static_cast<int>(draw.rhw.size()));
if (!draw.normals.empty()) {
PackedVector3Array normals;
normals.resize(static_cast<int64_t>(draw.normals.size() / 3));
if constexpr (sizeof(Vector3) == sizeof(float) * 3 && std::is_trivially_copyable_v<Vector3>) {
std::memcpy(normals.ptrw(), draw.normals.data(), draw.normals.size() * sizeof(float));
} else {
Vector3 *dst = normals.ptrw();
for (int64_t i = 0; i < normals.size(); ++i)
dst[i] = Vector3(draw.normals[i * 3], draw.normals[i * 3 + 1], draw.normals[i * 3 + 2]);
}
geometry["normals"] = normals;
}
auto uvs = [](const std::vector<float> &values) {
PackedVector2Array out;
out.resize(static_cast<int64_t>(values.size() / 2));
for (int64_t i = 0; i < out.size(); ++i)
out[i] = Vector2(values[i * 2], values[i * 2 + 1]);
if constexpr (sizeof(Vector2) == sizeof(float) * 2 && std::is_trivially_copyable_v<Vector2>) {
if (!values.empty())
std::memcpy(out.ptrw(), values.data(), values.size() * sizeof(float));
} else {
Vector2 *dst = out.ptrw();
for (int64_t i = 0; i < out.size(); ++i)
dst[i] = Vector2(values[i * 2], values[i * 2 + 1]);
}
return out;
};
if (!draw.uv0.empty())
item["uv0"] = uvs(draw.uv0);
if (!draw.uv1.empty())
item["uv1"] = uvs(draw.uv1);
if (!draw.diffuse.empty()) {
PackedColorArray colors;
colors.resize(static_cast<int64_t>(draw.diffuse.size()));
for (int64_t i = 0; i < colors.size(); ++i)
colors[i] = argb_color(draw.diffuse[i]);
item["diffuse"] = colors;
if (!draw.uv0.empty())
geometry["uv0"] = uvs(draw.uv0);
if (!draw.uv1.empty())
geometry["uv1"] = uvs(draw.uv1);
if (!draw.diffuse.empty()) {
PackedColorArray colors;
colors.resize(static_cast<int64_t>(draw.diffuse.size()));
for (int64_t i = 0; i < colors.size(); ++i)
colors[i] = argb_color(draw.diffuse[i]);
geometry["diffuse"] = colors;
}
PackedInt32Array indices;
indices.resize(static_cast<int64_t>(draw.indices.size()));
for (int64_t i = 0; i < indices.size(); ++i)
indices[i] = static_cast<int32_t>(draw.indices[i]);
geometry["indices"] = indices;
if (draw.geometry_key != 0)
geometry_cache[draw.geometry_key] = {draw.geometry_revision, extraction, geometry};
}
PackedInt32Array indices;
indices.resize(static_cast<int64_t>(draw.indices.size()));
for (int64_t i = 0; i < indices.size(); ++i)
indices[i] = static_cast<int32_t>(draw.indices[i]);
item["indices"] = indices;
item.merge(geometry);
item["alpha_blend"] = static_cast<int64_t>(draw.alpha_blend);
item["src_blend"] = static_cast<int64_t>(draw.src_blend);
@@ -226,6 +494,16 @@ Array Metin2PythonHost::render3d_draws() {
item["z_func"] = static_cast<int64_t>(draw.z_func);
item["lighting"] = static_cast<int64_t>(draw.lighting);
item["texture_factor"] = static_cast<int64_t>(draw.texture_factor);
bool uses_tf = false;
for (int s = 0; s < 2; ++s) {
if (draw.color_op[s] > 1 &&
(((draw.color_arg1[s] & 0xF) == 3) || ((draw.color_arg2[s] & 0xF) == 3)))
uses_tf = true;
if (draw.alpha_op[s] > 1 &&
(((draw.alpha_arg1[s] & 0xF) == 3) || ((draw.alpha_arg2[s] & 0xF) == 3)))
uses_tf = true;
}
item["uses_tf"] = uses_tf;
item["fog_enable"] = static_cast<int64_t>(draw.fog_enable);
item["color_op"] = static_cast<int64_t>(draw.color_op[0]);
item["alpha_op"] = static_cast<int64_t>(draw.alpha_op[0]);
@@ -243,7 +521,22 @@ Array Metin2PythonHost::render3d_draws() {
item["light0_ambient"] = Color(draw.light0_ambient[0], draw.light0_ambient[1], draw.light0_ambient[2],
draw.light0_ambient[3]);
item["ambient"] = argb_color(draw.ambient);
out.push_back(item);
PackedFloat32Array vp;
vp.resize(4);
vp[0] = draw.viewport[0];
vp[1] = draw.viewport[1];
vp[2] = draw.viewport[2];
vp[3] = draw.viewport[3];
item["viewport"] = vp;
out[draw_index++] = item;
}
if (extraction % 120 == 0) {
for (auto it = geometry_cache.begin(); it != geometry_cache.end();) {
if (extraction - it->second.last_used > 120)
it = geometry_cache.erase(it);
else
++it;
}
}
return out;
}
@@ -314,11 +607,14 @@ bool Metin2PythonHost::is_app_looping() { return false; }
void Metin2PythonHost::set_ui_size(int, int) {}
void Metin2PythonHost::ui_mouse_move(int, int) {}
void Metin2PythonHost::ui_mouse_button(int, bool, int, int) {}
void Metin2PythonHost::ui_mouse_wheel(int) {}
void Metin2PythonHost::ui_key(int, bool) {}
void Metin2PythonHost::ui_char(int) {}
void Metin2PythonHost::ui_ime_key(int) {}
void Metin2PythonHost::ui_update() {}
Array Metin2PythonHost::ui_render_commands() { return Array(); }
Array Metin2PythonHost::ui_render_commands_batched() { return Array(); }
bool Metin2PythonHost::has_3d_draws() { return false; }
Ref<Image> Metin2PythonHost::memory_texture(const String &, int64_t) { return Ref<Image>(); }
Array Metin2PythonHost::render3d_draws() { return Array(); }
Array Metin2PythonHost::audio_commands() { return Array(); }
@@ -341,11 +637,14 @@ void Metin2PythonHost::_bind_methods() {
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("set_ui_size", "width", "height"), &Metin2PythonHost::set_ui_size);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_mouse_move", "x", "y"), &Metin2PythonHost::ui_mouse_move);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_mouse_button", "button", "pressed", "x", "y"), &Metin2PythonHost::ui_mouse_button);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_mouse_wheel", "delta"), &Metin2PythonHost::ui_mouse_wheel);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_key", "key", "pressed"), &Metin2PythonHost::ui_key);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_char", "codepoint"), &Metin2PythonHost::ui_char);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_ime_key", "vkey"), &Metin2PythonHost::ui_ime_key);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_update"), &Metin2PythonHost::ui_update);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_render_commands"), &Metin2PythonHost::ui_render_commands);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_render_commands_batched"), &Metin2PythonHost::ui_render_commands_batched);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("has_3d_draws"), &Metin2PythonHost::has_3d_draws);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("memory_texture", "name", "known_revision"),
&Metin2PythonHost::memory_texture, DEFVAL(-1));
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("render3d_draws"), &Metin2PythonHost::render3d_draws);
+4
View File
@@ -38,12 +38,16 @@ public:
static void set_ui_size(int width, int height);
static void ui_mouse_move(int x, int y);
static void ui_mouse_button(int button, bool pressed, int x, int y);
static void ui_mouse_wheel(int delta);
static void ui_key(int key, bool pressed);
// WM_CHAR (Unicode code point) and WM_KEYDOWN (Win32 VK code) for the 40250 IME.
static void ui_char(int codepoint);
static void ui_ime_key(int vkey);
static void ui_update();
static godot::Array ui_render_commands();
// Ordered UI commands with consecutive, unclipped font quads packed into triangle arrays.
static godot::Array ui_render_commands_batched();
static bool has_3d_draws();
// The pixels of a "mem:<id>@<revision>" image command (the CGraphicFontTexture glyph pages) as an
// RGBA8 Image; null when the texture is gone or its revision is still `known_revision`.
static godot::Ref<godot::Image> memory_texture(const godot::String &name, int64_t known_revision);
+43 -6
View File
@@ -14,7 +14,11 @@
#include <asset_resolver.h>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <unordered_map>
#include <utility>
using namespace godot;
@@ -72,14 +76,23 @@ Ref<Shader> terrain_shader() {
// DDS -> RGBA8 Image,resize 到 size×size。缓存(非函数静态 —— 见 cleanup)。
std::unordered_map<std::string, Ref<godot::Image>> g_layer_cache;
Ref<godot::Image> layer_image(const std::string &real_path, int size) {
std::unordered_map<std::string, std::pair<int, int>> g_layer_dimensions;
Ref<godot::Image> layer_image(const std::string &real_path, int size,
std::unordered_map<std::string, mtgodot::Image> &decoded_sources) {
auto &cache = g_layer_cache;
std::string key = real_path + "@" + std::to_string(size);
auto it = cache.find(key);
if (it != cache.end())
return it->second;
Ref<godot::Image> out;
mtgodot::Image d = mtgodot::dds_from_file(godot::String(real_path.c_str()));
mtgodot::Image d;
auto decoded = decoded_sources.find(real_path);
if (decoded != decoded_sources.end()) {
d = std::move(decoded->second);
decoded_sources.erase(decoded);
} else {
d = mtgodot::dds_from_file(godot::String(real_path.c_str()));
}
if (d.ok()) {
PackedByteArray b;
b.resize((int64_t)d.rgba.size());
@@ -120,25 +133,38 @@ Ref<ImageTexture> weight_tex(const std::vector<const fmt::SplatLayer *> &four) {
void cleanup_terrain_shader() {
g_terrain_shader.unref();
g_layer_cache.clear(); // 释放缓存的 Ref<Image>,别拖到 __cxa_finalize(那时引擎已析构)
g_layer_dimensions.clear();
}
Ref<ShaderMaterial> build_chunk_terrain_material(const fmt::SplatSet &splat,
const fmt::TextureSet &tset, const fmt::AssetResolver &res,
const String &shadowmap_path) {
const bool profile = std::getenv("MT_PROFILE_MAP") != nullptr;
const auto now = [] { return std::chrono::steady_clock::now(); };
const auto start = now();
// Texture2DArray 要求各 slice 同尺寸 —— 取用到的图层里的最大源边长(上限 1024),
// 只放大不缩小最大源,避免把 512² 地表贴图硬降采样(PARITY-GAP §3.4)。
int src_max = 256;
// Keep newly decoded sources only until the image pass consumes them. The size pass and image pass
// used to decode the same DDS twice on its first use.
std::unordered_map<std::string, mtgodot::Image> decoded_sources;
for (const auto &L : splat.layers) {
if (L.layer >= 1 && L.layer <= (int)tset.layers.size()) {
std::string rp = res.resolve(tset.layers[L.layer - 1].texture, nullptr);
if (rp.empty())
continue;
mtgodot::Image d = mtgodot::dds_from_file(godot::String(rp.c_str()));
if (d.ok())
src_max = std::max<int>(src_max, std::max<int>(d.w, d.h));
auto it = g_layer_dimensions.find(rp);
if (it == g_layer_dimensions.end()) {
mtgodot::Image d = mtgodot::dds_from_file(godot::String(rp.c_str()));
it = g_layer_dimensions.emplace(rp, d.ok() ? std::make_pair(int(d.w), int(d.h))
: std::make_pair(0, 0)).first;
decoded_sources.emplace(rp, std::move(d));
}
src_max = std::max(src_max, std::max(it->second.first, it->second.second));
}
}
const int LSIZE = std::min(1024, src_max);
const auto dimensions_done = now();
int n = std::min<int>(MAX_LAYERS, (int)splat.layers.size());
if (n == 0)
@@ -167,7 +193,7 @@ Ref<ShaderMaterial> build_chunk_terrain_material(const fmt::SplatSet &splat,
const fmt::TextureLayer &tl = tset.layers[L.layer - 1];
std::string rp = res.resolve(tl.texture, nullptr);
if (!rp.empty())
img = layer_image(rp, LSIZE);
img = layer_image(rp, LSIZE, decoded_sources);
// 原客户端 TextureSet.cpp:185:u' = (TexCoordBase*UScale)*vtx_cm + UOffset,
// TexCoordBase = 1/(PATCH_XSIZE*CELLSCALE) = 1/3200;区块归一化 UV -> 平铺频率 = 8*Scale。
float us = tl.u_scale > 0.01f ? tl.u_scale : 1.0f;
@@ -176,10 +202,12 @@ Ref<ShaderMaterial> build_chunk_terrain_material(const fmt::SplatSet &splat,
}
imgs.push_back(img.is_valid() ? img : fallback);
}
const auto images_done = now();
Ref<Texture2DArray> arr;
arr.instantiate();
arr->create_from_images(imgs);
const auto array_done = now();
// 权重贴图:ceil(n/4) 张 RGBA8(每通道一层 alpha)
Ref<ImageTexture> wtex[4];
@@ -192,6 +220,7 @@ Ref<ShaderMaterial> build_chunk_terrain_material(const fmt::SplatSet &splat,
}
wtex[g] = weight_tex(grp);
}
const auto weights_done = now();
Ref<ShaderMaterial> mat;
mat.instantiate();
@@ -209,6 +238,7 @@ Ref<ShaderMaterial> build_chunk_terrain_material(const fmt::SplatSet &splat,
mat->set_shader_parameter("layer_uv", uva);
}
const auto shader_done = now();
if (!shadowmap_path.is_empty()) {
mtgodot::Image sm = mtgodot::dds_from_file(shadowmap_path);
if (sm.ok()) {
@@ -221,6 +251,13 @@ Ref<ShaderMaterial> build_chunk_terrain_material(const fmt::SplatSet &splat,
mat->set_shader_parameter("use_shadowmap", true);
}
}
if (profile) {
const auto ms = [](auto a, auto b) { return std::chrono::duration<double, std::milli>(b - a).count(); };
std::fprintf(stderr, "MATERIAL_PROFILE dimensions=%.3f images=%.3f array=%.3f weights=%.3f shader=%.3f shadow=%.3f total=%.3f layers=%d size=%d\n",
ms(start, dimensions_done), ms(dimensions_done, images_done), ms(images_done, array_done),
ms(array_done, weights_done), ms(weights_done, shader_done), ms(shader_done, now()),
ms(start, now()), n, LSIZE);
}
return mat;
}
@@ -13,6 +13,7 @@
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace mtnet::classic;
@@ -23,6 +24,16 @@ constexpr std::uint32_t kHandshake = 0x2468ace0;
constexpr int kHandshakeRetryLimit = 32; // game/src/desc.h HANDSHAKE_RETRY_LIMIT
constexpr int kIdleMs = 20000;
int fake_mob_count()
{
const char* value = std::getenv("MT_FAKE_MOB_COUNT");
if (!value || !*value)
return 1;
char* end = nullptr;
const long count = std::strtol(value, &end, 10);
return *end == '\0' && count >= 1 && count <= 64 ? static_cast<int>(count) : 1;
}
// get_dword_time(): milliseconds on the server's clock.
std::uint32_t now_ms()
{
@@ -582,6 +593,20 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
mob_update.attack_speed = 100;
if (!c.Send(mob) || !c.Send(mob_update))
return Fail(name + ": send monster");
// Optional crowd for render profiling. Keep the primary dog and its click path clear so the
// existing combat test still attacks kMobVID; additional dogs are passive scenery.
const int mob_count = fake_mob_count();
for (int i = 1; i < mob_count; ++i)
{
GCCharacterAdd extra = mob;
extra.vid = kMobVID + static_cast<std::uint32_t>(i);
extra.x = kMobX - 350 - ((i - 1) % 8) * 170;
extra.y = kMobY + 250 + ((i - 1) / 8) * 190;
GCCharacterUpdate extra_update = mob_update;
extra_update.vid = extra.vid;
if (!c.Send(extra) || !c.Send(extra_update))
return Fail(name + ": send extra monster");
}
// An NPC is ADD + ADDITIONAL_INFO + UPDATE like a PC (the name is the mob_proto locale name).
GCCharacterAdd keeper = zeroed<GCCharacterAdd>();
keeper.header = HDR_GC_CHARACTER_ADD;
+60 -3
View File
@@ -49,10 +49,12 @@
#include "GameLib/FlyingData.h"
#include "GameLib/FlyingObjectManager.h"
#include "EffectLib/EffectManager.h"
#include "EffectLib/EffectInstance.h"
#include "EterLib/Camera.h"
#include "GameLib/ItemManager.h"
#include "UserInterface/PythonItem.h"
#include "UserInterface/StdAfx.h"
#include "UserInterface/PythonPlayer.h"
#include "UserInterface/PythonExchange.h"
#include "UserInterface/PythonTextTail.h"
#include "UserInterface/PythonCharacterManager.h"
@@ -674,11 +676,11 @@ int main(int argc, char** argv)
const size_t moves_before = server ? server->Events().size() : 0;
if (main_instance)
main_instance->NEW_GetPixelPosition(&start);
PythonBoot::UIMouseButton(1, true, 400, 520);
PythonBoot::UIMouseButton(1, true, 400, 220);
pump_until(0.1, [] { return false; });
PythonBoot::UIMouseButton(1, false, 400, 520);
PythonBoot::UIMouseButton(1, false, 400, 220);
CHECK(main_instance && pump_until(3, [&] { return walked() > 100.0f; }));
CHECK(main_instance && pump_until(5, [&] { return !main_instance->IsWalking(); }));
CHECK(main_instance && pump_until(10, [&] { return !main_instance->IsWalking(); }));
std::printf("port_login_flow_test: click walked %.0f cm\n", main_instance ? walked() : 0.0f);
if (server)
{
@@ -746,6 +748,61 @@ int main(int argc, char** argv)
++attacks;
std::printf("port_login_flow_test: %zu attack(s) until the stray dog died\n", attacks);
CHECK(attacks >= (size_t) FakeLoginServer::kMobHits);
// Camera mouse wheel zoom test:
// Test zooming in (Wheel UP, positive delta) and zooming out (Wheel DOWN, negative delta).
{
CCamera* pkCmrCur = CCameraManager::Instance().GetCurrentCamera();
CHECK(pkCmrCur != nullptr);
if (pkCmrCur)
{
const float initial_dist = pkCmrCur->GetDistance();
std::printf("port_login_flow_test: initial camera distance = %f\n", initial_dist);
// Zoom IN: positive wheel delta
PythonBoot::UIMouseWheel(120);
pump_until(0.1, [] { return false; });
const float zoomed_in_dist = pkCmrCur->GetDistance();
std::printf("port_login_flow_test: zoomed in camera distance = %f\n", zoomed_in_dist);
CHECK(zoomed_in_dist < initial_dist);
// Zoom OUT: negative wheel delta
PythonBoot::UIMouseWheel(-240);
pump_until(0.1, [] { return false; });
const float zoomed_out_dist = pkCmrCur->GetDistance();
std::printf("port_login_flow_test: zoomed out camera distance = %f\n", zoomed_out_dist);
CHECK(zoomed_out_dist > zoomed_in_dist);
}
}
// P-grade skill test: verify P-grade skill (geomgyeong / Aura of the Sword) uses Grade 3 motion (geomgyeong_4.msa),
// spawns effect instances (geom_4_badak.mse, geom_4_sword_making.mse), and renders them.
{
CPythonPlayer& rkPlayer = CPythonPlayer::Instance();
rkPlayer.SetStatus(POINT_SP, 1000);
rkPlayer.SetStatus(POINT_MAX_SP, 1000);
rkPlayer.SetStatus(POINT_HP, 1000);
rkPlayer.SetStatus(POINT_MAX_HP, 1000);
CInstanceBase* pkInstMain = rkPlayer.NEW_GetMainActorPtr();
CHECK(pkInstMain != nullptr);
if (pkInstMain)
{
pkInstMain->ChangeWeapon(19);
rkPlayer.SetSkill(4, 4); // slot 4 = geomgyeong
rkPlayer.SetSkillLevel_(4, 3, 40); // Grade 3 (P), Level 40
CHECK(rkPlayer.GetSkillGrade(4) == 3);
rkPlayer.ClickSkillSlot(4);
CHECK(pkInstMain->IsUsingSkill());
bool has_effects = false;
for (int step = 0; step < 10; ++step)
{
pump_until(0.1, [] { return false; });
if (CEffectInstance::GetRenderingEffectCount() > 0)
has_effects = true;
}
CHECK(has_effects);
}
}
}
}
// 10. (批次 4-c) NPC shop: a click on the general store walks up to it (__OnClickActor) and sends CG_ON_CLICK;
+84
View File
@@ -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()
+153
View File
@@ -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.
+378
View File
@@ -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
+139
View File
@@ -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;
}
+151
View File
@@ -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);
}
}
}
+7
View File
@@ -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"
File diff suppressed because it is too large Load Diff
+732
View File
@@ -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_;
};
+140
View File
@@ -0,0 +1,140 @@
extends SceneTree
const PythonUISurface = preload("res://python_ui_surface.gd")
const Python3DSurface = preload("res://python_3d_surface.gd")
var ui: Control
var world: Node3D
func _initialize() -> void:
call_deferred("run")
func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == ""
func _pump_until(seconds: float, condition: Callable) -> bool:
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
while Time.get_ticks_msec() < deadline:
if condition.call():
return true
await process_frame
return condition.call()
func run() -> void:
root.size = Vector2i(1024, 768)
world = Python3DSurface.new()
world.name = "Python3DSurface"
ui = PythonUISurface.new()
ui.name = "PythonUISurface"
root.add_child(ui)
root.add_child(world)
var err: String = ui.run_app()
if err != "":
print("ERROR: run_app: ", err)
quit(1)
return
await _pump_until(15, func(): return _py(
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
))
await _pump_until(20, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
))
var host = OS.get_environment("MT_LIVE_HOST")
var login = OS.get_environment("MT_LIVE_LOGIN")
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n" % [host, game_port, host, auth_port]
+ "_stream.curPhaseWindow.Connect('%s', '%s')" % [login, pwd])
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
))
_py("_stream.curPhaseWindow.SelectSlot(0)\n_stream.curPhaseWindow.StartGame()")
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"
))
for i in 60:
await process_frame
print("=== CAMERA WORLD & HEIGHT PROFILE ===")
var draws: Array = Metin2PythonHost.render3d_draws()
if not draws.is_empty():
var vm: PackedFloat32Array = draws[0]["view"]
var cam_inv: Transform3D = Python3DSurface.d3d_transform(vm).affine_inverse()
print("Camera world origin: ", cam_inv.origin)
print("Camera world basis: ", cam_inv.basis)
print("Camera looking direction: ", -cam_inv.basis.z)
for y in range(68200, 66200, -100):
var bg_h = Metin2PythonHost.evaluate("str(background.GetHeight(59700.0, %f))" % float(y))
var terrain_h = float(world.terrain.call("sample_height", 597.0, float(y) / 100.0)) * 100.0 if world.terrain else 0.0
print("Profile y=%d: bg_h=%s terrain_h=%.2f" % [y, bg_h, terrain_h])
print("Total draws: ", draws.size())
for d in draws:
var tex: String = d.get("texture0", "")
if tex.contains("warrior") or tex.contains("stray_dog") or tex.contains("goods"):
var wm: PackedFloat32Array = d["world"]
var vm: PackedFloat32Array = d["view"]
var xform: Transform3D = Python3DSurface.d3d_transform(wm)
var cam_xform: Transform3D = Python3DSurface.d3d_transform(Python3DSurface.multiply(wm, vm))
var min_z := 1e9
var max_z := -1e9
var min_local_z := 1e9
var max_local_z := -1e9
for pos in d["positions"]:
min_local_z = minf(min_local_z, pos.z)
max_local_z = maxf(max_local_z, pos.z)
var wp: Vector3 = xform * pos
min_z = minf(min_z, wp.z)
max_z = maxf(max_z, wp.z)
# Map position: (xform.origin.x, -xform.origin.y)
var map_x := xform.origin.x
var map_y := -xform.origin.y
var actor_z := xform.origin.z
var bg_h = Metin2PythonHost.evaluate("str(background.GetHeight(%f, %f))" % [map_x, map_y])
var terrain_h = float(world.terrain.call("sample_height", map_x / 100.0, map_y / 100.0)) * 100.0 if world.terrain else 0.0
print("Actor [%s]:" % tex.get_file())
print(" World origin: ", xform.origin, " (map_x=%.1f, map_y=%.1f, origin_z=%.1f)" % [map_x, map_y, actor_z])
print(" Local vert Z range: [%.2f, %.2f]" % [min_local_z, max_local_z])
print(" World vert Z range: [%.2f, %.2f]" % [min_z, max_z])
print(" Background Height: %s, Terrain Height: %.2f" % [bg_h, terrain_h])
print(" Vert_Z min vs Height: diff = %.2f cm" % (min_z - float(bg_h)))
print(" Origin_Z vs Height: diff = %.2f cm" % (actor_z - float(bg_h)))
print(" Camera-space origin: ", cam_xform.origin)
if tex.contains("stray_dog"):
print("--- DOG TERRAIN DETAIL ---")
var from_map := Transform3D(Basis(Vector3(100, 0, 0), Vector3(0, 0, 100), Vector3(0, -100, 0)), Vector3.ZERO)
var dog_world := xform.origin # (59700, -66400, 19851.5)
print("Dog 40250 world: ", dog_world)
for child in world.terrain.find_children("*", "MeshInstance3D", true, false):
var mi := child as MeshInstance3D
if mi.mesh is ArrayMesh:
var am := mi.mesh as ArrayMesh
var arrays := am.surface_get_arrays(0)
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
for v in verts:
var world_v: Vector3 = from_map * (mi.transform * v)
if abs(world_v.x - dog_world.x) < 300.0 and abs(world_v.y - dog_world.y) < 300.0:
print("Terrain local vert: ", v, " -> mi.xform*v: ", mi.transform * v, " -> from_map: ", world_v)
print("Diff Z (terrain world Z - dog world Z): ", world_v.z - dog_world.z)
break
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://b5vx3h3lipsuf
+116
View File
@@ -0,0 +1,116 @@
extends SceneTree
const PythonUISurface = preload("res://python_ui_surface.gd")
const Python3DSurface = preload("res://python_3d_surface.gd")
var ui: Control
var world: Node3D
func _initialize() -> void:
call_deferred("run")
func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == ""
func _pump_until(seconds: float, condition: Callable) -> bool:
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
while Time.get_ticks_msec() < deadline:
if condition.call():
return true
await process_frame
return condition.call()
func run() -> void:
root.size = Vector2i(1024, 768)
world = Python3DSurface.new()
ui = PythonUISurface.new()
root.add_child(ui)
root.add_child(world)
ui.run_app()
await _pump_until(15, func(): return _py(
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
))
await _pump_until(20, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
))
var host = OS.get_environment("MT_LIVE_HOST")
var login = OS.get_environment("MT_LIVE_LOGIN")
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n" % [host, game_port, host, auth_port]
+ "_stream.curPhaseWindow.Connect('%s', '%s')" % [login, pwd])
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
))
_py("_stream.curPhaseWindow.SelectSlot(0)\n_stream.curPhaseWindow.StartGame()")
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"
))
for i in 60:
await process_frame
print("=== INVESTIGATING ALL 518 ACTORS ===")
var draws: Array = Metin2PythonHost.render3d_draws()
var actor_draws := {}
for d in draws:
var wm: PackedFloat32Array = d["world"]
var key := "%.1f,%.1f,%.1f" % [wm[12], wm[13], wm[14]]
if not actor_draws.has(key):
actor_draws[key] = []
actor_draws[key].append(d)
print("Unique actor positions: ", actor_draws.size())
var anomalies := []
for key in actor_draws:
var ds: Array = actor_draws[key]
var wm: PackedFloat32Array = ds[0]["world"]
var map_x := wm[12]
var map_y := -wm[13]
var actor_z := wm[14]
var bg_h = float(Metin2PythonHost.evaluate("str(background.GetHeight(%f, %f))" % [map_x, map_y]))
var terrain_h = float(world.terrain.call("sample_height", map_x / 100.0, map_y / 100.0)) * 100.0 if world.terrain else 0.0
var diff_bg = actor_z - bg_h
var diff_terrain = actor_z - terrain_h
# Find textures
var texs := []
var min_vert_z := 1e9
for d in ds:
var t: String = d.get("texture0", "")
if not t.is_empty():
texs.append(t.get_file())
for p in d["positions"]:
min_vert_z = minf(min_vert_z, actor_z + p.z)
if absf(diff_bg) > 5.0 or absf(diff_terrain) > 5.0:
anomalies.append({
"pos": [map_x, map_y, actor_z],
"bg_h": bg_h,
"terrain_h": terrain_h,
"diff_bg": diff_bg,
"diff_terrain": diff_terrain,
"vert_min_diff": min_vert_z - bg_h,
"textures": texs
})
print("Found %d anomalies with gap > 5cm!" % anomalies.size())
for a in anomalies.slice(0, 30):
print("Anomaly: pos=(%.1f, %.1f, %.1f), bg_h=%.1f, terrain_h=%.1f, diff_bg=%.1f, diff_terrain=%.1f, vert_min_diff=%.1f, tex=%s" % [
a["pos"][0], a["pos"][1], a["pos"][2], a["bg_h"], a["terrain_h"], a["diff_bg"], a["diff_terrain"], a["vert_min_diff"], a["textures"]
])
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://xn0yhjq7h8d
+143
View File
@@ -0,0 +1,143 @@
extends SceneTree
const PythonUISurface = preload("res://python_ui_surface.gd")
const Python3DSurface = preload("res://python_3d_surface.gd")
var ui: Control
var world: Node3D
func _initialize() -> void:
call_deferred("run")
func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == ""
func _key(keycode: Key, unicode: int, pressed: bool) -> void:
var event := InputEventKey.new()
event.keycode = keycode
event.physical_keycode = keycode
event.unicode = unicode
event.pressed = pressed
root.push_input(event)
func _press(keycode: Key) -> void:
_key(keycode, 0, true)
_key(keycode, 0, false)
func _type(text: String) -> void:
for i in text.length():
var ch := text.unicode_at(i)
var keycode := OS.find_keycode_from_string(text[i].to_upper())
_key(keycode, ch, true)
_key(keycode, ch, false)
func _pump_until(seconds: float, condition: Callable) -> bool:
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
while Time.get_ticks_msec() < deadline:
if condition.call():
return true
await process_frame
return condition.call()
func run() -> void:
root.size = Vector2i(1024, 768)
world = Python3DSurface.new()
world.name = "Python3DSurface"
ui = PythonUISurface.new()
ui.name = "PythonUISurface"
root.add_child(ui)
root.add_child(world)
var err: String = ui.run_app()
if err != "":
print("ERROR: run_app failed: ", err)
quit(1)
return
await _pump_until(15, func(): return _py(
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
))
await _pump_until(20, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
))
_py(
"_env = os.environ\n"
+ "_slot = int(_env.get('MT_LIVE_SLOT', '0'))\n"
+ "_stream.SetConnectInfo(_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_GAME_PORT', '13000')), "
+ "_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_AUTH_PORT', '11000')))\n"
+ "_w = _stream.curPhaseWindow\n"
+ "_w._LoginWindow__OpenLoginBoard()\n"
+ "_w.idEditLine.SetText('')\n"
+ "_w.pwdEditLine.SetText('')\n"
+ "_w.idEditLine.SetFocus()"
)
_type(OS.get_environment("MT_LIVE_LOGIN"))
_press(KEY_TAB)
_type(OS.get_environment("MT_LIVE_PASSWORD"))
_py("del _env, _w")
_press(KEY_ENTER)
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
))
_py("_stream.curPhaseWindow.SelectSlot(_slot)\n_stream.curPhaseWindow.StartGame()")
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"
))
for i in 30:
await process_frame
print("=== DEBUG HEIGHT REPORT ===")
var test_points = [
Vector2(59700, 68200),
Vector2(59700, 66400),
Vector2(60100, 66400),
Vector2(50000, 50000),
Vector2(30000, 30000),
Vector2(70000, 70000),
]
for pt in test_points:
var bg_h = Metin2PythonHost.evaluate("str(background.GetHeight(%f, %f))" % [pt.x, pt.y])
var tm_h = float(world.terrain.call("sample_height", pt.x / 100.0, pt.y / 100.0)) * 100.0 if world.terrain else 0.0
print("Point (%.0f, %.0f): 40250_bg_h=%s, Metin2World_h=%.1f, diff=%.1f" % [
pt.x, pt.y, bg_h, tm_h, float(bg_h) - tm_h
])
# Inspect terrain mesh triangles around dog
print("=== TERRAIN MESH VERTICES AROUND DOG (59700, -66400) ===")
# In camera space (since Camera3D is at origin):
# Dog world pos in 40250: (59700, -66400, 19851.5)
# What is dog in camera space?
var draws = Metin2PythonHost.render3d_draws()
for i in draws.size():
var d = draws[i]
var tex: String = d.get("texture0", "")
if tex.contains("stray_dog"):
var wm: PackedFloat32Array = d.get("world", PackedFloat32Array())
var vm: PackedFloat32Array = d.get("view", PackedFloat32Array())
var xform: Transform3D = Python3DSurface.d3d_transform(Python3DSurface.multiply(wm, vm))
print("DOG camera-space transform origin: ", xform.origin)
# Now let's check terrain mesh global transform and vertices:
for child in world.terrain.find_children("*", "MeshInstance3D", true, false):
var mi := child as MeshInstance3D
if mi.mesh is ArrayMesh:
var am := mi.mesh as ArrayMesh
var arrays := am.surface_get_arrays(0)
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
for v in verts:
var gv: Vector3 = mi.global_transform * v
# Dog in camera space is around xform.origin. Let's find terrain vertices within 200cm:
if abs(gv.x - xform.origin.x) < 200.0 and abs(gv.z - xform.origin.z) < 200.0:
print("Terrain vertex near dog in camera-space: ", gv, " dog origin: ", xform.origin, " diff_y (height in cam space): ", gv.y - xform.origin.y)
break
break
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://mpcddwfiipkc
+71
View File
@@ -0,0 +1,71 @@
extends SceneTree
const PythonUISurface = preload("res://python_ui_surface.gd")
const Python3DSurface = preload("res://python_3d_surface.gd")
var ui: Control
var world: Node3D
func _initialize() -> void:
call_deferred("run")
func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == ""
func _pump_until(seconds: float, condition: Callable) -> bool:
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
while Time.get_ticks_msec() < deadline:
if condition.call():
return true
await process_frame
return condition.call()
func run() -> void:
root.size = Vector2i(1024, 768)
world = Python3DSurface.new()
world.name = "Python3DSurface"
ui = PythonUISurface.new()
ui.name = "PythonUISurface"
root.add_child(ui)
root.add_child(world)
var err: String = ui.run_app()
if err != "":
print("ERROR: run_app: ", err)
quit(1)
return
await _pump_until(15, func(): return _py(
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
))
await _pump_until(20, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
))
var host = OS.get_environment("MT_LIVE_HOST")
var login = OS.get_environment("MT_LIVE_LOGIN")
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n" % [host, game_port, host, auth_port]
+ "_stream.curPhaseWindow.Connect('%s', '%s')" % [login, pwd])
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
))
print("=== IN INTRO SELECT PHASE ===")
for i in 60:
await process_frame
var cmds: Array = Metin2PythonHost.ui_render_commands()
print("UI commands count: ", cmds.size())
var file = FileAccess.open("/tmp/select_ui_cmds.json", FileAccess.WRITE)
file.store_string(JSON.stringify(cmds, " "))
file.close()
print("Saved /tmp/select_ui_cmds.json")
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://c0k854rmu55u7
+70
View File
@@ -0,0 +1,70 @@
extends SceneTree
const PythonUISurface = preload("res://python_ui_surface.gd")
const Python3DSurface = preload("res://python_3d_surface.gd")
func _initialize() -> void:
call_deferred("run")
func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == ""
func _pump_until(seconds: float, condition: Callable) -> bool:
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
while Time.get_ticks_msec() < deadline:
if condition.call():
return true
await process_frame
return condition.call()
func run() -> void:
root.size = Vector2i(1024, 768)
var world = Python3DSurface.new()
var ui = PythonUISurface.new()
root.add_child(ui)
root.add_child(world)
ui.run_app()
await _pump_until(15, func(): return _py(
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
))
await _pump_until(20, func(): return _py("assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"))
var host = OS.get_environment("MT_LIVE_HOST")
var login = OS.get_environment("MT_LIVE_LOGIN")
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n_stream.curPhaseWindow.Connect('%s', '%s')" % [host, game_port, host, auth_port, login, pwd])
await _pump_until(30, func(): return _py("assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"))
_py("_stream.curPhaseWindow.SelectSlot(0)\n_stream.curPhaseWindow.StartGame()")
await _pump_until(30, func(): return _py("assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"))
for i in 60:
await process_frame
print("=== INSPECTING STONE TILES AROUND PLAYER (59700, 68200) ===")
var draws: Array = Metin2PythonHost.render3d_draws()
var player_z := 19851.5
for d in draws:
var tex: String = d.get("texture0", "")
if tex.contains("stone") or tex.contains("floor") or tex.contains("tile") or tex.contains("sign"):
var wm: PackedFloat32Array = d["world"]
var xform: Transform3D = Python3DSurface.d3d_transform(wm)
# check if this object is near player (within 2000cm):
if absf(xform.origin.x - 59700.0) < 3000.0 and absf(-xform.origin.y - 68200.0) < 3000.0:
var min_z := 1e9
var max_z := -1e9
for p in d["positions"]:
var wp: Vector3 = xform * p
min_z = minf(min_z, wp.z)
max_z = maxf(max_z, wp.z)
print("Object [%s] at (%.1f, %.1f, %.1f), Z range: [%.2f, %.2f], diff vs player_z(19851.5): min=%.2f, max=%.2f" % [
tex.get_file(), xform.origin.x, -xform.origin.y, xform.origin.z, min_z, max_z, min_z - player_z, max_z - player_z
])
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://cy6rh17pn00hd
+115
View File
@@ -0,0 +1,115 @@
extends SceneTree
const PythonUISurface = preload("res://python_ui_surface.gd")
const Python3DSurface = preload("res://python_3d_surface.gd")
var ui: Control
var world: Node3D
func _initialize() -> void:
call_deferred("run")
func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == ""
func _pump_until(seconds: float, condition: Callable) -> bool:
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
while Time.get_ticks_msec() < deadline:
if condition.call():
return true
await process_frame
return condition.call()
func run() -> void:
root.size = Vector2i(1024, 768)
world = Python3DSurface.new()
world.name = "Python3DSurface"
ui = PythonUISurface.new()
ui.name = "PythonUISurface"
root.add_child(ui)
root.add_child(world)
var err: String = ui.run_app()
if err != "":
print("ERROR: run_app: ", err)
quit(1)
return
await _pump_until(15, func(): return _py(
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr, app\n"
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
))
await _pump_until(20, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
))
var host = OS.get_environment("MT_LIVE_HOST")
var login = OS.get_environment("MT_LIVE_LOGIN")
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n" % [host, game_port, host, auth_port]
+ "_stream.curPhaseWindow.Connect('%s', '%s')" % [login, pwd])
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
))
_py("_stream.curPhaseWindow.SelectSlot(0)\n_stream.curPhaseWindow.StartGame()")
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"
))
print("=== IN-GAME LOADED ===")
for i in 10:
await process_frame
print("--- EQUIPPING WEAPON 10 ---")
_py("chr.SelectInstance(player.GetMainCharacterIndex())\nchr.SetWeapon(10)")
for i in 10:
await process_frame
var check_draws = func(label: String):
var draws: Array = Metin2PythonHost.render3d_draws()
var body_draw = null
var weapon_draws = []
for d in draws:
var tex: String = d.get("texture0", "").to_lower()
if tex.contains("warrior") and not tex.contains("hair"):
body_draw = d
elif tex.contains("weapon") or tex.contains("sword") or tex.contains("00010") or tex.contains("00040"):
weapon_draws.append(d)
print("[%s] Total draws=%d, weapon_draws=%d" % [label, draws.size(), weapon_draws.size()])
var b_xf: Transform3D = Transform3D()
if body_draw != null:
var wm: PackedFloat32Array = body_draw["world"]
b_xf = Python3DSurface.d3d_transform(wm)
print(" Body origin: ", b_xf.origin, " tex=", body_draw.get("texture0", ""))
else:
print(" Body draw not found!")
for w in weapon_draws:
var wm: PackedFloat32Array = w["world"]
var xf: Transform3D = Python3DSurface.d3d_transform(wm)
var dist := xf.origin.distance_to(b_xf.origin) if body_draw != null else 0.0
print(" Weapon origin: ", xf.origin, " dist_to_body=%.2f cm tex=%s" % [dist, w.get("texture0", "")])
check_draws.call("IDLE_FRAME_0")
# Start moving!
print("--- STARTING MOVEMENT (DIK_UP) ---")
_py("player.SetSingleDIKKeyState(app.DIK_UP, True)")
for frame in range(1, 40):
await process_frame
if frame % 5 == 0:
check_draws.call("MOVING_FRAME_%d" % frame)
_py("player.SetSingleDIKKeyState(app.DIK_UP, False)")
for frame in range(1, 15):
await process_frame
check_draws.call("STOPPED")
quit(0)
+164 -29
View File
@@ -12,11 +12,13 @@
# in the recorded D3D camera's coordinate frame, and its .msenv supplies the character light.
extends Node3D
const AssetRoot = preload("res://asset_root.gd")
const UiAssets = preload("res://ui/ui_assets.gd")
# D3DCULL / D3DBLEND / D3DCMPFUNC values used below (D3D8Types.h).
const D3DCULL_NONE := 1
const D3DCULL_CW := 2
const D3DCULL_CCW := 3
const D3DBLEND_ONE := 2
const D3DBLEND_SRCALPHA := 5
const D3DBLEND_INVSRCALPHA := 6
@@ -31,6 +33,11 @@ var _atlas: Array = []
var _terrain_attempted := false
var _last_terrain_focus := Vector2(INF, INF)
var _meshes: Array[MeshInstance3D] = []
var _geometry_cache := {}
var _geometry_frame := 0
var _bg_mesh: MeshInstance3D
var _bg_mat: StandardMaterial3D
var _bg_quad: QuadMesh
# Materials keyed by texture and state, so an unchanged draw keeps its material from frame to frame.
var _materials := {}
@@ -39,6 +46,7 @@ var draw_count := 0
var unlit_stand_in_count := 0 # Compatibility report field; real lighting keeps this at zero.
func _ready() -> void:
get_viewport().transparent_bg = true
camera = Camera3D.new()
camera.name = "GameCamera"
camera.keep_aspect = Camera3D.KEEP_HEIGHT
@@ -53,8 +61,7 @@ func _ready() -> void:
add_child(light)
environment = WorldEnvironment.new()
environment.environment = Environment.new()
environment.environment.background_mode = Environment.BG_COLOR
environment.environment.background_color = Color.BLACK
environment.environment.background_mode = Environment.BG_CLEAR_COLOR
environment.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
environment.environment.ambient_light_color = Color.BLACK
environment.environment.tonemap_mode = Environment.TONE_MAPPER_LINEAR
@@ -62,7 +69,13 @@ func _ready() -> void:
func _process(_delta: float) -> void:
if Metin2PythonHost.is_running():
var profile_start := Time.get_ticks_usec() if _profile_frame else 0
update_frame()
if profile_start != 0:
profile_update_ms = float(Time.get_ticks_usec() - profile_start) / 1000.0
var _profile_frame := OS.has_environment("MT_PROFILE_FRAME")
var profile_update_ms := 0.0
# D3D row-vector matrix (v' = v * M) as a Godot Transform3D (v' = T * v): the rows of M are the basis
# columns, row 3 is the origin.
@@ -70,18 +83,8 @@ static func d3d_transform(m: PackedFloat32Array) -> Transform3D:
return Transform3D(Basis(Vector3(m[0], m[1], m[2]), Vector3(m[4], m[5], m[6]), Vector3(m[8], m[9], m[10])),
Vector3(m[12], m[13], m[14]))
static func multiply(a: PackedFloat32Array, b: PackedFloat32Array) -> PackedFloat32Array:
var out := PackedFloat32Array()
out.resize(16)
for r in 4:
for c in 4:
var sum := 0.0
for k in 4:
sum += a[r * 4 + k] * b[k * 4 + c]
out[r * 4 + c] = sum
return out
func update_frame() -> void:
_geometry_frame += 1
var draws: Array = Metin2PythonHost.render3d_draws()
var native_map: String = Metin2PythonHost.current_map_name()
if terrain != null and not native_map.is_empty() and native_map != map_path:
@@ -93,6 +96,7 @@ func update_frame() -> void:
var shown := 0
var camera_set := false
var light_set := false
var terrain_view := PackedFloat32Array()
for draw in draws:
# XYZRHW (screen-space) draws belong to the 2D pass; lines are debug geometry.
if draw["pretransformed"] or draw["lines"] or draw["indices"].is_empty():
@@ -102,21 +106,25 @@ func update_frame() -> void:
if proj[11] != -1.0:
continue
if not camera_set:
_apply_projection(proj)
_apply_projection(proj, draw)
camera_set = true
var view: PackedFloat32Array = draw["view"]
if terrain == null and not _terrain_attempted:
_load_terrain(draw)
if terrain != null:
_place_terrain(view)
terrain_view = view
if not light_set and draw["light0"]:
_apply_light(draw, view)
light_set = true
var instance := _mesh_instance(shown)
instance.transform = d3d_transform(multiply(draw["world"], view))
instance.mesh = _build_mesh(draw)
instance.transform = d3d_transform(view) * d3d_transform(draw["world"])
instance.mesh = _mesh_for_draw(draw)
instance.material_override = _material(draw)
instance.sorting_offset = float(shown) * 0.01
instance.visible = true
shown += 1
if terrain != null and not terrain_view.is_empty():
_place_terrain(terrain_view)
for i in range(shown, _meshes.size()):
_meshes[i].visible = false
_meshes[i].mesh = null
@@ -124,17 +132,75 @@ func update_frame() -> void:
light.visible = false
draw_count = shown
unlit_stand_in_count = 0
_update_background()
if _geometry_frame % 120 == 0:
for key in _geometry_cache.keys():
if _geometry_frame - int(_geometry_cache[key][2]) > 120:
_geometry_cache.erase(key)
func _update_background() -> void:
var bg_cmd: Dictionary = {}
for cmd in Metin2PythonHost.ui_render_commands_batched():
if cmd.get("behind_3d", false) and cmd.get("kind", "") == "image":
bg_cmd = cmd
break
if bg_cmd.is_empty():
if _bg_mesh != null:
_bg_mesh.visible = false
return
if _bg_mesh == null:
_bg_mesh = MeshInstance3D.new()
_bg_mesh.name = "BackgroundQuad"
_bg_mesh.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_bg_quad = QuadMesh.new()
_bg_mesh.mesh = _bg_quad
_bg_mat = StandardMaterial3D.new()
_bg_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
_bg_mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_ALWAYS
_bg_mat.cull_mode = BaseMaterial3D.CULL_DISABLED
_bg_mesh.material_override = _bg_mat
add_child(_bg_mesh)
var bg_tex_name: String = bg_cmd.get("text", "")
var tex := UiAssets.load_tex(AssetRoot.path(), bg_tex_name)
if tex is AtlasTexture:
var at: AtlasTexture = tex
_bg_mat.albedo_texture = at.atlas
var asize: Vector2 = at.atlas.get_size()
var r: Rect2 = at.region
_bg_mat.uv1_scale = Vector3(r.size.x / asize.x, r.size.y / asize.y, 1.0)
_bg_mat.uv1_offset = Vector3(r.position.x / asize.x, r.position.y / asize.y, 0.0)
else:
_bg_mat.albedo_texture = tex
if bg_cmd.has("uv"):
var uvs: PackedVector2Array = bg_cmd["uv"]
_bg_mat.uv1_scale = Vector3(uvs[3].x - uvs[0].x, uvs[3].y - uvs[0].y, 1.0)
_bg_mat.uv1_offset = Vector3(uvs[0].x, uvs[0].y, 0.0)
else:
_bg_mat.uv1_scale = Vector3(1.0, 1.0, 1.0)
_bg_mat.uv1_offset = Vector3(0.0, 0.0, 0.0)
var vp_size := get_viewport().get_visible_rect().size
var dist := 2800.0
var v_size := 2.0 * dist * tan(deg_to_rad(camera.fov) / 2.0)
var h_size := v_size * (float(vp_size.x) / float(vp_size.y))
_bg_quad.size = Vector2(h_size, v_size)
_bg_mesh.transform = Transform3D(Basis(), Vector3(0.0, 0.0, -dist))
_bg_mesh.visible = true
func _load_terrain(draw: Dictionary) -> void:
if not ClassDB.class_exists("Metin2World"):
return
var native_map: String = Metin2PythonHost.current_map_name()
if native_map.is_empty():
return
var camera_world := d3d_transform(draw["view"]).affine_inverse().origin
var focus := Vector2(camera_world.x, -camera_world.y)
if focus.distance_to(_last_terrain_focus) < 1000.0:
return
_last_terrain_focus = focus
var native_map: String = Metin2PythonHost.current_map_name()
map_path = native_map if not native_map.is_empty() else _map_for_position(focus)
map_path = native_map
terrain = ClassDB.instantiate("Metin2World")
terrain.name = "PackTerrain"
terrain.set("auto_load", false)
@@ -212,11 +278,38 @@ func _place_terrain(view: PackedFloat32Array) -> void:
Vector3(0, -100, 0)), Vector3.ZERO)
terrain.transform = d3d_transform(view) * from_map
func _apply_projection(proj: PackedFloat32Array) -> void:
func _apply_projection(proj: PackedFloat32Array, draw: Dictionary = {}) -> void:
# D3DXMatrixPerspectiveFovRH: _22 = cot(fovy / 2), _33 = zf / (zn - zf), _43 = zn * zf / (zn - zf).
camera.fov = rad_to_deg(2.0 * atan(1.0 / proj[5]))
camera.near = proj[14] / proj[10]
camera.far = proj[14] / (proj[10] + 1.0)
var fov_deg := rad_to_deg(2.0 * atan(1.0 / proj[5]))
var zn: float = proj[14] / proj[10]
var zf: float = proj[14] / (proj[10] + 1.0)
camera.fov = fov_deg
camera.near = zn
camera.far = zf
var vp_offset := Vector2.ZERO
if draw.has("viewport"):
var vp: PackedFloat32Array = draw["viewport"]
var vp_x: float = vp[0]
var vp_y: float = vp[1]
var vp_w: float = vp[2]
var vp_h: float = vp[3]
var vp_size := get_viewport().get_visible_rect().size
if vp_w > 0.0 and vp_h > 0.0 and (vp_w < vp_size.x or vp_h < vp_size.y or vp_x > 0.0 or vp_y > 0.0):
var offset_px_x: float = (vp_x + vp_w / 2.0) - (vp_size.x / 2.0)
var offset_px_y: float = (vp_y + vp_h / 2.0) - (vp_size.y / 2.0)
if absf(offset_px_x) > 0.5 or absf(offset_px_y) > 0.5:
var v_size_near := 2.0 * zn * tan(deg_to_rad(fov_deg) / 2.0)
var h_size_near := v_size_near * (vp_size.x / vp_size.y)
var off_near_x := (offset_px_x / vp_size.x) * h_size_near
var off_near_y := -(offset_px_y / vp_size.y) * v_size_near
vp_offset = Vector2(-off_near_x, off_near_y)
var v_size_near := 2.0 * zn * tan(deg_to_rad(fov_deg) / 2.0)
if vp_offset != Vector2.ZERO:
camera.set_frustum(v_size_near, vp_offset, zn, zf)
else:
camera.set_perspective(fov_deg, zn, zf)
func _apply_light(draw: Dictionary, view: PackedFloat32Array) -> void:
var view_basis := d3d_transform(view).basis
@@ -253,6 +346,19 @@ func _mesh_instance(index: int) -> MeshInstance3D:
_meshes.append(instance)
return _meshes[index]
func _mesh_for_draw(draw: Dictionary) -> ArrayMesh:
var key: int = draw.get("geometry_key", 0)
if key == 0:
return _build_mesh(draw)
var revision: int = draw.get("geometry_revision", 0)
var cached: Array = _geometry_cache.get(key, [])
if not cached.is_empty() and int(cached[0]) == revision:
cached[2] = _geometry_frame
return cached[1]
var mesh := _build_mesh(draw)
_geometry_cache[key] = [revision, mesh, _geometry_frame]
return mesh
func _build_mesh(draw: Dictionary) -> ArrayMesh:
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
@@ -266,15 +372,27 @@ func _build_mesh(draw: Dictionary) -> ArrayMesh:
arrays[Mesh.ARRAY_INDEX] = draw["indices"]
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
mesh.surface_set_material(0, _material(draw))
return mesh
func _material(draw: Dictionary) -> StandardMaterial3D:
var lit: bool = draw["lighting"]
var z_write: int = draw.get("z_write", 1)
var tf: int = int(draw.get("texture_factor", 0xFFFFFFFF))
var tf_a: float = float((tf >> 24) & 0xFF) / 255.0
var tf_r: float = float((tf >> 16) & 0xFF) / 255.0
var tf_g: float = float((tf >> 8) & 0xFF) / 255.0
var tf_b: float = float(tf & 0xFF) / 255.0
var factor_color := Color(tf_r, tf_g, tf_b, tf_a)
var uses_tf: bool = bool(draw.get("uses_tf", false))
var has_tf: bool = uses_tf and (tf != 0xFFFFFFFF and tf != -1)
var tf_key := "%02x%02x%02x%02x" % [int(tf_r * 63.0), int(tf_g * 63.0), int(tf_b * 63.0), int(tf_a * 63.0)] if has_tf else ""
var has_diffuse: bool = draw.has("diffuse")
var is_alpha: bool = draw["alpha_blend"] or (has_tf and factor_color.a < 0.99)
# alpha_blend / alpha_test / lit are bools: %d needs them as ints.
var key := "%s|%d|%d|%d|%d|%d|%d|%d|%s|%s" % [draw["texture0"], int(draw["alpha_blend"]), draw["src_blend"],
var key := "%s|%d|%d|%d|%d|%d|%d|%d|%d|%d|%s|%s|%s" % [draw["texture0"], int(draw["alpha_blend"]), draw["src_blend"],
draw["dest_blend"], int(draw["alpha_test"]), draw["alpha_ref"], draw["cull_mode"], int(lit),
draw["material_diffuse"], draw["material_emissive"]]
int(has_diffuse), z_write, draw["material_diffuse"], draw["material_emissive"], tf_key]
if _materials.has(key):
return _materials[key]
var material := StandardMaterial3D.new()
@@ -282,7 +400,14 @@ func _material(draw: Dictionary) -> StandardMaterial3D:
if not texture_name.is_empty():
# The ported client reads its models from 40250's packs; the texture must come from there too.
material.albedo_texture = UiAssets.load_pack_tex(texture_name)
material.albedo_color = draw["material_diffuse"] if lit else Color.WHITE
var base_diffuse: Color = draw["material_diffuse"] if lit else Color.WHITE
if has_tf:
material.albedo_color = Color(base_diffuse.r * factor_color.r, base_diffuse.g * factor_color.g,
base_diffuse.b * factor_color.b, base_diffuse.a * factor_color.a)
else:
material.albedo_color = base_diffuse
material.emission_enabled = lit and draw["material_emissive"] != Color(0, 0, 0, 0)
if material.emission_enabled:
material.emission = draw["material_emissive"]
@@ -296,18 +421,28 @@ func _material(draw: Dictionary) -> StandardMaterial3D:
else BaseMaterial3D.SHADING_MODE_UNSHADED
material.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
material.roughness = 1.0
material.vertex_color_use_as_albedo = draw.has("diffuse")
material.vertex_color_use_as_albedo = has_diffuse
match int(draw["cull_mode"]):
D3DCULL_NONE:
material.cull_mode = BaseMaterial3D.CULL_DISABLED
D3DCULL_CW:
material.cull_mode = BaseMaterial3D.CULL_BACK
D3DCULL_CCW:
material.cull_mode = BaseMaterial3D.CULL_FRONT
_:
material.cull_mode = BaseMaterial3D.CULL_BACK
if draw["alpha_blend"]:
if z_write == 0:
material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
elif is_alpha:
material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_ALWAYS
else:
material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_OPAQUE_ONLY
if is_alpha:
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
if int(draw["dest_blend"]) == D3DBLEND_ONE:
material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
else:
material.blend_mode = BaseMaterial3D.BLEND_MODE_MIX
elif draw["alpha_test"]:
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
material.alpha_scissor_threshold = float(draw["alpha_ref"]) / 255.0
+126 -11
View File
@@ -2,6 +2,7 @@
# 登录 → 选人 → 进游戏,把 RenderGame 的绘制命令(python_3d_surface.gd)画出来并截图。
#
# godot --path project --script res://python_game_render_test.gd
# MT_FAKE_MOB_COUNT=24 MT_PROFILE_FRAME=1 MT_PROFILE_COMBAT=1 script/python_game_render_test.sh
#
# 服务器与账号只从环境变量读(MT_LIVE_HOST / MT_LIVE_LOGIN / MT_LIVE_PASSWORD,可选 MT_LIVE_AUTH_PORT
# 11000、MT_LIVE_GAME_PORT 13000、MT_LIVE_SLOT 0);Python 侧也用 os.environ 取,不进源码文本。
@@ -72,8 +73,12 @@ func run() -> void:
var output := OS.get_environment("MT_RENDER_OUTPUT")
if output.is_empty():
output = ProjectSettings.globalize_path("res://../build/rendering/python-game-%d" % Time.get_unix_time_from_system())
elif not output.is_absolute_path():
output = ProjectSettings.globalize_path("res://../" + output)
DirAccess.make_dir_recursive_absolute(output)
var report := {"output": output, "headless": DisplayServer.get_name() == "headless"}
if OS.has_environment("MT_FAKE_MOB_COUNT"):
report["fake_mob_count"] = int(OS.get_environment("MT_FAKE_MOB_COUNT"))
var host := OS.get_environment("MT_LIVE_HOST")
if host.is_empty() or OS.get_environment("MT_LIVE_LOGIN").is_empty() or OS.get_environment("MT_LIVE_PASSWORD").is_empty():
@@ -132,9 +137,63 @@ func run() -> void:
# The main character's draws reach the 3D surface.
var drawn := in_game and await _pump_until(10, func(): return world.draw_count > 0)
_check(drawn, "RenderGame draws shown (%d)" % world.draw_count)
# Let the WAIT motion run a little before the picture.
for i in 30:
# Let the WAIT motion run a little before the picture, and let GamePhase (3 packets/frame)
# finish draining the entergame burst + GC_PING when MT_FAKE_MOB_COUNT is large.
var warmup_frames := maxi(30, int(report.get("fake_mob_count", 1)) + 20)
for i in warmup_frames:
await process_frame
if OS.has_environment("MT_PROFILE_FRAME"):
var frame_ms: Array[float] = []
var ui_update_ms: Array[float] = []
var ui_draw_ms: Array[float] = []
var world_update_ms: Array[float] = []
var previous := Time.get_ticks_usec()
for i in 240:
await process_frame
var current := Time.get_ticks_usec()
frame_ms.append(float(current - previous) / 1000.0)
ui_update_ms.append(ui.profile_ui_update_ms)
ui_draw_ms.append(ui.profile_draw_ms)
world_update_ms.append(world.profile_update_ms)
previous = current
frame_ms.sort()
ui_update_ms.sort()
ui_draw_ms.sort()
world_update_ms.sort()
var total := 0.0
for sample in frame_ms:
total += sample
report["frame_profile"] = {
"samples": frame_ms.size(),
"mean_ms": total / frame_ms.size(),
"p50_ms": frame_ms[119],
"p95_ms": frame_ms[227],
"ui_update_p50_ms": ui_update_ms[119],
"ui_draw_p50_ms": ui_draw_ms[119],
"world_update_p50_ms": world_update_ms[119],
}
var command_kinds := {}
var image_texts := {}
for command in Metin2PythonHost.ui_render_commands():
var kind: String = str(command.get("kind", "unknown"))
command_kinds[kind] = int(command_kinds.get(kind, 0)) + 1
if kind == "image":
var image_name: String = str(command.get("text", ""))
image_texts[image_name] = int(image_texts.get(image_name, 0)) + 1
report["frame_profile"]["ui_commands"] = command_kinds
report["frame_profile"]["image_texts"] = image_texts
var batched_commands := 0
var glyph_batches := 0
var batched_glyphs := 0
for command in Metin2PythonHost.ui_render_commands_batched():
batched_commands += 1
if command.get("kind", "") == "glyph_batch":
glyph_batches += 1
batched_glyphs += int(command.get("glyph_count", 0))
report["frame_profile"]["batched_commands"] = batched_commands
report["frame_profile"]["glyph_batches"] = glyph_batches
report["frame_profile"]["batched_glyphs"] = batched_glyphs
print("FRAME_PROFILE ", report["frame_profile"])
report["draw_count"] = world.draw_count
report["unlit_stand_in_count"] = world.unlit_stand_in_count
report["terrain"] = world.terrain_report
@@ -170,14 +229,25 @@ func run() -> void:
var draws: Array = Metin2PythonHost.render3d_draws()
var native_lit_draws := 0
var view_finite := true
var actor_foot_cm := INF
var foot_xy := Vector2.ZERO
# Identify character, monster and NPC draws (excluding map objects, buildings, trees and roofs).
var actor_draws: Array = []
for draw in draws:
var tex: String = String(draw.get("texture0", "")).to_lower().replace("\\", "/")
for k in ["pc/", "pc2/", "monster/", "npc/"]:
if k in tex:
actor_draws.append(draw)
break
for draw in draws:
if draw["light0"]:
native_lit_draws += 1
for value in draw["view"]:
if not is_finite(value):
view_finite = false
var actor_foot_cm := INF
var foot_xy := Vector2.ZERO
for draw in actor_draws:
var world_xform: Transform3D = world.d3d_transform(draw["world"])
for vertex in draw["positions"]:
var p: Vector3 = world_xform * vertex
@@ -187,14 +257,14 @@ func run() -> void:
# Every actor, not only the main one, stands on the map. An actor's parts (body, head, hair, teeth)
# share its world matrix; the lowest vertex over those parts meets sample_height.
var actor_low := {}
for draw in draws:
for draw in actor_draws:
var draw_xform: Transform3D = world.d3d_transform(draw["world"])
var key := str(draw["world"])
for vertex in draw["positions"]:
var p: Vector3 = draw_xform * vertex
if not actor_low.has(key) or p.z < actor_low[key].z:
actor_low[key] = p
if world.terrain:
if world.terrain and not actor_low.is_empty():
var worst_actor_gap_cm := 0.0
for low in actor_low.values():
var ground := float(world.terrain.call("sample_height", low.x / 100.0, -low.y / 100.0)) * 100.0
@@ -244,7 +314,7 @@ func run() -> void:
var distinct_textures := {}
for instance in world.find_children("Draw*", "MeshInstance3D", false, false):
if instance.visible and instance.mesh:
var material: StandardMaterial3D = instance.mesh.surface_get_material(0)
var material: StandardMaterial3D = instance.get_active_material(0)
if material and material.albedo_texture:
surfaces_textured += 1
distinct_textures[material.albedo_texture] = true
@@ -353,7 +423,7 @@ func run() -> void:
for frame in 600:
await process_frame
now = dog_screen.call()
if now.distance_to(last) < 0.5:
if now.x >= 0.0 and now.distance_to(last) < 0.5:
break
last = now
return now
@@ -377,15 +447,60 @@ func run() -> void:
await process_frame
button.call(true, at)
var mid_shot := false
var dead := await _pump_until(20, func():
var combat_condition := func():
var now: Vector2 = dog_screen.call()
if now.x >= 0.0:
at = now
move.call(at)
if not mid_shot and Metin2PythonHost.evaluate("33 in _target_hp") == "True" and DisplayServer.get_name() != "headless":
if not mid_shot and Metin2PythonHost.evaluate("33 in _target_hp") == "True" and DisplayServer.get_name() != "headless" and not OS.has_environment("MT_PROFILE_COMBAT"):
mid_shot = true
root.get_texture().get_image().save_png(output.path_join("combat_mid.png"))
return Metin2PythonHost.evaluate("_target_hp[-1:] == [0]") == "True")
return Metin2PythonHost.evaluate("_target_hp[-1:] == [0]") == "True"
var dead := false
if OS.has_environment("MT_PROFILE_COMBAT"):
var combat_frames: Array[float] = []
var combat_ui_update: Array[float] = []
var combat_ui_draw: Array[float] = []
var combat_world_update: Array[float] = []
var combat_draw_counts: Array[int] = []
var deadline := Time.get_ticks_msec() + 20000
var previous := Time.get_ticks_usec()
while Time.get_ticks_msec() < deadline:
if combat_condition.call():
dead = true
break
await process_frame
var current := Time.get_ticks_usec()
combat_frames.append(float(current - previous) / 1000.0)
combat_ui_update.append(ui.profile_ui_update_ms)
combat_ui_draw.append(ui.profile_draw_ms)
combat_world_update.append(world.profile_update_ms)
combat_draw_counts.append(world.draw_count)
previous = current
combat_frames.sort()
combat_ui_update.sort()
combat_ui_draw.sort()
combat_world_update.sort()
combat_draw_counts.sort()
if not combat_frames.is_empty():
var n := combat_frames.size()
report["combat_profile"] = {
"samples": n,
"frame_p50_ms": combat_frames[n / 2],
"frame_p95_ms": combat_frames[mini(n - 1, int(n * 0.95))],
"frame_max_ms": combat_frames[n - 1],
"ui_update_p50_ms": combat_ui_update[n / 2],
"ui_update_p95_ms": combat_ui_update[mini(n - 1, int(n * 0.95))],
"ui_draw_p50_ms": combat_ui_draw[n / 2],
"ui_draw_p95_ms": combat_ui_draw[mini(n - 1, int(n * 0.95))],
"world_update_p50_ms": combat_world_update[n / 2],
"world_update_p95_ms": combat_world_update[mini(n - 1, int(n * 0.95))],
"draw_count_p50": combat_draw_counts[n / 2],
"draw_count_max": combat_draw_counts[n - 1],
}
print("COMBAT_PROFILE ", report["combat_profile"])
else:
dead = await _pump_until(20, combat_condition)
button.call(false, at)
if DisplayServer.get_name() != "headless":
root.get_texture().get_image().save_png(output.path_join("combat.png"))
+284 -70
View File
@@ -8,16 +8,26 @@ const UiAssets = preload("res://ui/ui_assets.gd")
const KEY_TO_DIK := {
KEY_ESCAPE: 0x01, KEY_1: 0x02, KEY_2: 0x03, KEY_3: 0x04,
KEY_4: 0x05, KEY_5: 0x06, KEY_6: 0x07, KEY_7: 0x08,
KEY_8: 0x09, KEY_9: 0x0a, KEY_0: 0x0b, KEY_BACKSPACE: 0x0e,
KEY_TAB: 0x0f, KEY_Q: 0x10, KEY_W: 0x11, KEY_E: 0x12,
KEY_R: 0x13, KEY_T: 0x14, KEY_Y: 0x15, KEY_U: 0x16,
KEY_I: 0x17, KEY_O: 0x18, KEY_P: 0x19, KEY_ENTER: 0x1c,
KEY_A: 0x1e, KEY_S: 0x1f, KEY_D: 0x20, KEY_F: 0x21,
KEY_G: 0x22, KEY_H: 0x23, KEY_J: 0x24, KEY_K: 0x25,
KEY_L: 0x26, KEY_Z: 0x2c, KEY_X: 0x2d, KEY_C: 0x2e,
KEY_8: 0x09, KEY_9: 0x0a, KEY_0: 0x0b, KEY_MINUS: 0x0c,
KEY_EQUAL: 0x0d, KEY_BACKSPACE: 0x0e, KEY_TAB: 0x0f,
KEY_Q: 0x10, KEY_W: 0x11, KEY_E: 0x12, KEY_R: 0x13,
KEY_T: 0x14, KEY_Y: 0x15, KEY_U: 0x16, KEY_I: 0x17,
KEY_O: 0x18, KEY_P: 0x19, KEY_BRACKETLEFT: 0x1a, KEY_BRACKETRIGHT: 0x1b,
KEY_ENTER: 0x1c, KEY_CTRL: 0x1d, KEY_A: 0x1e, KEY_S: 0x1f,
KEY_D: 0x20, KEY_F: 0x21, KEY_G: 0x22, KEY_H: 0x23,
KEY_J: 0x24, KEY_K: 0x25, KEY_L: 0x26, KEY_SEMICOLON: 0x27,
KEY_APOSTROPHE: 0x28, KEY_QUOTELEFT: 0x29, KEY_SHIFT: 0x2a,
KEY_BACKSLASH: 0x2b, KEY_Z: 0x2c, KEY_X: 0x2d, KEY_C: 0x2e,
KEY_V: 0x2f, KEY_B: 0x30, KEY_N: 0x31, KEY_M: 0x32,
KEY_SPACE: 0x39, KEY_UP: 0xc8, KEY_LEFT: 0xcb,
KEY_RIGHT: 0xcd, KEY_DOWN: 0xd0,
KEY_COMMA: 0x33, KEY_PERIOD: 0x34, KEY_SLASH: 0x35,
KEY_ALT: 0x38, KEY_SPACE: 0x39,
KEY_F1: 0x3b, KEY_F2: 0x3c, KEY_F3: 0x3d, KEY_F4: 0x3e,
KEY_F5: 0x3f, KEY_F6: 0x40, KEY_F7: 0x41, KEY_F8: 0x42,
KEY_F9: 0x43, KEY_F10: 0x44, KEY_F11: 0x57, KEY_F12: 0x58,
KEY_HOME: 0xc7, KEY_UP: 0xc8, KEY_PAGEUP: 0xc9,
KEY_LEFT: 0xcb, KEY_RIGHT: 0xcd, KEY_END: 0xcf,
KEY_DOWN: 0xd0, KEY_PAGEDOWN: 0xd1, KEY_INSERT: 0xd2,
KEY_DELETE: 0xd3,
}
# Win32 virtual-key codes of the keys EditLine.OnIMEKeyDown handles (WM_KEYDOWN → OnIMEKeyDown);
@@ -36,6 +46,11 @@ const KEY_TO_CHAR := {
# "mem:<id>" → [revision, ImageTexture]: the CGraphicFontTexture glyph pages, refetched when the
# "@<revision>" of a command's name moves on (a glyph was added to the page).
var _memory_textures := {}
var _tex_info_cache := {}
var _color_cache := {}
var _white_color_array := PackedColorArray([Color.WHITE])
var _asset_root_path := ""
var _segments_used := 0
# True when this surface started system.py itself (run_app) and so owns the interpreter's lifetime.
var _owns_app := false
@@ -80,6 +95,8 @@ func _exit_tree() -> void:
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP
texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED
_asset_root_path = AssetRoot.path()
_sync_size()
func _notification(what: int) -> void:
@@ -100,7 +117,10 @@ func _process(_delta: float) -> void:
get_tree().quit(0)
return
if Metin2PythonHost.is_running():
var profile_start := Time.get_ticks_usec() if _profile_frame else 0
Metin2PythonHost.ui_update()
if profile_start != 0:
profile_ui_update_ms = float(Time.get_ticks_usec() - profile_start) / 1000.0
queue_redraw()
func _gui_input(event: InputEvent) -> void:
@@ -112,10 +132,30 @@ func _gui_input(event: InputEvent) -> void:
if event.button_index >= MOUSE_BUTTON_LEFT and event.button_index <= MOUSE_BUTTON_MIDDLE:
Metin2PythonHost.ui_mouse_button(event.button_index, event.pressed,
int(event.position.x), int(event.position.y))
elif event.pressed:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
var factor: float = event.factor if event.factor > 0.0 else 1.0
Metin2PythonHost.ui_mouse_wheel(int(round(120.0 * factor)))
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
var factor: float = event.factor if event.factor > 0.0 else 1.0
Metin2PythonHost.ui_mouse_wheel(-int(round(120.0 * factor)))
elif event is InputEventPanGesture:
# macOS trackpad two-finger scroll: scrolling up has negative delta.y, zooming in
var delta: int = -int(round(event.delta.y * 30.0))
if delta != 0:
Metin2PythonHost.ui_mouse_wheel(delta)
elif event is InputEventMagnifyGesture:
# macOS trackpad pinch to zoom: factor > 1.0 is zoom in
var delta: int = int(round((event.factor - 1.0) * 600.0))
if delta != 0:
Metin2PythonHost.ui_mouse_wheel(delta)
func _unhandled_key_input(event: InputEvent) -> void:
if event is InputEventKey and Metin2PythonHost.is_running():
var dik: int = KEY_TO_DIK.get(event.physical_keycode, 0)
var code: int = event.physical_keycode
var dik: int = KEY_TO_DIK.get(code, 0)
if dik == 0 and event.keycode != 0:
dik = KEY_TO_DIK.get(event.keycode, 0)
if dik != 0:
Metin2PythonHost.ui_key(dik, event.pressed)
if event.pressed:
@@ -159,79 +199,247 @@ var _segments: Array[RID] = []
var _materials: Array[RID] = []
var _mask_shader := RID()
var _white: ImageTexture
var _profile_frame := OS.has_environment("MT_PROFILE_FRAME")
var profile_ui_update_ms := 0.0
var profile_draw_ms := 0.0
func _get_color_array(argb: int) -> PackedColorArray:
if argb == 0xFFFFFFFF or argb == -1:
return _white_color_array
var arr: PackedColorArray = _color_cache.get(argb, PackedColorArray())
if not arr.is_empty():
return arr
var c := Color8((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255)
arr = PackedColorArray([c])
_color_cache[argb] = arr
return arr
func _get_tex_draw_info(name: String) -> Array:
var info: Array = _tex_info_cache.get(name, [])
if not info.is_empty():
return info
var texture: Texture2D = _texture(name)
if texture == null:
_tex_info_cache[name] = []
return []
var tex_rid: RID = texture.get_rid()
var is_atlas := false
var u0 := 0.0
var v0 := 0.0
var du := 1.0
var dv := 1.0
var default_uv := PackedVector2Array()
if texture is AtlasTexture:
var at: AtlasTexture = texture
if at.atlas != null:
tex_rid = at.atlas.get_rid()
var asize: Vector2 = at.atlas.get_size()
if asize.x > 0 and asize.y > 0:
is_atlas = true
var r: Rect2 = at.region
u0 = r.position.x / asize.x
v0 = r.position.y / asize.y
du = r.size.x / asize.x
dv = r.size.y / asize.y
default_uv = PackedVector2Array([
Vector2(u0, v0),
Vector2(u0 + du, v0),
Vector2(u0 + du, v0 + dv),
Vector2(u0, v0 + dv)
])
if default_uv.is_empty():
default_uv = PackedVector2Array([
Vector2(0, 0),
Vector2(1, 0),
Vector2(1, 1),
Vector2(0, 1)
])
info = [tex_rid, is_atlas, u0, v0, du, dv, default_uv, texture]
_tex_info_cache[name] = info
return info
func _draw() -> void:
var used := 0
var profile_start := Time.get_ticks_usec() if _profile_frame else 0
var fg_used := 0
var fg_plain := false
var masked := 0
var plain := false
for segment in _segments:
RenderingServer.canvas_item_clear(segment)
for i in _segments_used:
RenderingServer.canvas_item_clear(_segments[i])
_segments_used = 0
if not Metin2PythonHost.is_running():
return
for command in Metin2PythonHost.ui_render_commands():
var argb: int = command["argb"]
var color := Color8((argb >> 16) & 255, (argb >> 8) & 255,
argb & 255, (argb >> 24) & 255)
var p1 := Vector2(command["x1"], command["y1"])
var p2 := Vector2(command["x2"], command["y2"])
var clip := Rect2(Vector2(command["clip_x1"], command["clip_y1"]),
Vector2(command["clip_x2"] - command["clip_x1"],
command["clip_y2"] - command["clip_y1"]))
if command["kind"] == "bar":
var visible := Rect2(p1, p2 - p1).intersection(clip)
if visible.has_area():
if not plain:
used = _segment(used, RID())
plain = true
RenderingServer.canvas_item_add_rect(_segments[used - 1], visible, color)
elif command["kind"] == "gradient_bar":
var visible := Rect2(p1, p2 - p1).intersection(clip)
if visible.has_area():
if not plain:
used = _segment(used, RID())
plain = true
var bottom_argb: int = command["end_argb"]
var has_3d: bool = Metin2PythonHost.has_3d_draws()
for command in Metin2PythonHost.ui_render_commands_batched():
# Skip background UI commands when 3D is active, as the 3D surface renders the background Quad
if has_3d and bool(command.get(&"behind_3d", false)):
continue
var kind: Variant = command[&"kind"]
if kind == &"glyph_batch":
var info := _get_tex_draw_info(command[&"text"])
if info.is_empty():
continue
if not fg_plain:
fg_used = _segment(fg_used, RID())
fg_plain = true
RenderingServer.canvas_item_add_triangle_array(_segments[fg_used - 1],
command[&"indices"], command[&"points"], command[&"colors"],
command[&"uvs"], PackedInt32Array(), PackedFloat32Array(), info[0])
continue
var x1: float = command[&"x1"]
var y1: float = command[&"y1"]
var x2: float = command[&"x2"]
var y2: float = command[&"y2"]
var cx1: float = command[&"clip_x1"]
var cy1: float = command[&"clip_y1"]
var cx2: float = command[&"clip_x2"]
var cy2: float = command[&"clip_y2"]
# 1) Completely culled by scissor?
if x2 <= cx1 or x1 >= cx2 or y2 <= cy1 or y1 >= cy2:
continue
var argb: int = command[&"argb"]
var target_segments := _segments
if kind == &"image":
var text_name: String = command.get(&"text", "")
var mask_name: String = command.get(&"mask", "")
var mask_tex: Texture2D = _texture(mask_name) if not mask_name.is_empty() else null
if text_name.is_empty() and mask_tex != null:
text_name = "__white__"
var info := _get_tex_draw_info(text_name)
if info.is_empty():
continue
var tex_rid: RID = info[0]
var is_atlas: bool = info[1]
var u0: float = info[2]
var v0: float = info[3]
var du: float = info[4]
var dv: float = info[5]
var quad: PackedVector2Array = command[&"quad"]
var quad_uv: PackedVector2Array = command[&"uv"]
var needs_clip := x1 < cx1 or x2 > cx2 or y1 < cy1 or y2 > cy2
var poly_pts: PackedVector2Array
var poly_uv: PackedVector2Array
if not needs_clip:
poly_pts = PackedVector2Array([quad[0], quad[1], quad[3], quad[2]])
var is_full_uv := quad_uv[0] == Vector2.ZERO and quad_uv[1] == Vector2(1, 0) and quad_uv[2] == Vector2(0, 1) and quad_uv[3] == Vector2.ONE
if is_full_uv:
poly_uv = info[6]
elif is_atlas:
poly_uv = PackedVector2Array([
Vector2(u0 + quad_uv[0].x * du, v0 + quad_uv[0].y * dv),
Vector2(u0 + quad_uv[1].x * du, v0 + quad_uv[1].y * dv),
Vector2(u0 + quad_uv[3].x * du, v0 + quad_uv[3].y * dv),
Vector2(u0 + quad_uv[2].x * du, v0 + quad_uv[2].y * dv)
])
else:
poly_uv = PackedVector2Array([quad_uv[0], quad_uv[1], quad_uv[3], quad_uv[2]])
else:
var mapped_uv: PackedVector2Array
if is_atlas:
mapped_uv = PackedVector2Array([
Vector2(u0 + quad_uv[0].x * du, v0 + quad_uv[0].y * dv),
Vector2(u0 + quad_uv[1].x * du, v0 + quad_uv[1].y * dv),
Vector2(u0 + quad_uv[2].x * du, v0 + quad_uv[2].y * dv),
Vector2(u0 + quad_uv[3].x * du, v0 + quad_uv[3].y * dv)
])
else:
mapped_uv = quad_uv
var clip_rect := Rect2(cx1, cy1, cx2 - cx1, cy2 - cy1)
var clipped := _clip_quad(quad, mapped_uv, clip_rect)
if clipped[0].size() < 3:
continue
poly_pts = clipped[0]
poly_uv = clipped[1]
if mask_tex != null:
var mat_rid := _mask_material(masked, mask_tex, quad_uv, command[&"mask_uv"])
fg_used = _segment(fg_used, mat_rid)
fg_plain = false
masked += 1
elif not fg_plain:
fg_used = _segment(fg_used, RID())
fg_plain = true
var seg_idx := fg_used - 1
RenderingServer.canvas_item_add_polygon(target_segments[seg_idx], poly_pts,
_get_color_array(argb), poly_uv, tex_rid)
elif kind == &"bar":
var bx1 := maxf(x1, cx1)
var by1 := maxf(y1, cy1)
var bx2 := minf(x2, cx2)
var by2 := minf(y2, cy2)
if bx2 > bx1 and by2 > by1:
if not fg_plain:
fg_used = _segment(fg_used, RID())
fg_plain = true
var seg_idx := fg_used - 1
var color := Color8((argb >> 16) & 255, (argb >> 8) & 255,
argb & 255, (argb >> 24) & 255)
RenderingServer.canvas_item_add_rect(target_segments[seg_idx],
Rect2(bx1, by1, bx2 - bx1, by2 - by1), color)
elif kind == &"gradient_bar":
var bx1 := maxf(x1, cx1)
var by1 := maxf(y1, cy1)
var bx2 := minf(x2, cx2)
var by2 := minf(y2, cy2)
if bx2 > bx1 and by2 > by1:
if not fg_plain:
fg_used = _segment(fg_used, RID())
fg_plain = true
var seg_idx := fg_used - 1
var color := Color8((argb >> 16) & 255, (argb >> 8) & 255,
argb & 255, (argb >> 24) & 255)
var bottom_argb: int = command[&"end_argb"]
var bottom := Color8((bottom_argb >> 16) & 255, (bottom_argb >> 8) & 255,
bottom_argb & 255, (bottom_argb >> 24) & 255)
var top_factor := (visible.position.y - p1.y) / (p2.y - p1.y)
var bottom_factor := (visible.end.y - p1.y) / (p2.y - p1.y)
RenderingServer.canvas_item_add_polygon(_segments[used - 1],
PackedVector2Array([visible.position, Vector2(visible.end.x, visible.position.y),
visible.end, Vector2(visible.position.x, visible.end.y)]),
var top_factor := (by1 - y1) / (y2 - y1) if y2 != y1 else 0.0
var bottom_factor := (by2 - y1) / (y2 - y1) if y2 != y1 else 1.0
RenderingServer.canvas_item_add_polygon(target_segments[seg_idx],
PackedVector2Array([Vector2(bx1, by1), Vector2(bx2, by1),
Vector2(bx2, by2), Vector2(bx1, by2)]),
PackedColorArray([color.lerp(bottom, top_factor), color.lerp(bottom, top_factor),
color.lerp(bottom, bottom_factor), color.lerp(bottom, bottom_factor)]))
elif command["kind"] == "line":
var segment := _clip_line(p1, p2, clip)
elif kind == &"line":
var clip_rect := Rect2(cx1, cy1, cx2 - cx1, cy2 - cy1)
var p1 := Vector2(x1, y1)
var p2 := Vector2(x2, y2)
var segment := _clip_line(p1, p2, clip_rect)
if segment.size() == 2:
if not plain:
used = _segment(used, RID())
plain = true
RenderingServer.canvas_item_add_line(_segments[used - 1], segment[0], segment[1], color)
elif command["kind"] == "image":
var texture: Texture2D = _texture(command["text"])
var mask: Texture2D = _texture(command["mask"]) if command.has("mask") else null
if mask != null and texture == null:
texture = _white_texture()
if texture != null:
var clipped := _clip_quad(command["quad"], command["uv"], clip)
if clipped[0].size() >= 3:
if mask != null:
used = _segment(used, _mask_material(masked, mask, command["uv"], command["mask_uv"]))
masked += 1
plain = false
elif not plain:
used = _segment(used, RID())
plain = true
RenderingServer.canvas_item_add_polygon(_segments[used - 1], clipped[0],
PackedColorArray([color]), clipped[1], texture.get_rid())
if not fg_plain:
fg_used = _segment(fg_used, RID())
fg_plain = true
var seg_idx := fg_used - 1
var color := Color8((argb >> 16) & 255, (argb >> 8) & 255,
argb & 255, (argb >> 24) & 255)
RenderingServer.canvas_item_add_line(target_segments[seg_idx], segment[0], segment[1], color)
_segments_used = fg_used
if profile_start != 0:
profile_draw_ms = float(Time.get_ticks_usec() - profile_start) / 1000.0
# Opens the next pooled child item (draw order = index) with the given material; returns the count used.
func _segment(used: int, material: RID) -> int:
if used == _segments.size():
var pool := _segments
var parent_item: RID = get_canvas_item()
if used == pool.size():
var item := RenderingServer.canvas_item_create()
RenderingServer.canvas_item_set_parent(item, get_canvas_item())
_segments.push_back(item)
var item := _segments[used]
RenderingServer.canvas_item_set_parent(item, parent_item)
RenderingServer.canvas_item_set_default_texture_repeat(item, RenderingServer.CANVAS_ITEM_TEXTURE_REPEAT_ENABLED)
pool.push_back(item)
var item := pool[used]
RenderingServer.canvas_item_set_draw_index(item, used)
RenderingServer.canvas_item_set_material(item, material)
return used + 1
@@ -260,8 +468,11 @@ func _white_texture() -> Texture2D:
return _white
func _texture(name: String) -> Texture2D:
if name == "__white__":
return _white_texture()
if not name.begins_with("mem:"):
return UiAssets.load_tex(AssetRoot.path(), name)
var root := _asset_root_path if not _asset_root_path.is_empty() else AssetRoot.path()
return UiAssets.load_tex(root, name)
var at := name.find("@")
var key := name.substr(0, at) if at >= 0 else name
var revision := int(name.substr(at + 1)) if at >= 0 else 0
@@ -292,6 +503,9 @@ func _clip_quad(quad: PackedVector2Array, uv: PackedVector2Array, clip: Rect2) -
var det := edge_u.cross(edge_v)
if is_zero_approx(det):
return [PackedVector2Array(), PackedVector2Array()]
if clip.has_point(quad[0]) and clip.has_point(quad[1]) \
and clip.has_point(quad[2]) and clip.has_point(quad[3]):
return [outline, PackedVector2Array([uv[0], uv[1], uv[3], uv[2]])]
var rect := PackedVector2Array([clip.position, Vector2(clip.end.x, clip.position.y),
clip.end, Vector2(clip.position.x, clip.end.y)])
var pieces := Geometry2D.intersect_polygons(outline, rect)
+14
View File
@@ -0,0 +1,14 @@
@tool
extends SceneTree
func _init() -> void:
var tw = Metin2World.new()
tw.set("assets_root", "pack://")
tw.set("map_path", "metin2_map_a1")
tw.set("auto_load", false)
tw.call("set_focus_tile", Vector2i(2, 2))
if tw.call("load_map"):
for y in range(68200, 66000, -100):
var h = float(tw.call("sample_height", 59700.0 / 100.0, float(y) / 100.0)) * 100.0
print("y = %d -> height = %.2f" % [y, h])
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://bt4n4goaei8mi
+107
View File
@@ -0,0 +1,107 @@
extends SceneTree
const PythonUISurface = preload("res://python_ui_surface.gd")
const Python3DSurface = preload("res://python_3d_surface.gd")
var ui: Control
var world: Node3D
func _initialize() -> void:
call_deferred("run")
func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == ""
func _pump_until(seconds: float, condition: Callable) -> bool:
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
while Time.get_ticks_msec() < deadline:
if condition.call():
return true
await process_frame
return condition.call()
func run() -> void:
root.size = Vector2i(1024, 768)
world = Python3DSurface.new()
world.name = "Python3DSurface"
ui = PythonUISurface.new()
ui.name = "PythonUISurface"
root.add_child(ui)
root.add_child(world)
var err: String = ui.run_app()
if err != "":
print("ERROR: run_app: ", err)
quit(1)
return
await _pump_until(15, func(): return _py(
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
))
await _pump_until(20, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
))
var host = OS.get_environment("MT_LIVE_HOST")
var login = OS.get_environment("MT_LIVE_LOGIN")
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n" % [host, game_port, host, auth_port]
+ "_stream.curPhaseWindow.Connect('%s', '%s')" % [login, pwd])
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
))
_py("_stream.curPhaseWindow.SelectSlot(0)\n_stream.curPhaseWindow.StartGame()")
await _pump_until(60, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"
))
await _pump_until(10, func(): return world.draw_count > 0)
for i in 30:
await process_frame
var draws: Array = Metin2PythonHost.render3d_draws()
print("Game 3D draws count: ", draws.size())
# Find actual actors
var actor_draws: Array = []
for draw in draws:
var tex = draw.get("texture0", "").to_lower()
var is_actor = false
for k in ["pc/", "pc2/", "monster/", "npc/", "warrior", "assassin", "sura", "shaman", "dog", "wolf"]:
if k in tex:
is_actor = true
break
if is_actor:
actor_draws.append(draw)
print("Actual actor draws count: ", actor_draws.size())
var actor_low := {}
for draw in actor_draws:
var draw_xform: Transform3D = world.d3d_transform(draw["world"])
var key := str(draw["world"])
for vertex in draw["positions"]:
var p: Vector3 = draw_xform * vertex
if not actor_low.has(key) or p.z < actor_low[key].z:
actor_low[key] = p
print("Distinct actors count: ", actor_low.size())
if world.terrain:
var worst_actor_gap_cm := 0.0
for key in actor_low:
var low: Vector3 = actor_low[key]
var ground := float(world.terrain.call("sample_height", low.x / 100.0, -low.y / 100.0)) * 100.0
var gap = absf(low.z - ground)
print("Actor at (%f, %f): foot_z=%f, ground_z=%f, gap=%f cm" % [low.x, low.y, low.z, ground, gap])
worst_actor_gap_cm = maxf(worst_actor_gap_cm, gap)
print("WORST ACTUAL ACTOR GAP: ", worst_actor_gap_cm, " cm")
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://cmum40u4abuep
+13
View File
@@ -0,0 +1,13 @@
@tool
extends SceneTree
func _init() -> void:
var b := Basis(Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9))
print("b.x = ", b.x)
print("b.y = ", b.y)
print("b.z = ", b.z)
var v := Vector3(1, 0, 0)
print("b * Vector3(1, 0, 0) = ", b * v)
var v2 := Vector3(0, 1, 0)
print("b * Vector3(0, 1, 0) = ", b * v2)
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://d17rcsnfyx418
+95
View File
@@ -0,0 +1,95 @@
extends SceneTree
const PythonUISurface = preload("res://python_ui_surface.gd")
const Python3DSurface = preload("res://python_3d_surface.gd")
var ui: Control
var world: Node3D
func _initialize() -> void:
call_deferred("run")
func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == ""
func _key(keycode: Key, unicode: int, pressed: bool) -> void:
var event := InputEventKey.new()
event.keycode = keycode
event.physical_keycode = keycode
event.unicode = unicode
event.pressed = pressed
root.push_input(event)
func _press(keycode: Key) -> void:
_key(keycode, 0, true)
_key(keycode, 0, false)
func _type(text: String) -> void:
for i in text.length():
var ch := text.unicode_at(i)
var keycode := OS.find_keycode_from_string(text[i].to_upper())
_key(keycode, ch, true)
_key(keycode, ch, false)
func _pump_until(seconds: float, condition: Callable) -> bool:
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
while Time.get_ticks_msec() < deadline:
if condition.call():
return true
await process_frame
return condition.call()
func run() -> void:
root.size = Vector2i(1024, 768)
world = Python3DSurface.new()
world.name = "Python3DSurface"
ui = PythonUISurface.new()
ui.name = "PythonUISurface"
root.add_child(ui)
root.add_child(world)
var err: String = ui.run_app()
if err != "":
quit(1)
return
await _pump_until(15, func(): return _py(
"import __main__, os, networkModule, introLogin, introSelect\n"
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
))
await _pump_until(20, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
))
_py(
"_env = os.environ\n"
+ "_stream.SetConnectInfo(_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_GAME_PORT', '13000')), "
+ "_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_AUTH_PORT', '11000')))\n"
+ "_w = _stream.curPhaseWindow\n"
+ "_w._LoginWindow__OpenLoginBoard()\n"
+ "_w.idEditLine.SetText('')\n"
+ "_w.pwdEditLine.SetText('')\n"
+ "_w.idEditLine.SetFocus()"
)
_type(OS.get_environment("MT_LIVE_LOGIN"))
_press(KEY_TAB)
_type(OS.get_environment("MT_LIVE_PASSWORD"))
_py("del _env, _w")
_press(KEY_ENTER)
await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
))
for i in 15:
await process_frame
var img_final = root.get_texture().get_image()
img_final.save_png("/tmp/select_character_final.png")
print("Saved /tmp/select_character_final.png")
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://dpi4fhed4wc28
+99 -20
View File
@@ -1,10 +1,10 @@
# UiAssets (P1) —— 解析 uiscript 里的图片路径(`.sub` / `.tga` / `.png` / `.jpg`)。
# UiAssets (P1) —— 解析 uiscript 里的图片路径(`.sub` / `.tga` / `.png` / `.jpg` / `.dds`)。
#
# 路径形如 `d:/ymir work/ui/public/middle_button_01.sub`。解析:
# 1) 去掉盘符前缀
# 2) 依次试: <assets>/<rel> · <assets>/<pack>/<rel>(散包目录)
# 3) `.sub` = 子图描述(title/image/left/top/right/bottom)→ 载入其 image + 裁剪成 AtlasTexture
# `.dds` 走 Metin2World.load_dds(C++ dxt 解码)运行时解成 Image。
# 1) 优先从 40250 Client/pack(Metin2Pack)取(保持 d:/ 盘符或规范相对路径)
# 2) 支持 pack 内 .sub 子图切片(AtlasTexture),支持 version 1.0 / 2.0 及公用 Public.dds / IntroEmpire.dds 母图寻址
# 3) 若 pack 找不到且指定了 assets_root,回退到散文件目录
# 4) .dds 走 Metin2World.load_dds(C++ dxt 解码)运行时解成 Image。
extends RefCounted
static var _cache := {}
@@ -19,21 +19,20 @@ static func load_dds_image(path: String) -> Image:
return null
if not ClassDB.class_exists("Metin2World"):
return null
# Metin2World is a Node, not RefCounted; a static reference never frees it.
# The decoder returns an independently owned Image, so release the helper
# after each decode (textures themselves remain cached by load_tex).
var helper: Object = ClassDB.instantiate("Metin2World")
var img = helper.call("load_dds", path)
helper.free()
return img if img is Image else null
static func load_tex(assets_root: String, vpath: String) -> Texture2D:
if vpath == "" or assets_root == "":
if vpath == "":
return null
var key := assets_root + "|" + vpath
var key := (assets_root if assets_root != "" else "pack") + "|" + vpath
if _cache.has(key):
return _cache[key]
var tex: Texture2D = _load_uncached(assets_root, vpath)
var tex: Texture2D = _load_pack_any(vpath)
if tex == null and assets_root != "":
tex = _load_uncached(assets_root, vpath)
_cache[key] = tex
return tex
@@ -45,8 +44,7 @@ static func load_pack_tex(vpath: String) -> Texture2D:
var key := "pack|" + vpath
if _cache.has(key):
return _cache[key]
# 40250 的包名带盘符(d:/ymir work/...),原样交给 CEterPackManager。
var tex: Texture2D = _load_pack_image(vpath.replace("\\", "/"))
var tex: Texture2D = _load_pack_any(vpath)
_cache[key] = tex
return tex
@@ -56,6 +54,93 @@ static func _strip_drive(p: String) -> String:
s = s.substr(2)
return s.lstrip("/")
static func _find_pack_path(vpath: String) -> String:
if not ClassDB.class_exists("Metin2Pack") or not Metin2Pack.is_ready():
return ""
var norm := vpath.replace("\\", "/").strip_edges()
var stripped := _strip_drive(norm)
var candidates: Array[String] = [
norm,
stripped,
"d:/" + stripped,
norm.to_lower(),
stripped.to_lower(),
("d:/" + stripped).to_lower(),
]
for c in candidates:
if Metin2Pack.exists(c):
return c
return ""
static func _load_pack_any(vpath: String) -> Texture2D:
var pack_path := _find_pack_path(vpath)
if pack_path == "":
return null
var ext := pack_path.get_extension().to_lower()
if ext == "sub":
return _load_pack_sub(pack_path)
return _load_pack_image(pack_path)
static func _load_pack_sub(pack_path: String) -> Texture2D:
if not ClassDB.class_exists("Metin2Pack") or not Metin2Pack.is_ready():
return null
if not Metin2Pack.exists(pack_path):
return null
var bytes: PackedByteArray = Metin2Pack.get_bytes(pack_path)
if bytes.is_empty():
return null
var txt := bytes.get_string_from_ascii()
var image_name := ""
var version := ""
var l := 0
var t := 0
var r := -1
var b := -1
for line in txt.split("\n"):
var parts := line.strip_edges().split(" ", false)
if parts.size() < 2:
continue
match parts[0].to_lower():
"version": version = parts[1].strip_edges().trim_prefix('"').trim_suffix('"')
"image": image_name = parts[1].strip_edges().trim_prefix('"').trim_suffix('"')
"left": l = int(parts[1])
"top": t = int(parts[1])
"right": r = int(parts[1])
"bottom": b = int(parts[1])
if image_name == "":
return null
var sub_dir := pack_path.get_base_dir()
var candidates: Array[String] = []
if version == "2.0":
candidates.append(sub_dir.path_join(image_name))
candidates.append("d:/ymir work/ui/".path_join(image_name))
else:
candidates.append("d:/ymir work/ui/".path_join(image_name))
candidates.append(sub_dir.path_join(image_name))
var search_dir := sub_dir
for _i in 4:
var parent := search_dir.get_base_dir()
if parent == search_dir or parent.is_empty():
break
candidates.append(parent.path_join(image_name))
search_dir = parent
var base_tex: Texture2D = null
for cand in candidates:
base_tex = _load_pack_any(cand)
if base_tex != null:
break
if base_tex == null:
return null
if r <= l or b <= t:
return base_tex
var at := AtlasTexture.new()
at.atlas = base_tex
at.region = Rect2(l, t, r - l, b - t)
return at
static func _resolve(assets_root: String, rel: String) -> String:
var direct := assets_root.path_join(rel)
if FileAccess.file_exists(direct):
@@ -67,20 +152,18 @@ static func _resolve(assets_root: String, rel: String) -> String:
var cand := assets_root.path_join(sub).path_join(rel)
if FileAccess.file_exists(cand):
return cand
# 大小写不敏感兜底:<assets>/**/ymir work/ui/... —— 只按 basename 找
return ""
static func _load_uncached(assets_root: String, vpath: String) -> Texture2D:
var rel := _strip_drive(vpath)
var real := _resolve(assets_root, rel)
if real == "":
# 试把 .sub 换成 .tga / .png
for ext: String in [".tga", ".png", ".jpg"]:
real = _resolve(assets_root, rel.get_basename() + ext)
if real != "":
break
if real == "":
return _load_pack_image(rel)
return _load_pack_any(vpath)
if real.get_extension().to_lower() == "sub":
return _load_sub(real)
return _load_image_file(real)
@@ -112,8 +195,6 @@ static func _load_pack_image(rel: String) -> Texture2D:
return ImageTexture.create_from_image(image) if image != null and not image.is_empty() else null
static func _load_image_file(path: String) -> Texture2D:
# .sub 文件可能引用了未随当前资源包发布的共享贴图(例如 Public.tga)。
# 先做存在性检查,避免 headless/UI fallback 因缺失可选贴图刷错误日志。
if not FileAccess.file_exists(path):
return null
var ext := path.get_extension().to_lower()
@@ -144,8 +225,6 @@ static func _load_sub(path: String) -> Texture2D:
"bottom": b = int(parts[1])
if image_name == "":
return null
# Skill .sub files live below ui/skill/<job>, while their shared DDS
# lives in ui/. Search the containing directory and its parents.
var img_path := ""
var search_dir := path.get_base_dir()
for _i in 6:
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env node
// Run the macOS native client with a real server or the local fake server.
// A real-server run remains unverified until a person checks the in-game flows.
import fs from 'node:fs';
import path from 'node:path';
import net from 'node:net';
import { spawn, execFile } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { redact, secretLiterals } from './redact_stream.mjs';
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const defaults = {
binary: path.join(repo, 'build-release/native_render/mt_native_render'),
client: path.join(repo, 'Client'),
output: path.join(repo, 'build/native-acceptance', new Date().toISOString().replaceAll(':', '-')),
};
function usage() {
console.log(`Usage: node script/native_mac_acceptance.mjs (--server HOST:AUTH_PORT:GAME_PORT | --fake) [options]
--binary FILE Native live-client binary (default: build-release/native_render/mt_native_render)
--client DIR 40250 Client directory (default: Client)
--output DIR New evidence directory (default: build/native-acceptance/<timestamp>)
--min-seconds N Minimum real-server observation time (default: 1800)
--frames N Fake-server smoke frames (default: 180; fake mode only)
--help Show this help
Real-server mode opens the login screen. Enter credentials in the app, exercise
the checklist in the report, then close the window. Credentials are never
passed on the command line. The report requires manual review before PASS.`);
}
function optionsOf(argv) {
const options = { ...defaults, minSeconds: 1800, frames: 180 };
for (let i = 0; i < argv.length; i += 1) {
const key = argv[i];
if (key === '--help') { options.help = true; continue; }
if (!['--server', '--fake', '--binary', '--client', '--output', '--min-seconds', '--frames'].includes(key))
throw new Error(`Unknown option: ${key}`);
if (key === '--fake') { options.fake = true; continue; }
if (++i >= argv.length) throw new Error(`Missing value for ${key}`);
const field = { '--server': 'server', '--binary': 'binary', '--client': 'client',
'--output': 'output', '--min-seconds': 'minSeconds', '--frames': 'frames' }[key];
options[field] = argv[i];
}
if (!options.help) {
if (Boolean(options.server) === Boolean(options.fake)) throw new Error('Choose exactly one of --server or --fake');
for (const field of ['minSeconds', 'frames']) {
options[field] = Number(options[field]);
if (!Number.isInteger(options[field]) || options[field] < (field === 'frames' ? 1 : 0))
throw new Error(`Invalid ${field}`);
}
if (options.server && argv.includes('--frames')) throw new Error('--frames is only for fake-server smoke');
}
return options;
}
function parseServer(spec) {
const match = /^([^:\s]+):([0-9]+):([0-9]+)$/.exec(spec);
if (!match || [match[2], match[3]].some((value) => +value < 1 || +value > 65535))
throw new Error('Server must be HOST:AUTH_PORT:GAME_PORT (one shared host)');
return { host: match[1], authPort: +match[2], gamePort: +match[3] };
}
function reachable(host, port) {
return new Promise((resolve) => {
const socket = net.createConnection({ host, port });
socket.setTimeout(3000);
socket.once('connect', () => { socket.destroy(); resolve(true); });
socket.once('error', () => { socket.destroy(); resolve(false); });
socket.once('timeout', () => { socket.destroy(); resolve(false); });
});
}
function readRss(pid) {
return new Promise((resolve) => execFile('ps', ['-o', 'stat=,rss=', '-p', String(pid)],
{ timeout: 3000 }, (error, stdout) => {
if (error) return resolve(null);
const [state, value] = stdout.trim().split(/\s+/);
const rss = Number(value);
resolve(state?.startsWith('Z') || !Number.isInteger(rss) || rss <= 0 ? null : rss);
}));
}
function parseSummary(log) {
const line = log.trim().split('\n').reverse().find((row) => row.startsWith('device=') && row.includes(' frames='));
if (!line) return null;
const values = {};
for (const match of line.matchAll(/(?:^| )([a-z0-9_]+)=([^ ]+)/g)) {
const number = Number(match[2]);
values[match[1]] = Number.isFinite(number) ? number : match[2];
}
return values;
}
async function main() {
let options;
try { options = optionsOf(process.argv.slice(2)); }
catch (error) { console.error(error.message); usage(); return 2; }
if (options.help) { usage(); return 0; }
if (process.platform !== 'darwin') { console.error('This acceptance runner targets macOS'); return 2; }
if (!fs.existsSync(options.binary) || !fs.existsSync(path.join(path.dirname(options.binary), 'python27.zip'))
|| !fs.existsSync(path.join(options.client, 'pack/Index'))) {
console.error('Missing native live-client binary, adjacent python27.zip, or Client/pack/Index');
return 2;
}
if (fs.existsSync(options.output)) { console.error(`Output already exists: ${options.output}`); return 2; }
let server = null;
if (options.server) {
try { server = parseServer(options.server); }
catch (error) { console.error(error.message); return 2; }
for (const port of [server.authPort, server.gamePort]) {
if (!await reachable(server.host, port)) {
console.error(`Server endpoint unreachable: ${server.host}:${port}; client not started`);
return 2;
}
}
}
fs.mkdirSync(options.output, { recursive: true, mode: 0o700 });
const logPath = path.join(options.output, 'client.log');
const rssPath = path.join(options.output, 'rss.jsonl');
const logFile = fs.openSync(logPath, 'w', 0o600);
const rssFile = fs.openSync(rssPath, 'w', 0o600);
const args = ['--live-client', options.client];
if (server) args.push('--live-server', options.server, '--login-screen');
else args.push('--fake-mobs', '24', '--frames', String(options.frames));
const started = Date.now();
const child = spawn(options.binary, args, { cwd: repo, stdio: ['inherit', 'pipe', 'pipe'] });
const literals = secretLiterals();
let log = '';
let pending = '';
const append = (chunk) => {
pending += chunk.toString('utf8');
const lines = pending.split('\n');
pending = lines.pop();
for (const line of lines) {
const clean = `${redact(line, literals)}\n`;
fs.writeSync(logFile, clean);
process.stdout.write(clean);
log += clean;
}
};
child.stdout.on('data', append);
child.stderr.on('data', append);
let sampleBusy = false;
let pendingSample = Promise.resolve();
const samples = [];
const sample = async () => {
if (sampleBusy) return;
sampleBusy = true;
const rss = await readRss(child.pid);
if (rss !== null) {
const row = { wall_ms: Date.now(), rss_kib: rss };
samples.push(row);
fs.writeSync(rssFile, `${JSON.stringify(row)}\n`);
}
sampleBusy = false;
};
await sample();
const interval = setInterval(() => { pendingSample = sample(); }, 1000);
const onSignal = (signal) => child.kill(signal);
process.on('SIGINT', onSignal);
process.on('SIGTERM', onSignal);
const result = await new Promise((resolve) => {
child.once('error', (error) => resolve({ code: null, signal: null, error: error.message }));
child.once('close', (code, signal) => resolve({ code, signal }));
});
clearInterval(interval);
await pendingSample;
process.off('SIGINT', onSignal);
process.off('SIGTERM', onSignal);
if (pending) append('\n');
fs.closeSync(logFile);
fs.closeSync(rssFile);
const durationSeconds = Math.round((Date.now() - started) / 1000);
const summary = parseSummary(log);
const completed = result.code === 0 && summary?.frames > 0;
const status = !completed ? 'FAIL' : server ? 'NEEDS_MANUAL_REVIEW' : 'SMOKE_PASS';
const report = {
status,
mode: server ? 'real_server' : 'fake_server',
started_at: new Date(started).toISOString(), duration_seconds: durationSeconds,
minimum_seconds: server ? options.minSeconds : null,
minimum_met: server ? durationSeconds >= options.minSeconds : null,
exit: result, summary,
rss: { samples: samples.length, first_kib: samples[0]?.rss_kib ?? null,
last_kib: samples.at(-1)?.rss_kib ?? null,
max_kib: samples.length ? Math.max(...samples.map((row) => row.rss_kib)) : null },
manual_checks: server ? {
login: null, character_selection: null, movement: null, combat: null,
map_change: null, inventory_and_chat: null, resize_and_focus: null,
visual_comparison: null,
} : null,
note: server ? 'Review manual checks and frame/RSS trends; this runner does not certify gameplay or memory leaks.'
: 'Fake-server smoke checks startup and rendering only.',
};
const reportPath = path.join(options.output, 'report.json');
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
console.log(`Native acceptance: ${status}; report=${reportPath}`);
return completed ? 0 : 1;
}
main().then((code) => { process.exitCode = code; }).catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
+2
View File
@@ -3,6 +3,8 @@
# commands; project/python_game_render_test.gd takes the screenshot and writes report.json.
#
# script/python_game_render_test.sh offline: port_fake_login_server on loopback
# MT_FAKE_MOB_COUNT=24 MT_PROFILE_FRAME=1 MT_PROFILE_COMBAT=1 script/python_game_render_test.sh
# offline crowd sample (1..64 dogs; default 1)
# script/python_game_render_test.sh --live live: MT_LIVE_HOST / MT_LIVE_LOGIN / MT_LIVE_PASSWORD
# (+ MT_LIVE_AUTH_PORT / MT_LIVE_GAME_PORT / MT_LIVE_SLOT)
#
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
repo="$PWD"
godot_bin="${GODOT:-$(which godot 2>/dev/null || echo "/Applications/Godot.app/Contents/MacOS/Godot")}"
server="$repo/build/extension/src/port/port_fake_login_server"
server_pid=""
cleanup() {
if [ -n "$server_pid" ]; then
kill -TERM "$server_pid" 2>/dev/null || true
wait "$server_pid" 2>/dev/null || true
fi
}
trap cleanup EXIT
fifo="/tmp/server.ports"
rm -f "$fifo"
mkfifo "$fifo" 2>/dev/null || touch "$fifo"
"$server" > "$fifo" 2> "/tmp/server.log" &
server_pid=$!
for _ in $(seq 50); do
[ -s "$fifo" ] && break
sleep 0.1
done
read -r word auth_port game_port < "$fifo" || true
if [ "${word:-}" != "ports" ]; then
echo "Error: fake server did not start" >&2
exit 1
fi
export MT_LIVE_HOST=127.0.0.1
export MT_LIVE_LOGIN=fakeuser
export MT_LIVE_PASSWORD=fakepass
export MT_LIVE_AUTH_PORT="$auth_port"
export MT_LIVE_GAME_PORT="$game_port"
export MT_LIVE_SLOT=0
echo "Running test with Godot: $godot_bin"
"$godot_bin" --path "$repo/project" --script res://test_select_character.gd