mirror of
https://github.com/Toastconcern/FrameStudio.git
synced 2026-10-29 04:02:56 +01:00
Frame Studio and Frame Home: cross-platform builds
Frame Studio now builds for Windows (x64, x86, ARM64), Linux (x64, x86, ARM64)
and macOS (Apple Silicon, Intel):
- studio/platform.h with Win32 and POSIX implementations (dialogs through
tinyfiledialogs on Linux/macOS), a GLFW window beside the Win32 one, and a
platform-neutral frame pacer
- MoltenVK support in the renderer (portability enumeration) and bundled in
the macOS app
- 32-bit builds: large-address-aware, LZMA packing within a 32-bit address space
- unaligned file reads in the loaders go through loadUnaligned (no UB)
- scripts/build-studio.sh, studio/build_windows.bat [x64|x86|arm64],
cmake/llvm-mingw.cmake, and a GitHub Actions workflow building every target
Frame Home installs without Studio: frame/install-frame-home.{sh,ps1} and
"Install Frame Home.bat" (packaged as FrameHome-installer.zip).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
f7345a5ac3
1176 files changed
+1634784
No files matched your search
@@ -0,0 +1,10 @@
|
||||
# Windows scripts keep Windows line endings (cmd.exe misreads others); shell scripts keep Unix ones.
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
*.sh text eol=lf
|
||||
install.sh text eol=lf
|
||||
# built files: as they are
|
||||
*.so binary
|
||||
*.spv binary
|
||||
frame/payload/bin/* binary
|
||||
@@ -0,0 +1,107 @@
|
||||
# Builds Frame Studio for every computer it runs on, and the Frame Home installer, on each push. Each build is a
|
||||
# downloadable artifact of the run; a tag "v*" also publishes them all as a GitHub release.
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux ${{ matrix.arch }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { arch: x64, runner: ubuntu-24.04, extra: "" }
|
||||
- { arch: x86, runner: ubuntu-24.04, extra: "gcc-multilib g++-multilib" }
|
||||
- { arch: arm64, runner: ubuntu-24.04-arm, extra: "" }
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Tools and X11 headers
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ninja-build cmake libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxkbcommon-dev ${{ matrix.extra }}
|
||||
- name: Build
|
||||
run: sh scripts/build-studio.sh linux-${{ matrix.arch }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FrameStudio-linux-${{ matrix.arch }}
|
||||
path: dist/FrameStudio-linux-${{ matrix.arch }}.tar.gz
|
||||
|
||||
windows:
|
||||
name: Windows ${{ matrix.arch }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [x64, x86, arm64]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build (MSVC)
|
||||
shell: cmd
|
||||
run: studio\build_windows.bat ${{ matrix.arch }} "%GITHUB_WORKSPACE%\build\windows-${{ matrix.arch }}"
|
||||
- name: Package
|
||||
shell: pwsh
|
||||
run: |
|
||||
$name = "FrameStudio-windows-${{ matrix.arch }}"
|
||||
$stage = "build/stage/$name"
|
||||
New-Item -ItemType Directory -Force $stage | Out-Null
|
||||
Copy-Item "build/windows-${{ matrix.arch }}/Frame Studio.exe" $stage
|
||||
Copy-Item -Recurse "build/windows-${{ matrix.arch }}/frame" "$stage/frame"
|
||||
New-Item -ItemType Directory -Force dist | Out-Null
|
||||
Compress-Archive -Path $stage -DestinationPath "dist/$name.zip" -Force
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FrameStudio-windows-${{ matrix.arch }}
|
||||
path: dist/FrameStudio-windows-${{ matrix.arch }}.zip
|
||||
|
||||
macos:
|
||||
name: macOS ${{ matrix.arch }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { arch: arm64, runner: macos-14 }
|
||||
- { arch: x64, runner: macos-13 }
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Tools and MoltenVK (Vulkan on Metal, bundled into the app)
|
||||
run: brew install ninja molten-vk
|
||||
- name: Build
|
||||
run: sh scripts/build-studio.sh macos-${{ matrix.arch }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FrameStudio-macos-${{ matrix.arch }}
|
||||
path: dist/FrameStudio-macos-${{ matrix.arch }}.zip
|
||||
|
||||
frame-home:
|
||||
name: Frame Home installer
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Package
|
||||
run: sh scripts/build-studio.sh frame-home
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FrameHome-installer
|
||||
path: dist/FrameHome-installer.zip
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [linux, windows, macos, frame-home]
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh release create "${{ github.ref_name }}" dist/* --repo "${{ github.repository }}" --title "Frame Studio ${{ github.ref_name }}" --generate-notes
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# build output
|
||||
/build*/
|
||||
/dist/
|
||||
studio/build*/
|
||||
tools/*/build*/
|
||||
# editor / OS clutter
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
# never commit homes, environments or keys
|
||||
*.apk
|
||||
*.fenv
|
||||
*.pem
|
||||
id_rsa*
|
||||
id_ed25519*
|
||||
*:Zone.Identifier
|
||||
# astcenc's own test framework (not built here)
|
||||
third_party/astcenc/Source/GoogleTest/
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(frame_home LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
endif()
|
||||
|
||||
# SteamVR ships the OpenVR client library; link against the runtime's own copy.
|
||||
set(OPENVR_LIB_DIR "/opt/steamvr/bin/linuxarm64" CACHE PATH "Directory containing libopenvr_api.so")
|
||||
find_library(OPENVR_LIB NAMES openvr_api PATHS "${OPENVR_LIB_DIR}" NO_DEFAULT_PATH REQUIRED)
|
||||
find_package(Vulkan REQUIRED)
|
||||
|
||||
set(TP "${CMAKE_SOURCE_DIR}/third_party")
|
||||
|
||||
# ── Editor loader stack (Quest Home Editor: ASMH/HSTF/RENDMESH/MATL/RENDTXTR/RENDSHAD) ──
|
||||
add_library(miniz STATIC "${TP}/miniz/miniz.c" "${TP}/miniz/miniz_tdef.c" "${TP}/miniz/miniz_tinfl.c" "${TP}/miniz/miniz_zip.c")
|
||||
target_include_directories(miniz PUBLIC "${TP}/miniz")
|
||||
# APKs are signed and intact; re-checksumming every extracted asset was ~20% of load time.
|
||||
target_compile_definitions(miniz PRIVATE MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS)
|
||||
add_library(smolv STATIC "${TP}/smolv/smolv.cpp")
|
||||
target_include_directories(smolv PUBLIC "${TP}/smolv")
|
||||
|
||||
# astcenc: decoder for the homes' ASTC textures (LDR + HDR lightmaps).
|
||||
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
||||
set(ASTCENC_CLI OFF CACHE BOOL "" FORCE)
|
||||
set(ASTCENC_DECOMPRESSOR OFF CACHE BOOL "" FORCE)
|
||||
set(ASTCENC_UNIVERSAL_BUILD OFF CACHE BOOL "" FORCE)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(ASTCENC_ISA_NEON ON CACHE BOOL "" FORCE)
|
||||
set(ASTCENC_LIB astcenc-neon-static)
|
||||
else()
|
||||
set(ASTCENC_ISA_SSE41 ON CACHE BOOL "" FORCE)
|
||||
set(ASTCENC_LIB astcenc-sse4.1-static)
|
||||
endif()
|
||||
add_subdirectory("${TP}/astcenc" astcenc EXCLUDE_FROM_ALL)
|
||||
|
||||
# LZMA SDK (public domain): the .fenv scene coder (src/fenv_lzma.cpp), with its threaded match finder.
|
||||
add_library(lzma STATIC "${TP}/lzma/LzmaEnc.c" "${TP}/lzma/LzmaDec.c" "${TP}/lzma/LzFind.c" "${TP}/lzma/LzFindOpt.c"
|
||||
"${TP}/lzma/LzFindMt.c" "${TP}/lzma/Threads.c" "${TP}/lzma/CpuArch.c")
|
||||
target_include_directories(lzma PUBLIC "${TP}/lzma")
|
||||
|
||||
# The home loader: HSL / Clay / glTF homes, .fenv environments (baked scenes), Blender / Unity models.
|
||||
add_library(frame-scene STATIC
|
||||
src/hsl_scene.cpp
|
||||
src/scene_bake.cpp
|
||||
src/anim_bake.cpp
|
||||
src/fenv_lzma.cpp
|
||||
src/model_import.cpp
|
||||
"${TP}/ufbx/ufbx.c"
|
||||
"${TP}/qhe/cook/hzanim_acl.cpp"
|
||||
"${TP}/stb/stb_image_impl.c")
|
||||
target_include_directories(frame-scene PUBLIC
|
||||
src
|
||||
"${TP}/stb"
|
||||
"${TP}/qhe"
|
||||
"${TP}/miniz"
|
||||
"${TP}/cgltf"
|
||||
"${TP}/ufbx"
|
||||
"${TP}/acl/includes"
|
||||
"${TP}/rtm/includes"
|
||||
"${TP}/astcenc/Source")
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
set_source_files_properties("${TP}/qhe/cook/hzanim_acl.cpp" PROPERTIES COMPILE_OPTIONS "-flax-vector-conversions")
|
||||
endif()
|
||||
target_link_libraries(frame-scene PUBLIC miniz lzma smolv ${ASTCENC_LIB} pthread)
|
||||
# libdeflate when the system has it (the Frame): APK scene.zips inflate several times faster than with miniz.
|
||||
find_library(LIBDEFLATE_LIB deflate)
|
||||
find_path(LIBDEFLATE_INC libdeflate.h)
|
||||
if(LIBDEFLATE_LIB AND LIBDEFLATE_INC)
|
||||
target_compile_definitions(frame-scene PUBLIC QHE_HAVE_LIBDEFLATE)
|
||||
target_include_directories(frame-scene PUBLIC "${LIBDEFLATE_INC}")
|
||||
target_link_libraries(frame-scene PUBLIC "${LIBDEFLATE_LIB}")
|
||||
endif()
|
||||
|
||||
# Ogg Opus ambience (Clay homes' _BACKGROUND_LOOP.ogg, or a sound added in Frame Studio): Xiph libopus + libogg +
|
||||
# opusfile (BSD), decode only — as Frame Studio has it, so the headset plays every sound the preview does.
|
||||
set(OPUS_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory("${TP}/opus" opus EXCLUDE_FROM_ALL)
|
||||
add_library(ogg STATIC "${TP}/ogg/src/bitwise.c" "${TP}/ogg/src/framing.c")
|
||||
target_include_directories(ogg PUBLIC "${TP}/ogg/include")
|
||||
add_library(opusfile STATIC "${TP}/opusfile/src/info.c" "${TP}/opusfile/src/internal.c"
|
||||
"${TP}/opusfile/src/opusfile.c" "${TP}/opusfile/src/stream.c")
|
||||
target_include_directories(opusfile PUBLIC "${TP}/opusfile/include" PRIVATE "${TP}/opusfile/src")
|
||||
target_compile_definitions(opusfile PRIVATE OP_DISABLE_HTTP)
|
||||
target_link_libraries(opusfile PUBLIC opus ogg)
|
||||
|
||||
add_executable(frame-home
|
||||
src/main.cpp
|
||||
src/hsl_renderer.cpp
|
||||
src/panorama.cpp
|
||||
"${TP}/stb/stb_vorbis_impl.c"
|
||||
"${TP}/miniaudio/miniaudio_impl.c")
|
||||
target_include_directories(frame-home PRIVATE "${TP}/openvr" "${TP}/miniaudio")
|
||||
target_link_libraries(frame-home PRIVATE frame-scene opusfile Vulkan::Vulkan "${OPENVR_LIB}" dl m)
|
||||
set_target_properties(frame-home PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}")
|
||||
|
||||
# fenv-tool: convert homes and models into .fenv environments.
|
||||
add_executable(fenv-tool src/fenv_tool.cpp)
|
||||
target_link_libraries(fenv-tool PRIVATE frame-scene)
|
||||
|
||||
# frame-switcher: the SteamVR dashboard panel that switches the environment.
|
||||
add_executable(frame-switcher src/switcher.cpp)
|
||||
target_include_directories(frame-switcher PRIVATE src "${TP}/stb" "${TP}/qhe" "${TP}/miniz" "${TP}/openvr")
|
||||
target_link_libraries(frame-switcher PRIVATE frame-scene "${OPENVR_LIB}")
|
||||
set_target_properties(frame-switcher PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}")
|
||||
@@ -0,0 +1,62 @@
|
||||
# Frame Studio and Frame Home
|
||||
|
||||
**Frame Home** makes your homes the Steam Frame's environment: Meta Quest homes (HSL, Clay `.opa`, glTF) and models,
|
||||
converted into Frame-native `.fenv` environments and drawn in SteamVR's compositor in place of its own.
|
||||
|
||||
**Frame Studio** is the desktop editor for them. It opens Meta homes, `.fenv` environments and models (glTF, FBX,
|
||||
OBJ), shows them with the same renderer the headset runs, and lets you move, hide and delete objects, shape the walkable
|
||||
area, add sounds and pictures, save environments and install them on the headset.
|
||||
|
||||
## Downloads
|
||||
|
||||
Every push builds everything (Actions → the latest run → Artifacts); tagged versions are on the Releases page.
|
||||
|
||||
| Computer | Frame Studio |
|
||||
|---|---|
|
||||
| Windows, 64-bit PCs | `FrameStudio-windows-x64.zip` |
|
||||
| Windows, 32-bit | `FrameStudio-windows-x86.zip` |
|
||||
| Windows on ARM (Snapdragon, Surface Pro X…) | `FrameStudio-windows-arm64.zip` |
|
||||
| Linux, 64-bit PCs | `FrameStudio-linux-x64.tar.gz` |
|
||||
| Linux, 32-bit | `FrameStudio-linux-x86.tar.gz` |
|
||||
| Linux on ARM (Raspberry Pi 5, ARM laptops, the Steam Frame's desktop) | `FrameStudio-linux-arm64.tar.gz` |
|
||||
| Mac with Apple Silicon (M1 and later) | `FrameStudio-macos-arm64.zip` |
|
||||
| Mac with Intel | `FrameStudio-macos-x64.zip` |
|
||||
|
||||
Studio needs a GPU with Vulkan 1.2: any recent NVIDIA, AMD or Intel driver on Windows and Linux; on a Mac, Vulkan runs
|
||||
on Metal through MoltenVK, which comes inside the app. The 32-bit builds have at most 4 GB to work in: the very
|
||||
largest homes may not fit.
|
||||
|
||||
**Frame Home without Studio:** `FrameHome-installer.zip` installs it on the headset from any computer (or on the
|
||||
headset itself); see `frame/README-frame-home.txt` inside it.
|
||||
|
||||
## Building
|
||||
|
||||
Everything Studio needs is in `third_party/` (Vulkan is loaded at runtime). CMake 3.16+ and Ninja.
|
||||
|
||||
| On | Command | Builds |
|
||||
|---|---|---|
|
||||
| Windows (Visual Studio 2022 with C++) | `studio\build_windows.bat x64` (or `x86`, `arm64`) | Windows |
|
||||
| Linux | `scripts/build-studio.sh linux-x64` (or `linux-x86`, `linux-arm64` on an ARM machine) | Linux |
|
||||
| Linux with [llvm-mingw](https://github.com/mstorsjo/llvm-mingw) | `scripts/build-studio.sh windows-x64 windows-x86 windows-arm64` | Windows, cross |
|
||||
| macOS (Xcode command line tools, `brew install ninja molten-vk`) | `scripts/build-studio.sh macos-arm64 macos-x64` | macOS |
|
||||
| anywhere | `scripts/build-studio.sh frame-home` | the Frame Home installer |
|
||||
| anywhere it can | `scripts/build-studio.sh all` | everything this computer can build |
|
||||
|
||||
Linux builds need the X11 headers (`libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev`) and, for
|
||||
`linux-x86`, `gcc-multilib g++-multilib`. Packages land in `dist/`.
|
||||
|
||||
The headset side (`frame-home`, `frame-switcher`, `fenv-tool`, the compositor layer) is built on the Steam Frame
|
||||
itself against SteamVR's own OpenVR library: the top-level `CMakeLists.txt`, then
|
||||
`g++ -O2 -shared -fPIC -std=c++17 layer/frame_home_layer.cpp -o build/layer/libframe_home_layer.so`. Its ready-built
|
||||
copy is `frame/payload/`, which every Studio build and the installer carry.
|
||||
|
||||
## Layout
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `studio/` | Frame Studio: `studio.cpp` (the editor), `platform*.{h,cpp}` (Windows / Linux / macOS), `frame_pacer.h` |
|
||||
| `src/` | shared engine: renderer (`hsl_renderer`), scene loading and baking, `.fenv`, walkable area, Frame Home (`main.cpp`), `fenv-tool` |
|
||||
| `layer/` | the Vulkan layer inside SteamVR's compositor that shows Frame Home in place of its environment |
|
||||
| `frame/` | the headset payload and the stand-alone installers |
|
||||
| `third_party/` | vendored libraries (each with its own license) |
|
||||
| `tools/` | test and measuring tools (headless renders, walk tests, shader generators) |
|
||||
@@ -0,0 +1,32 @@
|
||||
# Cross-building Frame Studio for Windows from Linux with llvm-mingw (https://github.com/mstorsjo/llvm-mingw):
|
||||
# cmake -S studio -B build/windows-arm64 -DCMAKE_TOOLCHAIN_FILE=cmake/llvm-mingw.cmake -DWINDOWS_ARCH=aarch64 \
|
||||
# -DLLVM_MINGW=/path/to/llvm-mingw
|
||||
# WINDOWS_ARCH: x86_64 (64-bit PCs), i686 (32-bit), aarch64 (Windows on ARM). LLVM_MINGW defaults to $LLVM_MINGW,
|
||||
# then ~/.local/opt/llvm-mingw.
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
if(NOT WINDOWS_ARCH)
|
||||
set(WINDOWS_ARCH x86_64)
|
||||
endif()
|
||||
set(WINDOWS_ARCH "${WINDOWS_ARCH}" CACHE STRING "x86_64, i686 or aarch64")
|
||||
if(WINDOWS_ARCH STREQUAL "aarch64")
|
||||
set(CMAKE_SYSTEM_PROCESSOR ARM64)
|
||||
elseif(WINDOWS_ARCH STREQUAL "i686")
|
||||
set(CMAKE_SYSTEM_PROCESSOR X86)
|
||||
else()
|
||||
set(CMAKE_SYSTEM_PROCESSOR AMD64)
|
||||
endif()
|
||||
if(NOT LLVM_MINGW)
|
||||
if(DEFINED ENV{LLVM_MINGW})
|
||||
set(LLVM_MINGW "$ENV{LLVM_MINGW}")
|
||||
else()
|
||||
set(LLVM_MINGW "$ENV{HOME}/.local/opt/llvm-mingw")
|
||||
endif()
|
||||
endif()
|
||||
set(LLVM_MINGW "${LLVM_MINGW}" CACHE PATH "llvm-mingw's folder")
|
||||
set(CMAKE_C_COMPILER "${LLVM_MINGW}/bin/${WINDOWS_ARCH}-w64-mingw32-clang")
|
||||
set(CMAKE_CXX_COMPILER "${LLVM_MINGW}/bin/${WINDOWS_ARCH}-w64-mingw32-clang++")
|
||||
set(CMAKE_RC_COMPILER "${LLVM_MINGW}/bin/${WINDOWS_ARCH}-w64-mingw32-windres")
|
||||
set(CMAKE_FIND_ROOT_PATH "${LLVM_MINGW}/${WINDOWS_ARCH}-w64-mingw32")
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
rem Installs Frame Home on the Steam Frame (no Frame Studio needed). Double-click it, or drop .fenv environments on
|
||||
rem it to copy them to the headset too. Asks for the headset's address the first time (default: frame).
|
||||
setlocal
|
||||
set "HOSTFILE=%APPDATA%\FrameStudio\frame-host.txt"
|
||||
set "FRAMEHOST="
|
||||
if exist "%HOSTFILE%" set /p FRAMEHOST=<"%HOSTFILE%"
|
||||
if "%FRAMEHOST%"=="" set "FRAMEHOST=frame"
|
||||
set /p ENTERED=Steam Frame address [%FRAMEHOST%]:
|
||||
if not "%ENTERED%"=="" set "FRAMEHOST=%ENTERED%"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install-frame-home.ps1" -HostName "%FRAMEHOST%" %*
|
||||
if errorlevel 1 (echo. & echo Frame Home was NOT installed: see the messages above.) else (echo. & echo Frame Home is installed.)
|
||||
pause
|
||||
@@ -0,0 +1,40 @@
|
||||
Frame Home for the Steam Frame
|
||||
==============================
|
||||
|
||||
Frame Home replaces SteamVR's own environment on the Steam Frame with your homes (Meta Quest homes converted by
|
||||
Frame Studio, or any .fenv environment). This folder installs it without Frame Studio.
|
||||
|
||||
What you need
|
||||
-------------
|
||||
- The Steam Frame with developer mode and SSH turned on, on the same network as your computer.
|
||||
- SSH access to it: the headset's developer key in your SSH settings, or its password.
|
||||
The headset's address is "frame" when an entry of that name is in your ~/.ssh/config (or the network knows it by
|
||||
that name); otherwise use its IP address, e.g. steamos@192.168.1.50.
|
||||
|
||||
Install from a Windows PC
|
||||
-------------------------
|
||||
Double-click "Install Frame Home.bat" and type the headset's address (Enter keeps the one shown).
|
||||
To copy environments to the headset at the same time, drop .fenv files onto "Install Frame Home.bat".
|
||||
(Needs Windows' OpenSSH Client, under Settings > Apps > Optional features, or Git for Windows.)
|
||||
|
||||
Install from Linux or macOS
|
||||
---------------------------
|
||||
sh install-frame-home.sh --host steamos@frame [environment.fenv ...]
|
||||
|
||||
Install on the headset itself
|
||||
-----------------------------
|
||||
Copy this folder to the headset (Desktop Mode), open a terminal (Konsole) in it and run:
|
||||
|
||||
sh payload/install.sh
|
||||
|
||||
Put .fenv environments in ~/frame-home/envs; the Environments panel in the Steam menu lists them.
|
||||
|
||||
Uninstall
|
||||
---------
|
||||
sh install-frame-home.sh --uninstall (Linux / macOS)
|
||||
powershell -ExecutionPolicy Bypass -File install-frame-home.ps1 -Uninstall (Windows)
|
||||
sh payload/install.sh uninstall (on the headset)
|
||||
|
||||
SteamVR's own environment comes back; your environments in ~/frame-home stay.
|
||||
If anything goes wrong, creating the file ~/frame-home/layer/off on the headset turns Frame Home off at once
|
||||
(SteamVR's environment shows after its next start).
|
||||
@@ -0,0 +1,72 @@
|
||||
# Installs Frame Home on a Steam Frame from a Windows PC, without Frame Studio: the same steps as Studio's
|
||||
# Frame > Install on Steam Frame. Needs ssh access to the headset (its developer key, or a password) and an ssh
|
||||
# client: Windows' own OpenSSH (Settings > Optional features) or Git for Windows'.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File install-frame-home.ps1 [-HostName HOST] [-Uninstall] [environment.fenv ...]
|
||||
#
|
||||
# HOST: the headset's ssh address (default "steamos@frame": an entry named frame in ~/.ssh/config, or the headset's
|
||||
# name on the network; an IP works too, e.g. steamos@192.168.1.50). Environments (.fenv) given are copied into its
|
||||
# selector as well; the first is the one shown. -Uninstall gives the environment back to SteamVR (files stay).
|
||||
param(
|
||||
[string]$HostName = "steamos@frame",
|
||||
[switch]$Uninstall,
|
||||
[Parameter(ValueFromRemainingArguments = $true)][string[]]$Environments = @()
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
function Say($text) { Write-Host "frame-home: $text" }
|
||||
|
||||
# ssh / scp: Windows' OpenSSH, else Git for Windows' copy (as Frame Studio finds them)
|
||||
function Find-Tool($name) {
|
||||
$onPath = Get-Command "$name.exe" -ErrorAction SilentlyContinue
|
||||
if ($onPath) { return $onPath.Source }
|
||||
$tries = @("$env:WINDIR\System32\OpenSSH\$name.exe")
|
||||
foreach ($root in @($env:ProgramW6432, $env:ProgramFiles, $env:LOCALAPPDATA)) {
|
||||
if ($root) { $tries += "$root\Git\usr\bin\$name.exe"; $tries += "$root\Programs\Git\usr\bin\$name.exe" }
|
||||
}
|
||||
foreach ($t in $tries) { if (Test-Path $t) { return $t } }
|
||||
throw "No $name found: turn on Windows' OpenSSH Client (Settings > Apps > Optional features), or install Git for Windows."
|
||||
}
|
||||
$ssh = Find-Tool "ssh"
|
||||
$scp = Find-Tool "scp"
|
||||
if ($HostName -notmatch "@") { $HostName = "steamos@$HostName" }
|
||||
|
||||
# The name an environment gets on the headset: lowercase, spaces as dashes, only [a-z0-9._-] (as Studio names them).
|
||||
function Frame-Name($path) {
|
||||
$n = ([IO.Path]::GetFileName($path)).ToLowerInvariant() -replace " ", "-"
|
||||
return ($n -replace "[^a-z0-9._-]", "")
|
||||
}
|
||||
|
||||
$payload = Join-Path $PSScriptRoot "payload"
|
||||
if (-not $Uninstall -and -not (Test-Path (Join-Path $payload "install.sh"))) { throw "The headset files are missing: expected $payload\install.sh" }
|
||||
|
||||
Say "connecting to $HostName"
|
||||
$arch = & $ssh -o ConnectTimeout=8 $HostName "mkdir -p frame-home/envs frame-home/.install && uname -m"
|
||||
if ($LASTEXITCODE -ne 0) { throw "Can't reach $HostName over ssh (is the headset on, in developer mode, with ssh enabled?)" }
|
||||
if ("$arch".Trim() -ne "aarch64") { throw "$HostName is a $arch machine, not a Steam Frame" }
|
||||
|
||||
if ($Uninstall) {
|
||||
# (never installed from a copy like this one: Frame Home itself puts SteamVR's environment back)
|
||||
& $ssh $HostName "if [ -f frame-home/.install/frame/install.sh ]; then sh frame-home/.install/frame/install.sh uninstall; else frame-home/bin/frame-home --uninstall; fi"
|
||||
} else {
|
||||
Say "copying Frame Home"
|
||||
& $ssh $HostName "rm -rf frame-home/.install/frame"
|
||||
# Relative source from its own folder: scp would read "C:\..." as a host named C.
|
||||
Push-Location $PSScriptRoot
|
||||
try { & $scp -q -r payload "${HostName}:frame-home/.install/frame" } finally { Pop-Location }
|
||||
if ($LASTEXITCODE -ne 0) { throw "Copying to the headset failed" }
|
||||
foreach ($f in $Environments) {
|
||||
if (-not (Test-Path $f)) { Say "no such file: $f"; continue }
|
||||
$name = Frame-Name $f
|
||||
Say "copying $([IO.Path]::GetFileName($f)) -> $name"
|
||||
Push-Location (Split-Path -Parent (Resolve-Path $f))
|
||||
try { & $scp -q ([IO.Path]::GetFileName($f)) "${HostName}:frame-home/envs/$name.part" } finally { Pop-Location }
|
||||
& $ssh $HostName "mv -f frame-home/envs/$name.part frame-home/envs/$name"
|
||||
}
|
||||
& $ssh $HostName "sed -i 's/\r`$//' frame-home/.install/frame/install.sh; sh frame-home/.install/frame/install.sh"
|
||||
if ($LASTEXITCODE -ne 0) { throw "The installer on the headset failed (see above)" }
|
||||
if ($Environments.Count -gt 0 -and (Test-Path $Environments[0])) { # the first is the one shown
|
||||
$first = Frame-Name $Environments[0]
|
||||
& $ssh $HostName "printf '%s\n' '$first' > frame-home/current"
|
||||
}
|
||||
}
|
||||
Say "done"
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
# Installs Frame Home on a Steam Frame from a Linux or macOS computer, without Frame Studio: the same steps as
|
||||
# Studio's Frame > Install on Steam Frame. Needs ssh access to the headset (its developer key, or a password).
|
||||
#
|
||||
# sh install-frame-home.sh [--host HOST] [--uninstall] [environment.fenv ...]
|
||||
#
|
||||
# HOST: the headset's ssh address (default "steamos@frame": an entry named frame in ~/.ssh/config, or the
|
||||
# headset's name on the network; an IP works too, e.g. steamos@192.168.1.50). Environments (.fenv) given are
|
||||
# copied into its selector as well. --uninstall gives the environment back to SteamVR (files stay).
|
||||
set -eu
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
PAYLOAD="$HERE/payload"
|
||||
HOST=steamos@frame
|
||||
MODE=install
|
||||
ENVS=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--host) HOST=$2; shift 2 ;;
|
||||
--uninstall) MODE=uninstall; shift ;;
|
||||
-h|--help) sed -n '2,10p' "$0"; exit 0 ;;
|
||||
*) ENVS="$ENVS
|
||||
$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
case "$HOST" in *@*) ;; *) HOST="steamos@$HOST" ;; esac
|
||||
say() { echo "frame-home: $*"; }
|
||||
[ -f "$PAYLOAD/install.sh" ] || { say "the headset files are missing: expected $PAYLOAD/install.sh"; exit 1; }
|
||||
|
||||
say "connecting to $HOST"
|
||||
ARCH=$(ssh -o ConnectTimeout=8 "$HOST" "mkdir -p frame-home/envs frame-home/.install && uname -m") || {
|
||||
say "can't reach $HOST over ssh (is the headset on, in developer mode, with ssh enabled?)"; exit 1; }
|
||||
[ "$ARCH" = aarch64 ] || { say "$HOST is a $ARCH machine, not a Steam Frame"; exit 1; }
|
||||
|
||||
if [ "$MODE" = install ]; then
|
||||
say "copying Frame Home"
|
||||
ssh "$HOST" "rm -rf frame-home/.install/frame"
|
||||
scp -q -r "$PAYLOAD" "$HOST:frame-home/.install/frame"
|
||||
printf '%s\n' "$ENVS" | while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
[ -f "$f" ] || { say "no such file: $f"; continue; }
|
||||
# the name it gets on the headset: lowercase, spaces as dashes, only [a-z0-9._-] (as Studio names them)
|
||||
name=$(basename "$f" | tr 'A-Z ' 'a-z-' | tr -cd 'a-z0-9._-')
|
||||
say "copying $(basename "$f") -> $name"
|
||||
scp -q "$f" "$HOST:frame-home/envs/$name.part" && ssh "$HOST" "mv -f frame-home/envs/$name.part frame-home/envs/$name"
|
||||
done
|
||||
ssh "$HOST" "sed -i 's/\r\$//' frame-home/.install/frame/install.sh; sh frame-home/.install/frame/install.sh"
|
||||
# the first environment given is the one shown
|
||||
FIRST=$(printf '%s\n' "$ENVS" | sed -n '/./{p;q;}')
|
||||
if [ -n "$FIRST" ] && [ -f "$FIRST" ]; then
|
||||
name=$(basename "$FIRST" | tr 'A-Z ' 'a-z-' | tr -cd 'a-z0-9._-')
|
||||
ssh "$HOST" "printf '%s\n' '$name' > frame-home/current"
|
||||
fi
|
||||
else
|
||||
# (never installed from a copy like this one: Frame Home itself puts SteamVR's environment back)
|
||||
ssh "$HOST" "if [ -f frame-home/.install/frame/install.sh ]; then sh frame-home/.install/frame/install.sh uninstall; else frame-home/bin/frame-home --uninstall; fi"
|
||||
fi
|
||||
say "done"
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,126 @@
|
||||
#!/bin/sh
|
||||
# Frame Home installer — runs on the Steam Frame. Frame Studio uploads this folder and runs it over ssh
|
||||
# (Frame > Install on Steam Frame); it also works by hand:
|
||||
# sh install.sh install or update Frame Home and make it the headset's environment
|
||||
# sh install.sh uninstall give the environment back to SteamVR (environments and binaries stay)
|
||||
#
|
||||
# Frame Home replaces Aurora, SteamVR's own environment: a Vulkan layer in SteamVR's compositor draws your home where
|
||||
# Aurora goes (layer/), and frame-home --system renders it; the Environments panel (frame-switcher, started with
|
||||
# SteamVR) keeps that running. No app, no dock entry; it shows at boot, behind the Steam menu and during game loads.
|
||||
# Nothing is changed under /opt or /usr. ~/frame-home/layer/off turns it off (Aurora as usual).
|
||||
set -u
|
||||
P=$(cd "$(dirname "$0")" && pwd)
|
||||
H="$HOME/frame-home"
|
||||
CFG="${XDG_CONFIG_HOME:-$HOME/.config}/openvr/config"
|
||||
MODE="${1:-install}"
|
||||
say() { echo "frame-install: $*"; }
|
||||
chmod +x "$P"/bin/* 2>/dev/null # copied from Windows: no executable bit
|
||||
|
||||
# SteamVR not running: write the same registration into its config files (it reads them when it starts).
|
||||
offline() {
|
||||
python3 - "$CFG" "$1" "$H" <<'PY'
|
||||
import json, os, sys
|
||||
cfg, mode, home = sys.argv[1:4]
|
||||
def load(p): # missing: start empty; there but unreadable: stop (saving would wipe the user's SteamVR settings)
|
||||
try:
|
||||
with open(p) as f: return json.load(f)
|
||||
except FileNotFoundError: return {}
|
||||
except Exception as e: sys.exit('frame-install: %s does not read (%s): left as it is' % (p, e))
|
||||
def save(p, d):
|
||||
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||||
with open(p + '.tmp', 'w') as f: json.dump(d, f, indent=3)
|
||||
os.replace(p + '.tmp', p)
|
||||
ac = os.path.join(cfg, 'appconfig.json'); a = load(ac); paths = a.setdefault('manifest_paths', [])
|
||||
for m in (os.path.join(home, 'frame-home.vrmanifest'), os.path.join(home, 'frame-switcher.vrmanifest')):
|
||||
if mode == 'install' and m not in paths: paths.append(m)
|
||||
if mode == 'uninstall' and m in paths: paths.remove(m)
|
||||
save(ac, a)
|
||||
vs = os.path.join(cfg, 'steamvr.vrsettings'); s = load(vs)
|
||||
s.setdefault('steamvr', {})['enableHomeApp'] = False # the old way (Frame Home as SteamVR's home app) stays off
|
||||
if mode == 'install': s['steamvr']['environmentMode'] = 1 # Aurora: what the layer replaces
|
||||
s['steamvr'].pop('globalActionSetPriority', None) # earlier versions took the sticks over the Steam menu: the menu's again
|
||||
save(vs, s)
|
||||
if mode == 'install':
|
||||
save(os.path.join(cfg, 'vrappconfig', 'framehome.switcher.vrappconfig'), {'autolaunch': True, 'last_launch_time': '0'})
|
||||
PY
|
||||
}
|
||||
|
||||
LAYERJSON="$HOME/.local/share/vulkan/implicit_layer.d/frame_home_layer.json"
|
||||
if [ "$MODE" = uninstall ]; then
|
||||
mkdir -p "$H/layer"; : > "$H/layer/off"; rm -f "$LAYERJSON" # Aurora is back once SteamVR restarts
|
||||
pkill -f "^$H/bin/frame-home --system" 2>/dev/null
|
||||
if [ -x "$P/bin/frame-home" ]; then "$P/bin/frame-home" --uninstall; else "$H/bin/frame-home" --uninstall; fi; rc=$?
|
||||
[ $rc -eq 3 ] && { offline uninstall; rc=$?; }
|
||||
[ $rc -eq 0 ] && say "SteamVR's own environment is back" || say "uninstall failed ($rc)"
|
||||
exit $rc
|
||||
fi
|
||||
|
||||
[ -x "$P/bin/frame-home" ] || { say "this installer has no Frame Home binaries"; exit 1; }
|
||||
# Missing libraries (or a build for another machine) show up here, before anything installed is touched, rather than
|
||||
# as a silent black environment later.
|
||||
if ! LD_LIBRARY_PATH="$P/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$P/bin/frame-home" --version >/dev/null 2>&1; then
|
||||
missing=$(ldd "$P/bin/frame-home" 2>/dev/null | grep "not found" | head -3 | tr -s ' \t\n' ' ')
|
||||
say "Frame Home can't run on this headset: ${missing:-unknown error} (nothing was changed)"; exit 1
|
||||
fi
|
||||
mkdir -p "$H/bin" "$H/envs" || exit 1
|
||||
# New binaries beside the old ones, then renamed over them: a running copy keeps its file until it restarts.
|
||||
for x in frame-home frame-switcher fenv-tool; do
|
||||
[ -f "$P/bin/$x" ] || continue
|
||||
cp -f "$P/bin/$x" "$H/bin/$x.new" && chmod +x "$H/bin/$x.new" && mv -f "$H/bin/$x.new" "$H/bin/$x" || { say "couldn't install $x"; exit 1; }
|
||||
done
|
||||
# The compositor layer and its shader (renamed over the old ones: a running compositor keeps its copy).
|
||||
layerChanged=0
|
||||
if [ -f "$P/layer/libframe_home_layer.so" ]; then
|
||||
mkdir -p "$H/layer" "$(dirname "$LAYERJSON")"
|
||||
cmp -s "$P/layer/libframe_home_layer.so" "$H/layer/libframe_home_layer.so" || layerChanged=1
|
||||
cmp -s "$P/layer/env_ps.spv" "$H/layer/env_ps.spv" || layerChanged=1
|
||||
for f in libframe_home_layer.so env_ps.spv; do cp -f "$P/layer/$f" "$H/layer/.$f.new" && mv -f "$H/layer/.$f.new" "$H/layer/$f"; done
|
||||
rm -f "$H/layer/off" "$H/layer/starting" "$H/layer/crashes"
|
||||
[ -f "$LAYERJSON" ] || layerChanged=1
|
||||
cat > "$LAYERJSON" <<JSON
|
||||
{
|
||||
"file_format_version": "1.2.0",
|
||||
"layer": {
|
||||
"name": "VK_LAYER_FRAMEHOME_compositor",
|
||||
"type": "GLOBAL",
|
||||
"library_path": "$H/layer/libframe_home_layer.so",
|
||||
"api_version": "1.4.309",
|
||||
"implementation_version": "1",
|
||||
"description": "Frame Home: replaces the SteamVR compositor environment (active only in vrcompositor)",
|
||||
"functions": { "vkNegotiateLoaderLayerInterfaceVersion": "frameHome_NegotiateLoaderLayerInterfaceVersion" },
|
||||
"disable_environment": { "FRAME_HOME_LAYER_DISABLE": "1" }
|
||||
}
|
||||
}
|
||||
JSON
|
||||
fi
|
||||
# Libraries the binaries need that the headset may not have (none today; kept for future builds).
|
||||
[ -d "$P/lib" ] && { mkdir -p "$H/lib"; cp -f "$P"/lib/* "$H/lib/"; }
|
||||
# Environments that came with this install.
|
||||
for f in "$P"/envs/*.fenv; do [ -f "$f" ] && mv -f "$f" "$H/envs/"; done
|
||||
|
||||
# The Environments panel starts with SteamVR and keeps frame-home --system running. Restarted here so the new
|
||||
# binaries take over now; the compositor too when the layer changed (SteamVR brings it straight back).
|
||||
if pgrep -x vrserver >/dev/null; then
|
||||
pkill -x frame-switcher 2>/dev/null; pkill -f "^$H/bin/frame-home --system" 2>/dev/null
|
||||
for i in $(seq 1 20); do pgrep -x frame-switcher >/dev/null || break; sleep 0.5; done # gone before relaunching
|
||||
"$P/bin/frame-switcher" --install; rs=$?
|
||||
[ $rs -eq 0 ] || { say "SteamVR refused the Environments panel ($rs)"; exit 1; }
|
||||
sleep 3
|
||||
if ! pgrep -x frame-switcher >/dev/null; then # SteamVR still counted the old one as running: start it ourselves
|
||||
(cd "$H/bin" && STEAMVR_APPKEY=framehome.switcher setsid nohup ./frame-switcher >/dev/null 2>&1 </dev/null &)
|
||||
fi
|
||||
if [ $layerChanged -eq 1 ]; then pkill -x vrcompositor 2>/dev/null; fi
|
||||
say "installed — Frame Home is now the headset's environment (in Aurora's place)"
|
||||
else
|
||||
"$P/bin/frame-switcher" --install >/dev/null 2>&1 # writes its manifest (SteamVR isn't there to register it)
|
||||
offline install || { say "couldn't register with SteamVR"; exit 1; }
|
||||
say "installed — Frame Home becomes the environment when SteamVR next starts"
|
||||
fi
|
||||
# First environment: the newest one installed, unless one is already chosen.
|
||||
if [ ! -s "$H/current" ]; then
|
||||
first=$(ls -t "$H"/envs/*.fenv 2>/dev/null | head -1)
|
||||
[ -n "$first" ] && basename "$first" > "$H/current"
|
||||
fi
|
||||
# Thumbnails for the switcher, in the background at low priority.
|
||||
(nohup "$H/bin/frame-switcher" --thumbs >/dev/null 2>&1 &)
|
||||
exit 0
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,43 @@
|
||||
#version 450
|
||||
// Frame Home's environment, drawn by vrcompositor in place of Aurora (frame_home_layer.cpp swaps it in for every
|
||||
// Aurora pipeline: same vertex shader, same set 0, plus set 1 below).
|
||||
//
|
||||
// Aurora's vertex shader hands over each point's tracking-space position (location 1) and the frame's uniform block
|
||||
// holds the eye position (_m11, as Aurora's floor shader reads it). The direction from the eye to the point is
|
||||
// looked up in the newest frame Frame Home published: its projection * view rotation for that eye, so the picture
|
||||
// stays locked to the world as the head turns between Frame Home's frame and the compositor's (rotation reprojection).
|
||||
|
||||
// SteamVR's per-frame block (set 0, binding 4), as far as the members used here.
|
||||
layout(set = 0, binding = 4, std140) uniform CompositorFrame {
|
||||
layout(row_major) mat4 m0, m1, m2, m3, m4;
|
||||
vec4 m5[4];
|
||||
vec4 m6; // w: the environment's opacity (the compositor dims it behind the menu and in transitions)
|
||||
vec4 m7;
|
||||
vec2 m8, m9;
|
||||
vec4 m10;
|
||||
vec4 m11; // xyz: this eye's position, tracking space
|
||||
} frame;
|
||||
|
||||
layout(set = 1, binding = 0) uniform sampler2DArray home; // both eyes, layer 0 left, 1 right
|
||||
layout(set = 1, binding = 1, std140) uniform HomeFrame {
|
||||
mat4 viewProj[2]; // projection * view rotation, per eye (tracking space)
|
||||
vec4 eyePos[2]; // where each eye was when Frame Home rendered it
|
||||
vec4 params; // x: brightness (0 fades to black), y: 1 when there's something to show
|
||||
} homeFrame;
|
||||
|
||||
layout(location = 1) in vec3 worldPos;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
void main() {
|
||||
const vec3 eye = frame.m11.xyz;
|
||||
const vec3 dir = normalize(worldPos - eye);
|
||||
const int k = distance(eye, homeFrame.eyePos[0].xyz) <= distance(eye, homeFrame.eyePos[1].xyz) ? 0 : 1;
|
||||
const vec4 c = homeFrame.viewProj[k] * vec4(dir, 0.0);
|
||||
vec3 rgb = vec3(0.0);
|
||||
if (c.w > 1e-5 && homeFrame.params.y > 0.5) {
|
||||
const vec2 uv = (c.xy / c.w) * 0.5 + 0.5;
|
||||
// (explicit level 0: subsampled frames must be read so, and plain ones have only that level)
|
||||
rgb = textureLod(home, vec3(uv, float(k)), 0.0).rgb;
|
||||
}
|
||||
color = vec4(rgb * homeFrame.params.x, frame.m6.w);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/bin/sh
|
||||
# Builds Frame Studio for one or more computers and packages each into dist/, ready to hand out.
|
||||
#
|
||||
# scripts/build-studio.sh TARGET... [--build-dir DIR]
|
||||
#
|
||||
# Targets (each needs the matching compiler; GitHub Actions builds all of them: .github/workflows/build.yml):
|
||||
# linux-x64 linux-x86 linux-arm64 on Linux: the native compiler (x86: gcc-multilib / g++-multilib); X11
|
||||
# headers (libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev)
|
||||
# windows-x64 windows-x86 windows-arm64 on Linux with llvm-mingw (cmake/llvm-mingw.cmake; $LLVM_MINGW)
|
||||
# (on Windows itself: studio\build_windows.bat x64|x86|arm64)
|
||||
# macos-arm64 macos-x64 on a Mac with Xcode's command line tools; MoltenVK is bundled into the
|
||||
# app from $MOLTENVK (a libMoltenVK.dylib) or Homebrew's molten-vk
|
||||
# frame-home the Steam Frame installer on its own (no Studio needed): the headset
|
||||
# files from frame/payload and the install scripts
|
||||
# all every target this computer can build
|
||||
# Output: dist/FrameStudio-<target>.zip (.tar.gz on Linux) and dist/FrameHome-installer.zip.
|
||||
set -eu
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
DIST="$ROOT/dist"
|
||||
BUILD="$ROOT/build"
|
||||
CMAKE=${CMAKE:-cmake}
|
||||
TARGETS=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--build-dir) BUILD=$2; shift 2 ;;
|
||||
-h|--help) sed -n '2,19p' "$0"; exit 0 ;;
|
||||
*) TARGETS="$TARGETS $1"; shift ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$TARGETS" ] || { sed -n '2,19p' "$0"; exit 1; }
|
||||
mkdir -p "$DIST" "$BUILD"
|
||||
say() { echo "build-studio: $*"; }
|
||||
|
||||
if [ "$TARGETS" = " all" ]; then
|
||||
case "$(uname -s)" in
|
||||
Darwin) TARGETS="macos-arm64 macos-x64 frame-home" ;;
|
||||
*) case "$(uname -m)" in
|
||||
aarch64|arm64) TARGETS="linux-arm64 frame-home" ;;
|
||||
*) TARGETS="linux-x64 linux-x86 frame-home" ;;
|
||||
esac
|
||||
[ -x "${LLVM_MINGW:-$HOME/.local/opt/llvm-mingw}/bin/x86_64-w64-mingw32-clang" ] && TARGETS="$TARGETS windows-x64 windows-x86 windows-arm64" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# configure + build one target; $1 target, then extra cmake arguments
|
||||
build() {
|
||||
target=$1; shift
|
||||
dir="$BUILD/$target"
|
||||
say "building $target"
|
||||
"$CMAKE" -S "$ROOT/studio" -B "$dir" -DCMAKE_BUILD_TYPE=Release "$@" > "$dir.configure.log" 2>&1 || {
|
||||
tail -20 "$dir.configure.log"; say "$target: configure failed (full log: $dir.configure.log)"; exit 1; }
|
||||
"$CMAKE" --build "$dir" --target frame-studio --parallel
|
||||
}
|
||||
|
||||
# dist/FrameStudio-<target>: the program and, beside it, frame/ (the headset side Studio installs)
|
||||
package() {
|
||||
target=$1 dir="$BUILD/$1" name="FrameStudio-$1"
|
||||
stage="$BUILD/stage/$name"
|
||||
rm -rf "$stage" && mkdir -p "$stage"
|
||||
case "$target" in
|
||||
windows-*) cp "$dir/Frame Studio.exe" "$stage/" && cp -R "$dir/frame" "$stage/frame"
|
||||
(cd "$BUILD/stage" && rm -f "$DIST/$name.zip" && zip -qr "$DIST/$name.zip" "$name") ;;
|
||||
linux-*) cp "$dir/Frame Studio" "$stage/" && cp -R "$dir/frame" "$stage/frame"
|
||||
tar -czf "$DIST/$name.tar.gz" -C "$BUILD/stage" "$name" ;;
|
||||
macos-*) cp -R "$dir/Frame Studio.app" "$stage/"
|
||||
bundle_moltenvk "$stage/Frame Studio.app"
|
||||
(cd "$BUILD/stage" && rm -f "$DIST/$name.zip" && ditto -c -k --keepParent "$name" "$DIST/$name.zip") ;;
|
||||
esac
|
||||
say "$target -> dist/$(ls "$DIST" | grep "^$name\." | head -1)"
|
||||
}
|
||||
|
||||
# MoltenVK (Vulkan on Metal) inside the app: Contents/Frameworks/libMoltenVK.dylib, which Studio loads itself when
|
||||
# the Mac has no Vulkan loader installed.
|
||||
bundle_moltenvk() {
|
||||
app=$1
|
||||
lib=${MOLTENVK:-}
|
||||
[ -n "$lib" ] || { command -v brew > /dev/null && lib="$(brew --prefix molten-vk 2>/dev/null)/lib/libMoltenVK.dylib"; } || true
|
||||
if [ -n "$lib" ] && [ -f "$lib" ]; then
|
||||
mkdir -p "$app/Contents/Frameworks" && cp "$lib" "$app/Contents/Frameworks/"
|
||||
else
|
||||
say "warning: no MoltenVK found (set MOLTENVK or: brew install molten-vk): the app needs the Vulkan SDK installed to run"
|
||||
fi
|
||||
}
|
||||
|
||||
# dist/FrameHome-installer.zip: Frame Home for the Steam Frame, installed without Frame Studio
|
||||
package_frame_home() {
|
||||
stage="$BUILD/stage/FrameHome-installer"
|
||||
rm -rf "$stage" && mkdir -p "$stage"
|
||||
cp -R "$ROOT/frame/payload" "$stage/payload"
|
||||
cp "$ROOT/frame/install-frame-home.sh" "$ROOT/frame/install-frame-home.ps1" "$ROOT/frame/Install Frame Home.bat" "$ROOT/frame/README-frame-home.txt" "$stage/"
|
||||
chmod +x "$stage/install-frame-home.sh" "$stage/payload/install.sh" "$stage"/payload/bin/* 2>/dev/null || true
|
||||
(cd "$BUILD/stage" && rm -f "$DIST/FrameHome-installer.zip" && zip -qr "$DIST/FrameHome-installer.zip" FrameHome-installer)
|
||||
say "frame-home -> dist/FrameHome-installer.zip"
|
||||
}
|
||||
|
||||
for t in $TARGETS; do
|
||||
case "$t" in
|
||||
linux-x64|linux-arm64) build "$t" -G Ninja && package "$t" ;;
|
||||
linux-x86) build "$t" -G Ninja -DCMAKE_C_FLAGS=-m32 -DCMAKE_CXX_FLAGS=-m32 -DCMAKE_SYSTEM_PROCESSOR=i686 \
|
||||
-DCMAKE_LIBRARY_ARCHITECTURE=i386-linux-gnu && package "$t" ;;
|
||||
windows-x64) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=x86_64 && package "$t" ;;
|
||||
windows-x86) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=i686 && package "$t" ;;
|
||||
windows-arm64) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=aarch64 && package "$t" ;;
|
||||
macos-arm64) build "$t" -G Ninja -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 && package "$t" ;;
|
||||
macos-x64) build "$t" -G Ninja -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 && package "$t" ;;
|
||||
frame-home) package_frame_home ;;
|
||||
*) say "unknown target: $t (see --help)"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
@@ -0,0 +1,405 @@
|
||||
#include "anim_bake.h"
|
||||
#include "bake_io.h"
|
||||
|
||||
#include "loaders/opa_loader.h"
|
||||
#include "loaders/gltf_loader.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <functional>
|
||||
#include <set>
|
||||
#include <memory>
|
||||
|
||||
namespace {
|
||||
|
||||
using bakeio::Reader;
|
||||
using bakeio::Writer;
|
||||
|
||||
constexpr u8 kClay = 1, kGltf = 2;
|
||||
|
||||
// Which nodes the animated meshes hang from: those nodes and every ancestor the evaluation reaches from them, the
|
||||
// rest dropped (kept in their order, so each still follows its parents). `parentOf(i)` is the parent the evaluation
|
||||
// uses (-1: none); out[i] = the node's new index, -1 when it isn't kept.
|
||||
template <typename ParentOf>
|
||||
std::vector<int> keptNodes(size_t n, const std::vector<int>& used, ParentOf parentOf) {
|
||||
std::vector<char> keep(n, 0);
|
||||
for (int u : used) {
|
||||
for (int i = u, steps = 0; i >= 0 && static_cast<size_t>(i) < n && !keep[i] && steps <= static_cast<int>(n); ++steps) {
|
||||
keep[i] = 1;
|
||||
i = parentOf(i);
|
||||
}
|
||||
}
|
||||
std::vector<int> out(n, -1);
|
||||
int k = 0;
|
||||
for (size_t i = 0; i < n; ++i) if (keep[i]) out[i] = k++;
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Clay (OpaLoader) ──
|
||||
|
||||
void writeTrack(Writer& w, const OpaLoader::Track& t) { w.pod(t.comps); w.pod(t.nFrames); w.pod(t.effFrames); w.vec(t.v); }
|
||||
OpaLoader::Track readTrack(Reader& r) {
|
||||
OpaLoader::Track t;
|
||||
t.comps = r.pod<int>(); t.nFrames = r.pod<int>(); t.effFrames = r.pod<int>(); t.v = r.vec<float>();
|
||||
return t;
|
||||
}
|
||||
// sampleTrack reads nFrames x comps values when the track has frames and its comps are what it asks for (3 or 4)
|
||||
bool trackOk(const OpaLoader::Track& t) { return t.nFrames <= 0 || (t.comps != 3 && t.comps != 4) || t.v.size() >= static_cast<size_t>(t.nFrames) * t.comps; }
|
||||
|
||||
void saveClay(const OpaLoader& L, Writer& w) {
|
||||
w.f(L.animFps); w.pod(L.animMaxFrames); w.f(L.vatFps); w.f(L.uvMaxLoopSec);
|
||||
// the node hierarchy, as far as the animated meshes need it (evalAnimNodes: a parent at or after its child is none)
|
||||
std::vector<int> used;
|
||||
for (const auto& ar : L.animRecs) used.push_back(static_cast<int>(ar.nodeIdx));
|
||||
const std::vector<int> nodeMap = keptNodes(L.animNodes.size(), used, [&](int i) {
|
||||
const int p = L.animNodes[i].parent;
|
||||
return p >= 0 && p < i ? p : -1;
|
||||
});
|
||||
std::vector<const OpaLoader::Node*> nodes;
|
||||
for (size_t i = 0; i < L.animNodes.size(); ++i) if (nodeMap[i] >= 0) nodes.push_back(&L.animNodes[i]);
|
||||
w.u(static_cast<u32>(nodes.size()));
|
||||
for (const OpaLoader::Node* nd : nodes) {
|
||||
const int i = static_cast<int>(nd - L.animNodes.data()), p = nd->parent;
|
||||
w.str(nd->name); w.pod<int>(p >= 0 && p < i ? nodeMap[p] : -1); w.fa(nd->t, 3); w.fa(nd->r, 4); w.fa(nd->s, 3);
|
||||
}
|
||||
{ // their keyed tracks (by node name, as evalAnimNodes looks them up)
|
||||
std::vector<std::pair<const std::string*, const OpaLoader::NodeTracks*>> tracks;
|
||||
std::set<std::string> seen;
|
||||
for (const OpaLoader::Node* nd : nodes) {
|
||||
const auto it = L.nodeAnim.find(nd->name);
|
||||
if (it != L.nodeAnim.end() && seen.insert(nd->name).second) tracks.emplace_back(&it->first, &it->second);
|
||||
}
|
||||
w.u(static_cast<u32>(tracks.size()));
|
||||
for (const auto& t : tracks) { w.str(*t.first); writeTrack(w, t.second->t); writeTrack(w, t.second->r); writeTrack(w, t.second->s); }
|
||||
}
|
||||
w.u(static_cast<u32>(L.animRecs.size()));
|
||||
for (const auto& ar : L.animRecs) {
|
||||
w.u(static_cast<u32>(ar.meshIdx));
|
||||
w.pod<int>(ar.nodeIdx < nodeMap.size() ? nodeMap[ar.nodeIdx] : INT_MAX); // (out of range: evaluated as no node)
|
||||
w.vec(ar.basePos); w.pod(ar.rigid); w.pod(ar.rest); w.fa(ar.restInv, 16); w.b(ar.atRest);
|
||||
}
|
||||
// skin clips the skinned meshes play (animate() evaluates every clip each frame: the unused ones go)
|
||||
std::vector<int> clipMap(L.clips.size(), -1);
|
||||
std::vector<const OpaLoader::AnimClip*> clips;
|
||||
for (const auto& sr : L.skinRecs)
|
||||
if (sr.clipIdx >= 0 && static_cast<size_t>(sr.clipIdx) < L.clips.size() && clipMap[sr.clipIdx] < 0) clipMap[sr.clipIdx] = 0;
|
||||
for (size_t c = 0; c < L.clips.size(); ++c) if (clipMap[c] >= 0) { clipMap[c] = static_cast<int>(clips.size()); clips.push_back(&L.clips[c]); }
|
||||
w.u(static_cast<u32>(clips.size()));
|
||||
for (const OpaLoader::AnimClip* c : clips) { w.pod(c->numJoints); w.pod(c->numFrames); w.vec(c->parents); w.vec(c->mats); }
|
||||
w.u(static_cast<u32>(L.skinRecs.size()));
|
||||
for (const auto& sr : L.skinRecs) {
|
||||
w.u(static_cast<u32>(sr.meshIdx));
|
||||
w.pod<int>(sr.clipIdx >= 0 && static_cast<size_t>(sr.clipIdx) < clipMap.size() ? clipMap[sr.clipIdx] : -1);
|
||||
w.pod(sr.nJoints); w.b(sr.gpu);
|
||||
// skinned on the GPU: the bind pose and weights are the mesh's own (skinBindPos, boneIndices / boneWeights)
|
||||
static const std::vector<float> noF;
|
||||
static const std::vector<int> noI;
|
||||
w.vec(sr.gpu ? noF : sr.basePos); w.vec(sr.gpu ? noI : sr.jidx); w.vec(sr.gpu ? noF : sr.jw);
|
||||
w.vec(sr.boneClip); w.vec(sr.invBind); w.vec(sr.jointBox); w.fa(sr.unriggedBox, 6);
|
||||
}
|
||||
{ // UV tracks the UV-animated meshes play, each with its scroll-or-flipbook answer
|
||||
std::vector<std::pair<const std::string*, const OpaLoader::UVTrack*>> tracks;
|
||||
std::set<std::string> seen;
|
||||
for (const auto& ur : L.uvAnimRecs) {
|
||||
const auto it = L.matUVAnim.find(ur.node);
|
||||
if (it != L.matUVAnim.end() && seen.insert(ur.node).second) tracks.emplace_back(&it->first, &it->second);
|
||||
}
|
||||
w.u(static_cast<u32>(tracks.size()));
|
||||
for (const auto& t : tracks) {
|
||||
const OpaLoader::UVTrack& tr = *t.second;
|
||||
int scroll = tr.scroll;
|
||||
float ru = tr.ru, rv = tr.rv;
|
||||
if (scroll < 0) scroll = L.uvScrollRate(tr, ru, rv) ? 1 : 0;
|
||||
w.str(*t.first); w.pod(tr.nFrames); w.vec(tr.m); w.pod(scroll); w.f(ru); w.f(rv);
|
||||
}
|
||||
}
|
||||
w.u(static_cast<u32>(L.uvAnimRecs.size()));
|
||||
for (const auto& ur : L.uvAnimRecs) { w.u(static_cast<u32>(ur.meshIdx)); w.str(ur.node); w.vec(ur.baseUV); w.b(ur.atRest); }
|
||||
{
|
||||
std::vector<std::pair<const std::string*, const OpaLoader::TintTrack*>> tracks;
|
||||
std::set<std::string> seen;
|
||||
for (const auto& tr : L.tintRecs) {
|
||||
const auto it = L.matTintAnim.find(tr.node);
|
||||
if (it != L.matTintAnim.end() && seen.insert(tr.node).second) tracks.emplace_back(&it->first, &it->second);
|
||||
}
|
||||
w.u(static_cast<u32>(tracks.size()));
|
||||
for (const auto& t : tracks) { w.str(*t.first); w.pod(t.second->nFrames); w.vec(t.second->rgba); }
|
||||
}
|
||||
w.u(static_cast<u32>(L.tintRecs.size()));
|
||||
for (const auto& tr : L.tintRecs) { w.u(static_cast<u32>(tr.meshIdx)); w.str(tr.node); }
|
||||
std::vector<const OpaLoader::VatData*> vats;
|
||||
for (const auto& vr : L.vatRecs) if (vr.vd && std::find(vats.begin(), vats.end(), vr.vd) == vats.end()) vats.push_back(vr.vd);
|
||||
w.u(static_cast<u32>(vats.size()));
|
||||
for (const OpaLoader::VatData* vd : vats) { w.pod(vd->cols); w.pod(vd->frames); w.vec(vd->off); }
|
||||
w.u(static_cast<u32>(L.vatRecs.size()));
|
||||
for (const auto& vr : L.vatRecs) {
|
||||
w.u(static_cast<u32>(vr.meshIdx)); w.vec(vr.basePos); w.vec(vr.col); w.pod(vr.world);
|
||||
w.pod<int>(vr.vd ? static_cast<int>(std::find(vats.begin(), vats.end(), vr.vd) - vats.begin()) : -1);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<OpaLoader> loadClay(Reader& r, size_t meshes) {
|
||||
auto L = std::make_shared<OpaLoader>();
|
||||
L->verbose = false;
|
||||
L->animFps = r.f(); L->animMaxFrames = r.pod<int>(); L->vatFps = r.f(); L->uvMaxLoopSec = r.f();
|
||||
L->animNodes.resize(r.count(44));
|
||||
for (auto& nd : L->animNodes) { if (r.bad) return nullptr; nd.name = r.str(); nd.parent = r.pod<int>(); r.fa(nd.t, 3); r.fa(nd.r, 4); r.fa(nd.s, 3); }
|
||||
for (u32 n = r.count(40); n-- && !r.bad;) {
|
||||
const std::string name = r.str();
|
||||
OpaLoader::NodeTracks nt;
|
||||
nt.t = readTrack(r); nt.r = readTrack(r); nt.s = readTrack(r);
|
||||
if (!trackOk(nt.t) || !trackOk(nt.r) || !trackOk(nt.s)) return nullptr;
|
||||
L->nodeAnim[name] = std::move(nt);
|
||||
}
|
||||
L->animRecs.resize(r.count(145));
|
||||
for (auto& ar : L->animRecs) {
|
||||
if (r.bad) return nullptr;
|
||||
ar.meshIdx = r.u(); ar.nodeIdx = static_cast<u32>(r.pod<int>()); ar.basePos = r.vec<float>(); ar.rigid = r.pod<int>();
|
||||
ar.rest = r.pod<OpaLoader::Mat4>(); r.fa(ar.restInv, 16); ar.atRest = r.b();
|
||||
ar.parentWorld = OpaLoader::identity();
|
||||
if (ar.meshIdx >= meshes) return nullptr;
|
||||
}
|
||||
L->clips.resize(r.count(16));
|
||||
for (auto& c : L->clips) {
|
||||
if (r.bad) return nullptr;
|
||||
c.numJoints = r.pod<int>(); c.numFrames = r.pod<int>(); c.parents = r.vec<int>(); c.mats = r.vec<float>();
|
||||
if (c.numJoints < 0 || c.numFrames < 0 || c.mats.size() < static_cast<size_t>(c.numFrames) * c.numJoints * 16) return nullptr;
|
||||
}
|
||||
L->skinRecs.resize(r.count(57));
|
||||
for (auto& sr : L->skinRecs) {
|
||||
if (r.bad) return nullptr;
|
||||
sr.meshIdx = r.u(); sr.clipIdx = r.pod<int>(); sr.nJoints = r.pod<int>(); sr.gpu = r.b();
|
||||
sr.basePos = r.vec<float>(); sr.jidx = r.vec<int>(); sr.jw = r.vec<float>();
|
||||
sr.boneClip = r.vec<int>(); sr.invBind = r.vec<float>(); sr.jointBox = r.vec<float>(); r.fa(sr.unriggedBox, 6);
|
||||
const size_t nv = sr.basePos.size() / 3;
|
||||
if (sr.meshIdx >= meshes || (!sr.gpu && (sr.jidx.size() < nv * 4 || sr.jw.size() < nv * 4)) ||
|
||||
(sr.gpu && sr.jointBox.size() < static_cast<size_t>(std::max(0, sr.nJoints)) * 6))
|
||||
return nullptr;
|
||||
}
|
||||
for (u32 n = r.count(24); n-- && !r.bad;) {
|
||||
const std::string name = r.str();
|
||||
OpaLoader::UVTrack tr;
|
||||
tr.nFrames = r.pod<int>(); tr.m = r.vec<float>(); tr.scroll = r.pod<int>(); tr.ru = r.f(); tr.rv = r.f();
|
||||
if (tr.nFrames < 0 || tr.m.size() < static_cast<size_t>(tr.nFrames) * 6) return nullptr;
|
||||
L->matUVAnim[name] = std::move(tr);
|
||||
}
|
||||
L->uvAnimRecs.resize(r.count(13));
|
||||
for (auto& ur : L->uvAnimRecs) {
|
||||
if (r.bad) return nullptr;
|
||||
ur.meshIdx = r.u(); ur.node = r.str(); ur.baseUV = r.vec<float>(); ur.atRest = r.b();
|
||||
if (ur.meshIdx >= meshes) return nullptr;
|
||||
}
|
||||
for (u32 n = r.count(12); n-- && !r.bad;) {
|
||||
const std::string name = r.str();
|
||||
OpaLoader::TintTrack tt;
|
||||
tt.nFrames = r.pod<int>(); tt.rgba = r.vec<float>();
|
||||
if (tt.nFrames < 0 || tt.rgba.size() < static_cast<size_t>(tt.nFrames) * 4) return nullptr;
|
||||
L->matTintAnim[name] = std::move(tt);
|
||||
}
|
||||
L->tintRecs.resize(r.count(8));
|
||||
for (auto& tr : L->tintRecs) { if (r.bad) return nullptr; tr.meshIdx = r.u(); tr.node = r.str(); if (tr.meshIdx >= meshes) return nullptr; }
|
||||
std::vector<std::string> vatNames(r.count(12));
|
||||
for (size_t i = 0; i < vatNames.size() && !r.bad; ++i) {
|
||||
vatNames[i] = std::to_string(i); // (keyed by the geo name when parsed; by order here)
|
||||
OpaLoader::VatData& vd = L->vatByBase[vatNames[i]];
|
||||
vd.cols = r.pod<int>(); vd.frames = r.pod<int>(); vd.off = r.vec<float>();
|
||||
if (vd.cols < 0 || vd.frames < 0 || vd.off.size() < static_cast<size_t>(vd.frames) * vd.cols * 3) return nullptr;
|
||||
}
|
||||
L->vatRecs.resize(r.count(80));
|
||||
for (auto& vr : L->vatRecs) {
|
||||
if (r.bad) return nullptr;
|
||||
vr.meshIdx = r.u(); vr.basePos = r.vec<float>(); vr.col = r.vec<int>(); vr.world = r.pod<OpaLoader::Mat4>();
|
||||
const int vd = r.pod<int>();
|
||||
vr.vd = vd >= 0 && static_cast<size_t>(vd) < vatNames.size() ? &L->vatByBase[vatNames[vd]] : nullptr;
|
||||
if (vr.meshIdx >= meshes || (vr.vd && vr.col.size() < vr.basePos.size() / 3)) return nullptr;
|
||||
}
|
||||
return r.bad ? nullptr : L;
|
||||
}
|
||||
|
||||
// ── glTF (GltfLoader) ──
|
||||
|
||||
void saveGltf(const GltfLoader& L, Writer& w) {
|
||||
w.f(L.animDuration); w.fa(L.reanchor, 3);
|
||||
// the nodes the animated and skinned meshes follow, with their ancestors (nodeWorlds: an out-of-range parent is
|
||||
// evaluated as the identity, so it stays out of range)
|
||||
std::vector<int> used;
|
||||
for (const auto& rec : L.nodeAnimRecs) used.push_back(rec.nodeIdx);
|
||||
for (const auto& rec : L.skinned)
|
||||
if (rec.skin >= 0 && static_cast<size_t>(rec.skin) < L.gskins.size()) for (int j : L.gskins[rec.skin].joints) used.push_back(j);
|
||||
const size_t nn = L.gnodes.size();
|
||||
const std::vector<int> nodeMap = keptNodes(nn, used, [&](int i) { return L.gnodes[i].parent; });
|
||||
auto mapNode = [&](int n) { return n >= 0 && static_cast<size_t>(n) < nn ? (nodeMap[n] >= 0 ? nodeMap[n] : INT_MAX) : (n < 0 ? n : INT_MAX); };
|
||||
u32 kept = 0;
|
||||
for (int m : nodeMap) kept += m >= 0;
|
||||
w.u(kept);
|
||||
for (size_t i = 0; i < nn; ++i) {
|
||||
if (nodeMap[i] < 0) continue;
|
||||
const GltfLoader::GNode& g = L.gnodes[i];
|
||||
w.fa(g.t, 3); w.fa(g.r, 4); w.fa(g.s, 3); w.pod<int>(mapNode(g.parent));
|
||||
}
|
||||
w.u(static_cast<u32>(L.gskins.size()));
|
||||
for (const auto& sk : L.gskins) {
|
||||
std::vector<int> joints;
|
||||
for (int j : sk.joints) joints.push_back(mapNode(j));
|
||||
w.vec(joints); w.vec(sk.ibm);
|
||||
}
|
||||
// the channels on kept nodes (in their order: a later one on the same node wins), and the samplers they read
|
||||
std::vector<int> samplerMap(L.gsamplers.size(), -1);
|
||||
std::vector<const GltfLoader::GChannel*> channels;
|
||||
for (const auto& ch : L.gchannels)
|
||||
if (ch.node >= 0 && static_cast<size_t>(ch.node) < nn && nodeMap[ch.node] >= 0 && ch.sampler >= 0 && static_cast<size_t>(ch.sampler) < L.gsamplers.size()) {
|
||||
channels.push_back(&ch);
|
||||
samplerMap[ch.sampler] = 0;
|
||||
}
|
||||
std::vector<const GltfLoader::GSampler*> samplers;
|
||||
for (size_t s = 0; s < L.gsamplers.size(); ++s) if (samplerMap[s] >= 0) { samplerMap[s] = static_cast<int>(samplers.size()); samplers.push_back(&L.gsamplers[s]); }
|
||||
w.u(static_cast<u32>(samplers.size()));
|
||||
for (const GltfLoader::GSampler* s : samplers) { w.vec(s->times); w.vec(s->vals); w.pod(s->comps); w.b(s->step); }
|
||||
w.u(static_cast<u32>(channels.size()));
|
||||
for (const GltfLoader::GChannel* ch : channels) { w.pod<int>(nodeMap[ch->node]); w.pod(ch->path); w.pod<int>(samplerMap[ch->sampler]); }
|
||||
w.u(static_cast<u32>(L.skinned.size()));
|
||||
for (const auto& rec : L.skinned) { w.pod(rec.meshIdx); w.pod(rec.skin); w.u(rec.nv); w.vec(rec.basePos); w.vec(rec.jidx); w.vec(rec.jw); }
|
||||
w.u(static_cast<u32>(L.nodeAnimRecs.size()));
|
||||
for (const auto& rec : L.nodeAnimRecs) { w.pod(rec.meshIdx); w.pod<int>(mapNode(rec.nodeIdx)); w.vec(rec.basePos); w.pod(rec.rigid); w.fa(rec.rest, 16); w.fa(rec.restInv, 16); w.b(rec.atRest); }
|
||||
}
|
||||
|
||||
std::shared_ptr<GltfLoader> loadGltf(Reader& r, size_t meshes) {
|
||||
auto L = std::make_shared<GltfLoader>();
|
||||
L->verbose = false;
|
||||
L->animDuration = r.f(); r.fa(L->reanchor, 3);
|
||||
L->gnodes.resize(r.count(44));
|
||||
for (auto& g : L->gnodes) { if (r.bad) return nullptr; r.fa(g.t, 3); r.fa(g.r, 4); r.fa(g.s, 3); g.parent = r.pod<int>(); }
|
||||
L->gskins.resize(r.count(8));
|
||||
for (auto& sk : L->gskins) { if (r.bad) return nullptr; sk.joints = r.vec<int>(); sk.ibm = r.vec<float>(); }
|
||||
L->gsamplers.resize(r.count(13));
|
||||
for (auto& s : L->gsamplers) {
|
||||
if (r.bad) return nullptr;
|
||||
s.times = r.vec<float>(); s.vals = r.vec<float>(); s.comps = r.pod<int>(); s.step = r.b();
|
||||
// (sampleSampler: up to 4 values a key, a key's worth even without keys)
|
||||
if (s.comps < 0 || s.comps > 4 || (s.times.empty() ? !s.vals.empty() && s.vals.size() < static_cast<size_t>(s.comps)
|
||||
: s.vals.size() < s.times.size() * s.comps))
|
||||
return nullptr;
|
||||
}
|
||||
L->gchannels.resize(r.count(12));
|
||||
for (auto& ch : L->gchannels) { if (r.bad) return nullptr; ch.node = r.pod<int>(); ch.path = r.pod<int>(); ch.sampler = r.pod<int>(); }
|
||||
L->skinned.resize(r.count(24));
|
||||
for (auto& rec : L->skinned) {
|
||||
if (r.bad) return nullptr;
|
||||
rec.meshIdx = r.pod<int>(); rec.skin = r.pod<int>(); rec.nv = r.u(); rec.basePos = r.vec<float>(); rec.jidx = r.vec<u8>(); rec.jw = r.vec<float>();
|
||||
if (rec.meshIdx < 0 || static_cast<size_t>(rec.meshIdx) >= meshes || rec.basePos.size() < static_cast<size_t>(rec.nv) * 3 ||
|
||||
rec.jidx.size() < static_cast<size_t>(rec.nv) * 4 || rec.jw.size() < static_cast<size_t>(rec.nv) * 4)
|
||||
return nullptr;
|
||||
}
|
||||
L->nodeAnimRecs.resize(r.count(145));
|
||||
for (auto& rec : L->nodeAnimRecs) {
|
||||
if (r.bad) return nullptr;
|
||||
rec.meshIdx = r.pod<int>(); rec.nodeIdx = r.pod<int>(); rec.basePos = r.vec<float>(); rec.rigid = r.pod<int>(); r.fa(rec.rest, 16); r.fa(rec.restInv, 16); rec.atRest = r.b();
|
||||
if (rec.meshIdx < 0 || static_cast<size_t>(rec.meshIdx) >= meshes) return nullptr;
|
||||
}
|
||||
return r.bad ? nullptr : L;
|
||||
}
|
||||
|
||||
// The per-frame play of a loader's animation, parsed or rebuilt: it works on the scene's meshes (the loader indexes them
|
||||
// as its own, same order).
|
||||
template <typename Loader> std::function<void(float, std::vector<MeshData>&)> player(std::shared_ptr<Loader> L) {
|
||||
return [L](float t, std::vector<MeshData>& m) { std::swap(L->meshes, m); L->animateRuntime(t); std::swap(L->meshes, m); };
|
||||
}
|
||||
|
||||
// One stored animation, rebuilt: the loader holding it.
|
||||
struct Animator {
|
||||
std::shared_ptr<OpaLoader> clay;
|
||||
std::shared_ptr<GltfLoader> gltf;
|
||||
std::function<void(float, std::vector<MeshData>&)> animate() const { return clay ? player(clay) : gltf ? player(gltf) : nullptr; }
|
||||
void save(std::vector<u8>& out) const { if (clay) animbake::save(*clay, out); else if (gltf) animbake::save(*gltf, out); }
|
||||
};
|
||||
|
||||
bool rebuild(const std::vector<u8>& bytes, size_t meshes, Animator& a) {
|
||||
Reader r{bytes.data(), bytes.data() + bytes.size()};
|
||||
const u8 kind = r.pod<u8>();
|
||||
a = Animator();
|
||||
if (kind == kClay) a.clay = loadClay(r, meshes);
|
||||
else if (kind == kGltf) a.gltf = loadGltf(r, meshes);
|
||||
return (a.clay || a.gltf) && !r.bad && r.p == r.end;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace animbake {
|
||||
|
||||
void adopt(HslScene& scene, std::shared_ptr<OpaLoader> loader) {
|
||||
scene.animate = player(loader);
|
||||
scene.saveAnimation = [loader](std::vector<u8>& out) { save(*loader, out); };
|
||||
}
|
||||
|
||||
void adopt(HslScene& scene, std::shared_ptr<GltfLoader> loader) {
|
||||
scene.animate = player(loader);
|
||||
scene.saveAnimation = [loader](std::vector<u8>& out) { save(*loader, out); };
|
||||
}
|
||||
|
||||
void save(const OpaLoader& loader, std::vector<u8>& out) {
|
||||
out.clear();
|
||||
Writer w{out};
|
||||
w.pod(kClay);
|
||||
saveClay(loader, w);
|
||||
}
|
||||
|
||||
void save(const GltfLoader& loader, std::vector<u8>& out) {
|
||||
out.clear();
|
||||
Writer w{out};
|
||||
w.pod(kGltf);
|
||||
saveGltf(loader, w);
|
||||
}
|
||||
|
||||
bool load(const std::vector<u8>& bytes, size_t meshes, HslScene& scene) {
|
||||
Animator a;
|
||||
if (!rebuild(bytes, meshes, a)) return false;
|
||||
scene.animate = a.animate();
|
||||
scene.saveAnimation = [bytes](std::vector<u8>& out) { out = bytes; };
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<int> targets(const std::vector<u8>& bytes) {
|
||||
Animator a;
|
||||
std::vector<int> out;
|
||||
if (!rebuild(bytes, SIZE_MAX, a)) return out;
|
||||
if (a.clay) {
|
||||
const OpaLoader& L = *a.clay;
|
||||
for (const auto& x : L.animRecs) out.push_back(static_cast<int>(x.meshIdx));
|
||||
for (const auto& x : L.skinRecs) out.push_back(static_cast<int>(x.meshIdx));
|
||||
for (const auto& x : L.uvAnimRecs) out.push_back(static_cast<int>(x.meshIdx));
|
||||
for (const auto& x : L.tintRecs) out.push_back(static_cast<int>(x.meshIdx));
|
||||
for (const auto& x : L.vatRecs) out.push_back(static_cast<int>(x.meshIdx));
|
||||
} else {
|
||||
for (const auto& x : a.gltf->skinned) out.push_back(x.meshIdx);
|
||||
for (const auto& x : a.gltf->nodeAnimRecs) out.push_back(x.meshIdx);
|
||||
}
|
||||
std::sort(out.begin(), out.end());
|
||||
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
bool startPose(std::vector<u8>& bytes, const std::vector<MeshData>& meshes, std::vector<std::pair<size_t, MeshData>>& posed) {
|
||||
posed.clear();
|
||||
Animator a;
|
||||
if (!rebuild(bytes, meshes.size(), a)) return false;
|
||||
// only the meshes it writes are copied; the others stay empty (animate() never looks at them)
|
||||
std::vector<MeshData> work(meshes.size());
|
||||
const std::vector<int> written = targets(bytes);
|
||||
for (int m : written) work[m] = meshes[m];
|
||||
a.animate()(0.0f, work);
|
||||
for (int m : written) posed.emplace_back(static_cast<size_t>(m), std::move(work[m]));
|
||||
a.save(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool toStart(HslScene& scene) {
|
||||
if (!scene.saveAnimation) return false;
|
||||
std::vector<u8> bytes;
|
||||
scene.saveAnimation(bytes);
|
||||
std::vector<std::pair<size_t, MeshData>> posed;
|
||||
if (!startPose(bytes, scene.meshes, posed)) return false;
|
||||
for (auto& [m, md] : posed) scene.meshes[m] = std::move(md);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace animbake
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
// A Clay (.opa / .ovrscene) or glTF home's CPU animation as data, for the baked scene (scene.bin v13): everything the
|
||||
// loader's animateRuntime(t) reads (the node hierarchy and its keyed tracks, skins and their clips, UV scroll /
|
||||
// flipbook and tint tracks, vertex-animation offsets, each animated mesh's base vertices and its rigid-or-rewritten
|
||||
// choice), and nothing the parse needed besides. A .fenv's animation is rebuilt from it as an OpaLoader / GltfLoader
|
||||
// holding only that state, so it plays through the loaders' own animate() — the same code, the same numbers.
|
||||
// Worked out at the bake: the rigid choices, each UV track's scroll-or-flipbook answer, and the nodes / tracks / clips
|
||||
// no animated mesh depends on (left out).
|
||||
#include "hsl_scene.h"
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class OpaLoader;
|
||||
class GltfLoader;
|
||||
|
||||
namespace animbake {
|
||||
|
||||
// A parsed home's animation, given to its scene: scene.animate plays it through the loader's animateRuntime(t) and
|
||||
// scene.saveAnimation writes it. A .fenv's rebuilt one plays through the same function (one compiled copy of the
|
||||
// code, so no compiler choice — operand order, fused multiply-adds — can make the two differ).
|
||||
void adopt(HslScene& scene, std::shared_ptr<OpaLoader> loader);
|
||||
void adopt(HslScene& scene, std::shared_ptr<GltfLoader> loader);
|
||||
|
||||
// The loader's animation, written as data (read only: call it from the thread that animates the scene).
|
||||
void save(const OpaLoader& loader, std::vector<u8>& out);
|
||||
void save(const GltfLoader& loader, std::vector<u8>& out);
|
||||
|
||||
// The scene's animation from what save() wrote: scene.animate plays it, scene.saveAnimation writes the same bytes
|
||||
// again. `meshes`: how many meshes the scene has (an animation that writes past them is damage). False: damaged.
|
||||
bool load(const std::vector<u8>& bytes, size_t meshes, HslScene& scene);
|
||||
|
||||
// Every mesh the animation writes (animatedMeshes is the ones seen moving at a few times; this is all of them).
|
||||
std::vector<int> targets(const std::vector<u8>& bytes);
|
||||
|
||||
// The meshes the animation writes as a fresh load has them (the animation at t = 0, from a new animator), copied, by
|
||||
// mesh index; `bytes` becomes the animation as that animator saves it (its rigid choices made). False: damaged.
|
||||
bool startPose(std::vector<u8>& bytes, const std::vector<MeshData>& meshes, std::vector<std::pair<size_t, MeshData>>& posed);
|
||||
|
||||
// The same in place: the scene's animated meshes put back where a load has them (a copy of a scene Frame Studio has
|
||||
// been playing, before the optimizer reads its vertices). False: it has no stored animation, or it's damaged.
|
||||
bool toStart(HslScene& scene);
|
||||
|
||||
} // namespace animbake
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
// The baked scene's binary coder (scene_bake.cpp, anim_bake.cpp): little-endian PODs, length-prefixed strings and
|
||||
// vectors, and a reader that turns every short or damaged read into `bad` instead of reading past the end.
|
||||
#include "core/types.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace bakeio {
|
||||
|
||||
struct Writer {
|
||||
std::vector<u8>& out;
|
||||
template <typename T> void pod(const T& v) { const u8* p = reinterpret_cast<const u8*>(&v); out.insert(out.end(), p, p + sizeof(T)); }
|
||||
void u(u32 v) { pod(v); }
|
||||
void b(bool v) { pod(static_cast<u8>(v)); }
|
||||
void f(float v) { pod(v); }
|
||||
void fa(const float* a, int n) { for (int i = 0; i < n; ++i) f(a[i]); }
|
||||
void str(const std::string& s) { u(static_cast<u32>(s.size())); out.insert(out.end(), s.begin(), s.end()); }
|
||||
template <typename T> void vec(const std::vector<T>& v) {
|
||||
u(static_cast<u32>(v.size()));
|
||||
if (!v.empty()) { const u8* p = reinterpret_cast<const u8*>(v.data()); out.insert(out.end(), p, p + v.size() * sizeof(T)); }
|
||||
}
|
||||
};
|
||||
|
||||
struct Reader {
|
||||
const u8* p;
|
||||
const u8* end;
|
||||
bool bad = false;
|
||||
bool need(size_t n) { if (bad || static_cast<size_t>(end - p) < n) { bad = true; return false; } return true; }
|
||||
template <typename T> T pod() { T v{}; if (need(sizeof(T))) { std::memcpy(&v, p, sizeof(T)); p += sizeof(T); } return v; }
|
||||
u32 u() { return pod<u32>(); }
|
||||
bool b() { return pod<u8>() != 0; }
|
||||
float f() { return pod<float>(); }
|
||||
void fa(float* a, int n) { for (int i = 0; i < n; ++i) a[i] = f(); }
|
||||
std::string str() { const u32 n = u(); std::string s; if (need(n)) { s.assign(reinterpret_cast<const char*>(p), n); p += n; } return s; }
|
||||
// An element count, checked against what is left (each element takes at least `minBytes`): a damaged count
|
||||
// must not size a vector of billions before the reads run out.
|
||||
u32 count(size_t minBytes) {
|
||||
const u32 n = u();
|
||||
if (bad || static_cast<size_t>(end - p) / minBytes < n) { bad = true; return 0; }
|
||||
return n;
|
||||
}
|
||||
template <typename T> std::vector<T> vec() {
|
||||
const u32 n = u();
|
||||
std::vector<T> v;
|
||||
if (n && need(static_cast<size_t>(n) * sizeof(T))) { v.resize(n); std::memcpy(v.data(), p, static_cast<size_t>(n) * sizeof(T)); p += static_cast<size_t>(n) * sizeof(T); }
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace bakeio
|
||||
@@ -0,0 +1,272 @@
|
||||
#pragma once
|
||||
// GENERATED by tools/gen_clay_shader.py: the built-in Clay (.opa home) shaders. Do not edit by hand.
|
||||
#include <cstdint>
|
||||
|
||||
static const uint32_t kClayVertSpirv[] = {
|
||||
0x07230203,0x00010000,0x00000000,0x0000004d,0x00000000,0x00020011,0x00000001,0x00020011,
|
||||
0x00001157,0x0006000a,0x5f565053,0x5f52484b,0x746c756d,0x65697669,0x00000077,0x0003000e,
|
||||
0x00000000,0x00000001,0x0010000f,0x00000000,0x00000001,0x6e69616d,0x00000000,0x00000020,
|
||||
0x00000021,0x00000022,0x00000023,0x00000024,0x00000029,0x00000025,0x00000026,0x00000027,
|
||||
0x0000002a,0x00000028,0x00040005,0x00000001,0x6e69616d,0x00000000,0x00060005,0x0000000c,
|
||||
0x626f6c47,0x6e556c61,0x726f6669,0x0000736d,0x00060005,0x0000001e,0x626f6c67,0x6e556c61,
|
||||
0x726f6669,0x0000736d,0x00060005,0x0000000d,0x68737550,0x736e6f43,0x746e6174,0x00000073,
|
||||
0x00060005,0x0000001f,0x68737570,0x736e6f43,0x746e6174,0x00000073,0x00050005,0x00000020,
|
||||
0x69736f70,0x6e6f6974,0x00000000,0x00030005,0x00000021,0x00007675,0x00040005,0x00000022,
|
||||
0x6f6c6f63,0x00000072,0x00040005,0x00000023,0x6d726f6e,0x00006c61,0x00030005,0x00000025,
|
||||
0x00565576,0x00040005,0x00000026,0x6c6f4376,0x0000726f,0x00040005,0x00000027,0x726f4e76,
|
||||
0x006c616d,0x00050005,0x00000029,0x6867696c,0x70616d74,0x00007655,0x00040005,0x0000002a,
|
||||
0x31565576,0x00000000,0x00070006,0x0000000c,0x00000000,0x70696c63,0x6d6f7246,0x6c726f57,
|
||||
0x00000064,0x00070006,0x0000000d,0x00000000,0x6c726f77,0x6f724664,0x646f4d6d,0x00006c65,
|
||||
0x00050006,0x0000000d,0x00000001,0x6f6c6f63,0x00000072,0x00070006,0x0000000d,0x00000002,
|
||||
0x72547675,0x66736e61,0x556d726f,0x00000000,0x00070006,0x0000000d,0x00000003,0x72547675,
|
||||
0x66736e61,0x566d726f,0x00000000,0x00040047,0x00000020,0x0000001e,0x00000000,0x00040047,
|
||||
0x00000021,0x0000001e,0x00000001,0x00040047,0x00000022,0x0000001e,0x00000002,0x00040047,
|
||||
0x00000023,0x0000001e,0x00000003,0x00040047,0x00000029,0x0000001e,0x00000004,0x00040047,
|
||||
0x00000025,0x0000001e,0x00000000,0x00040047,0x00000026,0x0000001e,0x00000001,0x00040047,
|
||||
0x00000027,0x0000001e,0x00000002,0x00040047,0x0000002a,0x0000001e,0x00000003,0x00040047,
|
||||
0x00000028,0x0000000b,0x00000000,0x00040047,0x00000024,0x0000000b,0x00001158,0x00040047,
|
||||
0x0000000b,0x00000006,0x00000040,0x00050048,0x0000000c,0x00000000,0x00000023,0x00000000,
|
||||
0x00040048,0x0000000c,0x00000000,0x00000005,0x00050048,0x0000000c,0x00000000,0x00000007,
|
||||
0x00000010,0x00030047,0x0000000c,0x00000002,0x00040047,0x0000001e,0x00000022,0x00000000,
|
||||
0x00040047,0x0000001e,0x00000021,0x00000000,0x00050048,0x0000000d,0x00000000,0x00000023,
|
||||
0x00000000,0x00040048,0x0000000d,0x00000000,0x00000005,0x00050048,0x0000000d,0x00000000,
|
||||
0x00000007,0x00000010,0x00050048,0x0000000d,0x00000001,0x00000023,0x00000040,0x00050048,
|
||||
0x0000000d,0x00000002,0x00000023,0x00000050,0x00050048,0x0000000d,0x00000003,0x00000023,
|
||||
0x00000060,0x00030047,0x0000000d,0x00000002,0x00020013,0x00000002,0x00030021,0x00000003,
|
||||
0x00000002,0x00030016,0x00000004,0x00000020,0x00040015,0x00000005,0x00000020,0x00000001,
|
||||
0x00040015,0x00000006,0x00000020,0x00000000,0x00040017,0x00000007,0x00000004,0x00000002,
|
||||
0x00040017,0x00000008,0x00000004,0x00000003,0x00040017,0x00000009,0x00000004,0x00000004,
|
||||
0x00040018,0x0000000a,0x00000009,0x00000004,0x0004002b,0x00000006,0x0000001d,0x00000002,
|
||||
0x0004001c,0x0000000b,0x0000000a,0x0000001d,0x0003001e,0x0000000c,0x0000000b,0x0006001e,
|
||||
0x0000000d,0x0000000a,0x00000009,0x00000009,0x00000009,0x00040020,0x0000000e,0x00000002,
|
||||
0x0000000c,0x00040020,0x0000000f,0x00000002,0x0000000a,0x00040020,0x00000010,0x00000009,
|
||||
0x0000000d,0x00040020,0x00000011,0x00000009,0x0000000a,0x00040020,0x00000012,0x00000009,
|
||||
0x00000009,0x00040020,0x00000013,0x00000001,0x00000008,0x00040020,0x00000014,0x00000001,
|
||||
0x00000007,0x00040020,0x00000015,0x00000001,0x00000009,0x00040020,0x00000016,0x00000001,
|
||||
0x00000005,0x00040020,0x00000017,0x00000003,0x00000007,0x00040020,0x00000018,0x00000003,
|
||||
0x00000009,0x00040020,0x00000019,0x00000003,0x00000008,0x0004002b,0x00000004,0x0000001a,
|
||||
0x3f800000,0x0004002b,0x00000005,0x0000001b,0x00000000,0x0004002b,0x00000005,0x0000001c,
|
||||
0x00000001,0x0004002b,0x00000005,0x0000003f,0x00000002,0x0004002b,0x00000005,0x00000040,
|
||||
0x00000003,0x0004003b,0x0000000e,0x0000001e,0x00000002,0x0004003b,0x00000010,0x0000001f,
|
||||
0x00000009,0x0004003b,0x00000013,0x00000020,0x00000001,0x0004003b,0x00000014,0x00000021,
|
||||
0x00000001,0x0004003b,0x00000015,0x00000022,0x00000001,0x0004003b,0x00000013,0x00000023,
|
||||
0x00000001,0x0004003b,0x00000016,0x00000024,0x00000001,0x0004003b,0x00000017,0x00000025,
|
||||
0x00000003,0x0004003b,0x00000018,0x00000026,0x00000003,0x0004003b,0x00000019,0x00000027,
|
||||
0x00000003,0x0004003b,0x00000018,0x00000028,0x00000003,0x0004003b,0x00000014,0x00000029,
|
||||
0x00000001,0x0004003b,0x00000017,0x0000002a,0x00000003,0x00050036,0x00000002,0x00000001,
|
||||
0x00000000,0x00000003,0x000200f8,0x0000002c,0x0004003d,0x00000005,0x0000002d,0x00000024,
|
||||
0x00060041,0x0000000f,0x0000002e,0x0000001e,0x0000001b,0x0000002d,0x0004003d,0x0000000a,
|
||||
0x0000002f,0x0000002e,0x00050041,0x00000011,0x00000030,0x0000001f,0x0000001b,0x0004003d,
|
||||
0x0000000a,0x00000031,0x00000030,0x00050041,0x00000012,0x00000032,0x0000001f,0x0000001c,
|
||||
0x0004003d,0x00000009,0x00000033,0x00000032,0x0004003d,0x00000008,0x00000034,0x00000020,
|
||||
0x00050051,0x00000004,0x00000035,0x00000034,0x00000000,0x00050051,0x00000004,0x00000036,
|
||||
0x00000034,0x00000001,0x00050051,0x00000004,0x00000037,0x00000034,0x00000002,0x00070050,
|
||||
0x00000009,0x00000038,0x00000035,0x00000036,0x00000037,0x0000001a,0x00050091,0x00000009,
|
||||
0x00000039,0x00000031,0x00000038,0x00050091,0x00000009,0x0000003a,0x0000002f,0x00000039,
|
||||
0x0003003e,0x00000028,0x0000003a,0x0004003d,0x00000007,0x0000003b,0x00000021,0x00050041,
|
||||
0x00000012,0x00000041,0x0000001f,0x0000003f,0x0004003d,0x00000009,0x00000042,0x00000041,
|
||||
0x00050041,0x00000012,0x00000043,0x0000001f,0x00000040,0x0004003d,0x00000009,0x00000044,
|
||||
0x00000043,0x0008004f,0x00000008,0x00000045,0x00000042,0x00000042,0x00000000,0x00000001,
|
||||
0x00000002,0x0008004f,0x00000008,0x00000046,0x00000044,0x00000044,0x00000000,0x00000001,
|
||||
0x00000002,0x00050051,0x00000004,0x00000047,0x0000003b,0x00000000,0x00050051,0x00000004,
|
||||
0x00000048,0x0000003b,0x00000001,0x00060050,0x00000008,0x00000049,0x00000047,0x00000048,
|
||||
0x0000001a,0x00050094,0x00000004,0x0000004a,0x00000045,0x00000049,0x00050094,0x00000004,
|
||||
0x0000004b,0x00000046,0x00000049,0x00050050,0x00000007,0x0000004c,0x0000004a,0x0000004b,
|
||||
0x0003003e,0x00000025,0x0000004c,0x0003003e,0x00000026,0x00000033,0x0004003d,0x00000008,
|
||||
0x0000003e,0x00000023,0x0003003e,0x00000027,0x0000003e,0x0004003d,0x00000007,0x0000002b,
|
||||
0x00000029,0x0003003e,0x0000002a,0x0000002b,0x000100fd,0x00010038,
|
||||
};
|
||||
|
||||
static const uint32_t kClayFragSpirv[] = {
|
||||
0x07230203,0x00010000,0x00000000,0x0000002e,0x00000000,0x00020011,0x00000001,0x0003000e,
|
||||
0x00000000,0x00000001,0x0009000f,0x00000004,0x00000001,0x6e69616d,0x00000000,0x00000012,
|
||||
0x00000013,0x00000023,0x00000014,0x00030010,0x00000001,0x00000007,0x00040005,0x00000001,
|
||||
0x6e69616d,0x00000000,0x00060005,0x00000010,0x65736162,0x6f6c6f43,0x78655472,0x00000000,
|
||||
0x00070005,0x00000011,0x656e696c,0x72577261,0x61537061,0x656c706d,0x00000072,0x00030005,
|
||||
0x00000012,0x00565576,0x00040005,0x00000013,0x6c6f4376,0x0000726f,0x00050005,0x00000014,
|
||||
0x4374756f,0x726f6c6f,0x00000000,0x00050005,0x00000022,0x6867696c,0x70616d74,0x00786554,
|
||||
0x00040005,0x00000023,0x31565576,0x00000000,0x00040047,0x00000012,0x0000001e,0x00000000,
|
||||
0x00040047,0x00000013,0x0000001e,0x00000001,0x00040047,0x00000014,0x0000001e,0x00000000,
|
||||
0x00040047,0x00000011,0x00000022,0x00000002,0x00040047,0x00000011,0x00000021,0x00000000,
|
||||
0x00040047,0x00000010,0x00000022,0x00000002,0x00040047,0x00000010,0x00000021,0x00000001,
|
||||
0x00040047,0x00000022,0x00000022,0x00000002,0x00040047,0x00000022,0x00000021,0x00000002,
|
||||
0x00040047,0x00000023,0x0000001e,0x00000003,0x00020013,0x00000002,0x00030021,0x00000003,
|
||||
0x00000002,0x00030016,0x00000004,0x00000020,0x00020014,0x0000000a,0x00040017,0x00000005,
|
||||
0x00000004,0x00000002,0x00040017,0x00000006,0x00000004,0x00000004,0x00090019,0x00000007,
|
||||
0x00000004,0x00000001,0x00000000,0x00000000,0x00000000,0x00000001,0x00000000,0x0002001a,
|
||||
0x00000008,0x0003001b,0x00000009,0x00000007,0x00040020,0x0000000b,0x00000000,0x00000007,
|
||||
0x00040020,0x0000000c,0x00000000,0x00000008,0x00040020,0x0000000d,0x00000001,0x00000005,
|
||||
0x00040020,0x0000000e,0x00000001,0x00000006,0x00040020,0x0000000f,0x00000003,0x00000006,
|
||||
0x0004002b,0x00000004,0x00000015,0x3f000000,0x0004002b,0x00000004,0x0000002b,0x3f800000,
|
||||
0x0004003b,0x0000000b,0x00000010,0x00000000,0x0004003b,0x0000000c,0x00000011,0x00000000,
|
||||
0x0004003b,0x0000000d,0x00000012,0x00000001,0x0004003b,0x0000000e,0x00000013,0x00000001,
|
||||
0x0004003b,0x0000000f,0x00000014,0x00000003,0x0004003b,0x0000000b,0x00000022,0x00000000,
|
||||
0x0004003b,0x0000000d,0x00000023,0x00000001,0x00050036,0x00000002,0x00000001,0x00000000,
|
||||
0x00000003,0x000200f8,0x00000016,0x0004003d,0x00000007,0x00000017,0x00000010,0x0004003d,
|
||||
0x00000008,0x00000018,0x00000011,0x00050056,0x00000009,0x00000019,0x00000017,0x00000018,
|
||||
0x0004003d,0x00000005,0x0000001a,0x00000012,0x00050057,0x00000006,0x0000001b,0x00000019,
|
||||
0x0000001a,0x0004003d,0x00000006,0x0000001c,0x00000013,0x00050085,0x00000006,0x0000002d,
|
||||
0x0000001b,0x0000001c,0x0004003d,0x00000007,0x00000024,0x00000022,0x00050056,0x00000009,
|
||||
0x00000025,0x00000024,0x00000018,0x0004003d,0x00000005,0x00000026,0x00000023,0x00050057,
|
||||
0x00000006,0x00000027,0x00000025,0x00000026,0x00050051,0x00000004,0x00000028,0x00000027,
|
||||
0x00000000,0x00050051,0x00000004,0x00000029,0x00000027,0x00000001,0x00050051,0x00000004,
|
||||
0x0000002a,0x00000027,0x00000002,0x00070050,0x00000006,0x0000002c,0x00000028,0x00000029,
|
||||
0x0000002a,0x0000002b,0x00050085,0x00000006,0x0000001d,0x0000002d,0x0000002c,0x0003003e,
|
||||
0x00000014,0x0000001d,0x000100fd,0x00010038,
|
||||
};
|
||||
|
||||
static const uint32_t kClayFragCutoutSpirv[] = {
|
||||
0x07230203,0x00010000,0x00000000,0x0000002e,0x00000000,0x00020011,0x00000001,0x0003000e,
|
||||
0x00000000,0x00000001,0x0009000f,0x00000004,0x00000001,0x6e69616d,0x00000000,0x00000012,
|
||||
0x00000013,0x00000023,0x00000014,0x00030010,0x00000001,0x00000007,0x00040005,0x00000001,
|
||||
0x6e69616d,0x00000000,0x00060005,0x00000010,0x65736162,0x6f6c6f43,0x78655472,0x00000000,
|
||||
0x00070005,0x00000011,0x656e696c,0x72577261,0x61537061,0x656c706d,0x00000072,0x00030005,
|
||||
0x00000012,0x00565576,0x00040005,0x00000013,0x6c6f4376,0x0000726f,0x00050005,0x00000014,
|
||||
0x4374756f,0x726f6c6f,0x00000000,0x00050005,0x00000022,0x6867696c,0x70616d74,0x00786554,
|
||||
0x00040005,0x00000023,0x31565576,0x00000000,0x00040047,0x00000012,0x0000001e,0x00000000,
|
||||
0x00040047,0x00000013,0x0000001e,0x00000001,0x00040047,0x00000014,0x0000001e,0x00000000,
|
||||
0x00040047,0x00000011,0x00000022,0x00000002,0x00040047,0x00000011,0x00000021,0x00000000,
|
||||
0x00040047,0x00000010,0x00000022,0x00000002,0x00040047,0x00000010,0x00000021,0x00000001,
|
||||
0x00040047,0x00000022,0x00000022,0x00000002,0x00040047,0x00000022,0x00000021,0x00000002,
|
||||
0x00040047,0x00000023,0x0000001e,0x00000003,0x00020013,0x00000002,0x00030021,0x00000003,
|
||||
0x00000002,0x00030016,0x00000004,0x00000020,0x00020014,0x0000000a,0x00040017,0x00000005,
|
||||
0x00000004,0x00000002,0x00040017,0x00000006,0x00000004,0x00000004,0x00090019,0x00000007,
|
||||
0x00000004,0x00000001,0x00000000,0x00000000,0x00000000,0x00000001,0x00000000,0x0002001a,
|
||||
0x00000008,0x0003001b,0x00000009,0x00000007,0x00040020,0x0000000b,0x00000000,0x00000007,
|
||||
0x00040020,0x0000000c,0x00000000,0x00000008,0x00040020,0x0000000d,0x00000001,0x00000005,
|
||||
0x00040020,0x0000000e,0x00000001,0x00000006,0x00040020,0x0000000f,0x00000003,0x00000006,
|
||||
0x0004002b,0x00000004,0x00000015,0x3f000000,0x0004002b,0x00000004,0x0000002b,0x3f800000,
|
||||
0x0004003b,0x0000000b,0x00000010,0x00000000,0x0004003b,0x0000000c,0x00000011,0x00000000,
|
||||
0x0004003b,0x0000000d,0x00000012,0x00000001,0x0004003b,0x0000000e,0x00000013,0x00000001,
|
||||
0x0004003b,0x0000000f,0x00000014,0x00000003,0x0004003b,0x0000000b,0x00000022,0x00000000,
|
||||
0x0004003b,0x0000000d,0x00000023,0x00000001,0x00050036,0x00000002,0x00000001,0x00000000,
|
||||
0x00000003,0x000200f8,0x00000016,0x0004003d,0x00000007,0x00000017,0x00000010,0x0004003d,
|
||||
0x00000008,0x00000018,0x00000011,0x00050056,0x00000009,0x00000019,0x00000017,0x00000018,
|
||||
0x0004003d,0x00000005,0x0000001a,0x00000012,0x00050057,0x00000006,0x0000001b,0x00000019,
|
||||
0x0000001a,0x0004003d,0x00000006,0x0000001c,0x00000013,0x00050085,0x00000006,0x0000002d,
|
||||
0x0000001b,0x0000001c,0x0004003d,0x00000007,0x00000024,0x00000022,0x00050056,0x00000009,
|
||||
0x00000025,0x00000024,0x00000018,0x0004003d,0x00000005,0x00000026,0x00000023,0x00050057,
|
||||
0x00000006,0x00000027,0x00000025,0x00000026,0x00050051,0x00000004,0x00000028,0x00000027,
|
||||
0x00000000,0x00050051,0x00000004,0x00000029,0x00000027,0x00000001,0x00050051,0x00000004,
|
||||
0x0000002a,0x00000027,0x00000002,0x00070050,0x00000006,0x0000002c,0x00000028,0x00000029,
|
||||
0x0000002a,0x0000002b,0x00050085,0x00000006,0x0000001d,0x0000002d,0x0000002c,0x00050051,
|
||||
0x00000004,0x0000001e,0x0000001d,0x00000003,0x000500b8,0x0000000a,0x0000001f,0x0000001e,
|
||||
0x00000015,0x000300f7,0x00000021,0x00000000,0x000400fa,0x0000001f,0x00000020,0x00000021,
|
||||
0x000200f8,0x00000020,0x000100fc,0x000200f8,0x00000021,0x0003003e,0x00000014,0x0000001d,
|
||||
0x000100fd,0x00010038,
|
||||
};
|
||||
|
||||
static const uint32_t kClaySkinnedVertSpirv[] = {
|
||||
0x07230203,0x00010000,0x00000000,0x00000082,0x00000000,0x00020011,0x00000001,0x00020011,
|
||||
0x00001157,0x0006000a,0x5f565053,0x5f52484b,0x746c756d,0x65697669,0x00000077,0x0003000e,
|
||||
0x00000000,0x00000001,0x0012000f,0x00000000,0x00000001,0x6e69616d,0x00000000,0x00000020,
|
||||
0x00000021,0x00000022,0x00000023,0x00000024,0x00000029,0x00000025,0x00000026,0x00000027,
|
||||
0x0000002a,0x00000028,0x0000004f,0x00000050,0x00050005,0x0000004f,0x656e6f62,0x69646e49,
|
||||
0x00736563,0x00050005,0x00000050,0x656e6f62,0x67696557,0x00737468,0x00070005,0x00000053,
|
||||
0x6e696b53,0x676e696e,0x7274614d,0x73656369,0x00000000,0x00070005,0x00000055,0x6b536273,
|
||||
0x696e6e69,0x614d676e,0x63697274,0x00007365,0x00050006,0x00000053,0x00000000,0x6e696f6a,
|
||||
0x00007374,0x00040005,0x00000001,0x6e69616d,0x00000000,0x00060005,0x0000000c,0x626f6c47,
|
||||
0x6e556c61,0x726f6669,0x0000736d,0x00060005,0x0000001e,0x626f6c67,0x6e556c61,0x726f6669,
|
||||
0x0000736d,0x00060005,0x0000000d,0x68737550,0x736e6f43,0x746e6174,0x00000073,0x00060005,
|
||||
0x0000001f,0x68737570,0x736e6f43,0x746e6174,0x00000073,0x00050005,0x00000020,0x69736f70,
|
||||
0x6e6f6974,0x00000000,0x00030005,0x00000021,0x00007675,0x00040005,0x00000022,0x6f6c6f63,
|
||||
0x00000072,0x00040005,0x00000023,0x6d726f6e,0x00006c61,0x00030005,0x00000025,0x00565576,
|
||||
0x00040005,0x00000026,0x6c6f4376,0x0000726f,0x00040005,0x00000027,0x726f4e76,0x006c616d,
|
||||
0x00050005,0x00000029,0x6867696c,0x70616d74,0x00007655,0x00040005,0x0000002a,0x31565576,
|
||||
0x00000000,0x00070006,0x0000000c,0x00000000,0x70696c63,0x6d6f7246,0x6c726f57,0x00000064,
|
||||
0x00070006,0x0000000d,0x00000000,0x6c726f77,0x6f724664,0x646f4d6d,0x00006c65,0x00050006,
|
||||
0x0000000d,0x00000001,0x6f6c6f63,0x00000072,0x00070006,0x0000000d,0x00000002,0x72547675,
|
||||
0x66736e61,0x556d726f,0x00000000,0x00070006,0x0000000d,0x00000003,0x72547675,0x66736e61,
|
||||
0x566d726f,0x00000000,0x00040047,0x00000020,0x0000001e,0x00000000,0x00040047,0x00000021,
|
||||
0x0000001e,0x00000001,0x00040047,0x00000022,0x0000001e,0x00000002,0x00040047,0x00000023,
|
||||
0x0000001e,0x00000003,0x00040047,0x00000029,0x0000001e,0x00000004,0x00040047,0x00000025,
|
||||
0x0000001e,0x00000000,0x00040047,0x00000026,0x0000001e,0x00000001,0x00040047,0x00000027,
|
||||
0x0000001e,0x00000002,0x00040047,0x0000002a,0x0000001e,0x00000003,0x00040047,0x00000028,
|
||||
0x0000000b,0x00000000,0x00040047,0x00000024,0x0000000b,0x00001158,0x00040047,0x0000000b,
|
||||
0x00000006,0x00000040,0x00050048,0x0000000c,0x00000000,0x00000023,0x00000000,0x00040048,
|
||||
0x0000000c,0x00000000,0x00000005,0x00050048,0x0000000c,0x00000000,0x00000007,0x00000010,
|
||||
0x00030047,0x0000000c,0x00000002,0x00040047,0x0000001e,0x00000022,0x00000000,0x00040047,
|
||||
0x0000001e,0x00000021,0x00000000,0x00050048,0x0000000d,0x00000000,0x00000023,0x00000000,
|
||||
0x00040048,0x0000000d,0x00000000,0x00000005,0x00050048,0x0000000d,0x00000000,0x00000007,
|
||||
0x00000010,0x00050048,0x0000000d,0x00000001,0x00000023,0x00000040,0x00050048,0x0000000d,
|
||||
0x00000002,0x00000023,0x00000050,0x00050048,0x0000000d,0x00000003,0x00000023,0x00000060,
|
||||
0x00030047,0x0000000d,0x00000002,0x00040047,0x0000004f,0x0000001e,0x00000005,0x00040047,
|
||||
0x00000050,0x0000001e,0x00000006,0x00040047,0x00000052,0x00000006,0x00000040,0x00050048,
|
||||
0x00000053,0x00000000,0x00000023,0x00000000,0x00040048,0x00000053,0x00000000,0x00000005,
|
||||
0x00050048,0x00000053,0x00000000,0x00000007,0x00000010,0x00030047,0x00000053,0x00000002,
|
||||
0x00040047,0x00000055,0x00000022,0x00000002,0x00040047,0x00000055,0x00000021,0x00000003,
|
||||
0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,0x00030016,0x00000004,0x00000020,
|
||||
0x00040015,0x00000005,0x00000020,0x00000001,0x00040015,0x00000006,0x00000020,0x00000000,
|
||||
0x00040017,0x00000007,0x00000004,0x00000002,0x00040017,0x00000008,0x00000004,0x00000003,
|
||||
0x00040017,0x00000009,0x00000004,0x00000004,0x00040018,0x0000000a,0x00000009,0x00000004,
|
||||
0x0004002b,0x00000006,0x0000001d,0x00000002,0x0004001c,0x0000000b,0x0000000a,0x0000001d,
|
||||
0x0003001e,0x0000000c,0x0000000b,0x0006001e,0x0000000d,0x0000000a,0x00000009,0x00000009,
|
||||
0x00000009,0x00020014,0x00000056,0x00040017,0x0000004d,0x00000006,0x00000004,0x0004002b,
|
||||
0x00000006,0x00000051,0x00000100,0x0004001c,0x00000052,0x0000000a,0x00000051,0x0003001e,
|
||||
0x00000053,0x00000052,0x00040020,0x00000054,0x00000002,0x00000053,0x00040020,0x0000004e,
|
||||
0x00000001,0x0000004d,0x00040020,0x0000000e,0x00000002,0x0000000c,0x00040020,0x0000000f,
|
||||
0x00000002,0x0000000a,0x00040020,0x00000010,0x00000009,0x0000000d,0x00040020,0x00000011,
|
||||
0x00000009,0x0000000a,0x00040020,0x00000012,0x00000009,0x00000009,0x00040020,0x00000013,
|
||||
0x00000001,0x00000008,0x00040020,0x00000014,0x00000001,0x00000007,0x00040020,0x00000015,
|
||||
0x00000001,0x00000009,0x00040020,0x00000016,0x00000001,0x00000005,0x00040020,0x00000017,
|
||||
0x00000003,0x00000007,0x00040020,0x00000018,0x00000003,0x00000009,0x00040020,0x00000019,
|
||||
0x00000003,0x00000008,0x0004002b,0x00000004,0x0000001a,0x3f800000,0x0004002b,0x00000005,
|
||||
0x0000001b,0x00000000,0x0004002b,0x00000005,0x0000001c,0x00000001,0x0004002b,0x00000005,
|
||||
0x0000003f,0x00000002,0x0004002b,0x00000005,0x00000040,0x00000003,0x0004002b,0x00000004,
|
||||
0x00000057,0x38d1b717,0x0004002b,0x00000004,0x00000058,0x00000000,0x0004003b,0x00000054,
|
||||
0x00000055,0x00000002,0x0004003b,0x0000004e,0x0000004f,0x00000001,0x0004003b,0x00000015,
|
||||
0x00000050,0x00000001,0x0004003b,0x0000000e,0x0000001e,0x00000002,0x0004003b,0x00000010,
|
||||
0x0000001f,0x00000009,0x0004003b,0x00000013,0x00000020,0x00000001,0x0004003b,0x00000014,
|
||||
0x00000021,0x00000001,0x0004003b,0x00000015,0x00000022,0x00000001,0x0004003b,0x00000013,
|
||||
0x00000023,0x00000001,0x0004003b,0x00000016,0x00000024,0x00000001,0x0004003b,0x00000017,
|
||||
0x00000025,0x00000003,0x0004003b,0x00000018,0x00000026,0x00000003,0x0004003b,0x00000019,
|
||||
0x00000027,0x00000003,0x0004003b,0x00000018,0x00000028,0x00000003,0x0004003b,0x00000014,
|
||||
0x00000029,0x00000001,0x0004003b,0x00000017,0x0000002a,0x00000003,0x00050036,0x00000002,
|
||||
0x00000001,0x00000000,0x00000003,0x000200f8,0x0000002c,0x0004003d,0x00000005,0x0000002d,
|
||||
0x00000024,0x00060041,0x0000000f,0x0000002e,0x0000001e,0x0000001b,0x0000002d,0x0004003d,
|
||||
0x0000000a,0x0000002f,0x0000002e,0x00050041,0x00000011,0x00000030,0x0000001f,0x0000001b,
|
||||
0x0004003d,0x0000000a,0x00000031,0x00000030,0x00050041,0x00000012,0x00000032,0x0000001f,
|
||||
0x0000001c,0x0004003d,0x00000009,0x00000033,0x00000032,0x0004003d,0x00000008,0x00000034,
|
||||
0x00000020,0x00050051,0x00000004,0x00000035,0x00000034,0x00000000,0x00050051,0x00000004,
|
||||
0x00000036,0x00000034,0x00000001,0x00050051,0x00000004,0x00000037,0x00000034,0x00000002,
|
||||
0x00070050,0x00000009,0x00000038,0x00000035,0x00000036,0x00000037,0x0000001a,0x0004003d,
|
||||
0x0000004d,0x00000059,0x0000004f,0x0004003d,0x00000009,0x0000005a,0x00000050,0x00050051,
|
||||
0x00000006,0x0000005b,0x00000059,0x00000000,0x00050051,0x00000004,0x0000005c,0x0000005a,
|
||||
0x00000000,0x00060041,0x0000000f,0x0000005d,0x00000055,0x0000001b,0x0000005b,0x0004003d,
|
||||
0x0000000a,0x0000005e,0x0000005d,0x00050091,0x00000009,0x0000005f,0x0000005e,0x00000038,
|
||||
0x0005008e,0x00000009,0x00000060,0x0000005f,0x0000005c,0x00050051,0x00000006,0x00000061,
|
||||
0x00000059,0x00000001,0x00050051,0x00000004,0x00000062,0x0000005a,0x00000001,0x00060041,
|
||||
0x0000000f,0x00000063,0x00000055,0x0000001b,0x00000061,0x0004003d,0x0000000a,0x00000064,
|
||||
0x00000063,0x00050091,0x00000009,0x00000065,0x00000064,0x00000038,0x0005008e,0x00000009,
|
||||
0x00000066,0x00000065,0x00000062,0x00050081,0x00000009,0x00000067,0x00000060,0x00000066,
|
||||
0x00050081,0x00000004,0x00000068,0x0000005c,0x00000062,0x00050051,0x00000006,0x00000069,
|
||||
0x00000059,0x00000002,0x00050051,0x00000004,0x0000006a,0x0000005a,0x00000002,0x00060041,
|
||||
0x0000000f,0x0000006b,0x00000055,0x0000001b,0x00000069,0x0004003d,0x0000000a,0x0000006c,
|
||||
0x0000006b,0x00050091,0x00000009,0x0000006d,0x0000006c,0x00000038,0x0005008e,0x00000009,
|
||||
0x0000006e,0x0000006d,0x0000006a,0x00050081,0x00000009,0x0000006f,0x00000067,0x0000006e,
|
||||
0x00050081,0x00000004,0x00000070,0x00000068,0x0000006a,0x00050051,0x00000006,0x00000071,
|
||||
0x00000059,0x00000003,0x00050051,0x00000004,0x00000072,0x0000005a,0x00000003,0x00060041,
|
||||
0x0000000f,0x00000073,0x00000055,0x0000001b,0x00000071,0x0004003d,0x0000000a,0x00000074,
|
||||
0x00000073,0x00050091,0x00000009,0x00000075,0x00000074,0x00000038,0x0005008e,0x00000009,
|
||||
0x00000076,0x00000075,0x00000072,0x00050081,0x00000009,0x00000077,0x0000006f,0x00000076,
|
||||
0x00050081,0x00000004,0x00000078,0x00000070,0x00000072,0x000500b8,0x00000056,0x00000079,
|
||||
0x00000078,0x00000057,0x000600a9,0x00000004,0x0000007a,0x00000079,0x0000001a,0x00000058,
|
||||
0x00050083,0x00000004,0x0000007b,0x0000001a,0x0000007a,0x00050081,0x00000004,0x0000007c,
|
||||
0x00000078,0x0000007a,0x00050088,0x00000004,0x0000007d,0x0000001a,0x0000007c,0x0005008e,
|
||||
0x00000009,0x0000007e,0x00000077,0x0000007d,0x0005008e,0x00000009,0x0000007f,0x0000007e,
|
||||
0x0000007b,0x0005008e,0x00000009,0x00000080,0x00000038,0x0000007a,0x00050081,0x00000009,
|
||||
0x00000081,0x0000007f,0x00000080,0x00050091,0x00000009,0x00000039,0x00000031,0x00000081,
|
||||
0x00050091,0x00000009,0x0000003a,0x0000002f,0x00000039,0x0003003e,0x00000028,0x0000003a,
|
||||
0x0004003d,0x00000007,0x0000003b,0x00000021,0x00050041,0x00000012,0x00000041,0x0000001f,
|
||||
0x0000003f,0x0004003d,0x00000009,0x00000042,0x00000041,0x00050041,0x00000012,0x00000043,
|
||||
0x0000001f,0x00000040,0x0004003d,0x00000009,0x00000044,0x00000043,0x0008004f,0x00000008,
|
||||
0x00000045,0x00000042,0x00000042,0x00000000,0x00000001,0x00000002,0x0008004f,0x00000008,
|
||||
0x00000046,0x00000044,0x00000044,0x00000000,0x00000001,0x00000002,0x00050051,0x00000004,
|
||||
0x00000047,0x0000003b,0x00000000,0x00050051,0x00000004,0x00000048,0x0000003b,0x00000001,
|
||||
0x00060050,0x00000008,0x00000049,0x00000047,0x00000048,0x0000001a,0x00050094,0x00000004,
|
||||
0x0000004a,0x00000045,0x00000049,0x00050094,0x00000004,0x0000004b,0x00000046,0x00000049,
|
||||
0x00050050,0x00000007,0x0000004c,0x0000004a,0x0000004b,0x0003003e,0x00000025,0x0000004c,
|
||||
0x0003003e,0x00000026,0x00000033,0x0004003d,0x00000008,0x0000003e,0x00000023,0x0003003e,
|
||||
0x00000027,0x0000003e,0x0004003d,0x00000007,0x0000002b,0x00000029,0x0003003e,0x0000002a,
|
||||
0x0000002b,0x000100fd,0x00010038,
|
||||
};
|
||||
@@ -0,0 +1,178 @@
|
||||
#pragma once
|
||||
// The world layer drawn over the home in the headset (the Debug page's "World layer"): the walkable area as Frame
|
||||
// Home walks it, the surfaces and colliders, spawns, seats, hotspots, wall spots and gravity zones, in the colours
|
||||
// Frame Studio uses for them. Built once per home as guide geometry (guide_geom.h) and appended to the guide each
|
||||
// frame. Lines are thin crossed ribbons, so they read from any side without being rebuilt as you move.
|
||||
#include "guide_geom.h"
|
||||
#include "walkgrid.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace debuggeom {
|
||||
|
||||
using guide::C;
|
||||
using guide::P;
|
||||
|
||||
// A segment as two ribbons at right angles (w wide): visible edge-on from anywhere.
|
||||
inline void line(std::vector<GuideVertex>& o, P a, P b, C c, float w = 0.012f) {
|
||||
const P d = guide::norm(b - a);
|
||||
P u = guide::norm(guide::cross(d, std::fabs(d.y) < 0.9f ? P{0, 1, 0} : P{1, 0, 0}));
|
||||
P v = guide::cross(d, u);
|
||||
u = u * (w * 0.5f); v = v * (w * 0.5f);
|
||||
guide::quad(o, a - u, c, b - u, c, b + u, c, a + u, c);
|
||||
guide::quad(o, a - v, c, b - v, c, b + v, c, a + v, c);
|
||||
}
|
||||
|
||||
inline P xf(const float m[16], P p) { // column-major frame
|
||||
return {m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12], m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13], m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14]};
|
||||
}
|
||||
|
||||
inline void box(std::vector<GuideVertex>& o, const float m[16], const float h[3], C c) {
|
||||
P k[8];
|
||||
for (int i = 0; i < 8; ++i) k[i] = xf(m, {(i & 1) ? h[0] : -h[0], (i & 2) ? h[1] : -h[1], (i & 4) ? h[2] : -h[2]});
|
||||
static const int e[12][2] = {{0, 1}, {2, 3}, {4, 5}, {6, 7}, {0, 2}, {1, 3}, {4, 6}, {5, 7}, {0, 4}, {1, 5}, {2, 6}, {3, 7}};
|
||||
for (const auto& ed : e) line(o, k[ed[0]], k[ed[1]], c);
|
||||
}
|
||||
|
||||
inline void circle(std::vector<GuideVertex>& o, P c, float r, C col, int segs = 32) {
|
||||
for (int i = 0; i < segs; ++i) {
|
||||
const float a0 = 6.2831853f * i / segs, a1 = 6.2831853f * (i + 1) / segs;
|
||||
line(o, c + P{std::cos(a0) * r, 0, std::sin(a0) * r}, c + P{std::cos(a1) * r, 0, std::sin(a1) * r}, col);
|
||||
}
|
||||
}
|
||||
|
||||
// A marker on the floor: a ring and an arrow the way it faces (yaw 0 faces -Z), and a post to find it from afar.
|
||||
inline void marker(std::vector<GuideVertex>& o, const WorldPoint& p, float r, float post, C c) {
|
||||
const P at{p.pos[0], p.pos[1] + 0.02f, p.pos[2]};
|
||||
circle(o, at, r, c);
|
||||
const P f{-std::sin(p.yaw), 0, -std::cos(p.yaw)};
|
||||
line(o, at, at + f * (r * 1.6f), c);
|
||||
if (post > 0) line(o, at, at + P{0, post, 0}, guide::alpha(c, 0.6f));
|
||||
}
|
||||
|
||||
// Colours as Frame Studio draws them (linear, straight alpha).
|
||||
constexpr C kWalk{0.15f, 0.85f, 0.35f, 0.32f}, kWalkEdge{0.25f, 1.0f, 0.45f, 0.9f}, kBridged{0.95f, 0.75f, 0.2f, 0.25f};
|
||||
constexpr C kSolid{0.95f, 0.95f, 0.95f, 0.55f}, kObject{1.0f, 0.55f, 0.25f, 0.7f}, kTrigger{0.6f, 0.6f, 1.0f, 0.45f}, kWalkable{0.3f, 1.0f, 0.5f, 0.7f};
|
||||
constexpr C kSpawn{0.3f, 0.75f, 1.0f, 0.95f}, kSeat{1.0f, 0.8f, 0.25f, 0.95f}, kHotspot{0.85f, 0.45f, 1.0f, 0.95f};
|
||||
constexpr C kSpot{0.4f, 0.9f, 1.0f, 0.8f}, kGravity{1.0f, 0.35f, 0.6f, 0.6f};
|
||||
|
||||
// Everything, in world space. Mesh surfaces are drawn as their boundary-free wire (every edge) only up to a budget:
|
||||
// a mesh collider can be a whole room's geometry.
|
||||
inline void build(const HomeWorld& w, const WalkGrid* g, std::vector<GuideVertex>& o) {
|
||||
o.clear();
|
||||
if (g) {
|
||||
// (6 cm up: the walkable surface can be the collision under a carpet, and level with the carpet it flickers.)
|
||||
// The floor you can stand on, cell by cell, each corner at the height the cells around it share (a ramp or a
|
||||
// raked floor is a slope, not steps), and runs along x of flat cells at one height as one quad (a flat floor is
|
||||
// a few hundred quads, not one per cell); its edge as the smooth line you walk to.
|
||||
const float cs = g->p.cell, lift = 0.06f;
|
||||
auto corner = [&](int cx, int cz, float y) { // the corner at the low x / low z side of cell (cx, cz)
|
||||
float sum = 0;
|
||||
int n = 0;
|
||||
for (int j = -1; j <= 0; ++j)
|
||||
for (int i = -1; i <= 0; ++i)
|
||||
if (g->inside(cx + i, cz + j))
|
||||
if (const WalkSpan* q = g->walkNear(cx + i, cz + j, y, g->p.step)) { sum += q->hi; ++n; }
|
||||
return n ? sum / n : y;
|
||||
};
|
||||
for (int z = 0; z < g->nz; ++z) {
|
||||
struct Run { int x0, x1; float h; bool bridged; };
|
||||
std::vector<Run> open, next;
|
||||
const float z0 = g->oz + z * cs, z1 = g->oz + (z + 1) * cs; // (one way for both neighbours, or a seam shows)
|
||||
auto flush = [&](const Run& r) {
|
||||
const float x0 = g->ox + r.x0 * cs, x1 = g->ox + (r.x1 + 1) * cs, y = r.h + lift;
|
||||
const C col = r.bridged ? kBridged : kWalk;
|
||||
guide::quad(o, {x0, y, z0}, col, {x1, y, z0}, col, {x1, y, z1}, col, {x0, y, z1}, col);
|
||||
};
|
||||
for (int x = 0; x < g->nx; ++x) {
|
||||
next.clear();
|
||||
for (const WalkSpan* sp = g->begin(x, z); sp != g->end(x, z); ++sp) {
|
||||
if (!(sp->flags & (kWalkOk | kWalkNear)) || !g->walkable(g->centerX(x), g->centerZ(z), sp->hi)) continue;
|
||||
const bool br = (sp->flags & kWalkBridged) != 0;
|
||||
const float h00 = corner(x, z, sp->hi), h10 = corner(x + 1, z, sp->hi), h11 = corner(x + 1, z + 1, sp->hi), h01 = corner(x, z + 1, sp->hi);
|
||||
const bool flat = std::fabs(h00 - h10) < 0.002f && std::fabs(h00 - h11) < 0.002f && std::fabs(h00 - h01) < 0.002f;
|
||||
if (!flat) { // a slope: this cell alone, its corners where they are
|
||||
const float x0 = g->ox + x * cs, x1 = g->ox + (x + 1) * cs;
|
||||
const C col = br ? kBridged : kWalk;
|
||||
guide::quad(o, {x0, h00 + lift, z0}, col, {x1, h10 + lift, z0}, col, {x1, h11 + lift, z1}, col, {x0, h01 + lift, z1}, col);
|
||||
continue;
|
||||
}
|
||||
bool joined = false;
|
||||
for (Run& r : open)
|
||||
if (r.x1 == x - 1 && std::fabs(r.h - h00) < 0.002f && r.bridged == br) { r.x1 = x; next.push_back(r); r.x1 = -2; joined = true; break; }
|
||||
if (!joined) next.push_back({x, x, h00, br});
|
||||
}
|
||||
for (const Run& r : open) if (r.x1 != -2) flush(r); // ended at x - 1
|
||||
open.swap(next);
|
||||
}
|
||||
for (const Run& r : open) flush(r);
|
||||
}
|
||||
g->outline([](const float*, const float*, const float*, bool) {},
|
||||
[&](const float* a, const float* b) { // lying flat: it's seen from above
|
||||
const P pa{a[0], a[1] + 0.07f, a[2]}, pb{b[0], b[1] + 0.07f, b[2]};
|
||||
const P u = guide::norm(guide::cross(pb - pa, P{0, 1, 0})) * 0.012f;
|
||||
guide::quad(o, pa - u, kWalkEdge, pb - u, kWalkEdge, pb + u, kWalkEdge, pa + u, kWalkEdge);
|
||||
});
|
||||
}
|
||||
size_t meshEdges = 0;
|
||||
for (const WorldCollider& c : w.colliders) {
|
||||
const C col = c.role == WorldCollider::Trigger ? kTrigger : c.role == WorldCollider::Object ? kObject : c.role == WorldCollider::Walkable ? kWalkable : kSolid;
|
||||
switch (c.kind) {
|
||||
case WorldCollider::Box: box(o, c.world, c.half, col); break;
|
||||
case WorldCollider::Sphere: {
|
||||
const P ctr = xf(c.world, {0, 0, 0});
|
||||
circle(o, ctr, c.radius, col);
|
||||
for (int i = 0; i < 24; ++i) { // and a vertical ring
|
||||
const float a0 = 6.2831853f * i / 24, a1 = 6.2831853f * (i + 1) / 24;
|
||||
line(o, ctr + P{std::cos(a0) * c.radius, std::sin(a0) * c.radius, 0}, ctr + P{std::cos(a1) * c.radius, std::sin(a1) * c.radius, 0}, col);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WorldCollider::Capsule: {
|
||||
const P a = xf(c.world, {0, -c.length * 0.5f, 0}), b = xf(c.world, {0, c.length * 0.5f, 0});
|
||||
circle(o, a, c.radius, col); circle(o, b, c.radius, col); line(o, a, b, col);
|
||||
break;
|
||||
}
|
||||
default: { // each edge once (neighbouring triangles share them), as one ribbon facing up
|
||||
const std::vector<float> wp = worldMeshPositions(c);
|
||||
std::unordered_set<uint64_t> seen;
|
||||
auto edge = [&](u32 i0, u32 i1) {
|
||||
if (i0 > i1) std::swap(i0, i1);
|
||||
if (meshEdges >= 25000 || static_cast<size_t>(i1) * 3 + 2 >= wp.size() || !seen.insert((static_cast<uint64_t>(i0) << 32) | i1).second) return;
|
||||
++meshEdges;
|
||||
const P a{wp[i0 * 3], wp[i0 * 3 + 1], wp[i0 * 3 + 2]}, b{wp[i1 * 3], wp[i1 * 3 + 1], wp[i1 * 3 + 2]};
|
||||
const P d = guide::norm(b - a);
|
||||
const P u = guide::norm(guide::cross(d, std::fabs(d.y) < 0.9f ? P{0, 1, 0} : P{1, 0, 0})) * 0.004f;
|
||||
const C faint = guide::alpha(col, 0.45f); // a whole room's triangles: quieter than the boxes
|
||||
guide::quad(o, a - u, faint, b - u, faint, b + u, faint, a + u, faint);
|
||||
};
|
||||
for (size_t t = 0; t + 2 < c.indices.size(); t += 3) {
|
||||
edge(c.indices[t], c.indices[t + 1]); edge(c.indices[t + 1], c.indices[t + 2]); edge(c.indices[t + 2], c.indices[t]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const WorldPoint& p : w.spawns) marker(o, p, 0.3f, 1.8f, kSpawn);
|
||||
for (const WorldPoint& p : w.hotspots) marker(o, p, 0.25f, 1.2f, kHotspot);
|
||||
for (const WorldPoint& p : w.seats) {
|
||||
marker(o, p, 0.18f, 0, kSeat);
|
||||
const P at{p.pos[0], p.pos[1], p.pos[2]};
|
||||
const P ex{p.pos[0] + p.exit[0], p.pos[1] + p.exit[1] + 0.02f, p.pos[2] + p.exit[2]};
|
||||
if (p.exit[0] != 0 || p.exit[2] != 0) { line(o, at, ex, guide::alpha(kSeat, 0.5f)); circle(o, ex, 0.12f, guide::alpha(kSeat, 0.7f)); }
|
||||
}
|
||||
for (const WorldWallSpot& s : w.wallSpots) {
|
||||
const float h[3] = {s.width * 0.5f, s.height * 0.5f, 0.01f};
|
||||
box(o, s.world, h, kSpot);
|
||||
}
|
||||
for (const WorldGravity& z : w.gravity) {
|
||||
if (z.kind == WorldGravity::Planet) circle(o, xf(z.world, {0, 0, 0}), z.radius, kGravity, 64);
|
||||
else box(o, z.world, z.half, kGravity);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace debuggeom
|
||||
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
// GENERATED by tools/debugview/gen_debug_spirv.sh from tools/debugview/debug.{vert,frag}. Do not edit by hand.
|
||||
// vert: in vec3 position @0; set0 b0 buffer { Draw { mat4 clipFromModel[2]; mat4 worldFromModel; vec4 color; } d[]; } by gl_InstanceIndex
|
||||
// frag: out = color, faceted shading when spec 0 = 1
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
static const uint32_t kDebugVertSpirv[] = {
|
||||
0x07230203,0x00010300,0x000d000b,0x0000003f,0x00000000,0x00020011,0x00000001,0x00020011,
|
||||
0x00001157,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,
|
||||
0x00000000,0x00000001,0x000b000f,0x00000000,0x00000004,0x6e69616d,0x00000000,0x0000000d,
|
||||
0x00000019,0x0000001b,0x00000022,0x0000002d,0x00000039,0x00030047,0x0000000b,0x00000002,
|
||||
0x00050048,0x0000000b,0x00000000,0x0000000b,0x00000000,0x00050048,0x0000000b,0x00000001,
|
||||
0x0000000b,0x00000001,0x00050048,0x0000000b,0x00000002,0x0000000b,0x00000003,0x00050048,
|
||||
0x0000000b,0x00000003,0x0000000b,0x00000004,0x00040047,0x00000012,0x00000006,0x00000040,
|
||||
0x00040048,0x00000013,0x00000000,0x00000005,0x00050048,0x00000013,0x00000000,0x00000007,
|
||||
0x00000010,0x00050048,0x00000013,0x00000000,0x00000023,0x00000000,0x00040048,0x00000013,
|
||||
0x00000001,0x00000005,0x00050048,0x00000013,0x00000001,0x00000007,0x00000010,0x00050048,
|
||||
0x00000013,0x00000001,0x00000023,0x00000080,0x00050048,0x00000013,0x00000002,0x00000023,
|
||||
0x000000c0,0x00040047,0x00000014,0x00000006,0x000000d0,0x00030047,0x00000015,0x00000002,
|
||||
0x00040048,0x00000015,0x00000000,0x00000018,0x00050048,0x00000015,0x00000000,0x00000023,
|
||||
0x00000000,0x00030047,0x00000017,0x00000018,0x00040047,0x00000017,0x00000021,0x00000000,
|
||||
0x00040047,0x00000017,0x00000022,0x00000000,0x00040047,0x00000019,0x0000000b,0x0000002b,
|
||||
0x00040047,0x0000001b,0x0000000b,0x00001158,0x00040047,0x00000022,0x0000001e,0x00000000,
|
||||
0x00040047,0x0000002d,0x0000001e,0x00000000,0x00030047,0x00000039,0x0000000e,0x00040047,
|
||||
0x00000039,0x0000001e,0x00000001,0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,
|
||||
0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,0x00000006,0x00000004,0x00040015,
|
||||
0x00000008,0x00000020,0x00000000,0x0004002b,0x00000008,0x00000009,0x00000001,0x0004001c,
|
||||
0x0000000a,0x00000006,0x00000009,0x0006001e,0x0000000b,0x00000007,0x00000006,0x0000000a,
|
||||
0x0000000a,0x00040020,0x0000000c,0x00000003,0x0000000b,0x0004003b,0x0000000c,0x0000000d,
|
||||
0x00000003,0x00040015,0x0000000e,0x00000020,0x00000001,0x0004002b,0x0000000e,0x0000000f,
|
||||
0x00000000,0x00040018,0x00000010,0x00000007,0x00000004,0x0004002b,0x00000008,0x00000011,
|
||||
0x00000002,0x0004001c,0x00000012,0x00000010,0x00000011,0x0005001e,0x00000013,0x00000012,
|
||||
0x00000010,0x00000007,0x0003001d,0x00000014,0x00000013,0x0003001e,0x00000015,0x00000014,
|
||||
0x00040020,0x00000016,0x0000000c,0x00000015,0x0004003b,0x00000016,0x00000017,0x0000000c,
|
||||
0x00040020,0x00000018,0x00000001,0x0000000e,0x0004003b,0x00000018,0x00000019,0x00000001,
|
||||
0x0004003b,0x00000018,0x0000001b,0x00000001,0x00040020,0x0000001d,0x0000000c,0x00000010,
|
||||
0x00040017,0x00000020,0x00000006,0x00000003,0x00040020,0x00000021,0x00000001,0x00000020,
|
||||
0x0004003b,0x00000021,0x00000022,0x00000001,0x0004002b,0x00000006,0x00000024,0x3f800000,
|
||||
0x00040020,0x0000002a,0x00000003,0x00000007,0x00040020,0x0000002c,0x00000003,0x00000020,
|
||||
0x0004003b,0x0000002c,0x0000002d,0x00000003,0x0004002b,0x0000000e,0x0000002f,0x00000001,
|
||||
0x0004003b,0x0000002a,0x00000039,0x00000003,0x0004002b,0x0000000e,0x0000003b,0x00000002,
|
||||
0x00040020,0x0000003c,0x0000000c,0x00000007,0x00050036,0x00000002,0x00000004,0x00000000,
|
||||
0x00000003,0x000200f8,0x00000005,0x0004003d,0x0000000e,0x0000001a,0x00000019,0x0004003d,
|
||||
0x0000000e,0x0000001c,0x0000001b,0x00080041,0x0000001d,0x0000001e,0x00000017,0x0000000f,
|
||||
0x0000001a,0x0000000f,0x0000001c,0x0004003d,0x00000010,0x0000001f,0x0000001e,0x0004003d,
|
||||
0x00000020,0x00000023,0x00000022,0x00050051,0x00000006,0x00000025,0x00000023,0x00000000,
|
||||
0x00050051,0x00000006,0x00000026,0x00000023,0x00000001,0x00050051,0x00000006,0x00000027,
|
||||
0x00000023,0x00000002,0x00070050,0x00000007,0x00000028,0x00000025,0x00000026,0x00000027,
|
||||
0x00000024,0x00050091,0x00000007,0x00000029,0x0000001f,0x00000028,0x00050041,0x0000002a,
|
||||
0x0000002b,0x0000000d,0x0000000f,0x0003003e,0x0000002b,0x00000029,0x00070041,0x0000001d,
|
||||
0x00000030,0x00000017,0x0000000f,0x0000001a,0x0000002f,0x0004003d,0x00000010,0x00000031,
|
||||
0x00000030,0x00050091,0x00000007,0x00000037,0x00000031,0x00000028,0x0008004f,0x00000020,
|
||||
0x00000038,0x00000037,0x00000037,0x00000000,0x00000001,0x00000002,0x0003003e,0x0000002d,
|
||||
0x00000038,0x00070041,0x0000003c,0x0000003d,0x00000017,0x0000000f,0x0000001a,0x0000003b,
|
||||
0x0004003d,0x00000007,0x0000003e,0x0000003d,0x0003003e,0x00000039,0x0000003e,0x000100fd,
|
||||
0x00010038,
|
||||
};
|
||||
static const size_t kDebugVertSize = sizeof(kDebugVertSpirv);
|
||||
|
||||
static const uint32_t kDebugFragSpirv[] = {
|
||||
0x07230203,0x00010300,0x000d000b,0x00000049,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
|
||||
0x0008000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x0000000c,0x00000018,0x0000003b,
|
||||
0x00030010,0x00000004,0x00000007,0x00030047,0x0000000c,0x0000000e,0x00040047,0x0000000c,
|
||||
0x0000001e,0x00000001,0x00040047,0x00000010,0x00000001,0x00000000,0x00040047,0x00000018,
|
||||
0x0000001e,0x00000000,0x00040047,0x0000003b,0x0000001e,0x00000000,0x00020013,0x00000002,
|
||||
0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,
|
||||
0x00000006,0x00000003,0x00040017,0x0000000a,0x00000006,0x00000004,0x00040020,0x0000000b,
|
||||
0x00000001,0x0000000a,0x0004003b,0x0000000b,0x0000000c,0x00000001,0x00040015,0x0000000f,
|
||||
0x00000020,0x00000001,0x00040032,0x0000000f,0x00000010,0x00000001,0x0004002b,0x0000000f,
|
||||
0x00000011,0x00000001,0x00020014,0x00000012,0x00060034,0x00000012,0x00000013,0x000000aa,
|
||||
0x00000010,0x00000011,0x00040020,0x00000017,0x00000001,0x00000007,0x0004003b,0x00000017,
|
||||
0x00000018,0x00000001,0x0004002b,0x00000006,0x00000023,0x1e3ce508,0x0004002b,0x00000006,
|
||||
0x0000002a,0x3eb2c0f1,0x0004002b,0x00000006,0x0000002b,0x3f590edb,0x0004002b,0x00000006,
|
||||
0x0000002c,0x3ecc4a38,0x0006002c,0x00000007,0x0000002d,0x0000002a,0x0000002b,0x0000002c,
|
||||
0x0004002b,0x00000006,0x00000031,0x3f800000,0x0004002b,0x00000006,0x00000033,0x3ed70a3d,
|
||||
0x0004002b,0x00000006,0x00000034,0x3f147ae1,0x00040020,0x0000003a,0x00000003,0x0000000a,
|
||||
0x0004003b,0x0000003a,0x0000003b,0x00000003,0x00040015,0x0000003d,0x00000020,0x00000000,
|
||||
0x0004002b,0x0000003d,0x0000003e,0x00000003,0x00040020,0x0000003f,0x00000001,0x00000006,
|
||||
0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003d,
|
||||
0x0000000a,0x0000000d,0x0000000c,0x0008004f,0x00000007,0x0000000e,0x0000000d,0x0000000d,
|
||||
0x00000000,0x00000001,0x00000002,0x000300f7,0x00000015,0x00000000,0x000400fa,0x00000013,
|
||||
0x00000014,0x00000015,0x000200f8,0x00000014,0x0004003d,0x00000007,0x00000019,0x00000018,
|
||||
0x000400cf,0x00000007,0x0000001a,0x00000019,0x000400d0,0x00000007,0x0000001c,0x00000019,
|
||||
0x0007000c,0x00000007,0x0000001d,0x00000001,0x00000044,0x0000001a,0x0000001c,0x00050094,
|
||||
0x00000006,0x00000022,0x0000001d,0x0000001d,0x000500ba,0x00000012,0x00000024,0x00000022,
|
||||
0x00000023,0x000300f7,0x00000027,0x00000000,0x000400fa,0x00000024,0x00000026,0x00000030,
|
||||
0x000200f8,0x00000026,0x0006000c,0x00000007,0x00000029,0x00000001,0x00000045,0x0000001d,
|
||||
0x00050094,0x00000006,0x0000002e,0x00000029,0x0000002d,0x0006000c,0x00000006,0x0000002f,
|
||||
0x00000001,0x00000004,0x0000002e,0x000200f9,0x00000027,0x000200f8,0x00000030,0x000200f9,
|
||||
0x00000027,0x000200f8,0x00000027,0x000700f5,0x00000006,0x00000046,0x0000002f,0x00000026,
|
||||
0x00000031,0x00000030,0x00050085,0x00000006,0x00000036,0x00000034,0x00000046,0x00050081,
|
||||
0x00000006,0x00000037,0x00000033,0x00000036,0x0005008e,0x00000007,0x00000039,0x0000000e,
|
||||
0x00000037,0x000200f9,0x00000015,0x000200f8,0x00000015,0x000700f5,0x00000007,0x00000048,
|
||||
0x0000000e,0x00000005,0x00000039,0x00000027,0x00050041,0x0000003f,0x00000040,0x0000000c,
|
||||
0x0000003e,0x0004003d,0x00000006,0x00000041,0x00000040,0x00050051,0x00000006,0x00000042,
|
||||
0x00000048,0x00000000,0x00050051,0x00000006,0x00000043,0x00000048,0x00000001,0x00050051,
|
||||
0x00000006,0x00000044,0x00000048,0x00000002,0x00070050,0x0000000a,0x00000045,0x00000042,
|
||||
0x00000043,0x00000044,0x00000041,0x0003003e,0x0000003b,0x00000045,0x000100fd,0x00010038,
|
||||
};
|
||||
static const size_t kDebugFragSize = sizeof(kDebugFragSpirv);
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#pragma once
|
||||
// Frame Home's side of the system environment (env_share.h): the shared state, the exported frame images and the
|
||||
// connection to the compositor layer. frame-home --system renders into slot(), then publish() once the GPU is done.
|
||||
#include "env_share.h"
|
||||
#include "hsl_renderer.h"
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class EnvExport {
|
||||
public:
|
||||
~EnvExport() { disconnect(); if (state_) ::munmap(state_, sizeof(envshare::State)); if (stateFd_ >= 0) ::close(stateFd_); }
|
||||
|
||||
bool init() {
|
||||
stateFd_ = memfd_create("framehome-env", MFD_CLOEXEC);
|
||||
if (stateFd_ < 0 || ftruncate(stateFd_, sizeof(envshare::State)) != 0) return false;
|
||||
void* p = ::mmap(nullptr, sizeof(envshare::State), PROT_READ | PROT_WRITE, MAP_SHARED, stateFd_, 0);
|
||||
if (p == MAP_FAILED) return false;
|
||||
state_ = new (p) envshare::State();
|
||||
state_->show = 0;
|
||||
return true;
|
||||
}
|
||||
envshare::State* state() { return state_; }
|
||||
// The images go before the renderer's device does (after VR_Shutdown; exit and restart alike).
|
||||
void release(HslRenderer& r) { disconnect(); r.destroyExportImages(images_); }
|
||||
bool connected() const { return sock_ >= 0 && sent_; }
|
||||
|
||||
// (Re)creates the images at the renderer's size; they go to the layer on the next connect().
|
||||
bool createImages(HslRenderer& r) {
|
||||
r.exportSlot = -1;
|
||||
if (!r.createExportImages(envshare::kSlots, images_, memoryType_, uuid_)) return false;
|
||||
usage_ = r.exportUsage();
|
||||
flags_ = r.exportCreateFlags();
|
||||
w_ = r.width();
|
||||
h_ = r.height();
|
||||
++generation_; // the layer keeps showing the last frame of the old images until it has these
|
||||
if (sent_) sent_ = false, lastTry_ = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Connects to the layer (once a second at most) and sends the images; false while there's no layer.
|
||||
bool connect() {
|
||||
if (connected()) return true;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - lastTry_ < std::chrono::seconds(1) || images_.empty()) return false;
|
||||
lastTry_ = now;
|
||||
if (sock_ < 0) {
|
||||
const char* rt = std::getenv("XDG_RUNTIME_DIR");
|
||||
const std::string path = std::string(rt && *rt ? rt : "/run/user/1000") + "/" + envshare::kSocketName;
|
||||
sock_ = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
sockaddr_un a = {};
|
||||
a.sun_family = AF_UNIX;
|
||||
std::snprintf(a.sun_path, sizeof a.sun_path, "%s", path.c_str());
|
||||
if (sock_ < 0 || ::connect(sock_, reinterpret_cast<sockaddr*>(&a), sizeof a) != 0) { disconnect(); return false; }
|
||||
}
|
||||
envshare::Hello h;
|
||||
h.width = w_;
|
||||
h.height = h_;
|
||||
h.layers = 2;
|
||||
h.format = 43; // VK_FORMAT_R8G8B8A8_SRGB
|
||||
h.memoryType = memoryType_;
|
||||
h.generation = generation_;
|
||||
h.usage = usage_;
|
||||
h.flags = flags_;
|
||||
std::memcpy(h.deviceUUID, uuid_, 16);
|
||||
int fds[envshare::kSlots + 1];
|
||||
for (uint32_t i = 0; i < envshare::kSlots; ++i) { h.memorySize[i] = images_[i].size; fds[i] = images_[i].fd; }
|
||||
fds[envshare::kSlots] = stateFd_;
|
||||
iovec iov = {&h, sizeof h};
|
||||
char ctrl[CMSG_SPACE(sizeof fds)] = {};
|
||||
msghdr m = {};
|
||||
m.msg_iov = &iov;
|
||||
m.msg_iovlen = 1;
|
||||
m.msg_control = ctrl;
|
||||
m.msg_controllen = sizeof ctrl;
|
||||
cmsghdr* cm = CMSG_FIRSTHDR(&m);
|
||||
cm->cmsg_level = SOL_SOCKET;
|
||||
cm->cmsg_type = SCM_RIGHTS;
|
||||
cm->cmsg_len = CMSG_LEN(sizeof fds);
|
||||
std::memcpy(CMSG_DATA(cm), fds, sizeof fds);
|
||||
if (::sendmsg(sock_, &m, MSG_NOSIGNAL) != static_cast<ssize_t>(sizeof h)) { disconnect(); return false; }
|
||||
sent_ = true;
|
||||
std::fprintf(stderr, "[frame-home] connected to the compositor: Frame Home is the environment (%ux%u per eye)\n", w_, h_);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is the layer still there? (it closes the socket when the compositor goes away)
|
||||
void poll() {
|
||||
if (sock_ < 0) return;
|
||||
char b;
|
||||
const ssize_t n = ::recv(sock_, &b, 1, MSG_DONTWAIT);
|
||||
if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
|
||||
std::fprintf(stderr, "[frame-home] the compositor went away: reconnecting\n");
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// The slot to render the next frame into.
|
||||
int beginFrame() { const int k = static_cast<int>(next_); next_ = (next_ + 1) % envshare::kSlots; return k; }
|
||||
|
||||
// The frame in slot k is done on the GPU: hand it to the layer with the eye poses it was rendered from.
|
||||
void publish(int k, const float viewProj[2][16], const float eyePos[2][3]) {
|
||||
envshare::Slot& s = state_->slot[k];
|
||||
s.frame = ++frame_;
|
||||
s.generation = generation_;
|
||||
std::memcpy(s.viewProj, viewProj, sizeof s.viewProj);
|
||||
for (int e = 0; e < 2; ++e) { for (int k = 0; k < 3; ++k) s.eyePos[e][k] = eyePos[e][k]; s.eyePos[e][3] = 1; }
|
||||
state_->published.store(frame_);
|
||||
state_->latest.store(static_cast<uint32_t>(k), std::memory_order_release);
|
||||
state_->publishNs.store(envshare::nowNs());
|
||||
}
|
||||
|
||||
// The layer couldn't import the images with these create flags (0: none refused).
|
||||
uint32_t refusedFlags() const { return state_->refusedFlags.load() & flags_; }
|
||||
|
||||
// Seconds since the layer last drew the home (large while a game runs, or before the layer is there).
|
||||
double sinceSampled() const {
|
||||
const uint64_t s = state_->sampledNs.load();
|
||||
return s ? (envshare::nowNs() - s) / 1e9 : 1e9;
|
||||
}
|
||||
|
||||
private:
|
||||
void disconnect() { if (sock_ >= 0) ::close(sock_); sock_ = -1; sent_ = false; }
|
||||
envshare::State* state_ = nullptr;
|
||||
int stateFd_ = -1, sock_ = -1;
|
||||
bool sent_ = false;
|
||||
std::vector<HslRenderer::ExportImage> images_;
|
||||
uint32_t memoryType_ = 0, w_ = 0, h_ = 0, next_ = 0, usage_ = 0, flags_ = 0;
|
||||
uint8_t uuid_[16] = {};
|
||||
uint64_t frame_ = 0;
|
||||
uint32_t generation_ = 0;
|
||||
std::chrono::steady_clock::time_point lastTry_;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
// Frame Home as the system environment: what Frame Home (frame-home --system) and the compositor layer
|
||||
// (layer/frame_home_layer.cpp, inside vrcompositor) share.
|
||||
//
|
||||
// Frame Home renders the home for both eyes into one of kSlots exportable 2-layer images (Vulkan external memory,
|
||||
// opaque fds) and publishes it here once the GPU has finished it. The layer imports the images on the compositor's
|
||||
// device and, where the compositor draws Aurora, draws a shader that looks each pixel's direction up in the newest
|
||||
// published frame (rotation reprojection from the pose that frame was rendered with). No frame, or a stale one,
|
||||
// and the layer leaves Aurora alone: Frame Home stopping or crashing never blanks the headset.
|
||||
//
|
||||
// Hand-over: Frame Home connects to the layer's unix socket (kSocketName in $XDG_RUNTIME_DIR) and sends one Hello
|
||||
// with SCM_RIGHTS fds: kSlots image memories, then the shared State (a memfd). A new Hello (new size) replaces the
|
||||
// old images.
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
namespace envshare {
|
||||
|
||||
constexpr const char* kSocketName = "framehome-env.sock";
|
||||
constexpr uint32_t kMagic = 0x46484556; // "FHEV"
|
||||
constexpr uint32_t kVersion = 5; // 4: Hello says the images' usage (Frame Home draws straight into them)
|
||||
// 5: ...and their create flags (subsampled: the compositor smooths foveation)
|
||||
// Four: the compositor reads the newest while Frame Home publishes the one before it finished and renders the next.
|
||||
constexpr uint32_t kSlots = 4;
|
||||
|
||||
struct Hello {
|
||||
uint32_t magic = kMagic, version = kVersion;
|
||||
uint32_t width = 0, height = 0, layers = 2;
|
||||
uint32_t format = 0; // VkFormat of the images (R8G8B8A8_SRGB)
|
||||
uint32_t memoryType = 0; // memory type index the images were allocated from (same GPU)
|
||||
uint32_t dedicated = 1; // allocated as dedicated allocations
|
||||
uint64_t memorySize[kSlots] = {};
|
||||
uint8_t deviceUUID[16] = {}; // the layer refuses images from another GPU
|
||||
uint32_t generation = 0; // which set of images (a new Hello after a resize is a new generation)
|
||||
uint32_t usage = 0; // v4: VkImageUsageFlags they were created with (the import must match)
|
||||
uint32_t flags = 0; // v5: VkImageCreateFlags they were created with (likewise). SUBSAMPLED: the
|
||||
// foveated pass's own output, read through a subsampled sampler
|
||||
};
|
||||
// Older Hellos: the same without the later fields. v3: no `usage` (images made for copying into: TRANSFER_DST |
|
||||
// SAMPLED); v4: no `flags` (none).
|
||||
constexpr size_t kHelloV3Size = offsetof(Hello, usage);
|
||||
constexpr size_t kHelloV4Size = offsetof(Hello, flags);
|
||||
|
||||
struct Slot {
|
||||
uint64_t frame = 0; // frame number published into this slot (0 = never)
|
||||
uint32_t generation = 0; // the image generation it was rendered into (see Hello)
|
||||
float viewProj[2][16] = {}; // per eye: projection * view rotation (column-major), tracking space
|
||||
float eyePos[2][4] = {}; // per eye: where it was, tracking space (xyz)
|
||||
};
|
||||
|
||||
struct State {
|
||||
uint32_t magic = kMagic, version = kVersion;
|
||||
std::atomic<uint32_t> latest{0xFFFFFFFFu}; // newest finished slot (0xFFFFFFFF none)
|
||||
std::atomic<uint64_t> published{0}; // frame number of `latest`
|
||||
std::atomic<uint64_t> publishNs{0}; // CLOCK_MONOTONIC when it was published
|
||||
std::atomic<uint64_t> sampledNs{0}; // the layer: when it last drew the home (Frame Home idles if stale)
|
||||
std::atomic<uint32_t> fade{1000}; // brightness, 0..1000 (switching homes fades through black)
|
||||
std::atomic<uint32_t> show{1}; // 0: Frame Home has nothing to show yet (the layer draws black)
|
||||
std::atomic<uint64_t> busyUntilNs{0}; // Frame Home is uploading a home until then: keep the last frame (black)
|
||||
Slot slot[kSlots];
|
||||
std::atomic<uint32_t> refusedFlags{0}; // the layer: Hello flags it can't import with (Frame Home then sends
|
||||
// images without them)
|
||||
};
|
||||
|
||||
inline uint64_t nowNs() {
|
||||
timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return static_cast<uint64_t>(ts.tv_sec) * 1000000000ull + static_cast<uint64_t>(ts.tv_nsec);
|
||||
}
|
||||
|
||||
} // namespace envshare
|
||||
+446
@@ -0,0 +1,446 @@
|
||||
#pragma once
|
||||
// .fenv — a Frame environment in one file: the home's APKs (stored, not recompressed: they are already zips),
|
||||
// Frame Studio's object edits, imported models (glTF / GLB / FBX / OBJ from Blender or Unity, with the texture
|
||||
// files they reference) and a thumbnail, described by fenv.json:
|
||||
//
|
||||
// { "format": "fenv", "version": 1, "name": "Haven + Calming",
|
||||
// "sources": [ { "file": "apks/haven2025.apk", "role": "Footprint", "format": "HSL",
|
||||
// "package": "com.meta.shell.env.footprint.haven2025", "version": "206.0.0.0.70" } ],
|
||||
// "models": [ { "file": "models/chair/chair.glb" } ],
|
||||
// "edits": "edits.json", "thumbnail": "thumb.png" }
|
||||
//
|
||||
// Frame Studio writes and opens it; Frame Home and the switcher on the Steam Frame load it directly. Opening
|
||||
// extracts the members once into a cache folder keyed by the file's path, size and time, then loads them from there.
|
||||
// A member named "<file>.lz" is an LZMA stream (fenv_lzma.h) and is unpacked to <file> — the baked scene is stored
|
||||
// so ("scene.bin.lz"): the zip's own deflate is far weaker on ASTC textures.
|
||||
#include "core/tinyjson.h"
|
||||
#include "world_settings.h"
|
||||
#ifdef _WIN32
|
||||
#include <process.h>
|
||||
#include <windows.h> // LockFileEx (the builds define NOMINMAX / WIN32_LEAN_AND_MEAN)
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include "fenv_lzma.h"
|
||||
#include "miniz.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
struct FenvSource {
|
||||
std::string file, role, format, package, version;
|
||||
};
|
||||
|
||||
struct FenvManifest {
|
||||
std::string name;
|
||||
std::vector<FenvSource> sources; // footprint first, then the vista (or one whole home)
|
||||
std::vector<std::string> models; // imported model files (member paths)
|
||||
std::string edits, thumbnail; // member paths, empty when absent
|
||||
std::string scene; // the baked scene (scene_bake.h): when present it IS the home, sources are info
|
||||
std::string world; // world.json (core/world.h): spawns, seats, walls, walkable areas
|
||||
WorldSettings settings; // the world's own render settings (world_settings.h): none set = the headset's
|
||||
};
|
||||
|
||||
namespace fenv_detail {
|
||||
|
||||
inline std::string quote(const std::string& s) {
|
||||
std::string o = "\"";
|
||||
for (char c : s) {
|
||||
if (c == '"' || c == '\\') { o += '\\'; o += c; }
|
||||
else if (static_cast<unsigned char>(c) < 0x20) { char b[8]; std::snprintf(b, sizeof b, "\\u%04x", c); o += b; }
|
||||
else o += c;
|
||||
}
|
||||
return o + "\"";
|
||||
}
|
||||
|
||||
inline std::string manifestJson(const FenvManifest& m) {
|
||||
std::string j = "{\n \"format\": \"fenv\",\n \"version\": 1,\n \"name\": " + quote(m.name) + ",\n \"sources\": [";
|
||||
for (size_t i = 0; i < m.sources.size(); ++i) {
|
||||
const FenvSource& s = m.sources[i];
|
||||
j += std::string(i ? "," : "") + "\n { \"file\": " + quote(s.file) + ", \"role\": " + quote(s.role) + ", \"format\": " +
|
||||
quote(s.format) + ", \"package\": " + quote(s.package) + ", \"version\": " + quote(s.version) + " }";
|
||||
}
|
||||
j += "\n ],\n \"models\": [";
|
||||
for (size_t i = 0; i < m.models.size(); ++i) j += std::string(i ? "," : "") + "\n { \"file\": " + quote(m.models[i]) + " }";
|
||||
j += "\n ]";
|
||||
if (!m.edits.empty()) j += ",\n \"edits\": " + quote(m.edits);
|
||||
if (!m.thumbnail.empty()) j += ",\n \"thumbnail\": " + quote(m.thumbnail);
|
||||
if (!m.scene.empty()) j += ",\n \"scene\": " + quote(m.scene);
|
||||
if (!m.world.empty()) j += ",\n \"world\": " + quote(m.world);
|
||||
if (!m.settings.empty() || !m.settings.tuned.empty()) j += ",\n \"settings\": " + m.settings.toJson();
|
||||
return j + "\n}\n";
|
||||
}
|
||||
|
||||
// Everything one .fenv may unpack to (its members, LZMA ones as unpacked): a damaged or hostile file must not
|
||||
// fill the disk or memory. The largest homes are a few hundred MB.
|
||||
constexpr uint64_t kExtractBudget = uint64_t(4) << 30;
|
||||
|
||||
// Empty when missing or larger than `limit` (the zip's declared size, which miniz holds the inflate to).
|
||||
inline std::vector<uint8_t> readMember(mz_zip_archive& z, const char* name, uint64_t limit = uint64_t(64) << 20) {
|
||||
std::vector<uint8_t> out;
|
||||
const int i = mz_zip_reader_locate_file(&z, name, nullptr, 0);
|
||||
mz_zip_archive_file_stat st;
|
||||
if (i < 0 || !mz_zip_reader_file_stat(&z, static_cast<mz_uint>(i), &st) || st.m_uncomp_size > limit || st.m_comp_size > limit) return out;
|
||||
size_t n = 0;
|
||||
void* p = mz_zip_reader_extract_to_heap(&z, static_cast<mz_uint>(i), &n, 0);
|
||||
if (!p) return out;
|
||||
try { out.assign(static_cast<uint8_t*>(p), static_cast<uint8_t*>(p) + n); } catch (const std::exception&) { out = {}; }
|
||||
mz_free(p);
|
||||
return out;
|
||||
}
|
||||
|
||||
// A short tag for a file's full path: two "haven.fenv" in different folders get different cache folders.
|
||||
inline std::string pathTag(const std::filesystem::path& p) {
|
||||
std::error_code ec;
|
||||
std::filesystem::path full = std::filesystem::weakly_canonical(p, ec);
|
||||
if (ec || full.empty()) full = std::filesystem::absolute(p, ec).lexically_normal();
|
||||
std::string s = full.u8string();
|
||||
#ifdef _WIN32
|
||||
for (char& c : s) { if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a'); else if (c == '/') c = '\\'; } // one file, however spelled
|
||||
#endif
|
||||
uint64_t h = 1469598103934665603ull; // FNV-1a
|
||||
for (unsigned char c : s) h = (h ^ c) * 1099511628211ull;
|
||||
char b[16];
|
||||
std::snprintf(b, sizeof b, "%08x", static_cast<unsigned>(h ^ (h >> 32)));
|
||||
return b;
|
||||
}
|
||||
|
||||
// One unpacker per cache key at a time, across processes (Frame Home and the switcher may open the same
|
||||
// environment together; on Windows, two Studio windows).
|
||||
struct ExtractLock {
|
||||
#ifdef _WIN32
|
||||
HANDLE h = INVALID_HANDLE_VALUE;
|
||||
OVERLAPPED ov = {};
|
||||
explicit ExtractLock(const std::filesystem::path& p) {
|
||||
h = CreateFileW(p.wstring().c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
|
||||
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (h != INVALID_HANDLE_VALUE && !LockFileEx(h, LOCKFILE_EXCLUSIVE_LOCK, 0, MAXDWORD, MAXDWORD, &ov)) { CloseHandle(h); h = INVALID_HANDLE_VALUE; }
|
||||
}
|
||||
~ExtractLock() { if (h != INVALID_HANDLE_VALUE) { UnlockFileEx(h, 0, MAXDWORD, MAXDWORD, &ov); CloseHandle(h); } }
|
||||
#else
|
||||
int fd;
|
||||
explicit ExtractLock(const std::filesystem::path& p) : fd(open(p.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600)) { if (fd >= 0) flock(fd, LOCK_EX); }
|
||||
~ExtractLock() { if (fd >= 0) { flock(fd, LOCK_UN); close(fd); } }
|
||||
#endif
|
||||
ExtractLock(const ExtractLock&) = delete;
|
||||
ExtractLock& operator=(const ExtractLock&) = delete;
|
||||
};
|
||||
|
||||
inline long processId() {
|
||||
#ifdef _WIN32
|
||||
return static_cast<long>(_getpid());
|
||||
#else
|
||||
return static_cast<long>(getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
// A member path is safe to extract: relative, no "..", no drive letters.
|
||||
inline bool safeMember(const std::string& m) {
|
||||
if (m.empty() || m[0] == '/' || m[0] == '\\' || m.find(':') != std::string::npos) return false;
|
||||
for (size_t a = 0; a <= m.size();) {
|
||||
size_t b = m.find_first_of("/\\", a);
|
||||
if (b == std::string::npos) b = m.size();
|
||||
if (m.compare(a, b - a, "..") == 0 && b - a == 2) return false;
|
||||
a = b + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace fenv_detail
|
||||
|
||||
// Where opened .fenv files are unpacked: FRAME_FENV_CACHE, else %LOCALAPPDATA%\FrameStudio\cache (Windows) or
|
||||
// $XDG_CACHE_HOME / ~/.cache /frame-home/fenv.
|
||||
inline std::string fenvCacheDir() {
|
||||
if (const char* e = std::getenv("FRAME_FENV_CACHE")) return e;
|
||||
#ifdef _WIN32
|
||||
if (const char* l = std::getenv("LOCALAPPDATA")) return std::string(l) + "\\FrameStudio\\cache";
|
||||
#endif
|
||||
if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::string(x) + "/frame-home/fenv";
|
||||
const char* h = std::getenv("HOME");
|
||||
return std::string(h ? h : "/tmp") + "/.cache/frame-home/fenv";
|
||||
}
|
||||
|
||||
inline bool isFenvPath(const std::string& p) {
|
||||
return p.size() > 5 && (p.compare(p.size() - 5, 5, ".fenv") == 0 || p.compare(p.size() - 5, 5, ".FENV") == 0);
|
||||
}
|
||||
|
||||
inline bool fenvParseManifest(const std::string& json, FenvManifest& m) {
|
||||
try {
|
||||
const tinyjson::Value doc = tinyjson::parse(json);
|
||||
if (!doc.isObject() || doc["format"].asString() != "fenv") return false;
|
||||
m = FenvManifest{};
|
||||
m.name = doc["name"].asString();
|
||||
for (size_t i = 0; i < doc["sources"].size(); ++i) {
|
||||
const auto& s = doc["sources"][i];
|
||||
FenvSource src;
|
||||
src.file = s["file"].asString(); src.role = s["role"].asString(); src.format = s["format"].asString();
|
||||
src.package = s["package"].asString(); src.version = s["version"].asString();
|
||||
if (fenv_detail::safeMember(src.file)) m.sources.push_back(src);
|
||||
}
|
||||
for (size_t i = 0; i < doc["models"].size(); ++i) {
|
||||
const std::string f = doc["models"][i]["file"].asString();
|
||||
if (fenv_detail::safeMember(f)) m.models.push_back(f);
|
||||
}
|
||||
if (doc.has("edits") && fenv_detail::safeMember(doc["edits"].asString())) m.edits = doc["edits"].asString();
|
||||
if (doc.has("thumbnail") && fenv_detail::safeMember(doc["thumbnail"].asString())) m.thumbnail = doc["thumbnail"].asString();
|
||||
if (doc.has("scene") && fenv_detail::safeMember(doc["scene"].asString())) m.scene = doc["scene"].asString();
|
||||
if (doc.has("world") && fenv_detail::safeMember(doc["world"].asString())) m.world = doc["world"].asString();
|
||||
if (doc.has("settings")) m.settings.fromValue(doc["settings"]);
|
||||
return !m.sources.empty() || !m.models.empty() || !m.scene.empty();
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reads fenv.json (and, if asked, the thumbnail bytes) without extracting anything else.
|
||||
inline bool fenvReadManifest(const std::string& path, FenvManifest& m, std::vector<uint8_t>* thumb = nullptr) {
|
||||
mz_zip_archive z = {};
|
||||
if (!mz_zip_reader_init_file(&z, path.c_str(), 0)) return false;
|
||||
const std::vector<uint8_t> j = fenv_detail::readMember(z, "fenv.json");
|
||||
bool ok = !j.empty() && fenvParseManifest(std::string(j.begin(), j.end()), m);
|
||||
if (ok && thumb && !m.thumbnail.empty()) *thumb = fenv_detail::readMember(z, m.thumbnail.c_str());
|
||||
mz_zip_reader_end(&z);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Extracts every member into <cacheRoot>/<name>-<path tag>-<size>-<time>/ (once) and returns the sources' and
|
||||
// models' paths there, plus the edits file ("" when the environment has none).
|
||||
inline bool fenvExtract(const std::string& path, const std::string& cacheRoot, FenvManifest& m,
|
||||
std::vector<std::string>& sources, std::vector<std::string>& models, std::string& editsPath,
|
||||
std::string& error) {
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
const fs::path src = fs::u8path(path);
|
||||
const auto size = fs::file_size(src, ec);
|
||||
// (a 128-bit count on some standard libraries: no to_string for it, and 64 bits hold any file's date)
|
||||
const long long time = static_cast<long long>(fs::last_write_time(src, ec).time_since_epoch().count());
|
||||
if (!fenvReadManifest(path, m)) { error = "not a Frame environment (.fenv): " + path; return false; }
|
||||
const std::string stem = src.stem().u8string(), key = stem + "-" + fenv_detail::pathTag(src);
|
||||
const fs::path root = fs::u8path(cacheRoot);
|
||||
const fs::path dir = root / (key + "-" + std::to_string(size) + "-" + std::to_string(time));
|
||||
const fs::path done = dir / ".complete";
|
||||
fs::create_directories(root, ec);
|
||||
const fenv_detail::ExtractLock lock(root / (key + ".lock"));
|
||||
if (!fs::exists(done, ec)) {
|
||||
// Earlier versions of this environment (same file, other size / time) are stale now, as are folders of the
|
||||
// naming before the path tag ("<name>-<size>-<time>") and unpacks that never finished (".tmp-<pid>": the
|
||||
// lock is ours, so nobody is still writing one).
|
||||
const auto digits = [](std::string t) { // the time part can be negative
|
||||
if (!t.empty() && t[0] == '-') t.erase(0, 1);
|
||||
return !t.empty() && t.find_first_not_of("0123456789") == std::string::npos;
|
||||
};
|
||||
const auto sizeTime = [&](const std::string& rest) { // exactly "<size>-<time>"
|
||||
const size_t dash = rest.find('-');
|
||||
return dash != std::string::npos && digits(rest.substr(0, dash)) && digits(rest.substr(dash + 1));
|
||||
};
|
||||
for (const auto& e : fs::directory_iterator(root, ec)) {
|
||||
if (!e.is_directory(ec) || e.path() == dir) continue;
|
||||
const std::string f = e.path().filename().u8string();
|
||||
// "haven-1a2b3c4d-123-456" is this haven's, "haven-calming-..." and another folder's haven are not
|
||||
bool ours = f.rfind(key + "-", 0) == 0;
|
||||
if (ours) {
|
||||
std::string rest = f.substr(key.size() + 1);
|
||||
const size_t t = rest.find(".tmp-");
|
||||
if (t != std::string::npos && digits(rest.substr(t + 5))) rest.erase(t);
|
||||
ours = sizeTime(rest);
|
||||
}
|
||||
const bool legacy = f.rfind(stem + "-", 0) == 0 && sizeTime(f.substr(stem.size() + 1));
|
||||
if (ours || legacy) fs::remove_all(e.path(), ec);
|
||||
}
|
||||
// Unpacked beside the cache folder and renamed into place once whole: a crash or a full disk midway
|
||||
// leaves no folder that looks finished.
|
||||
const fs::path tmp = fs::u8path(dir.u8string() + ".tmp-" + std::to_string(fenv_detail::processId()));
|
||||
fs::remove_all(tmp, ec);
|
||||
fs::remove_all(dir, ec);
|
||||
fs::create_directories(tmp, ec);
|
||||
mz_zip_archive z = {};
|
||||
if (!mz_zip_reader_init_file(&z, path.c_str(), 0)) { error = "can't open " + path; fs::remove_all(tmp, ec); return false; }
|
||||
const mz_uint count = mz_zip_reader_get_num_files(&z);
|
||||
uint64_t claimed = 0; // what the zip says it holds, before anything is written
|
||||
for (mz_uint i = 0; i < count; ++i) {
|
||||
mz_zip_archive_file_stat st;
|
||||
if (mz_zip_reader_file_stat(&z, i, &st) && !st.m_is_directory) claimed += std::max(st.m_comp_size, st.m_uncomp_size);
|
||||
}
|
||||
bool ok = claimed <= fenv_detail::kExtractBudget;
|
||||
if (!ok) error = path + " claims " + std::to_string(claimed >> 20) + " MB unpacked: damaged";
|
||||
uint64_t written = 0;
|
||||
for (mz_uint i = 0; i < count && ok; ++i) {
|
||||
mz_zip_archive_file_stat st;
|
||||
if (!mz_zip_reader_file_stat(&z, i, &st) || st.m_is_directory) continue;
|
||||
const std::string name = st.m_filename;
|
||||
if (!fenv_detail::safeMember(name)) continue;
|
||||
const fs::path out = tmp / fs::u8path(name);
|
||||
fs::create_directories(out.parent_path(), ec);
|
||||
if (name.size() > 3 && name.compare(name.size() - 3, 3, ".lz") == 0) {
|
||||
std::string why;
|
||||
try {
|
||||
std::vector<uint8_t> packed = fenv_detail::readMember(z, name.c_str(), fenv_detail::kExtractBudget - written), raw;
|
||||
ok = !packed.empty() && fenvLzmaDecompress(packed.data(), packed.size(), raw, why);
|
||||
packed = std::vector<uint8_t>();
|
||||
if (ok && raw.size() > fenv_detail::kExtractBudget - written) { ok = false; why = "larger than any environment"; }
|
||||
if (ok) {
|
||||
written += raw.size();
|
||||
const fs::path plain = tmp / fs::u8path(name.substr(0, name.size() - 3));
|
||||
std::ofstream f(plain, std::ios::binary);
|
||||
f.write(reinterpret_cast<const char*>(raw.data()), static_cast<std::streamsize>(raw.size()));
|
||||
ok = static_cast<bool>(f);
|
||||
}
|
||||
} catch (const std::exception&) { // bad_alloc: the scene doesn't fit in this memory
|
||||
ok = false;
|
||||
why = "not enough memory";
|
||||
}
|
||||
if (!ok) error = "couldn't unpack " + name + " from " + path + (why.empty() ? "" : ": " + why);
|
||||
continue;
|
||||
}
|
||||
written += std::max(st.m_comp_size, st.m_uncomp_size);
|
||||
ok = written <= fenv_detail::kExtractBudget && mz_zip_reader_extract_to_file(&z, i, out.u8string().c_str(), 0);
|
||||
if (!ok) error = "couldn't extract " + name + " from " + path;
|
||||
}
|
||||
mz_zip_reader_end(&z);
|
||||
if (ok) {
|
||||
std::ofstream f(tmp / ".complete");
|
||||
f << "ok\n";
|
||||
f.close(); // (closed before the rename: Windows won't move a folder with open files)
|
||||
ok = static_cast<bool>(f);
|
||||
if (!ok) error = "couldn't write the cache for " + path;
|
||||
}
|
||||
if (ok) {
|
||||
fs::rename(tmp, dir, ec);
|
||||
ok = !ec;
|
||||
if (!ok) error = "couldn't finish the cache for " + path + ": " + ec.message();
|
||||
}
|
||||
if (!ok) { fs::remove_all(tmp, ec); return false; }
|
||||
}
|
||||
sources.clear();
|
||||
models.clear();
|
||||
for (const FenvSource& s : m.sources)
|
||||
if (fs::exists(dir / fs::u8path(s.file), ec)) sources.push_back((dir / fs::u8path(s.file)).u8string());
|
||||
for (const std::string& f : m.models) models.push_back((dir / fs::u8path(f)).u8string());
|
||||
editsPath = m.edits.empty() ? std::string() : (dir / fs::u8path(m.edits)).u8string();
|
||||
if (!m.scene.empty()) m.scene = (dir / fs::u8path(m.scene)).u8string(); // now the extracted file
|
||||
if (!m.world.empty()) m.world = (dir / fs::u8path(m.world)).u8string();
|
||||
return true;
|
||||
}
|
||||
|
||||
// One member to write: its path inside the .fenv and either a file on disk or bytes.
|
||||
struct FenvMember {
|
||||
std::string name, file;
|
||||
std::vector<uint8_t> bytes;
|
||||
bool store = false; // already compressed (APKs, PNG/JPG, GLB): stored as-is
|
||||
bool fast = false; // large: deflated at the fastest level (inflates as fast; saving is 2x quicker)
|
||||
bool lzma = false; // packed with LZMA as "<name>.lz" (the baked scene: much smaller than deflate)
|
||||
FenvPacking packing = FenvPacking::Smallest;
|
||||
};
|
||||
|
||||
// progress(done, total): LZMA members, as they are packed (from the packing threads).
|
||||
inline bool fenvWrite(const std::string& outPath, const FenvManifest& m, const std::vector<FenvMember>& members, std::string& error,
|
||||
const std::function<void(size_t, size_t)>& progress = {}) {
|
||||
namespace fs = std::filesystem;
|
||||
const std::string tmp = outPath + ".part";
|
||||
mz_zip_archive z = {};
|
||||
if (!mz_zip_writer_init_file_v2(&z, tmp.c_str(), 0, 0)) { error = "can't write " + outPath; return false; }
|
||||
const std::string json = fenv_detail::manifestJson(m);
|
||||
bool ok = mz_zip_writer_add_mem(&z, "fenv.json", json.data(), json.size(), MZ_DEFAULT_COMPRESSION);
|
||||
for (const FenvMember& f : members) {
|
||||
if (!ok) break;
|
||||
const mz_uint level = f.store ? MZ_NO_COMPRESSION : f.fast ? MZ_BEST_SPEED : MZ_DEFAULT_COMPRESSION;
|
||||
if (f.lzma) {
|
||||
std::vector<uint8_t> packed;
|
||||
std::string why;
|
||||
ok = fenvLzmaCompress(f.bytes.data(), f.bytes.size(), packed, why, f.packing, progress) &&
|
||||
mz_zip_writer_add_mem(&z, (f.name + ".lz").c_str(), packed.data(), packed.size(), MZ_NO_COMPRESSION);
|
||||
if (!ok) error = "couldn't pack " + f.name + (why.empty() ? "" : ": " + why);
|
||||
continue;
|
||||
}
|
||||
ok = f.file.empty() ? mz_zip_writer_add_mem(&z, f.name.c_str(), f.bytes.data(), f.bytes.size(), level)
|
||||
: mz_zip_writer_add_file(&z, f.name.c_str(), f.file.c_str(), nullptr, 0, level);
|
||||
if (!ok) error = "couldn't add " + (f.file.empty() ? f.name : f.file);
|
||||
}
|
||||
ok = ok && mz_zip_writer_finalize_archive(&z);
|
||||
mz_zip_writer_end(&z);
|
||||
std::error_code ec;
|
||||
if (!ok) { fs::remove(fs::u8path(tmp), ec); if (error.empty()) error = "couldn't finish " + outPath; return false; }
|
||||
fs::rename(fs::u8path(tmp), fs::u8path(outPath), ec);
|
||||
if (ec) { fs::remove(fs::u8path(outPath), ec); fs::rename(fs::u8path(tmp), fs::u8path(outPath), ec); }
|
||||
if (ec) { error = "couldn't replace " + outPath + ": " + ec.message(); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
// A new manifest for an existing .fenv, every other member copied over as it is (nothing re-packed): how a world's
|
||||
// settings change without saving the whole world again.
|
||||
inline bool fenvRewriteManifest(const std::string& path, const FenvManifest& m, std::string& error) {
|
||||
namespace fs = std::filesystem;
|
||||
mz_zip_archive in = {}, out = {};
|
||||
if (!mz_zip_reader_init_file(&in, path.c_str(), 0)) { error = "can't read " + path; return false; }
|
||||
const std::string tmp = path + ".part";
|
||||
if (!mz_zip_writer_init_file_v2(&out, tmp.c_str(), 0, 0)) { mz_zip_reader_end(&in); error = "can't write " + tmp; return false; }
|
||||
const std::string json = fenv_detail::manifestJson(m);
|
||||
bool ok = mz_zip_writer_add_mem(&out, "fenv.json", json.data(), json.size(), MZ_DEFAULT_COMPRESSION);
|
||||
const mz_uint n = mz_zip_reader_get_num_files(&in);
|
||||
for (mz_uint i = 0; ok && i < n; ++i) {
|
||||
char name[512];
|
||||
mz_zip_reader_get_filename(&in, i, name, sizeof name);
|
||||
if (std::strcmp(name, "fenv.json") == 0) continue;
|
||||
ok = mz_zip_writer_add_from_zip_reader(&out, &in, i);
|
||||
if (!ok) error = std::string("couldn't copy ") + name;
|
||||
}
|
||||
ok = ok && mz_zip_writer_finalize_archive(&out);
|
||||
mz_zip_writer_end(&out);
|
||||
mz_zip_reader_end(&in);
|
||||
std::error_code ec;
|
||||
if (!ok) { fs::remove(fs::u8path(tmp), ec); if (error.empty()) error = "couldn't finish " + tmp; return false; }
|
||||
fs::rename(fs::u8path(tmp), fs::u8path(path), ec);
|
||||
if (ec) { fs::remove(fs::u8path(tmp), ec); error = "couldn't replace " + path + ": " + ec.message(); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
// A home's own settings: a .fenv's manifest, else "<apk>.settings.json" beside it (none: all unset).
|
||||
inline WorldSettings worldSettingsFor(const std::string& homePath) {
|
||||
WorldSettings w;
|
||||
if (isFenvPath(homePath)) {
|
||||
FenvManifest m;
|
||||
if (fenvReadManifest(homePath, m)) w = m.settings;
|
||||
return w;
|
||||
}
|
||||
std::FILE* f = std::fopen(worldSettingsPathFor(homePath).c_str(), "rb");
|
||||
if (!f) return w;
|
||||
std::string j;
|
||||
char b[4096];
|
||||
for (size_t k; (k = std::fread(b, 1, sizeof b, f)) > 0;) j.append(b, k);
|
||||
std::fclose(f);
|
||||
if (!w.fromJson(j)) w = WorldSettings();
|
||||
return w;
|
||||
}
|
||||
|
||||
// Stores them: into the .fenv's manifest, or the APK's settings file (removed when none are set).
|
||||
inline bool setWorldSettings(const std::string& homePath, const WorldSettings& w, std::string& error) {
|
||||
if (isFenvPath(homePath)) {
|
||||
FenvManifest m;
|
||||
if (!fenvReadManifest(homePath, m)) { error = "not a Frame environment (.fenv): " + homePath; return false; }
|
||||
m.settings = w;
|
||||
return fenvRewriteManifest(homePath, m, error);
|
||||
}
|
||||
const std::string p = worldSettingsPathFor(homePath);
|
||||
std::error_code ec;
|
||||
if (w.empty() && w.tuned.empty()) { std::filesystem::remove(std::filesystem::u8path(p), ec); return true; }
|
||||
const std::string j = w.toJson() + "\n", tmp = p + ".part";
|
||||
std::FILE* f = std::fopen(tmp.c_str(), "wb");
|
||||
if (!f || std::fwrite(j.data(), 1, j.size(), f) != j.size()) { if (f) std::fclose(f); error = "can't write " + p; return false; }
|
||||
std::fclose(f);
|
||||
std::filesystem::rename(std::filesystem::u8path(tmp), std::filesystem::u8path(p), ec);
|
||||
if (ec) { error = "can't write " + p + ": " + ec.message(); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "fenv_lzma.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
extern "C" {
|
||||
#include "LzmaDec.h"
|
||||
#include "LzmaEnc.h"
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void* lzAlloc(ISzAllocPtr, size_t size) { return ::operator new(size, std::nothrow); }
|
||||
void lzFree(ISzAllocPtr, void* p) { ::operator delete(p); }
|
||||
const ISzAlloc kAlloc = {lzAlloc, lzFree};
|
||||
|
||||
struct Progress {
|
||||
ICompressProgress vt;
|
||||
std::atomic<size_t>* done;
|
||||
size_t last = 0, total;
|
||||
const std::function<void(size_t, size_t)>* cb;
|
||||
};
|
||||
SRes onProgress(ICompressProgressPtr p, UInt64 in, UInt64) {
|
||||
Progress* pr = reinterpret_cast<Progress*>(const_cast<ICompressProgress*>(p)); // vt is the first member
|
||||
const size_t now = static_cast<size_t>(in);
|
||||
if (now > pr->last) { *pr->done += now - pr->last; pr->last = now; }
|
||||
if (*pr->cb) (*pr->cb)(pr->done->load(), pr->total);
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
struct Chunk { size_t at = 0, size = 0; std::vector<uint8_t> packed; uint8_t props[5] = {}; SRes res = SZ_OK; };
|
||||
|
||||
void put64(std::vector<uint8_t>& o, uint64_t v) { for (int i = 0; i < 8; ++i) o.push_back(static_cast<uint8_t>(v >> (8 * i))); }
|
||||
uint64_t get64(const uint8_t* p) { uint64_t v = 0; for (int i = 7; i >= 0; --i) v = v << 8 | p[i]; return v; }
|
||||
|
||||
// Runs fn(i) for i in [0, n) on up to `threads` threads.
|
||||
template <typename F> void parallel(size_t n, unsigned threads, F&& fn) {
|
||||
std::atomic<size_t> next{0};
|
||||
std::vector<std::thread> pool;
|
||||
for (unsigned t = 0; t < std::min<size_t>(threads, n); ++t)
|
||||
pool.emplace_back([&] { for (size_t i; (i = next++) < n;) fn(i); });
|
||||
for (auto& th : pool) th.join();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool fenvLzmaIsStream(const uint8_t* data, size_t size) {
|
||||
return size >= 8 && (std::memcmp(data, "FLZ2", 4) == 0 || std::memcmp(data, "FLZ1", 4) == 0);
|
||||
}
|
||||
|
||||
bool fenvLzmaCompress(const uint8_t* data, size_t size, std::vector<uint8_t>& out, std::string& error, FenvPacking packing,
|
||||
const std::function<void(size_t, size_t)>& progress) {
|
||||
// A 32-bit process (Frame Studio for 32-bit Windows / Linux) has 2-4 GB of address space, and the encoder takes
|
||||
// ~11.5x its dictionary: 256 MB dictionaries on 8 threads need far more. There: 32 MB at most, two at a time
|
||||
// (a little bigger file; the decoder reads the size from the stream, so every build opens it).
|
||||
constexpr bool kSmallAddressSpace = sizeof(void*) < 8;
|
||||
constexpr uint32_t kMaxDict = kSmallAddressSpace ? (32u << 20) : (256u << 20);
|
||||
const unsigned hw = kSmallAddressSpace ? std::min(2u, std::max(1u, std::thread::hardware_concurrency()))
|
||||
: std::max(1u, std::thread::hardware_concurrency());
|
||||
std::vector<Chunk> chunks;
|
||||
if (packing == FenvPacking::Fast && size > (48u << 20)) {
|
||||
// Up to 8 chunks of at least 32 MB: more would cost size (each chunk starts with an empty model) and memory.
|
||||
const size_t n = std::min<size_t>({8, hw, (size + (32u << 20) - 1) / (32u << 20)});
|
||||
const size_t step = ((size + n - 1) / n + 15) & ~size_t(15); // on 16-byte steps: ASTC blocks stay aligned
|
||||
for (size_t at = 0; at < size; at += step) { Chunk c; c.at = at; c.size = std::min(step, size - at); chunks.push_back(std::move(c)); }
|
||||
} else {
|
||||
Chunk c; c.size = size; chunks.push_back(std::move(c));
|
||||
}
|
||||
std::atomic<size_t> done{0};
|
||||
const bool single = chunks.size() == 1;
|
||||
parallel(chunks.size(), hw, [&](size_t i) {
|
||||
Chunk& c = chunks[i];
|
||||
CLzmaEncProps p;
|
||||
LzmaEncProps_Init(&p);
|
||||
p.level = 9;
|
||||
p.dictSize = 1u << 20;
|
||||
while (p.dictSize < c.size && p.dictSize < kMaxDict) p.dictSize <<= 1;
|
||||
p.lc = 0; p.lp = 4; p.pb = 4; // ASTC: a 16-byte block's byte position predicts best
|
||||
p.algo = 1; p.fb = 64; p.btMode = 1; p.numHashBytes = 4; p.mc = 1u << 30; // longer fb finds nothing more
|
||||
p.numThreads = single ? 2 : 1; // one stream: the match finder on a second thread
|
||||
SizeT len = c.size + c.size / 3 + (1 << 16);
|
||||
c.packed.resize(len);
|
||||
SizeT propsLen = 5;
|
||||
Progress pr{{onProgress}, &done, 0, size, &progress};
|
||||
c.res = LzmaEncode(c.packed.data(), &len, data + c.at, c.size, &p, c.props, &propsLen, 0, &pr.vt, &kAlloc, &kAlloc);
|
||||
c.packed.resize(c.res == SZ_OK ? len : 0);
|
||||
});
|
||||
out.clear();
|
||||
out.insert(out.end(), {'F', 'L', 'Z', '2'});
|
||||
const uint32_t n = static_cast<uint32_t>(chunks.size());
|
||||
for (int i = 0; i < 4; ++i) out.push_back(static_cast<uint8_t>(n >> (8 * i)));
|
||||
for (const Chunk& c : chunks) {
|
||||
if (c.res != SZ_OK) { error = c.res == SZ_ERROR_MEM ? "not enough memory to compress" : "compression failed"; out.clear(); return false; }
|
||||
out.insert(out.end(), c.props, c.props + 5);
|
||||
put64(out, c.size);
|
||||
put64(out, c.packed.size());
|
||||
out.insert(out.end(), c.packed.begin(), c.packed.end());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fenvLzmaDecompress(const uint8_t* data, size_t size, std::vector<uint8_t>& out, std::string& error) {
|
||||
if (!data || !fenvLzmaIsStream(data, size)) { error = "not an LZMA stream"; return false; }
|
||||
// The sizes in the header are the file's word: a damaged one must not make us allocate terabytes. A real scene
|
||||
// is a few hundred MB and packs at most a few times smaller; 1000x the packed size (or 8 GB) is never one.
|
||||
const uint64_t cap = std::min<uint64_t>(uint64_t(8) << 30, static_cast<uint64_t>(size) * 1000 + (1u << 20));
|
||||
struct Part { const uint8_t* props; uint64_t raw, packed; const uint8_t* src; size_t at; };
|
||||
std::vector<Part> parts;
|
||||
uint64_t total = 0;
|
||||
if (std::memcmp(data, "FLZ1", 4) == 0) { // earlier builds: one chunk, size then data
|
||||
if (size < 17) { error = "damaged LZMA stream"; return false; }
|
||||
parts.push_back({data + 4, get64(data + 9), size - 17, data + 17, 0});
|
||||
total = parts[0].raw;
|
||||
} else {
|
||||
uint32_t n = 0;
|
||||
std::memcpy(&n, data + 4, 4);
|
||||
size_t at = 8;
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
if (at + 21 > size) { error = "damaged LZMA stream"; return false; }
|
||||
Part p{data + at, get64(data + at + 5), get64(data + at + 13), data + at + 21, static_cast<size_t>(total)};
|
||||
if (p.packed > size - at - 21 || p.raw > cap) { error = "damaged LZMA stream"; return false; }
|
||||
at += 21 + p.packed;
|
||||
total += p.raw;
|
||||
if (total > cap) { error = "damaged LZMA stream (claims " + std::to_string(total >> 20) + " MB)"; return false; }
|
||||
parts.push_back(p);
|
||||
}
|
||||
}
|
||||
if (total > cap || total > SIZE_MAX) { error = "damaged LZMA stream (claims " + std::to_string(total >> 20) + " MB)"; return false; }
|
||||
try {
|
||||
out.resize(static_cast<size_t>(total));
|
||||
} catch (const std::exception&) { // bad_alloc / length_error: within the cap, but not in this memory
|
||||
out = std::vector<uint8_t>();
|
||||
error = "not enough memory to unpack " + std::to_string(total >> 20) + " MB";
|
||||
return false;
|
||||
}
|
||||
std::atomic<bool> ok{true};
|
||||
parallel(parts.size(), std::max(1u, std::thread::hardware_concurrency()), [&](size_t i) {
|
||||
const Part& p = parts[i];
|
||||
SizeT destLen = p.raw, srcLen = p.packed;
|
||||
ELzmaStatus status;
|
||||
const SRes r = LzmaDecode(out.data() + p.at, &destLen, p.src, &srcLen, p.props, 5, LZMA_FINISH_END, &status, &kAlloc);
|
||||
if (r != SZ_OK || destLen != p.raw) ok = false;
|
||||
});
|
||||
if (!ok) { error = "damaged LZMA stream"; out.clear(); return false; }
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
// LZMA for a .fenv's scene data: the strongest lossless coder we found for ASTC textures (about a fifth smaller than
|
||||
// deflate on real homes). Tuned for ASTC: position bits on the 16-byte block (lc0 lp4 pb4), optimal parsing.
|
||||
// Smallest: one stream with a dictionary as large as the data (up to 256 MB); the match finder runs on its own
|
||||
// thread. Every repeat anywhere in the scene is found.
|
||||
// Fast: up to 8 chunks coded at once (~3% bigger, several times quicker to save).
|
||||
// Either way the chunks unpack in parallel.
|
||||
// Stream "FLZ2": u32 chunk count, then per chunk: 5 property bytes, u64 raw size, u64 packed size, LZMA data.
|
||||
// ("FLZ1" — one chunk, no count — from earlier builds still unpacks.)
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class FenvPacking { Smallest, Fast };
|
||||
|
||||
// progress(done, total) is called from the coding threads as input is consumed (may be empty).
|
||||
bool fenvLzmaCompress(const uint8_t* data, size_t size, std::vector<uint8_t>& out, std::string& error,
|
||||
FenvPacking packing = FenvPacking::Smallest, const std::function<void(size_t, size_t)>& progress = {});
|
||||
bool fenvLzmaIsStream(const uint8_t* data, size_t size);
|
||||
bool fenvLzmaDecompress(const uint8_t* data, size_t size, std::vector<uint8_t>& out, std::string& error);
|
||||
@@ -0,0 +1,476 @@
|
||||
// fenv-tool — convert homes and models into Frame environments (.fenv).
|
||||
//
|
||||
// fenv-tool pack out.fenv [--name NAME] [--edits edits.json] [--thumb thumb.png] [--keep-apks] [--fast] [--no-optimize] <input>...
|
||||
// --fast: pack in parallel chunks (several times quicker, ~3% bigger; see fenv_lzma.h)
|
||||
// --no-optimize: keep what can't be seen from anywhere you can be (visibility_bake.h; pack, convert, upgrade)
|
||||
// inputs: a home APK (HSL, Clay .opa, glTF .ovrscene), a footprint + its vista, models (.glb .gltf .fbx
|
||||
// .obj). Every home is baked (scene.bin: opens several times faster; a Clay / glTF home's animation goes in as
|
||||
// data, anim_bake.h). --keep-apks stores the source APKs as well.
|
||||
// fenv-tool info file.fenv
|
||||
// fenv-tool convert outdir <input>... one .fenv per input ("a.apk+b.apk" pairs a footprint with a vista)
|
||||
// fenv-tool upgrade file.fenv... re-bake a .fenv's scene with this build, everything else kept (one that
|
||||
// loads from its homes is baked: its APKs, models and world.json give way)
|
||||
// fenv-tool report [--json] <input>... what a home is made of, what it costs and what's wrong with it (exit 3: broken)
|
||||
// fenv-tool settings <home.fenv | home.apk> [JSON | --clear] a world's own settings (world_settings.h), shown or set
|
||||
// fenv-tool tune [--seconds S] [--late F] [--cool C] [--dry-run] <home.fenv | home.apk>
|
||||
// the world's best settings on this headset (walked by frame-home --bench, paced at the display rate), stored in
|
||||
// the .fenv (or <apk>.settings.json); prints them as JSON last
|
||||
#include "fenv.h"
|
||||
#include "home_report.h"
|
||||
#include "tex_mips.h"
|
||||
#include "scene_bake.h"
|
||||
#include "visibility_bake.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
#ifndef _WIN32
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <set>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
FenvPacking g_packing = FenvPacking::Smallest;
|
||||
bool g_optimize = true;
|
||||
|
||||
// The mesh optimizer for a bake (the object edits named keep their meshes whole).
|
||||
bool optimize(const HslScene& scene, const std::string& editsFile, VisibilityResult& vis, BakeOptions& opt) {
|
||||
if (!g_optimize) return false;
|
||||
HomeEdits e;
|
||||
const bool hasEdits = !editsFile.empty() && loadHomeEdits(editsFile, e);
|
||||
return optimizeForBake(scene, hasEdits ? &e.objects : nullptr, vis, opt);
|
||||
}
|
||||
|
||||
std::string stem(const std::string& p) { return fs::u8path(p).stem().u8string(); }
|
||||
|
||||
std::string displayName(const HslScene& s, const std::vector<std::string>& inputs) {
|
||||
std::string n;
|
||||
for (size_t i = 0; i < inputs.size(); ++i) {
|
||||
std::string part = i < s.sources.size() && !s.sources[i].package.empty() ? s.sources[i].package : stem(inputs[i]);
|
||||
const size_t dot = part.find_last_of('.');
|
||||
if (dot != std::string::npos && part.find("com.") == 0) part = part.substr(dot + 1);
|
||||
n += (n.empty() ? "" : " + ") + part;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int pack(const std::string& out, const std::vector<std::string>& inputs, std::string name, const std::string& editsFile,
|
||||
const std::string& thumbFile, bool keepApks) {
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
HslScene scene;
|
||||
std::string err;
|
||||
if (!loadHslScene(inputs, scene, err)) { std::fprintf(stderr, "fenv-tool: %s\n", err.c_str()); return 1; }
|
||||
FenvManifest m;
|
||||
m.name = name.empty() ? displayName(scene, inputs) : name;
|
||||
std::vector<FenvMember> members;
|
||||
std::vector<u8> blob;
|
||||
VisibilityResult vis;
|
||||
BakeOptions opt;
|
||||
const bool optimized = optimize(scene, editsFile, vis, opt);
|
||||
const bool baked = bakeScene(scene, blob, err, optimized ? &opt : nullptr);
|
||||
if (baked) {
|
||||
m.scene = "scene.bin";
|
||||
FenvMember b;
|
||||
b.name = m.scene;
|
||||
b.bytes = std::move(blob);
|
||||
b.lzma = true; // LZMA (fenv_lzma.h): unpacked once, into the .fenv cache
|
||||
b.packing = g_packing;
|
||||
members.push_back(std::move(b));
|
||||
}
|
||||
for (size_t i = 0; i < inputs.size(); ++i) {
|
||||
const HslSourceInfo info = i < scene.sources.size() ? scene.sources[i] : HslSourceInfo{};
|
||||
if (info.role == "Model") {
|
||||
if (baked) continue; // its meshes are in scene.bin
|
||||
const fs::path base = fs::u8path(inputs[i]).parent_path();
|
||||
const std::string dir = "models/" + stem(inputs[i]) + "/";
|
||||
const std::vector<std::string>& files = i < scene.sourceFiles.size() ? scene.sourceFiles[i] : std::vector<std::string>{inputs[i]};
|
||||
for (const std::string& f : files) {
|
||||
std::error_code ec;
|
||||
fs::path rel = fs::relative(fs::u8path(f), base, ec);
|
||||
if (ec || rel.empty() || rel.u8string().find("..") == 0) rel = fs::u8path(f).filename();
|
||||
FenvMember mm;
|
||||
mm.name = dir + rel.generic_u8string();
|
||||
mm.file = f;
|
||||
members.push_back(mm);
|
||||
if (fs::equivalent(fs::u8path(f), fs::u8path(inputs[i]), ec)) m.models.push_back(mm.name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
FenvSource src;
|
||||
src.file = "apks/" + fs::u8path(inputs[i]).filename().u8string();
|
||||
src.role = info.role; src.format = info.format; src.package = info.package; src.version = info.version;
|
||||
m.sources.push_back(src);
|
||||
if (!baked || keepApks) {
|
||||
FenvMember a;
|
||||
a.name = src.file;
|
||||
a.file = inputs[i];
|
||||
a.store = true;
|
||||
members.push_back(a);
|
||||
}
|
||||
}
|
||||
if (!editsFile.empty()) { m.edits = "edits.json"; FenvMember e; e.name = m.edits; e.file = editsFile; members.push_back(e); }
|
||||
if (!thumbFile.empty()) { m.thumbnail = "thumb.png"; FenvMember t; t.name = m.thumbnail; t.file = thumbFile; t.store = true; members.push_back(t); }
|
||||
if (!fenvWrite(out, m, members, err)) { std::fprintf(stderr, "fenv-tool: %s\n", err.c_str()); return 1; }
|
||||
std::error_code ec;
|
||||
const double s = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||
std::printf("%s: \"%s\", %zu meshes, %s, %.0f MB, %.1f s\n", out.c_str(), m.name.c_str(), scene.meshes.size(),
|
||||
baked ? "baked" : ("from its homes (" + err + ")").c_str(), fs::file_size(fs::u8path(out), ec) / 1048576.0, s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int info(const std::string& path) {
|
||||
FenvManifest m;
|
||||
std::vector<u8> thumb;
|
||||
if (!fenvReadManifest(path, m, &thumb)) { std::fprintf(stderr, "fenv-tool: not a .fenv: %s\n", path.c_str()); return 1; }
|
||||
std::printf("%s\n name: %s\n %s\n", path.c_str(), m.name.c_str(), m.scene.empty() ? "loads from its homes" : "baked scene");
|
||||
for (const FenvSource& s : m.sources) std::printf(" %-9s %-17s %s %s\n", s.role.c_str(), s.format.c_str(), s.package.c_str(), s.version.c_str());
|
||||
for (const std::string& f : m.models) std::printf(" model %s\n", f.c_str());
|
||||
std::printf(" edits: %s, thumbnail: %s\n", m.edits.empty() ? "no" : "yes", thumb.empty() ? "no" : "yes");
|
||||
std::printf(" settings: %s%s\n", m.settings.describe().c_str(), m.settings.tuned.empty() ? "" : (" (" + m.settings.tuned + ")").c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Re-bakes a .fenv's scene with this build (smaller texture levels baked in, tex_mips.h), keeping everything else in it
|
||||
// (world, edits, sound, picture, models) byte for byte. One whose textures all have their levels already is left alone.
|
||||
// One that loads from its homes (saved before Clay / glTF animation could be baked) is baked: the scene replaces its
|
||||
// APKs, models and world.json (the world as it loads goes in the scene).
|
||||
int upgrade(const std::string& path) {
|
||||
FenvManifest m;
|
||||
if (!fenvReadManifest(path, m)) { std::fprintf(stderr, "fenv-tool: not a .fenv: %s\n", path.c_str()); return 1; }
|
||||
const bool fromHomes = m.scene.empty();
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
HslScene scene;
|
||||
std::string err;
|
||||
if (!loadHslScene(std::vector<std::string>{path}, scene, err)) { std::fprintf(stderr, "fenv-tool: %s: %s\n", path.c_str(), err.c_str()); return 1; }
|
||||
size_t want = 0;
|
||||
{
|
||||
std::set<const TexSource*> seen;
|
||||
auto look = [&](const std::shared_ptr<const TexSource>& t) { if (t && seen.insert(t.get()).second && texmips::wants(*t)) ++want; };
|
||||
for (const MeshData& md : scene.meshes) {
|
||||
for (const auto* t : {&md.texSrc, &md.normalSrc, &md.ormSrc, &md.emissiveSrc, &md.lmLdrSrc}) look(*t);
|
||||
for (const auto& nt : md.namedTex) look(nt.second);
|
||||
}
|
||||
}
|
||||
VisibilityResult vis;
|
||||
BakeOptions opt;
|
||||
const bool optimized = optimize(scene, scene.fenvEdits, vis, opt) && vis.trisAfter < vis.trisBefore;
|
||||
if (!want && !optimized && !fromHomes) { std::printf("%s: already up to date\n", path.c_str()); return 0; }
|
||||
std::vector<u8> blob;
|
||||
if (!bakeScene(scene, blob, err, optimized ? &opt : nullptr)) { std::fprintf(stderr, "fenv-tool: %s: %s\n", path.c_str(), err.c_str()); return 1; }
|
||||
std::vector<FenvMember> members;
|
||||
const std::string world = m.world;
|
||||
if (fromHomes) { m.scene = "scene.bin"; m.world.clear(); m.models.clear(); }
|
||||
FenvMember b;
|
||||
b.name = m.scene;
|
||||
b.bytes = std::move(blob);
|
||||
b.lzma = true;
|
||||
b.packing = g_packing;
|
||||
members.push_back(std::move(b));
|
||||
{ // everything else, as it is
|
||||
mz_zip_archive z = {};
|
||||
if (!mz_zip_reader_init_file(&z, path.c_str(), 0)) { std::fprintf(stderr, "fenv-tool: can't open %s\n", path.c_str()); return 1; }
|
||||
for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&z); ++i) {
|
||||
mz_zip_archive_file_stat st;
|
||||
if (!mz_zip_reader_file_stat(&z, i, &st) || st.m_is_directory) continue;
|
||||
const std::string name = st.m_filename;
|
||||
if (name == "fenv.json" || name == m.scene || name == m.scene + ".lz") continue;
|
||||
if (fromHomes && (name.rfind("apks/", 0) == 0 || name.rfind("models/", 0) == 0 || name == world)) continue; // in the scene now
|
||||
FenvMember f;
|
||||
f.name = name;
|
||||
f.bytes = fenv_detail::readMember(z, name.c_str(), uint64_t(4) << 30);
|
||||
if (f.bytes.size() != st.m_uncomp_size) { mz_zip_reader_end(&z); std::fprintf(stderr, "fenv-tool: can't read %s in %s\n", name.c_str(), path.c_str()); return 1; }
|
||||
f.store = st.m_method == 0; // stored before (APKs, pictures): stored again
|
||||
members.push_back(std::move(f));
|
||||
}
|
||||
mz_zip_reader_end(&z);
|
||||
}
|
||||
if (!fenvWrite(path, m, members, err)) { std::fprintf(stderr, "fenv-tool: %s\n", err.c_str()); return 1; }
|
||||
std::error_code ec;
|
||||
std::printf("%s: %s%zu textures given their smaller levels, %.0f MB, %.1f s\n", path.c_str(), fromHomes ? "baked from its homes, " : "", want, fs::file_size(fs::u8path(path), ec) / 1048576.0,
|
||||
std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The files one home argument names: a footprint + vista written as a+b, or one file. A file that exists is taken
|
||||
// whole even when its name holds a '+' (a converted pair's .fenv is named footprint+vista.fenv).
|
||||
std::vector<std::string> homeFiles(const std::string& arg) {
|
||||
std::error_code ec;
|
||||
if (fs::is_regular_file(fs::u8path(arg), ec)) return {arg};
|
||||
std::vector<std::string> files;
|
||||
for (size_t p = 0, q; p <= arg.size(); p = q + 1) {
|
||||
q = arg.find('+', p);
|
||||
if (q == std::string::npos) q = arg.size();
|
||||
if (q > p) files.push_back(arg.substr(p, q - p));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
// What a home is made of, what it costs and what's wrong with it (home_report.h): a footprint + vista as a+b.
|
||||
int report(const std::string& arg, bool json) {
|
||||
const std::vector<std::string> group = homeFiles(arg);
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
HslScene scene;
|
||||
std::string err;
|
||||
if (!loadHslScene(group, scene, err)) { std::fprintf(stderr, "fenv-tool: %s: %s\n", arg.c_str(), err.c_str()); return 1; }
|
||||
const double loadSec = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||
const HomeWorld walk = walkWorld(scene);
|
||||
HomeReport r = makeHomeReport(scene, &walk);
|
||||
r.loadSec = loadSec;
|
||||
const std::string out = json ? r.json() : r.text();
|
||||
std::fwrite(out.data(), 1, out.size(), stdout);
|
||||
return r.worst() >= 2 ? 3 : 0;
|
||||
}
|
||||
|
||||
|
||||
#ifndef _WIN32
|
||||
// ── tune: a world's best settings, measured on this headset ──
|
||||
// Frame Home's bench walks the world (FH_BENCH_WALK + FH_BENCH_MOVES: walking, snap turns, teleports, fast head turns)
|
||||
// at candidate settings, paced at the display rate beside the live compositor, as the headset runs it. A candidate
|
||||
// keeps up when the walk holds the display's rate with at most `--late` of its frames late. Your MSAA is kept: the
|
||||
// foveation strength moves first (lower when there's room: sharper edges; higher when not), then the resolution, then
|
||||
// half frame rate as the last resort.
|
||||
|
||||
// One paced walk through the world (frame-home --bench at the display rate, beside the live compositor as on the
|
||||
// headset): whether it kept up. GPU cycles from the perf counters can't judge that: Adreno's counters are GPU-wide, so
|
||||
// they also count the compositor's work, and an unpaced run doesn't meet the compositor at all.
|
||||
struct TuneRun {
|
||||
bool ok = false;
|
||||
double fps = 0, gpuAvg = 0, gpuP95 = 0;
|
||||
int frames = 0, late = 0, hitches = 0; // late: frames over the display period; hitches: over 1.5 of it
|
||||
double lateShare() const { return frames > 0 ? static_cast<double>(late) / frames : 1.0; }
|
||||
};
|
||||
|
||||
// The hottest thermal sensor, in thousandths of a degree (0: none readable).
|
||||
long hottest() {
|
||||
long t = 0;
|
||||
std::error_code ec;
|
||||
for (const auto& e : fs::directory_iterator("/sys/class/thermal", ec)) {
|
||||
if (e.path().filename().u8string().rfind("thermal_zone", 0) != 0) continue;
|
||||
if (std::FILE* f = std::fopen((e.path() / "temp").c_str(), "rb")) {
|
||||
long v = 0;
|
||||
if (std::fscanf(f, "%ld", &v) == 1) t = std::max(t, v);
|
||||
std::fclose(f);
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
std::string shellQuote(const std::string& s) {
|
||||
std::string q = "'";
|
||||
for (char c : s) q += c == '\'' ? std::string("'\\''") : std::string(1, c);
|
||||
return q + "'";
|
||||
}
|
||||
|
||||
// One paced walk through the world at these settings (the world's own settings left out).
|
||||
TuneRun tuneRun(const std::string& bench, const std::string& home, float strength, float scale, int seconds, long coolBelow) {
|
||||
TuneRun r;
|
||||
// Waits for the headset to cool first: a hot GPU runs slower, and every candidate should start alike.
|
||||
for (int waited = 0; coolBelow > 0 && hottest() >= coolBelow && waited < 600; waited += 5) {
|
||||
if (waited % 30 == 0) { std::printf("tune: cooling (%.1f C)\n", hottest() / 1000.0); std::fflush(stdout); }
|
||||
std::this_thread::sleep_for(std::chrono::seconds(5));
|
||||
}
|
||||
// One GPU benchmark at a time on this headset.
|
||||
const int lock = ::open("/tmp/frame-gpu.lock", O_RDWR | O_CREAT, 0666);
|
||||
if (lock >= 0) ::flock(lock, LOCK_EX);
|
||||
char env[512];
|
||||
std::snprintf(env, sizeof env,
|
||||
"FH_BENCH_WALK=1 FH_BENCH_MOVES=1 FH_BENCH_NOWORLD=1 FH_BENCH_OVERLAP=1 FH_BENCH_EARLY=1 "
|
||||
"FH_BENCH_FOVSTRENGTH=%.3f FH_BENCH_SCALE=%.3f",
|
||||
strength, scale);
|
||||
const std::string cmd = std::string(env) + " " + shellQuote(bench) + " --bench --seconds " + std::to_string(seconds) + " " +
|
||||
shellQuote(home) + " 2>&1";
|
||||
if (std::FILE* p = ::popen(cmd.c_str(), "r")) {
|
||||
char line[8192];
|
||||
while (std::fgets(line, sizeof line, p)) {
|
||||
const std::string l = line;
|
||||
if (l.rfind("{\"home\"", 0) != 0) continue;
|
||||
try {
|
||||
const tinyjson::Value j = tinyjson::parse(l);
|
||||
r.fps = j["fps"].asFloat();
|
||||
r.frames = static_cast<int>(j["frames"].asFloat());
|
||||
r.late = static_cast<int>(j["overBudget"].asFloat());
|
||||
r.hitches = static_cast<int>(j["hitches"].asFloat());
|
||||
r.gpuAvg = j["gpuAvg"].asFloat();
|
||||
r.gpuP95 = j["gpuP95"].asFloat();
|
||||
r.ok = r.frames > 0;
|
||||
} catch (...) {}
|
||||
}
|
||||
::pclose(p);
|
||||
}
|
||||
if (lock >= 0) { ::flock(lock, LOCK_UN); ::close(lock); }
|
||||
return r;
|
||||
}
|
||||
|
||||
// The best-looking settings this headset keeps up with in this world, found by walking it (tuneRun) at candidates:
|
||||
// first the headset's own; if they keep up, sharper foveation while it still does; if not, stronger foveation, then
|
||||
// lower resolution, and as a last resort half frame rate. "Keeps up": the display's rate, at most `maxLate` of the
|
||||
// frames late.
|
||||
int tune(const std::string& home, const std::string& benchArg, int seconds, double maxLate, long coolBelow, bool dryRun, double hz) {
|
||||
std::error_code ec;
|
||||
std::string bench = benchArg;
|
||||
if (bench.empty()) bench = (fs::read_symlink("/proc/self/exe", ec).parent_path() / "frame-home").u8string();
|
||||
if (!fs::exists(fs::u8path(bench), ec)) { std::fprintf(stderr, "fenv-tool tune: no frame-home beside fenv-tool (--bench PATH)\n"); return 1; }
|
||||
const char* homeDir = std::getenv("HOME");
|
||||
PerfSettings headset;
|
||||
{
|
||||
std::string j;
|
||||
if (std::FILE* f = std::fopen((std::string(homeDir ? homeDir : ".") + "/frame-home/settings.json").c_str(), "rb")) {
|
||||
char b[4096];
|
||||
for (size_t k; (k = std::fread(b, 1, sizeof b, f)) > 0;) j.append(b, k);
|
||||
std::fclose(f);
|
||||
}
|
||||
if (!headset.fromJson(j)) headset = PerfSettings();
|
||||
}
|
||||
const float s0 = static_cast<float>(headset.foveationLevel), scale0 = headset.autoResolution ? 1.0f : headset.resolution;
|
||||
std::printf("tune: %s: the headset's resolution %.0f%%, %dx MSAA, foveation %.0f; walking it %d s at each try (%.0f fps, at most %.1f%% late)\n",
|
||||
fs::u8path(home).filename().u8string().c_str(), scale0 * 100, headset.msaa, s0, seconds, hz, maxLate * 100);
|
||||
std::fflush(stdout);
|
||||
std::map<std::pair<int, int>, TuneRun> seen;
|
||||
auto keepsUp = [&](const TuneRun& r) { return r.ok && r.fps >= hz * 0.998 && r.lateShare() <= maxLate; };
|
||||
auto measure = [&](float s, float sc) {
|
||||
const std::pair<int, int> key{static_cast<int>(std::lround(s * 100)), static_cast<int>(std::lround(sc * 100))};
|
||||
auto it = seen.find(key);
|
||||
if (it != seen.end()) return it->second;
|
||||
const TuneRun r = tuneRun(bench, home, s, sc, seconds, coolBelow);
|
||||
if (r.ok) std::printf("tune: foveation %.2f, resolution %.0f%%: %.1f fps, %d of %d frames late, GPU %.2f ms (p95 %.2f) (%s)\n", s, sc * 100,
|
||||
r.fps, r.late, r.frames, r.gpuAvg, r.gpuP95, keepsUp(r) ? "keeps up" : "too much");
|
||||
else std::printf("tune: foveation %.2f, resolution %.0f%%: the bench didn't run\n", s, sc * 100);
|
||||
std::fflush(stdout);
|
||||
seen[key] = r;
|
||||
return r;
|
||||
};
|
||||
float s = s0, sc = scale0;
|
||||
int frameRate = -1;
|
||||
TuneRun at = measure(s, sc);
|
||||
if (!at.ok) { std::fprintf(stderr, "fenv-tool tune: the bench didn't run (%s)\n", bench.c_str()); return 1; }
|
||||
if (keepsUp(at)) { // room to spare: sharper edges, half a step at a time
|
||||
for (float t = s - 0.5f; t >= 1.0f - 1e-3f; t -= 0.5f) {
|
||||
const TuneRun r = measure(t, sc);
|
||||
if (!keepsUp(r)) break;
|
||||
s = t; at = r;
|
||||
}
|
||||
} else {
|
||||
bool ok = false;
|
||||
for (float t = s + 0.5f; t <= 3.0f + 1e-3f && !ok; t += 0.5f) { const TuneRun r = measure(t, sc); s = t; at = r; ok = keepsUp(r); }
|
||||
for (float c = sc - 0.1f; !ok && c >= std::max(0.7f, scale0 * 0.75f) - 1e-3f; c -= 0.1f) { const TuneRun r = measure(s, c); sc = c; at = r; ok = keepsUp(r); }
|
||||
if (!ok) frameRate = 1; // drawn every other display frame: twice the time each
|
||||
}
|
||||
WorldSettings w;
|
||||
if (std::abs(s - s0) > 1e-3f) w.foveation = s;
|
||||
if (std::abs(sc - scale0) > 1e-3f) w.resolution = sc;
|
||||
if (frameRate >= 0) w.frameRate = frameRate;
|
||||
char when[32];
|
||||
const std::time_t now = std::time(nullptr);
|
||||
std::strftime(when, sizeof when, "%Y-%m-%d", std::localtime(&now));
|
||||
char note[256];
|
||||
std::snprintf(note, sizeof note, "tuned %s: %.1f fps walking, %.1f%% of frames late%s", when, at.fps, at.lateShare() * 100,
|
||||
frameRate == 1 ? ", even so over: half frame rate" : "");
|
||||
w.tuned = note;
|
||||
w.clamp();
|
||||
std::printf("tune: best: %s (%s)\n", w.describe().c_str(), w.tuned.c_str());
|
||||
if (!dryRun) {
|
||||
std::string err;
|
||||
if (!setWorldSettings(home, w, err)) { std::fprintf(stderr, "fenv-tool tune: %s\n", err.c_str()); return 1; }
|
||||
std::printf("tune: stored in %s\n", isFenvPath(home) ? home.c_str() : worldSettingsPathFor(home).c_str());
|
||||
}
|
||||
std::printf("{\"settings\": %s, \"fps\": %.1f, \"lateShare\": %.4f}\n", w.toJson().c_str(), at.fps, at.lateShare());
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc >= 3 && !std::strcmp(argv[1], "info")) return info(argv[2]);
|
||||
if (argc >= 3 && !std::strcmp(argv[1], "upgrade")) {
|
||||
for (int i = 2; i < argc; ++i) if (!std::strcmp(argv[i], "--no-optimize")) g_optimize = false;
|
||||
int rc = 0;
|
||||
for (int i = 2; i < argc; ++i) if (std::strcmp(argv[i], "--no-optimize")) rc = std::max(rc, upgrade(argv[i]));
|
||||
return rc;
|
||||
}
|
||||
if (argc >= 3 && !std::strcmp(argv[1], "report")) {
|
||||
int rc = 0;
|
||||
const bool json = argc >= 4 && !std::strcmp(argv[2], "--json");
|
||||
for (int i = json ? 3 : 2; i < argc; ++i) { if (i > (json ? 3 : 2) && !json) std::printf("\n"); rc = std::max(rc, report(argv[i], json)); }
|
||||
return rc;
|
||||
}
|
||||
if (argc >= 4 && !std::strcmp(argv[1], "pack")) {
|
||||
std::string name, edits, thumb;
|
||||
bool keep = false;
|
||||
std::vector<std::string> inputs;
|
||||
for (int i = 3; i < argc; ++i) {
|
||||
const std::string a = argv[i];
|
||||
if (a == "--name" && i + 1 < argc) name = argv[++i];
|
||||
else if (a == "--edits" && i + 1 < argc) edits = argv[++i];
|
||||
else if (a == "--thumb" && i + 1 < argc) thumb = argv[++i];
|
||||
else if (a == "--keep-apks") keep = true;
|
||||
else if (a == "--fast") g_packing = FenvPacking::Fast;
|
||||
else if (a == "--no-optimize") g_optimize = false;
|
||||
else inputs.push_back(a);
|
||||
}
|
||||
if (inputs.empty()) { std::fprintf(stderr, "fenv-tool pack: no inputs\n"); return 2; }
|
||||
return pack(argv[2], inputs, name, edits, thumb, keep);
|
||||
}
|
||||
if (argc >= 4 && !std::strcmp(argv[1], "convert")) {
|
||||
int failed = 0;
|
||||
std::error_code ec;
|
||||
fs::create_directories(fs::u8path(argv[2]), ec);
|
||||
for (int i = 3; i < argc; ++i) {
|
||||
const std::string a = argv[i];
|
||||
if (a == "--no-optimize") { g_optimize = false; continue; }
|
||||
const std::vector<std::string> group = homeFiles(a);
|
||||
std::string base;
|
||||
for (const auto& g : group) base += (base.empty() ? "" : "+") + stem(g);
|
||||
failed += pack((fs::u8path(argv[2]) / fs::u8path(base + ".fenv")).u8string(), group, "", "", "", false) != 0;
|
||||
}
|
||||
return failed ? 1 : 0;
|
||||
}
|
||||
if (argc >= 3 && !std::strcmp(argv[1], "settings")) { // shown, or set from JSON ("{}" / --clear: none)
|
||||
if (argc == 3) { const WorldSettings w = worldSettingsFor(argv[2]); std::printf("%s\n", w.toJson().c_str()); return 0; }
|
||||
WorldSettings w;
|
||||
if (std::strcmp(argv[3], "--clear") != 0 && !w.fromJson(argv[3])) { std::fprintf(stderr, "fenv-tool settings: not settings JSON: %s\n", argv[3]); return 2; }
|
||||
std::string err;
|
||||
if (!setWorldSettings(argv[2], w, err)) { std::fprintf(stderr, "fenv-tool settings: %s\n", err.c_str()); return 1; }
|
||||
std::printf("%s: %s\n", argv[2], w.describe().c_str());
|
||||
return 0;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
if (argc >= 3 && !std::strcmp(argv[1], "tune")) {
|
||||
std::string bench, home;
|
||||
int seconds = 30; // each candidate's walk
|
||||
double maxLate = 0.01, hz = 120; // keeping up: the display's rate with at most 1% of frames late
|
||||
long cool = 80000; // each candidate starts below 80 C (as the bench scripts do)
|
||||
bool dry = false;
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string a = argv[i];
|
||||
if (a == "--bench" && i + 1 < argc) bench = argv[++i];
|
||||
else if (a == "--seconds" && i + 1 < argc) seconds = std::clamp(std::atoi(argv[++i]), 5, 300);
|
||||
else if (a == "--late" && i + 1 < argc) maxLate = std::clamp(std::atof(argv[++i]), 0.0, 0.5);
|
||||
else if (a == "--hz" && i + 1 < argc) hz = std::clamp(std::atof(argv[++i]), 30.0, 240.0);
|
||||
else if (a == "--cool" && i + 1 < argc) cool = static_cast<long>(std::atof(argv[++i]) * 1000);
|
||||
else if (a == "--dry-run") dry = true;
|
||||
else home = a;
|
||||
}
|
||||
if (home.empty()) { std::fprintf(stderr, "fenv-tool tune: which world?\n"); return 2; }
|
||||
return tune(home, bench, seconds, maxLate, cool, dry, hz);
|
||||
}
|
||||
#endif
|
||||
std::fprintf(stderr,
|
||||
"usage:\n fenv-tool pack out.fenv [--name NAME] [--edits edits.json] [--thumb thumb.png] [--keep-apks] [--fast] <home.apk|vista.apk|model>...\n"
|
||||
" fenv-tool info file.fenv\n fenv-tool convert outdir <home.apk | footprint.apk+vista.apk | model>...\n"
|
||||
" fenv-tool upgrade file.fenv... re-bake with this build (keeps world, edits, sound, picture)\n"
|
||||
" fenv-tool report [--json] <home.apk | home.fenv | footprint.apk+vista.apk>... what it costs and what's wrong with it\n"
|
||||
" fenv-tool settings <home.fenv | home.apk> [JSON | --clear] a world's own settings, shown or set\n"
|
||||
" fenv-tool tune [--seconds 30] [--late 0.01] [--cool 88] [--dry-run] <home.fenv | home.apk> its best settings on this headset, stored in it\n");
|
||||
return 2;
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
// Generated by tools/fovpass/gen.sh from tools/fovpass/fovpass.vert / .frag: the smooth-foveation pass.
|
||||
#include <cstdint>
|
||||
static const uint32_t kFovPassVert[] = {
|
||||
0x07230203u, 0x00010300u, 0x000d000bu, 0x0000002cu,
|
||||
0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
|
||||
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0007000fu, 0x00000000u, 0x00000004u, 0x6e69616du,
|
||||
0x00000000u, 0x0000000cu, 0x0000001du, 0x00040047u,
|
||||
0x0000000cu, 0x0000000bu, 0x0000002au, 0x00030047u,
|
||||
0x0000001bu, 0x00000002u, 0x00050048u, 0x0000001bu,
|
||||
0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u,
|
||||
0x0000001bu, 0x00000001u, 0x0000000bu, 0x00000001u,
|
||||
0x00050048u, 0x0000001bu, 0x00000002u, 0x0000000bu,
|
||||
0x00000003u, 0x00050048u, 0x0000001bu, 0x00000003u,
|
||||
0x0000000bu, 0x00000004u, 0x00020013u, 0x00000002u,
|
||||
0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u,
|
||||
0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u,
|
||||
0x00000006u, 0x00000002u, 0x00040015u, 0x0000000au,
|
||||
0x00000020u, 0x00000001u, 0x00040020u, 0x0000000bu,
|
||||
0x00000001u, 0x0000000au, 0x0004003bu, 0x0000000bu,
|
||||
0x0000000cu, 0x00000001u, 0x0004002bu, 0x0000000au,
|
||||
0x0000000eu, 0x00000001u, 0x0004002bu, 0x0000000au,
|
||||
0x00000010u, 0x00000002u, 0x00040017u, 0x00000017u,
|
||||
0x00000006u, 0x00000004u, 0x00040015u, 0x00000018u,
|
||||
0x00000020u, 0x00000000u, 0x0004002bu, 0x00000018u,
|
||||
0x00000019u, 0x00000001u, 0x0004001cu, 0x0000001au,
|
||||
0x00000006u, 0x00000019u, 0x0006001eu, 0x0000001bu,
|
||||
0x00000017u, 0x00000006u, 0x0000001au, 0x0000001au,
|
||||
0x00040020u, 0x0000001cu, 0x00000003u, 0x0000001bu,
|
||||
0x0004003bu, 0x0000001cu, 0x0000001du, 0x00000003u,
|
||||
0x0004002bu, 0x0000000au, 0x0000001eu, 0x00000000u,
|
||||
0x0004002bu, 0x00000006u, 0x00000020u, 0x40000000u,
|
||||
0x0004002bu, 0x00000006u, 0x00000022u, 0x3f800000u,
|
||||
0x0004002bu, 0x00000006u, 0x00000025u, 0x00000000u,
|
||||
0x00040020u, 0x00000029u, 0x00000003u, 0x00000017u,
|
||||
0x0005002cu, 0x00000007u, 0x0000002bu, 0x00000022u,
|
||||
0x00000022u, 0x00050036u, 0x00000002u, 0x00000004u,
|
||||
0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u,
|
||||
0x0004003du, 0x0000000au, 0x0000000du, 0x0000000cu,
|
||||
0x000500c4u, 0x0000000au, 0x0000000fu, 0x0000000du,
|
||||
0x0000000eu, 0x000500c7u, 0x0000000au, 0x00000011u,
|
||||
0x0000000fu, 0x00000010u, 0x0004006fu, 0x00000006u,
|
||||
0x00000012u, 0x00000011u, 0x000500c7u, 0x0000000au,
|
||||
0x00000014u, 0x0000000du, 0x00000010u, 0x0004006fu,
|
||||
0x00000006u, 0x00000015u, 0x00000014u, 0x00050050u,
|
||||
0x00000007u, 0x00000016u, 0x00000012u, 0x00000015u,
|
||||
0x0005008eu, 0x00000007u, 0x00000021u, 0x00000016u,
|
||||
0x00000020u, 0x00050083u, 0x00000007u, 0x00000024u,
|
||||
0x00000021u, 0x0000002bu, 0x00050051u, 0x00000006u,
|
||||
0x00000026u, 0x00000024u, 0x00000000u, 0x00050051u,
|
||||
0x00000006u, 0x00000027u, 0x00000024u, 0x00000001u,
|
||||
0x00070050u, 0x00000017u, 0x00000028u, 0x00000026u,
|
||||
0x00000027u, 0x00000025u, 0x00000022u, 0x00050041u,
|
||||
0x00000029u, 0x0000002au, 0x0000001du, 0x0000001eu,
|
||||
0x0003003eu, 0x0000002au, 0x00000028u, 0x000100fdu,
|
||||
0x00010038u,
|
||||
};
|
||||
static const uint32_t kFovPassFrag[] = {
|
||||
0x07230203u, 0x00010300u, 0x000d000bu, 0x00000027u,
|
||||
0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u,
|
||||
0x00001157u, 0x0006000bu, 0x00000001u, 0x4c534c47u,
|
||||
0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu,
|
||||
0x00000000u, 0x00000001u, 0x0008000fu, 0x00000004u,
|
||||
0x00000004u, 0x6e69616du, 0x00000000u, 0x00000009u,
|
||||
0x00000010u, 0x0000001eu, 0x00030010u, 0x00000004u,
|
||||
0x00000007u, 0x00040047u, 0x00000009u, 0x0000001eu,
|
||||
0x00000000u, 0x00040047u, 0x0000000du, 0x00000021u,
|
||||
0x00000000u, 0x00040047u, 0x0000000du, 0x00000022u,
|
||||
0x00000000u, 0x00040047u, 0x00000010u, 0x0000000bu,
|
||||
0x0000000fu, 0x00030047u, 0x00000014u, 0x00000002u,
|
||||
0x00050048u, 0x00000014u, 0x00000000u, 0x00000023u,
|
||||
0x00000000u, 0x00040047u, 0x0000001eu, 0x0000000bu,
|
||||
0x00001158u, 0x00030047u, 0x0000001eu, 0x0000000eu,
|
||||
0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u,
|
||||
0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u,
|
||||
0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u,
|
||||
0x00040020u, 0x00000008u, 0x00000003u, 0x00000007u,
|
||||
0x0004003bu, 0x00000008u, 0x00000009u, 0x00000003u,
|
||||
0x00090019u, 0x0000000au, 0x00000006u, 0x00000001u,
|
||||
0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u,
|
||||
0x00000000u, 0x0003001bu, 0x0000000bu, 0x0000000au,
|
||||
0x00040020u, 0x0000000cu, 0x00000000u, 0x0000000bu,
|
||||
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000000u,
|
||||
0x00040020u, 0x0000000fu, 0x00000001u, 0x00000007u,
|
||||
0x0004003bu, 0x0000000fu, 0x00000010u, 0x00000001u,
|
||||
0x00040017u, 0x00000011u, 0x00000006u, 0x00000002u,
|
||||
0x0003001eu, 0x00000014u, 0x00000011u, 0x00040020u,
|
||||
0x00000015u, 0x00000009u, 0x00000014u, 0x0004003bu,
|
||||
0x00000015u, 0x00000016u, 0x00000009u, 0x00040015u,
|
||||
0x00000017u, 0x00000020u, 0x00000001u, 0x0004002bu,
|
||||
0x00000017u, 0x00000018u, 0x00000000u, 0x00040020u,
|
||||
0x00000019u, 0x00000009u, 0x00000011u, 0x00040020u,
|
||||
0x0000001du, 0x00000001u, 0x00000017u, 0x0004003bu,
|
||||
0x0000001du, 0x0000001eu, 0x00000001u, 0x00040017u,
|
||||
0x00000021u, 0x00000006u, 0x00000003u, 0x0004002bu,
|
||||
0x00000006u, 0x00000025u, 0x00000000u, 0x00050036u,
|
||||
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u,
|
||||
0x000200f8u, 0x00000005u, 0x0004003du, 0x0000000bu,
|
||||
0x0000000eu, 0x0000000du, 0x0004003du, 0x00000007u,
|
||||
0x00000012u, 0x00000010u, 0x0007004fu, 0x00000011u,
|
||||
0x00000013u, 0x00000012u, 0x00000012u, 0x00000000u,
|
||||
0x00000001u, 0x00050041u, 0x00000019u, 0x0000001au,
|
||||
0x00000016u, 0x00000018u, 0x0004003du, 0x00000011u,
|
||||
0x0000001bu, 0x0000001au, 0x00050085u, 0x00000011u,
|
||||
0x0000001cu, 0x00000013u, 0x0000001bu, 0x0004003du,
|
||||
0x00000017u, 0x0000001fu, 0x0000001eu, 0x0004006fu,
|
||||
0x00000006u, 0x00000020u, 0x0000001fu, 0x00050051u,
|
||||
0x00000006u, 0x00000022u, 0x0000001cu, 0x00000000u,
|
||||
0x00050051u, 0x00000006u, 0x00000023u, 0x0000001cu,
|
||||
0x00000001u, 0x00060050u, 0x00000021u, 0x00000024u,
|
||||
0x00000022u, 0x00000023u, 0x00000020u, 0x00070058u,
|
||||
0x00000007u, 0x00000026u, 0x0000000eu, 0x00000024u,
|
||||
0x00000002u, 0x00000025u, 0x0003003eu, 0x00000009u,
|
||||
0x00000026u, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
+521
@@ -0,0 +1,521 @@
|
||||
#pragma once
|
||||
// Walking with any "down" (WorldGravity zones): onto walls, across ceilings, all the way round a planet, and falling.
|
||||
// Frame Home uses GravityWalker for homes that have gravity zones; without zones Locomotion keeps its upright walk.
|
||||
//
|
||||
// The rig (worldFromTracking) is a full frame here: tracking +Y is the player's up, -Z their facing, origin on the
|
||||
// floor under the tracking origin. Each frame:
|
||||
// 1. turns (snap or smooth: the caller's per-frame angle) rotate about up, through the head
|
||||
// 2. up eases toward the zone's (Direction: the zone's +Y; Planet: away from the centre; Surface: out of the
|
||||
// ground underfoot, or out of a wall walked into), turning about the feet so you roll onto the new floor
|
||||
// 3. the stick walks along the ground plane; walls (steep for this up) push back, except in Surface zones,
|
||||
// where walking into a wall makes it the floor
|
||||
// 4. a ray down (along -up) finds the ground: stand on it (ramps followed exactly, steps eased), or fall with
|
||||
// gravity and land; falling further than the world's fallLimit puts you back at the spawn
|
||||
// Walking speeds up and slows down over a fraction of a second (grav::approach), never instantly.
|
||||
#include "core/world.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace grav {
|
||||
|
||||
struct V {
|
||||
float x = 0, y = 0, z = 0;
|
||||
V() = default;
|
||||
V(float a, float b, float c) : x(a), y(b), z(c) {}
|
||||
explicit V(const float* p) : x(p[0]), y(p[1]), z(p[2]) {}
|
||||
V operator+(const V& o) const { return {x + o.x, y + o.y, z + o.z}; }
|
||||
V operator-(const V& o) const { return {x - o.x, y - o.y, z - o.z}; }
|
||||
V operator*(float s) const { return {x * s, y * s, z * s}; }
|
||||
V operator-() const { return {-x, -y, -z}; }
|
||||
};
|
||||
inline float dot(const V& a, const V& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
||||
inline V cross(const V& a, const V& b) { return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; }
|
||||
inline float len(const V& a) { return std::sqrt(dot(a, a)); }
|
||||
inline V norm(const V& a) { const float l = len(a); return l > 1e-9f ? a * (1.0f / l) : V(0, 1, 0); }
|
||||
// Rotates v about unit axis k by angle a (Rodrigues).
|
||||
inline V rotate(const V& v, const V& k, float a) {
|
||||
const float c = std::cos(a), s = std::sin(a);
|
||||
return v * c + cross(k, v) * s + k * (dot(k, v) * (1 - c));
|
||||
}
|
||||
inline V xform(const float* m, const V& p) {
|
||||
return {m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12], m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13], m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14]};
|
||||
}
|
||||
inline V closestOnTri(const V& p, const V& a, const V& b, const V& c) { // Ericson, Real-Time Collision Detection 5.1.5
|
||||
const V ab = b - a, ac = c - a, ap = p - a;
|
||||
const float d1 = dot(ab, ap), d2 = dot(ac, ap);
|
||||
if (d1 <= 0 && d2 <= 0) return a;
|
||||
const V bp = p - b;
|
||||
const float d3 = dot(ab, bp), d4 = dot(ac, bp);
|
||||
if (d3 >= 0 && d4 <= d3) return b;
|
||||
const float vc = d1 * d4 - d3 * d2;
|
||||
if (vc <= 0 && d1 >= 0 && d3 <= 0) return a + ab * (d1 / (d1 - d3));
|
||||
const V cp = p - c;
|
||||
const float d5 = dot(ab, cp), d6 = dot(ac, cp);
|
||||
if (d6 >= 0 && d5 <= d6) return c;
|
||||
const float vb = d5 * d2 - d1 * d6;
|
||||
if (vb <= 0 && d2 >= 0 && d6 <= 0) return a + ac * (d2 / (d2 - d6));
|
||||
const float va = d3 * d6 - d5 * d4;
|
||||
if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) return b + (c - b) * ((d4 - d3) / ((d4 - d3) + (d5 - d6)));
|
||||
const float den = 1 / (va + vb + vc);
|
||||
return a + ab * (vb * den) + ac * (vc * den);
|
||||
}
|
||||
|
||||
// Walking velocity eased toward the stick's: full speed in kAccel s from a standstill, a stop in kDecel s. Turning
|
||||
// (the head, mid-walk) rides the same rate, so the path curves instead of kinking. Instant starts and stops are a
|
||||
// classic comfort problem in VR; a short ramp reads as natural without feeling floaty.
|
||||
constexpr float kAccel = 0.15f, kDecel = 0.12f;
|
||||
inline void approach(V& v, const V& want, float maxSpeed, float dt) {
|
||||
const V dv = want - v;
|
||||
const float l = len(dv);
|
||||
if (l < 1e-6f) { v = want; return; }
|
||||
const float rate = maxSpeed / (dot(want, want) > dot(v, v) ? kAccel : kDecel);
|
||||
v = v + dv * (std::min(l, rate * dt) / l);
|
||||
}
|
||||
// Critically damped easing of x toward target (Unity's SmoothDamp): starts and ends with no jolt, never overshoots.
|
||||
// vel carries over between frames.
|
||||
inline float smoothDamp(float x, float target, float& vel, float smoothTime, float dt) {
|
||||
const float o = 2 / smoothTime, k = o * dt, e = 1 / (1 + k + 0.48f * k * k + 0.235f * k * k * k);
|
||||
const float ch = x - target, tmp = (vel + o * ch) * dt;
|
||||
vel = (vel - o * tmp) * e;
|
||||
float out = target + (ch + tmp) * e;
|
||||
if ((target - x > 0) == (out > target)) { out = target; vel = 0; }
|
||||
return out;
|
||||
}
|
||||
// Steps vs ramps: a change of floor height within this of the distance walked is a slope, followed exactly (easing
|
||||
// it would sink you into a ramp going up); more is a step, eased up (kStepUp s) or down (kStepDown s).
|
||||
constexpr float kRampSlope = 1.2f, kStepUp = 0.09f, kStepDown = 0.11f;
|
||||
// The walk stick: a round deadzone (no axis snapping), then n^1.6 so a light push strolls and only a full one
|
||||
// walks at full speed.
|
||||
inline void shapeStick(float x, float y, float& ox, float& oy) {
|
||||
const float m = std::sqrt(x * x + y * y);
|
||||
if (m < 0.15f) { ox = oy = 0; return; }
|
||||
const float n = std::min(1.0f, (m - 0.15f) / 0.85f), k = std::pow(n, 1.6f) / m;
|
||||
ox = x * k; oy = y * k;
|
||||
}
|
||||
|
||||
// Every surface of the world in a 3D grid, queried in any direction (WorldRuntime only looks straight down).
|
||||
class Surfaces {
|
||||
public:
|
||||
struct Tri { V a, b, c, n; float floorUp; bool blocks; bool allWall; }; // floorUp: |n.up| needed to stand (0 = never floor);
|
||||
// allWall: every face blocks (the Wall role)
|
||||
struct Ball { V c; float r; float floorUp; bool blocks; };
|
||||
|
||||
void build(const HomeWorld& w) {
|
||||
tris_.clear(); balls_.clear(); grid_.clear(); big_.clear();
|
||||
auto addTri = [&](const V& a, const V& b, const V& c, float floorUp, bool blocks, bool allWall = false) {
|
||||
const V n = cross(b - a, c - a);
|
||||
if (len(n) < 1e-9f) return;
|
||||
Tri t{a, b, c, norm(n), floorUp, blocks, allWall};
|
||||
const uint32_t id = static_cast<uint32_t>(tris_.size());
|
||||
tris_.push_back(t);
|
||||
const int x0 = cell(std::min({a.x, b.x, c.x})), x1 = cell(std::max({a.x, b.x, c.x}));
|
||||
const int y0 = cell(std::min({a.y, b.y, c.y})), y1 = cell(std::max({a.y, b.y, c.y}));
|
||||
const int z0 = cell(std::min({a.z, b.z, c.z})), z1 = cell(std::max({a.z, b.z, c.z}));
|
||||
if (static_cast<int64_t>(x1 - x0 + 1) * (y1 - y0 + 1) * (z1 - z0 + 1) > 4096) { big_.push_back(id); return; }
|
||||
for (int x = x0; x <= x1; ++x) for (int y = y0; y <= y1; ++y) for (int z = z0; z <= z1; ++z) grid_[key(x, y, z)].push_back(id);
|
||||
};
|
||||
if (!w.walkIndices.empty()) {
|
||||
const size_t nv = w.walkPositions.size() / 3;
|
||||
for (size_t i = 0; i + 2 < w.walkIndices.size(); i += 3)
|
||||
if (w.walkIndices[i] < nv && w.walkIndices[i + 1] < nv && w.walkIndices[i + 2] < nv)
|
||||
addTri(V(&w.walkPositions[w.walkIndices[i] * 3]), V(&w.walkPositions[w.walkIndices[i + 1] * 3]), V(&w.walkPositions[w.walkIndices[i + 2] * 3]), 0.01f, false);
|
||||
}
|
||||
for (const WorldCollider& c : w.colliders) {
|
||||
if (c.role == WorldCollider::Trigger) continue;
|
||||
const float floorUp = c.role == WorldCollider::Solid ? 0.7f : c.role == WorldCollider::Floor || c.role == WorldCollider::Walkable ? 0.5f : 0.0f;
|
||||
const bool blocks = c.role == WorldCollider::Solid || c.role == WorldCollider::Wall || c.role == WorldCollider::Object;
|
||||
const bool allWall = c.role == WorldCollider::Wall || c.role == WorldCollider::Object; // blocks at any slope
|
||||
if (c.kind == WorldCollider::Mesh) {
|
||||
const std::vector<float> p = worldMeshPositions(c);
|
||||
const size_t nv = p.size() / 3;
|
||||
for (size_t i = 0; i + 2 < c.indices.size(); i += 3)
|
||||
if (c.indices[i] < nv && c.indices[i + 1] < nv && c.indices[i + 2] < nv)
|
||||
addTri(V(&p[c.indices[i] * 3]), V(&p[c.indices[i + 1] * 3]), V(&p[c.indices[i + 2] * 3]), floorUp, blocks, allWall);
|
||||
} else if (c.kind == WorldCollider::Box) {
|
||||
V v[8];
|
||||
for (int k = 0; k < 8; ++k) v[k] = xform(c.world, V((k & 1 ? 1 : -1) * c.half[0], (k & 2 ? 1 : -1) * c.half[1], (k & 4 ? 1 : -1) * c.half[2]));
|
||||
static const int f[6][4] = {{0, 1, 3, 2}, {4, 6, 7, 5}, {0, 4, 5, 1}, {2, 3, 7, 6}, {0, 2, 6, 4}, {1, 5, 7, 3}};
|
||||
for (const auto& q : f) { addTri(v[q[0]], v[q[1]], v[q[2]], floorUp, blocks, allWall); addTri(v[q[0]], v[q[2]], v[q[3]], floorUp, blocks, allWall); }
|
||||
} else if (c.kind == WorldCollider::Sphere) {
|
||||
balls_.push_back({xform(c.world, V(0, 0, 0)), c.radius, floorUp, blocks});
|
||||
} else { // capsule: three balls along its axis
|
||||
for (float t : {-0.5f, 0.0f, 0.5f}) balls_.push_back({xform(c.world, V(0, t * c.length, 0)), c.radius, floorUp, blocks});
|
||||
}
|
||||
}
|
||||
stamp_.assign(tris_.size(), 0);
|
||||
}
|
||||
|
||||
bool empty() const { return tris_.empty() && balls_.empty(); }
|
||||
|
||||
// First surface you could stand on along o + d t (d unit), t in [0, maxT]. anyAngle: every surface counts as
|
||||
// ground (Surface zones). n comes back facing the ray's origin.
|
||||
// standableOut: when given, every surface stops the ray (a teleport arc doesn't pass through walls and
|
||||
// ceilings), and it says whether the one hit can be stood on.
|
||||
bool ray(const V& o, const V& d, float maxT, const V& up, bool anyAngle, float& tHit, V& nHit, bool* standableOut = nullptr) const {
|
||||
bool hit = false;
|
||||
tHit = maxT;
|
||||
auto test = [&](uint32_t id) {
|
||||
const Tri& t = tris_[id];
|
||||
const V facing = dot(t.n, d) < 0 ? t.n : -t.n; // the side the ray comes from
|
||||
const bool ok = standable(t.floorUp, facing, up, anyAngle);
|
||||
if (!ok && !standableOut) return;
|
||||
float th;
|
||||
if (rayTri(o, d, t, th) && th >= 0 && th <= tHit) { tHit = th; nHit = facing; hit = true; if (standableOut) *standableOut = ok; }
|
||||
};
|
||||
++gen_;
|
||||
const float step = kCell * 0.5f;
|
||||
for (float s = 0; s <= maxT + step; s += step) {
|
||||
const V p = o + d * std::min(s, maxT);
|
||||
const int cx = cell(p.x), cy = cell(p.y), cz = cell(p.z);
|
||||
for (int x = cx - 1; x <= cx + 1; ++x) for (int y = cy - 1; y <= cy + 1; ++y) for (int z = cz - 1; z <= cz + 1; ++z) {
|
||||
auto it = grid_.find(key(x, y, z));
|
||||
if (it == grid_.end()) continue;
|
||||
for (uint32_t id : it->second) if (stamp_[id] != gen_) { stamp_[id] = gen_; test(id); }
|
||||
}
|
||||
if (s > tHit) break;
|
||||
}
|
||||
for (uint32_t id : big_) test(id);
|
||||
for (const Ball& b : balls_) {
|
||||
const V oc = o - b.c;
|
||||
const float bq = dot(oc, d), cq = dot(oc, oc) - b.r * b.r, disc = bq * bq - cq;
|
||||
if (disc < 0) continue;
|
||||
const float th = -bq - std::sqrt(disc);
|
||||
if (th < 0 || th > tHit) continue;
|
||||
const V n = norm(o + d * th - b.c);
|
||||
const bool ok = standable(b.floorUp, n, up, anyAngle);
|
||||
if (!ok && !standableOut) continue;
|
||||
tHit = th; nHit = n; hit = true;
|
||||
if (standableOut) *standableOut = ok;
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
// Pushes a ball (the body) out of what blocks it for this up: steep blocking surfaces, blocking balls.
|
||||
// onlyWalls: just the surfaces that can never be floor (Surface zones, where every other surface is walkable).
|
||||
V pushOut(V p, float r, const V& up, bool onlyWalls = false) const {
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
++gen_;
|
||||
const int cx = cell(p.x), cy = cell(p.y), cz = cell(p.z);
|
||||
auto test = [&](uint32_t id) {
|
||||
const Tri& t = tris_[id];
|
||||
if (!t.blocks || (onlyWalls ? t.floorUp > 0 : !t.allWall && std::fabs(dot(t.n, up)) >= 0.7f)) return;
|
||||
const V q = closestOnTri(p, t.a, t.b, t.c), dv = p - q;
|
||||
const float dl = len(dv);
|
||||
if (dl < r && dl > 1e-6f) p = p + dv * ((r - dl) / dl);
|
||||
};
|
||||
for (int x = cx - 1; x <= cx + 1; ++x) for (int y = cy - 1; y <= cy + 1; ++y) for (int z = cz - 1; z <= cz + 1; ++z) {
|
||||
auto it = grid_.find(key(x, y, z));
|
||||
if (it == grid_.end()) continue;
|
||||
for (uint32_t id : it->second) if (stamp_[id] != gen_) { stamp_[id] = gen_; test(id); }
|
||||
}
|
||||
for (uint32_t id : big_) test(id);
|
||||
}
|
||||
for (const Ball& b : balls_) {
|
||||
if (!b.blocks || (onlyWalls && b.floorUp > 0)) continue;
|
||||
const V dv = p - b.c;
|
||||
const float dl = len(dv);
|
||||
if (dl < b.r + r && dl > 1e-6f) p = b.c + dv * ((b.r + r) / dl);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr float kCell = 2.0f;
|
||||
std::vector<Tri> tris_;
|
||||
std::vector<Ball> balls_;
|
||||
std::unordered_map<int64_t, std::vector<uint32_t>> grid_;
|
||||
std::vector<uint32_t> big_;
|
||||
mutable std::vector<uint32_t> stamp_;
|
||||
mutable uint32_t gen_ = 0;
|
||||
|
||||
static int cell(float v) { return static_cast<int>(std::floor(v / kCell)); }
|
||||
static int64_t key(int x, int y, int z) { return (static_cast<int64_t>(x & 0x1FFFFF) << 42) | (static_cast<int64_t>(y & 0x1FFFFF) << 21) | (z & 0x1FFFFF); }
|
||||
// n: the surface's normal on the side being approached. Facing up (for this up) to stand on it: the underside
|
||||
// of a ceiling or a beam is never floor.
|
||||
static bool standable(float floorUp, const V& n, const V& up, bool anyAngle) {
|
||||
if (floorUp <= 0) return false; // walls never carry you
|
||||
return anyAngle || dot(n, up) > floorUp;
|
||||
}
|
||||
static bool rayTri(const V& o, const V& d, const Tri& t, float& th) {
|
||||
const V e1 = t.b - t.a, e2 = t.c - t.a, pv = cross(d, e2);
|
||||
const float det = dot(e1, pv);
|
||||
if (std::fabs(det) < 1e-9f) return false;
|
||||
const float inv = 1 / det;
|
||||
const V tv = o - t.a;
|
||||
const float u = dot(tv, pv) * inv;
|
||||
if (u < 0 || u > 1) return false;
|
||||
const V qv = cross(tv, e1);
|
||||
const float v = dot(d, qv) * inv;
|
||||
if (v < 0 || u + v > 1) return false;
|
||||
th = dot(e2, qv) * inv;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// The zone deciding down at p: the highest priority one containing it (ties: the later), or -1.
|
||||
inline int zoneAt(const std::vector<WorldGravity>& zones, const V& p) {
|
||||
int best = -1;
|
||||
for (size_t i = 0; i < zones.size(); ++i) {
|
||||
const WorldGravity& g = zones[i];
|
||||
bool in;
|
||||
if (g.kind == WorldGravity::Planet) in = len(p - V(&g.world[12])) <= g.radius;
|
||||
else {
|
||||
const V d = p - V(&g.world[12]);
|
||||
in = true;
|
||||
for (int k = 0; k < 3 && in; ++k) {
|
||||
const V col(&g.world[k * 4]);
|
||||
const float sc = len(col); // a scaled frame scales the region (as Studio draws it)
|
||||
in = sc > 1e-6f && std::fabs(dot(d, col * (1.0f / sc))) <= g.half[k] * sc;
|
||||
}
|
||||
}
|
||||
if (in && (best < 0 || g.priority >= zones[best].priority)) best = static_cast<int>(i);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
} // namespace grav
|
||||
|
||||
class GravityWalker {
|
||||
public:
|
||||
using V = grav::V;
|
||||
|
||||
void build(const HomeWorld& w) {
|
||||
surf_.build(w);
|
||||
zones_ = w.gravity;
|
||||
fallLimit_ = w.fallLimit > 1 ? w.fallLimit : 30.0f;
|
||||
hasSpawn_ = !w.spawns.empty();
|
||||
if (hasSpawn_) spawn_ = w.spawns[0];
|
||||
reset();
|
||||
}
|
||||
|
||||
// To the spawn (or the origin), standing on the ground under it, facing its way.
|
||||
void reset() {
|
||||
V p = hasSpawn_ ? V(spawn_.pos) : V(0, 0, 0);
|
||||
const V up = upAt(p, V(0, 1, 0)); // not the old up: you may have fallen off a wall
|
||||
const float yaw = hasSpawn_ ? spawn_.yaw : 0.0f;
|
||||
V fwd(-std::sin(yaw), 0, -std::cos(yaw));
|
||||
fwd = fwd - up * grav::dot(fwd, up);
|
||||
if (grav::len(fwd) < 1e-3f) fwd = grav::cross(up, std::fabs(up.x) < 0.9f ? V(1, 0, 0) : V(0, 0, 1));
|
||||
setBasis(up, grav::norm(fwd));
|
||||
float t; V n;
|
||||
if (surf_.ray(p + up * 1.5f, -up, 6.0f, up, zoneKind(p) == WorldGravity::Surface, t, n)) p = p + up * 1.5f - up * t;
|
||||
origin_ = p;
|
||||
vel_ = fallen_ = 0;
|
||||
falling_ = false;
|
||||
wallUp_ = V();
|
||||
wallTime_ = 0;
|
||||
walkV_ = V();
|
||||
rem_ = riseV_ = 0;
|
||||
lastFeet_ = origin_;
|
||||
}
|
||||
|
||||
void rig(float m[16]) const {
|
||||
const float r[16] = {x_.x, x_.y, x_.z, 0, up_.x, up_.y, up_.z, 0, z_.x, z_.y, z_.z, 0, origin_.x, origin_.y, origin_.z, 1};
|
||||
std::copy(r, r + 16, m);
|
||||
}
|
||||
V up() const { return up_; }
|
||||
bool falling() const { return falling_; }
|
||||
int respawns() const { return respawns_; }
|
||||
V head(const float headT[16]) const { return toWorld(V(&headT[12])); }
|
||||
void setSpeed(float metresPerSecond) { speed_ = metresPerSecond; } // the walk stick at full deflection
|
||||
|
||||
// One frame: moveX / moveY the walk stick (-1..1), turn the turn to make now (radians, + = left).
|
||||
void update(float dt, const float headT[16], float moveX, float moveY, float turn) {
|
||||
dt = std::min(dt, 0.05f);
|
||||
const V h(&headT[12]);
|
||||
V hw = toWorld(h);
|
||||
if (turn != 0) rotateRig(up_, turn, hw);
|
||||
// Up: the zone's, or the wall walked into (Surface zones), eased so you roll onto it.
|
||||
const int zi = grav::zoneAt(zones_, hw);
|
||||
const bool surfaceZone = zi >= 0 && zones_[zi].kind == WorldGravity::Surface;
|
||||
V target = upAt(hw, up_);
|
||||
if (surfaceZone) {
|
||||
float t; V n;
|
||||
if (surf_.ray(hw, -up_, h.y + 1.0f, up_, true, t, n)) target = n;
|
||||
if (wallTime_ > 0) { target = wallUp_; wallTime_ -= dt; }
|
||||
}
|
||||
const float ang = std::acos(std::clamp(grav::dot(up_, target), -1.0f, 1.0f));
|
||||
if (ang > 1e-3f) {
|
||||
V axis = grav::cross(up_, target);
|
||||
if (grav::len(axis) < 1e-5f) axis = x_; // straight flip: roll forward
|
||||
const float stepA = std::min(ang, (surfaceZone ? 4.0f : 2.5f) * dt);
|
||||
rotateRig(grav::norm(axis), stepA, hw - up_ * h.y); // about the feet
|
||||
}
|
||||
hw = toWorld(h);
|
||||
// Walk along the ground plane, head-relative, the velocity eased (approach) toward the stick's.
|
||||
{
|
||||
V want;
|
||||
V fwd = toWorldDir(V(-headT[8], -headT[9], -headT[10]));
|
||||
fwd = fwd - up_ * grav::dot(fwd, up_);
|
||||
if ((moveX != 0 || moveY != 0) && grav::len(fwd) > 1e-4f) {
|
||||
fwd = grav::norm(fwd);
|
||||
want = (fwd * moveY + grav::cross(fwd, up_) * moveX) * speed_;
|
||||
}
|
||||
walkV_ = walkV_ - up_ * grav::dot(walkV_, up_); // along the ground as up turns
|
||||
grav::approach(walkV_, want, speed_, dt);
|
||||
}
|
||||
if (grav::len(walkV_) > 1e-5f) {
|
||||
V d = walkV_ * dt;
|
||||
const V feet = hw - up_ * h.y;
|
||||
if (surfaceZone) {
|
||||
// A wall ahead becomes the floor: start rolling onto it.
|
||||
float t; V n;
|
||||
const V dd = grav::norm(d);
|
||||
if (surf_.ray(feet + up_ * 0.3f, dd, grav::len(d) + 0.35f, up_, true, t, n) && std::fabs(grav::dot(n, up_)) < 0.7f) {
|
||||
wallUp_ = n;
|
||||
wallTime_ = 0.6f;
|
||||
}
|
||||
// Walls that can never be floor (the Wall role) still block here.
|
||||
const V body = feet + up_ * 0.9f;
|
||||
d = surf_.pushOut(body + d, 0.25f, up_, true) - body;
|
||||
d = d - up_ * grav::dot(d, up_);
|
||||
} else {
|
||||
const V body = feet + up_ * 0.9f;
|
||||
V moved = surf_.pushOut(body + d, 0.25f, up_);
|
||||
d = moved - body;
|
||||
d = d - up_ * grav::dot(d, up_);
|
||||
}
|
||||
origin_ = origin_ + d;
|
||||
hw = hw + d;
|
||||
walkV_ = d * (1 / dt); // what a wall left of it: no pent-up push
|
||||
}
|
||||
// Stand on the ground, or fall. The ground is looked for from knee height (a step), not from the head: a
|
||||
// head inside a cabinet or under a beam would otherwise find that and pop you up onto it.
|
||||
float t; V n;
|
||||
const V feet = hw - up_ * h.y;
|
||||
constexpr float kStep = 0.45f;
|
||||
V walked = feet - lastFeet_; // stick and roomscale, along the ground
|
||||
walked = walked - up_ * grav::dot(walked, up_);
|
||||
lastFeet_ = feet;
|
||||
if (!falling_ && surf_.ray(feet + up_ * kStep, -up_, kStep + 0.6f, up_, surfaceZone, t, n)) {
|
||||
float rise = kStep - t; // + : the ground is above the feet (a step up)
|
||||
// The ground's own change since last frame: a ramp is followed exactly, a step joins the eased rest.
|
||||
const float d = rise - rem_;
|
||||
if (std::fabs(d) <= grav::len(walked) * grav::kRampSlope + 0.004f) { origin_ = origin_ + up_ * d; rise -= d; }
|
||||
const float left = grav::smoothDamp(rise, 0, riseV_, rise > 0 ? grav::kStepUp : grav::kStepDown, dt);
|
||||
origin_ = origin_ + up_ * (rise - left);
|
||||
rem_ = left;
|
||||
vel_ = fallen_ = 0;
|
||||
} else {
|
||||
rem_ = riseV_ = 0;
|
||||
falling_ = true;
|
||||
vel_ = std::min(vel_ + 9.8f * dt, 55.0f);
|
||||
const float stepD = vel_ * dt;
|
||||
// Landing: from a step above the feet, so ground rising under you while you fall is still found.
|
||||
if (surf_.ray(feet + up_ * kStep, -up_, stepD + kStep, up_, surfaceZone, t, n)) {
|
||||
origin_ = origin_ + up_ * (kStep - t);
|
||||
falling_ = false;
|
||||
vel_ = fallen_ = 0;
|
||||
} else {
|
||||
origin_ = origin_ - up_ * stepD;
|
||||
fallen_ += stepD;
|
||||
if (fallen_ > fallLimit_) { ++respawns_; reset(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The teleport arc from the hand (tracking space), under this gravity; true with the landing point. It stops at
|
||||
// the first surface it meets: one you can stand on is the landing, anything else (a wall, a ceiling) means
|
||||
// nowhere. normal (optional): the landing surface's normal, facing the arc. path (optional): the arc's points
|
||||
// in world space, hand first, for drawing it. headPos (optional, tracking space): a hand reached through a wall
|
||||
// from the head aims nothing.
|
||||
bool aim(const float handPos[3], const float handDir[3], float target[3], float normal[3] = nullptr,
|
||||
std::vector<float>* path = nullptr, const float* headPos = nullptr) const {
|
||||
V p = toWorld(V(handPos)), v = toWorldDir(V(handDir)) * 7.0f;
|
||||
auto keep = [&](const V& q) { if (path) { path->push_back(q.x); path->push_back(q.y); path->push_back(q.z); } };
|
||||
if (path) path->clear();
|
||||
keep(p);
|
||||
const bool anyAngle = zoneKind(p) == WorldGravity::Surface;
|
||||
if (headPos) {
|
||||
const V hd = toWorld(V(headPos)), to = p - hd;
|
||||
const float tl = grav::len(to);
|
||||
float t; V n; bool st = false;
|
||||
if (tl > 1e-4f && surf_.ray(hd, to * (1 / tl), tl, up_, anyAngle, t, n, &st)) return false;
|
||||
}
|
||||
for (int i = 0; i < 90; ++i) {
|
||||
const float dt = 0.03f;
|
||||
const V seg = v * dt - up_ * (4.9f * dt * dt);
|
||||
const float sl = grav::len(seg);
|
||||
float t; V n;
|
||||
bool canStand = false;
|
||||
if (sl > 1e-6f && surf_.ray(p, seg * (1 / sl), sl, up_, anyAngle, t, n, &canStand)) {
|
||||
const V hit = p + seg * (t / sl);
|
||||
keep(hit);
|
||||
if (!canStand) return false; // a wall or a ceiling in the way: nowhere to land
|
||||
target[0] = hit.x; target[1] = hit.y; target[2] = hit.z;
|
||||
if (normal) { normal[0] = n.x; normal[1] = n.y; normal[2] = n.z; }
|
||||
return true;
|
||||
}
|
||||
p = p + seg;
|
||||
keep(p);
|
||||
v = v - up_ * (9.8f * dt);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Moves the player so their feet land on `target` (world). In a Surface zone, `normal` (the landing surface's)
|
||||
// becomes the new up there: teleporting onto a wall stands you on it.
|
||||
void teleport(const float target[3], const float headT[16], const float* normal = nullptr) {
|
||||
if (normal && zoneKind(V(target)) == WorldGravity::Surface) {
|
||||
const V nu = grav::norm(V(normal));
|
||||
const float ang = std::acos(std::clamp(grav::dot(up_, nu), -1.0f, 1.0f));
|
||||
if (ang > 1e-3f) {
|
||||
V axis = grav::cross(up_, nu);
|
||||
if (grav::len(axis) < 1e-5f) axis = x_;
|
||||
rotateRig(grav::norm(axis), ang, toWorld(V(&headT[12])) - up_ * headT[13]);
|
||||
}
|
||||
wallTime_ = 0;
|
||||
}
|
||||
const V feet = toWorld(V(&headT[12])) - up_ * headT[13];
|
||||
origin_ = origin_ + (V(target) - feet);
|
||||
falling_ = false;
|
||||
vel_ = fallen_ = 0;
|
||||
walkV_ = V(); // you arrive standing still
|
||||
rem_ = riseV_ = 0;
|
||||
lastFeet_ = V(target);
|
||||
}
|
||||
|
||||
// tracking -> world, for overlays (the teleport ring): the ring's frame at a world point.
|
||||
V toTracking(const V& w) const { const V d = w - origin_; return V(grav::dot(d, x_), grav::dot(d, up_), grav::dot(d, z_)); }
|
||||
|
||||
private:
|
||||
grav::Surfaces surf_;
|
||||
std::vector<WorldGravity> zones_;
|
||||
float fallLimit_ = 30, speed_ = 2.0f;
|
||||
bool hasSpawn_ = false;
|
||||
WorldPoint spawn_;
|
||||
V x_{1, 0, 0}, up_{0, 1, 0}, z_{0, 0, 1}, origin_;
|
||||
float vel_ = 0, fallen_ = 0;
|
||||
bool falling_ = false;
|
||||
V wallUp_;
|
||||
float wallTime_ = 0;
|
||||
int respawns_ = 0;
|
||||
V walkV_; // walking velocity (eased, along the ground)
|
||||
float rem_ = 0, riseV_ = 0; // a step still being eased (metres along up), its rate
|
||||
V lastFeet_;
|
||||
|
||||
V toWorld(const V& t) const { return x_ * t.x + up_ * t.y + z_ * t.z + origin_; }
|
||||
V toWorldDir(const V& t) const { return x_ * t.x + up_ * t.y + z_ * t.z; }
|
||||
void setBasis(const V& up, const V& fwd) { up_ = grav::norm(up); z_ = -fwd; x_ = grav::norm(grav::cross(up_, z_)); z_ = grav::cross(x_, up_); }
|
||||
void rotateRig(const V& axis, float a, const V& pivot) {
|
||||
up_ = grav::norm(grav::rotate(up_, axis, a));
|
||||
x_ = grav::rotate(x_, axis, a);
|
||||
x_ = grav::norm(x_ - up_ * grav::dot(x_, up_)); // Gram-Schmidt: no drift from many small turns
|
||||
z_ = grav::cross(x_, up_);
|
||||
origin_ = pivot + grav::rotate(origin_ - pivot, axis, a);
|
||||
}
|
||||
int zoneKind(const V& p) const { const int zi = grav::zoneAt(zones_, p); return zi >= 0 ? zones_[zi].kind : -1; }
|
||||
// Up at p for Direction / Planet zones and no zone; Surface zones keep `cur` (their up comes from the ground).
|
||||
V upAt(const V& p, const V& cur) const {
|
||||
const int zi = grav::zoneAt(zones_, p);
|
||||
if (zi < 0) return V(0, 1, 0);
|
||||
const WorldGravity& g = zones_[zi];
|
||||
if (g.kind == WorldGravity::Direction) return grav::norm(V(&g.world[4]));
|
||||
if (g.kind == WorldGravity::Planet) { const V d = p - V(&g.world[12]); return grav::len(d) > 1e-3f ? grav::norm(d) : cur; }
|
||||
return cur;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,225 @@
|
||||
#pragma once
|
||||
// Guide geometry: what Frame Home draws itself to show you where you're going (the teleport arc and its landing
|
||||
// marker, the seats you can sit in, the pointer on a seat). Built on the CPU each frame as plain world-space
|
||||
// triangles with a colour each, and drawn by HslRenderer::setGuide over the scene (depth tested, so walls hide it,
|
||||
// with a faint copy where it's hidden). It's Frame Home's own frame, so it shows in system mode too, where
|
||||
// SteamVR overlays of a non-scene app can't be relied on.
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
struct GuideVertex { float pos[3]; float rgba[4]; }; // world (home) space; linear colour, straight alpha
|
||||
|
||||
// What locomotion wants shown this frame (Locomotion::guide()).
|
||||
struct LocoGuide {
|
||||
bool aiming = false, valid = false, seat = false; // the arc is up; it lands; it lands on a seat
|
||||
std::vector<float> arc; // xyz points, the hand first, the end last
|
||||
float target[3] = {0, 0, 0}, normal[3] = {0, 1, 0}; // landing point and its surface's normal
|
||||
float aimTime = 0; // seconds the arc has been up (it fades in)
|
||||
struct Seat { float pos[3]; float yaw; bool hot; float near = 1; }; // hot: pointed at (or the arc lands on it); near: 0..1 fade
|
||||
std::vector<Seat> seats; // seats near you (not the one you sit in)
|
||||
bool pointer = false; // a hand points at a seat: a short line to it
|
||||
float pointerFrom[3] = {0, 0, 0}, pointerTo[3] = {0, 0, 0};
|
||||
};
|
||||
|
||||
namespace guide {
|
||||
|
||||
struct P { float x, y, z; };
|
||||
inline P operator+(P a, P b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
|
||||
inline P operator-(P a, P b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
|
||||
inline P operator*(P a, float s) { return {a.x * s, a.y * s, a.z * s}; }
|
||||
inline float dot(P a, P b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
||||
inline P cross(P a, P b) { return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; }
|
||||
inline P norm(P a) { const float l = std::sqrt(dot(a, a)); return l > 1e-9f ? a * (1 / l) : P{0, 1, 0}; }
|
||||
inline float smooth(float e0, float e1, float x) { const float t = std::clamp((x - e0) / (e1 - e0), 0.0f, 1.0f); return t * t * (3 - 2 * t); }
|
||||
|
||||
struct C { float r, g, b, a; };
|
||||
inline C mix(C a, C b, float t) { return {a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t, a.a + (b.a - a.a) * t}; }
|
||||
inline C alpha(C c, float a) { return {c.r, c.g, c.b, c.a * a}; }
|
||||
|
||||
inline void vert(std::vector<GuideVertex>& o, P p, C c) { o.push_back({{p.x, p.y, p.z}, {c.r, c.g, c.b, c.a}}); }
|
||||
inline void tri(std::vector<GuideVertex>& o, P a, C ca, P b, C cb, P c, C cc) { vert(o, a, ca); vert(o, b, cb); vert(o, c, cc); }
|
||||
inline void quad(std::vector<GuideVertex>& o, P a, C ca, P b, C cb, P c, C cc, P d, C cd) { tri(o, a, ca, b, cb, c, cc); tri(o, a, ca, c, cc, d, cd); }
|
||||
|
||||
// Two axes across a surface with normal n.
|
||||
inline void basis(P n, P& u, P& w) {
|
||||
u = norm(cross(std::fabs(n.y) < 0.9f ? P{0, 1, 0} : P{1, 0, 0}, n));
|
||||
w = cross(n, u);
|
||||
}
|
||||
|
||||
// A flat band between radii r0 and r1 around c (in the u/w plane), soft-edged: transparent at both rims, `col`
|
||||
// along its middle.
|
||||
inline void ring(std::vector<GuideVertex>& o, P c, P u, P w, float r0, float r1, C col, int segs = 64) {
|
||||
const float rm = (r0 + r1) * 0.5f;
|
||||
const C clear = alpha(col, 0);
|
||||
for (int i = 0; i < segs; ++i) {
|
||||
const float a0 = 6.2831853f * i / segs, a1 = 6.2831853f * (i + 1) / segs;
|
||||
const P d0 = u * std::cos(a0) + w * std::sin(a0), d1 = u * std::cos(a1) + w * std::sin(a1);
|
||||
quad(o, c + d0 * r0, clear, c + d1 * r0, clear, c + d1 * rm, col, c + d0 * rm, col);
|
||||
quad(o, c + d0 * rm, col, c + d1 * rm, col, c + d1 * r1, clear, c + d0 * r1, clear);
|
||||
}
|
||||
}
|
||||
|
||||
// A filled disc, `mid` at the centre fading to `rim` at radius r.
|
||||
inline void disc(std::vector<GuideVertex>& o, P c, P u, P w, float r, C mid, C rim, int segs = 48) {
|
||||
for (int i = 0; i < segs; ++i) {
|
||||
const float a0 = 6.2831853f * i / segs, a1 = 6.2831853f * (i + 1) / segs;
|
||||
tri(o, c, mid, c + (u * std::cos(a0) + w * std::sin(a0)) * r, rim, c + (u * std::cos(a1) + w * std::sin(a1)) * r, rim);
|
||||
}
|
||||
}
|
||||
|
||||
// The comfort vignette: the edges of view darkened by `amount` (0..1) while you move without moving yourself. A band
|
||||
// a hand's width in front of the eyes, facing along f (u, w across it): clear inside `inner`, dark from `outer` (both
|
||||
// tangents of the angle off the view's centre) out past the edge of view.
|
||||
inline void vignette(std::vector<GuideVertex>& o, P eye, P f, P u, P w, float inner, float outer, float amount, int segs = 48) {
|
||||
const float d = 0.25f;
|
||||
const P c = eye + f * d;
|
||||
const C clear{0, 0, 0, 0}, dark{0, 0, 0, amount};
|
||||
for (int i = 0; i < segs; ++i) {
|
||||
const float a0 = 6.2831853f * i / segs, a1 = 6.2831853f * (i + 1) / segs;
|
||||
const P d0 = u * std::cos(a0) + w * std::sin(a0), d1 = u * std::cos(a1) + w * std::sin(a1);
|
||||
quad(o, c + d0 * (inner * d), clear, c + d1 * (inner * d), clear, c + d1 * (outer * d), dark, c + d0 * (outer * d), dark);
|
||||
quad(o, c + d0 * (outer * d), dark, c + d1 * (outer * d), dark, c + d1 * (4.0f * d), dark, c + d0 * (4.0f * d), dark);
|
||||
}
|
||||
}
|
||||
|
||||
// A chevron (an arrowhead ">") pointing along f in the plane of f and s, its tip at `tip`.
|
||||
inline void chevron(std::vector<GuideVertex>& o, P tip, P f, P s, float size, float thick, C col) {
|
||||
const C clear = alpha(col, 0);
|
||||
for (float side : {-1.0f, 1.0f}) {
|
||||
const P a = tip, b = tip - f * size + s * (side * size);
|
||||
const P across = norm(cross(cross(f, s), b - a)) * (thick * 0.5f); // in the plane, across the stroke
|
||||
quad(o, a - across, clear, b - across, clear, b, col, a, col);
|
||||
quad(o, a, col, b, col, b + across, clear, a + across, clear);
|
||||
}
|
||||
}
|
||||
|
||||
// A camera-facing ribbon along the points, soft-edged across. width(s, u) / colour(s, u): s metres from the
|
||||
// start, u = s / length.
|
||||
template <typename W, typename K>
|
||||
inline void ribbon(std::vector<GuideVertex>& o, const std::vector<P>& pts, P eye, W width, K colour) {
|
||||
if (pts.size() < 2) return;
|
||||
std::vector<float> s(pts.size(), 0);
|
||||
for (size_t i = 1; i < pts.size(); ++i) s[i] = s[i - 1] + std::sqrt(dot(pts[i] - pts[i - 1], pts[i] - pts[i - 1]));
|
||||
const float len = std::max(s.back(), 1e-4f);
|
||||
P prevL{}, prevM{}, prevR{};
|
||||
C prevC{};
|
||||
for (size_t i = 0; i < pts.size(); ++i) {
|
||||
const P t = norm(pts[std::min(i + 1, pts.size() - 1)] - pts[i > 0 ? i - 1 : 0]);
|
||||
const P side = norm(cross(t, eye - pts[i])) * width(s[i], s[i] / len);
|
||||
const C c = colour(s[i], s[i] / len);
|
||||
const P L = pts[i] - side, M = pts[i], R = pts[i] + side;
|
||||
if (i > 0) {
|
||||
quad(o, prevL, alpha(prevC, 0), L, alpha(c, 0), M, c, prevM, prevC);
|
||||
quad(o, prevM, prevC, M, c, R, alpha(c, 0), prevR, alpha(prevC, 0));
|
||||
}
|
||||
prevL = L; prevM = M; prevR = R; prevC = c;
|
||||
}
|
||||
}
|
||||
|
||||
// The points resampled every `step` metres (dashes need even spacing; the arc's own points are 7 m/s x 30 ms).
|
||||
inline std::vector<P> resample(const std::vector<P>& in, float step) {
|
||||
std::vector<P> out;
|
||||
if (in.empty()) return out;
|
||||
out.push_back(in[0]);
|
||||
float carry = 0;
|
||||
for (size_t i = 1; i < in.size(); ++i) {
|
||||
const P d = in[i] - in[i - 1];
|
||||
const float l = std::sqrt(dot(d, d));
|
||||
float at = step - carry;
|
||||
for (; at < l; at += step) out.push_back(in[i - 1] + d * (at / l));
|
||||
carry = l - (at - step);
|
||||
}
|
||||
if (std::sqrt(dot(out.back() - in.back(), out.back() - in.back())) > 1e-4f) out.push_back(in.back());
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace guide
|
||||
|
||||
// Everything in `g` as triangles, for an eye at `eye` (ribbons face it) at time `t` seconds (dashes flow, rings
|
||||
// pulse). Appends to `out`.
|
||||
inline void buildGuide(const LocoGuide& g, const float eye[3], float t, std::vector<GuideVertex>& out) {
|
||||
using namespace guide;
|
||||
const P E{eye[0], eye[1], eye[2]};
|
||||
const C cyan{0.30f, 0.78f, 1.00f, 1}, ice{0.86f, 0.97f, 1.00f, 1}, red{1.00f, 0.32f, 0.26f, 1}, gold{1.00f, 0.78f, 0.42f, 1};
|
||||
const float appear = smooth(0.0f, 0.12f, g.aimTime);
|
||||
const float pulse = 0.5f + 0.5f * std::sin(t * 6.2831853f * 1.1f); // ~1 Hz: alive, not flashing
|
||||
|
||||
// Seats near you: a faint ring on the seat, a backrest arc behind it and a chevron the way it faces;
|
||||
// bright and breathing while pointed at.
|
||||
for (const LocoGuide::Seat& s : g.seats) {
|
||||
const P c{s.pos[0], s.pos[1] + 0.08f, s.pos[2]}; // a little over the seat point: cushions sit above it
|
||||
const P f{-std::sin(s.yaw), 0, -std::cos(s.yaw)}, r{std::cos(s.yaw), 0, -std::sin(s.yaw)}, up{0, 1, 0};
|
||||
constexpr float kSize = 0.6f; // small: a hint on the seat, not furniture of its own
|
||||
const float k = s.hot ? 0.8f + 0.2f * pulse : 0.38f * s.near, grow = (s.hot ? 1.0f + 0.05f * pulse : 1.0f) * kSize;
|
||||
const C col = alpha(s.hot ? mix(gold, C{1, 0.95f, 0.85f, 1}, 0.3f) : gold, k);
|
||||
ring(out, c, r, f, 0.20f * grow, 0.235f * grow, col);
|
||||
if (s.hot) disc(out, c, r, f, 0.20f * grow, alpha(gold, 0.16f), alpha(gold, 0.05f));
|
||||
// the backrest: an upright arc behind the seat, so it reads as a chair from any side
|
||||
std::vector<P> back;
|
||||
for (int i = 0; i <= 16; ++i) {
|
||||
const float a = -1.1f + 2.2f * i / 16;
|
||||
back.push_back(c - f * (0.235f * grow * std::cos(a)) + r * (0.235f * grow * std::sin(a)) + up * (0.20f * grow * std::cos(a * 0.9f)));
|
||||
}
|
||||
ribbon(out, back, E, [](float, float) { return 0.012f; }, [&](float, float) { return col; });
|
||||
chevron(out, c + f * (0.34f * grow), f, r, 0.07f * grow, 0.022f * grow, col);
|
||||
// and a "here" chevron floating over it, above chair backs, so it shows from behind the furniture too;
|
||||
// pointed at, it bobs gently (the marker moves, never your view)
|
||||
if (s.hot) { // (only while pointed at: close by, the seat's ring says enough)
|
||||
const P over = c + up * (0.45f + 0.03f * pulse);
|
||||
const P across = norm(cross(up, E - over));
|
||||
chevron(out, over, up * -1.0f, across, 0.06f, 0.02f, col);
|
||||
}
|
||||
}
|
||||
|
||||
// The pointer: from the hand to the seat it's on, fading in from the hand.
|
||||
if (g.pointer) {
|
||||
const P a{g.pointerFrom[0], g.pointerFrom[1], g.pointerFrom[2]}, b{g.pointerTo[0], g.pointerTo[1], g.pointerTo[2]};
|
||||
std::vector<P> line;
|
||||
for (int i = 0; i <= 24; ++i) line.push_back(a + (b - a) * (i / 24.0f));
|
||||
ribbon(out, line, E, [](float, float u) { return 0.003f + 0.002f * u; },
|
||||
[&](float, float u) { return alpha(gold, 0.85f * smooth(0.0f, 0.35f, u)); });
|
||||
P u, w;
|
||||
basis(norm(E - b), u, w);
|
||||
disc(out, b, u, w, 0.018f, alpha(C{1, 0.95f, 0.85f, 1}, 0.9f), alpha(gold, 0), 16);
|
||||
}
|
||||
|
||||
if (!g.aiming || g.arc.size() < 6) return;
|
||||
std::vector<P> raw;
|
||||
for (size_t i = 0; i + 2 < g.arc.size(); i += 3) raw.push_back({g.arc[i], g.arc[i + 1], g.arc[i + 2]});
|
||||
const std::vector<P> pts = resample(raw, 0.03f);
|
||||
const C tip = g.seat ? gold : ice;
|
||||
// The arc: a soft glow under a bright core, both transparent at the hand (so it grows out of the controller
|
||||
// rather than starting in your face) and, when it lands, dashes flowing toward the target. Aimed nowhere it is
|
||||
// dim red and still.
|
||||
auto core = [&](float s, float u) {
|
||||
const float hand = smooth(0.0f, 0.35f, s);
|
||||
if (!g.valid) return alpha(red, 0.55f * hand * appear * (1 - 0.6f * smooth(0.7f, 1.0f, u)));
|
||||
const float d = 0.5f + 0.5f * std::cos(6.2831853f * (s - t * 1.4f) / 0.22f); // a dash every 22 cm, 1.4 m/s
|
||||
const float dash = 0.4f + 0.6f * smooth(0.25f, 0.75f, d);
|
||||
return alpha(mix(cyan, tip, smooth(0.2f, 1.0f, u)), 0.95f * hand * dash * appear);
|
||||
};
|
||||
ribbon(out, pts, E, [](float, float u) { return 0.030f + 0.012f * u; },
|
||||
[&](float s, float u) { const C c = core(s, u); return alpha(c, 0.28f); });
|
||||
ribbon(out, pts, E, [](float, float u) { return 0.009f + 0.004f * u; }, core);
|
||||
if (!g.valid) return;
|
||||
|
||||
// The landing marker, flat on the surface: a pulsing ring, a faint disc, a ripple spreading out every second,
|
||||
// and a chevron pointing the way you're going.
|
||||
const P n = norm(P{g.normal[0], g.normal[1], g.normal[2]});
|
||||
const P c = P{g.target[0], g.target[1], g.target[2]} + n * 0.012f;
|
||||
P u, w;
|
||||
basis(n, u, w);
|
||||
const float R = 0.26f * (1.0f + 0.035f * pulse);
|
||||
ring(out, c, u, w, R - 0.022f, R + 0.022f, alpha(tip, 0.9f * appear));
|
||||
disc(out, c, u, w, R - 0.02f, alpha(tip, 0.20f * appear), alpha(cyan, 0.08f * appear));
|
||||
const float ph = std::fmod(t, 1.0f);
|
||||
ring(out, c, u, w, R + 0.1f * ph - 0.012f, R + 0.1f * ph + 0.012f, alpha(tip, 0.5f * (1 - ph) * appear));
|
||||
// which way: from the hand toward the target, along the surface
|
||||
P f = P{g.target[0], g.target[1], g.target[2]} - raw.front();
|
||||
f = f - n * dot(f, n);
|
||||
if (dot(f, f) > 1e-6f) {
|
||||
f = norm(f);
|
||||
chevron(out, c + f * (R - 0.06f), f, cross(n, f), 0.08f, 0.026f, alpha(tip, 0.85f * appear));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
// GENERATED by tools/guide/gen_guide_spirv.sh from tools/guide/guide.{vert,frag}. Do not edit by hand.
|
||||
// vert: in vec3 position @0, vec4 color @1; push { mat4 clipFromWorld[2]; }; spec 0 = alpha scale
|
||||
// frag: out = color
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
static const uint32_t kGuideVertSpirv[] = {
|
||||
0x07230203,0x00010300,0x000d000b,0x00000037,0x00000000,0x00020011,0x00000001,0x00020011,
|
||||
0x00001157,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,
|
||||
0x00000000,0x00000001,0x000a000f,0x00000000,0x00000004,0x6e69616d,0x00000000,0x0000000d,
|
||||
0x00000017,0x0000001e,0x00000028,0x0000002a,0x00030047,0x0000000b,0x00000002,0x00050048,
|
||||
0x0000000b,0x00000000,0x0000000b,0x00000000,0x00050048,0x0000000b,0x00000001,0x0000000b,
|
||||
0x00000001,0x00050048,0x0000000b,0x00000002,0x0000000b,0x00000003,0x00050048,0x0000000b,
|
||||
0x00000003,0x0000000b,0x00000004,0x00040047,0x00000012,0x00000006,0x00000040,0x00030047,
|
||||
0x00000013,0x00000002,0x00040048,0x00000013,0x00000000,0x00000005,0x00050048,0x00000013,
|
||||
0x00000000,0x00000007,0x00000010,0x00050048,0x00000013,0x00000000,0x00000023,0x00000000,
|
||||
0x00040047,0x00000017,0x0000000b,0x00001158,0x00040047,0x0000001e,0x0000001e,0x00000000,
|
||||
0x00040047,0x00000028,0x0000001e,0x00000000,0x00040047,0x0000002a,0x0000001e,0x00000001,
|
||||
0x00040047,0x00000031,0x00000001,0x00000000,0x00020013,0x00000002,0x00030021,0x00000003,
|
||||
0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,0x00000006,0x00000004,
|
||||
0x00040015,0x00000008,0x00000020,0x00000000,0x0004002b,0x00000008,0x00000009,0x00000001,
|
||||
0x0004001c,0x0000000a,0x00000006,0x00000009,0x0006001e,0x0000000b,0x00000007,0x00000006,
|
||||
0x0000000a,0x0000000a,0x00040020,0x0000000c,0x00000003,0x0000000b,0x0004003b,0x0000000c,
|
||||
0x0000000d,0x00000003,0x00040015,0x0000000e,0x00000020,0x00000001,0x0004002b,0x0000000e,
|
||||
0x0000000f,0x00000000,0x00040018,0x00000010,0x00000007,0x00000004,0x0004002b,0x00000008,
|
||||
0x00000011,0x00000002,0x0004001c,0x00000012,0x00000010,0x00000011,0x0003001e,0x00000013,
|
||||
0x00000012,0x00040020,0x00000014,0x00000009,0x00000013,0x0004003b,0x00000014,0x00000015,
|
||||
0x00000009,0x00040020,0x00000016,0x00000001,0x0000000e,0x0004003b,0x00000016,0x00000017,
|
||||
0x00000001,0x00040020,0x00000019,0x00000009,0x00000010,0x00040017,0x0000001c,0x00000006,
|
||||
0x00000003,0x00040020,0x0000001d,0x00000001,0x0000001c,0x0004003b,0x0000001d,0x0000001e,
|
||||
0x00000001,0x0004002b,0x00000006,0x00000020,0x3f800000,0x00040020,0x00000026,0x00000003,
|
||||
0x00000007,0x0004003b,0x00000026,0x00000028,0x00000003,0x00040020,0x00000029,0x00000001,
|
||||
0x00000007,0x0004003b,0x00000029,0x0000002a,0x00000001,0x0004002b,0x00000008,0x0000002d,
|
||||
0x00000003,0x00040020,0x0000002e,0x00000001,0x00000006,0x00040032,0x00000006,0x00000031,
|
||||
0x3f800000,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,
|
||||
0x0004003d,0x0000000e,0x00000018,0x00000017,0x00060041,0x00000019,0x0000001a,0x00000015,
|
||||
0x0000000f,0x00000018,0x0004003d,0x00000010,0x0000001b,0x0000001a,0x0004003d,0x0000001c,
|
||||
0x0000001f,0x0000001e,0x00050051,0x00000006,0x00000021,0x0000001f,0x00000000,0x00050051,
|
||||
0x00000006,0x00000022,0x0000001f,0x00000001,0x00050051,0x00000006,0x00000023,0x0000001f,
|
||||
0x00000002,0x00070050,0x00000007,0x00000024,0x00000021,0x00000022,0x00000023,0x00000020,
|
||||
0x00050091,0x00000007,0x00000025,0x0000001b,0x00000024,0x00050041,0x00000026,0x00000027,
|
||||
0x0000000d,0x0000000f,0x0003003e,0x00000027,0x00000025,0x0004003d,0x00000007,0x0000002b,
|
||||
0x0000002a,0x00050041,0x0000002e,0x0000002f,0x0000002a,0x0000002d,0x0004003d,0x00000006,
|
||||
0x00000030,0x0000002f,0x00050085,0x00000006,0x00000032,0x00000030,0x00000031,0x00050051,
|
||||
0x00000006,0x00000033,0x0000002b,0x00000000,0x00050051,0x00000006,0x00000034,0x0000002b,
|
||||
0x00000001,0x00050051,0x00000006,0x00000035,0x0000002b,0x00000002,0x00070050,0x00000007,
|
||||
0x00000036,0x00000033,0x00000034,0x00000035,0x00000032,0x0003003e,0x00000028,0x00000036,
|
||||
0x000100fd,0x00010038,
|
||||
};
|
||||
static const size_t kGuideVertSize = sizeof(kGuideVertSpirv);
|
||||
|
||||
static const uint32_t kGuideFragSpirv[] = {
|
||||
0x07230203,0x00010300,0x000d000b,0x0000000d,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
|
||||
0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x00000009,0x0000000b,0x00030010,
|
||||
0x00000004,0x00000007,0x00040047,0x00000009,0x0000001e,0x00000000,0x00040047,0x0000000b,
|
||||
0x0000001e,0x00000000,0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,0x00030016,
|
||||
0x00000006,0x00000020,0x00040017,0x00000007,0x00000006,0x00000004,0x00040020,0x00000008,
|
||||
0x00000003,0x00000007,0x0004003b,0x00000008,0x00000009,0x00000003,0x00040020,0x0000000a,
|
||||
0x00000001,0x00000007,0x0004003b,0x0000000a,0x0000000b,0x00000001,0x00050036,0x00000002,
|
||||
0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003d,0x00000007,0x0000000c,
|
||||
0x0000000b,0x0003003e,0x00000009,0x0000000c,0x000100fd,0x00010038,
|
||||
};
|
||||
static const size_t kGuideFragSize = sizeof(kGuideFragSpirv);
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
#pragma once
|
||||
// Per-home edits made in Frame Studio (moved / deleted objects), saved as "<home>.apk.edits.json"
|
||||
// next to the home on the Frame. The home itself stays Meta's untouched APK.
|
||||
//
|
||||
// { "home": "theatre.apk",
|
||||
// "objects": [ { "index": 12, "name": "merged_sofa", "deleted": false,
|
||||
// "pivot": [16 floats], "world": [16 floats] } ] }
|
||||
//
|
||||
// `index` is the mesh's position in the loaded scene; `name` guards against a different home version.
|
||||
// `world` is the mesh's final world matrix; `pivot` is the editor's gizmo frame (only Studio uses it).
|
||||
//
|
||||
// A background sound added in Frame Studio, and how loud the home's own ambience plays (0: off):
|
||||
// "sound": { "file": "haven.apk.sound-1a2b3c4d.mp3", "name": "rain.mp3", "volume": 0.8, "homeVolume": 1 }
|
||||
// `file` sits next to this edits file (on the Frame beside the home; in a .fenv beside its edits.json): Ogg Vorbis,
|
||||
// Ogg Opus, WAV, MP3 or FLAC, looped.
|
||||
//
|
||||
// What the environment is called and its picture, as you set them in Frame Studio (the headset's Environments panel
|
||||
// shows them; a .fenv carries them as its name and thumbnail):
|
||||
// "title": "Rainy Haven", "image": "haven.apk.image-1a2b3c4d.jpg" (the image beside this file, like the sound)
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct ObjectEdit {
|
||||
int index = -1;
|
||||
std::string name;
|
||||
bool deleted = false;
|
||||
float pivot[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
float world[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
};
|
||||
|
||||
struct HomeSound {
|
||||
std::string file; // "" = no sound of its own
|
||||
std::string name; // what the file was called when it was added (shown in Studio)
|
||||
float volume = 0.8f; // 0..1
|
||||
float homeVolume = 1; // the home's own ambience, 0..1
|
||||
bool any() const { return !file.empty() || homeVolume != 1; }
|
||||
};
|
||||
|
||||
struct HomeEdits {
|
||||
std::string home;
|
||||
std::vector<ObjectEdit> objects;
|
||||
HomeSound sound;
|
||||
std::string title; // the environment's name ("" = the home's own)
|
||||
std::string image; // its picture, a file beside this one ("" = a view of it)
|
||||
bool empty() const { return objects.empty() && !sound.any() && title.empty() && image.empty(); }
|
||||
};
|
||||
|
||||
// Sound files a background sound can be.
|
||||
inline bool isSoundFile(std::string name) {
|
||||
for (char& c : name) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
for (const char* x : {".ogg", ".opus", ".oga", ".wav", ".mp3", ".flac"})
|
||||
if (name.size() > std::strlen(x) && name.compare(name.size() - std::strlen(x), std::strlen(x), x) == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// The sound file of edits read from `editsPath` (it sits beside it); "" when there's none.
|
||||
inline std::string soundPathFor(const std::string& editsPath, const HomeSound& s) {
|
||||
if (s.file.empty() || s.file.find_first_of("/\\") != std::string::npos || s.file == ".." || s.file == ".") return {};
|
||||
const size_t slash = editsPath.find_last_of("/\\");
|
||||
return (slash == std::string::npos ? std::string() : editsPath.substr(0, slash + 1)) + s.file;
|
||||
}
|
||||
|
||||
namespace home_edits_detail {
|
||||
|
||||
inline std::string quote(const std::string& s) {
|
||||
std::string o = "\"";
|
||||
for (char c : s) {
|
||||
if (c == '"' || c == '\\') { o += '\\'; o += c; }
|
||||
else if (static_cast<unsigned char>(c) < 0x20) { char b[8]; std::snprintf(b, sizeof b, "\\u%04x", c); o += b; }
|
||||
else o += c;
|
||||
}
|
||||
return o + "\"";
|
||||
}
|
||||
|
||||
// Minimal reader for the subset this file uses: objects, arrays, strings, numbers, booleans.
|
||||
struct Reader {
|
||||
const char* p;
|
||||
const char* end;
|
||||
void ws() { while (p < end && std::isspace(static_cast<unsigned char>(*p))) ++p; }
|
||||
bool eat(char c) { ws(); if (p < end && *p == c) { ++p; return true; } return false; }
|
||||
bool str(std::string& out) {
|
||||
ws();
|
||||
if (p >= end || *p != '"') return false;
|
||||
++p;
|
||||
out.clear();
|
||||
while (p < end && *p != '"') {
|
||||
if (*p == '\\' && p + 1 < end) {
|
||||
++p;
|
||||
if (*p == 'u' && p + 4 < end) { out += static_cast<char>(std::strtol(std::string(p + 1, 4).c_str(), nullptr, 16)); p += 5; continue; }
|
||||
out += *p == 'n' ? '\n' : *p == 't' ? '\t' : *p;
|
||||
++p;
|
||||
} else {
|
||||
out += *p++;
|
||||
}
|
||||
}
|
||||
return eat('"');
|
||||
}
|
||||
bool num(double& v) {
|
||||
ws();
|
||||
char* e = nullptr;
|
||||
v = std::strtod(p, &e);
|
||||
if (e == p) return false;
|
||||
p = e;
|
||||
return true;
|
||||
}
|
||||
bool boolean(bool& b) {
|
||||
ws();
|
||||
if (end - p >= 4 && std::strncmp(p, "true", 4) == 0) { b = true; p += 4; return true; }
|
||||
if (end - p >= 5 && std::strncmp(p, "false", 5) == 0) { b = false; p += 5; return true; }
|
||||
return false;
|
||||
}
|
||||
bool floats(float* out, int n) {
|
||||
if (!eat('[')) return false;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double v;
|
||||
if (!num(v)) return false;
|
||||
out[i] = static_cast<float>(v);
|
||||
if (i + 1 < n && !eat(',')) return false;
|
||||
}
|
||||
return eat(']');
|
||||
}
|
||||
// Skips any value (for keys this version does not know). Depth-capped: a hostile file of nested '['
|
||||
// would otherwise recurse until the stack overflows.
|
||||
bool skip(int depth = 0) {
|
||||
ws();
|
||||
if (p >= end || depth > 256) return false;
|
||||
if (*p == '"') { std::string s; return str(s); }
|
||||
if (*p == '{' || *p == '[') {
|
||||
const char close = *p == '{' ? '}' : ']';
|
||||
++p;
|
||||
if (eat(close)) return true;
|
||||
do {
|
||||
if (close == '}') { std::string k; if (!str(k) || !eat(':')) return false; }
|
||||
if (!skip(depth + 1)) return false;
|
||||
} while (eat(','));
|
||||
return eat(close);
|
||||
}
|
||||
bool b; double d;
|
||||
return boolean(b) || num(d) || (end - p >= 4 && std::strncmp(p, "null", 4) == 0 && (p += 4, true));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace home_edits_detail
|
||||
|
||||
inline std::string editsPathFor(const std::string& homePath) { return homePath + ".edits.json"; }
|
||||
// Frame Studio's world layer for a home (spawns, seats, walls, walkable areas: core/world.h), next to it.
|
||||
inline std::string worldPathFor(const std::string& homePath) { return homePath + ".world.json"; }
|
||||
|
||||
inline bool saveHomeEdits(const std::string& path, const HomeEdits& e) {
|
||||
using home_edits_detail::quote;
|
||||
std::ostringstream o;
|
||||
o.precision(9);
|
||||
auto mat = [&](const float* m) {
|
||||
o << "[";
|
||||
for (int i = 0; i < 16; ++i) o << (i ? ", " : "") << m[i];
|
||||
o << "]";
|
||||
};
|
||||
o << "{\n \"home\": " << quote(e.home) << ",\n \"objects\": [";
|
||||
for (size_t i = 0; i < e.objects.size(); ++i) {
|
||||
const ObjectEdit& x = e.objects[i];
|
||||
o << (i ? ",\n" : "\n") << " { \"index\": " << x.index << ", \"name\": " << quote(x.name)
|
||||
<< ", \"deleted\": " << (x.deleted ? "true" : "false") << ",\n \"pivot\": ";
|
||||
mat(x.pivot);
|
||||
o << ",\n \"world\": ";
|
||||
mat(x.world);
|
||||
o << " }";
|
||||
}
|
||||
o << "\n ]";
|
||||
if (e.sound.any())
|
||||
o << ",\n \"sound\": { \"file\": " << quote(e.sound.file) << ", \"name\": " << quote(e.sound.name) << ", \"volume\": " << e.sound.volume << ", \"homeVolume\": " << e.sound.homeVolume << " }";
|
||||
if (!e.title.empty()) o << ",\n \"title\": " << quote(e.title);
|
||||
if (!e.image.empty()) o << ",\n \"image\": " << quote(e.image);
|
||||
o << "\n}\n";
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f) return false;
|
||||
f << o.str();
|
||||
return static_cast<bool>(f);
|
||||
}
|
||||
|
||||
inline bool loadHomeEdits(const std::string& path, HomeEdits& out) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) return false;
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
const std::string text = ss.str();
|
||||
home_edits_detail::Reader r{text.data(), text.data() + text.size()};
|
||||
out = HomeEdits{};
|
||||
if (!r.eat('{')) return false;
|
||||
if (r.eat('}')) return true;
|
||||
do {
|
||||
std::string key;
|
||||
if (!r.str(key) || !r.eat(':')) return false;
|
||||
if (key == "home") {
|
||||
if (!r.str(out.home)) return false;
|
||||
} else if (key == "title") {
|
||||
if (!r.str(out.title)) return false;
|
||||
} else if (key == "image") {
|
||||
if (!r.str(out.image)) return false;
|
||||
} else if (key == "objects") {
|
||||
if (!r.eat('[')) return false;
|
||||
if (r.eat(']')) continue;
|
||||
do {
|
||||
ObjectEdit x;
|
||||
if (!r.eat('{')) return false;
|
||||
do {
|
||||
std::string k;
|
||||
if (!r.str(k) || !r.eat(':')) return false;
|
||||
double d;
|
||||
bool ok = k == "index" ? (r.num(d) && (x.index = static_cast<int>(d), true))
|
||||
: k == "name" ? r.str(x.name)
|
||||
: k == "deleted" ? r.boolean(x.deleted)
|
||||
: k == "pivot" ? r.floats(x.pivot, 16)
|
||||
: k == "world" ? r.floats(x.world, 16)
|
||||
: r.skip();
|
||||
if (!ok) return false;
|
||||
} while (r.eat(','));
|
||||
if (!r.eat('}')) return false;
|
||||
out.objects.push_back(x);
|
||||
} while (r.eat(','));
|
||||
if (!r.eat(']')) return false;
|
||||
} else if (key == "sound") {
|
||||
if (!r.eat('{')) return false;
|
||||
if (r.eat('}')) continue;
|
||||
do {
|
||||
std::string k;
|
||||
if (!r.str(k) || !r.eat(':')) return false;
|
||||
double d = 0;
|
||||
const bool ok = k == "file" ? r.str(out.sound.file)
|
||||
: k == "name" ? r.str(out.sound.name)
|
||||
: k == "volume" ? (r.num(d) && (out.sound.volume = static_cast<float>(std::clamp(d, 0.0, 1.0)), true))
|
||||
: k == "homeVolume" ? (r.num(d) && (out.sound.homeVolume = static_cast<float>(std::clamp(d, 0.0, 1.0)), true))
|
||||
: r.skip();
|
||||
if (!ok) return false;
|
||||
} while (r.eat(','));
|
||||
if (!r.eat('}')) return false;
|
||||
} else if (!r.skip()) {
|
||||
return false;
|
||||
}
|
||||
} while (r.eat(','));
|
||||
return r.eat('}');
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
#pragma once
|
||||
// A home's health report: what it's made of, what it costs and what's wrong with it, from the loaded scene alone
|
||||
// (no GPU). fenv-tool report prints it, Frame Studio shows it (Debug > Home Report), and Frame Home writes it to
|
||||
// ~/frame-home/report.json after each load for the Environments panel's Debug page.
|
||||
#include "hsl_scene.h"
|
||||
#include "walkgrid.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct HomeProblem {
|
||||
int level = 1; // 0 worth knowing, 1 costs speed or looks, 2 broken
|
||||
std::string what; // one line
|
||||
int mesh = -1; // the HslScene::meshes index it's about, if one
|
||||
};
|
||||
|
||||
struct HomeReport {
|
||||
std::string name, format;
|
||||
size_t meshes = 0, triangles = 0, vertices = 0, empty = 0;
|
||||
size_t seeThrough = 0, seeThroughTris = 0, cutout = 0, skinned = 0, animated = 0, lightmapped = 0, sky = 0, programs = 0;
|
||||
size_t textures = 0; // distinct
|
||||
double textureMB = 0, decodedMB = 0, geometryMB = 0; // compressed (as the GPU gets it when it can), decoded RGBA, vertices + indices
|
||||
size_t spawns = 0, seats = 0, hotspots = 0, colliders = 0, wallSpots = 0, gravity = 0;
|
||||
double walkArea = -1; // m², < 0: no walkable area known
|
||||
double loadSec = 0; // set by the caller
|
||||
struct Mesh { size_t tris; int mesh; };
|
||||
struct Tex { unsigned w, h, mips; double mb; size_t users; int mesh; };
|
||||
std::vector<Mesh> biggestMeshes; // most triangles first
|
||||
std::vector<Tex> biggestTextures; // most memory first
|
||||
std::vector<HomeProblem> problems; // worst first
|
||||
std::vector<std::string> meshNames; // by HslScene::meshes index (for the lists above)
|
||||
|
||||
int worst() const { int w = -1; for (const auto& p : problems) w = std::max(w, p.level); return w; }
|
||||
std::string meshName(int i) const { return i >= 0 && static_cast<size_t>(i) < meshNames.size() ? meshNames[i] : std::string(); }
|
||||
|
||||
std::string text() const {
|
||||
std::string o;
|
||||
char b[512];
|
||||
std::snprintf(b, sizeof b, "%s%s%s\n", name.c_str(), format.empty() ? "" : " · ", format.c_str());
|
||||
o += b;
|
||||
std::snprintf(b, sizeof b, "%zu meshes, %.2f M triangles, %.2f M vertices%s\n", meshes, triangles / 1e6, vertices / 1e6,
|
||||
loadSec > 0 ? ("; loaded in " + num(loadSec, 1) + " s").c_str() : "");
|
||||
o += b;
|
||||
std::snprintf(b, sizeof b, "see-through %zu (%.2f M triangles), cut-out %zu, skinned %zu, animated %zu, lightmapped %zu, sky %zu, %zu shaders\n",
|
||||
seeThrough, seeThroughTris / 1e6, cutout, skinned, animated, lightmapped, sky, programs);
|
||||
o += b;
|
||||
std::snprintf(b, sizeof b, "%zu textures: %.0f MB compressed, %.0f MB decoded; geometry %.0f MB\n", textures, textureMB, decodedMB, geometryMB);
|
||||
o += b;
|
||||
std::snprintf(b, sizeof b, "world: %zu spawns, %zu seats, %zu hotspots, %zu surfaces, %zu wall spots, %zu gravity zones", spawns, seats, hotspots,
|
||||
colliders, wallSpots, gravity);
|
||||
o += b;
|
||||
o += walkArea >= 0 ? "; walkable " + num(walkArea, 0) + " m²\n" : "\n";
|
||||
if (!biggestMeshes.empty()) o += "most triangles:\n";
|
||||
for (const Mesh& m : biggestMeshes) { std::snprintf(b, sizeof b, " %9zu %s\n", m.tris, meshName(m.mesh).c_str()); o += b; }
|
||||
if (!biggestTextures.empty()) o += "biggest textures:\n";
|
||||
for (const Tex& t : biggestTextures) {
|
||||
std::snprintf(b, sizeof b, " %6.1f MB %ux%u, %u mip%s, %zu mesh%s %s\n", t.mb, t.w, t.h, t.mips, t.mips == 1 ? "" : "s", t.users,
|
||||
t.users == 1 ? "" : "es", meshName(t.mesh).c_str());
|
||||
o += b;
|
||||
}
|
||||
if (problems.empty()) o += "no problems found\n";
|
||||
for (const HomeProblem& p : problems) o += std::string(p.level >= 2 ? "PROBLEM " : p.level == 1 ? "warning " : "note ") + p.what + "\n";
|
||||
return o;
|
||||
}
|
||||
std::string json() const {
|
||||
std::string o = "{\"name\":" + q(name) + ",\"format\":" + q(format);
|
||||
auto n = [&](const char* k, double v) { o += ",\""; o += k; o += "\":" + num(v, v == std::floor(v) ? 0 : 2); };
|
||||
n("meshes", meshes); n("triangles", triangles); n("vertices", vertices); n("seeThrough", seeThrough); n("seeThroughTris", seeThroughTris);
|
||||
n("cutout", cutout); n("skinned", skinned); n("animated", animated); n("lightmapped", lightmapped); n("programs", programs);
|
||||
n("textures", textures); n("textureMB", std::round(textureMB)); n("decodedMB", std::round(decodedMB)); n("geometryMB", std::round(geometryMB));
|
||||
n("spawns", spawns); n("seats", seats); n("colliders", colliders); n("wallSpots", wallSpots); n("walkArea", std::round(walkArea));
|
||||
n("loadSec", std::round(loadSec * 10) / 10);
|
||||
o += ",\"biggestMeshes\":[";
|
||||
for (size_t i = 0; i < biggestMeshes.size(); ++i) o += (i ? ",{\"name\":" : "{\"name\":") + q(meshName(biggestMeshes[i].mesh)) + ",\"tris\":" + num(biggestMeshes[i].tris, 0) + "}";
|
||||
o += "],\"problems\":[";
|
||||
for (size_t i = 0; i < problems.size(); ++i)
|
||||
o += (i ? ",{\"level\":" : "{\"level\":") + num(problems[i].level, 0) + ",\"what\":" + q(problems[i].what) + ",\"mesh\":" + q(meshName(problems[i].mesh)) + "}";
|
||||
return o + "]}\n";
|
||||
}
|
||||
|
||||
static std::string num(double v, int digits) { char b[64]; std::snprintf(b, sizeof b, "%.*f", digits, std::isfinite(v) ? v : 0.0); return b; }
|
||||
static std::string q(const std::string& s) {
|
||||
std::string o = "\"";
|
||||
for (unsigned char c : s) {
|
||||
if (c == '"' || c == '\\') { o += '\\'; o += static_cast<char>(c); }
|
||||
else if (c >= 0x20) o += static_cast<char>(c);
|
||||
}
|
||||
return o + "\"";
|
||||
}
|
||||
};
|
||||
|
||||
// `walk`: the world as Frame Home walks it (walkWorld), for the walkable area and the spawns on it; null to leave
|
||||
// those out.
|
||||
inline HomeReport makeHomeReport(const HslScene& s, const HomeWorld* walk = nullptr) {
|
||||
HomeReport r;
|
||||
if (!s.apkPaths.empty()) {
|
||||
std::string f = s.fenvPath.empty() ? s.apkPaths.front() : s.fenvPath;
|
||||
const size_t sl = f.find_last_of("/\\");
|
||||
r.name = sl == std::string::npos ? f : f.substr(sl + 1);
|
||||
}
|
||||
if (!s.sources.empty()) r.format = s.sources.front().format + (s.sources.front().role.empty() ? "" : " " + s.sources.front().role);
|
||||
r.meshes = s.meshes.size();
|
||||
r.programs = s.programs.size();
|
||||
r.animated = s.animatedMeshes.size();
|
||||
r.meshNames.reserve(s.meshes.size());
|
||||
// Textures: each compressed source once (meshes share them), decoded RGBA per mesh (or its share source).
|
||||
struct TexUse { unsigned w, h, mips; double mb; size_t users; int mesh; };
|
||||
std::map<const void*, TexUse> tex;
|
||||
auto useSrc = [&](const std::shared_ptr<const TexSource>& t, int mesh) {
|
||||
if (!t) return;
|
||||
auto [it, fresh] = tex.try_emplace(t.get(), TexUse{t->w, t->h, t->mips, t->astc.size() / 1048576.0, 0, mesh});
|
||||
++it->second.users;
|
||||
if (fresh) r.textureMB += it->second.mb;
|
||||
};
|
||||
auto useRgba = [&](const std::vector<u8>& px, unsigned w, unsigned h, int mesh) {
|
||||
if (px.empty()) return;
|
||||
auto [it, fresh] = tex.try_emplace(px.data(), TexUse{w, h, 1, px.size() / 1048576.0, 0, mesh});
|
||||
++it->second.users;
|
||||
if (fresh) r.decodedMB += it->second.mb;
|
||||
};
|
||||
size_t nanMeshes = 0, badIndexMeshes = 0, degenerateMeshes = 0;
|
||||
for (size_t i = 0; i < s.meshes.size(); ++i) {
|
||||
const MeshData& m = s.meshes[i];
|
||||
const int mi = static_cast<int>(i);
|
||||
r.meshNames.push_back(m.name.empty() ? "(unnamed " + std::to_string(i) + ")" : m.name);
|
||||
const size_t tris = m.indices.size() / 3, verts = m.positions.size() / 3;
|
||||
r.triangles += tris;
|
||||
r.vertices += verts;
|
||||
if (!tris) ++r.empty;
|
||||
r.geometryMB += ((m.positions.size() + m.uvs.size() + m.uvs3.size() + m.uvs4.size()) * 4 + m.indices.size() * 4 + m.colors.size()) / 1048576.0;
|
||||
if (m.useBlend || m.additive) { ++r.seeThrough; r.seeThroughTris += tris; }
|
||||
r.cutout += m.alphaTest;
|
||||
r.skinned += !m.boneIndices.empty();
|
||||
r.lightmapped += m.hasLmHdr() || m.hasLightmap || m.lmLdrSrc != nullptr;
|
||||
r.sky += m.isSkybox;
|
||||
r.biggestMeshes.push_back({tris, mi});
|
||||
for (const auto* t : {&m.texSrc, &m.normalSrc, &m.ormSrc, &m.emissiveSrc, &m.lmSrc, &m.lmLdrSrc, &m.cubeSrc}) useSrc(*t, mi);
|
||||
for (const auto& nt : m.namedTex) useSrc(nt.second, mi);
|
||||
for (const auto& nh : m.namedHdr) useSrc(nh.second.src, mi);
|
||||
if (m.texShareSrc < 0) {
|
||||
if (!m.texSrc) useRgba(m.texRGBA, m.texW, m.texH, mi);
|
||||
useRgba(m.normalRGBA, m.normalW, m.normalH, mi);
|
||||
useRgba(m.ormRGBA, m.ormW, m.ormH, mi);
|
||||
useRgba(m.emissiveRGBA, m.emissiveW, m.emissiveH, mi);
|
||||
useRgba(m.lmRGBA, m.lmW, m.lmH, mi);
|
||||
}
|
||||
// Broken geometry: positions that aren't numbers, indices past the vertices, triangles with no area.
|
||||
bool nan = false;
|
||||
for (float v : m.positions) if (!std::isfinite(v)) { nan = true; break; }
|
||||
bool badIndex = false;
|
||||
size_t degenerate = 0;
|
||||
for (size_t t = 0; t + 2 < m.indices.size(); t += 3) {
|
||||
const u32 a = m.indices[t], b = m.indices[t + 1], c = m.indices[t + 2];
|
||||
if (a >= verts || b >= verts || c >= verts) { badIndex = true; break; }
|
||||
if (a == b || b == c || a == c) { ++degenerate; continue; }
|
||||
const float* pa = &m.positions[a * 3]; const float* pb = &m.positions[b * 3]; const float* pc = &m.positions[c * 3];
|
||||
const float ux = pb[0] - pa[0], uy = pb[1] - pa[1], uz = pb[2] - pa[2], vx = pc[0] - pa[0], vy = pc[1] - pa[1], vz = pc[2] - pa[2];
|
||||
const float cx = uy * vz - uz * vy, cy = uz * vx - ux * vz, cz = ux * vy - uy * vx;
|
||||
if (cx * cx + cy * cy + cz * cz < 1e-18f) ++degenerate;
|
||||
}
|
||||
if (nan && nanMeshes++ < 5) r.problems.push_back({2, "Positions that aren't numbers in " + r.meshNames.back() + ": it may not draw, or draw as spikes", mi});
|
||||
if (badIndex && badIndexMeshes++ < 5) r.problems.push_back({2, "Triangles pointing past the vertices in " + r.meshNames.back() + ": it can't be drawn safely", mi});
|
||||
if (tris >= 1000 && degenerate * 5 > tris && degenerateMeshes++ < 3)
|
||||
r.problems.push_back({0, r.meshNames.back() + ": " + std::to_string(degenerate * 100 / tris) + "% of its triangles have no area (wasted work)", mi});
|
||||
if (tris > 500000)
|
||||
r.problems.push_back({1, r.meshNames.back() + " alone has " + HomeReport::num(tris / 1e6, 2) + " M triangles", mi});
|
||||
if ((m.useBlend || m.additive) && tris > 60000)
|
||||
r.problems.push_back({1, "See-through " + r.meshNames.back() + " has " + std::to_string(tris / 1000) + " k triangles: blended surfaces are drawn back to front, every layer paid for", mi});
|
||||
}
|
||||
r.textures = tex.size();
|
||||
// Textures the GPU can't sample well, one line per kind: the count, and the biggest of them.
|
||||
size_t huge = 0, noMips = 0;
|
||||
const TexUse* hugeTop = nullptr;
|
||||
const TexUse* noMipsTop = nullptr;
|
||||
std::map<const void*, bool> base; // compressed base textures (the ones that get their mips made)
|
||||
for (const MeshData& m : s.meshes) if (m.texSrc) base[m.texSrc.get()] = true;
|
||||
for (const auto& kv : tex) {
|
||||
const TexUse& t = kv.second;
|
||||
r.biggestTextures.push_back({t.w, t.h, t.mips, t.mb, t.users, t.mesh});
|
||||
const unsigned side = std::max(t.w, t.h);
|
||||
if (side > 4096) { ++huge; if (!hugeTop || t.mb > hugeTop->mb) hugeTop = &t; }
|
||||
if (t.mips <= 1 && side > 256 && base.count(kv.first)) { ++noMips; if (!noMipsTop || t.mb > noMipsTop->mb) noMipsTop = &t; }
|
||||
}
|
||||
auto dims = [](const TexUse* t) { return std::to_string(t->w) + "x" + std::to_string(t->h); };
|
||||
if (noMips)
|
||||
r.problems.push_back({1, std::to_string(noMips) + (noMips == 1 ? " texture has" : " textures have") + " no smaller versions (the biggest " +
|
||||
dims(noMipsTop) + ", " + r.meshName(noMipsTop->mesh) + "): Frame Home makes them at load, which loads slower",
|
||||
noMipsTop->mesh});
|
||||
if (huge)
|
||||
r.problems.push_back({0, std::to_string(huge) + (huge == 1 ? " texture is" : " textures are") + " bigger than 4096 (" + dims(hugeTop) + ", " +
|
||||
r.meshName(hugeTop->mesh) + "): GPUs that can't read them compressed get them at 2048", hugeTop->mesh});
|
||||
std::sort(r.biggestMeshes.begin(), r.biggestMeshes.end(), [](const auto& a, const auto& b) { return a.tris > b.tris; });
|
||||
if (r.biggestMeshes.size() > 8) r.biggestMeshes.resize(8);
|
||||
std::sort(r.biggestTextures.begin(), r.biggestTextures.end(), [](const auto& a, const auto& b) { return a.mb > b.mb; });
|
||||
if (r.biggestTextures.size() > 8) r.biggestTextures.resize(8);
|
||||
// The whole home.
|
||||
if (r.triangles > 1500000)
|
||||
r.problems.push_back({1, HomeReport::num(r.triangles / 1e6, 1) + " M triangles in all: the Frame keeps its frame rate best under about 1 M in view"});
|
||||
if (r.meshes > 2500) r.problems.push_back({1, std::to_string(r.meshes) + " separate meshes: each one in view is a draw, and draws cost CPU time every frame"});
|
||||
if (r.seeThroughTris > 300000)
|
||||
r.problems.push_back({1, HomeReport::num(r.seeThroughTris / 1e6, 2) + " M see-through triangles: overdraw is likely what's slow (try Debug view: Overdraw)"});
|
||||
if (r.textureMB + r.decodedMB > 1500)
|
||||
r.problems.push_back({1, HomeReport::num(r.textureMB + r.decodedMB, 0) + " MB of textures: loads slowly and leaves little memory for the apps in the Steam menu"});
|
||||
if (r.empty) r.problems.push_back({0, std::to_string(r.empty) + " meshes with no triangles"});
|
||||
// The world.
|
||||
const HomeWorld& w = walk ? *walk : s.world;
|
||||
r.spawns = w.spawns.size(); r.seats = w.seats.size(); r.hotspots = w.hotspots.size(); r.colliders = w.colliders.size();
|
||||
r.wallSpots = w.wallSpots.size(); r.gravity = w.gravity.size();
|
||||
const WalkGrid* g = walk ? walk->walkGridCache.get() : nullptr;
|
||||
float floorY = 0;
|
||||
if (w.spawns.empty() && w.gravity.empty() && (!g || !g->floorAt(0, 0, 0, 0.5f, 1.0f, floorY)))
|
||||
r.problems.push_back({g ? 1 : 0, "No spawn point: you start at the home's centre, which isn't on walkable floor"});
|
||||
if (g) {
|
||||
r.walkArea = g->walkArea;
|
||||
if (w.gravity.empty() && g->walkArea < 2) r.problems.push_back({2, "Almost nowhere to walk (" + HomeReport::num(g->walkArea, 1) + " m²): the floor wasn't found"});
|
||||
for (size_t i = 0; i < w.spawns.size() && w.gravity.empty(); ++i) {
|
||||
const WorldPoint& sp = w.spawns[i];
|
||||
float h = 0;
|
||||
if (!g->floorAt(sp.pos[0], sp.pos[2], sp.pos[1], 0.5f, 1.0f, h))
|
||||
r.problems.push_back({1, "Spawn " + (sp.name.empty() ? std::to_string(i) : sp.name) + " isn't on walkable floor: you may start stuck or falling"});
|
||||
}
|
||||
}
|
||||
std::stable_sort(r.problems.begin(), r.problems.end(), [](const HomeProblem& a, const HomeProblem& b) { return a.level > b.level; });
|
||||
return r;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,548 @@
|
||||
#pragma once
|
||||
// Stereo Vulkan renderer for Meta HSL homes that runs each home's OWN shaders (RENDSHAD forward pass).
|
||||
// Every resource a shader declares is bound by its SPIR-V name, the way libshell binds them:
|
||||
// set0 globalUniforms (both eyes' clipFromWorld / viewFromWorld / camera, time) + linearWrapSampler
|
||||
// set1 lighting: lightUniforms, sbLightItems, lightGridCells, IBL cubes, shadow map + compare sampler
|
||||
// set2 per material: matParams (cooked constants bound by MurmurHash3 name), lightprobesParams,
|
||||
// lightBakerParams, instance, skinning, samplers, and textures routed by name
|
||||
// Both eyes render in ONE multiview pass into a 2-layer target, then go to vrcompositor as layers 0/1.
|
||||
#include <unordered_set>
|
||||
#include "guide_geom.h"
|
||||
#include "hsl_scene.h"
|
||||
#include "shader_bake.h"
|
||||
#include "spv_reflect.h"
|
||||
|
||||
#ifdef FH_USE_VOLK
|
||||
#include <volk.h> // Windows: Vulkan entry points loaded at runtime (no SDK needed)
|
||||
#else
|
||||
#include <vulkan/vulkan.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
struct Mat4 { float m[16]; }; // column-major
|
||||
|
||||
// Largest texture side decoded for GPUs without native ASTC (Frame Studio's setting; 0 = no limit). Applies to
|
||||
// textures decoded from then on.
|
||||
void setTextureCap(uint32_t side);
|
||||
|
||||
struct EyeView {
|
||||
Mat4 proj; // clip-from-view (Vulkan, reversed-Z)
|
||||
Mat4 view; // view-from-world
|
||||
float position[3]; // eye position in world
|
||||
};
|
||||
|
||||
class HslRenderer {
|
||||
public:
|
||||
// Device setup on the GPU and with the extensions SteamVR requires.
|
||||
bool init(uint32_t eyeWidth, uint32_t eyeHeight);
|
||||
// Uploads a home (replacing any previous one).
|
||||
bool load(const HslScene& scene);
|
||||
// Incremental load: beginLoad, then loadStep(ms) once per frame until it returns true.
|
||||
void beginLoad(const HslScene& scene);
|
||||
bool loadStep(double budgetMs);
|
||||
float loadProgress() const;
|
||||
void unload();
|
||||
// Renders both eyes; the result is left in `colorImage()` layers 0 (left) / 1 (right), TRANSFER_SRC layout.
|
||||
void render(const EyeView eyes[2], float timeSec);
|
||||
// Copies one eye layer of the last frame to RGBA8 (sRGB-encoded) — headless snapshots / tests.
|
||||
bool readback(uint32_t layer, std::vector<u8>& rgba);
|
||||
void shutdown();
|
||||
// New eye size (window viewports); programs, pipelines and the loaded home are kept.
|
||||
void resize(uint32_t eyeWidth, uint32_t eyeHeight);
|
||||
// UNORM 2D view of one eye layer: the frame's sRGB bytes as-is, for display in a UI.
|
||||
VkImageView layerView(uint32_t layer) const { return layerViews_[layer]; }
|
||||
|
||||
// Texture streaming: deferred (still-compressed) textures are uploaded as ASTC when the GPU samples it,
|
||||
// otherwise decoded on background threads and swapped in a few per frame (render() does this).
|
||||
size_t texturesPending();
|
||||
// The previous frame is still on the GPU: a UI that skips this frame instead of calling render() never
|
||||
// blocks (WebGPU never makes HSR Studio's page wait either).
|
||||
bool gpuBusy() const;
|
||||
// Load-time: decode and upload every texture of the home now (not as objects come into view), so moving and
|
||||
// looking around never waits on a texture. Progress 0..1; render() uploads with a larger budget meanwhile.
|
||||
void requestAllTextures();
|
||||
float textureProgress();
|
||||
size_t drawnCount() const { return drawn_; } // meshes that passed culling last frame
|
||||
void finishStreaming(); // blocks until every queued texture is in (snapshots)
|
||||
|
||||
// Editing: the meshes that were uploaded (draw order), each tied to its HslScene::meshes index.
|
||||
size_t meshCount() const { return meshes_.size(); }
|
||||
int meshSceneIndex(size_t i) const { return meshes_[i].sceneIndex; }
|
||||
const float* meshWorld(size_t i) const { return meshes_[i].world; }
|
||||
void setMeshWorld(size_t i, const float world[16]);
|
||||
bool meshVisible(size_t i) const { return meshes_[i].visible; }
|
||||
int meshBlend(size_t i) const { return meshes_[i].blend; } // 0 opaque, 1 alpha, 2 additive (as drawn)
|
||||
bool meshDynamic(size_t i) const { return meshes_[i].dynamic; }
|
||||
// The home isn't on screen (faded out, nobody looking): textures stream in big steps.
|
||||
void setUploadsUnseen(bool unseen) { uploadsUnseen_ = unseen; }
|
||||
void setMeshVisible(size_t i, bool visible) { meshes_[i].visible = visible; }
|
||||
// Scene meshes whose vertices change every frame (HslScene::animatedMeshes). Set before beginLoad().
|
||||
void setDynamicMeshes(const std::vector<int>& sceneIndices);
|
||||
// Highlight one mesh (its faces tinted, every triangle edge drawn) on the GPU; -1 clears. Built once per call,
|
||||
// nothing per frame on the CPU.
|
||||
void setSelection(int mesh);
|
||||
// Guide geometry: world-space triangles drawn over the home, unlit and alpha blended; depth tested so walls hide
|
||||
// them, with a faint copy where they do. False from guideAvailable(): the pipelines couldn't be built (draws nothing).
|
||||
// Two kinds, both drawn every frame until replaced ({} clears):
|
||||
// - setGuide: what changes from frame to frame (guide_geom.h: the teleport arc, landing and seat markers, the
|
||||
// vignette). Copied into a host-visible buffer each frame, so keep it small.
|
||||
// - setStaticGuide: what stays put for many frames (the walkable area, debug_geom.h's world layer). Uploaded once
|
||||
// into video memory. Waits for the GPU when it replaces geometry (a hitch), so not for every frame.
|
||||
void setGuide(const std::vector<GuideVertex>& tris) { guideVerts_ = tris; }
|
||||
void setStaticGuide(const std::vector<GuideVertex>& tris);
|
||||
// System mode: frames recorded while the GPU finishes the one before (see render()). FH_NO_EARLY_RECORD=1: never.
|
||||
void setEarlyRecord(bool on) { earlyRecord_ = on; }
|
||||
bool guideAvailable() const { return guide_.visible != VK_NULL_HANDLE; }
|
||||
const std::string& meshProgram(size_t i) const { return programs_[meshes_[i].program].path; }
|
||||
|
||||
// Debug views (Frame Studio's View > Debug View, the Environments panel's Debug page): how a home is put together
|
||||
// and what it costs, drawn from each mesh's positions alone (skinned meshes in their rest pose).
|
||||
// Wireframe every triangle edge over the home (see-through meshes blue, cut-out green)
|
||||
// Meshes each mesh in its own colour, shaded by its slope (no textures or lighting)
|
||||
// Cost the home tinted by what each mesh costs the GPU: green from costScaleMs / 50 down, yellow, red from
|
||||
// costScaleMs up. Timed per draw on desktop GPUs; on tile-based ones (the Frame) by drawing each mesh
|
||||
// twice in turn and timing what that adds, so a full round takes a couple of seconds
|
||||
// (measured per draw, which slows the GPU a little while it's on)
|
||||
// Overdraw every mesh added up, nothing hidden: bright where many surfaces cover the same pixels
|
||||
enum DebugView { kViewNormal = 0, kViewWireframe, kViewMeshes, kViewCost, kViewOverdraw, kDebugViews };
|
||||
void setDebugView(int view);
|
||||
int debugView() const { return debugView_; }
|
||||
bool wireframeAvailable() const { return wireOk_; } // the GPU draws lines (fillModeNonSolid)
|
||||
// Leaves parts of the home out, to find the part that's slow or wrong (bits, applies from the next frame).
|
||||
enum DebugHide : unsigned { kHideSeeThrough = 1, kHideCutout = 2, kHideSky = 4, kHideAnimated = 8, kHideSolid = 16 };
|
||||
unsigned debugHide = 0;
|
||||
float costScaleMs = 0.25f;
|
||||
// Cost view: a mesh's GPU time (ms, smoothed), by draw index; < 0 not measured.
|
||||
float meshCostMs(size_t i) const { return i < cost_.size() ? cost_[i] : -1.0f; }
|
||||
// The n costliest meshes as last measured: (ms, HslScene::meshes index), costliest first.
|
||||
std::vector<std::pair<float, int>> costliest(size_t n) const;
|
||||
|
||||
VkInstance instance() const { return instance_; }
|
||||
VkPhysicalDevice physicalDevice() const { return physical_; }
|
||||
VkDevice device() const { return device_; }
|
||||
VkQueue queue() const { return queue_; }
|
||||
uint32_t queueFamily() const { return queueFamily_; }
|
||||
VkImage colorImage() const { return resolve_.image; }
|
||||
// Frame Home as the system environment (env_share.h): n images like the eye images that another process (the
|
||||
// compositor) can import, as opaque fds it takes ownership of. Recreate after resize(). render() copies the eye
|
||||
// images into exportSlot (when >= 0) at the end of the frame; waitFrame() blocks until that frame is done.
|
||||
struct ExportImage { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; VkDeviceSize size = 0; int fd = -1; };
|
||||
bool createExportImages(uint32_t n, std::vector<ExportImage>& out, uint32_t& memoryType, uint8_t deviceUUID[16]);
|
||||
// Set before init(): frames are drawn straight into the export images (their MSAA resolve lands there) instead of
|
||||
// being copied in at the end of the frame (1.1-1.5 ms a frame on the Frame). Foveation then redraws its density
|
||||
// map where you look each frame rather than offsetting it (the images can't carry the offset flag across).
|
||||
bool exportDirect = false;
|
||||
// Set before init(), with exportDirect: with smooth foveation the export images are the foveated pass's own
|
||||
// subsampled output, and the compositor layer reads them through a subsampled sampler, which smooths the coarse
|
||||
// regions as it samples. That replaces our full-screen smoothing pass (~0.65 ms a frame at 2416x2416 per eye).
|
||||
bool exportSubsampled = false;
|
||||
bool exportsSubsampled() const { return exportDirect && exportSubsampled && smoothFov_; } // (known after init())
|
||||
uint32_t exportUsage() const; // the export images' usage flags (the layer imports them alike)
|
||||
uint32_t exportCreateFlags() const; // ...and their VkImageCreateFlags (likewise)
|
||||
void destroyExportImages(std::vector<ExportImage>& images);
|
||||
int exportSlot = -1;
|
||||
void waitFrame();
|
||||
// Frames counted on the GPU (a timeline semaphore, when the device has them): render() signals frameSerial() once
|
||||
// that frame is done, and waitFrameSerial() may be called from another thread (system mode publishes from one).
|
||||
bool hasFrameSerial() const { return timeline_ != VK_NULL_HANDLE; }
|
||||
uint64_t frameSerial() const { return serial_; }
|
||||
bool waitFrameSerial(uint64_t serial, uint64_t timeoutNs);
|
||||
// The GPU was reset under this device (a hang anywhere on the GPU can take it): render() does nothing from then
|
||||
// on, and only a new device (Frame Home restarts itself) draws again.
|
||||
bool deviceLost() const { return lost_; }
|
||||
uint32_t width() const { return width_; }
|
||||
uint32_t height() const { return height_; }
|
||||
|
||||
float clearColor[3] = {0, 0, 0}; // linear; shows wherever the home draws nothing
|
||||
std::string pipelineCachePath; // compiled pipelines saved here between runs (empty = no file)
|
||||
uint32_t viewCount = 2; // set before init(): 2 = both eyes (VR), 1 = a single view (editor, half the work)
|
||||
// Performance options, set before init() (Frame Home / Studio settings):
|
||||
int msaa = 4; // samples: 1, 2, 4 or 8 (the largest the GPU has up to this)
|
||||
float anisotropy = 4; // texture filtering, 1..16 (clamped to the GPU's limit)
|
||||
// Foveated rendering: full detail where you look, coarser toward the edges (VK_EXT_fragment_density_map with
|
||||
// VK_QCOM_fragment_density_map_offset to move it each frame). Used only when the GPU has both.
|
||||
bool foveation = false;
|
||||
// Strength 1 (gentle) .. 3 (strong); where each eye looks, in pixels of its image (from eye tracking, or the
|
||||
// lens centre). Both take effect from the next frame.
|
||||
void setFoveationLevel(int level);
|
||||
// The same on a continuous scale (0 = full detail everywhere, 1-3 = the levels): the rings move smoothly with it
|
||||
// (the thermal governor turns it a little at a time). Steps of 1/32.
|
||||
void setFoveationStrength(float strength);
|
||||
float foveationStrength() const { return fovStrength_; }
|
||||
void setFoveationCenter(uint32_t eye, float px, float py) { if (eye < 2) { fovCenter_[eye][0] = px; fovCenter_[eye][1] = py; } }
|
||||
bool foveationActive() const { return fdmOn_; }
|
||||
|
||||
// Provided by the caller (OpenVR knows which GPU and extensions the compositor needs).
|
||||
std::vector<std::string> instanceExtensions, deviceExtensions;
|
||||
VkPhysicalDevice (*pickPhysicalDevice)(VkInstance) = nullptr;
|
||||
|
||||
private:
|
||||
// memOffset/memSize/pooled: a range of a shared MemBlock (see allocMemory) rather than its own allocation.
|
||||
struct Buffer { VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; void* mapped = nullptr; VkDeviceSize size = 0;
|
||||
VkDeviceSize memOffset = 0, memSize = 0; bool pooled = false; };
|
||||
struct Image { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE;
|
||||
VkDeviceSize memOffset = 0, memSize = 0; bool pooled = false; };
|
||||
// Suballocation, as Dawn (HSR Studio's WebGPU) does it: thousands of small vkAllocateMemory calls make the
|
||||
// Windows video memory manager stall the queue for seconds. Buffers and images keep separate blocks.
|
||||
struct MemBlock { VkDeviceMemory mem = VK_NULL_HANDLE; VkDeviceSize size = 0; uint32_t type = 0; bool linear = false;
|
||||
void* mapped = nullptr; std::map<VkDeviceSize, VkDeviceSize> free; };
|
||||
struct MemRange { VkDeviceMemory mem = VK_NULL_HANDLE; VkDeviceSize offset = 0, size = 0; void* mapped = nullptr; bool pooled = false; };
|
||||
std::vector<MemBlock> blocks_;
|
||||
std::mutex allocMutex_;
|
||||
MemRange allocMemory(const VkMemoryRequirements& req, uint32_t type, bool linear, bool map);
|
||||
void freeMemory(VkDeviceMemory mem, VkDeviceSize offset, VkDeviceSize size, bool pooled, bool mapped);
|
||||
uint32_t hostMemoryType(uint32_t bits);
|
||||
|
||||
enum class Role { Pos, Normal, Uv0, Uv1, Color, BoneIdx, BoneWgt, Tangent, Zero, Uv2, Uv3 };
|
||||
struct VertexAttr { uint32_t location; VkFormat format; uint32_t offset, size; Role role; bool integer; };
|
||||
|
||||
struct Program {
|
||||
std::string path;
|
||||
VkShaderModule vs = VK_NULL_HANDLE, fs = VK_NULL_HANDLE;
|
||||
VkShaderModule fsAlpha = VK_NULL_HANDLE; // fs writing (a,a,a,a): its colour work left for the compiler to drop
|
||||
std::vector<VertexAttr> attrs;
|
||||
uint32_t stride = 0;
|
||||
std::vector<spv::Resource> sets[3]; // merged vert+frag, sorted by binding
|
||||
VkDescriptorSetLayout setLayouts[3] = {};
|
||||
VkPipelineLayout layout = VK_NULL_HANDLE;
|
||||
VkDescriptorSet set0 = VK_NULL_HANDLE, set1 = VK_NULL_HANDLE;
|
||||
uint32_t pushSize = 0;
|
||||
int pcWorld = -1, pcColor = -1, pcUvU = -1, pcUvV = -1; // push-constant member offsets
|
||||
std::map<uint32_t, VkPipeline> pipelines; // key: blend mode * 2 + cull
|
||||
bool multiview = false;
|
||||
bool lightBaker = false; // has lightBakerParams (lightmap atlas uvScale/uvOffset)
|
||||
bool clay = false; // a built-in Clay shader: fragment alpha = texture alpha x tint alpha
|
||||
// Specialized fragment shaders (spv_specialize.h): the uniform values constant for a material baked in. One per
|
||||
// distinct set of those values, shared by the meshes that have it; each with its own pipelines.
|
||||
struct Variant { VkShaderModule fs = VK_NULL_HANDLE, fsAlpha = VK_NULL_HANDLE; std::map<uint32_t, VkPipeline> pipelines; };
|
||||
const ShaderProgramSrc* src = nullptr; // the scene's (its baked variants)
|
||||
bool boned = false; // its vertex stage reads bone indices
|
||||
std::vector<std::pair<uint32_t, uint32_t>> foldBlocks; // (set, binding) of the blocks it reads constant members of
|
||||
std::vector<Variant> variants;
|
||||
std::map<uint64_t, int> variantFor; // hash of those blocks' bytes -> variant (-1: nothing folds)
|
||||
};
|
||||
|
||||
struct GpuMesh {
|
||||
int program = -1;
|
||||
bool cutOnce = false; // a cut-out drawn in one pass (its shader has no alpha-only form)
|
||||
int variant = -1; // its Program::variants entry (-1: the shader as shipped)
|
||||
int sceneIndex = -1;
|
||||
bool visible = true;
|
||||
Buffer vbo, ibo;
|
||||
uint32_t indexCount = 0;
|
||||
std::vector<Image> images;
|
||||
std::vector<Buffer> ubos;
|
||||
VkDescriptorSet set2 = VK_NULL_HANDLE;
|
||||
float world[16];
|
||||
float color[4] = {1, 1, 1, 1};
|
||||
int blend = 0; // 0 opaque, 1 alpha, 2 additive
|
||||
bool cull = false, skybox = false;
|
||||
float centroid[3] = {0, 0, 0};
|
||||
float localCentroid[3] = {0, 0, 0};
|
||||
float localMin[3] = {0, 0, 0}, localMax[3] = {0, 0, 0};
|
||||
float worldMin[3] = {0, 0, 0}, worldMax[3] = {0, 0, 0};
|
||||
bool inView = true; // passed the view-frustum test this frame
|
||||
std::vector<std::pair<const TexSource*, bool>> wants; // streamed textures still to request
|
||||
int skinUbo = -1; // index in ubos of the sbSkinningMatrices buffer
|
||||
bool alphaToCoverage = false; // cut-out foliage (SHAD pass state)
|
||||
bool dynamic = false; // animated on the CPU: host-visible vertices, rewritten per frame
|
||||
uint32_t posGen = 0, uvGen = 0; // the MeshData posGen / uvGen its vertex buffer holds
|
||||
uint32_t skinGen = 0; // the MeshData skinGen its skinning buffer holds
|
||||
float baseWorld[16]; // its own world; world = baseWorld * anim
|
||||
float anim[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; // MeshData::animXform last applied
|
||||
float uvXform[6] = {1, 0, 0, 0, 1, 0}; // MeshData::uvXform (pushed as uvTransformU / V)
|
||||
bool debug = false; // FH_DEBUG matched: cull state logged once
|
||||
std::string debugName;
|
||||
};
|
||||
|
||||
// Vulkan plumbing
|
||||
uint32_t memoryType(uint32_t bits, VkMemoryPropertyFlags flags);
|
||||
Buffer createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, bool hostVisible);
|
||||
Buffer createDeviceBuffer(const void* data, VkDeviceSize size, VkBufferUsageFlags usage);
|
||||
void destroy(Buffer& b);
|
||||
void destroy(Image& i);
|
||||
void retire(Buffer& b);
|
||||
VkCommandBuffer recordInto_ = VK_NULL_HANDLE; // set while uploads ride in the frame's commands
|
||||
std::vector<Buffer> frameStaging_; // their staging, freed after that frame
|
||||
VkPipelineCache pipelineCache_ = VK_NULL_HANDLE;
|
||||
static constexpr uint32_t kMaxQueries = 8192; // the Cost view times every draw (2 per eye each)
|
||||
VkQueryPool queryPool_ = VK_NULL_HANDLE;
|
||||
public:
|
||||
float lastGpuMs() const { return gpuMs_; }
|
||||
// FH_PERF=name,name...: the GPU's own counters (VK_KHR_performance_query; on Turnip with TU_DEBUG=perfc) whose
|
||||
// names contain one of these, summed over the frames read back; FH_PERF=list logs every counter there is.
|
||||
std::vector<std::pair<std::string, double>> perfCounterAverages() const;
|
||||
uint64_t gpuResults() const { return gpuResults_; } // counts lastGpuMs updates (a frame without one keeps the old value)
|
||||
// CPU side of the last render() (FH_PROFILE / --bench): where its time went, in ms.
|
||||
struct FrameStats {
|
||||
double waitMs = 0, readbackMs = 0, animMs = 0, globalsMs = 0, dynamicMs = 0, uploadMs = 0, recordMs = 0, submitMs = 0, totalMs = 0;
|
||||
uint32_t uploads = 0, dynamicRebuilt = 0, dynamicMeshes = 0, draws = 0, pipelineBinds = 0;
|
||||
uint64_t triangles = 0; // drawn this frame (per eye)
|
||||
bool early = false; // recorded while the GPU still drew the frame before
|
||||
};
|
||||
const FrameStats& lastStats() const { return stats_; }
|
||||
float lastGpuUploadMs() const { return gpuUploadMs_; } // parts of lastGpuMs: texture uploads, and the copy of the
|
||||
float lastGpuCopyMs() const { return gpuCopyMs_; } // frame into the compositor's slot (system mode)
|
||||
private:
|
||||
FrameStats stats_;
|
||||
// Load-time uploads (loadStep) ride in one command buffer per step instead of a queue round trip each.
|
||||
VkCommandBuffer loadCmd_ = VK_NULL_HANDLE;
|
||||
VkDeviceSize loadStaged_ = 0; // staging bytes recorded into loadCmd_
|
||||
std::vector<Buffer> loadStaging_; // ...and their staging, freed when it's flushed
|
||||
void beginUploadBatch();
|
||||
void flushUploadBatch();
|
||||
float timestampPeriod_ = 0;
|
||||
bool fdmOn_ = false; // foveation running (render pass has a density map)
|
||||
Image fdm_; // the density map: 2x the eye image, fovea in the middle
|
||||
uint32_t fdmTexel_ = 32, fdmGranularity_ = 8, fdmGranularityY_ = 8, fdmW_ = 0, fdmH_ = 0;
|
||||
VkFormat fdmFormat_ = VK_FORMAT_UNDEFINED;
|
||||
int fovLevel_ = 2;
|
||||
float fovStrength_ = 2.0f;
|
||||
float fovCenter_[2][2] = {{-1, -1}, {-1, -1}};
|
||||
void writeDensityMap();
|
||||
float gpuUploadMs_ = 0, gpuCopyMs_ = 0;
|
||||
float gpuMs_ = 0, gpuSum_ = 0, gpuMax_ = 0; // GPU time of the last frame, and a running average
|
||||
int gpuFrames_ = 0;
|
||||
uint64_t gpuResults_ = 0;
|
||||
uint32_t profNext_ = 0;
|
||||
std::vector<std::pair<size_t, uint32_t>> profDraws_; // mesh, first query
|
||||
bool profFrame_ = false; // frame timestamps written (last 3 queries)
|
||||
uint32_t lastUploads_ = 0, prevUploads_ = 0; // textures recorded this frame / in the frame being waited on
|
||||
double lastUploadMB_ = 0, prevUploadMB_ = 0;
|
||||
template <typename F> void oneShot(F&& record);
|
||||
Image createImage2D(uint32_t w, uint32_t h, uint32_t layers, uint32_t mips, VkSampleCountFlagBits samples,
|
||||
VkFormat fmt, VkImageUsageFlags usage, VkImageAspectFlags aspect, VkImageViewType viewType,
|
||||
VkImageCreateFlags flags = 0);
|
||||
Image createTexture(const u8* rgba, uint32_t w, uint32_t h, bool srgb);
|
||||
Image createTextureFmt(const void* pixels, uint32_t w, uint32_t h, VkFormat fmt);
|
||||
Image createSolidCube(const float top[3], const float side[3], const float bottom[3]);
|
||||
Image createSolid3D(const u8 rgba[4]);
|
||||
// `bytes`: what `faces` holds; a cube whose levels need more is refused (Image{}), never over-read.
|
||||
Image createCubeTexture(const void* faces, size_t bytes, uint32_t size, VkFormat fmt = VK_FORMAT_R8G8B8A8_SRGB, uint32_t mips = 1,
|
||||
uint32_t levels = 0);
|
||||
Image createShadowDepth();
|
||||
|
||||
void createTargets();
|
||||
void createSharedResources();
|
||||
int programFor(const ShaderProgramSrc& src);
|
||||
// Cut-out (alpha-to-coverage) meshes in two passes: kCutDepth writes their depth from fsAlpha (Meta's shader
|
||||
// computing only its alpha), kCutShade shades them with depth writes off, the front-most sample alone passing.
|
||||
enum CutPass { kCutNone = 0, kCutDepth = 1, kCutShade = 2 };
|
||||
VkPipeline pipelineFor(Program& p, int blend, bool cull, bool sky = false, bool a2c = false, CutPass cut = kCutNone, int variant = -1);
|
||||
// The variant of p for a mesh whose set-2 uniform buffers hold `set2` (binding -> bytes): made on first use.
|
||||
int variantFor(Program& p, const std::map<uint32_t, std::vector<u8>>& set2);
|
||||
shaderbake::SceneConstants sceneConsts_; // the scene-wide uniform bytes that never change (shader_bake.h)
|
||||
size_t specMade_ = 0, specBaked_ = 0; // this load's specialized shaders: made at load / from the .fenv
|
||||
void writeSharedSets(Program& p);
|
||||
VkImageView textureFor(const spv::Resource& r, GpuMesh& gm, const MeshData& md);
|
||||
bool uploadMesh(const MeshData& md, int sceneIndex);
|
||||
void updateGlobals(const EyeView eyes[2], float timeSec);
|
||||
|
||||
VkInstance instance_ = VK_NULL_HANDLE;
|
||||
VkQueryPool perfPool_ = VK_NULL_HANDLE;
|
||||
std::vector<std::string> perfNames_;
|
||||
std::vector<VkPerformanceCounterStorageKHR> perfStorage_;
|
||||
std::vector<double> perfSum_;
|
||||
uint32_t perfFrames_ = 0;
|
||||
bool perfRecorded_ = false;
|
||||
VkPhysicalDevice physical_ = VK_NULL_HANDLE;
|
||||
VkDevice device_ = VK_NULL_HANDLE;
|
||||
VkQueue queue_ = VK_NULL_HANDLE;
|
||||
uint32_t queueFamily_ = 0;
|
||||
VkCommandPool pool_ = VK_NULL_HANDLE;
|
||||
VkCommandBuffer cmd_ = VK_NULL_HANDLE;
|
||||
VkFence fence_ = VK_NULL_HANDLE;
|
||||
VkSemaphore timeline_ = VK_NULL_HANDLE;
|
||||
uint64_t serial_ = 0;
|
||||
std::atomic<bool> lost_{false}; // (set by the publisher thread's waits too)
|
||||
bool fenceUnsignalled_ = false; // the last submit failed: its fence never signals
|
||||
std::vector<VkImage> exportImages_; // createExportImages: render() copies into exportImages_[exportSlot]
|
||||
VkRenderPass exportPass_ = VK_NULL_HANDLE; // exportDirect: renderPass_ ending in GENERAL, for the export images
|
||||
std::vector<VkImageView> exportViews_;
|
||||
std::vector<VkFramebuffer> exportFbs_; // per export image
|
||||
int lastExportSlot_ = -1; // the export image the last frame was drawn into (readback)
|
||||
bool fdmOffset_ = false; // foveation moved by VK_QCOM_fragment_density_map_offset
|
||||
bool fdmInit_ = false; // the density map has been written (its layout is FRAGMENT_DENSITY_MAP_OPTIMAL)
|
||||
int64_t fdmKey_ = -1; // the density map's centres and level as last written (not rewritten unchanged)
|
||||
void createExportFramebuffers();
|
||||
void destroyExportFramebuffers();
|
||||
void densityTexels(float cx, float cy, std::vector<float>& dens) const; // two per texel: across, down
|
||||
void packDensity(const std::vector<float>& dens, uint8_t* dst) const; // in the map's format
|
||||
std::vector<bool> exportInited_;
|
||||
VkDescriptorPool descPool_ = VK_NULL_HANDLE;
|
||||
// Selection overlay (selection_spirv.h): edges as a line list + faces, solid colour from a 1x1 texture.
|
||||
struct Selection {
|
||||
int mesh = -1;
|
||||
Buffer vbo, edges, tris, ubo;
|
||||
uint32_t edgeCount = 0, triCount = 0;
|
||||
VkDescriptorSetLayout layout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipeLayout = VK_NULL_HANDLE;
|
||||
VkPipeline lines = VK_NULL_HANDLE, faces = VK_NULL_HANDLE;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
Image color;
|
||||
} sel_;
|
||||
void createSelectionPipelines();
|
||||
// Guide geometry: setGuide's goes into a host-visible vertex buffer per command buffer, rewritten each frame;
|
||||
// setStaticGuide's into one device-local buffer. `hidden` draws where the home is in front, faintly.
|
||||
struct Guide {
|
||||
VkPipelineLayout layout = VK_NULL_HANDLE;
|
||||
VkPipeline visible = VK_NULL_HANDLE, hidden = VK_NULL_HANDLE;
|
||||
Buffer vbo[2]; // per command buffer: one is written while the other may be read
|
||||
Buffer staticVbo; // setStaticGuide's, in video memory
|
||||
uint32_t staticVertexCount = 0;
|
||||
} guide_;
|
||||
std::vector<GuideVertex> guideVerts_;
|
||||
void createGuidePipelines();
|
||||
void destroyGuide();
|
||||
void drawGuide(const EyeView eyes[2]);
|
||||
void destroySelection();
|
||||
// Debug views (setDebugView): one pipeline per vertex layout and mode, and the draws' matrices and colours in
|
||||
// one host-visible buffer (indexed by firstInstance), rewritten each frame after the fence.
|
||||
enum DebugMode { kDbgSolid = 0, kDbgTint, kDbgWire, kDbgAdd };
|
||||
struct Debug {
|
||||
VkDescriptorSetLayout setLayout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout layout = VK_NULL_HANDLE;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
VkShaderModule vs = VK_NULL_HANDLE, fs = VK_NULL_HANDLE;
|
||||
Buffer draws;
|
||||
std::map<uint64_t, VkPipeline> pipes; // (stride, position offset, format, mode)
|
||||
uint32_t used = 0, capacity = 0;
|
||||
} dbg_;
|
||||
int debugView_ = kViewNormal;
|
||||
bool wireOk_ = false;
|
||||
std::vector<float> cost_;
|
||||
bool tiler_ = false; // tile-based GPU: the Cost view draws meshes twice to time them
|
||||
int timedSlot_ = -1; // the export slot of the frame being timed
|
||||
int doubled_ = -1; // drawn twice in the last frame (timed at the next render())
|
||||
size_t costNext_ = 0;
|
||||
uint64_t costFrame_ = 0;
|
||||
static constexpr uint32_t kCostSamples = 6, kCostBase = 48;
|
||||
std::vector<float> costMin_; // per mesh: its last kCostSamples doubled-frame times
|
||||
std::vector<uint32_t> costSeen_;
|
||||
float costBaseRing_[kCostBase] = {1e9f}; // the last baseline frames (nothing doubled)
|
||||
uint32_t costBaseNext_ = 0;
|
||||
std::vector<std::pair<double, size_t>> opaqueOrder_; // render(): this frame's opaque draw order (kept: no allocation)
|
||||
bool createDebug();
|
||||
void destroyDebug();
|
||||
VkPipeline debugPipeline(const Program& p, int mode);
|
||||
void drawDebug(const GpuMesh& gm, int mode, const float color[4], const EyeView eyes[2], bool nudge);
|
||||
std::unordered_set<int> dynamicScene_;
|
||||
// cpu: tints, matrices, bounds and which meshes rebuild (no GPU memory); upload: those rebuilds and the bones,
|
||||
// into what the GPU reads (only once the frame before is done with them).
|
||||
void refreshDynamicMeshes(bool cpu, bool upload);
|
||||
struct DynJob { GpuMesh* gm; const MeshData* md; bool pos, uv; };
|
||||
std::vector<DynJob> dynJobs_;
|
||||
std::vector<GpuMesh*> skinCopies_; // GPU-skinned meshes whose bones changed
|
||||
bool canRecordEarly();
|
||||
int64_t fdmWantKey() const; // the foveation map this frame wants (fdmKey_ when it has it)
|
||||
void fdmCentres(int32_t cx[2], int32_t cy[2]) const;
|
||||
VkCommandBuffer cmds_[2] = {};
|
||||
int cmdIndex_ = 0;
|
||||
bool earlyRecord_ = false, fdmCopyRecorded_ = false;
|
||||
void buildVertices(const Program& p, const MeshData& md, std::vector<u8>& vbo, std::vector<float>& normals) const;
|
||||
VkSampleCountFlagBits samples_ = VK_SAMPLE_COUNT_1_BIT;
|
||||
float maxAniso_ = 1;
|
||||
uint32_t maxImageDim_ = 4096, maxCubeDim_ = 4096; // the GPU's limits: larger sizes from a file are refused
|
||||
bool anisotropy_ = false;
|
||||
uint32_t width_ = 0, height_ = 0;
|
||||
|
||||
VkRenderPass renderPass_ = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer_ = VK_NULL_HANDLE;
|
||||
Image colorMs_, depth_, resolve_;
|
||||
// Smooth foveation: the foveated pass draws into sub_ (subsampled: the driver keeps its coarse regions coarse),
|
||||
// and one full-screen pass reads it through the subsampled sampler (filtering between the coarse pixels) into
|
||||
// resolve_ or the compositor's image. Without it the coarse regions were stretched into blocks.
|
||||
bool smoothFov_ = false;
|
||||
Image sub_;
|
||||
VkRenderPass fovPass_ = VK_NULL_HANDLE;
|
||||
VkFramebuffer fovFb_ = VK_NULL_HANDLE;
|
||||
VkSampler subSampler_ = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout fovSetLayout_ = VK_NULL_HANDLE;
|
||||
VkPipelineLayout fovLayout_ = VK_NULL_HANDLE;
|
||||
VkPipeline fovPipe_ = VK_NULL_HANDLE;
|
||||
VkDescriptorPool fovPool_ = VK_NULL_HANDLE;
|
||||
VkDescriptorSet fovSet_ = VK_NULL_HANDLE;
|
||||
VkRenderPass colorOnlyPass(VkImageLayout finalLayout) const; // one 1-sample colour attachment, both views
|
||||
void createFovPass();
|
||||
void recordFovPass(VkFramebuffer fb, VkRenderPass pass);
|
||||
VkImageView layerViews_[2] = {};
|
||||
|
||||
VkSampler wrapSampler_ = VK_NULL_HANDLE, anisoSampler_ = VK_NULL_HANDLE, shadowSampler_ = VK_NULL_HANDLE;
|
||||
Image nullTex_;
|
||||
Image noMetal_; // white, alpha 0: BaseColorMetallic placeholder (never a chrome flash)
|
||||
size_t texturesRequested_ = 0; // requestAllTextures: how many were queued (progress)
|
||||
bool preloading_ = false; // the initial everything-at-once texture load is running
|
||||
Image white_, black_, flatNormal_, whiteCube_, white3D_, iblDiffuse_, iblReflection_, shadowMap_;
|
||||
Buffer globals_, lightUniforms_, lightItems_, lightGrid_;
|
||||
uint32_t globalsSize_ = 1024;
|
||||
std::vector<spv::Member> globalMembers_;
|
||||
|
||||
std::vector<Program> programs_;
|
||||
std::map<std::string, int> programIndex_;
|
||||
std::vector<GpuMesh> meshes_;
|
||||
std::map<uint64_t, Image> textureCache_; // content hash -> shared texture (freed on unload)
|
||||
const HslScene* scene_ = nullptr;
|
||||
size_t loadNext_ = 0, loadSkipped_ = 0;
|
||||
bool loadDone_ = true;
|
||||
std::chrono::steady_clock::time_point loadStart_;
|
||||
float ambient_[3] = {1, 1, 1};
|
||||
|
||||
// ── Texture streaming ──
|
||||
using TexKey = std::pair<const TexSource*, bool>; // source, sRGB
|
||||
struct TexSlot { size_t mesh; uint32_t binding, count; VkDescriptorType type; VkSampler sampler; };
|
||||
struct TexJob { std::shared_ptr<const TexSource> src; bool srgb; uint64_t gen; };
|
||||
// staging: its own buffer (load-time path) or, for streamed textures, the shared upload ring at stagingOffset;
|
||||
// pixels: a streamed texture's decoded texels, copied into the ring when its upload is recorded.
|
||||
struct PreparedTex { Image img; Buffer staging; VkDeviceSize stagingOffset = 0; std::vector<u8> pixels; uint32_t w = 0, h = 0, mips = 1; };
|
||||
struct TexDone { TexKey key; uint64_t gen; PreparedTex prep; bool ok; };
|
||||
PreparedTex prepareTexture(const void* pixels, uint32_t w, uint32_t h, VkFormat fmt, bool ownStaging = true);
|
||||
// One persistent upload buffer, reused every frame (as WebGPU's writeTexture does): per-texture staging
|
||||
// allocations made Windows page memory in and out and held the GPU queue for seconds.
|
||||
static constexpr VkDeviceSize kRingSize = 256ull << 20;
|
||||
Buffer ring_;
|
||||
VkDeviceSize ringHead_ = 0;
|
||||
void recordTextureUpload(VkCommandBuffer c, const PreparedTex& t);
|
||||
VkImageView streamTexture(const std::shared_ptr<const TexSource>& src, bool srgb, VkImageView placeholder);
|
||||
void pumpTextures(double budgetMs);
|
||||
void startWorkers();
|
||||
void stopWorkers();
|
||||
VkFormat astcFormat(u32 bw, u32 bh, bool srgb) const;
|
||||
Image createAstcTexture(const TexSource& src, VkFormat fmt);
|
||||
bool astcLdr_ = false; // GPU samples ASTC (the Frame's Adreno)
|
||||
bool astcHdr_ = false; // ...and HDR-profile ASTC (lightmaps, panoramas) too
|
||||
// An HDR map (lightmap, panorama): uploaded once per source, as ASTC when the GPU can, else RGBA16F.
|
||||
// exact: data the shader reads texel by texel (a vertex-animation table): never shrunk.
|
||||
VkImageView hdrTexture(const std::vector<u16>* half, const std::shared_ptr<const TexSource>& src, u32 w, u32 h, bool exact = false);
|
||||
std::map<TexKey, VkImageView> streamed_; // in GPU memory
|
||||
std::map<TexKey, std::vector<TexSlot>> waiting_; // descriptors showing a placeholder until it lands
|
||||
std::map<TexKey, std::shared_ptr<const TexSource>> pendingSrc_; // not decoded until something using it is in view
|
||||
std::map<TexKey, bool> requested_;
|
||||
size_t drawn_ = 0;
|
||||
bool uploadsUnseen_ = false;
|
||||
void updateBounds(GpuMesh& gm);
|
||||
void placeMesh(GpuMesh& gm);
|
||||
void measureMesh(GpuMesh& gm, const MeshData& md, uint32_t nv);
|
||||
void updateAnimations(float timeSec);
|
||||
std::map<int, size_t> sceneToMesh_; // HslScene::meshes index -> meshes_ index
|
||||
void requestTextures(GpuMesh& gm);
|
||||
bool slotPending_ = false; // textureFor queued one: uploadMesh records the slot
|
||||
TexKey pendingKey_{};
|
||||
std::vector<std::shared_ptr<const TexSource>> keepAlive_;
|
||||
std::deque<TexJob> jobs_;
|
||||
std::vector<TexDone> done_;
|
||||
std::mutex jobMutex_;
|
||||
std::condition_variable jobCv_;
|
||||
std::vector<std::thread> workers_;
|
||||
bool stopping_ = false;
|
||||
uint64_t generation_ = 0; // bumped on unload: stale results are dropped
|
||||
};
|
||||
@@ -0,0 +1,870 @@
|
||||
#include "hsl_scene.h"
|
||||
#include "world_gen.h"
|
||||
#include "surface_mesh.h"
|
||||
|
||||
#include "loaders/scene_loader.h"
|
||||
#include "loaders/rendshad_parser.h"
|
||||
#include "loaders/opa_loader.h"
|
||||
#include "loaders/gltf_loader.h"
|
||||
#include "clay_spirv.h"
|
||||
#include "core/fsb5_vorbis.h"
|
||||
#include "fenv.h"
|
||||
#include "model_import.h"
|
||||
#include "scene_bake.h"
|
||||
#include "anim_bake.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <array>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
|
||||
// Reads a whole zip entry; empty on failure.
|
||||
std::vector<u8> extract(mz_zip_archive& z, int index) {
|
||||
mz_zip_archive_file_stat st;
|
||||
if (!mz_zip_reader_file_stat(&z, static_cast<mz_uint>(index), &st)) return {};
|
||||
std::vector<u8> out(static_cast<size_t>(st.m_uncomp_size)); // unpacked straight into place
|
||||
if (!mz_zip_reader_extract_to_mem(&z, static_cast<mz_uint>(index), out.data(), out.size(), 0)) return {};
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string surfaceName(const std::string& path) {
|
||||
size_t s = path.find(".surface");
|
||||
if (s == std::string::npos) return {};
|
||||
size_t b = path.rfind('/', s);
|
||||
return path.substr(b == std::string::npos ? 0 : b + 1, s + 8 - (b == std::string::npos ? 0 : b + 1));
|
||||
}
|
||||
|
||||
// Scans assets/scene.zip: every RENDSHAD forward pass (shader programs) and the merged lightprobe
|
||||
// ambient (lightprobes_merged.lprb, field2 = global SH; L00 / 3.5449 = ambient radiance).
|
||||
void scanSceneZip(const std::string& apkPath, HslScene& scene) {
|
||||
const std::shared_ptr<const std::vector<u8>> sceneZip = scenezip::get(apkPath); // the loader's copy
|
||||
if (!sceneZip || sceneZip->empty()) return;
|
||||
|
||||
mz_zip_archive z = {};
|
||||
if (!mz_zip_reader_init_mem(&z, sceneZip->data(), sceneZip->size(), 0)) return;
|
||||
for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&z); ++i) {
|
||||
mz_zip_archive_file_stat st;
|
||||
if (!mz_zip_reader_file_stat(&z, i, &st)) continue;
|
||||
std::string name = st.m_filename;
|
||||
std::string base = name.substr(name.find_last_of('/') + 1);
|
||||
|
||||
// Any "<name>.surface/<hash>/shader*" entry: older homes keep theirs under meta/clay_shaders/0.0.1/, not /shaders/.
|
||||
if (name.find(".surface/") != std::string::npos && base.rfind("shader", 0) == 0) {
|
||||
std::vector<u8> data = extract(z, static_cast<int>(i));
|
||||
std::vector<SpirvBlob> blobs;
|
||||
if (!parseRendShadForward(data, blobs)) { std::fprintf(stderr, "[scene] shader %s: no forward pass parsed\n", name.c_str()); continue; }
|
||||
ShaderProgramSrc p;
|
||||
p.path = name.rfind("content/", 0) == 0 ? name.substr(8) : name;
|
||||
for (auto& b : blobs) (b.stageType == 0 ? p.vert : p.frag) = std::move(b.code);
|
||||
if (!p.vert.empty() && !p.frag.empty()) scene.programs[p.path] = std::move(p);
|
||||
else std::fprintf(stderr, "[scene] shader %s: %zu blob(s), vertex %s, fragment %s\n", name.c_str(), blobs.size(),
|
||||
p.vert.empty() ? "missing" : "ok", p.frag.empty() ? "missing" : "ok");
|
||||
} else if (name.find("lightprobes_merged.lprb") != std::string::npos) {
|
||||
std::vector<u8> d = extract(z, static_cast<int>(i));
|
||||
if (d.size() < 80 || std::memcmp(d.data() + 4, "LPRB", 4) != 0) continue;
|
||||
auto rd32 = [&](size_t o) { u32 v = 0; if (o + 4 <= d.size()) std::memcpy(&v, d.data() + o, 4); return v; };
|
||||
auto rd16 = [&](size_t o) { u16 v = 0; if (o + 2 <= d.size()) std::memcpy(&v, d.data() + o, 2); return v; };
|
||||
u32 root = rd32(0);
|
||||
u32 vt = root - rd32(root);
|
||||
if (rd16(vt) < 10) continue;
|
||||
u16 f2 = rd16(vt + 8);
|
||||
if (!f2) continue;
|
||||
u32 vec = root + f2 + rd32(root + f2);
|
||||
if (vec + 16 > d.size() || rd32(vec) < 3) continue;
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
float v; std::memcpy(&v, d.data() + vec + 4 + c * 4, 4);
|
||||
scene.ambientRGB[c] = v / 3.5449f;
|
||||
}
|
||||
scene.hasEnvAmbient = true;
|
||||
}
|
||||
}
|
||||
mz_zip_reader_end(&z);
|
||||
}
|
||||
|
||||
// AndroidManifest.xml (binary XML): the <manifest> element's package and versionName attributes.
|
||||
static void readManifest(const std::string& apkPath, std::string& package, std::string& version) {
|
||||
mz_zip_archive apk = {};
|
||||
if (!mz_zip_reader_init_file(&apk, apkPath.c_str(), 0)) return;
|
||||
const int mi = mz_zip_reader_locate_file(&apk, "AndroidManifest.xml", nullptr, 0);
|
||||
const std::vector<u8> x = mi >= 0 ? extract(apk, mi) : std::vector<u8>();
|
||||
mz_zip_reader_end(&apk);
|
||||
auto rd16 = [&](size_t o) -> u32 { return o + 2 <= x.size() ? x[o] | x[o + 1] << 8 : 0; };
|
||||
auto rd32 = [&](size_t o) -> u32 { return o + 4 <= x.size() ? rd16(o) | rd16(o + 2) << 16 : 0; };
|
||||
if (x.size() < 16 || rd16(0) != 0x0003 || rd16(8) != 0x0001) return;
|
||||
// String pool: count, flags (0x100 = UTF-8), offsets.
|
||||
const size_t pool = 8, count = rd32(pool + 8), poolEnd = pool + rd32(pool + 4);
|
||||
const bool utf8 = rd32(pool + 16) & 0x100;
|
||||
const size_t strings = pool + rd32(pool + 20);
|
||||
auto str = [&](u32 i) -> std::string {
|
||||
if (i >= count) return {};
|
||||
size_t o = strings + rd32(pool + 28 + i * 4);
|
||||
std::string out;
|
||||
if (utf8) {
|
||||
if (o < x.size() && (x[o] & 0x80)) ++o; // UTF-16 length (1-2 bytes), then the UTF-8 byte length
|
||||
++o;
|
||||
size_t n = o < x.size() ? x[o] : 0;
|
||||
if (n & 0x80) { n = (n & 0x7F) << 8 | (o + 1 < x.size() ? x[o + 1] : 0); ++o; }
|
||||
++o;
|
||||
if (o + n <= x.size()) out.assign(reinterpret_cast<const char*>(&x[o]), n);
|
||||
} else {
|
||||
size_t n = rd16(o);
|
||||
if (n & 0x8000) { n = (n & 0x7FFF) << 16 | rd16(o + 2); o += 2; }
|
||||
o += 2;
|
||||
for (size_t k = 0; k < n && o + 2 * k + 2 <= x.size(); ++k) { const u32 c = rd16(o + 2 * k); out += c < 0x80 ? static_cast<char>(c) : '?'; }
|
||||
}
|
||||
return out;
|
||||
};
|
||||
for (size_t c = poolEnd; c + 36 <= x.size();) {
|
||||
const u32 type = rd16(c), size = rd32(c + 4);
|
||||
if (size < 8) break;
|
||||
if (type == 0x0102 && str(rd32(c + 20)) == "manifest") { // START_ELEMENT
|
||||
const size_t attrs = c + 16 + rd16(c + 24), stride = rd16(c + 26), n = rd16(c + 28);
|
||||
for (size_t a = 0; a < n; ++a) {
|
||||
const size_t at = attrs + a * stride;
|
||||
const std::string name = str(rd32(at + 4));
|
||||
const u32 raw = rd32(at + 8);
|
||||
const std::string value = raw != 0xFFFFFFFFu ? str(raw) : x.size() > at + 15 && x[at + 15] == 0x03 ? str(rd32(at + 16)) : std::string();
|
||||
if (name == "package") package = value;
|
||||
if (name == "versionName") version = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
c += size;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The built-in programs Clay (.opa) homes are drawn with (tools/gen_clay_shader.py): libshell builds its Clay
|
||||
// shaders natively, so - like Quest Home Editor - an unlit texture x vertex colour x tint program with real alpha,
|
||||
// and its cut-out twin for AlphaTest materials.
|
||||
constexpr const char* kClayProgram = "builtin/clay.surface/0/shader";
|
||||
constexpr const char* kClayCutoutProgram = "builtin/clay_cutout.surface/0/shader";
|
||||
constexpr const char* kClaySkinnedProgram = "builtin/clay_skinned.surface/0/shader"; // skinned on the GPU
|
||||
constexpr const char* kClaySkinnedCutoutProgram = "builtin/clay_skinned_cutout.surface/0/shader";
|
||||
|
||||
// The home's looping ambience: an Ogg Vorbis entry of scene.zip (Clay homes ship _BACKGROUND_LOOP.ogg).
|
||||
static std::vector<u8> findAmbientOgg(const std::string& apkPath, std::string& source, std::string& name) {
|
||||
const std::shared_ptr<const std::vector<u8>> zipData = scenezip::get(apkPath); // the loader's copy
|
||||
std::vector<u8> best;
|
||||
mz_zip_archive z = {};
|
||||
if (!zipData || zipData->empty() || !mz_zip_reader_init_mem(&z, zipData->data(), zipData->size(), 0)) return {};
|
||||
bool bestIsLoop = false;
|
||||
std::vector<int> banks; // HSL homes: the FMOD banks hold the same bed (see fsb5_vorbis.h)
|
||||
for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&z); ++i) {
|
||||
mz_zip_archive_file_stat st;
|
||||
if (!mz_zip_reader_file_stat(&z, i, &st)) continue;
|
||||
std::string n = st.m_filename;
|
||||
for (auto& c : n) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (n.find(".bank/") != std::string::npos && n.find(".strings.bank/") == std::string::npos)
|
||||
banks.push_back(static_cast<int>(i)); // the home's own bank and shared ones (nativespacesmain)
|
||||
if (n.size() < 4 || n.compare(n.size() - 4, 4, ".ogg") != 0) continue;
|
||||
const bool loop = n.find("background") != std::string::npos || n.find("loop") != std::string::npos;
|
||||
if (!best.empty() && (bestIsLoop || !loop)) continue;
|
||||
best = extract(z, static_cast<int>(i));
|
||||
bestIsLoop = loop;
|
||||
name = st.m_filename; // its file, without the folders and ".ogg"
|
||||
if (const size_t sl = name.find_last_of('/'); sl != std::string::npos) name = name.substr(sl + 1);
|
||||
if (name.size() > 4) name.resize(name.size() - 4);
|
||||
}
|
||||
if (!best.empty()) source = "Ogg loop";
|
||||
if (best.empty() && !banks.empty()) {
|
||||
std::vector<std::vector<u8>> data;
|
||||
std::vector<const std::vector<u8>*> ptrs;
|
||||
for (int b : banks) data.push_back(extract(z, b));
|
||||
for (const auto& d : data) ptrs.push_back(&d);
|
||||
std::string why;
|
||||
best = fsb5::ambienceOgg(ptrs, &why);
|
||||
std::fprintf(stderr, "[scene] ambience from the FMOD bank: %s (%s)\n", best.empty() ? "unsupported" : "Ogg Vorbis rebuilt",
|
||||
why.c_str());
|
||||
if (!best.empty()) {
|
||||
source = "FMOD bank";
|
||||
const size_t q0 = why.find('"'), q1 = q0 == std::string::npos ? q0 : why.find('"', q0 + 1); // the sound's name in the bank
|
||||
name = q1 != std::string::npos ? why.substr(q0 + 1, q1 - q0 - 1) : std::string();
|
||||
}
|
||||
}
|
||||
mz_zip_reader_end(&z);
|
||||
return best;
|
||||
}
|
||||
|
||||
// Desert Terrace 210's palms (desertterrace_palms_v8.fbx, palmtreefoliage) ship wound inside-out and without
|
||||
// normals. Their shader discards back faces (doubleSided 0), so the trunks drew their far, inner walls (black) and
|
||||
// the fronds lost their lit side. Rendered with the order reversed, trunks and fronds match the textures. Only this
|
||||
// shader reads gl_FrontFacing among the homes checked, and Meta's own meshes (authored normals) wind correctly.
|
||||
static bool insideOutPalms(const MeshData& md) {
|
||||
if (md.shaderPath.find("palmtreefoliage") == std::string::npos) return false;
|
||||
for (float n : md.normals) if (n != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Which meshes a loader's animate() changes: sample the loop at a few times and compare each mesh's
|
||||
// positions / uvs / tint / rigid motion with the t=0 pose. Only those re-upload vertices every frame.
|
||||
static std::vector<int> findAnimatedMeshes(const std::function<void(float, std::vector<MeshData>&)>& animate,
|
||||
std::vector<MeshData>& meshes, float duration) {
|
||||
auto sig = [](const MeshData& m) {
|
||||
uint64_t h = 1469598103934665603ull;
|
||||
auto mix = [&](const void* p, size_t n) {
|
||||
const u8* b = static_cast<const u8*>(p);
|
||||
for (size_t i = 0; i < n; ++i) h = (h ^ b[i]) * 1099511628211ull;
|
||||
};
|
||||
mix(m.positions.data(), m.positions.size() * 4);
|
||||
mix(m.uvs.data(), m.uvs.size() * 4);
|
||||
mix(m.curTint, sizeof m.curTint);
|
||||
mix(m.animXform, sizeof m.animXform);
|
||||
mix(m.uvXform, sizeof m.uvXform);
|
||||
mix(m.skinJoints.data(), m.skinJoints.size() * 4);
|
||||
return h;
|
||||
};
|
||||
animate(0.0f, meshes);
|
||||
std::vector<uint64_t> base(meshes.size());
|
||||
for (size_t i = 0; i < meshes.size(); ++i) base[i] = sig(meshes[i]);
|
||||
std::vector<char> moved(meshes.size(), 0);
|
||||
const float d = duration > 0 ? duration : 10.0f;
|
||||
for (float t : {0.1f, 0.5f, d * 0.37f, d * 0.71f}) {
|
||||
animate(t, meshes);
|
||||
for (size_t i = 0; i < meshes.size(); ++i) if (sig(meshes[i]) != base[i]) moved[i] = 1;
|
||||
}
|
||||
animate(0.0f, meshes);
|
||||
std::vector<int> out;
|
||||
for (size_t i = 0; i < meshes.size(); ++i) if (moved[i]) out.push_back(static_cast<int>(i));
|
||||
return out;
|
||||
}
|
||||
|
||||
static void addClayPrograms(HslScene& scene) {
|
||||
ShaderProgramSrc p;
|
||||
p.path = kClayProgram;
|
||||
p.vert.assign(std::begin(kClayVertSpirv), std::end(kClayVertSpirv));
|
||||
p.frag.assign(std::begin(kClayFragSpirv), std::end(kClayFragSpirv));
|
||||
scene.programs[p.path] = p;
|
||||
p.path = kClayCutoutProgram;
|
||||
p.frag.assign(std::begin(kClayFragCutoutSpirv), std::end(kClayFragCutoutSpirv));
|
||||
scene.programs[p.path] = p;
|
||||
p.path = kClaySkinnedCutoutProgram;
|
||||
p.vert.assign(std::begin(kClaySkinnedVertSpirv), std::end(kClaySkinnedVertSpirv));
|
||||
scene.programs[p.path] = p;
|
||||
p.path = kClaySkinnedProgram;
|
||||
p.frag.assign(std::begin(kClayFragSpirv), std::end(kClayFragSpirv));
|
||||
scene.programs[p.path] = p;
|
||||
}
|
||||
|
||||
// A .fenv: its baked scene when it has one (instant), else its homes and models through the normal loaders.
|
||||
static bool loadFenv(const std::string& path, HslScene& scene, std::string& error) {
|
||||
FenvManifest m;
|
||||
std::vector<std::string> sources, models;
|
||||
std::string edits;
|
||||
if (!fenvExtract(path, fenvCacheDir(), m, sources, models, edits, error)) return false;
|
||||
if (!m.scene.empty()) {
|
||||
std::ifstream f(std::filesystem::u8path(m.scene), std::ios::binary | std::ios::ate);
|
||||
std::vector<u8> blob(f ? static_cast<size_t>(f.tellg()) : 0); // in one read (an istreambuf copy was 750 ms for 316 MB)
|
||||
f.seekg(0);
|
||||
if (!blob.empty() && !f.read(reinterpret_cast<char*>(blob.data()), static_cast<std::streamsize>(blob.size()))) blob.clear();
|
||||
if (!unbakeScene(blob.data(), blob.size(), scene, error)) { error += ": " + path; return false; }
|
||||
scene.sourceFiles.resize(scene.apkPaths.size());
|
||||
} else {
|
||||
std::vector<std::string> all = sources;
|
||||
all.insert(all.end(), models.begin(), models.end());
|
||||
if (all.empty()) { error = "empty environment: " + path; return false; }
|
||||
if (!loadHslScene(all, scene, error)) return false;
|
||||
}
|
||||
scene.fenvPath = path;
|
||||
scene.fenvEdits = edits;
|
||||
if (!m.world.empty()) { // the environment's own world layer (edited in Frame Studio) replaces the converted one
|
||||
std::ifstream wf(std::filesystem::u8path(m.world));
|
||||
const std::string wj((std::istreambuf_iterator<char>(wf)), std::istreambuf_iterator<char>());
|
||||
HomeWorld w;
|
||||
if (worldFromJson(wj, w)) scene.world = std::move(w);
|
||||
}
|
||||
std::fprintf(stderr, "[scene] environment %s (%s): %zu meshes\n", m.name.c_str(), m.scene.empty() ? "from its homes" : "baked", scene.meshes.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std::string& error) {
|
||||
if (apkPaths.size() == 1 && isFenvPath(apkPaths[0])) {
|
||||
if (!loadFenv(apkPaths[0], scene, error)) return false;
|
||||
simplifyWorldMeshes(scene.world);
|
||||
return true;
|
||||
}
|
||||
scene = HslScene{};
|
||||
scene.apkPaths = apkPaths;
|
||||
g_deferTextures = true;
|
||||
const scenezip::Keep keepSceneZips(apkPaths); // each scene.zip unpacked once for the whole load
|
||||
{
|
||||
std::lock_guard<std::mutex> l(g_sceneFogMutex);
|
||||
g_sceneFog = SceneFog{};
|
||||
} // textures stay compressed; the renderer streams them in
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
// Footprint and vista parse in parallel (each decodes its own textures).
|
||||
std::vector<std::unique_ptr<SceneLoader>> loaders(apkPaths.size());
|
||||
std::vector<std::unique_ptr<OpaLoader>> clays(apkPaths.size()); // pre-HSL Clay homes (cascadia.apk...)
|
||||
std::vector<std::unique_ptr<GltfLoader>> gltfs(apkPaths.size()); // the oldest custom homes (_world_model.gltf.ovrscene)
|
||||
std::vector<char> ok(apkPaths.size(), 0);
|
||||
std::vector<std::vector<MeshData>> models(apkPaths.size()); // imported Blender / Unity models
|
||||
std::vector<std::string> modelErrors(apkPaths.size());
|
||||
scene.sourceFiles.resize(apkPaths.size());
|
||||
std::vector<std::thread> threads;
|
||||
for (size_t i = 0; i < apkPaths.size(); ++i) {
|
||||
loaders[i] = std::make_unique<SceneLoader>();
|
||||
loaders[i]->verbose = std::getenv("FH_VERBOSE") != nullptr;
|
||||
threads.emplace_back([&, i] {
|
||||
if (isModelPath(apkPaths[i])) {
|
||||
if (importModel(apkPaths[i], models[i], scene.sourceFiles[i], modelErrors[i])) ok[i] = 4;
|
||||
return;
|
||||
}
|
||||
if (loaders[i]->load(apkPaths[i])) { ok[i] = 1; return; }
|
||||
clays[i] = std::make_unique<OpaLoader>(); // not HSL: a Clay (.opa) home?
|
||||
if (clays[i]->load(apkPaths[i])) { ok[i] = 2; return; }
|
||||
clays[i].reset();
|
||||
gltfs[i] = std::make_unique<GltfLoader>(); // not .opa: a Clay .ovrscene home (glTF in scene.zip: SideQuest custom homes)?
|
||||
if (gltfs[i]->load(apkPaths[i])) ok[i] = 3;
|
||||
else gltfs[i].reset();
|
||||
});
|
||||
}
|
||||
for (auto& t : threads) t.join();
|
||||
for (size_t i = 0; i < apkPaths.size(); ++i)
|
||||
if (!ok[i]) {
|
||||
error = !modelErrors[i].empty() ? modelErrors[i] : "not a loadable home (HSL v203+, Clay .opa, glTF .ovrscene) or model (glTF, GLB, FBX, OBJ): " + apkPaths[i];
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < apkPaths.size(); ++i) {
|
||||
HslSourceInfo info;
|
||||
readManifest(apkPaths[i], info.package, info.version);
|
||||
info.format = ok[i] == 1 ? "HSL" : ok[i] == 2 ? "Clay (.opa)" : ok[i] == 3 ? "Clay (.ovrscene)" : "Model";
|
||||
if (ok[i] == 4) {
|
||||
const std::string file = apkPaths[i].substr(apkPaths[i].find_last_of("/\\") + 1);
|
||||
info.package = file;
|
||||
std::string ext = file.substr(file.find_last_of('.') + 1);
|
||||
for (char& c : ext) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
info.format = "Model (" + ext + ")";
|
||||
}
|
||||
// Split homes (Haven-era) ship com.meta.shell.env.footprint.<room> + ...vista.<world>; "prod.vista" is
|
||||
// Cascadia, a whole home. Without a package, the file name decides.
|
||||
std::string n = info.package.empty() ? apkPaths[i].substr(apkPaths[i].find_last_of("/\\") + 1) : info.package;
|
||||
for (char& c : n) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
const bool named = !info.package.empty();
|
||||
info.role = ok[i] == 4 ? "Model" : ok[i] != 1 ? "Home"
|
||||
: n.find(named ? ".footprint." : "footprint") != std::string::npos ? "Footprint"
|
||||
: n.find(named ? ".vista." : "vista") != std::string::npos ? "Vista" : "Home";
|
||||
scene.sources.push_back(info);
|
||||
}
|
||||
const auto t1 = std::chrono::steady_clock::now();
|
||||
// Baked lightmaps: each APK's meshes against its own overrides, then the companion's (the vista
|
||||
// ships the footprint's lightmaps and vice versa).
|
||||
for (size_t i = 0; i < loaders.size(); ++i) {
|
||||
loaders[i]->applyLightmapOverrides(loaders[i]->meshes);
|
||||
for (size_t j = 0; j < loaders.size(); ++j)
|
||||
if (j != i) loaders[i]->applyLightmapOverrides(loaders[i]->meshes, loaders[j].get());
|
||||
}
|
||||
size_t lightmapped = 0;
|
||||
for (size_t i = 0; i < loaders.size(); ++i) {
|
||||
if (ok[i] == 4) { // imported model: the built-in unlit programs
|
||||
addClayPrograms(scene);
|
||||
for (MeshData& md : models[i]) {
|
||||
md.shaderPath = md.alphaTest ? kClayCutoutProgram : kClayProgram;
|
||||
scene.meshes.push_back(std::move(md));
|
||||
scene.meshSource.push_back(static_cast<int>(i));
|
||||
}
|
||||
std::fprintf(stderr, "[scene] model %s: %zu parts\n", apkPaths[i].c_str(), models[i].size());
|
||||
continue;
|
||||
}
|
||||
if (clays[i] || gltfs[i]) { // Clay / glTF home: built-in programs
|
||||
addClayPrograms(scene);
|
||||
// The home animates here (below: the first home, the rest only models): its skinned meshes skin on the GPU.
|
||||
if (clays[i] && clays[i]->hasAnimation() && i == 0 && std::count(ok.begin(), ok.end(), 4) + 1 == static_cast<long>(apkPaths.size()))
|
||||
clays[i]->prepareGpuSkin();
|
||||
// Base textures: one shared, still-compressed source per owner. The loader lets meshes cut from the same
|
||||
// part BORROW the owner's texture (texShareSrc) and empties their own copy — unresolved, those uploaded
|
||||
// nothing and drew white. Every user of a texture now points at the same source: decoded once in the
|
||||
// background, uploaded once.
|
||||
std::vector<MeshData>& cm = clays[i] ? clays[i]->meshes : gltfs[i]->meshes;
|
||||
std::vector<std::shared_ptr<TexSource>> srcOf(cm.size());
|
||||
for (size_t j = 0; j < cm.size(); ++j) {
|
||||
MeshData& o = cm[j];
|
||||
if (o.srcAstc.empty() || !o.srcAstcBw || !o.srcAstcBh || o.texW <= 1) continue;
|
||||
auto t = std::make_shared<TexSource>();
|
||||
t->astc = std::move(o.srcAstc);
|
||||
o.srcAstc.clear();
|
||||
t->w = o.texW; t->h = o.texH; t->bw = o.srcAstcBw; t->bh = o.srcAstcBh;
|
||||
t->mips = std::max<u32>(1, o.srcAstcMips);
|
||||
srcOf[j] = t;
|
||||
}
|
||||
for (size_t j = 0; j < cm.size(); ++j) {
|
||||
MeshData& md = cm[j];
|
||||
const size_t owner = md.texShareSrc >= 0 && static_cast<size_t>(md.texShareSrc) < cm.size() ? md.texShareSrc : j;
|
||||
if (srcOf[owner]) {
|
||||
md.texSrc = srcOf[owner];
|
||||
std::vector<u8>().swap(md.texRGBA);
|
||||
} else if (owner != j && !md.texSrc && cm[owner].texSrc) {
|
||||
md.texSrc = cm[owner].texSrc; // glTF: the owner's compressed source
|
||||
} else if (owner != j && md.texRGBA.empty()) {
|
||||
md.texRGBA = cm[owner].texRGBA; // no compressed source: copy the pixels
|
||||
}
|
||||
if (md.texSrc && !std::getenv("FH_CLAYDBG")) std::vector<u8>().swap(md.texRGBA); // the loader's small preview: drawn from texSrc
|
||||
|
||||
}
|
||||
size_t n = 0;
|
||||
for (auto& md : cm) {
|
||||
if (!md.texSrc && md.texW * md.texH <= 1) // flat fallback colour: no real texture
|
||||
std::fprintf(stderr, "[clay] untextured '%s' (%u verts) colour %u,%u,%u,%u lightmap %d\n", md.name.c_str(), md.nVerts,
|
||||
md.texRGBA.size() >= 4 ? md.texRGBA[0] : 0, md.texRGBA.size() >= 4 ? md.texRGBA[1] : 0,
|
||||
md.texRGBA.size() >= 4 ? md.texRGBA[2] : 0, md.texRGBA.size() >= 4 ? md.texRGBA[3] : 0, (int)md.hasLightmap);
|
||||
if (!md.skinBindPos.empty()) md.shaderPath = md.alphaTest ? kClaySkinnedCutoutProgram : kClaySkinnedProgram;
|
||||
else md.shaderPath = md.alphaTest ? kClayCutoutProgram : kClayProgram;
|
||||
std::memcpy(md.curTint, md.tint, sizeof md.curTint);
|
||||
if (md.hasLightmap) // ShellEnv: diffuse x lightmap x lightmappower
|
||||
for (int c = 0; c < 3; ++c) md.curTint[c] *= md.lightmapPower[c];
|
||||
else if (std::getenv("FH_CLAYDBG")) { // diagnostics: what lights a mesh without a lightmap?
|
||||
double avg[3] = {0, 0, 0};
|
||||
const size_t px = md.texRGBA.size() / 4;
|
||||
for (size_t q = 0; q < px; q += std::max<size_t>(1, px / 4096))
|
||||
for (int c = 0; c < 3; ++c) avg[c] += md.texRGBA[q * 4 + c];
|
||||
const double cnt = static_cast<double>(px / std::max<size_t>(1, px / 4096));
|
||||
std::fprintf(stderr, "[clay] no lightmap: '%s' tex %ux%u avg %.0f,%.0f,%.0f blend %d iblLit %d\n", md.name.c_str(), md.texW,
|
||||
md.texH, avg[0] / std::max(1.0, cnt), avg[1] / std::max(1.0, cnt), avg[2] / std::max(1.0, cnt),
|
||||
(int)md.useBlend, (int)md.iblLit);
|
||||
if (avg[0] + avg[1] + avg[2] == 0) {
|
||||
size_t nz = 0;
|
||||
for (size_t q = 0; q < md.texRGBA.size(); ++q) nz += md.texRGBA[q] != 0;
|
||||
std::vector<u8> re;
|
||||
const bool ok = !md.srcAstc.empty() &&
|
||||
astc::decodeASTC(md.srcAstc.data(), static_cast<u32>(md.srcAstc.size()), md.texW, md.texH,
|
||||
md.srcAstcBw, md.srcAstcBh, re, false, 1);
|
||||
size_t nz2 = 0;
|
||||
for (u8 b : re) nz2 += b != 0;
|
||||
std::fprintf(stderr, "[clay] texRGBA %zu bytes, %zu non-zero; src %zu bytes (%ux%u blocks) re-decode %s, %zu non-zero\n",
|
||||
md.texRGBA.size(), nz, md.srcAstc.size(), md.srcAstcBw, md.srcAstcBh, ok ? "ok" : "failed", nz2);
|
||||
}
|
||||
}
|
||||
scene.meshes.push_back(std::move(md));
|
||||
scene.meshSource.push_back(static_cast<int>(i));
|
||||
++n;
|
||||
}
|
||||
std::fprintf(stderr, "[scene] %s home %s: %zu meshes\n", clays[i] ? "Clay" : "glTF .ovrscene", apkPaths[i].c_str(), n);
|
||||
// The loader keeps evaluating the animation (anim_bake.h: as a .fenv's does); it works on the scene's copy of its
|
||||
// meshes (a single Clay / glTF home: the scene's meshes ARE that loader's meshes, same order).
|
||||
// Models may follow the home: they sit after its meshes, which the animator indexes from 0.
|
||||
if (i == 0 && std::count(ok.begin(), ok.end(), 4) + 1 == static_cast<long>(apkPaths.size())) {
|
||||
if (clays[i] && clays[i]->hasAnimation()) {
|
||||
std::shared_ptr<OpaLoader> L(std::move(clays[i]));
|
||||
scene.animDuration = L->animDuration();
|
||||
animbake::adopt(scene, L);
|
||||
} else if (gltfs[i] && gltfs[i]->hasAnimation()) {
|
||||
std::shared_ptr<GltfLoader> L(std::move(gltfs[i]));
|
||||
scene.animDuration = L->animDuration;
|
||||
animbake::adopt(scene, L);
|
||||
}
|
||||
if (scene.animate) {
|
||||
scene.animatedMeshes = findAnimatedMeshes(scene.animate, scene.meshes, scene.animDuration);
|
||||
std::fprintf(stderr, "[scene] animation: %.1f s loop, %zu animated meshes\n", scene.animDuration,
|
||||
scene.animatedMeshes.size());
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const int base = static_cast<int>(scene.meshes.size());
|
||||
for (auto& g : loaders[i]->animGroups) {
|
||||
if (!(g.skel.ok() && g.clip.ok() && !g.skinRecs.empty())) continue;
|
||||
HslAnimGroup a;
|
||||
a.skel = g.skel;
|
||||
a.clip = g.clip;
|
||||
for (const auto& r : g.skinRecs) a.meshes.push_back(base + static_cast<int>(r.meshIdx));
|
||||
scene.anims.push_back(std::move(a));
|
||||
}
|
||||
{ // the world layer: the footprint's spawns first, then the vista's additions
|
||||
HomeWorld& w = loaders[i]->world;
|
||||
auto add = [](auto& to, auto& from) { to.insert(to.end(), std::make_move_iterator(from.begin()), std::make_move_iterator(from.end())); };
|
||||
add(scene.world.spawns, w.spawns); add(scene.world.seats, w.seats); add(scene.world.hotspots, w.hotspots);
|
||||
add(scene.world.colliders, w.colliders); add(scene.world.wallSpots, w.wallSpots);
|
||||
if (!scene.world.empty()) scene.world.origin = "meta";
|
||||
}
|
||||
for (auto& md : loaders[i]->meshes) {
|
||||
if (md.hasLightmap) ++lightmapped;
|
||||
if (insideOutPalms(md)) for (size_t t = 0; t + 2 < md.indices.size(); t += 3) std::swap(md.indices[t + 1], md.indices[t + 2]);
|
||||
scene.meshes.push_back(std::move(md));
|
||||
scene.meshSource.push_back(static_cast<int>(i));
|
||||
}
|
||||
scanSceneZip(apkPaths[i], scene);
|
||||
}
|
||||
// The ambience is the vista's (the world outside you hear: Calming's, Central's), the footprint's only without one
|
||||
// (Haven's footprint carries a stand-in bed of its own, Central's).
|
||||
for (auto p = apkPaths.rbegin(); p != apkPaths.rend(); ++p)
|
||||
if (scene.ambientOgg.empty()) scene.ambientOgg = findAmbientOgg(*p, scene.ambientSource, scene.ambientName);
|
||||
// HSL rigs: the timeline spans the longest clip.
|
||||
if (scene.animDuration <= 0)
|
||||
for (const auto& a : scene.anims)
|
||||
if (a.clip.nFrames > 1 && a.clip.fps > 0) scene.animDuration = std::max(scene.animDuration, a.clip.nFrames / a.clip.fps);
|
||||
const auto t2 = std::chrono::steady_clock::now();
|
||||
std::fprintf(stderr, "[scene] %zu APK(s), %zu meshes (%zu lightmapped), %zu shader programs, ambient %s (%.3f, %.3f, %.3f); "
|
||||
"parse %.1fs, lightmaps+shaders %.1fs, %zu animated rigs\n",
|
||||
apkPaths.size(), scene.meshes.size(), lightmapped, scene.programs.size(),
|
||||
scene.hasEnvAmbient ? "from env" : "synthesized", scene.ambientRGB[0], scene.ambientRGB[1], scene.ambientRGB[2],
|
||||
std::chrono::duration<double>(t1 - t0).count(), std::chrono::duration<double>(t2 - t1).count(), scene.anims.size());
|
||||
if (scene.meshes.empty()) { error = "home has no meshes"; return false; }
|
||||
simplifyWorldMeshes(scene.world);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<WorldCollider> geometryColliders(const HslScene& scene) {
|
||||
std::vector<WorldCollider> out;
|
||||
for (const MeshData& md : scene.meshes) {
|
||||
if (md.isSkybox || md.useBlend || md.additive || md.overlayKind || md.indices.empty()) continue;
|
||||
WorldCollider c;
|
||||
c.name = md.name;
|
||||
c.kind = WorldCollider::Mesh;
|
||||
float m[16];
|
||||
if (md.hasWorldMatrix) std::memcpy(m, md.worldMatrix, sizeof m); else trsToMat4(md.transform, m);
|
||||
c.positions.resize(md.positions.size());
|
||||
float lo[3] = {1e30f, 1e30f, 1e30f}, hi[3] = {-1e30f, -1e30f, -1e30f};
|
||||
for (size_t v = 0; v + 2 < md.positions.size(); v += 3)
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const float x = m[k] * md.positions[v] + m[4 + k] * md.positions[v + 1] + m[8 + k] * md.positions[v + 2] + m[12 + k];
|
||||
c.positions[v + k] = x;
|
||||
lo[k] = std::min(lo[k], x);
|
||||
hi[k] = std::max(hi[k], x);
|
||||
}
|
||||
if (hi[0] - lo[0] > 200 || hi[1] - lo[1] > 200 || hi[2] - lo[2] > 200) continue; // the vista's backdrop
|
||||
c.indices = md.indices;
|
||||
simplifySurfaceMesh(c.positions, c.indices, kSurfaceCell); // (world space)
|
||||
out.push_back(std::move(c));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
int walkMeshUse(const MeshData& md) {
|
||||
if (md.isSkybox || md.additive || md.overlayKind || md.indices.empty() || md.positions.empty()) return 0;
|
||||
if (!md.useBlend) return 1;
|
||||
// See-through: glass blocks; fog, light shafts, glow cards, spray and the like are only drawn.
|
||||
std::string n = md.name;
|
||||
for (char& c : n) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
for (const char* fx : {"fog", "cloud", "mist", "haze", "smoke", "ray", "shaft", "beam", "glow", "light", "splash", "waterfall", "particle",
|
||||
"dust", "flare", "halo", "volum", "spray", "steam", "fx", "card", "decal", "shadow", "water", "sky"})
|
||||
if (n.find(fx) != std::string::npos) return 0;
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::vector<WalkSource> drawnWalkSources(const HslScene& scene, const HomeWorld& world, const std::vector<const float*>* meshWorld,
|
||||
const std::vector<char>* removed, bool leaveLinked) {
|
||||
// meshes a collider stands for (Studio's surfaces: "<mesh index>:<name>") are left to it (the bake)
|
||||
std::vector<char> linked(scene.meshes.size(), 0);
|
||||
if (leaveLinked)
|
||||
for (const WorldCollider& c : world.colliders) {
|
||||
if (c.mesh.empty() || c.mesh.find("#floor") != std::string::npos) continue; // (a drawn floor: the mesh still counts)
|
||||
const size_t colon = c.mesh.find(':');
|
||||
char* endp = nullptr;
|
||||
const long i = std::strtol(c.mesh.c_str(), &endp, 10);
|
||||
if (colon != std::string::npos && endp == c.mesh.c_str() + colon && i >= 0 && static_cast<size_t>(i) < linked.size()) linked[i] = 1;
|
||||
}
|
||||
// skinned creatures (birds, butterflies) move about: not something to bump into. (Clay's animatedMeshes also
|
||||
// lists floors whose tint or UVs animate, so it isn't used.)
|
||||
for (const HslAnimGroup& a : scene.anims) for (int m : a.meshes) if (m >= 0 && static_cast<size_t>(m) < linked.size()) linked[m] = 1;
|
||||
std::vector<WalkSource> src;
|
||||
src.reserve(scene.meshes.size());
|
||||
std::vector<std::string> names;
|
||||
for (const MeshData& md : scene.meshes) names.push_back(md.name);
|
||||
const std::string prefix = walkSharedPrefix(names);
|
||||
for (size_t m = 0; m < scene.meshes.size(); ++m) {
|
||||
const MeshData& md = scene.meshes[m];
|
||||
const int use = walkMeshUse(md);
|
||||
if (!use || linked[m] || (removed && m < removed->size() && (*removed)[m])) continue;
|
||||
if (scene.apkPaths.size() > 1 && m < scene.meshSource.size() && scene.meshSource[m] != 0) continue; // a vista: scenery, never walked
|
||||
WalkSource s;
|
||||
s.pos = md.positions.data(); s.verts = md.positions.size() / 3; s.idx = md.indices.data(); s.indices = md.indices.size();
|
||||
if (meshWorld && m < meshWorld->size() && (*meshWorld)[m]) std::memcpy(s.world, (*meshWorld)[m], sizeof s.world);
|
||||
else if (md.hasWorldMatrix) std::memcpy(s.world, md.worldMatrix, sizeof s.world);
|
||||
else trsToMat4(md.transform, s.world);
|
||||
s.noFloor = use == 2;
|
||||
if (md.alphaTest) { // cut-out: foliage (blocks; never floor) unless it's laid flat like ground (raked sand, a grass mat)
|
||||
double up = 0, all = 0;
|
||||
for (size_t t = 0; t + 2 < md.indices.size(); t += 3) {
|
||||
float p[3][3];
|
||||
bool ok = true;
|
||||
for (int k = 0; k < 3 && ok; ++k) {
|
||||
const uint32_t v = md.indices[t + k];
|
||||
if (v * 3 + 2 >= md.positions.size()) { ok = false; break; }
|
||||
for (int j = 0; j < 3; ++j) p[k][j] = s.world[j] * md.positions[v * 3] + s.world[4 + j] * md.positions[v * 3 + 1] + s.world[8 + j] * md.positions[v * 3 + 2] + s.world[12 + j];
|
||||
}
|
||||
if (!ok) continue;
|
||||
const float e1[3] = {p[1][0] - p[0][0], p[1][1] - p[0][1], p[1][2] - p[0][2]}, e2[3] = {p[2][0] - p[0][0], p[2][1] - p[0][1], p[2][2] - p[0][2]};
|
||||
const float n[3] = {e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]};
|
||||
const float L = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
|
||||
all += L * 0.5f;
|
||||
if (L > 0 && std::fabs(n[1]) / L > 0.9f) up += L * 0.5f;
|
||||
}
|
||||
s.cutout = !(up >= 4.0 && up >= all * 0.8);
|
||||
}
|
||||
s.maxSize = 200; // bigger: the vista's backdrop
|
||||
s.tag = static_cast<int>(m);
|
||||
s.hint = walkNameHint(md.name, prefix);
|
||||
s.name = md.name;
|
||||
src.push_back(s);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The home's invisible walls: tall solid boxes (Meta's, of any name) with nothing drawn on their sides. They keep a
|
||||
// Quest user in the spot the home was made to be seen from; a box that is a drawn wall's collision is kept.
|
||||
std::vector<char> invisibleWalls(const HomeWorld& world, const std::vector<WalkSource>& drawn) {
|
||||
std::vector<char> out(world.colliders.size(), 0);
|
||||
struct Cand { size_t i; float lo[3], hi[3]; };
|
||||
std::vector<Cand> cands;
|
||||
for (size_t i = 0; i < world.colliders.size(); ++i) {
|
||||
const WorldCollider& c = world.colliders[i];
|
||||
if (c.kind != WorldCollider::Box || c.role != WorldCollider::Solid || !c.mesh.empty()) continue;
|
||||
const float up = std::sqrt(c.world[4] * c.world[4] + c.world[5] * c.world[5] + c.world[6] * c.world[6]);
|
||||
if (2 * c.half[1] * up < 1.5f) continue; // a wall (a low ramp or kerb stays)
|
||||
Cand k{i, {1e30f, 1e30f, 1e30f}, {-1e30f, -1e30f, -1e30f}};
|
||||
for (int v = 0; v < 8; ++v) {
|
||||
const float l[3] = {(v & 1 ? 1 : -1) * c.half[0], (v & 2 ? 1 : -1) * c.half[1], (v & 4 ? 1 : -1) * c.half[2]};
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
const float w = c.world[j] * l[0] + c.world[4 + j] * l[1] + c.world[8 + j] * l[2] + c.world[12 + j];
|
||||
k.lo[j] = std::min(k.lo[j], w - 0.3f); k.hi[j] = std::max(k.hi[j], w + 0.3f);
|
||||
}
|
||||
}
|
||||
cands.push_back(k);
|
||||
}
|
||||
if (cands.empty()) return out;
|
||||
// the drawn triangles near them, in a 1 m hash
|
||||
std::vector<std::array<float, 9>> tris;
|
||||
std::unordered_map<int64_t, std::vector<uint32_t>> cell;
|
||||
auto key = [](int x, int y, int z) { return (static_cast<int64_t>(x & 0x1fffff) << 42) | (static_cast<int64_t>(y & 0x1fffff) << 21) | (z & 0x1fffff); };
|
||||
for (const WalkSource& s : drawn) {
|
||||
std::vector<float> w(s.verts * 3);
|
||||
for (size_t v = 0; v < s.verts; ++v)
|
||||
for (int j = 0; j < 3; ++j) w[v * 3 + j] = s.world[j] * s.pos[v * 3] + s.world[4 + j] * s.pos[v * 3 + 1] + s.world[8 + j] * s.pos[v * 3 + 2] + s.world[12 + j];
|
||||
for (size_t t = 0; t + 2 < s.indices; t += 3) {
|
||||
std::array<float, 9> tr;
|
||||
float lo[3] = {1e30f, 1e30f, 1e30f}, hi[3] = {-1e30f, -1e30f, -1e30f};
|
||||
bool bad = false;
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const uint32_t v = s.idx[t + k];
|
||||
if (v >= s.verts) { bad = true; break; }
|
||||
for (int j = 0; j < 3; ++j) { tr[k * 3 + j] = w[v * 3 + j]; lo[j] = std::min(lo[j], w[v * 3 + j]); hi[j] = std::max(hi[j], w[v * 3 + j]); }
|
||||
}
|
||||
if (bad) continue;
|
||||
bool close = false;
|
||||
for (const Cand& c : cands) close |= lo[0] <= c.hi[0] && hi[0] >= c.lo[0] && lo[1] <= c.hi[1] && hi[1] >= c.lo[1] && lo[2] <= c.hi[2] && hi[2] >= c.lo[2];
|
||||
if (!close || hi[0] - lo[0] > 40 || hi[1] - lo[1] > 40 || hi[2] - lo[2] > 40) continue;
|
||||
const uint32_t id = static_cast<uint32_t>(tris.size());
|
||||
tris.push_back(tr);
|
||||
for (int x = static_cast<int>(std::floor(lo[0])); x <= static_cast<int>(std::floor(hi[0])); ++x)
|
||||
for (int y = static_cast<int>(std::floor(lo[1])); y <= static_cast<int>(std::floor(hi[1])); ++y)
|
||||
for (int z = static_cast<int>(std::floor(lo[2])); z <= static_cast<int>(std::floor(hi[2])); ++z) cell[key(x, y, z)].push_back(id);
|
||||
}
|
||||
}
|
||||
auto dist2 = [](const float* p, const std::array<float, 9>& t) { // point to triangle (Ericson), squared
|
||||
const float* a = &t[0]; const float* b = &t[3]; const float* c = &t[6];
|
||||
float ab[3], ac[3], ap[3];
|
||||
for (int k = 0; k < 3; ++k) { ab[k] = b[k] - a[k]; ac[k] = c[k] - a[k]; ap[k] = p[k] - a[k]; }
|
||||
auto dot = [](const float* u, const float* v) { return u[0] * v[0] + u[1] * v[1] + u[2] * v[2]; };
|
||||
float q[3];
|
||||
const float d1 = dot(ab, ap), d2 = dot(ac, ap);
|
||||
float bp[3], cp[3];
|
||||
for (int k = 0; k < 3; ++k) { bp[k] = p[k] - b[k]; cp[k] = p[k] - c[k]; }
|
||||
const float d3 = dot(ab, bp), d4 = dot(ac, bp), d5 = dot(ab, cp), d6 = dot(ac, cp);
|
||||
const float vc = d1 * d4 - d3 * d2, vb = d5 * d2 - d1 * d6, va = d3 * d6 - d5 * d4;
|
||||
if (d1 <= 0 && d2 <= 0) std::copy_n(a, 3, q);
|
||||
else if (d3 >= 0 && d4 <= d3) std::copy_n(b, 3, q);
|
||||
else if (d6 >= 0 && d5 <= d6) std::copy_n(c, 3, q);
|
||||
else if (vc <= 0 && d1 >= 0 && d3 <= 0) { const float v = d1 / (d1 - d3); for (int k = 0; k < 3; ++k) q[k] = a[k] + v * ab[k]; }
|
||||
else if (vb <= 0 && d2 >= 0 && d6 <= 0) { const float w = d2 / (d2 - d6); for (int k = 0; k < 3; ++k) q[k] = a[k] + w * ac[k]; }
|
||||
else if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) { const float w = (d4 - d3) / ((d4 - d3) + (d5 - d6)); for (int k = 0; k < 3; ++k) q[k] = b[k] + w * (c[k] - b[k]); }
|
||||
else { const float den = 1 / (va + vb + vc), v = vb * den, w = vc * den; for (int k = 0; k < 3; ++k) q[k] = a[k] + ab[k] * v + ac[k] * w; }
|
||||
return (p[0] - q[0]) * (p[0] - q[0]) + (p[1] - q[1]) * (p[1] - q[1]) + (p[2] - q[2]) * (p[2] - q[2]);
|
||||
};
|
||||
for (const Cand& k : cands) {
|
||||
const WorldCollider& c = world.colliders[k.i];
|
||||
int samples = 0, drawnHits = 0;
|
||||
for (int face = 0; face < 4; ++face) { // its four sides (local +-X, +-Z)
|
||||
const int ax = face < 2 ? 0 : 2, other = face < 2 ? 2 : 0;
|
||||
const float sgn = face & 1 ? -1.0f : 1.0f;
|
||||
const int na = std::clamp(static_cast<int>(2 * c.half[other] / 0.3f), 1, 12), nb = std::clamp(static_cast<int>(2 * c.half[1] / 0.3f), 1, 12);
|
||||
for (int i = 0; i < na; ++i)
|
||||
for (int j = 0; j < nb; ++j) {
|
||||
float l[3];
|
||||
l[ax] = sgn * c.half[ax];
|
||||
l[other] = ((i + 0.5f) / na * 2 - 1) * c.half[other];
|
||||
l[1] = ((j + 0.5f) / nb * 2 - 1) * c.half[1];
|
||||
float p[3];
|
||||
for (int q = 0; q < 3; ++q) p[q] = c.world[q] * l[0] + c.world[4 + q] * l[1] + c.world[8 + q] * l[2] + c.world[12 + q];
|
||||
++samples;
|
||||
auto it = cell.find(key(static_cast<int>(std::floor(p[0])), static_cast<int>(std::floor(p[1])), static_cast<int>(std::floor(p[2]))));
|
||||
if (it == cell.end()) continue;
|
||||
for (uint32_t t : it->second) if (dist2(p, tris[t]) < 0.15f * 0.15f) { ++drawnHits; break; }
|
||||
}
|
||||
}
|
||||
out[k.i] = drawnHits < samples * 0.2f;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<const WalkGrid> bakeWalkGrid(const HslScene& scene, const HomeWorld& world, const WalkParams& params, WalkReport* report,
|
||||
const std::vector<const float*>* meshWorld, const std::vector<char>* removed) {
|
||||
std::vector<WalkSource> src = drawnWalkSources(scene, world, meshWorld, removed, true);
|
||||
std::vector<std::vector<float>> sp;
|
||||
std::vector<std::vector<uint32_t>> si;
|
||||
const size_t meshSources = src.size();
|
||||
// Meta's mesh colliders are mostly copies of drawn meshes (their own collision meshes don't read): one is enough.
|
||||
auto keyOf = [](const float* m, const float* pos, size_t verts, size_t indices) {
|
||||
float a[3], b[3];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
a[k] = m[k] * pos[0] + m[4 + k] * pos[1] + m[8 + k] * pos[2] + m[12 + k];
|
||||
const float* q = pos + (verts - 1) * 3;
|
||||
b[k] = m[k] * q[0] + m[4 + k] * q[1] + m[8 + k] * q[2] + m[12 + k];
|
||||
}
|
||||
char buf[160];
|
||||
std::snprintf(buf, sizeof buf, "%zu/%zu/%.2f,%.2f,%.2f/%.2f,%.2f,%.2f", verts, indices, a[0], a[1], a[2], b[0], b[1], b[2]);
|
||||
return std::string(buf);
|
||||
};
|
||||
std::set<std::string> seen;
|
||||
for (size_t i = 0; i < meshSources; ++i) if (src[i].verts) seen.insert(keyOf(src[i].world, src[i].pos, src[i].verts, src[i].indices));
|
||||
HomeWorld cw;
|
||||
cw.colliders.reserve(world.colliders.size());
|
||||
const std::vector<char> invisible = params.metaBounds < 0.5f ? invisibleWalls(world, drawnWalkSources(scene, world, meshWorld, removed, false))
|
||||
: std::vector<char>(world.colliders.size(), 0);
|
||||
for (size_t ci = 0; ci < world.colliders.size(); ++ci) {
|
||||
const WorldCollider& c = world.colliders[ci];
|
||||
if (invisible[ci]) { // walked past: left out (keeps report tags)
|
||||
WorldCollider stub;
|
||||
stub.role = WorldCollider::Trigger;
|
||||
cw.colliders.push_back(std::move(stub));
|
||||
continue;
|
||||
}
|
||||
if (c.kind == WorldCollider::Mesh && c.role == WorldCollider::Solid && c.positions.size() >= 3 &&
|
||||
!seen.insert(keyOf(c.world, c.positions.data(), c.positions.size() / 3, c.indices.size())).second) {
|
||||
WorldCollider stub; // (keeps the indices: report tags)
|
||||
stub.role = WorldCollider::Trigger;
|
||||
cw.colliders.push_back(std::move(stub));
|
||||
continue;
|
||||
}
|
||||
cw.colliders.push_back(c);
|
||||
}
|
||||
walkColliderSources(cw, src, sp, si, static_cast<int>(scene.meshes.size()));
|
||||
// (a vista's meshes aren't sources, so the colliders' extent counts like the meshes': a home whose floor and
|
||||
// furniture are all Studio surfaces is all colliders)
|
||||
return buildWalkGrid(src, walkSeeds(world), params, report);
|
||||
}
|
||||
|
||||
// Baked walkable areas, kept between opens (a big home's bake is most of a second): keyed by this build, every file
|
||||
// the home was read from (path, size, time) and its world layer, so any change bakes afresh. Under
|
||||
// %LOCALAPPDATA%\FrameStudio\cache\walk or ~/.cache/frame-home/walk; the 300 most recent are kept.
|
||||
static std::filesystem::path walkCacheDir() {
|
||||
#ifdef _WIN32
|
||||
if (const char* l = std::getenv("LOCALAPPDATA")) return std::filesystem::u8path(std::string(l) + "\\FrameStudio\\cache\\walk");
|
||||
#endif
|
||||
if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::filesystem::u8path(std::string(x) + "/frame-home/walk");
|
||||
const char* h = std::getenv("HOME");
|
||||
return std::filesystem::u8path(std::string(h ? h : "/tmp") + "/.cache/frame-home/walk");
|
||||
}
|
||||
|
||||
static std::filesystem::path walkCachePath(const HslScene& scene, const HomeWorld& w) {
|
||||
uint64_t k = 1469598103934665603ull;
|
||||
auto mix = [&](const std::string& t) { for (unsigned char c : t) k = (k ^ c) * 1099511628211ull; k = (k ^ 0xff) * 1099511628211ull; };
|
||||
mix(__DATE__ " " __TIME__); // this build's bake
|
||||
std::vector<std::string> files = scene.apkPaths;
|
||||
for (const auto& v : scene.sourceFiles) files.insert(files.end(), v.begin(), v.end());
|
||||
if (!scene.fenvPath.empty()) files.push_back(scene.fenvPath);
|
||||
for (const std::string& f : files) {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path p = std::filesystem::u8path(f);
|
||||
mix(f);
|
||||
mix(std::to_string(std::filesystem::file_size(p, ec)));
|
||||
mix(std::to_string(static_cast<long long>(std::filesystem::last_write_time(p, ec).time_since_epoch().count())));
|
||||
}
|
||||
mix(worldToJson(w));
|
||||
char name[32];
|
||||
std::snprintf(name, sizeof name, "%016llx.fwg", static_cast<unsigned long long>(k));
|
||||
return walkCacheDir() / name;
|
||||
}
|
||||
|
||||
static void pruneWalkCache() {
|
||||
std::error_code ec;
|
||||
std::vector<std::pair<std::filesystem::file_time_type, std::filesystem::path>> f;
|
||||
for (const auto& e : std::filesystem::directory_iterator(walkCacheDir(), ec))
|
||||
if (e.path().extension() == ".fwg") f.push_back({e.last_write_time(ec), e.path()});
|
||||
if (f.size() <= 300) return;
|
||||
std::sort(f.begin(), f.end());
|
||||
for (size_t i = 0; i + 300 < f.size(); ++i) std::filesystem::remove(f[i].second, ec);
|
||||
}
|
||||
|
||||
static HomeWorld finishWalkWorld(const HslScene& scene, HomeWorld w);
|
||||
|
||||
HomeWorld walkWorld(const HslScene& scene) {
|
||||
HomeWorld w = scene.world;
|
||||
simplifyWorldMeshes(w); // (a world.json merged in after loading: Frame Home's from Studio; simplified ones don't change)
|
||||
if (!w.walkGridCache && !w.walkGrid.empty()) {
|
||||
w.walkGridCache = decodeWalkGrid(w.walkGrid);
|
||||
if (!w.walkGridCache) std::fprintf(stderr, "[walk] the saved walkable area doesn't read: baking one\n");
|
||||
}
|
||||
static const bool noWalkCache = std::getenv("FH_NO_WALK_CACHE") != nullptr;
|
||||
const std::filesystem::path cached = w.walkGridCache || noWalkCache ? std::filesystem::path() : walkCachePath(scene, w);
|
||||
if (!w.walkGridCache && !cached.empty()) {
|
||||
std::ifstream f(cached, std::ios::binary);
|
||||
const std::string text((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
if (!text.empty() && (w.walkGridCache = decodeWalkGrid(text))) {
|
||||
std::error_code ec;
|
||||
std::filesystem::last_write_time(cached, std::filesystem::file_time_type::clock::now(), ec);
|
||||
std::fprintf(stderr, "[walk] walkable area from the cache: %.1f m2\n", w.walkGridCache->walkArea);
|
||||
return finishWalkWorld(scene, std::move(w));
|
||||
}
|
||||
}
|
||||
if (!w.walkGridCache) {
|
||||
WalkReport r;
|
||||
w.walkGridCache = bakeWalkGrid(scene, w, WalkParams{}, &r);
|
||||
std::fprintf(stderr, "[walk] walkable area baked: %.1f m2 in %d area(s) (%d left out, %.1f m2), %.1f m2 of seams bridged; %zu triangles, %dx%d cells, %zu spans, %.0f ms\n",
|
||||
r.walkArea, r.areas, r.unreached, r.unreachedArea, r.bridgedArea, r.triangles, r.nx, r.nz, r.spans, r.ms);
|
||||
if (w.walkGridCache && !cached.empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(cached.parent_path(), ec);
|
||||
const std::filesystem::path tmp = cached.string() + ".part";
|
||||
{ std::ofstream o(tmp, std::ios::binary); o << encodeWalkGrid(*w.walkGridCache); }
|
||||
std::filesystem::rename(tmp, cached, ec);
|
||||
if (ec) std::filesystem::remove(tmp, ec);
|
||||
pruneWalkCache();
|
||||
}
|
||||
// (a home no one has set up in Frame Studio walks on its meshes as they are: flat enough is floor, steeper
|
||||
// blocks — what Studio's Auto-generate makes their surfaces; nothing more is added)
|
||||
} else {
|
||||
std::fprintf(stderr, "[walk] walkable area saved with the home: %.1f m2\n", w.walkGridCache->walkArea);
|
||||
}
|
||||
return finishWalkWorld(scene, std::move(w));
|
||||
}
|
||||
|
||||
// (the rest of walkWorld: what doesn't come from the walk bake)
|
||||
static HomeWorld finishWalkWorld(const HslScene& scene, HomeWorld w) {
|
||||
// Gravity homes walk on colliders (GravityWalker): the geometry's floors and walls stand in while nothing blocks.
|
||||
if (!w.gravity.empty()) {
|
||||
bool solid = false;
|
||||
for (const WorldCollider& c : w.colliders) solid |= c.role == WorldCollider::Solid || c.role == WorldCollider::Wall || c.role == WorldCollider::Object;
|
||||
if (!solid) {
|
||||
std::vector<WorldCollider> geo = geometryColliders(scene);
|
||||
w.colliders.insert(w.colliders.end(), std::make_move_iterator(geo.begin()), std::make_move_iterator(geo.end()));
|
||||
}
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
const ShaderProgramSrc* findProgram(const HslScene& scene, const std::string& shaderPath) {
|
||||
if (shaderPath.empty()) return nullptr;
|
||||
auto it = scene.programs.find(shaderPath);
|
||||
if (it != scene.programs.end()) return &it->second;
|
||||
const std::string want = surfaceName(shaderPath);
|
||||
if (want.empty()) return nullptr;
|
||||
for (const auto& kv : scene.programs)
|
||||
if (surfaceName(kv.first) == want) return &kv.second;
|
||||
return nullptr;
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
// A Meta HSL home (v203–v210) loaded for rendering. On Quest a home is a FOOTPRINT (the room, e.g.
|
||||
// Haven2025) plus a VISTA (the world outside, e.g. Calming) loaded together; the vista also ships the
|
||||
// baked lightmaps for the footprint's meshes. Both load here as one scene: the editor's SceneLoader
|
||||
// meshes from each APK, every shader program they ship (forward pass, SMOL-V decoded + names restored),
|
||||
// the baked lightmaps resolved across the pair, and the real ambient light.
|
||||
#include "core/types.h"
|
||||
#include "core/world.h"
|
||||
#include "walkgrid.h"
|
||||
#include "loaders/rendskel_parser.h"
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct ShaderProgramSrc {
|
||||
std::string path; // scene.zip asset path without "content/" (matches MeshData::shaderPath)
|
||||
std::vector<u32> vert, frag;
|
||||
// Made by the bake (shader_bake.h; a .fenv carries them), else by the renderer at load. prepared: the two below are
|
||||
// made (fragAlpha empty: the shader has no alpha-only form).
|
||||
bool prepared = false;
|
||||
std::vector<u32> vertInvariant, fragAlpha;
|
||||
// Per distinct set of material values: the fragment shader with them baked in, by shaderbake::variantKey (frag
|
||||
// empty: nothing bakes in, the shader as shipped).
|
||||
struct Variant { uint64_t key = 0; std::vector<u32> frag, fragAlpha; };
|
||||
std::vector<Variant> variants;
|
||||
};
|
||||
|
||||
// A skeleton + its clip driving skinned meshes (birds, butterflies, creatures). Bone indices of those meshes
|
||||
// are already skeleton joint indices, so the sampled per-joint skin matrices go straight to the GPU.
|
||||
struct HslAnimGroup {
|
||||
RendSkel skel;
|
||||
RendClip clip;
|
||||
std::vector<int> meshes; // HslScene::meshes indices
|
||||
};
|
||||
|
||||
// What one APK of the home is (the editor's info panel).
|
||||
struct HslSourceInfo {
|
||||
std::string package, version; // from AndroidManifest.xml: com.meta.shell.env.footprint.haven2025, 206.0.0.0.70
|
||||
std::string format; // "HSL", "Clay (.opa)" or "Clay (.ovrscene)" (SideQuest-style custom homes: glTF in scene.zip)
|
||||
std::string role; // "Footprint" (the room), "Vista" (the world outside) or "Home" (both in one)
|
||||
};
|
||||
|
||||
struct HslScene {
|
||||
std::vector<std::string> apkPaths; // footprint first, then the vista (if any)
|
||||
std::vector<MeshData> meshes; // all APKs' meshes, in apkPaths order
|
||||
std::vector<int> meshSource; // per mesh: index into apkPaths
|
||||
std::vector<HslAnimGroup> anims;
|
||||
std::map<std::string, ShaderProgramSrc> programs; // by asset path
|
||||
float ambientRGB[3] = {0.95f, 0.90f, 0.83f}; // radiance; replaced by the env's .lprb L00 when present
|
||||
bool hasEnvAmbient = false;
|
||||
// Clay / glTF homes: evaluates the home's authored animation at t seconds (node motion, skinning, UV flipbooks
|
||||
// and scrolls such as the fire, tint fades, vertex-animation textures) by rewriting the affected meshes'
|
||||
// positions / uvs / curTint on the CPU. Empty for HSL homes: their rigs and shaders animate from the frame time.
|
||||
std::function<void(float, std::vector<MeshData>&)> animate;
|
||||
// The same animation as data (anim_bake.h), for a bake: what a .fenv plays it from. Set with animate; it reads the
|
||||
// animator, so call it from the thread that animates the scene.
|
||||
std::function<void(std::vector<u8>&)> saveAnimation;
|
||||
std::vector<int> animatedMeshes; // the meshes animate() changes (their vertices re-upload every frame)
|
||||
float animDuration = 0; // the authored loop length in seconds (0 = none authored)
|
||||
std::vector<u8> ambientOgg; // the home's looping ambience (_BACKGROUND_LOOP.ogg), if it ships one
|
||||
std::string ambientSource; // where it came from: "Ogg loop", "FMOD bank" or empty (none)
|
||||
std::string ambientName; // its own name in the home: the Ogg file's, or the bank sound's ("amb_CalmingVista_...")
|
||||
std::vector<HslSourceInfo> sources; // per apkPaths entry
|
||||
std::vector<std::vector<std::string>> sourceFiles; // per apkPaths entry: the files an imported model read
|
||||
std::string fenvPath, fenvEdits; // opened from a .fenv: the file, and its edits.json (extracted; "" if none)
|
||||
HomeWorld world; // spawns, seats, colliders, walkable areas (Meta's, or a .fenv's world.json)
|
||||
};
|
||||
|
||||
// Loads one or more APKs as a single home (footprint + vista). Returns false (with `error`) when an APK
|
||||
// is not an HSL home or fails to parse.
|
||||
bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std::string& error);
|
||||
inline bool loadHslScene(const std::string& apkPath, HslScene& scene, std::string& error) {
|
||||
return loadHslScene(std::vector<std::string>{apkPath}, scene, error);
|
||||
}
|
||||
|
||||
// The world to walk in: the home's own (Meta's converted, or a .fenv's) when it has floors or walls; else floors and
|
||||
// walls taken from the drawn geometry (opaque, not the sky, not distant scenery) — Clay homes and homes like Theater
|
||||
// ship no collision at all. Spawns, seats and wall spots are kept either way.
|
||||
// The world carries its walkable area (walkGridCache): the one Frame Studio baked (world.json), else one baked here from
|
||||
// the drawn geometry plus the colliders (bakeWalkGrid). Gravity homes also get the geometry's floors and walls as
|
||||
// colliders while nothing blocks (the gravity walker reads colliders).
|
||||
HomeWorld walkWorld(const HslScene& scene);
|
||||
std::vector<WorldCollider> geometryColliders(const HslScene& scene); // floors + walls from the drawn meshes
|
||||
|
||||
// Bakes the home's walkable area from everything drawn that's solid (opaque, not the sky or the distant backdrop;
|
||||
// see-through solids block but aren't floor) and the world's colliders (roles override: a mesh a collider is
|
||||
// made from — "mesh" names it — is left to that collider). meshWorld: per mesh, its world matrix when it was moved
|
||||
// (Frame Studio; nullptr: as loaded); removed: per mesh, left out (deleted). Report tags: mesh index, or
|
||||
// meshes.size() + collider index.
|
||||
std::shared_ptr<const WalkGrid> bakeWalkGrid(const HslScene& scene, const HomeWorld& world, const WalkParams& params, WalkReport* report = nullptr,
|
||||
const std::vector<const float*>* meshWorld = nullptr, const std::vector<char>* removed = nullptr);
|
||||
// The home's drawn solid meshes as walk-bake sources, in world space (meshWorld / removed: Frame Studio's edits).
|
||||
// leaveLinked: meshes a Studio surface stands for are left out (the bake; the world generators want them all).
|
||||
std::vector<WalkSource> drawnWalkSources(const HslScene& scene, const HomeWorld& world, const std::vector<const float*>* meshWorld = nullptr,
|
||||
const std::vector<char>* removed = nullptr, bool leaveLinked = true);
|
||||
// Is this mesh in the walkable-area bake: 0 no, 1 solid, 2 blocks but isn't floor (see-through).
|
||||
int walkMeshUse(const MeshData& md);
|
||||
|
||||
// Finds the program a mesh's material references (exact asset path, else same ".surface" name).
|
||||
const ShaderProgramSrc* findProgram(const HslScene& scene, const std::string& shaderPath);
|
||||
@@ -0,0 +1,850 @@
|
||||
#pragma once
|
||||
// Moving around a home in Frame Home, the way Meta's homes do it:
|
||||
// you start at the home's spawn point, its floor on your real floor, facing where the home faces
|
||||
// left stick walks (head-relative, moveSpeed m/s at full push, default 2; a light push strolls), easing up to speed
|
||||
// and to a stop in a fraction of a second; walls stop you and slide you along them, the floor follows ramps
|
||||
// exactly and eases you up and down steps (roomscale walking too). Walking off (setOptions walk 0): the stick
|
||||
// does nothing.
|
||||
// right stick left / right turns about your head: snap turns of snapAngle degrees (30 / 45 / 90) on a flick, or
|
||||
// smooth turning (up to 120 degrees/s, by how far the stick is pushed)
|
||||
// right stick forward aims a teleport arc from the right hand, drawn by Frame Home itself (guide(), guide_geom.h)
|
||||
// with a marker where you'll land. The arc stops at the first thing it meets: a floor you can stand on (or a
|
||||
// seat) is the landing; a wall, a ceiling or the side of furniture means nowhere — unless it's low, then you land
|
||||
// at its foot. Let go to go there: instantly behind a quick fade to black (a blink), or as a 0.25 s
|
||||
// eased dash along the ground (slide). Landing near a teleport hotspot snaps onto it. Teleport off: the stick
|
||||
// forward does nothing. In gravity homes a slide is an instant blink (the walker re-orients you on landing).
|
||||
// seats show a faint marker near you; pointing a hand at one lights it up (with a short pointer line), and pulling
|
||||
// that hand's trigger sits you in it (a slide or a blink, as teleports go).
|
||||
// landing on a seat sits you in it: eyes at a seated height over the seat, facing its way. Seated you can turn
|
||||
// (snap turn, or turn your head) but stay put; walking, teleporting or stepping more than half a metre away
|
||||
// stands you up — beside the seat (its exit point) when you walk off.
|
||||
// with the Steam menu open the controllers belong to the menu: nothing moves you until it's closed.
|
||||
// falling (off an edge, roomscale) is real; falling further than the world's fallLimit puts you back at the spawn.
|
||||
// homes with gravity zones (walls you walk up, planets) are walked by GravityWalker (gravity.h) instead.
|
||||
// The rig is worldFromTracking: SteamVR's standing space (y = 0 on your floor) placed into the home.
|
||||
#include "gravity.h"
|
||||
#include "guide_geom.h"
|
||||
#include "world_runtime.h"
|
||||
|
||||
#include <openvr.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class Locomotion {
|
||||
public:
|
||||
// Input actions (written next to Frame Home on first run) and the teleport ring overlay.
|
||||
bool init(const std::filesystem::path& dir) {
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
fs::create_directories(dir, ec);
|
||||
const char* binding = R"({ "bindings": { "/actions/main": {
|
||||
"sources": [
|
||||
{ "path": "/user/hand/left/input/thumbstick", "mode": "joystick", "inputs": { "position": { "output": "/actions/main/in/left_stick" } } },
|
||||
{ "path": "/user/hand/right/input/thumbstick", "mode": "joystick", "inputs": { "position": { "output": "/actions/main/in/right_stick" } } },
|
||||
{ "path": "/user/hand/left/input/trigger", "mode": "button", "inputs": { "click": { "output": "/actions/main/in/left_trigger" } } },
|
||||
{ "path": "/user/hand/right/input/trigger", "mode": "button", "inputs": { "click": { "output": "/actions/main/in/right_trigger" } } } ],
|
||||
"poses": [
|
||||
{ "output": "/actions/main/in/left_aim", "path": "/user/hand/left/pose/tip" },
|
||||
{ "output": "/actions/main/in/right_aim", "path": "/user/hand/right/pose/tip" } ] } },
|
||||
"controller_type": "%TYPE%", "description": "Frame Home: walk, turn, teleport", "name": "Frame Home" })";
|
||||
const char* types[] = {"frame_controller", "knuckles", "oculus_touch", "vive_cosmos_controller"};
|
||||
std::string defaults;
|
||||
for (const char* t : types) {
|
||||
std::string b = binding;
|
||||
b.replace(b.find("%TYPE%"), 6, t);
|
||||
std::ofstream(dir / (std::string("bindings_") + t + ".json")) << b;
|
||||
defaults += std::string(defaults.empty() ? "" : ",\n ") + R"({ "controller_type": ")" + t + R"(", "binding_url": "bindings_)" + t + R"(.json" })";
|
||||
}
|
||||
// The headset's eye tracker (foveated rendering follows your gaze).
|
||||
std::ofstream(dir / "bindings_frame_hmd.json") << R"({ "bindings": { "/actions/main": {
|
||||
"eyetracking": [ { "path": "/user/head/eyetracking", "output": "/actions/main/in/gaze" } ] } },
|
||||
"controller_type": "frame_hmd", "description": "Frame Home: eye tracking", "name": "Frame Home" })";
|
||||
defaults += R"(,
|
||||
{ "controller_type": "frame_hmd", "binding_url": "bindings_frame_hmd.json" })";
|
||||
std::ofstream(dir / "actions.json") << R"({
|
||||
"default_bindings": [
|
||||
)" << defaults << R"( ],
|
||||
"actions": [
|
||||
{ "name": "/actions/main/in/left_stick", "type": "vector2" },
|
||||
{ "name": "/actions/main/in/right_stick", "type": "vector2" },
|
||||
{ "name": "/actions/main/in/left_aim", "type": "pose" },
|
||||
{ "name": "/actions/main/in/right_aim", "type": "pose" },
|
||||
{ "name": "/actions/main/in/left_trigger", "type": "boolean" },
|
||||
{ "name": "/actions/main/in/right_trigger", "type": "boolean" },
|
||||
{ "name": "/actions/main/in/gaze", "type": "eyetracking" } ],
|
||||
"action_sets": [ { "name": "/actions/main", "usage": "leftright" } ],
|
||||
"localization": [ { "language_tag": "en_US", "/actions/main": "Frame Home",
|
||||
"/actions/main/in/left_stick": "Walk", "/actions/main/in/right_stick": "Turn and teleport",
|
||||
"/actions/main/in/left_aim": "Left hand", "/actions/main/in/right_aim": "Teleport aim",
|
||||
"/actions/main/in/left_trigger": "Sit: left hand", "/actions/main/in/right_trigger": "Sit: right hand", "/actions/main/in/gaze": "Where you look" } ]
|
||||
})";
|
||||
vr::IVRInput* in = vr::VRInput();
|
||||
if (!in || in->SetActionManifestPath(fs::absolute(dir / "actions.json").c_str()) != vr::VRInputError_None) return false;
|
||||
in->GetActionSetHandle("/actions/main", &set_);
|
||||
in->GetActionHandle("/actions/main/in/left_stick", &leftStick_);
|
||||
in->GetActionHandle("/actions/main/in/right_stick", &rightStick_);
|
||||
in->GetActionHandle("/actions/main/in/right_aim", &rightAim_);
|
||||
in->GetActionHandle("/actions/main/in/left_aim", &leftAim_);
|
||||
in->GetActionHandle("/actions/main/in/right_trigger", &trigger_[0]);
|
||||
in->GetActionHandle("/actions/main/in/left_trigger", &trigger_[1]);
|
||||
in->GetActionHandle("/actions/main/in/gaze", &gaze_);
|
||||
// The landing ring: a flat overlay placed in the room each frame while aiming — only when Frame Home can't draw
|
||||
// the arc itself (setRingFallback).
|
||||
if (vr::VROverlay() && vr::VROverlay()->CreateOverlay("framehome.teleport", "Teleport", &ring_) == vr::VROverlayError_None) {
|
||||
const int n = 128;
|
||||
std::vector<uint8_t> px(static_cast<size_t>(n) * n * 4);
|
||||
for (int y = 0; y < n; ++y)
|
||||
for (int x = 0; x < n; ++x) {
|
||||
const float dx = (x + 0.5f) / n * 2 - 1, dy = (y + 0.5f) / n * 2 - 1, r = std::sqrt(dx * dx + dy * dy);
|
||||
const float ring = std::clamp(1 - std::fabs(r - 0.82f) / 0.12f, 0.0f, 1.0f), dot = std::clamp((0.18f - r) / 0.05f, 0.0f, 1.0f);
|
||||
const float a = std::max(ring, dot);
|
||||
uint8_t* p = &px[(static_cast<size_t>(y) * n + x) * 4];
|
||||
p[0] = 255; p[1] = 168; p[2] = 70; p[3] = static_cast<uint8_t>(a * 230);
|
||||
}
|
||||
vr::VROverlay()->SetOverlayRaw(ring_, px.data(), n, n, 4);
|
||||
vr::VROverlay()->SetOverlayWidthInMeters(ring_, 0.55f);
|
||||
}
|
||||
return ok_ = true;
|
||||
}
|
||||
|
||||
// A new home: its world, and the player at its spawn.
|
||||
void setWorld(HomeWorld world) {
|
||||
world_ = std::move(world);
|
||||
runtime_.build(world_);
|
||||
fallLimit_ = world_.fallLimit > 1 ? world_.fallLimit : 30.0f;
|
||||
gravityMode_ = !world_.gravity.empty();
|
||||
toSpawn();
|
||||
if (gravityMode_) walker_.build(world_);
|
||||
}
|
||||
const HomeWorld& world() const { return world_; } // as walked (the debug overlay draws it)
|
||||
const WalkGrid* walkGrid() const { return runtime_.grid(); }
|
||||
|
||||
// Back to the first spawn, standing, nothing under way (a new home, or a fall too far).
|
||||
void toSpawn() {
|
||||
float x = 0, z = 0, y = 0, yaw = 0;
|
||||
if (!world_.spawns.empty()) { const WorldPoint& s = world_.spawns[0]; x = s.pos[0]; y = s.pos[1]; z = s.pos[2]; yaw = s.yaw; }
|
||||
float f;
|
||||
if (runtime_.floorAt(x, z, y + 0.5f, 0.6f, 3.0f, f)) y = f;
|
||||
rigYaw_ = yaw; rigX_ = x; rigZ_ = z; floorY_ = targetFloor_ = y;
|
||||
seated_ = -1;
|
||||
aiming_ = valid_ = false; // a teleport aimed in the last home doesn't land here
|
||||
seat_ = -1;
|
||||
vel_[0] = vel_[1] = stepV_ = 0;
|
||||
pointed_[0] = pointed_[1] = -1;
|
||||
guide_ = LocoGuide();
|
||||
if (ring_ && vr::VROverlay()) vr::VROverlay()->HideOverlay(ring_);
|
||||
if (blinkT_ >= 0) blinkIn(); // a jump pending in the last home is dropped, eyes open
|
||||
blinkT_ = slideT_ = -1;
|
||||
fallV_ = fallen_ = 0;
|
||||
}
|
||||
|
||||
// Movement options (Settings), applied live. walk: 0 off, 1 smooth. teleport: 0 off, 1 instant (blink), 2 slide.
|
||||
// turn: 0 snap (by snapAngleDeg), 1 smooth. moveSpeed: m/s at full stick.
|
||||
void setOptions(int walk, int teleport, int turn, int snapAngleDeg, float moveSpeed) {
|
||||
walk_ = walk; teleport_ = teleport; turn_ = turn;
|
||||
snap_ = static_cast<float>(std::clamp(snapAngleDeg, 5, 180)) * 0.01745329f;
|
||||
speed_ = std::clamp(moveSpeed, 0.1f, 10.0f);
|
||||
walker_.setSpeed(speed_);
|
||||
if (teleport_ == 0 && aiming_) dropAim(); // an arc being aimed when teleport is switched off
|
||||
}
|
||||
|
||||
bool gravityMode() const { return gravityMode_; }
|
||||
int respawns() const { return respawns_ + (gravityMode_ ? walker_.respawns() : 0); }
|
||||
|
||||
int seated() const { return seated_; }
|
||||
// Where you look, in standing space, for the frame being rendered (false: no eye tracking right now — none on
|
||||
// this headset, eyes closed, or the Steam menu has the input).
|
||||
bool gaze(float origin[3], float target[3]) const {
|
||||
if (!ok_ || !gaze_) return false;
|
||||
vr::VREyeTrackingData_t e = {};
|
||||
if (vr::VRInput()->GetEyeTrackingDataForNextFrame(gaze_, vr::TrackingUniverseStanding, &e, sizeof e) != vr::VRInputError_None ||
|
||||
!e.bActive || !e.bValid)
|
||||
return false;
|
||||
for (int k = 0; k < 3; ++k) { origin[k] = e.vGazeOrigin.v[k]; target[k] = e.vGazeTarget.v[k]; }
|
||||
return true;
|
||||
}
|
||||
void setPoseValid(bool v) { poseValid_ = v; }
|
||||
// Test hooks (tools/loco_test): what landing on a seat, and walking off it, do.
|
||||
void testSit(int seat, const float headT[16]) { float R[16], h[3]; rig(R); apply(R, &headT[12], h); sit(seat, R, headT, h); }
|
||||
void testWalkOff(const float headT[16]) { float R[16], h[3]; rig(R); apply(R, &headT[12], h); if (seated_ >= 0) standUp(h, true); }
|
||||
// Sticks (and the right hand's aim pose, 3x4 row-major as OpenVR gives it) without SteamVR; from now on update()
|
||||
// reads these instead of the controllers.
|
||||
void testInput(float lx, float ly, float rx, float ry, const float* aim34 = nullptr) {
|
||||
test_ = true;
|
||||
testIn_[0] = lx; testIn_[1] = ly; testIn_[2] = rx; testIn_[3] = ry;
|
||||
testAimOk_ = aim34 != nullptr;
|
||||
if (aim34) std::copy(aim34, aim34 + 12, testAim_);
|
||||
}
|
||||
// The triggers (0 right, 1 left) and the left hand's aim pose, as testInput's.
|
||||
void testButtons(bool rightTrigger, bool leftTrigger, const float* leftAim34 = nullptr) {
|
||||
test_ = true;
|
||||
testTrig_[0] = rightTrigger; testTrig_[1] = leftTrigger;
|
||||
testLeftOk_ = leftAim34 != nullptr;
|
||||
if (leftAim34) std::copy(leftAim34, leftAim34 + 12, testLeft_);
|
||||
}
|
||||
bool testTarget(float t[3]) const { std::copy(target_, target_ + 3, t); return aiming_ && valid_; }
|
||||
int testTargetSeat() const { return aiming_ && valid_ ? seat_ : -1; }
|
||||
int pointedSeat(int hand) const { return pointed_[hand & 1]; } // 0 right, 1 left; -1: none
|
||||
// A straight ray (world) against the seats' pointable volumes, stopped by walls and floors in front: the seat
|
||||
// hit, or -1, and how far along (metres). Hitting the chair itself (its back, from behind) counts as the seat.
|
||||
int seatOnRay(const float o[3], const float dir[3], float maxLen, float& t) const {
|
||||
const float d[3] = {dir[0] * maxLen, dir[1] * maxLen, dir[2] * maxLen};
|
||||
float ts, tb = 2, n[3];
|
||||
const WorldCollider* shape = nullptr;
|
||||
int i = seatOnSegment(o, d, ts);
|
||||
if (!runtime_.hasFloor() || runtime_.trace(o, d, tb, n, &shape) == WorldRuntime::None) tb = 2;
|
||||
if (i >= 0 && ts <= tb) { t = ts * maxLen; return i; }
|
||||
if (tb > 1) return -1;
|
||||
const float q[3] = {o[0] + d[0] * tb, o[1] + d[1] * tb, o[2] + d[2] * tb};
|
||||
if ((i = seatOf(q, shape)) >= 0) t = tb * maxLen;
|
||||
return i;
|
||||
}
|
||||
// What to draw this frame (after update()): the arc and its landing, seat markers, the pointer.
|
||||
const LocoGuide& guide() const { return guide_; }
|
||||
// Frame Home draws the guide itself: the SteamVR ring overlay only stands in when it can't (fallback = true).
|
||||
void setRingFallback(bool on) { ringOn_ = on; if (!on && ring_ && vr::VROverlay()) vr::VROverlay()->HideOverlay(ring_); }
|
||||
bool sliding() const { return slideT_ >= 0; }
|
||||
|
||||
// worldFromTracking (column-major).
|
||||
void rig(float m[16]) const {
|
||||
if (gravityMode_) { // the walker's frame, with the tracking floor lifted by lift_ (an unset floor)
|
||||
walker_.rig(m);
|
||||
for (int k = 0; k < 3; ++k) m[12 + k] += m[4 + k] * lift_;
|
||||
return;
|
||||
}
|
||||
const float c = std::cos(rigYaw_), s = std::sin(rigYaw_);
|
||||
const float r[16] = {c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, rigX_, seated_ >= 0 ? seatRigY_ : floorY_ + lift_, rigZ_, 1};
|
||||
std::copy(r, r + 16, m);
|
||||
}
|
||||
|
||||
// Once per frame, with the head's tracking-space pose (column-major, as from OpenVR).
|
||||
void update(float dt, const float headT[16]) {
|
||||
dt = std::min(dt, 0.05f);
|
||||
checkEyeHeight(dt, headT[13]);
|
||||
guide_.seats.clear();
|
||||
guide_.pointer = false;
|
||||
if (gravityMode_ && !ok_ && !test_) { // no controllers: the walker still holds you to the ground (and lets you fall)
|
||||
float h[16];
|
||||
std::copy(headT, headT + 16, h);
|
||||
h[13] += lift_;
|
||||
walker_.update(dt, h, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
float R[16];
|
||||
rig(R);
|
||||
float head[3];
|
||||
apply(R, &headT[12], head);
|
||||
if (ok_ || test_) {
|
||||
vr::InputAnalogActionData_t l = {}, r = {};
|
||||
bool trig[2] = {false, false};
|
||||
if (ok_) {
|
||||
vr::VRActiveActionSet_t active = {};
|
||||
active.ulActionSet = set_;
|
||||
vr::VRInput()->UpdateActionState(&active, sizeof active, 1);
|
||||
vr::VRInput()->GetAnalogActionData(leftStick_, &l, sizeof l, vr::k_ulInvalidInputValueHandle);
|
||||
vr::VRInput()->GetAnalogActionData(rightStick_, &r, sizeof r, vr::k_ulInvalidInputValueHandle);
|
||||
for (int h = 0; h < 2; ++h) {
|
||||
vr::InputDigitalActionData_t b = {};
|
||||
if (trigger_[h] && vr::VRInput()->GetDigitalActionData(trigger_[h], &b, sizeof b, vr::k_ulInvalidInputValueHandle) == vr::VRInputError_None)
|
||||
trig[h] = b.bActive && b.bState;
|
||||
}
|
||||
if (l.bActive != ownInput_) { ownInput_ = l.bActive; std::fprintf(stderr, "frame-home: controller input %s\n", ownInput_ ? "ours" : "taken (Steam menu open?)"); }
|
||||
} else {
|
||||
l.bActive = r.bActive = true;
|
||||
l.x = testIn_[0]; l.y = testIn_[1]; r.x = testIn_[2]; r.y = testIn_[3];
|
||||
trig[0] = testTrig_[0]; trig[1] = testTrig_[1];
|
||||
}
|
||||
// The Steam menu open: the controllers are the menu's (pointing at and clicking the dock), not the home's.
|
||||
// An aim in progress is dropped, not jumped to (the stick reads as let go).
|
||||
menuAim_ = false;
|
||||
bool menu = false;
|
||||
if (ok_ && vr::VROverlay() && vr::VROverlay()->IsDashboardVisible()) {
|
||||
l.bActive = r.bActive = false;
|
||||
menu = true;
|
||||
if (aiming_) dropAim();
|
||||
}
|
||||
// Walk: the stick in the direction the head faces (flattened). Seated, it stands you up first. Walking
|
||||
// off, the stick is dead (a teleport-only user with a worn stick doesn't drift); mid-dash or blink too.
|
||||
const bool walk = l.bActive && walk_ != 0 && slideT_ < 0 && blinkT_ < 0;
|
||||
float lx = 0, ly = 0;
|
||||
if (walk) grav::shapeStick(l.x, l.y, lx, ly);
|
||||
if (gravityMode_) { updateGravity(dt, headT, lx, ly, r); return; }
|
||||
// Walking and teleport both off: the stick still stands you up from a seat (else only stepping away would)
|
||||
if (seated_ >= 0 && walk_ == 0 && teleport_ == 0 && l.bActive) {
|
||||
float sx = 0, sy = 0;
|
||||
grav::shapeStick(l.x, l.y, sx, sy);
|
||||
if ((sx != 0 || sy != 0) && slideT_ < 0 && blinkT_ < 0) { leaveSeat(headT, head, R); rig(R); apply(R, &headT[12], head); }
|
||||
}
|
||||
if ((lx != 0 || ly != 0) && seated_ >= 0) { leaveSeat(headT, head, R); rig(R); apply(R, &headT[12], head); }
|
||||
// The velocity eases toward the stick's (grav::approach); a jump or a seat stops it dead.
|
||||
grav::V want;
|
||||
if (lx != 0 || ly != 0) {
|
||||
float fwd[3];
|
||||
const float hz[3] = {-headT[8], -headT[9], -headT[10]}; // the head's -Z in tracking space
|
||||
dir(R, hz, fwd);
|
||||
const float fl = std::sqrt(fwd[0] * fwd[0] + fwd[2] * fwd[2]);
|
||||
if (fl > 1e-4f) {
|
||||
const float fx = fwd[0] / fl, fz = fwd[2] / fl;
|
||||
want = grav::V((fx * ly - fz * lx) * speed_, 0, (fz * ly + fx * lx) * speed_);
|
||||
}
|
||||
}
|
||||
grav::V v(vel_[0], 0, vel_[1]);
|
||||
if (slideT_ >= 0 || blinkT_ >= 0 || seated_ >= 0 || walk_ == 0) v = grav::V();
|
||||
else grav::approach(v, want, speed_, dt);
|
||||
vel_[0] = v.x; vel_[1] = v.z;
|
||||
if (v.x != 0 || v.z != 0) {
|
||||
float x = head[0], z = head[2];
|
||||
const float dx = v.x * dt, dz = v.z * dt;
|
||||
if (runtime_.hasFloor()) runtime_.move(x, z, floorY_, dx, dz); else { x += dx; z += dz; }
|
||||
float f;
|
||||
if (!runtime_.hasFloor() || runtime_.floorAt(x, z, floorY_, 0.45f, 1.2f, f)) { // never walk off an edge
|
||||
rigX_ += x - head[0]; rigZ_ += z - head[2];
|
||||
vel_[0] = (x - head[0]) / dt; vel_[1] = (z - head[2]) / dt; // what a wall left of it: no pent-up push
|
||||
head[0] = x; head[2] = z;
|
||||
} else {
|
||||
vel_[0] = vel_[1] = 0;
|
||||
}
|
||||
}
|
||||
// Turn, about the head (so you spin in place, not round the room's origin); momentum turns with you.
|
||||
const float rx = r.bActive ? r.x : 0, ry = r.bActive ? r.y : 0;
|
||||
if (const float a = turnBy(dt, rx, ry)) {
|
||||
const float c = std::cos(a), s = std::sin(a);
|
||||
const float ox = rigX_ - head[0], oz = rigZ_ - head[2];
|
||||
rigX_ = head[0] + c * ox + s * oz;
|
||||
rigZ_ = head[2] - s * ox + c * oz;
|
||||
rigYaw_ += a;
|
||||
const float vx = vel_[0], vz = vel_[1];
|
||||
vel_[0] = c * vx + s * vz; vel_[1] = -s * vx + c * vz;
|
||||
rig(R);
|
||||
}
|
||||
// Teleport: aim while the stick is forward, go on release (one jump at a time).
|
||||
if (teleport_ == 0) { if (aiming_) dropAim(); }
|
||||
else if (ry > 0.7f && std::fabs(rx) < 0.6f) aim(R, head, dt);
|
||||
else if (aiming_ && ry < 0.4f) {
|
||||
aiming_ = false;
|
||||
guide_.aiming = false;
|
||||
if (ring_) vr::VROverlay()->HideOverlay(ring_);
|
||||
if (valid_ && slideT_ < 0 && blinkT_ < 0) {
|
||||
std::copy(target_, target_ + 3, jump_); // aiming again mid-blink mustn't move it
|
||||
jumpSeat_ = seat_;
|
||||
startJump(headT, head, R, teleport_ == 2);
|
||||
}
|
||||
}
|
||||
// Seats: pointing a hand at one lights it; that hand's trigger (pressed now) sits you in it.
|
||||
for (int h = 0; h < 2; ++h) {
|
||||
const bool press = trig[h] && !trigHeld_[h] && !menu;
|
||||
trigHeld_[h] = trig[h];
|
||||
pointed_[h] = -1;
|
||||
if (menu || (h == 0 && aiming_) || world_.seats.empty()) continue; // the right hand is aiming the arc
|
||||
float m[12];
|
||||
if (!(h == 0 ? handPose(m) : leftPose(m))) continue;
|
||||
float o[3], d[3];
|
||||
const float po[3] = {m[3], m[7], m[11]}, pd[3] = {-m[2], -m[6], -m[10]};
|
||||
apply(R, po, o);
|
||||
dir(R, pd, d);
|
||||
float t;
|
||||
const int i = seatOnRay(o, d, kPointReach, t);
|
||||
if (i < 0 || i == seated_) continue;
|
||||
pointed_[h] = i;
|
||||
if (!guide_.pointer) {
|
||||
guide_.pointer = true;
|
||||
// to the seat itself (the ray may have stopped on the chair's collider, short of it)
|
||||
const float* sp = world_.seats[i].pos;
|
||||
for (int k = 0; k < 3; ++k) { guide_.pointerFrom[k] = o[k]; guide_.pointerTo[k] = sp[k] + (k == 1 ? 0.08f : 0.0f); }
|
||||
}
|
||||
if (press && slideT_ < 0 && blinkT_ < 0) {
|
||||
seatTarget(i, jump_);
|
||||
jumpSeat_ = i;
|
||||
if (aiming_) dropAim();
|
||||
std::fprintf(stderr, "frame-home: trigger on %s\n", world_.seats[i].name.c_str());
|
||||
startJump(headT, head, R, teleport_ == 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Seat markers: the seats right by you (fading in from 1.2 m to 2 m away), and one you point or aim at from
|
||||
// anywhere; never the one you're in.
|
||||
for (size_t i = 0; i < world_.seats.size(); ++i) {
|
||||
const WorldPoint& st = world_.seats[i];
|
||||
if (static_cast<int>(i) == seated_) continue;
|
||||
const float dx = st.pos[0] - head[0], dz = st.pos[2] - head[2], dist = std::hypot(dx, dz);
|
||||
const bool hot = pointed_[0] == static_cast<int>(i) || pointed_[1] == static_cast<int>(i) || (aiming_ && valid_ && seat_ == static_cast<int>(i));
|
||||
const float near = std::clamp((kMarkerReach - dist) / (kMarkerReach - 1.2f), 0.0f, 1.0f);
|
||||
if (!hot && near <= 0) continue;
|
||||
guide_.seats.push_back({{st.pos[0], st.pos[1], st.pos[2]}, st.yaw, hot, near});
|
||||
}
|
||||
// A blink: the jump happens once the view is black, then it fades back in.
|
||||
if (blinkT_ >= 0 && (blinkT_ -= dt) <= 0) { blinkT_ = -1; land(headT, head); blinkIn(); }
|
||||
// A dash in progress: eased (slow, fast, slow) so it starts and stops without a jolt; the floor under the
|
||||
// way isn't followed (the dash may cross a gap or a step the arc flew over).
|
||||
if (slideT_ >= 0) {
|
||||
const float t0 = slideT_, t1 = std::min(1.0f, slideT_ + dt / kSlide), de = ease(t1) - ease(t0);
|
||||
rigX_ += slideDX_ * de; rigZ_ += slideDZ_ * de;
|
||||
head[0] += slideDX_ * de; head[2] += slideDZ_ * de;
|
||||
floorY_ = targetFloor_ = slideY0_ + (jump_[1] - slideY0_) * ease(t1);
|
||||
fallV_ = fallen_ = stepV_ = 0;
|
||||
slideT_ = t1;
|
||||
if (t1 >= 1) {
|
||||
slideT_ = -1;
|
||||
floorY_ = targetFloor_ = jump_[1];
|
||||
if (jumpSeat_ >= 0) { rig(R); sit(jumpSeat_, R, headT, head); }
|
||||
}
|
||||
lastHead_[0] = head[0]; lastHead_[1] = head[2];
|
||||
return;
|
||||
}
|
||||
// Never inside anything: the eyes stay kHeadClear from whatever is solid at their height, and the body doesn't
|
||||
// stand in a coffee table, however you got there (the stick slides along edges already; this is moving for
|
||||
// real, leaning, bending over a table). The world moves back instead.
|
||||
if (const WalkGrid* g = runtime_.grid(); g && seated_ < 0) {
|
||||
float push[2];
|
||||
if (g->headPush(head[0], head[1], head[2], kHeadClear, push) ||
|
||||
g->bodyPush(head[0], floorY_, head[2], push)) {
|
||||
rigX_ += push[0]; rigZ_ += push[1];
|
||||
head[0] += push[0]; head[2] += push[1];
|
||||
}
|
||||
}
|
||||
const float walked = std::hypot(head[0] - lastHead_[0], head[2] - lastHead_[1]); // stick and roomscale
|
||||
lastHead_[0] = head[0]; lastHead_[1] = head[2];
|
||||
if (seated_ >= 0) {
|
||||
// Seated: turning is free, moving isn't. Leaning is fine; stepping away (roomscale) stands you up there.
|
||||
const WorldPoint& st = world_.seats[seated_];
|
||||
if (std::hypot(head[0] - st.pos[0], head[2] - st.pos[2]) > 0.55f) standUp(head, false);
|
||||
else {
|
||||
// Sitting down (or up) for real slowly brings the eyes back to the seated height; quick head
|
||||
// movements still show.
|
||||
const float want = st.pos[1] + kSeatedEye - headT[13];
|
||||
seatRigY_ += (want - seatRigY_) * std::min(1.0f, dt * 0.7f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The floor under the head (stick or roomscale walking). A ramp is followed exactly; a step is eased
|
||||
// (critically damped: no jolt as it starts or ends). Nothing within a step below: you fall, and land on
|
||||
// whatever is under you; too far, and you're back at the spawn.
|
||||
// The floor is looked for within a step of where you are (down stairs: the next tread, never the floor under the
|
||||
// staircase), then a hand's width round (a stair's edge, a seam): only with none there do you fall.
|
||||
float f;
|
||||
bool on = runtime_.hasFloor() && runtime_.floorAt(head[0], head[2], floorY_, 0.45f, 0.45f, f);
|
||||
if (!on && runtime_.grid()) {
|
||||
float nx = head[0], nz = head[2];
|
||||
on = runtime_.grid()->roomAt(nx, floorY_, nz, 0.2f) && runtime_.floorAt(nx, nz, floorY_, 0.45f, 0.45f, f);
|
||||
}
|
||||
if (on) {
|
||||
const float d = f - targetFloor_;
|
||||
if (std::fabs(d) <= walked * grav::kRampSlope + 0.004f) floorY_ += d;
|
||||
targetFloor_ = f;
|
||||
fallV_ = fallen_ = 0;
|
||||
} else if (runtime_.hasFloor()) {
|
||||
stepV_ = 0;
|
||||
fallV_ = std::min(fallV_ + 9.8f * dt, 55.0f);
|
||||
const float stepD = fallV_ * dt;
|
||||
if (runtime_.floorAt(head[0], head[2], floorY_, 0.0f, stepD + 0.05f, f)) { floorY_ = targetFloor_ = f; fallV_ = fallen_ = 0; return; }
|
||||
floorY_ -= stepD;
|
||||
targetFloor_ = floorY_;
|
||||
fallen_ += stepD;
|
||||
if (fallen_ > fallLimit_) {
|
||||
++respawns_;
|
||||
std::fprintf(stderr, "frame-home: fell %.0f m: back to the spawn\n", fallen_);
|
||||
toSpawn();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const float left = targetFloor_ - floorY_;
|
||||
floorY_ = targetFloor_ - grav::smoothDamp(left, 0, stepV_, left > 0 ? grav::kStepUp : grav::kStepDown, dt);
|
||||
}
|
||||
|
||||
private:
|
||||
// Scale is true metres end to end (Meta's homes are authored in metres, floor at y = 0), so the home looks its
|
||||
// real size only when the tracking floor is the real floor. A headset whose standing space has no floor (its
|
||||
// origin at the head) puts the eyes at floor level and every room looks giant: after 2 s of tracking with the
|
||||
// eyes under 0.6 m, lift them to a standing 1.65 m. FH_EYE_HEIGHT=<metres> forces an eye height; 0 = never lift.
|
||||
void checkEyeHeight(float dt, float headY) {
|
||||
if (liftDecided_ || headY == 0 || !poseValid_) return; // only real head poses count (not asleep / lost)
|
||||
static const char* forced = std::getenv("FH_EYE_HEIGHT");
|
||||
if (forced) {
|
||||
const float want = static_cast<float>(std::atof(forced));
|
||||
lift_ = want > 0 ? want - headY : 0;
|
||||
liftDecided_ = true;
|
||||
std::fprintf(stderr, "frame-home: eye height forced to %.2f m (tracking %.2f m)\n", want, headY);
|
||||
return;
|
||||
}
|
||||
lowTime_ = headY < 0.6f ? lowTime_ + dt : 0;
|
||||
seenTime_ += dt;
|
||||
if (lowTime_ > 2.0f) {
|
||||
lift_ = 1.65f - headY;
|
||||
liftDecided_ = true;
|
||||
std::fprintf(stderr, "frame-home: tracking floor looks unset (eyes %.2f m over it): lifting to 1.65 m\n", headY);
|
||||
} else if (seenTime_ > 4.0f) {
|
||||
liftDecided_ = true; // a real floor: true scale, no lift
|
||||
}
|
||||
}
|
||||
|
||||
// Sits in seat i: the head over the seat, still facing the way you were, eyes kSeatedEye over its surface (whatever
|
||||
// your real height: you may be standing in the room).
|
||||
static constexpr float kSeatedEye = 0.78f;
|
||||
static constexpr float kArcSpeed = 10.0f; // the teleport arc's launch speed (m/s)
|
||||
static constexpr float kHeadClear = 0.15f; // the eyes never come closer than this to anything solid (m)
|
||||
void sit(int i, const float R[16], const float headT[16], float head[3]) {
|
||||
const WorldPoint& st = world_.seats[i];
|
||||
rigX_ += st.pos[0] - head[0];
|
||||
rigZ_ += st.pos[2] - head[2];
|
||||
head[0] = st.pos[0]; head[2] = st.pos[2];
|
||||
(void)R;
|
||||
seatRigY_ = st.pos[1] + kSeatedEye - headT[13];
|
||||
seated_ = i;
|
||||
std::fprintf(stderr, "frame-home: sitting in %s\n", st.name.c_str());
|
||||
}
|
||||
|
||||
// Moving the stick in a seat: out to its stand-up spot (Frame Studio's "stand up here"; half a metre in front when
|
||||
// it names none), the way a teleport goes there — a dash with slide teleports, else a blink or a jump.
|
||||
void leaveSeat(const float headT[16], float head[3], const float R[16]) {
|
||||
if (seated_ < 0) return;
|
||||
const WorldPoint& st = world_.seats[seated_];
|
||||
const float c = std::cos(st.yaw), s = std::sin(st.yaw);
|
||||
float ex = st.exit[0], ez = st.exit[2];
|
||||
if (std::hypot(ex, ez) < 0.05f) { ex = 0; ez = -0.5f; }
|
||||
jump_[0] = st.pos[0] + c * ex + s * ez;
|
||||
jump_[2] = st.pos[2] - s * ex + c * ez;
|
||||
float f;
|
||||
if (!runtime_.hasFloor() || !runtime_.floorAt(jump_[0], jump_[2], st.pos[1], 0.3f, 2.0f, f)) { standUp(head, true); return; }
|
||||
jump_[1] = f;
|
||||
jumpSeat_ = -1;
|
||||
std::fprintf(stderr, "frame-home: getting up from %s\n", st.name.c_str());
|
||||
startJump(headT, head, R, teleport_ == 2);
|
||||
if (slideT_ < 0 && blinkT_ < 0) seated_ = -1; // (a jump: done; a dash or a blink: under way)
|
||||
}
|
||||
|
||||
// Leaves the seat. byStick: you walked off — you step out at the seat's exit point (beside it, or half a metre
|
||||
// in front when the seat has none). Otherwise you stood up where your head is.
|
||||
void standUp(float head[3], bool byStick) {
|
||||
const WorldPoint& st = world_.seats[seated_];
|
||||
seated_ = -1;
|
||||
float to[3] = {head[0], 0, head[2]};
|
||||
if (byStick) {
|
||||
const float c = std::cos(st.yaw), s = std::sin(st.yaw);
|
||||
float ex = st.exit[0], ez = st.exit[2];
|
||||
if (std::hypot(ex, ez) < 0.05f) { ex = 0; ez = -0.5f; } // forward (-Z) when the seat names no exit
|
||||
to[0] = st.pos[0] + c * ex + s * ez;
|
||||
to[2] = st.pos[2] - s * ex + c * ez;
|
||||
}
|
||||
float f = st.pos[1] - 0.45f;
|
||||
if (runtime_.hasFloor() && runtime_.floorAt(to[0], to[2], st.pos[1], 0.3f, 2.0f, f)) {}
|
||||
else if (byStick) { to[0] = head[0]; to[2] = head[2]; runtime_.floorAt(to[0], to[2], st.pos[1], 0.3f, 2.0f, f); }
|
||||
rigX_ += to[0] - head[0];
|
||||
rigZ_ += to[2] - head[2];
|
||||
head[0] = to[0]; head[2] = to[2];
|
||||
floorY_ = targetFloor_ = f;
|
||||
std::fprintf(stderr, "frame-home: stood up from %s\n", st.name.c_str());
|
||||
}
|
||||
|
||||
// Gravity zones: the same sticks and teleport, but with the walker's up.
|
||||
void updateGravity(float dt, const float headT[16], float lx, float ly, const vr::InputAnalogActionData_t& r) {
|
||||
float h[16];
|
||||
std::copy(headT, headT + 16, h);
|
||||
h[13] += lift_;
|
||||
const float rx = r.bActive ? r.x : 0, ry = r.bActive ? r.y : 0;
|
||||
walker_.update(dt, h, lx, ly, turnBy(dt, rx, ry));
|
||||
// Teleport: aim along the walker's gravity, go on release. A slide is a blink here: the landing may turn
|
||||
// your up (onto a wall), which a straight glide can't follow.
|
||||
if (teleport_ == 0) { if (aiming_) dropAim(); }
|
||||
else if (ry > 0.7f && std::fabs(rx) < 0.6f) {
|
||||
if (!aiming_) guide_.aimTime = 0;
|
||||
aiming_ = true;
|
||||
valid_ = false;
|
||||
guide_.aimTime += dt;
|
||||
float m[12];
|
||||
guide_.arc.clear();
|
||||
if (handPose(m)) {
|
||||
const float po[3] = {m[3], m[7] + lift_, m[11]}, pd[3] = {-m[2], -m[6], -m[10]};
|
||||
valid_ = walker_.aim(po, pd, target_, targetN_, &guide_.arc, &h[12]);
|
||||
}
|
||||
guide_.aiming = !guide_.arc.empty();
|
||||
guide_.valid = valid_;
|
||||
guide_.seat = false;
|
||||
std::copy(target_, target_ + 3, guide_.target);
|
||||
std::copy(targetN_, targetN_ + 3, guide_.normal);
|
||||
if (ring_ && ringOn_) {
|
||||
if (!valid_) vr::VROverlay()->HideOverlay(ring_);
|
||||
else { // tracking up is the walker's up, so the ring lies flat in tracking space
|
||||
const grav::V t = walker_.toTracking(grav::V(target_));
|
||||
vr::HmdMatrix34_t tm = {{{1, 0, 0, t.x}, {0, 0, 1, t.y + 0.02f - lift_}, {0, -1, 0, t.z}}};
|
||||
vr::VROverlay()->SetOverlayTransformAbsolute(ring_, vr::TrackingUniverseStanding, &tm);
|
||||
vr::VROverlay()->ShowOverlay(ring_);
|
||||
}
|
||||
}
|
||||
} else if (aiming_ && ry < 0.4f) {
|
||||
aiming_ = false;
|
||||
guide_.aiming = false;
|
||||
if (ring_) vr::VROverlay()->HideOverlay(ring_);
|
||||
if (valid_ && blinkT_ < 0) {
|
||||
std::copy(target_, target_ + 3, jump_);
|
||||
std::copy(targetN_, targetN_ + 3, jumpN_);
|
||||
if (blinkOut()) blinkT_ = kBlinkOut;
|
||||
else walker_.teleport(jump_, h, jumpN_);
|
||||
}
|
||||
}
|
||||
if (blinkT_ >= 0 && (blinkT_ -= dt) <= 0) { blinkT_ = -1; walker_.teleport(jump_, h, jumpN_); blinkIn(); }
|
||||
}
|
||||
|
||||
// This frame's turn (radians, + = left) from the right stick's x. Snap: one snap_ per flick (the stick must
|
||||
// come back near centre to arm the next). Smooth: up to kTurnRate past the deadzone, not while aiming (an arc
|
||||
// held slightly off-centre shouldn't spin you).
|
||||
static constexpr float kTurnRate = 2.0944f; // 120 degrees/s
|
||||
float turnBy(float dt, float rx, float ry) {
|
||||
if (turn_ == 1) return aiming_ || std::fabs(ry) > 0.7f ? 0.0f : -dead(rx) * kTurnRate * dt;
|
||||
if (!turnArmed_ && std::fabs(rx) < 0.3f) turnArmed_ = true;
|
||||
if (turnArmed_ && std::fabs(rx) > 0.75f && std::fabs(ry) < 0.6f) { turnArmed_ = false; return rx > 0 ? -snap_ : snap_; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Instant teleports blink: fade to black, jump, fade back. In system mode Frame Home isn't the scene app and
|
||||
// the compositor may ignore the fade; then the blink is just a 80 ms wait.
|
||||
static constexpr float kBlinkOut = 0.08f, kSlide = 0.25f;
|
||||
bool blinkOut() const { if (!ok_ || !vr::VRCompositor()) return false; vr::VRCompositor()->FadeToColor(kBlinkOut, 0, 0, 0, 1); return true; }
|
||||
void blinkIn() const { if (ok_ && vr::VRCompositor()) vr::VRCompositor()->FadeToColor(0.12f, 0, 0, 0, 0); }
|
||||
static float ease(float t) { return t * t * (3 - 2 * t); }
|
||||
|
||||
// Off to jump_ (and into seat jumpSeat_ when >= 0): a 0.25 s dash (slide), or a blink — instant behind a fade.
|
||||
void startJump(const float headT[16], float head[3], const float R[16], bool slide) {
|
||||
vel_[0] = vel_[1] = 0;
|
||||
if (slide) {
|
||||
// The dash: the rig glides by the head's offset to the target, its height from where you are
|
||||
// (standing, or a seat's eye height) to the target's floor. The arc found that floor.
|
||||
slideT_ = 0;
|
||||
slideDX_ = jump_[0] - head[0]; slideDZ_ = jump_[2] - head[2];
|
||||
slideY0_ = R[13] - lift_;
|
||||
seated_ = -1;
|
||||
} else if (blinkOut()) blinkT_ = kBlinkOut;
|
||||
else land(headT, head); // no compositor of ours: just jump
|
||||
}
|
||||
|
||||
// Puts the head over jump_ on its floor (a jumped-to seat sits you in it).
|
||||
void land(const float headT[16], float head[3]) {
|
||||
seated_ = -1; // teleporting off a seat stands you up
|
||||
rigX_ += jump_[0] - head[0];
|
||||
rigZ_ += jump_[2] - head[2];
|
||||
floorY_ = targetFloor_ = jump_[1];
|
||||
fallV_ = fallen_ = stepV_ = 0;
|
||||
vel_[0] = vel_[1] = 0;
|
||||
head[0] = jump_[0]; head[2] = jump_[2];
|
||||
if (jumpSeat_ >= 0) { float R[16]; rig(R); sit(jumpSeat_, R, headT, head); }
|
||||
}
|
||||
|
||||
void dropAim() { aiming_ = valid_ = guide_.aiming = false; if (ring_ && vr::VROverlay()) vr::VROverlay()->HideOverlay(ring_); }
|
||||
|
||||
// The right hand's aim (3x4 row-major, tracking space): the controller, the test's, or the menu's.
|
||||
bool handPose(float m[12]) const {
|
||||
if (test_) { if (testAimOk_) std::copy(testAim_, testAim_ + 12, m); return testAimOk_; }
|
||||
vr::InputPoseActionData_t pose = {};
|
||||
if (vr::VRInput()->GetPoseActionDataForNextFrame(rightAim_, vr::TrackingUniverseStanding, &pose, sizeof pose, vr::k_ulInvalidInputValueHandle) == vr::VRInputError_None &&
|
||||
pose.bActive && pose.pose.bPoseIsValid) { std::memcpy(m, pose.pose.mDeviceToAbsoluteTracking.m, 12 * sizeof(float)); return true; }
|
||||
if (menuAim_) { std::memcpy(m, menuAimPose_, 12 * sizeof(float)); return true; }
|
||||
return false;
|
||||
}
|
||||
bool leftPose(float m[12]) const {
|
||||
if (test_) { if (testLeftOk_) std::copy(testLeft_, testLeft_ + 12, m); return testLeftOk_; }
|
||||
vr::InputPoseActionData_t pose = {};
|
||||
if (!leftAim_ || vr::VRInput()->GetPoseActionDataForNextFrame(leftAim_, vr::TrackingUniverseStanding, &pose, sizeof pose, vr::k_ulInvalidInputValueHandle) != vr::VRInputError_None ||
|
||||
!pose.bActive || !pose.pose.bPoseIsValid) return false;
|
||||
std::memcpy(m, pose.pose.mDeviceToAbsoluteTracking.m, 12 * sizeof(float));
|
||||
return true;
|
||||
}
|
||||
|
||||
GravityWalker walker_;
|
||||
float targetN_[3] = {0, 1, 0}; // the teleport target's surface normal
|
||||
bool gravityMode_ = false;
|
||||
float fallV_ = 0, fallen_ = 0, fallLimit_ = 30;
|
||||
int respawns_ = 0;
|
||||
int seated_ = -1;
|
||||
float seatRigY_ = 0;
|
||||
bool ownInput_ = false;
|
||||
bool menuAim_ = false;
|
||||
float menuAimPose_[12] = {};
|
||||
float lift_ = 0, lowTime_ = 0, seenTime_ = 0;
|
||||
bool poseValid_ = true;
|
||||
bool liftDecided_ = false;
|
||||
bool ok_ = false;
|
||||
vr::VRActionSetHandle_t set_ = 0;
|
||||
vr::VRActionHandle_t leftStick_ = 0, rightStick_ = 0, rightAim_ = 0, leftAim_ = 0, gaze_ = 0;
|
||||
vr::VRActionHandle_t trigger_[2] = {0, 0}; // 0 right, 1 left
|
||||
bool trigHeld_[2] = {false, false}; // last frame's (a press is the edge)
|
||||
int pointed_[2] = {-1, -1}; // the seat each hand points at
|
||||
vr::VROverlayHandle_t ring_ = 0;
|
||||
bool ringOn_ = true;
|
||||
LocoGuide guide_;
|
||||
float vel_[2] = {0, 0}; // walking velocity (world x, z), eased
|
||||
float stepV_ = 0; // a step's easing rate
|
||||
float lastHead_[2] = {0, 0};
|
||||
// Seats: a ball of kSeatRadius over each seat point is what the arc and pointing hands hit (and the chair's own
|
||||
// surfaces within kSeatOwn of it); markers show within kMarkerReach, pointing reaches kPointReach.
|
||||
static constexpr float kSeatRadius = 0.35f, kSeatOwn = 0.65f, kSeatLift = 0.1f, kMarkerReach = 2.0f, kPointReach = 10.0f; // markers: only seats right by you
|
||||
HomeWorld world_;
|
||||
WorldRuntime runtime_;
|
||||
float rigX_ = 0, rigZ_ = 0, rigYaw_ = 0, floorY_ = 0, targetFloor_ = 0;
|
||||
bool turnArmed_ = true, aiming_ = false, valid_ = false;
|
||||
float target_[3] = {0, 0, 0};
|
||||
int seat_ = -1;
|
||||
// Options (setOptions) and the jump in flight: where it lands (copied from the aim on release), a blink's time
|
||||
// to black, a dash's progress 0..1 (-1: none).
|
||||
int walk_ = 1, teleport_ = 1, turn_ = 0;
|
||||
float snap_ = 0.5236f, speed_ = 2.0f;
|
||||
float jump_[3] = {0, 0, 0}, jumpN_[3] = {0, 1, 0};
|
||||
int jumpSeat_ = -1;
|
||||
float blinkT_ = -1, slideT_ = -1, slideDX_ = 0, slideDZ_ = 0, slideY0_ = 0;
|
||||
bool test_ = false, testAimOk_ = false, testLeftOk_ = false, testTrig_[2] = {false, false};
|
||||
float testIn_[4] = {}, testAim_[12] = {}, testLeft_[12] = {};
|
||||
|
||||
// Where you stand to sit in seat i: over it, on the floor under it (the seat's own top when it is one).
|
||||
void seatTarget(int i, float t[3]) const {
|
||||
const WorldPoint& s = world_.seats[i];
|
||||
t[0] = s.pos[0]; t[1] = s.pos[1] - 0.45f; t[2] = s.pos[2];
|
||||
float f;
|
||||
if (runtime_.floorAt(t[0], t[2], s.pos[1], 0.2f, 2.0f, f)) t[1] = f;
|
||||
}
|
||||
// The seat whose furniture a surface hit at q likely belongs to (within kSeatOwn of the seat point), or -1.
|
||||
int seatNear(const float q[3]) const {
|
||||
int best = -1;
|
||||
float bd = kSeatOwn * kSeatOwn;
|
||||
for (size_t i = 0; i < world_.seats.size(); ++i) {
|
||||
const float* p = world_.seats[i].pos;
|
||||
const float dx = q[0] - p[0], dy = q[1] - (p[1] + kSeatLift), dz = q[2] - p[2], dd = dx * dx + dy * dy + dz * dz;
|
||||
if (dd < bd) { bd = dd; best = static_cast<int>(i); }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
// seatNear, or the seat whose own collider `shape` is (centred on it: Meta gives chairs a ball or box that can
|
||||
// be well over a metre across).
|
||||
int seatOf(const float q[3], const WorldCollider* shape) const {
|
||||
const int i = seatNear(q);
|
||||
return i >= 0 || !shape ? i : seatNear(&shape->world[12]);
|
||||
}
|
||||
// The first seat ball a segment o + d t, t in [0, 1], enters, and where (t).
|
||||
int seatOnSegment(const float o[3], const float d[3], float& t) const {
|
||||
int best = -1;
|
||||
t = 2;
|
||||
for (size_t i = 0; i < world_.seats.size(); ++i) {
|
||||
const float* p = world_.seats[i].pos;
|
||||
const float oc[3] = {o[0] - p[0], o[1] - (p[1] + kSeatLift), o[2] - p[2]};
|
||||
const float A = d[0] * d[0] + d[1] * d[1] + d[2] * d[2], B = oc[0] * d[0] + oc[1] * d[1] + oc[2] * d[2];
|
||||
const float C = oc[0] * oc[0] + oc[1] * oc[1] + oc[2] * oc[2] - kSeatRadius * kSeatRadius, disc = B * B - A * C;
|
||||
if (A < 1e-12f || disc < 0 || C < 0) continue; // (a hand inside one isn't pointing)
|
||||
const float th = (-B - std::sqrt(disc)) / A;
|
||||
if (th >= 0 && th <= 1 && th < t) { t = th; best = static_cast<int>(i); }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static float dead(float v) { return std::fabs(v) < 0.15f ? 0.0f : (v - std::copysign(0.15f, v)) / 0.85f; }
|
||||
static void apply(const float m[16], const float p[3], float o[3]) { for (int k = 0; k < 3; ++k) o[k] = m[k] * p[0] + m[4 + k] * p[1] + m[8 + k] * p[2] + m[12 + k]; }
|
||||
static void dir(const float m[16], const float d[3], float o[3]) { for (int k = 0; k < 3; ++k) o[k] = m[k] * d[0] + m[4 + k] * d[1] + m[8 + k] * d[2]; }
|
||||
float headYaw(const float R[16], const float headT[16]) const {
|
||||
float f[3];
|
||||
const float hz[3] = {-headT[8], -headT[9], -headT[10]};
|
||||
dir(R, hz, f);
|
||||
return std::atan2(-f[0], -f[2]);
|
||||
}
|
||||
|
||||
// The teleport arc from the right hand: a parabola (10 m/s: about 10 m on the flat) traced in 30 ms segments until
|
||||
// it meets something; a seat's ball sits you in it, and where else it ends puts you somewhere if it can
|
||||
// (WorldRuntime::landingFor: a floor, the floor by a table's top, a wall's or furniture's foot, the floor under a
|
||||
// ceiling). Leaves don't stop it. Measured (tools/walktest/teleportfeel): every floor in sight 1.5-10 m away is
|
||||
// reachable.
|
||||
void aim(const float R[16], const float head[3], float dt) {
|
||||
if (!aiming_) guide_.aimTime = 0;
|
||||
aiming_ = true;
|
||||
valid_ = false;
|
||||
seat_ = -1;
|
||||
guide_.aimTime += dt;
|
||||
guide_.arc.clear();
|
||||
targetN_[0] = targetN_[2] = 0; targetN_[1] = 1;
|
||||
float m[12];
|
||||
if (handPose(m)) {
|
||||
const float po[3] = {m[3], m[7], m[11]}, pd[3] = {-m[2], -m[6], -m[10]};
|
||||
float o[3], d[3];
|
||||
apply(R, po, o);
|
||||
dir(R, pd, d);
|
||||
traceArc(head, o, d);
|
||||
}
|
||||
if (valid_ && seat_ < 0) { // snap onto a seat aimed right at, or a hotspot
|
||||
float best = 0.3f;
|
||||
for (size_t i = 0; i < world_.seats.size(); ++i) {
|
||||
const float* s = world_.seats[i].pos, dd = std::hypot(s[0] - target_[0], s[2] - target_[2]);
|
||||
if (dd < best) { best = dd; seat_ = static_cast<int>(i); }
|
||||
}
|
||||
if (seat_ >= 0) seatTarget(seat_, target_);
|
||||
else {
|
||||
for (const WorldPoint& h : world_.hotspots)
|
||||
if (std::hypot(h.pos[0] - target_[0], h.pos[2] - target_[2]) < 1.0f) { target_[0] = h.pos[0]; target_[1] = h.pos[1]; target_[2] = h.pos[2]; break; }
|
||||
}
|
||||
}
|
||||
if (seat_ >= 0) { targetN_[0] = targetN_[2] = 0; targetN_[1] = 1; }
|
||||
guide_.aiming = !guide_.arc.empty();
|
||||
guide_.valid = valid_;
|
||||
guide_.seat = seat_ >= 0;
|
||||
std::copy(target_, target_ + 3, guide_.target);
|
||||
std::copy(targetN_, targetN_ + 3, guide_.normal);
|
||||
if (!ring_ || !ringOn_) return;
|
||||
if (!valid_) { vr::VROverlay()->HideOverlay(ring_); return; }
|
||||
// tracking = rig^-1 * world; the ring lies flat (rotated -90 degrees about X).
|
||||
const float c = std::cos(rigYaw_), s = std::sin(rigYaw_);
|
||||
float Rn[16];
|
||||
rig(Rn);
|
||||
const float wx = target_[0] - rigX_, wy = target_[1] + 0.02f - Rn[13], wz = target_[2] - rigZ_;
|
||||
const float tx = c * wx - s * wz, tz = s * wx + c * wz;
|
||||
vr::HmdMatrix34_t t = {{{1, 0, 0, tx}, {0, 0, 1, wy}, {0, -1, 0, tz}}};
|
||||
vr::VROverlay()->SetOverlayTransformAbsolute(ring_, vr::TrackingUniverseStanding, &t);
|
||||
vr::VROverlay()->ShowOverlay(ring_);
|
||||
}
|
||||
|
||||
// The arc's flight from o along d (world), into guide_.arc; sets valid_, target_, targetN_, seat_.
|
||||
void traceArc(const float head[3], const float o[3], const float d[3]) {
|
||||
auto keep = [&](const float* q) { guide_.arc.insert(guide_.arc.end(), q, q + 3); };
|
||||
keep(o);
|
||||
if (!runtime_.hasFloor()) return; // nothing to land on anywhere
|
||||
{ // a hand reached through a wall (standing right at it) doesn't aim from the far side
|
||||
const float hs[3] = {o[0] - head[0], o[1] - head[1], o[2] - head[2]};
|
||||
float t, n[3];
|
||||
if (runtime_.trace(head, hs, t, n) == WorldRuntime::Block) return;
|
||||
}
|
||||
float p[3] = {o[0], o[1], o[2]}, v[3] = {d[0] * kArcSpeed, d[1] * kArcSpeed, d[2] * kArcSpeed};
|
||||
for (int i = 0; i < 90; ++i) {
|
||||
const float dt = 0.03f;
|
||||
const float seg[3] = {v[0] * dt, (v[1] - 4.9f * dt) * dt, v[2] * dt};
|
||||
float t, n[3], ts;
|
||||
const WorldCollider* shape = nullptr;
|
||||
const WorldRuntime::Hit h = runtime_.trace(p, seg, t, n, &shape);
|
||||
const int si = seatOnSegment(p, seg, ts);
|
||||
if (si >= 0 && (h == WorldRuntime::None || ts <= t)) {
|
||||
const float q[3] = {p[0] + seg[0] * ts, p[1] + seg[1] * ts, p[2] + seg[2] * ts};
|
||||
keep(q);
|
||||
seat_ = si;
|
||||
seatTarget(si, target_);
|
||||
valid_ = true;
|
||||
return;
|
||||
}
|
||||
if (h != WorldRuntime::None) {
|
||||
const float q[3] = {p[0] + seg[0] * t, p[1] + seg[1] * t, p[2] + seg[2] * t};
|
||||
keep(q);
|
||||
if (h == WorldRuntime::Block && (seat_ = seatOf(q, shape)) >= 0) { // the chair's back or arm: the chair
|
||||
seatTarget(seat_, target_);
|
||||
valid_ = true;
|
||||
} else if (runtime_.landingFor(h, q, n, target_)) {
|
||||
valid_ = true;
|
||||
const bool onIt = h == WorldRuntime::Land && std::fabs(target_[1] - q[1]) < 0.05f;
|
||||
for (int k = 0; k < 3; ++k) targetN_[k] = onIt ? n[k] : (k == 1 ? 1.0f : 0.0f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int k = 0; k < 3; ++k) p[k] += seg[k];
|
||||
keep(p);
|
||||
v[1] -= 9.8f * dt;
|
||||
if (p[1] < floorY_ - 30) return;
|
||||
}
|
||||
}
|
||||
};
|
||||
+1988
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
// Walking while the Steam menu is open. SteamVR gives a scene app (Frame Home) no controller input while its
|
||||
// dashboard — the Steam menu — is up, even when you're looking away from it. frame-switcher, an overlay app that
|
||||
// always runs, takes the sticks through a global-priority action set (steamvr.globalActionSetPriority) whenever the
|
||||
// menu is up, Frame Home is the environment, and that hand isn't pointing at a panel, and passes them to Frame Home
|
||||
// through a shared page (/dev/shm/framehome-sticks). Pointing at the menu still drives the menu.
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
struct MenuSticks {
|
||||
static constexpr uint32_t kMagic = 0x46534b31; // "FSK1"
|
||||
uint32_t magic;
|
||||
std::atomic<uint32_t> seq; // odd while being written
|
||||
double time; // CLOCK_MONOTONIC seconds when written (both processes share the clock)
|
||||
float lx, ly, rx, ry; // thumbsticks
|
||||
uint8_t left, right; // which hands are taken for walking / turning
|
||||
uint8_t aimValid;
|
||||
float aim[12]; // right hand's aim pose in standing space (3x4, row-major)
|
||||
};
|
||||
|
||||
inline double menuNow() { return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count(); }
|
||||
|
||||
inline MenuSticks* menuSticksMap(bool create) {
|
||||
const int fd = open("/dev/shm/framehome-sticks", O_RDWR | (create ? O_CREAT : 0), 0600);
|
||||
if (fd < 0) return nullptr;
|
||||
if (create && ftruncate(fd, sizeof(MenuSticks)) != 0) { close(fd); return nullptr; }
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0 || st.st_size < static_cast<off_t>(sizeof(MenuSticks))) { close(fd); return nullptr; } // not sized yet
|
||||
void* p = mmap(nullptr, sizeof(MenuSticks), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
close(fd);
|
||||
return p == MAP_FAILED ? nullptr : static_cast<MenuSticks*>(p);
|
||||
}
|
||||
|
||||
// A consistent copy (seqlock); false when there is nothing recent (within maxAge seconds).
|
||||
inline bool menuSticksRead(const MenuSticks* m, MenuSticks& out, double maxAge = 0.15) {
|
||||
if (!m || m->magic != MenuSticks::kMagic) return false;
|
||||
for (int tries = 0; tries < 4; ++tries) {
|
||||
const uint32_t a = m->seq.load(std::memory_order_acquire);
|
||||
if (a & 1) continue;
|
||||
std::memcpy(static_cast<void*>(&out), static_cast<const void*>(m), sizeof out);
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
if (m->seq.load(std::memory_order_relaxed) == a) return menuNow() - out.time < maxAge;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline void menuSticksWrite(MenuSticks* m, const MenuSticks& v) {
|
||||
if (!m) return;
|
||||
uint32_t s = m->seq.load(std::memory_order_relaxed);
|
||||
if (s & 1) ++s; // a writer killed mid-write left it odd: start even again
|
||||
m->seq.store(s + 1, std::memory_order_release);
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
m->time = v.time; m->lx = v.lx; m->ly = v.ly; m->rx = v.rx; m->ry = v.ry;
|
||||
m->left = v.left; m->right = v.right; m->aimValid = v.aimValid;
|
||||
std::memcpy(m->aim, v.aim, sizeof m->aim);
|
||||
m->magic = MenuSticks::kMagic;
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
m->seq.store(s + 2, std::memory_order_release);
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
#include "model_import.h"
|
||||
|
||||
#define CGLTF_IMPLEMENTATION
|
||||
#include "cgltf.h"
|
||||
#include "ufbx.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
|
||||
extern "C" unsigned char* stbi_load_from_memory(const unsigned char* buffer, int len, int* x, int* y, int* comp, int req_comp);
|
||||
extern "C" void stbi_image_free(void* retval_from_stbi_load);
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::string lower(std::string s) {
|
||||
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return s;
|
||||
}
|
||||
|
||||
std::vector<u8> readFile(const std::string& path) {
|
||||
std::ifstream f(fs::u8path(path), std::ios::binary);
|
||||
return std::vector<u8>((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
struct Image { std::vector<u8> rgba; u32 w = 0, h = 0; bool alpha = false; };
|
||||
|
||||
bool decode(const u8* data, size_t n, Image& img) {
|
||||
int w = 0, h = 0, comp = 0;
|
||||
unsigned char* px = stbi_load_from_memory(data, static_cast<int>(n), &w, &h, &comp, 4);
|
||||
if (!px) return false;
|
||||
img.w = static_cast<u32>(w);
|
||||
img.h = static_cast<u32>(h);
|
||||
img.rgba.assign(px, px + static_cast<size_t>(w) * h * 4);
|
||||
stbi_image_free(px);
|
||||
for (size_t i = 3; i < img.rgba.size() && !img.alpha; i += 4) img.alpha = img.rgba[i] < 250;
|
||||
return true;
|
||||
}
|
||||
|
||||
// A finished part: texture (or a flat colour), colour factor, blend mode.
|
||||
void finish(MeshData& md, const Image* img, const float color[4], bool blend, bool cutout, float cutoff) {
|
||||
md.nVerts = static_cast<u32>(md.positions.size() / 3);
|
||||
md.nIdx = static_cast<u32>(md.indices.size());
|
||||
md.hasTexture = true;
|
||||
if (img && !img->rgba.empty()) {
|
||||
md.texRGBA = img->rgba;
|
||||
md.texW = img->w;
|
||||
md.texH = img->h;
|
||||
} else {
|
||||
md.texRGBA = {255, 255, 255, 255};
|
||||
md.texW = md.texH = 1;
|
||||
}
|
||||
for (int k = 0; k < 4; ++k) md.tint[k] = md.curTint[k] = color[k];
|
||||
md.useBlend = blend;
|
||||
md.alphaTest = cutout;
|
||||
md.alphaCutoff = cutoff;
|
||||
md.doubleSided = true;
|
||||
}
|
||||
|
||||
bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||
cgltf_options opt = {};
|
||||
cgltf_data* data = nullptr;
|
||||
if (cgltf_parse_file(&opt, path.c_str(), &data) != cgltf_result_success) { error = "not a readable glTF file: " + path; return false; }
|
||||
if (cgltf_load_buffers(&opt, data, path.c_str()) != cgltf_result_success) { cgltf_free(data); error = "glTF buffers missing next to " + path; return false; }
|
||||
// Accessor / buffer-view ranges, index ranges and node loops are only checked here; the reads below trust them.
|
||||
if (cgltf_validate(data) != cgltf_result_success) { cgltf_free(data); error = "malformed glTF (failed validation): " + path; return false; }
|
||||
files.push_back(path);
|
||||
const fs::path dir = fs::u8path(path).parent_path();
|
||||
for (cgltf_size b = 0; b < data->buffers_count; ++b)
|
||||
if (data->buffers[b].uri && std::strncmp(data->buffers[b].uri, "data:", 5) != 0) files.push_back((dir / fs::u8path(data->buffers[b].uri)).u8string());
|
||||
std::map<const cgltf_image*, Image> images;
|
||||
auto image = [&](const cgltf_texture_view& view) -> const Image* {
|
||||
if (!view.texture || !view.texture->image) return nullptr;
|
||||
const cgltf_image* im = view.texture->image;
|
||||
auto it = images.find(im);
|
||||
if (it != images.end()) return &it->second;
|
||||
Image& out = images[im];
|
||||
if (im->buffer_view && im->buffer_view->buffer->data) {
|
||||
const cgltf_buffer_view& bv = *im->buffer_view;
|
||||
if (bv.offset > bv.buffer->size || bv.size > bv.buffer->size - bv.offset) return &out; // range past the buffer
|
||||
const u8* p = static_cast<const u8*>(im->buffer_view->buffer->data) + im->buffer_view->offset;
|
||||
decode(p, im->buffer_view->size, out);
|
||||
} else if (im->uri && std::strncmp(im->uri, "data:", 5) == 0) {
|
||||
const char* comma = std::strchr(im->uri, ',');
|
||||
if (comma) {
|
||||
const size_t len = std::strlen(comma + 1), size = len / 4 * 3;
|
||||
void* bytes = nullptr;
|
||||
if (cgltf_load_buffer_base64(&opt, size, comma + 1, &bytes) == cgltf_result_success) {
|
||||
decode(static_cast<const u8*>(bytes), size, out);
|
||||
std::free(bytes);
|
||||
}
|
||||
}
|
||||
} else if (im->uri) {
|
||||
std::string rel = im->uri;
|
||||
cgltf_decode_uri(rel.data());
|
||||
rel.resize(std::strlen(rel.c_str()));
|
||||
const std::string full = (dir / fs::u8path(rel)).u8string();
|
||||
const std::vector<u8> bytes = readFile(full);
|
||||
if (!bytes.empty() && decode(bytes.data(), bytes.size(), out)) files.push_back(full);
|
||||
}
|
||||
return &out;
|
||||
};
|
||||
for (cgltf_size n = 0; n < data->nodes_count; ++n) {
|
||||
const cgltf_node& node = data->nodes[n];
|
||||
if (!node.mesh) continue;
|
||||
float world[16];
|
||||
cgltf_node_transform_world(&node, world);
|
||||
for (cgltf_size p = 0; p < node.mesh->primitives_count; ++p) {
|
||||
const cgltf_primitive& prim = node.mesh->primitives[p];
|
||||
if (prim.type != cgltf_primitive_type_triangles) continue;
|
||||
const cgltf_accessor *pos = nullptr, *uv = nullptr, *col = nullptr;
|
||||
for (cgltf_size a = 0; a < prim.attributes_count; ++a) {
|
||||
const cgltf_attribute& at = prim.attributes[a];
|
||||
if (at.type == cgltf_attribute_type_position) pos = at.data;
|
||||
else if (at.type == cgltf_attribute_type_texcoord && at.index == 0) uv = at.data;
|
||||
else if (at.type == cgltf_attribute_type_color && at.index == 0) col = at.data;
|
||||
}
|
||||
if (!pos || pos->count == 0) continue;
|
||||
MeshData md;
|
||||
md.name = std::string(node.name ? node.name : node.mesh->name ? node.mesh->name : "mesh") + (node.mesh->primitives_count > 1 ? "_" + std::to_string(p) : "");
|
||||
for (const cgltf_node* a = node.parent; a; a = a->parent) // the nodes above it in the file
|
||||
if (a->name) md.path = md.path.empty() ? std::string(a->name) : std::string(a->name) + "/" + md.path;
|
||||
md.positions.resize(pos->count * 3);
|
||||
md.uvs.assign(pos->count * 2, 0.0f);
|
||||
for (cgltf_size v = 0; v < pos->count; ++v) {
|
||||
cgltf_accessor_read_float(pos, v, &md.positions[v * 3], 3);
|
||||
if (uv) cgltf_accessor_read_float(uv, v, &md.uvs[v * 2], 2);
|
||||
}
|
||||
if (col) {
|
||||
md.colors.resize(pos->count * 4);
|
||||
for (cgltf_size v = 0; v < pos->count; ++v) {
|
||||
float c[4] = {1, 1, 1, 1};
|
||||
cgltf_accessor_read_float(col, v, c, cgltf_num_components(col->type));
|
||||
for (int k = 0; k < 4; ++k) md.colors[v * 4 + k] = static_cast<u8>(std::clamp(c[k], 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
}
|
||||
}
|
||||
if (prim.indices) {
|
||||
md.indices.resize(prim.indices->count);
|
||||
// Clamp out-of-range indices (degenerate triangle) rather than reading past the vertex arrays.
|
||||
for (cgltf_size i = 0; i < prim.indices->count; ++i)
|
||||
md.indices[i] = static_cast<u32>(std::min<cgltf_size>(cgltf_accessor_read_index(prim.indices, i), pos->count - 1));
|
||||
} else {
|
||||
md.indices.resize(pos->count);
|
||||
for (cgltf_size i = 0; i < pos->count; ++i) md.indices[i] = static_cast<u32>(i);
|
||||
}
|
||||
std::memcpy(md.worldMatrix, world, sizeof world);
|
||||
md.hasWorldMatrix = true;
|
||||
float color[4] = {1, 1, 1, 1};
|
||||
const Image* img = nullptr;
|
||||
bool blend = false, cutout = false;
|
||||
float cutoff = 0.5f;
|
||||
if (prim.material) {
|
||||
const cgltf_material& m = *prim.material;
|
||||
if (m.has_pbr_metallic_roughness) {
|
||||
std::memcpy(color, m.pbr_metallic_roughness.base_color_factor, sizeof color);
|
||||
img = image(m.pbr_metallic_roughness.base_color_texture);
|
||||
} else if (m.has_pbr_specular_glossiness) {
|
||||
std::memcpy(color, m.pbr_specular_glossiness.diffuse_factor, sizeof color);
|
||||
img = image(m.pbr_specular_glossiness.diffuse_texture);
|
||||
}
|
||||
blend = m.alpha_mode == cgltf_alpha_mode_blend;
|
||||
cutout = m.alpha_mode == cgltf_alpha_mode_mask;
|
||||
cutoff = m.alpha_cutoff;
|
||||
if (m.unlit && !img) {} // unlit materials draw the same way here
|
||||
}
|
||||
finish(md, img, color, blend, cutout, cutoff);
|
||||
meshes.push_back(std::move(md));
|
||||
}
|
||||
}
|
||||
cgltf_free(data);
|
||||
if (meshes.empty()) { error = "no triangle meshes in " + path; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
bool importUfbx(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||
ufbx_load_opts opts = {};
|
||||
opts.target_axes = ufbx_axes_right_handed_y_up;
|
||||
opts.target_unit_meters = 1.0f;
|
||||
opts.space_conversion = UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY;
|
||||
opts.load_external_files = true;
|
||||
ufbx_error err;
|
||||
ufbx_scene* scene = ufbx_load_file(path.c_str(), &opts, &err);
|
||||
if (!scene) {
|
||||
char msg[512];
|
||||
ufbx_format_error(msg, sizeof msg, &err);
|
||||
error = "couldn't read " + path + ": " + msg;
|
||||
return false;
|
||||
}
|
||||
files.push_back(path);
|
||||
const fs::path dir = fs::u8path(path).parent_path();
|
||||
const bool flipV = true; // FBX / OBJ texture space starts bottom-left; images load top-first
|
||||
std::map<const ufbx_texture*, Image> images;
|
||||
auto image = [&](const ufbx_texture* t) -> const Image* {
|
||||
if (!t) return nullptr;
|
||||
auto it = images.find(t);
|
||||
if (it != images.end()) return &it->second;
|
||||
Image& out = images[t];
|
||||
if (t->content.size) {
|
||||
decode(static_cast<const u8*>(t->content.data), t->content.size, out);
|
||||
return &out;
|
||||
}
|
||||
for (const ufbx_string& s : {t->filename, t->absolute_filename, t->relative_filename}) {
|
||||
if (!s.length) continue;
|
||||
std::string f(s.data, s.length);
|
||||
std::vector<u8> bytes = readFile(f);
|
||||
if (bytes.empty()) { f = (dir / fs::u8path(f).filename()).u8string(); bytes = readFile(f); } // moved next to the model
|
||||
if (!bytes.empty() && decode(bytes.data(), bytes.size(), out)) { files.push_back(f); break; }
|
||||
}
|
||||
return &out;
|
||||
};
|
||||
std::vector<uint32_t> tri;
|
||||
for (size_t ni = 0; ni < scene->nodes.count; ++ni) {
|
||||
const ufbx_node* node = scene->nodes.data[ni];
|
||||
const ufbx_mesh* mesh = node->mesh;
|
||||
if (!mesh) continue;
|
||||
tri.resize(mesh->max_face_triangles * 3);
|
||||
const ufbx_matrix& g = node->geometry_to_world;
|
||||
const float world[16] = {static_cast<float>(g.m00), static_cast<float>(g.m10), static_cast<float>(g.m20), 0,
|
||||
static_cast<float>(g.m01), static_cast<float>(g.m11), static_cast<float>(g.m21), 0,
|
||||
static_cast<float>(g.m02), static_cast<float>(g.m12), static_cast<float>(g.m22), 0,
|
||||
static_cast<float>(g.m03), static_cast<float>(g.m13), static_cast<float>(g.m23), 1};
|
||||
for (size_t pi = 0; pi < mesh->material_parts.count; ++pi) {
|
||||
const ufbx_mesh_part& part = mesh->material_parts.data[pi];
|
||||
if (part.num_triangles == 0) continue;
|
||||
MeshData md;
|
||||
md.name = std::string(node->name.data, node->name.length) + (mesh->material_parts.count > 1 ? "_" + std::to_string(pi) : "");
|
||||
for (const ufbx_node* a = node->parent; a && !a->is_root; a = a->parent) // the nodes above it in the file
|
||||
if (a->name.length) md.path = md.path.empty() ? std::string(a->name.data, a->name.length) : std::string(a->name.data, a->name.length) + "/" + md.path;
|
||||
for (size_t fi = 0; fi < part.face_indices.count; ++fi) {
|
||||
const ufbx_face face = mesh->faces.data[part.face_indices.data[fi]];
|
||||
const uint32_t n = ufbx_triangulate_face(tri.data(), tri.size(), mesh, face);
|
||||
for (uint32_t k = 0; k < n * 3; ++k) {
|
||||
const uint32_t ix = tri[k];
|
||||
const ufbx_vec3 p = ufbx_get_vertex_vec3(&mesh->vertex_position, ix);
|
||||
md.positions.insert(md.positions.end(), {static_cast<float>(p.x), static_cast<float>(p.y), static_cast<float>(p.z)});
|
||||
if (mesh->vertex_uv.exists) {
|
||||
const ufbx_vec2 t = ufbx_get_vertex_vec2(&mesh->vertex_uv, ix);
|
||||
md.uvs.insert(md.uvs.end(), {static_cast<float>(t.x), flipV ? 1.0f - static_cast<float>(t.y) : static_cast<float>(t.y)});
|
||||
} else {
|
||||
md.uvs.insert(md.uvs.end(), {0.0f, 0.0f});
|
||||
}
|
||||
if (mesh->vertex_color.exists) {
|
||||
const ufbx_vec4 c = ufbx_get_vertex_vec4(&mesh->vertex_color, ix);
|
||||
for (double v : {c.x, c.y, c.z, c.w}) md.colors.push_back(static_cast<u8>(std::clamp(v, 0.0, 1.0) * 255.0 + 0.5));
|
||||
}
|
||||
md.indices.push_back(static_cast<u32>(md.indices.size()));
|
||||
}
|
||||
}
|
||||
std::memcpy(md.worldMatrix, world, sizeof world);
|
||||
md.hasWorldMatrix = true;
|
||||
float color[4] = {1, 1, 1, 1};
|
||||
const Image* img = nullptr;
|
||||
bool blend = false;
|
||||
const ufbx_material* mat = pi < mesh->materials.count ? mesh->materials.data[pi] : nullptr;
|
||||
if (mat) {
|
||||
const ufbx_material_map& base = mat->pbr.base_color.has_value ? mat->pbr.base_color : mat->fbx.diffuse_color;
|
||||
if (base.has_value) for (int k = 0; k < 3; ++k) color[k] = static_cast<float>(base.value_vec4.v[k]);
|
||||
if (mat->pbr.base_factor.has_value && mat->pbr.base_factor.value_real > 0)
|
||||
for (int k = 0; k < 3; ++k) color[k] *= static_cast<float>(mat->pbr.base_factor.value_real);
|
||||
img = image(mat->pbr.base_color.texture ? mat->pbr.base_color.texture : mat->fbx.diffuse_color.texture);
|
||||
if (img && img->rgba.size() > 4) color[0] = color[1] = color[2] = 1.0f; // the texture carries the colour
|
||||
const double opacity = mat->pbr.opacity.has_value ? mat->pbr.opacity.value_real : 1.0;
|
||||
color[3] = static_cast<float>(opacity);
|
||||
blend = opacity < 0.999 || (mat->pbr.opacity.texture != nullptr);
|
||||
}
|
||||
finish(md, img, color, blend, img && img->alpha && !blend, 0.5f);
|
||||
meshes.push_back(std::move(md));
|
||||
}
|
||||
}
|
||||
ufbx_free_scene(scene);
|
||||
if (meshes.empty()) { error = "no meshes in " + path; return false; }
|
||||
std::sort(files.begin(), files.end());
|
||||
files.erase(std::unique(files.begin(), files.end()), files.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool isModelPath(const std::string& path) {
|
||||
const std::string ext = lower(fs::u8path(path).extension().u8string());
|
||||
return ext == ".gltf" || ext == ".glb" || ext == ".fbx" || ext == ".obj";
|
||||
}
|
||||
|
||||
bool importModel(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||
const std::string ext = lower(fs::u8path(path).extension().u8string());
|
||||
if (ext == ".gltf" || ext == ".glb") return importGltf(path, meshes, files, error);
|
||||
if (ext == ".fbx" || ext == ".obj") return importUfbx(path, meshes, files, error);
|
||||
error = "unsupported model type: " + path;
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
// Models from Blender or Unity placed into a home: glTF 2.0 (.gltf / .glb — Blender's native export, Unity's
|
||||
// glTFast / UnityGLTF), FBX and OBJ (both tools' classic exports). Each mesh part becomes one MeshData drawn
|
||||
// with the built-in unlit textured program (the same one Clay homes use): base-colour texture x colour factor,
|
||||
// alpha blending or cut-out from the material. Units become metres and up becomes +Y.
|
||||
#include "core/types.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
bool isModelPath(const std::string& path);
|
||||
|
||||
// `files` receives every file the model read (itself, external buffers, textures), so a .fenv can carry them.
|
||||
bool importModel(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error);
|
||||
@@ -0,0 +1,360 @@
|
||||
#pragma once
|
||||
// OverlayTexture: hands a CPU-drawn RGBA canvas to a SteamVR overlay through Vulkan images this process owns
|
||||
// (SetOverlayTexture), instead of SetOverlayRaw. SetOverlayRaw makes SteamVR create a new texture on every call,
|
||||
// and the dashboard can show that texture missing or half-made for a frame: the panel flickered whenever it
|
||||
// redrew (pointer moves, stats refreshes). Here the canvas goes into one of three images in turn (staging buffer,
|
||||
// copy, wait for the copy's fence), so the image SteamVR is showing is never the one being written.
|
||||
//
|
||||
// Vulkan is loaded at run time (dlopen libvulkan.so.1): no link dependency, and if anything is missing init()
|
||||
// fails and the caller keeps using SetOverlayRaw.
|
||||
#ifndef VK_NO_PROTOTYPES
|
||||
#define VK_NO_PROTOTYPES
|
||||
#endif
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <openvr.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class OverlayTexture {
|
||||
public:
|
||||
static constexpr int kSlots = 3;
|
||||
|
||||
// w x h RGBA8 (sRGB-encoded bytes, like SetOverlayRaw takes). Needs VR_Init done (the HMD's GPU comes from
|
||||
// VRSystem()->GetOutputDevice, the extensions from VRCompositor()).
|
||||
bool init(uint32_t w, uint32_t h, std::string& err) {
|
||||
w_ = w; h_ = h;
|
||||
if (!(lib_ = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL)) && !(lib_ = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL))) return fail(err, "no libvulkan");
|
||||
gipa_ = reinterpret_cast<PFN_vkGetInstanceProcAddr>(dlsym(lib_, "vkGetInstanceProcAddr"));
|
||||
if (!gipa_) return fail(err, "no vkGetInstanceProcAddr");
|
||||
#define OT_GLOBAL(n) n = reinterpret_cast<PFN_##n>(gipa_(VK_NULL_HANDLE, #n)); if (!n) return fail(err, "missing " #n)
|
||||
OT_GLOBAL(vkCreateInstance);
|
||||
OT_GLOBAL(vkEnumerateInstanceExtensionProperties);
|
||||
#undef OT_GLOBAL
|
||||
if (!vr::VRCompositor() || !vr::VRSystem()) return fail(err, "no compositor");
|
||||
// Instance, with the extensions SteamVR wants (those this loader has).
|
||||
std::vector<std::string> want = split(instanceExtensions());
|
||||
uint32_t n = 0;
|
||||
vkEnumerateInstanceExtensionProperties(nullptr, &n, nullptr);
|
||||
std::vector<VkExtensionProperties> have(n);
|
||||
vkEnumerateInstanceExtensionProperties(nullptr, &n, have.data());
|
||||
std::vector<const char*> ie;
|
||||
for (const std::string& e : want)
|
||||
if (std::any_of(have.begin(), have.end(), [&](const VkExtensionProperties& p) { return e == p.extensionName; })) ie.push_back(e.c_str());
|
||||
VkApplicationInfo app = {VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
||||
app.pApplicationName = "frame-switcher";
|
||||
app.apiVersion = VK_API_VERSION_1_1;
|
||||
VkInstanceCreateInfo ici = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
|
||||
ici.pApplicationInfo = &app;
|
||||
ici.enabledExtensionCount = static_cast<uint32_t>(ie.size());
|
||||
ici.ppEnabledExtensionNames = ie.data();
|
||||
if (VkResult r = vkCreateInstance(&ici, nullptr, &inst_); r != VK_SUCCESS) return fail(err, "vkCreateInstance " + std::to_string(r));
|
||||
if (!loadInstance()) return fail(err, "missing Vulkan entry points");
|
||||
// The GPU the headset is on (the only one on the Frame, but ask).
|
||||
uint64_t out = 0;
|
||||
vr::VRSystem()->GetOutputDevice(&out, vr::TextureType_Vulkan, reinterpret_cast<VkInstance_T*>(inst_));
|
||||
pd_ = reinterpret_cast<VkPhysicalDevice>(out);
|
||||
if (!pd_) {
|
||||
uint32_t c = 1;
|
||||
VkPhysicalDevice first = VK_NULL_HANDLE;
|
||||
vkEnumeratePhysicalDevices(inst_, &c, &first);
|
||||
pd_ = first;
|
||||
}
|
||||
if (!pd_) return fail(err, "no GPU");
|
||||
uint32_t qn = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(pd_, &qn, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> qf(qn);
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(pd_, &qn, qf.data());
|
||||
qf_ = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < qn; ++i) if (qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { qf_ = i; break; }
|
||||
if (qf_ == UINT32_MAX) return fail(err, "no graphics queue");
|
||||
// Device, with SteamVR's device extensions (external memory: SteamVR copies the image out on our queue).
|
||||
uint32_t dn = vr::VRCompositor()->GetVulkanDeviceExtensionsRequired(reinterpret_cast<VkPhysicalDevice_T*>(pd_), nullptr, 0);
|
||||
std::string dext(dn, '\0');
|
||||
if (dn) vr::VRCompositor()->GetVulkanDeviceExtensionsRequired(reinterpret_cast<VkPhysicalDevice_T*>(pd_), &dext[0], dn);
|
||||
devExt_ = split(dext.c_str());
|
||||
uint32_t en = 0;
|
||||
vkEnumerateDeviceExtensionProperties(pd_, nullptr, &en, nullptr);
|
||||
std::vector<VkExtensionProperties> dhave(en);
|
||||
vkEnumerateDeviceExtensionProperties(pd_, nullptr, &en, dhave.data());
|
||||
std::vector<const char*> de;
|
||||
for (const std::string& e : devExt_)
|
||||
if (std::any_of(dhave.begin(), dhave.end(), [&](const VkExtensionProperties& p) { return e == p.extensionName; })) de.push_back(e.c_str());
|
||||
const float prio = 1.0f;
|
||||
VkDeviceQueueCreateInfo qci = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
qci.queueFamilyIndex = qf_;
|
||||
qci.queueCount = 1;
|
||||
qci.pQueuePriorities = &prio;
|
||||
VkDeviceCreateInfo dci = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
||||
dci.queueCreateInfoCount = 1;
|
||||
dci.pQueueCreateInfos = &qci;
|
||||
dci.enabledExtensionCount = static_cast<uint32_t>(de.size());
|
||||
dci.ppEnabledExtensionNames = de.data();
|
||||
if (VkResult r = vkCreateDevice(pd_, &dci, nullptr, &dev_); r != VK_SUCCESS) return fail(err, "vkCreateDevice " + std::to_string(r));
|
||||
vkGetDeviceQueue(dev_, qf_, 0, &queue_);
|
||||
VkPhysicalDeviceMemoryProperties mp;
|
||||
vkGetPhysicalDeviceMemoryProperties(pd_, &mp);
|
||||
VkCommandPoolCreateInfo pci = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pci.queueFamilyIndex = qf_;
|
||||
if (vkCreateCommandPool(dev_, &pci, nullptr, &pool_) != VK_SUCCESS) return fail(err, "vkCreateCommandPool");
|
||||
const VkDeviceSize bytes = static_cast<VkDeviceSize>(w_) * h_ * 4;
|
||||
for (Slot& s : slots_) {
|
||||
VkImageCreateInfo ii = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
ii.imageType = VK_IMAGE_TYPE_2D;
|
||||
ii.format = format();
|
||||
ii.extent = {w_, h_, 1};
|
||||
ii.mipLevels = 1;
|
||||
ii.arrayLayers = 1;
|
||||
ii.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
ii.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
ii.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
ii.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
if (vkCreateImage(dev_, &ii, nullptr, &s.image) != VK_SUCCESS) return fail(err, "vkCreateImage");
|
||||
VkMemoryRequirements mr;
|
||||
vkGetImageMemoryRequirements(dev_, s.image, &mr);
|
||||
if (!allocate(mp, mr, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, s.imageMem) && !allocate(mp, mr, 0, s.imageMem)) return fail(err, "image memory");
|
||||
vkBindImageMemory(dev_, s.image, s.imageMem, 0);
|
||||
VkBufferCreateInfo bi = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
bi.size = bytes;
|
||||
bi.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
if (vkCreateBuffer(dev_, &bi, nullptr, &s.staging) != VK_SUCCESS) return fail(err, "vkCreateBuffer");
|
||||
vkGetBufferMemoryRequirements(dev_, s.staging, &mr);
|
||||
if (!allocate(mp, mr, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, s.stagingMem)) return fail(err, "staging memory");
|
||||
vkBindBufferMemory(dev_, s.staging, s.stagingMem, 0);
|
||||
if (vkMapMemory(dev_, s.stagingMem, 0, bytes, 0, &s.mapped) != VK_SUCCESS) return fail(err, "vkMapMemory");
|
||||
VkCommandBufferAllocateInfo ai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
ai.commandPool = pool_;
|
||||
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
ai.commandBufferCount = 1;
|
||||
if (vkAllocateCommandBuffers(dev_, &ai, &s.cmd) != VK_SUCCESS) return fail(err, "vkAllocateCommandBuffers");
|
||||
VkFenceCreateInfo fi = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
fi.flags = VK_FENCE_CREATE_SIGNALED_BIT;
|
||||
if (vkCreateFence(dev_, &fi, nullptr, &s.fence) != VK_SUCCESS) return fail(err, "vkCreateFence");
|
||||
}
|
||||
ok_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Copies `rgba` (w x h x 4) into the next image and shows it on `ov`. False: the caller should fall back.
|
||||
bool upload(vr::VROverlayHandle_t ov, const uint8_t* rgba, std::string& err) {
|
||||
if (!ok_) return false;
|
||||
Slot& s = slots_[next_];
|
||||
next_ = (next_ + 1) % kSlots;
|
||||
// This slot was last shown two uploads ago: its copy finished long since, but don't assume.
|
||||
if (vkWaitForFences(dev_, 1, &s.fence, VK_TRUE, 1000000000ull) != VK_SUCCESS) { err = "fence timeout"; return false; }
|
||||
vkResetFences(dev_, 1, &s.fence);
|
||||
std::memcpy(s.mapped, rgba, static_cast<size_t>(w_) * h_ * 4);
|
||||
vkResetCommandBuffer(s.cmd, 0);
|
||||
VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
vkBeginCommandBuffer(s.cmd, &bi);
|
||||
VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
b.image = s.image;
|
||||
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
// After SteamVR's copy out of it (on this queue, in TRANSFER_SRC_OPTIMAL) → ready for ours in.
|
||||
b.oldLayout = s.used ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
b.srcAccessMask = s.used ? VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT : 0;
|
||||
b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(s.cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
VkBufferImageCopy c = {};
|
||||
c.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
c.imageExtent = {w_, h_, 1};
|
||||
vkCmdCopyBufferToImage(s.cmd, s.staging, s.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &c);
|
||||
// SteamVR takes Vulkan overlay images in TRANSFER_SRC_OPTIMAL (it copies them out).
|
||||
b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
b.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT;
|
||||
vkCmdPipelineBarrier(s.cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
vkEndCommandBuffer(s.cmd);
|
||||
VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
si.commandBufferCount = 1;
|
||||
si.pCommandBuffers = &s.cmd;
|
||||
if (VkResult r = vkQueueSubmit(queue_, 1, &si, s.fence); r != VK_SUCCESS) { err = "vkQueueSubmit " + std::to_string(r); return false; }
|
||||
s.used = true;
|
||||
// The whole image is in before SteamVR looks at it.
|
||||
if (vkWaitForFences(dev_, 1, &s.fence, VK_TRUE, 1000000000ull) != VK_SUCCESS) { err = "copy fence timeout"; return false; }
|
||||
vr::VRVulkanTextureData_t vt = {};
|
||||
vt.m_nImage = reinterpret_cast<uint64_t>(s.image);
|
||||
vt.m_pDevice = reinterpret_cast<VkDevice_T*>(dev_);
|
||||
vt.m_pPhysicalDevice = reinterpret_cast<VkPhysicalDevice_T*>(pd_);
|
||||
vt.m_pInstance = reinterpret_cast<VkInstance_T*>(inst_);
|
||||
vt.m_pQueue = reinterpret_cast<VkQueue_T*>(queue_);
|
||||
vt.m_nQueueFamilyIndex = qf_;
|
||||
vt.m_nWidth = w_;
|
||||
vt.m_nHeight = h_;
|
||||
vt.m_nFormat = format();
|
||||
vt.m_nSampleCount = 1;
|
||||
vr::Texture_t tex = {&vt, vr::TextureType_Vulkan, vr::ColorSpace_Auto};
|
||||
const vr::EVROverlayError oe = vr::VROverlay()->SetOverlayTexture(ov, &tex);
|
||||
if (oe != vr::VROverlayError_None) { err = std::string("SetOverlayTexture: ") + vr::VROverlay()->GetOverlayErrorNameFromEnum(oe); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
// After VR_Shutdown (vrclient still holds the images until then).
|
||||
void destroy() {
|
||||
if (dev_) {
|
||||
vkDeviceWaitIdle(dev_);
|
||||
for (Slot& s : slots_) {
|
||||
if (s.fence) vkDestroyFence(dev_, s.fence, nullptr);
|
||||
if (s.staging) vkDestroyBuffer(dev_, s.staging, nullptr);
|
||||
if (s.stagingMem) vkFreeMemory(dev_, s.stagingMem, nullptr);
|
||||
if (s.image) vkDestroyImage(dev_, s.image, nullptr);
|
||||
if (s.imageMem) vkFreeMemory(dev_, s.imageMem, nullptr);
|
||||
s = Slot();
|
||||
}
|
||||
if (pool_) vkDestroyCommandPool(dev_, pool_, nullptr);
|
||||
vkDestroyDevice(dev_, nullptr);
|
||||
}
|
||||
if (inst_ && vkDestroyInstance) vkDestroyInstance(inst_, nullptr);
|
||||
dev_ = VK_NULL_HANDLE; inst_ = VK_NULL_HANDLE; pool_ = VK_NULL_HANDLE;
|
||||
ok_ = false;
|
||||
// libvulkan stays loaded: drivers don't like being unloaded under a live process.
|
||||
}
|
||||
bool ok() const { return ok_; }
|
||||
|
||||
// The canvas holds sRGB-encoded bytes. UNORM with ColorSpace_Auto (= gamma for 8-bit formats) shows them as
|
||||
// SetOverlayRaw did; FH_SWITCHER_SRGB=1 tries the _SRGB format instead.
|
||||
static VkFormat format() {
|
||||
static const VkFormat f = [] { const char* e = std::getenv("FH_SWITCHER_SRGB"); return e && *e == '1' ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM; }();
|
||||
return f;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Slot {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory imageMem = VK_NULL_HANDLE;
|
||||
VkBuffer staging = VK_NULL_HANDLE;
|
||||
VkDeviceMemory stagingMem = VK_NULL_HANDLE;
|
||||
void* mapped = nullptr;
|
||||
VkCommandBuffer cmd = VK_NULL_HANDLE;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
bool used = false;
|
||||
};
|
||||
|
||||
static std::vector<std::string> split(const std::string& s) {
|
||||
std::vector<std::string> out;
|
||||
std::istringstream in(s);
|
||||
for (std::string w; in >> w;) out.push_back(w);
|
||||
return out;
|
||||
}
|
||||
static std::string instanceExtensions() {
|
||||
const uint32_t n = vr::VRCompositor()->GetVulkanInstanceExtensionsRequired(nullptr, 0);
|
||||
std::string ext(n, '\0');
|
||||
if (n) vr::VRCompositor()->GetVulkanInstanceExtensionsRequired(&ext[0], n);
|
||||
return ext.c_str();
|
||||
}
|
||||
bool fail(std::string& err, const std::string& what) {
|
||||
err = what;
|
||||
destroy();
|
||||
return false;
|
||||
}
|
||||
bool allocate(const VkPhysicalDeviceMemoryProperties& mp, const VkMemoryRequirements& mr, VkMemoryPropertyFlags want, VkDeviceMemory& out) {
|
||||
for (uint32_t i = 0; i < mp.memoryTypeCount; ++i)
|
||||
if ((mr.memoryTypeBits & (1u << i)) && (mp.memoryTypes[i].propertyFlags & want) == want) {
|
||||
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
ai.allocationSize = mr.size;
|
||||
ai.memoryTypeIndex = i;
|
||||
return vkAllocateMemory(dev_, &ai, nullptr, &out) == VK_SUCCESS;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool loadInstance() {
|
||||
#define OT_INST(n) n = reinterpret_cast<PFN_##n>(gipa_(inst_, #n)); if (!n) return false
|
||||
OT_INST(vkDestroyInstance);
|
||||
OT_INST(vkEnumeratePhysicalDevices);
|
||||
OT_INST(vkGetPhysicalDeviceQueueFamilyProperties);
|
||||
OT_INST(vkGetPhysicalDeviceMemoryProperties);
|
||||
OT_INST(vkEnumerateDeviceExtensionProperties);
|
||||
OT_INST(vkCreateDevice);
|
||||
OT_INST(vkDestroyDevice);
|
||||
OT_INST(vkGetDeviceQueue);
|
||||
OT_INST(vkDeviceWaitIdle);
|
||||
OT_INST(vkCreateImage);
|
||||
OT_INST(vkDestroyImage);
|
||||
OT_INST(vkGetImageMemoryRequirements);
|
||||
OT_INST(vkBindImageMemory);
|
||||
OT_INST(vkCreateBuffer);
|
||||
OT_INST(vkDestroyBuffer);
|
||||
OT_INST(vkGetBufferMemoryRequirements);
|
||||
OT_INST(vkBindBufferMemory);
|
||||
OT_INST(vkAllocateMemory);
|
||||
OT_INST(vkFreeMemory);
|
||||
OT_INST(vkMapMemory);
|
||||
OT_INST(vkCreateCommandPool);
|
||||
OT_INST(vkDestroyCommandPool);
|
||||
OT_INST(vkAllocateCommandBuffers);
|
||||
OT_INST(vkResetCommandBuffer);
|
||||
OT_INST(vkBeginCommandBuffer);
|
||||
OT_INST(vkEndCommandBuffer);
|
||||
OT_INST(vkCmdPipelineBarrier);
|
||||
OT_INST(vkCmdCopyBufferToImage);
|
||||
OT_INST(vkQueueSubmit);
|
||||
OT_INST(vkCreateFence);
|
||||
OT_INST(vkDestroyFence);
|
||||
OT_INST(vkWaitForFences);
|
||||
OT_INST(vkResetFences);
|
||||
#undef OT_INST
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t w_ = 0, h_ = 0;
|
||||
void* lib_ = nullptr;
|
||||
PFN_vkGetInstanceProcAddr gipa_ = nullptr;
|
||||
VkInstance inst_ = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice pd_ = VK_NULL_HANDLE;
|
||||
VkDevice dev_ = VK_NULL_HANDLE;
|
||||
VkQueue queue_ = VK_NULL_HANDLE;
|
||||
uint32_t qf_ = 0;
|
||||
VkCommandPool pool_ = VK_NULL_HANDLE;
|
||||
std::vector<std::string> devExt_;
|
||||
Slot slots_[kSlots];
|
||||
int next_ = 0;
|
||||
bool ok_ = false;
|
||||
|
||||
PFN_vkCreateInstance vkCreateInstance = nullptr;
|
||||
PFN_vkEnumerateInstanceExtensionProperties vkEnumerateInstanceExtensionProperties = nullptr;
|
||||
PFN_vkDestroyInstance vkDestroyInstance = nullptr;
|
||||
PFN_vkEnumeratePhysicalDevices vkEnumeratePhysicalDevices = nullptr;
|
||||
PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties = nullptr;
|
||||
PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties = nullptr;
|
||||
PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties = nullptr;
|
||||
PFN_vkCreateDevice vkCreateDevice = nullptr;
|
||||
PFN_vkDestroyDevice vkDestroyDevice = nullptr;
|
||||
PFN_vkGetDeviceQueue vkGetDeviceQueue = nullptr;
|
||||
PFN_vkDeviceWaitIdle vkDeviceWaitIdle = nullptr;
|
||||
PFN_vkCreateImage vkCreateImage = nullptr;
|
||||
PFN_vkDestroyImage vkDestroyImage = nullptr;
|
||||
PFN_vkGetImageMemoryRequirements vkGetImageMemoryRequirements = nullptr;
|
||||
PFN_vkBindImageMemory vkBindImageMemory = nullptr;
|
||||
PFN_vkCreateBuffer vkCreateBuffer = nullptr;
|
||||
PFN_vkDestroyBuffer vkDestroyBuffer = nullptr;
|
||||
PFN_vkGetBufferMemoryRequirements vkGetBufferMemoryRequirements = nullptr;
|
||||
PFN_vkBindBufferMemory vkBindBufferMemory = nullptr;
|
||||
PFN_vkAllocateMemory vkAllocateMemory = nullptr;
|
||||
PFN_vkFreeMemory vkFreeMemory = nullptr;
|
||||
PFN_vkMapMemory vkMapMemory = nullptr;
|
||||
PFN_vkCreateCommandPool vkCreateCommandPool = nullptr;
|
||||
PFN_vkDestroyCommandPool vkDestroyCommandPool = nullptr;
|
||||
PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers = nullptr;
|
||||
PFN_vkResetCommandBuffer vkResetCommandBuffer = nullptr;
|
||||
PFN_vkBeginCommandBuffer vkBeginCommandBuffer = nullptr;
|
||||
PFN_vkEndCommandBuffer vkEndCommandBuffer = nullptr;
|
||||
PFN_vkCmdPipelineBarrier vkCmdPipelineBarrier = nullptr;
|
||||
PFN_vkCmdCopyBufferToImage vkCmdCopyBufferToImage = nullptr;
|
||||
PFN_vkQueueSubmit vkQueueSubmit = nullptr;
|
||||
PFN_vkCreateFence vkCreateFence = nullptr;
|
||||
PFN_vkDestroyFence vkDestroyFence = nullptr;
|
||||
PFN_vkWaitForFences vkWaitForFences = nullptr;
|
||||
PFN_vkResetFences vkResetFences = nullptr;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
// SteamVR's panels (overlays) of every app. OpenVR can't list them, but a handle is (slot << 32 | serial) with both
|
||||
// small, so probing that range finds them all; a whole sweep takes about 0.1 s. Used to tell whether a hand points
|
||||
// at a panel (frame-switcher) and to dock free-floating panels on a home's wall spots (Frame Home).
|
||||
#include <openvr.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct OverlayInfo {
|
||||
vr::VROverlayHandle_t handle = 0;
|
||||
std::string key;
|
||||
vr::VROverlayTransformType type = vr::VROverlayTransform_Absolute;
|
||||
};
|
||||
|
||||
inline std::vector<OverlayInfo> listOverlays() {
|
||||
std::vector<OverlayInfo> out;
|
||||
vr::IVROverlay* ov = vr::VROverlay();
|
||||
if (!ov) return out;
|
||||
for (uint64_t slot = 0; slot < 256; ++slot)
|
||||
for (uint64_t serial = 0; serial < 1024; ++serial) {
|
||||
const vr::VROverlayHandle_t h = slot << 32 | serial;
|
||||
char key[vr::k_unVROverlayMaxKeyLength] = {};
|
||||
vr::EVROverlayError e = vr::VROverlayError_None;
|
||||
if (!ov->GetOverlayKey(h, key, sizeof key, &e) || e != vr::VROverlayError_None || !key[0]) continue;
|
||||
OverlayInfo o;
|
||||
o.handle = h;
|
||||
o.key = key;
|
||||
ov->GetOverlayTransformType(h, &o.type);
|
||||
out.push_back(std::move(o));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Panels a person looks at and points at: not cursors, laser pointers, thumbnails or Frame Home's own.
|
||||
inline bool isUserPanel(const OverlayInfo& o) {
|
||||
if (o.type == vr::VROverlayTransform_Cursor || o.type == vr::VROverlayTransform_DashboardThumb) return false;
|
||||
for (const char* p : {"framehome.", "system.pointer", "system.cursor", "gamescope.", "XRService"})
|
||||
if (o.key.rfind(p, 0) == 0) return false;
|
||||
return o.key.find(".thumb") == std::string::npos && o.key.find("cursor") == std::string::npos;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
#pragma once
|
||||
// Wall spots at work: a SteamVR panel (a desktop or game window, any free-floating overlay) let go of near one of
|
||||
// the home's wall spots docks onto it — flush with the wall, facing the room, no wider than the spot — and stays
|
||||
// there as you walk and teleport (panels live in tracking space, so a docked one is re-placed from the home's frame
|
||||
// whenever you move). Grab it away and it's free again, back at its own size. Docked panels are re-placed only when
|
||||
// the rig moves: setting one every frame would overwrite a grab before it could be seen.
|
||||
// Meta's homes mark these spots with WallPlacementComponent; Frame Studio edits them (World > Wall Spot).
|
||||
#include "core/world.h"
|
||||
#include "overlays.h"
|
||||
|
||||
#include <openvr.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
class PanelDock {
|
||||
public:
|
||||
~PanelDock() { stop(); }
|
||||
// Before VR_Shutdown: the scanner calls into SteamVR's client library, which shutting down unloads.
|
||||
void stop() { stop_ = true; if (scanner_.joinable()) scanner_.join(); }
|
||||
|
||||
void setWorld(const HomeWorld& w) {
|
||||
undockAll();
|
||||
spots_ = w.wallSpots;
|
||||
if (!spots_.empty() && !scanner_.joinable()) scanner_ = std::thread([this] { scanLoop(); });
|
||||
}
|
||||
|
||||
// Once per frame with worldFromTracking.
|
||||
void update(const float rig[16]) {
|
||||
if (spots_.empty()) return;
|
||||
std::vector<OverlayInfo> panels;
|
||||
{ std::lock_guard<std::mutex> l(mutex_); panels = panels_; }
|
||||
float inv[16];
|
||||
invertRigid(rig, inv);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
vr::IVROverlay* ov = vr::VROverlay();
|
||||
// Panels whose app went away: their state (and any spot they held) goes too.
|
||||
for (auto it = state_.begin(); it != state_.end();) {
|
||||
bool present = false;
|
||||
for (const OverlayInfo& p : panels) present |= p.handle == it->first;
|
||||
it = present ? std::next(it) : state_.erase(it);
|
||||
}
|
||||
for (const OverlayInfo& p : panels) {
|
||||
if (!ov->IsOverlayVisible(p.handle)) continue;
|
||||
vr::VROverlayTransformType tt;
|
||||
if (ov->GetOverlayTransformType(p.handle, &tt) != vr::VROverlayError_None || tt != vr::VROverlayTransform_Absolute) continue;
|
||||
vr::ETrackingUniverseOrigin origin;
|
||||
vr::HmdMatrix34_t cur;
|
||||
if (ov->GetOverlayTransformAbsolute(p.handle, &origin, &cur) != vr::VROverlayError_None) continue;
|
||||
if (origin != vr::TrackingUniverseStanding) continue; // the spots are placed in standing space
|
||||
State& s = state_[p.handle];
|
||||
if (s.spot >= 0) {
|
||||
// Docked: still where we put it? Then keep it on the wall as the rig moves; else it was grabbed away.
|
||||
if (differs(cur, s.placed, 0.03f)) { undock(p.handle, s, "moved away"); s.still = now; s.last = cur; continue; }
|
||||
// Re-placed only once the rig has moved a few millimetres since it was placed (per panel, so one
|
||||
// hidden while you teleported is put right when it shows again). Tiny continuous rig changes —
|
||||
// seated eye-height easing, step easing — would otherwise overwrite a grab every frame.
|
||||
float moved = 0;
|
||||
for (int k = 0; k < 16; ++k) moved = std::max(moved, std::fabs(rig[k] - s.rig[k]));
|
||||
if (moved < 0.005f) continue;
|
||||
vr::HmdMatrix34_t m = spotInTracking(s.spot, inv, s.flip);
|
||||
ov->SetOverlayTransformAbsolute(p.handle, origin, &m);
|
||||
s.placed = m;
|
||||
std::memcpy(s.rig, rig, sizeof s.rig);
|
||||
continue;
|
||||
}
|
||||
if (differs(cur, s.last, 0.002f)) { s.last = cur; s.still = now; continue; } // still moving
|
||||
if (now - s.still < std::chrono::milliseconds(500) || s.cooldown > now) continue;
|
||||
// At rest: dock onto the nearest free spot within 0.6 m.
|
||||
float pt[3];
|
||||
for (int k = 0; k < 3; ++k) pt[k] = rig[k] * cur.m[0][3] + rig[4 + k] * cur.m[1][3] + rig[8 + k] * cur.m[2][3] + rig[12 + k];
|
||||
int best = -1;
|
||||
float bestD = 0.6f;
|
||||
for (size_t i = 0; i < spots_.size(); ++i) {
|
||||
if (taken(static_cast<int>(i))) continue;
|
||||
const float* c = &spots_[i].world[12];
|
||||
const float d = std::sqrt((pt[0] - c[0]) * (pt[0] - c[0]) + (pt[1] - c[1]) * (pt[1] - c[1]) + (pt[2] - c[2]) * (pt[2] - c[2]));
|
||||
if (d < bestD) { bestD = d; best = static_cast<int>(i); }
|
||||
}
|
||||
if (best < 0) { s.cooldown = now + std::chrono::milliseconds(500); continue; }
|
||||
// Face the panel the way it was facing (towards the user), whichever way the spot's frame points.
|
||||
const float* z = &spots_[best].world[8];
|
||||
float pz[3];
|
||||
for (int k = 0; k < 3; ++k) pz[k] = rig[k] * cur.m[0][2] + rig[4 + k] * cur.m[1][2] + rig[8 + k] * cur.m[2][2];
|
||||
s.flip = z[0] * pz[0] + z[1] * pz[1] + z[2] * pz[2] < 0;
|
||||
ov->GetOverlayWidthInMeters(p.handle, &s.width);
|
||||
const float fit = std::min(s.width, spots_[best].width);
|
||||
ov->SetOverlayWidthInMeters(p.handle, fit);
|
||||
vr::HmdMatrix34_t m = spotInTracking(best, inv, s.flip);
|
||||
ov->SetOverlayTransformAbsolute(p.handle, origin, &m);
|
||||
s.placed = m;
|
||||
std::memcpy(s.rig, rig, sizeof s.rig);
|
||||
s.spot = best;
|
||||
std::fprintf(stderr, "frame-home: docked panel %s on wall spot %s\n", p.key.c_str(), spots_[best].name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct State {
|
||||
int spot = -1;
|
||||
bool flip = false;
|
||||
float width = 1; // its own width, given back when it leaves the wall
|
||||
vr::HmdMatrix34_t placed = {}, last = {};
|
||||
float rig[16] = {}; // the rig it was last placed for
|
||||
std::chrono::steady_clock::time_point still{}, cooldown{};
|
||||
};
|
||||
std::vector<WorldWallSpot> spots_;
|
||||
std::map<vr::VROverlayHandle_t, State> state_;
|
||||
std::vector<OverlayInfo> panels_;
|
||||
std::mutex mutex_;
|
||||
std::thread scanner_;
|
||||
std::atomic<bool> stop_{false};
|
||||
|
||||
// The panel list, refreshed every 2 s off the frame loop (a sweep takes ~0.1 s).
|
||||
void scanLoop() {
|
||||
while (!stop_) {
|
||||
std::vector<OverlayInfo> found;
|
||||
for (const OverlayInfo& o : listOverlays())
|
||||
if (isUserPanel(o) && o.key.rfind("system.", 0) != 0) found.push_back(o);
|
||||
{ std::lock_guard<std::mutex> l(mutex_); panels_ = std::move(found); }
|
||||
for (int i = 0; i < 20 && !stop_; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
}
|
||||
|
||||
bool taken(int spot) const {
|
||||
for (const auto& kv : state_) if (kv.second.spot == spot) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void undock(vr::VROverlayHandle_t h, State& s, const char* why) {
|
||||
vr::VROverlay()->SetOverlayWidthInMeters(h, s.width);
|
||||
std::fprintf(stderr, "frame-home: panel left wall spot %s (%s)\n", spots_[s.spot].name.c_str(), why);
|
||||
s.spot = -1;
|
||||
s.cooldown = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
}
|
||||
|
||||
void undockAll() {
|
||||
for (auto& kv : state_) if (kv.second.spot >= 0) undock(kv.first, kv.second, "new home");
|
||||
state_.clear();
|
||||
}
|
||||
|
||||
// The spot's frame in tracking space, 1 cm off the wall, optionally turned to face the other way.
|
||||
vr::HmdMatrix34_t spotInTracking(int i, const float inv[16], bool flip) const {
|
||||
float w[16];
|
||||
std::memcpy(w, spots_[i].world, 64);
|
||||
for (int c = 0; c < 3; ++c) { // unit axes (a scaled spot frame would scale the panel)
|
||||
const float L = std::sqrt(w[c * 4] * w[c * 4] + w[c * 4 + 1] * w[c * 4 + 1] + w[c * 4 + 2] * w[c * 4 + 2]);
|
||||
if (L > 1e-6f) for (int r = 0; r < 3; ++r) w[c * 4 + r] /= L;
|
||||
}
|
||||
if (flip) for (int r = 0; r < 3; ++r) { w[r] = -w[r]; w[8 + r] = -w[8 + r]; } // 180 degrees about Y
|
||||
for (int r = 0; r < 3; ++r) w[12 + r] += w[8 + r] * 0.01f;
|
||||
float t[16];
|
||||
for (int c = 0; c < 4; ++c)
|
||||
for (int r = 0; r < 4; ++r) t[c * 4 + r] = inv[r] * w[c * 4] + inv[4 + r] * w[c * 4 + 1] + inv[8 + r] * w[c * 4 + 2] + inv[12 + r] * w[c * 4 + 3];
|
||||
vr::HmdMatrix34_t m;
|
||||
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) m.m[r][c] = t[c * 4 + r];
|
||||
return m;
|
||||
}
|
||||
|
||||
static void invertRigid(const float m[16], float o[16]) {
|
||||
for (int r = 0; r < 3; ++r) for (int c = 0; c < 3; ++c) o[c * 4 + r] = m[r * 4 + c];
|
||||
for (int r = 0; r < 3; ++r) o[12 + r] = -(o[r] * m[12] + o[4 + r] * m[13] + o[8 + r] * m[14]);
|
||||
o[3] = o[7] = o[11] = 0; o[15] = 1;
|
||||
}
|
||||
|
||||
static bool differs(const vr::HmdMatrix34_t& a, const vr::HmdMatrix34_t& b, float eps) {
|
||||
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) if (std::fabs(a.m[r][c] - b.m[r][c]) > eps) return true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
#include "panorama.h"
|
||||
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "stb_image_write.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kPi = 3.14159265358979f;
|
||||
|
||||
// Camera-to-world rotation for a view yawed about +Y then pitched about +X (looks down -Z at 0, 0).
|
||||
void basis(float yaw, float pitch, float right[3], float up[3], float back[3]) {
|
||||
const float cy = std::cos(yaw), sy = std::sin(yaw), cp = std::cos(pitch), sp = std::sin(pitch);
|
||||
right[0] = cy; right[1] = 0; right[2] = -sy;
|
||||
up[0] = sy * sp; up[1] = cp; up[2] = cy * sp;
|
||||
back[0] = sy * cp; back[1] = -sp; back[2] = cy * cp;
|
||||
}
|
||||
|
||||
struct Face {
|
||||
float right[3], up[3], back[3];
|
||||
std::vector<u8> rgba;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool renderPanorama(HslRenderer& r, const PanoramaView& view, uint32_t width, std::vector<u8>& rgb) {
|
||||
const uint32_t w = std::max<uint32_t>(64, width & ~1u), h = w / 2;
|
||||
// A face spans 90 degrees = w/4 equirect pixels; a little more keeps it sharp after resampling.
|
||||
const uint32_t n = std::min<uint32_t>(4096, (w / 4) * 5 / 4);
|
||||
r.resize(n, n);
|
||||
|
||||
// Reversed-Z infinite projection with a 90-degree square frustum (what the renderer expects).
|
||||
Mat4 proj = {};
|
||||
proj.m[0] = 1.0f;
|
||||
proj.m[5] = -1.0f;
|
||||
proj.m[14] = 0.05f;
|
||||
proj.m[11] = -1.0f;
|
||||
|
||||
const float faces[6][2] = {{0, 0}, {kPi * 0.5f, 0}, {kPi, 0}, {-kPi * 0.5f, 0}, {0, kPi * 0.5f}, {0, -kPi * 0.5f}};
|
||||
Face f[6];
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
basis(faces[i][0], faces[i][1], f[i].right, f[i].up, f[i].back);
|
||||
EyeView e;
|
||||
e.proj = proj;
|
||||
// view = inverse(rotation | eye): rows are the basis vectors, translation -R^T * eye.
|
||||
Mat4& v = e.view;
|
||||
std::memset(v.m, 0, sizeof v.m);
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
v.m[k * 4 + 0] = f[i].right[k];
|
||||
v.m[k * 4 + 1] = f[i].up[k];
|
||||
v.m[k * 4 + 2] = f[i].back[k];
|
||||
}
|
||||
auto dot = [&](const float* a) { return a[0] * view.eye[0] + a[1] * view.eye[1] + a[2] * view.eye[2]; };
|
||||
v.m[12] = -dot(f[i].right);
|
||||
v.m[13] = -dot(f[i].up);
|
||||
v.m[14] = -dot(f[i].back);
|
||||
v.m[15] = 1;
|
||||
std::memcpy(e.position, view.eye, 12);
|
||||
EyeView eyes[2] = {e, e};
|
||||
r.render(eyes, 0.0f);
|
||||
if (!r.readback(0, f[i].rgba)) return false;
|
||||
}
|
||||
|
||||
rgb.assign(static_cast<size_t>(w) * h * 3, 0);
|
||||
const float yaw = view.yawDeg * kPi / 180.0f;
|
||||
auto rows = [&](uint32_t y0, uint32_t y1) {
|
||||
for (uint32_t y = y0; y < y1; ++y) {
|
||||
const float lat = (0.5f - (y + 0.5f) / h) * kPi;
|
||||
const float cl = std::cos(lat), sl = std::sin(lat);
|
||||
for (uint32_t x = 0; x < w; ++x) {
|
||||
const float lon = ((x + 0.5f) / w - 0.5f) * 2.0f * kPi - yaw;
|
||||
const float d[3] = {std::sin(lon) * cl, sl, -std::cos(lon) * cl};
|
||||
// The face this direction points into, and where it lands on that face's image.
|
||||
int best = 0;
|
||||
float bx = 0, by = 0, bz = -1e9f;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
const float vx = d[0] * f[i].right[0] + d[1] * f[i].right[1] + d[2] * f[i].right[2];
|
||||
const float vy = d[0] * f[i].up[0] + d[1] * f[i].up[1] + d[2] * f[i].up[2];
|
||||
const float vz = -(d[0] * f[i].back[0] + d[1] * f[i].back[1] + d[2] * f[i].back[2]);
|
||||
if (vz > bz) { bz = vz; bx = vx; by = vy; best = i; }
|
||||
}
|
||||
const float px = (bx / bz * 0.5f + 0.5f) * n - 0.5f, py = (0.5f - by / bz * 0.5f) * n - 0.5f;
|
||||
const int x0 = std::clamp(static_cast<int>(std::floor(px)), 0, static_cast<int>(n) - 1);
|
||||
const int y0i = std::clamp(static_cast<int>(std::floor(py)), 0, static_cast<int>(n) - 1);
|
||||
const int x1 = std::min(x0 + 1, static_cast<int>(n) - 1), y1i = std::min(y0i + 1, static_cast<int>(n) - 1);
|
||||
const float fx = std::clamp(px - x0, 0.0f, 1.0f), fy = std::clamp(py - y0i, 0.0f, 1.0f);
|
||||
const u8* img = f[best].rgba.data();
|
||||
u8* out = &rgb[(static_cast<size_t>(y) * w + x) * 3];
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
const float a = img[(y0i * n + x0) * 4 + c], b = img[(y0i * n + x1) * 4 + c];
|
||||
const float cc = img[(y1i * n + x0) * 4 + c], dd = img[(y1i * n + x1) * 4 + c];
|
||||
out[c] = static_cast<u8>(std::lround((a + (b - a) * fx) * (1 - fy) + (cc + (dd - cc) * fx) * fy));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const uint32_t threads = std::max(1u, std::min(16u, std::thread::hardware_concurrency()));
|
||||
std::vector<std::thread> pool;
|
||||
for (uint32_t t = 0; t < threads; ++t) pool.emplace_back(rows, h * t / threads, h * (t + 1) / threads);
|
||||
for (auto& t : pool) t.join();
|
||||
return true;
|
||||
}
|
||||
|
||||
float panoramaDomeRadius(const HslRenderer& r, const HslScene& scene, const float eye[3], float fallback) {
|
||||
constexpr int kRays = 72;
|
||||
std::vector<float> nearest(kRays, 1e30f);
|
||||
for (size_t m = 0; m < r.meshCount(); ++m) {
|
||||
if (!r.meshVisible(m)) continue;
|
||||
const MeshData& md = scene.meshes[r.meshSceneIndex(m)];
|
||||
if (md.isSkybox) continue;
|
||||
const float* W = r.meshWorld(m);
|
||||
const size_t nv = md.positions.size() / 3;
|
||||
std::vector<float> P(nv * 3);
|
||||
for (size_t v = 0; v < nv; ++v)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
P[v * 3 + k] = W[k] * md.positions[v * 3] + W[4 + k] * md.positions[v * 3 + 1] + W[8 + k] * md.positions[v * 3 + 2] + W[12 + k];
|
||||
for (size_t t = 0; t + 2 < md.indices.size(); t += 3) {
|
||||
const u32 ia = md.indices[t], ib = md.indices[t + 1], ic = md.indices[t + 2];
|
||||
if (ia >= nv || ib >= nv || ic >= nv) continue;
|
||||
const float* a = &P[ia * 3]; const float* b = &P[ib * 3]; const float* c = &P[ic * 3];
|
||||
// Only triangles spanning eye height can be hit by horizontal rays.
|
||||
if (std::min({a[1], b[1], c[1]}) > eye[1] || std::max({a[1], b[1], c[1]}) < eye[1]) continue;
|
||||
const float e1[3] = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}, e2[3] = {c[0] - a[0], c[1] - a[1], c[2] - a[2]};
|
||||
const float tv[3] = {eye[0] - a[0], eye[1] - a[1], eye[2] - a[2]};
|
||||
for (int i = 0; i < kRays; ++i) {
|
||||
const float ang = 2.0f * kPi * i / kRays;
|
||||
const float d[3] = {std::sin(ang), 0, -std::cos(ang)};
|
||||
const float p[3] = {d[1] * e2[2] - d[2] * e2[1], d[2] * e2[0] - d[0] * e2[2], d[0] * e2[1] - d[1] * e2[0]};
|
||||
const float det = e1[0] * p[0] + e1[1] * p[1] + e1[2] * p[2];
|
||||
if (std::fabs(det) < 1e-12f) continue;
|
||||
const float id = 1.0f / det;
|
||||
const float u = (tv[0] * p[0] + tv[1] * p[1] + tv[2] * p[2]) * id;
|
||||
if (u < 0 || u > 1) continue;
|
||||
const float q[3] = {tv[1] * e1[2] - tv[2] * e1[1], tv[2] * e1[0] - tv[0] * e1[2], tv[0] * e1[1] - tv[1] * e1[0]};
|
||||
const float v = (d[0] * q[0] + d[1] * q[1] + d[2] * q[2]) * id;
|
||||
if (v < 0 || u + v > 1) continue;
|
||||
const float dist = (e2[0] * q[0] + e2[1] * q[1] + e2[2] * q[2]) * id;
|
||||
if (dist > 0.05f && dist < nearest[i]) nearest[i] = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<float> hits;
|
||||
for (float d : nearest) if (d < 500.0f) hits.push_back(d);
|
||||
if (hits.size() < kRays / 4) return fallback;
|
||||
std::nth_element(hits.begin(), hits.begin() + hits.size() / 2, hits.end());
|
||||
return hits[hits.size() / 2];
|
||||
}
|
||||
|
||||
bool writePng(const std::string& path, const std::vector<u8>& rgb, uint32_t w, uint32_t h) {
|
||||
stbi_write_png_compression_level = 6;
|
||||
return stbi_write_png(path.c_str(), static_cast<int>(w), static_cast<int>(h), 3, rgb.data(), static_cast<int>(w * 3)) != 0;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
// Homes as the Steam Frame's native environment.
|
||||
//
|
||||
// SteamVR's compositor draws its environment itself from an equirectangular background image
|
||||
// (8192x4096, like its own night_mountains.png), optionally dome-projected: the image is projected
|
||||
// from the viewer's eye height onto a sphere of a given radius with a flat floor, so the floor and the
|
||||
// room's walls appear at roughly the right distance. No app runs; the compositor owns it.
|
||||
//
|
||||
// A home is rendered with its own Meta shaders into six 90-degree faces around the viewer and
|
||||
// resampled into that layout. The image centre is the direction `yawDeg` faces (0 = -Z, the home's
|
||||
// forward), with the image's right-hand side to the viewer's right.
|
||||
#include "hsl_renderer.h"
|
||||
#include "hsl_scene.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct PanoramaView {
|
||||
float eye[3] = {0, 1.6f, 0}; // where the viewer stands (eye height above the home's floor)
|
||||
float yawDeg = 0; // direction that becomes the image centre
|
||||
};
|
||||
|
||||
// Renders width x width/2 RGB8 (sRGB). Leaves the renderer resized to the face size.
|
||||
bool renderPanorama(HslRenderer& r, const PanoramaView& view, uint32_t width, std::vector<u8>& rgb);
|
||||
|
||||
// Median horizontal distance from `eye` to the home's visible geometry: the dome radius that puts the
|
||||
// projected walls where they are. Falls back to `fallback` when nothing is hit (open outdoor homes).
|
||||
float panoramaDomeRadius(const HslRenderer& r, const HslScene& scene, const float eye[3], float fallback = 10.0f);
|
||||
|
||||
bool writePng(const std::string& path, const std::vector<u8>& rgb, uint32_t w, uint32_t h);
|
||||
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
// Frame Home's performance and quality settings: ~/frame-home/settings.json on the Steam Frame. Frame Home reads it
|
||||
// when it starts and again whenever it changes; the Environments panel (Settings) and Frame Studio (Frame > Headset
|
||||
// Settings) edit it. Frame Home writes how it's doing to stats.json beside it for those two to show.
|
||||
#include "core/tinyjson.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
struct PerfSettings {
|
||||
// Resolution, as a share of SteamVR's recommended size per eye. Auto keeps each frame inside the display's time
|
||||
// budget by moving it between 70% and 150%: sharper whenever the home is light enough.
|
||||
bool autoResolution = true;
|
||||
float resolution = 1.0f;
|
||||
int msaa = 4; // anti-aliasing samples: 1 (off), 2, 4
|
||||
int anisotropy = 8; // texture filtering: 4, 8, 16
|
||||
// Foveated rendering: full detail where you look, coarser toward the edges, and the time that frees goes to
|
||||
// resolution (with Auto). 0 off, 1 fixed (the lens centre), 2 eye tracked (follows your gaze).
|
||||
int foveation = 2;
|
||||
int foveationLevel = 2; // 1 gentle, 2 medium, 3 strong
|
||||
// Moving around (applies live).
|
||||
int walk = 1; // left stick: 0 off, 1 smooth walking
|
||||
int teleport = 1; // right stick forward: 0 off, 1 instant (a quick blink), 2 slide (glide there)
|
||||
int turn = 0; // right stick left / right: 0 snap, 1 smooth
|
||||
int snapAngle = 30; // 30, 45, 90 degrees
|
||||
float moveSpeed = 2.0f; // walking speed, m/s: 1.4 slow, 2 normal, 3 fast
|
||||
int vignette = 0; // comfort: darken the edges of view while walking or turning smoothly: 0 off, 1 gentle, 2 strong
|
||||
int seatMarkers = 1; // the seats near you shown as markers: 0 hidden (pointing still finds them), 1 shown
|
||||
// The home itself (applies live).
|
||||
float volume = 1.0f; // all of the home's sound: 0 .. 1
|
||||
int animations = 1; // 0: the home holds still (animated homes cost less), 1 plays
|
||||
int menuRate = 1; // behind the Steam menu: 0 half rate (the GPU goes to the menu and its apps), 1 full
|
||||
// The environment's own frame rate: 0 the display's, 1 half (drawn every other frame and turned with your head in
|
||||
// between by the compositor layer, as Meta's home runs: each frame gets twice the GPU time).
|
||||
int frameRate = 0;
|
||||
// The headset's temperature to stay at or under, degrees C (0: no limit). Above it the edges of view get coarser,
|
||||
// a little at a time, until it's back (where you look stays sharp). 86 is the Steam Frame's own resting level with
|
||||
// SteamVR running; past the mid-90s it slows its GPU to a third.
|
||||
int tempLimit = 86;
|
||||
|
||||
static constexpr float kAutoMin = 0.7f, kAutoMax = 1.5f;
|
||||
|
||||
std::string toJson() const {
|
||||
char b[640];
|
||||
std::snprintf(b, sizeof b,
|
||||
"{\"autoResolution\":%s,\"resolution\":%.2f,\"msaa\":%d,\"anisotropy\":%d,\"foveation\":%d,\"foveationLevel\":%d,"
|
||||
"\"walk\":%d,\"teleport\":%d,\"turn\":%d,\"snapAngle\":%d,\"moveSpeed\":%.1f,\"vignette\":%d,\"seatMarkers\":%d,"
|
||||
"\"volume\":%.2f,\"animations\":%d,\"menuRate\":%d,\"frameRate\":%d,\"tempLimit\":%d}\n",
|
||||
autoResolution ? "true" : "false", resolution, msaa, anisotropy, foveation, foveationLevel, walk, teleport, turn, snapAngle, moveSpeed,
|
||||
vignette, seatMarkers, volume, animations, menuRate, frameRate, tempLimit);
|
||||
return b;
|
||||
}
|
||||
bool fromJson(const std::string& j) {
|
||||
// The parser takes a file cut short ("{\"msaa\":2,") as a whole object: a settings file must end its object.
|
||||
const size_t last = j.find_last_not_of(" \t\r\n");
|
||||
if (last == std::string::npos || j[last] != '}') return false;
|
||||
try {
|
||||
const tinyjson::Value d = tinyjson::parse(j);
|
||||
if (!d.isObject()) return false; // empty, cut short, or not settings
|
||||
if (d.has("autoResolution")) autoResolution = d["autoResolution"].boolVal;
|
||||
if (d.has("resolution")) resolution = static_cast<float>(d["resolution"].asFloat());
|
||||
if (d.has("msaa")) msaa = static_cast<int>(d["msaa"].asFloat());
|
||||
if (d.has("anisotropy")) anisotropy = static_cast<int>(d["anisotropy"].asFloat());
|
||||
if (d.has("foveation")) foveation = static_cast<int>(d["foveation"].asFloat());
|
||||
if (d.has("foveationLevel")) foveationLevel = static_cast<int>(d["foveationLevel"].asFloat());
|
||||
if (d.has("walk")) walk = static_cast<int>(d["walk"].asFloat());
|
||||
if (d.has("teleport")) teleport = static_cast<int>(d["teleport"].asFloat());
|
||||
if (d.has("turn")) turn = static_cast<int>(d["turn"].asFloat());
|
||||
if (d.has("snapAngle")) snapAngle = static_cast<int>(d["snapAngle"].asFloat());
|
||||
if (d.has("moveSpeed")) moveSpeed = static_cast<float>(d["moveSpeed"].asFloat());
|
||||
if (d.has("vignette")) vignette = static_cast<int>(d["vignette"].asFloat());
|
||||
if (d.has("seatMarkers")) seatMarkers = static_cast<int>(d["seatMarkers"].asFloat());
|
||||
if (d.has("volume")) volume = static_cast<float>(d["volume"].asFloat());
|
||||
if (d.has("animations")) animations = static_cast<int>(d["animations"].asFloat());
|
||||
if (d.has("menuRate")) menuRate = static_cast<int>(d["menuRate"].asFloat());
|
||||
if (d.has("frameRate")) frameRate = static_cast<int>(d["frameRate"].asFloat());
|
||||
if (d.has("tempLimit")) tempLimit = static_cast<int>(d["tempLimit"].asFloat());
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
clamp();
|
||||
return true;
|
||||
}
|
||||
void clamp() {
|
||||
resolution = std::clamp(resolution, 0.5f, 2.0f);
|
||||
msaa = msaa >= 4 ? 4 : msaa >= 2 ? 2 : 1;
|
||||
anisotropy = anisotropy >= 16 ? 16 : anisotropy >= 8 ? 8 : 4;
|
||||
foveation = std::clamp(foveation, 0, 2);
|
||||
foveationLevel = std::clamp(foveationLevel, 1, 3);
|
||||
walk = std::clamp(walk, 0, 1);
|
||||
teleport = std::clamp(teleport, 0, 2);
|
||||
turn = std::clamp(turn, 0, 1);
|
||||
snapAngle = snapAngle >= 90 ? 90 : snapAngle >= 45 ? 45 : 30;
|
||||
if (!std::isfinite(moveSpeed)) moveSpeed = 2.0f;
|
||||
moveSpeed = std::clamp(moveSpeed, 0.5f, 4.0f);
|
||||
vignette = std::clamp(vignette, 0, 2);
|
||||
seatMarkers = std::clamp(seatMarkers, 0, 1);
|
||||
if (!std::isfinite(volume)) volume = 1.0f;
|
||||
volume = std::clamp(volume, 0.0f, 1.0f);
|
||||
animations = std::clamp(animations, 0, 1);
|
||||
menuRate = std::clamp(menuRate, 0, 1);
|
||||
frameRate = std::clamp(frameRate, 0, 1);
|
||||
tempLimit = tempLimit <= 0 ? 0 : std::clamp(tempLimit, 70, 95);
|
||||
}
|
||||
// What changing from `o` to this needs: true when Frame Home must restart (render targets, samplers and the
|
||||
// render pass are built for these); resolution and foveation strength apply live.
|
||||
bool needsRestart(const PerfSettings& o) const {
|
||||
return msaa != o.msaa || anisotropy != o.anisotropy || (foveation > 0) != (o.foveation > 0);
|
||||
}
|
||||
bool operator==(const PerfSettings& o) const {
|
||||
return autoResolution == o.autoResolution && std::abs(resolution - o.resolution) < 1e-3f && msaa == o.msaa &&
|
||||
anisotropy == o.anisotropy && foveation == o.foveation && foveationLevel == o.foveationLevel && walk == o.walk &&
|
||||
teleport == o.teleport && turn == o.turn && snapAngle == o.snapAngle && std::abs(moveSpeed - o.moveSpeed) < 1e-3f &&
|
||||
vignette == o.vignette && seatMarkers == o.seatMarkers && std::abs(volume - o.volume) < 1e-3f && animations == o.animations &&
|
||||
menuRate == o.menuRate && frameRate == o.frameRate && tempLimit == o.tempLimit;
|
||||
}
|
||||
};
|
||||
|
||||
// Frame Home's debug tools: ~/frame-home/debug.json, beside settings.json. The Environments panel's Debug page and
|
||||
// Frame Studio (Frame > Headset Debug) edit it and Frame Home applies it within a second. They're for finding out
|
||||
// why a home looks or runs wrong, not for keeping: Frame Home clears the file each time it starts.
|
||||
struct DebugSettings {
|
||||
int view = 0; // HslRenderer::DebugView: 0 normal, 1 wireframe, 2 meshes, 3 GPU cost, 4 overdraw
|
||||
int world = 0; // the world layer drawn over the home: walkable area, surfaces, spawns, seats, wall spots
|
||||
int hideSeeThrough = 0, hideCutout = 0, hideSky = 0; // leave those parts of the home out
|
||||
int shot = 0; // asks for a picture of what's shown (logs/frame-<time>.png): the time it was asked
|
||||
// (Unix seconds); one from before Frame Home started is old
|
||||
|
||||
bool any() const { return view || world || hideSeeThrough || hideCutout || hideSky; }
|
||||
std::string toJson() const {
|
||||
char b[256];
|
||||
std::snprintf(b, sizeof b, "{\"view\":%d,\"world\":%d,\"hideSeeThrough\":%d,\"hideCutout\":%d,\"hideSky\":%d,\"shot\":%d}\n",
|
||||
view, world, hideSeeThrough, hideCutout, hideSky, shot);
|
||||
return b;
|
||||
}
|
||||
bool fromJson(const std::string& j) {
|
||||
const size_t last = j.find_last_not_of(" \t\r\n");
|
||||
if (last == std::string::npos || j[last] != '}') return false;
|
||||
try {
|
||||
const tinyjson::Value d = tinyjson::parse(j);
|
||||
if (!d.isObject()) return false;
|
||||
auto get = [&](const char* k, int& v) { if (d.has(k)) v = static_cast<int>(d[k].asFloat()); };
|
||||
get("view", view); get("world", world); get("hideSeeThrough", hideSeeThrough); get("hideCutout", hideCutout);
|
||||
get("hideSky", hideSky); get("shot", shot);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
view = std::clamp(view, 0, 4);
|
||||
for (int* v : {&world, &hideSeeThrough, &hideCutout, &hideSky}) *v = std::clamp(*v, 0, 1);
|
||||
return true;
|
||||
}
|
||||
unsigned hideBits() const { return (hideSeeThrough ? 1u : 0u) | (hideCutout ? 2u : 0u) | (hideSky ? 4u : 0u); } // HslRenderer::DebugHide
|
||||
bool operator==(const DebugSettings& o) const {
|
||||
return view == o.view && world == o.world && hideSeeThrough == o.hideSeeThrough && hideCutout == o.hideCutout && hideSky == o.hideSky &&
|
||||
shot == o.shot;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
// Running copies of a Frame program, for the installers. SteamVR's own bookkeeping (GetApplicationProcessId) loses
|
||||
// track of an app once a utility process started from the same binary has connected, so look at /proc instead.
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Processes named `name` running without arguments: the environment / the panel, not tools (--snapshot, --list).
|
||||
inline std::vector<pid_t> runningCopies(const char* name) {
|
||||
std::vector<pid_t> out;
|
||||
std::error_code ec;
|
||||
for (const auto& e : std::filesystem::directory_iterator("/proc", ec)) {
|
||||
const std::string id = e.path().filename().string();
|
||||
if (id.empty() || id.find_first_not_of("0123456789") != std::string::npos) continue;
|
||||
const pid_t pid = static_cast<pid_t>(std::stol(id));
|
||||
if (pid == getpid()) continue;
|
||||
std::string comm;
|
||||
std::getline(std::ifstream(e.path() / "comm"), comm);
|
||||
if (comm != name) continue;
|
||||
std::ifstream c(e.path() / "cmdline", std::ios::binary);
|
||||
const std::string cmd((std::istreambuf_iterator<char>(c)), std::istreambuf_iterator<char>());
|
||||
size_t args = 0;
|
||||
for (char ch : cmd) args += ch == '\0';
|
||||
if (args <= 1) out.push_back(pid);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Asks them to quit, then insists after 5 s.
|
||||
// Running copies of `name` (other than this process) started with `arg` among their arguments.
|
||||
inline std::vector<pid_t> runningWith(const char* name, const std::string& arg) {
|
||||
std::vector<pid_t> out;
|
||||
std::error_code ec;
|
||||
for (const auto& e : std::filesystem::directory_iterator("/proc", ec)) {
|
||||
const std::string id = e.path().filename().string();
|
||||
if (id.empty() || id.find_first_not_of("0123456789") != std::string::npos) continue;
|
||||
const pid_t pid = static_cast<pid_t>(std::stol(id));
|
||||
if (pid == getpid()) continue;
|
||||
std::string comm;
|
||||
std::getline(std::ifstream(e.path() / "comm"), comm);
|
||||
if (comm != name) continue;
|
||||
std::ifstream c(e.path() / "cmdline", std::ios::binary);
|
||||
for (std::string a; std::getline(c, a, '\0');) if (a == arg) { out.push_back(pid); break; }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline void stopCopies(const char* name) {
|
||||
const std::vector<pid_t> pids = runningCopies(name);
|
||||
for (pid_t p : pids) kill(p, SIGTERM);
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
bool alive = false;
|
||||
for (pid_t p : pids) alive |= kill(p, 0) == 0;
|
||||
if (!alive) return;
|
||||
usleep(100000);
|
||||
}
|
||||
for (pid_t p : pids) kill(p, SIGKILL);
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
#include "scene_bake.h"
|
||||
#include "visibility_bake.h"
|
||||
#include "surface_mesh.h"
|
||||
#include "walkgrid.h"
|
||||
#include "tex_mips.h"
|
||||
#include "tex_cache.h"
|
||||
#include "core/parallel.h"
|
||||
#include "shader_bake.h"
|
||||
#include "bake_io.h"
|
||||
#include "anim_bake.h"
|
||||
#include <atomic>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace {
|
||||
|
||||
using bakeio::Writer;
|
||||
using bakeio::Reader;
|
||||
|
||||
// Shared buffers (one texture chain serves many meshes) are written once, then referenced by index.
|
||||
template <typename T> struct Table {
|
||||
std::map<const T*, u32> index;
|
||||
std::vector<const T*> items;
|
||||
u32 ref(const std::shared_ptr<const T>& p) {
|
||||
if (!p) return 0xFFFFFFFFu;
|
||||
auto it = index.find(p.get());
|
||||
if (it != index.end()) return it->second;
|
||||
index[p.get()] = static_cast<u32>(items.size());
|
||||
items.push_back(p.get());
|
||||
return static_cast<u32>(items.size() - 1);
|
||||
}
|
||||
};
|
||||
|
||||
void writeMesh(Writer& w, const MeshData& m, Table<TexSource>& tex, Table<std::vector<u16>>& half, const std::vector<u32>* indices = nullptr) {
|
||||
w.str(m.name);
|
||||
w.str(m.path);
|
||||
w.u(static_cast<u32>(m.components.size()));
|
||||
for (const EnvComponent& c : m.components) {
|
||||
w.str(c.cls); w.str(c.shortCls); w.pod(c.version);
|
||||
w.u(static_cast<u32>(c.fields.size()));
|
||||
for (const auto& kv : c.fields) { w.str(kv.first); w.str(kv.second); }
|
||||
}
|
||||
w.b(m.isSkybox);
|
||||
w.vec(m.positions); w.vec(m.uvs); w.vec(m.colors); w.vec(m.uvs3); w.vec(m.uvs4); w.vec(indices ? *indices : m.indices);
|
||||
w.u(m.nVerts); w.u(indices ? static_cast<u32>(indices->size()) : m.nIdx);
|
||||
w.vec(m.texRGBA); w.u(m.texW); w.u(m.texH); w.b(m.hasTexture); w.pod(m.texShareSrc);
|
||||
w.vec(m.srcAstc); w.u(m.srcAstcBw); w.u(m.srcAstcBh); w.u(m.srcAstcMips);
|
||||
w.vec(m.normalRGBA); w.u(m.normalW); w.u(m.normalH); w.b(m.hasNormal);
|
||||
w.vec(m.ormRGBA); w.u(m.ormW); w.u(m.ormH); w.b(m.hasOrm);
|
||||
w.str(m.ormTexName);
|
||||
w.vec(m.emissiveRGBA); w.u(m.emissiveW); w.u(m.emissiveH); w.b(m.hasEmissive);
|
||||
w.vec(m.lmRGBA); w.u(m.lmW); w.u(m.lmH); w.b(m.hasLightmap);
|
||||
w.u(half.ref(m.lmHalf)); w.u(tex.ref(m.lmSrc)); w.u(tex.ref(m.lmLdrSrc));
|
||||
w.vec(m.cubeRGBA); w.u(m.cubeSize); w.u(tex.ref(m.cubeSrc));
|
||||
w.u(tex.ref(m.texSrc)); w.u(tex.ref(m.normalSrc)); w.u(tex.ref(m.ormSrc)); w.u(tex.ref(m.emissiveSrc));
|
||||
w.u(static_cast<u32>(m.namedTex.size()));
|
||||
for (const auto& nt : m.namedTex) { w.u(nt.first); w.u(tex.ref(nt.second)); }
|
||||
w.u(static_cast<u32>(m.namedHdr.size()));
|
||||
for (const auto& nh : m.namedHdr) { w.u(nh.first); w.u(half.ref(nh.second.half)); w.u(nh.second.w); w.u(nh.second.h); w.u(tex.ref(nh.second.src)); }
|
||||
w.vec(m.reflCubeHalf); w.u(m.reflCubeSize); w.u(m.reflCubeMips); w.u(tex.ref(m.reflCubeSrc));
|
||||
w.fa(m.lmUvScaleOffset, 4);
|
||||
w.vec(m.vatRaw); w.u(m.vatW); w.u(m.vatH); w.b(m.hasVat);
|
||||
w.fa(m.tint, 4);
|
||||
w.f(m.atlasCellIndex); w.f(m.vatTrackIndex); w.f(m.vatRateFactor); w.f(m.vatTimeOffset);
|
||||
w.u(static_cast<u32>(m.matOverrides.size()));
|
||||
for (const MatOverride& o : m.matOverrides) { w.str(o.name); w.fa(o.v, 4); }
|
||||
w.pod(m.shaderIng); w.str(m.shaderPath); w.str(m.meshPath); w.b(m.tiled);
|
||||
w.vec(m.matParamsBlob); w.vec(m.constParams); w.vec(m.texSlots);
|
||||
w.vec(m.astcRaw); w.u(m.astcBw); w.u(m.astcBh);
|
||||
w.pod(m.transform);
|
||||
w.fa(m.worldMatrix, 16); w.b(m.hasWorldMatrix);
|
||||
w.vec(m.boneIndices); w.vec(m.boneWeights); w.b(m.hasBones); w.vec(m.bonePalette);
|
||||
w.b(m.useBlend); w.b(m.additive); w.b(m.alphaTest); w.f(m.alphaCutoff); w.pod(m.srcAlphaMode); w.f(m.srcFactorA);
|
||||
w.pod(m.overlayKind); w.b(m.doubleSided); w.b(m.dynamicVerts); w.pod(m.gltfMeshIndex);
|
||||
w.b(m.iblLit); w.b(m.iblFullSpec); w.b(m.bakeLightmapVtx);
|
||||
w.vec(m.uvs2); w.b(m.alphaToCoverage); w.vec(m.normals); w.vec(m.tangents);
|
||||
w.fa(m.lightmapPower, 3); w.f(m.metallic); w.f(m.roughness); w.f(m.speciblDiffScale); w.f(m.speciblSpecScale);
|
||||
w.fa(m.albedoFactor, 3); w.fa(m.curTint, 4); w.b(m.animatedTintAlpha);
|
||||
w.fa(m.animXform, 16); w.fa(m.uvXform, 6); w.vec(m.skinBindPos); w.vec(m.skinJoints); w.fa(m.skinBox, 6);
|
||||
}
|
||||
|
||||
void readMesh(Reader& r, MeshData& m, const std::vector<std::shared_ptr<const TexSource>>& tex,
|
||||
const std::vector<std::shared_ptr<const std::vector<u16>>>& half, u32 version) {
|
||||
const bool v3 = version >= 3; // v2 (.fenv saved before HDR maps stayed ASTC) still opens
|
||||
auto texRef = [&](u32 i) { return i < tex.size() ? tex[i] : nullptr; };
|
||||
auto halfRef = [&](u32 i) { return i < half.size() ? half[i] : nullptr; };
|
||||
m.name = r.str();
|
||||
if (version >= 7) m.path = r.str();
|
||||
m.components.resize(r.count(16));
|
||||
for (EnvComponent& c : m.components) {
|
||||
if (r.bad) return;
|
||||
c.cls = r.str(); c.shortCls = r.str(); c.version = r.pod<int>();
|
||||
c.fields.resize(r.count(8));
|
||||
for (auto& kv : c.fields) { if (r.bad) return; kv.first = r.str(); kv.second = r.str(); }
|
||||
}
|
||||
m.isSkybox = r.b();
|
||||
m.positions = r.vec<float>(); m.uvs = r.vec<float>(); m.colors = r.vec<u8>(); m.uvs3 = r.vec<float>(); m.uvs4 = r.vec<float>();
|
||||
m.indices = r.vec<u32>();
|
||||
m.nVerts = r.u(); m.nIdx = r.u();
|
||||
m.texRGBA = r.vec<u8>(); m.texW = r.u(); m.texH = r.u(); m.hasTexture = r.b(); m.texShareSrc = r.pod<int>();
|
||||
m.srcAstc = r.vec<u8>(); m.srcAstcBw = r.u(); m.srcAstcBh = r.u(); m.srcAstcMips = r.u();
|
||||
m.normalRGBA = r.vec<u8>(); m.normalW = r.u(); m.normalH = r.u(); m.hasNormal = r.b();
|
||||
m.ormRGBA = r.vec<u8>(); m.ormW = r.u(); m.ormH = r.u(); m.hasOrm = r.b();
|
||||
m.ormTexName = r.str();
|
||||
m.emissiveRGBA = r.vec<u8>(); m.emissiveW = r.u(); m.emissiveH = r.u(); m.hasEmissive = r.b();
|
||||
m.lmRGBA = r.vec<u8>(); m.lmW = r.u(); m.lmH = r.u(); m.hasLightmap = r.b();
|
||||
m.lmHalf = halfRef(r.u()); if (v3) m.lmSrc = texRef(r.u());
|
||||
if (version >= 9) m.lmLdrSrc = texRef(r.u());
|
||||
m.cubeRGBA = r.vec<u8>(); m.cubeSize = r.u(); if (v3) m.cubeSrc = texRef(r.u());
|
||||
m.texSrc = texRef(r.u()); m.normalSrc = texRef(r.u()); m.ormSrc = texRef(r.u()); m.emissiveSrc = texRef(r.u());
|
||||
m.namedTex.resize(r.count(8));
|
||||
for (auto& nt : m.namedTex) { if (r.bad) return; nt.first = r.u(); nt.second = texRef(r.u()); }
|
||||
m.namedHdr.resize(r.count(v3 ? 20 : 16));
|
||||
for (auto& nh : m.namedHdr) { if (r.bad) return; nh.first = r.u(); nh.second.half = halfRef(r.u()); nh.second.w = r.u(); nh.second.h = r.u(); if (v3) nh.second.src = texRef(r.u()); }
|
||||
m.reflCubeHalf = r.vec<u16>(); m.reflCubeSize = r.u(); m.reflCubeMips = r.u();
|
||||
if (version >= 10) m.reflCubeSrc = texRef(r.u());
|
||||
r.fa(m.lmUvScaleOffset, 4);
|
||||
m.vatRaw = r.vec<u8>(); m.vatW = r.u(); m.vatH = r.u(); m.hasVat = r.b();
|
||||
r.fa(m.tint, 4);
|
||||
m.atlasCellIndex = r.f(); m.vatTrackIndex = r.f(); m.vatRateFactor = r.f(); m.vatTimeOffset = r.f();
|
||||
m.matOverrides.resize(r.count(20));
|
||||
for (MatOverride& o : m.matOverrides) { if (r.bad) return; o.name = r.str(); r.fa(o.v, 4); }
|
||||
m.shaderIng = r.pod<u64>(); m.shaderPath = r.str(); m.meshPath = r.str(); m.tiled = r.b();
|
||||
m.matParamsBlob = r.vec<u8>(); m.constParams = r.vec<ConstParam>(); m.texSlots = r.vec<u32>();
|
||||
m.astcRaw = r.vec<u8>(); m.astcBw = r.u(); m.astcBh = r.u();
|
||||
m.transform = r.pod<Transform>();
|
||||
r.fa(m.worldMatrix, 16); m.hasWorldMatrix = r.b();
|
||||
m.boneIndices = r.vec<u8>(); m.boneWeights = r.vec<u8>(); m.hasBones = r.b(); m.bonePalette = r.vec<u32>();
|
||||
m.useBlend = r.b(); m.additive = r.b(); m.alphaTest = r.b(); m.alphaCutoff = r.f(); m.srcAlphaMode = r.pod<int>(); m.srcFactorA = r.f();
|
||||
m.overlayKind = r.pod<int>(); m.doubleSided = r.b(); m.dynamicVerts = r.b(); m.gltfMeshIndex = r.pod<int>();
|
||||
m.iblLit = r.b(); m.iblFullSpec = r.b(); m.bakeLightmapVtx = r.b();
|
||||
m.uvs2 = r.vec<float>(); m.alphaToCoverage = r.b(); m.normals = r.vec<float>(); m.tangents = r.vec<float>();
|
||||
r.fa(m.lightmapPower, 3); m.metallic = r.f(); m.roughness = r.f(); m.speciblDiffScale = r.f(); m.speciblSpecScale = r.f();
|
||||
r.fa(m.albedoFactor, 3); r.fa(m.curTint, 4); m.animatedTintAlpha = r.b();
|
||||
if (version >= 13) { r.fa(m.animXform, 16); r.fa(m.uvXform, 6); m.skinBindPos = r.vec<float>(); m.skinJoints = r.vec<float>(); r.fa(m.skinBox, 6); }
|
||||
}
|
||||
|
||||
// The world layer in binary (world.json is the editable form): mesh colliders and walkable surfaces are
|
||||
// megabytes of floats, which as text were the largest part of some blobs and slow to parse.
|
||||
void writePoint(Writer& w, const WorldPoint& p) { w.str(p.name); w.fa(p.pos, 3); w.f(p.yaw); w.fa(p.exit, 3); }
|
||||
WorldPoint readPoint(Reader& r) { WorldPoint p; p.name = r.str(); r.fa(p.pos, 3); p.yaw = r.f(); r.fa(p.exit, 3); return p; }
|
||||
|
||||
// Mesh colliders made from a drawn mesh (Meta's, whose physics mesh is the render mesh) are written as a reference
|
||||
// to that mesh instead of a second copy of its vertices.
|
||||
int sameMesh(const WorldCollider& c, const std::vector<MeshData>& meshes) {
|
||||
for (size_t m = 0; m < meshes.size(); ++m) {
|
||||
const MeshData& md = meshes[m];
|
||||
if (md.positions.size() == c.positions.size() && md.indices.size() == c.indices.size() && !c.positions.empty() &&
|
||||
std::memcmp(md.positions.data(), c.positions.data(), c.positions.size() * 4) == 0 &&
|
||||
std::memcmp(md.indices.data(), c.indices.data(), c.indices.size() * 4) == 0)
|
||||
return static_cast<int>(m);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void writeWorld(Writer& w, const HomeWorld& wd, const std::vector<MeshData>& meshes, const std::vector<char>* thinned, const std::string& walkGrid) {
|
||||
w.str(wd.origin);
|
||||
for (const auto* list : {&wd.spawns, &wd.seats, &wd.hotspots}) {
|
||||
w.u(static_cast<u32>(list->size()));
|
||||
for (const WorldPoint& p : *list) writePoint(w, p);
|
||||
}
|
||||
w.u(static_cast<u32>(wd.colliders.size()));
|
||||
for (const WorldCollider& c : wd.colliders) {
|
||||
w.str(c.name); w.pod(c.kind); w.pod(c.role); w.fa(c.world, 16); w.fa(c.half, 3); w.f(c.radius); w.f(c.length);
|
||||
int ref = c.kind == WorldCollider::Mesh ? sameMesh(c, meshes) : -1;
|
||||
if (ref >= 0 && thinned && static_cast<size_t>(ref) < thinned->size() && (*thinned)[ref]) ref = -1; // its mesh lost triangles: its own
|
||||
w.pod(ref);
|
||||
if (ref < 0) { w.vec(c.positions); w.vec(c.indices); }
|
||||
w.str(c.mesh);
|
||||
}
|
||||
w.vec(wd.walkPositions); w.vec(wd.walkIndices);
|
||||
w.u(static_cast<u32>(wd.wallSpots.size()));
|
||||
for (const WorldWallSpot& ws : wd.wallSpots) { w.str(ws.name); w.fa(ws.world, 16); w.f(ws.width); w.f(ws.height); }
|
||||
w.u(static_cast<u32>(wd.gravity.size()));
|
||||
for (const WorldGravity& g : wd.gravity) { w.str(g.name); w.pod(g.kind); w.fa(g.world, 16); w.fa(g.half, 3); w.f(g.radius); w.pod(g.priority); }
|
||||
w.f(wd.fallLimit);
|
||||
w.str(walkGrid);
|
||||
}
|
||||
|
||||
void readWorld(Reader& r, HomeWorld& wd, u32 version, std::vector<std::pair<size_t, int>>& refs) {
|
||||
wd = HomeWorld{};
|
||||
wd.origin = r.str();
|
||||
for (auto* list : {&wd.spawns, &wd.seats, &wd.hotspots})
|
||||
for (u32 n = r.u(); n-- && !r.bad;) list->push_back(readPoint(r));
|
||||
wd.colliders.resize(r.count(93));
|
||||
for (WorldCollider& c : wd.colliders) {
|
||||
if (r.bad) break;
|
||||
c.name = r.str(); c.kind = r.pod<int>();
|
||||
if (version >= 4) c.role = r.pod<int>(); else c.role = r.b() ? WorldCollider::Trigger : WorldCollider::Solid;
|
||||
r.fa(c.world, 16); r.fa(c.half, 3); c.radius = r.f(); c.length = r.f();
|
||||
const int ref = version >= 5 ? r.pod<int>() : -1;
|
||||
if (ref >= 0) refs.emplace_back(&c - wd.colliders.data(), ref); // filled from the mesh once meshes are read
|
||||
else { c.positions = r.vec<float>(); c.indices = r.vec<u32>(); }
|
||||
if (version >= 4) c.mesh = r.str();
|
||||
}
|
||||
wd.walkPositions = r.vec<float>(); wd.walkIndices = r.vec<u32>();
|
||||
wd.wallSpots.resize(r.count(76));
|
||||
for (WorldWallSpot& ws : wd.wallSpots) { if (r.bad) break; ws.name = r.str(); r.fa(ws.world, 16); ws.width = r.f(); ws.height = r.f(); }
|
||||
if (version >= 6) {
|
||||
wd.gravity.resize(r.count(92));
|
||||
for (WorldGravity& g : wd.gravity) { if (r.bad) break; g.name = r.str(); g.kind = r.pod<int>(); r.fa(g.world, 16); r.fa(g.half, 3); g.radius = r.f(); g.priority = r.pod<int>(); }
|
||||
wd.fallLimit = r.f();
|
||||
}
|
||||
if (version >= 11) { // its mesh colliders were simplified by the bake
|
||||
wd.walkGrid = r.str();
|
||||
for (WorldCollider& c : wd.colliders) c.simplified = c.kind == WorldCollider::Mesh;
|
||||
}
|
||||
worldAdoptLegacyWalkable(wd);
|
||||
}
|
||||
|
||||
constexpr u32 kMagic = 0x4E435346; // "FSCN"
|
||||
|
||||
// The block footprints ASTC has (2D): the renderer divides by them and picks its Vulkan format from them.
|
||||
bool astcFootprint(u32 bw, u32 bh) {
|
||||
static const u8 k[][2] = {{4, 4}, {5, 4}, {5, 5}, {6, 5}, {6, 6}, {8, 5}, {8, 6}, {8, 8}, {10, 5}, {10, 6}, {10, 8}, {10, 10}, {12, 10}, {12, 12}};
|
||||
for (const auto& f : k) if (f[0] == bw && f[1] == bh) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// What the readers index by without checking, checked once here: anything a bake can't have written fails the load.
|
||||
bool sane(HslScene& s) {
|
||||
for (MeshData& m : s.meshes) {
|
||||
m.nVerts = static_cast<u32>(std::min<size_t>(m.nVerts, m.positions.size() / 3));
|
||||
m.nIdx = static_cast<u32>(std::min<size_t>(m.nIdx, m.indices.size()));
|
||||
if (m.cubeSize && (m.cubeSrc ? m.cubeSrc->faces != 6 : m.cubeRGBA.size() / 24 / m.cubeSize < m.cubeSize)) return false;
|
||||
if ((!m.skinBindPos.empty() && (m.skinBindPos.size() < static_cast<size_t>(m.nVerts) * 3 || m.boneIndices.size() < static_cast<size_t>(m.nVerts) * 4 ||
|
||||
m.boneWeights.size() < static_cast<size_t>(m.nVerts) * 4)) ||
|
||||
m.skinJoints.size() % 16 || m.skinJoints.size() > 256 * 16)
|
||||
return false;
|
||||
}
|
||||
for (const HslAnimGroup& g : s.anims) {
|
||||
const size_t joints = g.skel.joints.size(), keys = static_cast<size_t>(std::max(0, g.clip.nFrames)) * std::max(0, g.clip.nJoints);
|
||||
if (g.clip.nFrames < 0 || g.clip.nJoints < 0) return false;
|
||||
// empty is fine (sampled as the bind pose); a partial array is damage
|
||||
if ((!g.skel.invBind.empty() && g.skel.invBind.size() < joints * 16) || (!g.clip.quats.empty() && g.clip.quats.size() / 4 < keys))
|
||||
return false;
|
||||
}
|
||||
s.meshSource.resize(s.meshes.size(), 0); // per mesh, always (Studio indexes it by mesh)
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool bakeScene(const HslScene& scene, std::vector<u8>& out, std::string& error, const BakeOptions* options) {
|
||||
// A Clay / glTF home's animation goes in as data (anim_bake.h), and the meshes it writes as a fresh load has them
|
||||
// (the animation at t = 0): Frame Studio plays it, so they may be anywhere in it now. When that moved vertices, the
|
||||
// bake works from a copy of the scene with them back (the walkable area and the colliders read the vertices).
|
||||
std::vector<u8> anim;
|
||||
std::vector<std::pair<size_t, MeshData>> posed;
|
||||
if (scene.saveAnimation) {
|
||||
scene.saveAnimation(anim);
|
||||
if (!animbake::startPose(anim, scene.meshes, posed)) { error = "this home's animation couldn't be stored"; return false; }
|
||||
} else if (scene.animate) {
|
||||
error = "this home's animation can't be stored";
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<HslScene> atStart;
|
||||
for (const auto& [m, md] : posed)
|
||||
if (md.positions.size() != scene.meshes[m].positions.size() ||
|
||||
std::memcmp(md.positions.data(), scene.meshes[m].positions.data(), md.positions.size() * sizeof(float))) {
|
||||
atStart = std::make_unique<HslScene>(scene);
|
||||
for (auto& [i, pmd] : posed) atStart->meshes[i] = std::move(pmd);
|
||||
posed.clear();
|
||||
break;
|
||||
}
|
||||
const HslScene& s = atStart ? *atStart : scene;
|
||||
std::vector<const MeshData*> meshes(s.meshes.size());
|
||||
for (size_t i = 0; i < meshes.size(); ++i) meshes[i] = &s.meshes[i];
|
||||
for (const auto& [m, md] : posed) meshes[m] = &md;
|
||||
const VisibilityResult* vis = options ? options->visibility : nullptr;
|
||||
if (vis && vis->changed.size() != s.meshes.size()) vis = nullptr;
|
||||
out.clear();
|
||||
// Meshes first into their own buffer: that fills the shared-texture tables, which go ahead of them.
|
||||
std::vector<u8> meshBytes;
|
||||
Writer mw{meshBytes};
|
||||
Table<TexSource> tex;
|
||||
Table<std::vector<u16>> half;
|
||||
for (size_t i = 0; i < s.meshes.size(); ++i) writeMesh(mw, *meshes[i], tex, half, vis && vis->changed[i] ? &vis->indices[i] : nullptr);
|
||||
|
||||
Writer w{out};
|
||||
w.u(kMagic); w.u(kSceneBakeVersion);
|
||||
w.u(static_cast<u32>(s.apkPaths.size()));
|
||||
for (size_t i = 0; i < s.apkPaths.size(); ++i) {
|
||||
w.str(s.apkPaths[i]);
|
||||
const HslSourceInfo info = i < s.sources.size() ? s.sources[i] : HslSourceInfo{};
|
||||
w.str(info.package); w.str(info.version); w.str(info.format); w.str(info.role);
|
||||
}
|
||||
w.vec(s.meshSource);
|
||||
w.fa(s.ambientRGB, 3); w.b(s.hasEnvAmbient);
|
||||
{ // the world's fog: a global the HSTF parser sets while loading (renderer reads it per frame)
|
||||
std::lock_guard<std::mutex> l(g_sceneFogMutex);
|
||||
w.b(g_sceneFog.has); w.fa(g_sceneFog.color, 4); w.f(g_sceneFog.start); w.f(g_sceneFog.density); w.b(g_sceneFog.travel);
|
||||
}
|
||||
w.f(s.animDuration); w.vec(s.animatedMeshes);
|
||||
w.vec(s.ambientOgg); w.str(s.ambientSource); w.str(s.ambientName);
|
||||
{ // the mesh colliders as every load simplifies them (simplifyWorldMeshes): done here once, not at each load
|
||||
HomeWorld world = s.world;
|
||||
simplifyWorldMeshes(world);
|
||||
// The walkable area goes in baked: the optimizer's, else Frame Studio's (as edited there), else the one Frame
|
||||
// Home would bake at load (the same bake, with its defaults), so the headset never bakes one (0.3-0.5 s).
|
||||
std::string walkGrid = options && !options->walkGrid.empty() ? options->walkGrid : s.world.walkGrid;
|
||||
if (walkGrid.empty())
|
||||
if (const std::shared_ptr<const WalkGrid> g = bakeWalkGrid(s, world, WalkParams{})) walkGrid = encodeWalkGrid(*g);
|
||||
writeWorld(w, world, s.meshes, vis ? &vis->changed : nullptr, walkGrid);
|
||||
}
|
||||
// The shaders as the Frame's GPU runs them (shader_bake.h): each program's prepared forms, and per distinct set
|
||||
// of material values its meshes have, the fragment shader with them baked in.
|
||||
std::map<std::string, ShaderProgramSrc> programs = s.programs;
|
||||
const auto ts = std::chrono::steady_clock::now();
|
||||
shaderbake::bakePrograms(s.meshes, [&](const MeshData& md) -> std::string {
|
||||
const ShaderProgramSrc* src = findProgram(s, md.shaderPath);
|
||||
if (!src && md.isSkybox) // (as the renderer: the shell's skybox shader ships in renderer_module)
|
||||
for (const auto& kv : s.programs)
|
||||
if (shaderbake::contains(shaderbake::lc(kv.first), "skybox")) { src = &kv.second; break; }
|
||||
for (const auto& kv : s.programs) if (&kv.second == src) return kv.first;
|
||||
return std::string();
|
||||
}, s.ambientRGB, s.hasEnvAmbient, programs);
|
||||
{
|
||||
size_t specialized = 0;
|
||||
for (const auto& kv : programs) for (const auto& v : kv.second.variants) specialized += !v.frag.empty();
|
||||
std::fprintf(stderr, "[bake] shaders: %zu programs prepared, %zu specialized for their materials in %.0f ms\n", programs.size(), specialized,
|
||||
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - ts).count());
|
||||
}
|
||||
w.u(static_cast<u32>(programs.size()));
|
||||
for (const auto& kv : programs) {
|
||||
const ShaderProgramSrc& p = kv.second;
|
||||
w.str(kv.first); w.str(p.path); w.vec(p.vert); w.vec(p.frag);
|
||||
w.b(p.prepared); w.vec(p.vertInvariant); w.vec(p.fragAlpha);
|
||||
w.u(static_cast<u32>(p.variants.size()));
|
||||
for (const auto& v : p.variants) { w.pod(v.key); w.vec(v.frag); w.vec(v.fragAlpha); }
|
||||
}
|
||||
w.u(static_cast<u32>(s.anims.size()));
|
||||
for (const HslAnimGroup& g : s.anims) {
|
||||
w.u(static_cast<u32>(g.skel.joints.size()));
|
||||
for (const RendJoint& j : g.skel.joints) { w.fa(j.pos, 3); w.fa(j.quat, 4); w.f(j.scale); w.pod(j.parent); w.str(j.name); }
|
||||
w.vec(g.skel.bind); w.vec(g.skel.invBind);
|
||||
w.pod(g.clip.nJoints); w.pod(g.clip.nFrames); w.f(g.clip.fps); w.vec(g.clip.quats); w.vec(g.clip.trans);
|
||||
w.vec(g.clip.acl ? hzAclBytes(g.clip.acl) : std::vector<uint8_t>());
|
||||
w.pod(g.clip.aclJoints);
|
||||
w.vec(g.meshes);
|
||||
}
|
||||
w.vec(anim);
|
||||
w.u(static_cast<u32>(tex.items.size()));
|
||||
// Textures shipped without smaller levels get their chain here (tex_mips.h): sampled natively on the Frame at full
|
||||
// size instead of being shrunk and rebuilt at every load. FH_NO_BAKE_MIPS=1 keeps them as shipped.
|
||||
static const bool bakeMips = !std::getenv("FH_NO_BAKE_MIPS");
|
||||
const auto tm0 = std::chrono::steady_clock::now();
|
||||
size_t mipped = 0, cached = 0;
|
||||
// Each chain from the texture cache when an earlier open or save built it (tex_cache.h), else made here and kept
|
||||
// there: a save of the same home again (and every auto-save) makes none. The ones made run a texture per core.
|
||||
std::vector<std::shared_ptr<TexSource>> chains(tex.items.size());
|
||||
std::atomic<size_t> fromCache{0};
|
||||
if (bakeMips)
|
||||
par::forEach(tex.items.size(), [&](size_t i) {
|
||||
const TexSource& t = *tex.items[i];
|
||||
if (!texmips::wants(t)) return;
|
||||
if ((chains[i] = texcache::load(t))) { ++fromCache; return; }
|
||||
if ((chains[i] = texmips::withMips(t, 1))) texcache::store(*chains[i], texcache::key(t));
|
||||
});
|
||||
cached = fromCache;
|
||||
for (size_t i = 0; i < tex.items.size(); ++i) {
|
||||
const TexSource* t = tex.items[i];
|
||||
if (chains[i]) { t = chains[i].get(); ++mipped; }
|
||||
w.vec(t->astc); w.u(t->w); w.u(t->h); w.u(t->bw); w.u(t->bh); w.u(t->mips); w.b(t->hdr); w.u(t->faces);
|
||||
}
|
||||
if (mipped)
|
||||
std::fprintf(stderr, "[bake] gave %zu textures their smaller levels (%zu from the cache) in %.1f s\n", mipped, cached,
|
||||
std::chrono::duration<double>(std::chrono::steady_clock::now() - tm0).count());
|
||||
w.u(static_cast<u32>(half.items.size()));
|
||||
for (const std::vector<u16>* h : half.items) w.vec(*h);
|
||||
w.u(static_cast<u32>(s.meshes.size()));
|
||||
out.insert(out.end(), meshBytes.begin(), meshBytes.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool optimizeForBake(const HslScene& scene, const std::vector<ObjectEdit>* edits, VisibilityResult& vis, BakeOptions& options) {
|
||||
vis = VisibilityResult();
|
||||
options = BakeOptions();
|
||||
if (std::getenv("FH_NO_VIS_CULL")) { vis.skipped = "turned off (FH_NO_VIS_CULL)"; return false; }
|
||||
if (scene.animate && !scene.saveAnimation) { vis.skipped = "the home's animation can't be stored (it isn't baked)"; return false; }
|
||||
// Everything a CPU animation writes stays whole (and hides nothing): its motion isn't in what's seen from where.
|
||||
VisibilityOptions vo;
|
||||
if (scene.saveAnimation) {
|
||||
std::vector<u8> anim;
|
||||
scene.saveAnimation(anim);
|
||||
vo.pinned = animbake::targets(anim);
|
||||
}
|
||||
const HomeWorld w = walkWorld(scene);
|
||||
if (!bakeVisibility(scene, w, w.walkGridCache.get(), edits, vis, vo)) {
|
||||
std::fprintf(stderr, "[optimize] nothing left out: %s\n", vis.skipped.c_str());
|
||||
return false;
|
||||
}
|
||||
options.visibility = &vis;
|
||||
options.walkGrid = encodeWalkGrid(*w.walkGridCache);
|
||||
if (std::getenv("FH_VIS_DRYRUN")) { // diagnostics: everything as optimized (the walkable area stored) but no triangle left out
|
||||
vis.changed.assign(vis.changed.size(), 0);
|
||||
vis.trisAfter = vis.trisBefore;
|
||||
}
|
||||
std::fprintf(stderr, "[optimize] from %zu eye spots: %zu of %zu triangles left out (%.1f%%), %zu meshes emptied, %zu thinned, %.1f s\n",
|
||||
vis.samples, vis.trisBefore - vis.trisAfter, vis.trisBefore, vis.trisBefore ? 100.0 * (vis.trisBefore - vis.trisAfter) / vis.trisBefore : 0.0,
|
||||
vis.meshesEmptied, vis.meshesTouched, vis.ms / 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unbakeScene(const u8* data, size_t size, HslScene& s, std::string& error) {
|
||||
Reader r{data, data + size};
|
||||
if (r.u() != kMagic) { error = "not a baked scene"; return false; }
|
||||
const u32 version = r.u();
|
||||
if (version < 2 || version > kSceneBakeVersion) { error = "baked scene from a different Frame Studio version"; return false; }
|
||||
s = HslScene{};
|
||||
s.apkPaths.resize(r.count(20));
|
||||
for (size_t i = 0; i < s.apkPaths.size() && !r.bad; ++i) {
|
||||
s.apkPaths[i] = r.str();
|
||||
HslSourceInfo info;
|
||||
info.package = r.str(); info.version = r.str(); info.format = r.str(); info.role = r.str();
|
||||
s.sources.push_back(info);
|
||||
}
|
||||
s.meshSource = r.vec<int>();
|
||||
r.fa(s.ambientRGB, 3); s.hasEnvAmbient = r.b();
|
||||
SceneFog fog;
|
||||
fog.has = r.b(); r.fa(fog.color, 4); fog.start = r.f(); fog.density = r.f(); fog.travel = r.b();
|
||||
s.animDuration = r.f(); s.animatedMeshes = r.vec<int>();
|
||||
s.ambientOgg = r.vec<u8>(); s.ambientSource = r.str();
|
||||
if (version >= 8) s.ambientName = r.str();
|
||||
std::vector<std::pair<size_t, int>> meshRefs;
|
||||
if (version >= 3) readWorld(r, s.world, version, meshRefs); else worldFromJson(r.str(), s.world);
|
||||
for (u32 n = r.u(); n-- && !r.bad;) {
|
||||
const std::string key = r.str();
|
||||
ShaderProgramSrc p;
|
||||
p.path = r.str(); p.vert = r.vec<u32>(); p.frag = r.vec<u32>();
|
||||
if (version >= 12) {
|
||||
p.prepared = r.b(); p.vertInvariant = r.vec<u32>(); p.fragAlpha = r.vec<u32>();
|
||||
p.variants.resize(r.count(20));
|
||||
for (auto& v : p.variants) { if (r.bad) break; v.key = r.pod<uint64_t>(); v.frag = r.vec<u32>(); v.fragAlpha = r.vec<u32>(); }
|
||||
if (p.prepared && p.vertInvariant.empty()) p.prepared = false; // (a damaged file: made at load instead)
|
||||
}
|
||||
s.programs[key] = std::move(p);
|
||||
}
|
||||
s.anims.resize(r.count(44));
|
||||
for (HslAnimGroup& g : s.anims) {
|
||||
if (r.bad) break;
|
||||
g.skel.joints.resize(r.count(40));
|
||||
for (RendJoint& j : g.skel.joints) { if (r.bad) break; r.fa(j.pos, 3); r.fa(j.quat, 4); j.scale = r.f(); j.parent = r.pod<int>(); j.name = r.str(); }
|
||||
g.skel.bind = r.vec<float>(); g.skel.invBind = r.vec<float>();
|
||||
g.clip.nJoints = r.pod<int>(); g.clip.nFrames = r.pod<int>(); g.clip.fps = r.f(); g.clip.quats = r.vec<float>(); g.clip.trans = r.vec<float>();
|
||||
const std::vector<u8> acl = r.vec<u8>();
|
||||
g.clip.acl = acl.empty() ? nullptr : hzAclCreate(acl.data(), acl.size()); // owned for the scene's life, as when parsed
|
||||
g.clip.aclJoints = r.pod<int>();
|
||||
g.meshes = r.vec<int>();
|
||||
}
|
||||
const std::vector<u8> anim = version >= 13 ? r.vec<u8>() : std::vector<u8>();
|
||||
std::vector<std::shared_ptr<const TexSource>> tex(r.count(version >= 3 ? 29 : 25));
|
||||
for (auto& t : tex) {
|
||||
if (r.bad) break;
|
||||
auto ts = std::make_shared<TexSource>();
|
||||
ts->astc = r.vec<u8>(); ts->w = r.u(); ts->h = r.u(); ts->bw = r.u(); ts->bh = r.u(); ts->mips = r.u(); ts->hdr = r.b(); if (version >= 3) ts->faces = r.u();
|
||||
if (!astcFootprint(ts->bw, ts->bh) || !ts->w || !ts->h || ts->w > 16384 || ts->h > 16384 || (ts->faces != 1 && ts->faces != 6) ||
|
||||
(ts->faces == 6 && ts->w != ts->h)) { r.bad = true; break; }
|
||||
u32 chain = 1; // a mip count past the image's own chain: the levels it can have
|
||||
while ((std::max(ts->w, ts->h) >> chain) != 0) ++chain;
|
||||
ts->mips = std::min(std::max(ts->mips, 1u), chain);
|
||||
t = ts;
|
||||
}
|
||||
std::vector<std::shared_ptr<const std::vector<u16>>> half(r.count(4));
|
||||
for (auto& h : half) { if (r.bad) break; h = std::make_shared<std::vector<u16>>(r.vec<u16>()); }
|
||||
s.meshes.resize(r.count(64));
|
||||
for (MeshData& m : s.meshes) { if (r.bad) break; readMesh(r, m, tex, half, version); }
|
||||
for (const auto& [ci, mi] : meshRefs) {
|
||||
if (ci >= s.world.colliders.size() || mi >= static_cast<int>(s.meshes.size())) { r.bad = true; break; }
|
||||
s.world.colliders[ci].positions = s.meshes[mi].positions;
|
||||
s.world.colliders[ci].indices = s.meshes[mi].indices;
|
||||
}
|
||||
if (!r.bad && !anim.empty() && !animbake::load(anim, s.meshes.size(), s)) r.bad = true; // (it plays the meshes as read)
|
||||
if (r.bad || !sane(s)) { error = "baked scene is truncated or damaged"; s = HslScene{}; return false; }
|
||||
std::lock_guard<std::mutex> l(g_sceneFogMutex);
|
||||
g_sceneFog = fog;
|
||||
g_deferTextures = true;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
// A loaded home, baked: everything HslScene holds after the slow part of loading (APK unzip, HSTF / RENDMESH /
|
||||
// RENDSHAD parsing, SMOL-V decode, lightmap HDR decode, override resolution) written as one binary blob, so a
|
||||
// .fenv opens in a fraction of the time. Textures (HDR lightmaps too) stay as their cooked ASTC chains (the Frame's GPU samples them
|
||||
// directly), so the blob is GPU-ready and about the size of the home's own data.
|
||||
//
|
||||
// Clay (.opa / .ovrscene) and glTF homes animate on the CPU: their animation goes in as data too (anim_bake.h), played by
|
||||
// the loader's own code from it.
|
||||
#include "hsl_scene.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
constexpr u32 kSceneBakeVersion = 13; // 2: world; 3: HDR maps as ASTC, binary world; 4: roles; 5: mesh refs; 6: gravity; 7: mesh paths; 8: ambience name; 9: compressed 8-bit lightmaps; 10: reflection cube as ASTC; 11: walkable area (and simplified colliders); 12: prepared and specialized shaders; 13: Clay / glTF CPU animation
|
||||
|
||||
struct VisibilityResult;
|
||||
struct ObjectEdit;
|
||||
|
||||
// What a bake leaves out or adds beyond the scene itself: the visibility pass's index lists (visibility_bake.h) and
|
||||
// the walkable area to carry (the scene's own when empty).
|
||||
struct BakeOptions {
|
||||
const VisibilityResult* visibility = nullptr;
|
||||
std::string walkGrid;
|
||||
};
|
||||
|
||||
// False (with `error`) when the scene can't be baked (an animation that can't be stored).
|
||||
bool bakeScene(const HslScene& scene, std::vector<u8>& out, std::string& error, const BakeOptions* options = nullptr);
|
||||
|
||||
// The mesh optimizer for a save: the visibility pass over the scene's walkable area (Frame Studio's object edits kept
|
||||
// whole), its result in `vis` and the options for bakeScene in `options` (the walkable area it was worked out from
|
||||
// goes with it: Frame Home must not bake one from the thinned meshes). False when it didn't run (`vis.skipped`).
|
||||
// FH_NO_VIS_CULL=1: never.
|
||||
bool optimizeForBake(const HslScene& scene, const std::vector<ObjectEdit>* edits, VisibilityResult& vis, BakeOptions& options);
|
||||
bool unbakeScene(const u8* data, size_t size, HslScene& scene, std::string& error);
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
// GENERATED by tools/gen_clay_shader.py --selection: the selection-highlight overlay shaders. Do not edit by hand.
|
||||
// vert: in vec3 position @0, vec2 uv @1; set0 b0 uniform { mat4 mvp } frag: set0 b1 sampler2D tex -> rgb, a=1
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
static const uint32_t kSelectionVertSpirv[] = {
|
||||
0x07230203,0x00010000,0x00000000,0x00000022,0x00000000,0x00020011,0x00000001,0x0003000e,
|
||||
0x00000000,0x00000001,0x0009000f,0x00000000,0x00000001,0x6e69616d,0x00000000,0x00000014,
|
||||
0x00000015,0x00000016,0x00000017,0x00040005,0x00000001,0x6e69616d,0x00000000,0x00050005,
|
||||
0x00000013,0x656c6573,0x6f697463,0x0000006e,0x00050005,0x00000014,0x69736f70,0x6e6f6974,
|
||||
0x00000000,0x00030005,0x00000015,0x00007675,0x00030005,0x00000016,0x00565576,0x00040006,
|
||||
0x0000000a,0x00000000,0x0070766d,0x00040047,0x00000014,0x0000001e,0x00000000,0x00040047,
|
||||
0x00000015,0x0000001e,0x00000001,0x00040047,0x00000016,0x0000001e,0x00000000,0x00040047,
|
||||
0x00000017,0x0000000b,0x00000000,0x00050048,0x0000000a,0x00000000,0x00000023,0x00000000,
|
||||
0x00040048,0x0000000a,0x00000000,0x00000005,0x00050048,0x0000000a,0x00000000,0x00000007,
|
||||
0x00000010,0x00030047,0x0000000a,0x00000002,0x00040047,0x00000013,0x00000022,0x00000000,
|
||||
0x00040047,0x00000013,0x00000021,0x00000000,0x00020013,0x00000002,0x00030021,0x00000003,
|
||||
0x00000002,0x00030016,0x00000004,0x00000020,0x00040015,0x00000005,0x00000020,0x00000001,
|
||||
0x00040017,0x00000006,0x00000004,0x00000002,0x00040017,0x00000007,0x00000004,0x00000003,
|
||||
0x00040017,0x00000008,0x00000004,0x00000004,0x00040018,0x00000009,0x00000008,0x00000004,
|
||||
0x0003001e,0x0000000a,0x00000009,0x00040020,0x0000000b,0x00000002,0x0000000a,0x00040020,
|
||||
0x0000000c,0x00000002,0x00000009,0x00040020,0x0000000d,0x00000001,0x00000007,0x00040020,
|
||||
0x0000000e,0x00000001,0x00000006,0x00040020,0x0000000f,0x00000003,0x00000006,0x00040020,
|
||||
0x00000010,0x00000003,0x00000008,0x0004002b,0x00000004,0x00000011,0x3f800000,0x0004002b,
|
||||
0x00000005,0x00000012,0x00000000,0x0004003b,0x0000000b,0x00000013,0x00000002,0x0004003b,
|
||||
0x0000000d,0x00000014,0x00000001,0x0004003b,0x0000000e,0x00000015,0x00000001,0x0004003b,
|
||||
0x0000000f,0x00000016,0x00000003,0x0004003b,0x00000010,0x00000017,0x00000003,0x00050036,
|
||||
0x00000002,0x00000001,0x00000000,0x00000003,0x000200f8,0x00000018,0x00050041,0x0000000c,
|
||||
0x00000019,0x00000013,0x00000012,0x0004003d,0x00000009,0x0000001a,0x00000019,0x0004003d,
|
||||
0x00000007,0x0000001b,0x00000014,0x00050051,0x00000004,0x0000001c,0x0000001b,0x00000000,
|
||||
0x00050051,0x00000004,0x0000001d,0x0000001b,0x00000001,0x00050051,0x00000004,0x0000001e,
|
||||
0x0000001b,0x00000002,0x00070050,0x00000008,0x0000001f,0x0000001c,0x0000001d,0x0000001e,
|
||||
0x00000011,0x00050091,0x00000008,0x00000020,0x0000001a,0x0000001f,0x0003003e,0x00000017,
|
||||
0x00000020,0x0004003d,0x00000006,0x00000021,0x00000015,0x0003003e,0x00000016,0x00000021,
|
||||
0x000100fd,0x00010038,
|
||||
};
|
||||
static const size_t kSelectionVertSize = sizeof(kSelectionVertSpirv);
|
||||
|
||||
static const uint32_t kSelectionFragSpirv[] = {
|
||||
0x07230203,0x00010000,0x00000000,0x00000018,0x00000000,0x00020011,0x00000001,0x0003000e,
|
||||
0x00000000,0x00000001,0x0007000f,0x00000004,0x00000001,0x6e69616d,0x00000000,0x0000000d,
|
||||
0x0000000e,0x00030010,0x00000001,0x00000007,0x00040005,0x00000001,0x6e69616d,0x00000000,
|
||||
0x00030005,0x0000000c,0x00786574,0x00030005,0x0000000d,0x00565576,0x00050005,0x0000000e,
|
||||
0x4374756f,0x726f6c6f,0x00000000,0x00040047,0x0000000d,0x0000001e,0x00000000,0x00040047,
|
||||
0x0000000e,0x0000001e,0x00000000,0x00040047,0x0000000c,0x00000022,0x00000000,0x00040047,
|
||||
0x0000000c,0x00000021,0x00000001,0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,
|
||||
0x00030016,0x00000004,0x00000020,0x00040017,0x00000005,0x00000004,0x00000002,0x00040017,
|
||||
0x00000006,0x00000004,0x00000004,0x00090019,0x00000007,0x00000004,0x00000001,0x00000000,
|
||||
0x00000000,0x00000000,0x00000001,0x00000000,0x0003001b,0x00000008,0x00000007,0x00040020,
|
||||
0x00000009,0x00000000,0x00000008,0x00040020,0x0000000a,0x00000001,0x00000005,0x00040020,
|
||||
0x0000000b,0x00000003,0x00000006,0x0004002b,0x00000004,0x0000000f,0x3f800000,0x0004003b,
|
||||
0x00000009,0x0000000c,0x00000000,0x0004003b,0x0000000a,0x0000000d,0x00000001,0x0004003b,
|
||||
0x0000000b,0x0000000e,0x00000003,0x00050036,0x00000002,0x00000001,0x00000000,0x00000003,
|
||||
0x000200f8,0x00000010,0x0004003d,0x00000008,0x00000011,0x0000000c,0x0004003d,0x00000005,
|
||||
0x00000012,0x0000000d,0x00050057,0x00000006,0x00000013,0x00000011,0x00000012,0x00050051,
|
||||
0x00000004,0x00000014,0x00000013,0x00000000,0x00050051,0x00000004,0x00000015,0x00000013,
|
||||
0x00000001,0x00050051,0x00000004,0x00000016,0x00000013,0x00000002,0x00070050,0x00000006,
|
||||
0x00000017,0x00000014,0x00000015,0x00000016,0x0000000f,0x0003003e,0x0000000e,0x00000017,
|
||||
0x000100fd,0x00010038,
|
||||
};
|
||||
static const size_t kSelectionFragSize = sizeof(kSelectionFragSpirv);
|
||||
@@ -0,0 +1,408 @@
|
||||
#pragma once
|
||||
// The shaders as the Frame's GPU runs them, made once: Frame Studio (scene_bake) stores the results in the .fenv, and
|
||||
// the renderer makes the same here for a home opened straight from its APK. The renderer checks a baked result
|
||||
// against what it would make (the key of the uniform values it bakes in) and makes its own when they differ, so a
|
||||
// .fenv baked by an older build never draws wrong.
|
||||
// - invariantPosition: the vertex shader with its position Invariant (a cut-out's depth pass and its shading pass
|
||||
// compute every position bit for bit alike)
|
||||
// - alphaOnly: the fragment shader writing only its alpha (the cut-out depth pass)
|
||||
// - specialize: per material, the uniform values that never change baked in as constants (spv_specialize.h)
|
||||
// The set-2 (material) uniform buffers are filled here too, by the same code for both, so the bytes a baked shader
|
||||
// assumes are the bytes the renderer uploads.
|
||||
#include "core/types.h"
|
||||
#include "hsl_scene.h"
|
||||
#include "spv_reflect.h"
|
||||
#include "spv_specialize.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace shaderbake {
|
||||
|
||||
inline std::string lc(std::string s) {
|
||||
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return s;
|
||||
}
|
||||
inline bool contains(const std::string& s, const char* sub) { return s.find(sub) != std::string::npos; }
|
||||
|
||||
// The vertex shader with its position marked Invariant. (An OpMemberDecorate / OpDecorate ... Invariant after the
|
||||
// BuiltIn Position one.)
|
||||
inline std::vector<u32> invariantPosition(const std::vector<u32>& code) {
|
||||
if (code.size() < 5 || code[0] != 0x07230203u) return code;
|
||||
std::vector<u32> out(code.begin(), code.begin() + 5);
|
||||
out.reserve(code.size() + 4);
|
||||
for (size_t i = 5; i < code.size();) {
|
||||
const u32 words = code[i] >> 16, op = code[i] & 0xFFFF;
|
||||
if (!words || i + words > code.size()) return code;
|
||||
out.insert(out.end(), code.begin() + i, code.begin() + i + words);
|
||||
if (op == 72 && words == 5 && code[i + 3] == 11 && code[i + 4] == 0) // OpMemberDecorate s m BuiltIn Position
|
||||
out.insert(out.end(), {(4u << 16) | 72u, code[i + 1], code[i + 2], 18u});
|
||||
else if (op == 71 && words == 4 && code[i + 2] == 11 && code[i + 3] == 0) // OpDecorate v BuiltIn Position
|
||||
out.insert(out.end(), {(3u << 16) | 71u, code[i + 1], 18u});
|
||||
i += words;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// The fragment shader made to write only its colour's alpha: every whole-vec4 store to the Location 0 output becomes
|
||||
// (a,a,a,a) of the same value. Discards and everything the alpha depends on stay as they are, so alpha-to-coverage
|
||||
// picks the same samples; the lighting the colour needed no longer reaches an output and the driver drops it. Empty
|
||||
// when the output is written any other way (a component at a time).
|
||||
inline std::vector<u32> alphaOnly(const std::vector<u32>& code) {
|
||||
if (code.size() < 5 || code[0] != 0x07230203u) return {};
|
||||
u32 outVar = 0, bound = code[3];
|
||||
std::map<u32, u32> vec4Of; // OpTypeVector id (4 floats) -> its float type id
|
||||
std::map<u32, u32> ptrPointee; // Output pointer type -> pointee
|
||||
std::map<u32, u32> varType; // variable -> its pointer type
|
||||
std::set<u32> loc0;
|
||||
for (size_t i = 5; i < code.size();) {
|
||||
const u32 words = code[i] >> 16, op = code[i] & 0xFFFF;
|
||||
if (!words || i + words > code.size()) return {};
|
||||
if (op == 71 && words == 4 && code[i + 2] == 30 && code[i + 3] == 0) loc0.insert(code[i + 1]); // Location 0
|
||||
if (op == 23 && words == 4 && code[i + 3] == 4) vec4Of[code[i + 1]] = code[i + 2]; // OpTypeVector
|
||||
if (op == 32 && words == 4 && code[i + 2] == 3) ptrPointee[code[i + 1]] = code[i + 3]; // OpTypePointer Output
|
||||
if (op == 59 && words >= 4 && code[i + 3] == 3 && loc0.count(code[i + 2])) { outVar = code[i + 2]; varType[outVar] = code[i + 1]; }
|
||||
i += words;
|
||||
}
|
||||
if (!outVar || !ptrPointee.count(varType[outVar]) || !vec4Of.count(ptrPointee[varType[outVar]])) return {};
|
||||
const u32 vecT = ptrPointee[varType[outVar]], floatT = vec4Of[vecT];
|
||||
std::vector<u32> out(code.begin(), code.begin() + 5);
|
||||
for (size_t i = 5; i < code.size();) {
|
||||
const u32 words = code[i] >> 16, op = code[i] & 0xFFFF;
|
||||
if ((op == 65 || op == 66) && words >= 4 && code[i + 3] == outVar) return {}; // OpAccessChain into it
|
||||
if (op == 62 && words >= 3 && code[i + 1] == outVar) { // OpStore out, v
|
||||
const u32 a = bound++, v = bound++;
|
||||
out.insert(out.end(), {(5u << 16) | 81u, floatT, a, code[i + 2], 3u}); // a = OpCompositeExtract v[3]
|
||||
out.insert(out.end(), {(7u << 16) | 80u, vecT, v, a, a, a, a}); // (a,a,a,a)
|
||||
out.insert(out.end(), {(3u << 16) | 62u, outVar, v});
|
||||
i += words;
|
||||
continue;
|
||||
}
|
||||
out.insert(out.end(), code.begin() + i, code.begin() + i + words);
|
||||
i += words;
|
||||
}
|
||||
out[3] = bound;
|
||||
return out;
|
||||
}
|
||||
|
||||
// A program's descriptor sets as the renderer lays them out: the union of both stages, the larger declaration of a
|
||||
// block winning, sorted by binding.
|
||||
inline void mergedSets(const spv::Module& vr, const spv::Module& fr, std::vector<spv::Resource> sets[3]) {
|
||||
for (const spv::Module* m : {&vr, &fr}) {
|
||||
for (const auto& r : m->resources) {
|
||||
if (r.set > 2) continue;
|
||||
auto& v = sets[r.set];
|
||||
auto f = std::find_if(v.begin(), v.end(), [&](const spv::Resource& x) { return x.binding == r.binding; });
|
||||
if (f == v.end()) v.push_back(r);
|
||||
else if (f->size < r.size) { f->size = r.size; f->members = r.members; }
|
||||
}
|
||||
}
|
||||
for (int s = 0; s < 3; ++s)
|
||||
std::sort(sets[s].begin(), sets[s].end(), [](const spv::Resource& a, const spv::Resource& b) { return a.binding < b.binding; });
|
||||
}
|
||||
|
||||
// Whether the vertex stage reads bone indices (its skinning palette may be an unnamed block).
|
||||
inline bool boned(const spv::Module& vr) {
|
||||
for (const auto& in : vr.inputs) {
|
||||
const std::string n = lc(in.name);
|
||||
if (contains(n, "boneind") || contains(n, "joint") || contains(n, "indices")) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A per-material uniform block, as HSR Studio supplies it: zero, then the material's cooked constants bound by
|
||||
// MurmurHash3("<block>.<member>") (matParams, lightprobesParams.L00 and lightBakerParams are all authored this way),
|
||||
// then the entity's per-instance overrides. Returns which members the material authored.
|
||||
inline std::vector<bool> fillBlock(const spv::Resource& r, const MeshData& md, float* f) {
|
||||
const size_t cap = std::max<size_t>(r.size, 16);
|
||||
u8* bytes = reinterpret_cast<u8*>(f);
|
||||
std::memset(bytes, 0, cap);
|
||||
std::vector<bool> authored(r.members.size(), false);
|
||||
const std::string block = r.name.empty() ? std::string("matParams") : r.name;
|
||||
for (size_t mi = 0; mi < r.members.size(); ++mi) {
|
||||
const auto& m = r.members[mi];
|
||||
const std::string full = block + "." + m.name;
|
||||
const u32 want = murmur3_x86_32(full.data(), full.size(), 0);
|
||||
for (const auto& cp : md.constParams) {
|
||||
if (cp.nameHash != want) continue;
|
||||
if (static_cast<size_t>(cp.blobOffset) + cp.byteSize > md.matParamsBlob.size() || m.offset + cp.byteSize > cap) break;
|
||||
std::memcpy(bytes + m.offset, md.matParamsBlob.data() + cp.blobOffset, cp.byteSize);
|
||||
authored[mi] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const std::string prefix = block + ".";
|
||||
for (const auto& ov : md.matOverrides) {
|
||||
if (ov.name.rfind(prefix, 0) != 0) continue;
|
||||
const std::string member = ov.name.substr(prefix.size());
|
||||
for (size_t mi = 0; mi < r.members.size(); ++mi) {
|
||||
const auto& m = r.members[mi];
|
||||
if (m.name != member || m.offset >= cap) continue;
|
||||
const size_t n = std::min<size_t>(std::min<u32>(m.size ? m.size : 4, 16), cap - m.offset);
|
||||
if (m.integer) { // overrides arrive as floats: store them as the integers the shader reads
|
||||
for (size_t k = 0; k * 4 < n; ++k) { const int32_t iv = static_cast<int32_t>(ov.v[k]); std::memcpy(bytes + m.offset + k * 4, &iv, 4); }
|
||||
} else {
|
||||
std::memcpy(bytes + m.offset, ov.v, n);
|
||||
}
|
||||
authored[mi] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// HSR's one non-zero default: a dielectric's Fresnel F0 at n = 1.5.
|
||||
for (size_t mi = 0; mi < r.members.size(); ++mi)
|
||||
if (!authored[mi] && r.members[mi].name == "incidentReflectivityF0" && r.members[mi].offset + 4 <= cap) {
|
||||
const float f0 = 0.04f;
|
||||
std::memcpy(bytes + r.members[mi].offset, &f0, 4);
|
||||
}
|
||||
return authored;
|
||||
}
|
||||
|
||||
// matParams, as HSR Studio fills it: zero (a value the material does not author is zero, never a guess), then the
|
||||
// material's cooked constants bound by MurmurHash3("matParams."+member), then the entity's per-instance overrides.
|
||||
inline void fillMatParams(const spv::Resource& r, const MeshData& md, const std::string& programPath, float* f) {
|
||||
fillBlock(r, md, f);
|
||||
// Desert Terrace 210's palms (palmtreefoliage) light from the standard lightmap, not their custom HDR one: with it
|
||||
// (useCustomLightmap 1, as authored) every trunk lit a flat bright yellow, nothing like the Clay twin's dark,
|
||||
// side-lit trunks; with the standard lightmap trunks and fronds match it (checked side by side, 2026-10-20).
|
||||
if (programPath.find("palmtreefoliage") != std::string::npos)
|
||||
for (const auto& m : r.members)
|
||||
if (m.name == "useCustomLightmap" && m.offset + 4 <= std::max<size_t>(r.size, 16)) { const int32_t zero = 0; std::memcpy(reinterpret_cast<u8*>(f) + m.offset, &zero, 4); }
|
||||
// FH_MATSET=member=value,...: diagnostics, a material constant overridden for every mesh that has it (ints too).
|
||||
if (const char* e = std::getenv("FH_MATSET")) {
|
||||
std::string rest = e;
|
||||
while (!rest.empty()) {
|
||||
const size_t c = rest.find(',');
|
||||
const std::string kv = rest.substr(0, c);
|
||||
rest = c == std::string::npos ? "" : rest.substr(c + 1);
|
||||
const size_t eq = kv.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
for (const auto& m : r.members)
|
||||
if (m.name == kv.substr(0, eq) && m.offset + 4 <= std::max<size_t>(r.size, 16)) {
|
||||
if (m.name.rfind("use", 0) == 0 || m.name.find("Skip") != std::string::npos) {
|
||||
const int32_t v = std::atoi(kv.c_str() + eq + 1);
|
||||
std::memcpy(reinterpret_cast<u8*>(f) + m.offset, &v, 4);
|
||||
} else {
|
||||
f[m.offset / 4] = static_cast<float>(std::atof(kv.c_str() + eq + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Set2 { Material, Bones, Probe, Baker, Instance, Other };
|
||||
|
||||
// What a set-2 buffer holds, routed by name. The bone palette: named in most shaders; vista's ibl/ibltint declare it
|
||||
// as an unnamed runtime array (left zero, every vertex collapsed to the origin and the whole architecture vanished).
|
||||
inline Set2 set2Kind(const spv::Resource& r, bool bonedProgram) {
|
||||
const std::string n = lc(r.name);
|
||||
if (contains(n, "skinning") || contains(n, "sbskin") || contains(n, "bonemat") || (bonedProgram && r.name.empty() && r.size == 0 && r.binding != 0))
|
||||
return Set2::Bones;
|
||||
if (contains(n, "matparam") || (n.empty() && r.binding == 0)) return Set2::Material;
|
||||
if (contains(n, "lightprobe")) return Set2::Probe;
|
||||
if (contains(n, "lightbaker")) return Set2::Baker;
|
||||
if (contains(n, "instance")) return Set2::Instance;
|
||||
return Set2::Other;
|
||||
}
|
||||
|
||||
// A set-2 buffer's contents (`f` holds max(r.size, 16) bytes at least), but for the bone palette (the renderer's).
|
||||
// ambient: the scene's ambient light as the renderer uses it.
|
||||
inline void fillSet2(const spv::Resource& r, Set2 kind, const MeshData& md, const std::string& programPath, const float ambient[3], float* f) {
|
||||
std::memset(f, 0, std::max<size_t>(r.size, 16));
|
||||
if (kind == Set2::Material) {
|
||||
fillMatParams(r, md, programPath, f);
|
||||
} else if (kind == Set2::Probe) {
|
||||
// L00 is authored by the material (HSR: "matl"); the higher SH bands are runtime probe data: zero.
|
||||
const std::vector<bool> got = fillBlock(r, md, f);
|
||||
if (std::find(got.begin(), got.end(), true) == got.end()) {
|
||||
for (int k = 0; k < 3; ++k) f[k] = ambient[k] * 3.5449f;
|
||||
f[3] = 1;
|
||||
}
|
||||
} else if (kind == Set2::Baker) {
|
||||
// {uvScale, uvOffset}, used as-is (no V-flip: HSR samples the lightmap with the raw UV2). The entity's
|
||||
// lightingParams atlas patch outranks the material's: materials author the neutral (1,1)/(0,0), and with it
|
||||
// every Theater wall sampled the whole shared 2048 atlas.
|
||||
const std::vector<bool> got = fillBlock(r, md, f);
|
||||
static const float kNeutral[4] = {1, 1, 0, 0};
|
||||
if (std::find(got.begin(), got.end(), true) == got.end() || std::memcmp(md.lmUvScaleOffset, kNeutral, 16) != 0)
|
||||
std::memcpy(f, md.lmUvScaleOffset, 16);
|
||||
if (const char* e = std::getenv("FH_LBSET")) // diagnostics: every lightmap's uvScale.xy, uvOffset.xy
|
||||
std::sscanf(e, "%f,%f,%f,%f", &f[0], &f[1], &f[2], &f[3]);
|
||||
} else if (kind == Set2::Instance) {
|
||||
f[0] = md.atlasCellIndex >= 0 ? md.atlasCellIndex : 0;
|
||||
f[1] = md.vatTrackIndex >= 0 ? md.vatTrackIndex : 0;
|
||||
f[2] = md.vatRateFactor >= 0 ? md.vatRateFactor : 1;
|
||||
f[3] = md.vatTimeOffset >= 0 ? md.vatTimeOffset : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// The bytes of the scene-wide uniform buffers that never change while the scene shows, as every program's shaders
|
||||
// see them: the frame globals (set 0, binding 0: zero but for the white point, and reverseZ with FH_REVERSEZ; the
|
||||
// matrices, camera, time, viewport and fog change and are never folded) and lightUniforms (set 1: zero but for the
|
||||
// five members HSR fills with 1, for every program's layout of it).
|
||||
struct SceneConstants {
|
||||
std::vector<u8> globals, globalsFoldable, light;
|
||||
};
|
||||
|
||||
// globalsSize / lightSize: the renderer's buffers. The globals' member list is the largest any program declares.
|
||||
inline SceneConstants sceneConstants(const std::map<std::string, ShaderProgramSrc>& programs, uint32_t globalsSize, uint32_t lightSize) {
|
||||
SceneConstants sc;
|
||||
sc.globals.assign(globalsSize, 0);
|
||||
sc.globalsFoldable.assign(globalsSize, 1);
|
||||
sc.light.assign(lightSize, 0);
|
||||
std::vector<spv::Member> globalMembers;
|
||||
for (const auto& kv : programs) {
|
||||
spv::Module vr, fr;
|
||||
if (!spv::reflect(kv.second.vert, vr) || !spv::reflect(kv.second.frag, fr)) continue;
|
||||
std::vector<spv::Resource> sets[3];
|
||||
mergedSets(vr, fr, sets);
|
||||
for (const auto& r : sets[0])
|
||||
if (r.binding == 0 && r.kind == spv::ResKind::UniformBuffer && r.members.size() > globalMembers.size()) globalMembers = r.members;
|
||||
for (const auto& r : sets[1]) {
|
||||
const std::string n = lc(r.name);
|
||||
if (r.kind != spv::ResKind::UniformBuffer || contains(n, "item") || contains(n, "grid")) continue;
|
||||
for (const auto& m : r.members) {
|
||||
const std::string mn = lc(m.name);
|
||||
// HSR's derived lightUniforms: these five are 1 (byte 112-156, 168-172), all else zero.
|
||||
const bool one = mn == "iblreflectiontint" || mn == "ibldiffusetint" || mn == "externalaverageluminance" ||
|
||||
mn == "iblreflectionmaxadaptationfactor" || mn == "iblreflectionluminanceinverse";
|
||||
if (!one || m.integer || m.offset + m.size > lightSize) continue;
|
||||
const float v = 1.0f;
|
||||
for (uint32_t k = 0; k * 4 < m.size; ++k) std::memcpy(&sc.light[m.offset + k * 4], &v, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
static const bool revZ = std::getenv("FH_REVERSEZ") != nullptr;
|
||||
for (const auto& m : globalMembers) {
|
||||
const std::string n = lc(m.name);
|
||||
const bool perFrame = contains(n, "clipfromworld") || contains(n, "viewfromworld") || contains(n, "worldcamerapos") || n == "fogcolor" ||
|
||||
n == "fogstart" || n == "fogdensity" || n == "time" || n == "worldtime" || contains(n, "viewportsize");
|
||||
if (perFrame || m.offset + m.size > globalsSize) {
|
||||
for (uint32_t k = m.offset; k < std::min(m.offset + m.size, globalsSize); ++k) sc.globalsFoldable[k] = 0;
|
||||
continue;
|
||||
}
|
||||
const float one = 1;
|
||||
if (n == "whitepoint" || (n == "reversez" && revZ)) std::memcpy(&sc.globals[m.offset], &one, 4);
|
||||
}
|
||||
return sc;
|
||||
}
|
||||
|
||||
// The fetch for spvspec::specialize: the scene's constant images for sets 0 and 1, the mesh's buffers for set 2.
|
||||
// set2: binding -> bytes (the bone palette left out).
|
||||
inline spvspec::Fetch fetchFor(const SceneConstants& sc, const std::vector<spv::Resource> sets[3],
|
||||
const std::map<uint32_t, std::vector<u8>>& set2) {
|
||||
return [&sc, sets, &set2](uint32_t set, uint32_t binding, spvspec::BlockBytes& out) -> bool {
|
||||
const spv::Resource* res = nullptr;
|
||||
for (const auto& r : sets[std::min<uint32_t>(set, 2)]) if (r.binding == binding) res = &r;
|
||||
if (!res || res->kind != spv::ResKind::UniformBuffer) return false;
|
||||
if (set == 0) {
|
||||
out.data = sc.globals.data();
|
||||
out.size = static_cast<uint32_t>(sc.globals.size());
|
||||
const std::vector<u8>* mask = &sc.globalsFoldable;
|
||||
out.foldable = [mask](uint32_t o, uint32_t n) {
|
||||
for (uint32_t k = o; k < o + n; ++k) if (k >= mask->size() || !(*mask)[k]) return false;
|
||||
return true;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
if (set == 1) { // lightUniforms (light items and the grid are storage buffers)
|
||||
const std::string n = lc(res->name);
|
||||
if (contains(n, "item") || contains(n, "grid")) return false;
|
||||
out.data = sc.light.data();
|
||||
out.size = static_cast<uint32_t>(sc.light.size());
|
||||
return true;
|
||||
}
|
||||
const auto it = set2.find(binding);
|
||||
if (it == set2.end()) return false;
|
||||
out.data = it->second.data();
|
||||
out.size = static_cast<uint32_t>(it->second.size());
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// Which float values specialize bakes in: 0 none, 1 zeros, 2 zeros and +-1 (the default; FH_SPEC_FLOATS).
|
||||
inline int floatsMode() {
|
||||
static const int m = std::getenv("FH_SPEC_FLOATS") ? std::atoi(std::getenv("FH_SPEC_FLOATS")) : 2;
|
||||
return m;
|
||||
}
|
||||
|
||||
// The key of a mesh's specialization: every byte of the blocks its fragment shader reads constant members of (in
|
||||
// blocksRead's order), and the float mode. The same in the bake and the renderer for the same values.
|
||||
inline uint64_t variantKey(const std::vector<std::pair<uint32_t, uint32_t>>& blocks, const spvspec::Fetch& fetch, int floats) {
|
||||
uint64_t key = 1469598103934665603ull;
|
||||
auto mix = [&](const u8* d, size_t n) { for (size_t k = 0; k < n; ++k) key = (key ^ d[k]) * 0x100000001B3ull; };
|
||||
const uint32_t mode = static_cast<uint32_t>(floats);
|
||||
mix(reinterpret_cast<const u8*>(&mode), 4);
|
||||
for (const auto& sb : blocks) {
|
||||
spvspec::BlockBytes bb;
|
||||
const uint32_t tag[3] = {sb.first, sb.second, fetch(sb.first, sb.second, bb) ? bb.size : 0};
|
||||
mix(reinterpret_cast<const u8*>(tag), sizeof tag);
|
||||
if (bb.data) mix(bb.data, bb.size);
|
||||
if (bb.foldable) // (the globals' mask is part of what was baked in)
|
||||
for (uint32_t k = 0; k < bb.size; ++k) { const u8 f = bb.foldable(k, 1) ? 1 : 0; mix(&f, 1); }
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
// Frame Studio's half: every program's prepared shaders and, per distinct set of material values its meshes have,
|
||||
// the specialized fragment shader (and its alpha-only form), stored in the programs for the .fenv.
|
||||
// programOf: the key in `programs` of the program the renderer draws a mesh with (empty: none).
|
||||
inline void bakePrograms(const std::vector<MeshData>& meshes, const std::function<std::string(const MeshData&)>& programOf,
|
||||
const float sceneAmbient[3], bool envAmbient, std::map<std::string, ShaderProgramSrc>& programs,
|
||||
uint32_t globalsSize = 1024, uint32_t lightSize = 2048) {
|
||||
const SceneConstants sc = sceneConstants(programs, globalsSize, lightSize);
|
||||
float ambient[3];
|
||||
for (int k = 0; k < 3; ++k) ambient[k] = sceneAmbient[k] * (envAmbient ? 1.0f : 1.8f);
|
||||
struct Info { std::vector<spv::Resource> sets[3]; bool boned = false; std::vector<std::pair<uint32_t, uint32_t>> blocks; bool ok = false; };
|
||||
std::map<std::string, Info> info;
|
||||
for (auto& kv : programs) {
|
||||
ShaderProgramSrc& p = kv.second;
|
||||
p.vertInvariant = invariantPosition(p.vert);
|
||||
p.fragAlpha = alphaOnly(p.frag);
|
||||
p.variants.clear();
|
||||
p.prepared = true;
|
||||
Info& in = info[kv.first];
|
||||
spv::Module vr, fr;
|
||||
if (!spv::reflect(p.vert, vr) || !spv::reflect(p.frag, fr)) continue;
|
||||
mergedSets(vr, fr, in.sets);
|
||||
in.boned = boned(vr);
|
||||
in.blocks = spvspec::blocksRead(p.frag);
|
||||
in.ok = true;
|
||||
}
|
||||
const int floats = floatsMode();
|
||||
for (const MeshData& md : meshes) {
|
||||
const auto pi = programs.find(programOf(md));
|
||||
if (pi == programs.end()) continue;
|
||||
const Info& in = info[pi->first];
|
||||
if (!in.ok || in.blocks.empty()) continue;
|
||||
std::map<uint32_t, std::vector<u8>> set2;
|
||||
for (const auto& r : in.sets[2]) {
|
||||
if (r.kind != spv::ResKind::UniformBuffer || !r.size) continue;
|
||||
const Set2 kind = set2Kind(r, in.boned);
|
||||
if (kind == Set2::Bones) continue;
|
||||
std::vector<float> f((std::max<size_t>(r.size, 16) + 3) / 4, 0.0f);
|
||||
fillSet2(r, kind, md, pi->second.path, ambient, f.data());
|
||||
set2[r.binding].assign(reinterpret_cast<const u8*>(f.data()), reinterpret_cast<const u8*>(f.data()) + r.size);
|
||||
}
|
||||
const spvspec::Fetch fetch = fetchFor(sc, in.sets, set2);
|
||||
const uint64_t key = variantKey(in.blocks, fetch, floats);
|
||||
ShaderProgramSrc& p = pi->second;
|
||||
if (std::any_of(p.variants.begin(), p.variants.end(), [&](const ShaderProgramSrc::Variant& v) { return v.key == key; })) continue;
|
||||
uint32_t folded = 0;
|
||||
ShaderProgramSrc::Variant v;
|
||||
v.key = key;
|
||||
v.frag = spvspec::specialize(p.frag, fetch, &folded, -1, floats);
|
||||
if (!folded) v.frag.clear(); // (nothing to bake in: the shader as shipped)
|
||||
else v.fragAlpha = alphaOnly(v.frag);
|
||||
p.variants.push_back(std::move(v));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace shaderbake
|
||||
@@ -0,0 +1,236 @@
|
||||
#pragma once
|
||||
// Minimal SPIR-V reflection for Meta HSL shaders: vertex inputs, descriptor resources (by set/binding,
|
||||
// with names and uniform-block member offsets), push-constant block, image dimensionality and the
|
||||
// MultiView capability. Names come from OpName/OpMemberName — present in v206 shaders and restored for
|
||||
// stripped v210 ones by spirv_rehydrate.h before reflection.
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace spv {
|
||||
|
||||
enum class ResKind { UniformBuffer, StorageBuffer, Sampler, SampledImage, CombinedImageSampler, StorageImage };
|
||||
enum class ImageDim { D2, D3, Cube };
|
||||
|
||||
struct Member { std::string name; uint32_t offset = 0, size = 0; bool integer = false; }; // integer: int/uint/bool scalar, vector or array
|
||||
|
||||
struct Resource {
|
||||
ResKind kind = ResKind::UniformBuffer;
|
||||
uint32_t set = 0, binding = 0, count = 1;
|
||||
std::string name; // variable name, else block type name
|
||||
uint32_t size = 0; // buffers: byte size of the block (0 for runtime arrays)
|
||||
std::vector<Member> members; // buffers: top-level members
|
||||
ImageDim dim = ImageDim::D2; // images
|
||||
bool depthImage = false;
|
||||
};
|
||||
|
||||
struct Input { uint32_t location = 0; std::string name; uint32_t components = 1; bool integer = false; uint32_t width = 32; };
|
||||
|
||||
struct Module {
|
||||
uint32_t stage = 0xFFFFFFFFu; // SPIR-V execution model: 0 vertex, 4 fragment
|
||||
bool multiview = false;
|
||||
std::vector<Input> inputs; // vertex stage only
|
||||
std::vector<Resource> resources;
|
||||
uint32_t pushSize = 0;
|
||||
std::vector<Member> pushMembers; // flattened one level: "pushConstants.worldFromModel"
|
||||
};
|
||||
|
||||
inline std::string readString(const uint32_t* w, size_t n) {
|
||||
std::string s(reinterpret_cast<const char*>(w), n * 4);
|
||||
return s.substr(0, s.find('\0'));
|
||||
}
|
||||
|
||||
inline bool reflect(const std::vector<uint32_t>& code, Module& out) {
|
||||
if (code.size() < 5 || code[0] != 0x07230203u) return false;
|
||||
struct Type { uint32_t op = 0; std::vector<uint32_t> a; };
|
||||
std::map<uint32_t, Type> types;
|
||||
std::map<uint32_t, std::string> names;
|
||||
std::map<std::pair<uint32_t, uint32_t>, std::string> memberNames;
|
||||
std::map<uint32_t, std::map<uint32_t, uint32_t>> deco; // id -> decoration -> value
|
||||
std::map<std::pair<uint32_t, uint32_t>, std::map<uint32_t, uint32_t>> mdeco; // (struct, member) -> deco -> value
|
||||
std::map<uint32_t, uint32_t> constants; // id -> 32-bit value
|
||||
struct Var { uint32_t id, ptrType, storage; };
|
||||
std::vector<Var> vars;
|
||||
|
||||
for (size_t i = 5; i < code.size();) {
|
||||
uint32_t op = code[i] & 0xFFFF, wc = code[i] >> 16;
|
||||
if (wc == 0 || i + wc > code.size()) return false;
|
||||
const uint32_t* a = &code[i + 1];
|
||||
uint32_t n = wc - 1;
|
||||
switch (op) {
|
||||
case 17: if (n >= 1 && a[0] == 4439) out.multiview = true; break; // OpCapability MultiView
|
||||
case 15: if (out.stage == 0xFFFFFFFFu && n >= 1) out.stage = a[0]; break; // OpEntryPoint
|
||||
case 5: if (n >= 2) names[a[0]] = readString(a + 1, n - 1); break; // OpName
|
||||
case 6: if (n >= 3) memberNames[{a[0], a[1]}] = readString(a + 2, n - 2); break;
|
||||
case 71: if (n >= 2) deco[a[0]][a[1]] = n >= 3 ? a[2] : 1; break; // OpDecorate
|
||||
case 72: if (n >= 3) mdeco[{a[0], a[1]}][a[2]] = n >= 4 ? a[3] : 1; break; // OpMemberDecorate
|
||||
case 43: if (n >= 3) constants[a[1]] = a[2]; break; // OpConstant
|
||||
case 59: if (n >= 3) vars.push_back({a[1], a[0], a[2]}); break; // OpVariable
|
||||
default:
|
||||
if ((op >= 19 && op <= 32) || op == 27) { // OpType*
|
||||
if (n >= 1) { Type t; t.op = op; t.a.assign(a + 1, a + n); types[a[0]] = t; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
i += wc;
|
||||
}
|
||||
|
||||
// Malformed modules can make types self-referential (recursion forever) or a DAG of wide structs (exponential
|
||||
// walk): cap both the nesting depth and the total work. Real HSL types nest a handful of levels.
|
||||
constexpr int kMaxTypeDepth = 32;
|
||||
size_t typeBudget = 1u << 20;
|
||||
// Byte size of a type as laid out in a buffer block (uses Offset/ArrayStride/MatrixStride decorations).
|
||||
std::function<uint32_t(uint32_t, uint32_t, int)> sizeOf;
|
||||
sizeOf = [&](uint32_t id, uint32_t matrixStride, int depth) -> uint32_t {
|
||||
if (depth > kMaxTypeDepth || !typeBudget) return 0;
|
||||
--typeBudget;
|
||||
auto it = types.find(id);
|
||||
if (it == types.end()) return 0;
|
||||
const Type& t = it->second;
|
||||
switch (t.op) {
|
||||
case 21: case 22: return t.a.empty() ? 4 : t.a[0] / 8; // int / float
|
||||
case 20: return 4; // bool
|
||||
case 23: return t.a.size() >= 2 ? sizeOf(t.a[0], 0, depth + 1) * t.a[1] : 0; // vector
|
||||
case 24: { // matrix (columns)
|
||||
if (t.a.empty()) return 0;
|
||||
uint32_t cols = t.a.size() >= 2 ? t.a[1] : 4;
|
||||
return matrixStride ? matrixStride * cols : sizeOf(t.a[0], 0, depth + 1) * cols;
|
||||
}
|
||||
case 28: { // array
|
||||
if (t.a.empty()) return 0;
|
||||
uint32_t len = t.a.size() >= 2 && constants.count(t.a[1]) ? constants[t.a[1]] : 1;
|
||||
auto d = deco.find(id);
|
||||
uint32_t stride = d != deco.end() && d->second.count(6) ? d->second.at(6) : sizeOf(t.a[0], matrixStride, depth + 1);
|
||||
return stride * len;
|
||||
}
|
||||
case 29: return 0; // runtime array
|
||||
case 30: { // struct
|
||||
uint32_t end = 0;
|
||||
for (uint32_t m = 0; m < t.a.size(); ++m) {
|
||||
auto md = mdeco.find({id, m});
|
||||
uint32_t off = md != mdeco.end() && md->second.count(35) ? md->second.at(35) : 0;
|
||||
uint32_t ms = md != mdeco.end() && md->second.count(7) ? md->second.at(7) : 0;
|
||||
end = std::max(end, off + sizeOf(t.a[m], ms, depth + 1));
|
||||
}
|
||||
return end;
|
||||
}
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
// int / uint / bool at the bottom of a vector / matrix / array type
|
||||
std::function<bool(uint32_t, int)> isInteger = [&](uint32_t id, int depth) -> bool {
|
||||
if (depth > kMaxTypeDepth) return false;
|
||||
auto it = types.find(id);
|
||||
if (it == types.end()) return false;
|
||||
const Type& t = it->second;
|
||||
if (t.op == 20 || t.op == 21) return true;
|
||||
if ((t.op == 23 || t.op == 24 || t.op == 28 || t.op == 29) && !t.a.empty()) return isInteger(t.a[0], depth + 1);
|
||||
return false;
|
||||
};
|
||||
auto membersOf = [&](uint32_t structId) {
|
||||
std::vector<Member> ms;
|
||||
auto it = types.find(structId);
|
||||
if (it == types.end() || it->second.op != 30) return ms;
|
||||
for (uint32_t m = 0; m < it->second.a.size(); ++m) {
|
||||
auto md = mdeco.find({structId, m});
|
||||
Member mm;
|
||||
mm.offset = md != mdeco.end() && md->second.count(35) ? md->second.at(35) : 0;
|
||||
uint32_t mstride = md != mdeco.end() && md->second.count(7) ? md->second.at(7) : 0;
|
||||
mm.size = sizeOf(it->second.a[m], mstride, 0);
|
||||
mm.integer = isInteger(it->second.a[m], 0);
|
||||
auto nm = memberNames.find({structId, m});
|
||||
mm.name = nm != memberNames.end() ? nm->second : std::string();
|
||||
ms.push_back(mm);
|
||||
}
|
||||
return ms;
|
||||
};
|
||||
auto pointee = [&](uint32_t ptrType) -> uint32_t {
|
||||
auto it = types.find(ptrType);
|
||||
return it != types.end() && it->second.op == 32 && it->second.a.size() >= 2 ? it->second.a[1] : 0;
|
||||
};
|
||||
|
||||
for (const Var& v : vars) {
|
||||
uint32_t ty = pointee(v.ptrType);
|
||||
auto dv = deco.find(v.id);
|
||||
const bool builtin = dv != deco.end() && dv->second.count(11);
|
||||
if (v.storage == 1 /*Input*/ && out.stage == 0 && !builtin) {
|
||||
if (dv == deco.end() || !dv->second.count(30)) continue;
|
||||
Input in;
|
||||
in.location = dv->second.at(30);
|
||||
in.name = names.count(v.id) ? names[v.id] : std::string();
|
||||
auto t = types.find(ty);
|
||||
uint32_t scalar = ty;
|
||||
if (t != types.end() && t->second.op == 23 && t->second.a.size() >= 2) { in.components = t->second.a[1]; scalar = t->second.a[0]; }
|
||||
auto st = types.find(scalar);
|
||||
if (st != types.end()) { in.integer = st->second.op == 21; in.width = st->second.a.empty() ? 32 : st->second.a[0]; }
|
||||
out.inputs.push_back(in);
|
||||
continue;
|
||||
}
|
||||
if (v.storage == 9 /*PushConstant*/) {
|
||||
out.pushSize = std::max(out.pushSize, sizeOf(ty, 0, 0));
|
||||
for (const Member& m : membersOf(ty)) {
|
||||
// One level of nesting: pushConstants { worldFromModel, color }.
|
||||
auto mt = types.find(ty);
|
||||
uint32_t idx = 0;
|
||||
for (; idx < mt->second.a.size(); ++idx) {
|
||||
auto md = mdeco.find({ty, idx});
|
||||
if ((md != mdeco.end() && md->second.count(35) ? md->second.at(35) : 0) == m.offset) break;
|
||||
}
|
||||
uint32_t inner = idx < mt->second.a.size() ? mt->second.a[idx] : 0;
|
||||
auto it = types.find(inner);
|
||||
if (it != types.end() && it->second.op == 30) {
|
||||
for (Member sm : membersOf(inner)) { sm.offset += m.offset; sm.name = m.name + "." + sm.name; out.pushMembers.push_back(sm); }
|
||||
} else {
|
||||
out.pushMembers.push_back(m);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (v.storage != 0 && v.storage != 2 && v.storage != 12) continue;
|
||||
if (dv == deco.end() || !dv->second.count(34) || !dv->second.count(33)) continue;
|
||||
Resource r;
|
||||
r.set = dv->second.at(34);
|
||||
r.binding = dv->second.at(33);
|
||||
r.name = names.count(v.id) ? names[v.id] : std::string();
|
||||
uint32_t base = ty;
|
||||
auto bt = types.find(base);
|
||||
if (bt != types.end() && (bt->second.op == 28 || bt->second.op == 29) && !bt->second.a.empty()) {
|
||||
if (bt->second.op == 28 && bt->second.a.size() >= 2 && constants.count(bt->second.a[1])) r.count = constants[bt->second.a[1]];
|
||||
base = bt->second.a[0];
|
||||
bt = types.find(base);
|
||||
}
|
||||
if (bt == types.end()) continue;
|
||||
switch (bt->second.op) {
|
||||
case 30: {
|
||||
auto db = deco.find(base);
|
||||
bool bufferBlock = db != deco.end() && db->second.count(3);
|
||||
r.kind = (v.storage == 12 || bufferBlock) ? ResKind::StorageBuffer : ResKind::UniformBuffer;
|
||||
r.size = sizeOf(base, 0, 0);
|
||||
r.members = membersOf(base);
|
||||
if (r.name.empty() && names.count(base)) r.name = names[base];
|
||||
break;
|
||||
}
|
||||
case 26: r.kind = ResKind::Sampler; break;
|
||||
case 25: case 27: {
|
||||
if (bt->second.op == 27 && bt->second.a.empty()) continue;
|
||||
uint32_t img = bt->second.op == 27 ? bt->second.a[0] : base;
|
||||
auto it = types.find(img);
|
||||
if (it == types.end() || it->second.a.size() < 6) continue;
|
||||
uint32_t dim = it->second.a[1];
|
||||
r.dim = dim == 3 ? ImageDim::Cube : dim == 2 ? ImageDim::D3 : ImageDim::D2;
|
||||
r.depthImage = it->second.a[2] == 1;
|
||||
r.kind = bt->second.op == 27 ? ResKind::CombinedImageSampler
|
||||
: it->second.a[5] == 2 ? ResKind::StorageImage : ResKind::SampledImage;
|
||||
break;
|
||||
}
|
||||
default: continue;
|
||||
}
|
||||
out.resources.push_back(r);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace spv
|
||||
@@ -0,0 +1,267 @@
|
||||
#pragma once
|
||||
// A fragment shader with the uniform values that never change for one material baked in as constants: an OpLoad of a
|
||||
// uniform block member reached through constant indices (matParams flags and layers, lightUniforms.iblReflectionTint,
|
||||
// globals.whitePoint ...) whose value is integer, or floats of exactly 0, 1 or -1, becomes an OpCopyObject of an
|
||||
// OpConstant / OpConstantComposite holding the exact bytes the buffer holds. The bindings and layouts stay as they were (the buffers are still bound, just no longer read there),
|
||||
// and the driver's compiler folds the constants and drops what they make dead (a mix() by 0, a multiply by 1, a
|
||||
// branch on a flag). The same bits reach the same instructions: the picture doesn't change.
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace spvspec {
|
||||
|
||||
// What a block's bytes are: the data and which of them may be folded (others are left as loads).
|
||||
struct BlockBytes {
|
||||
const uint8_t* data = nullptr;
|
||||
uint32_t size = 0;
|
||||
std::function<bool(uint32_t offset, uint32_t size)> foldable; // empty: all of it
|
||||
};
|
||||
using Fetch = std::function<bool(uint32_t set, uint32_t binding, BlockBytes& out)>;
|
||||
|
||||
namespace detail {
|
||||
enum : uint32_t {
|
||||
OpDecorate = 71, OpMemberDecorate = 72, OpTypeBool = 20, OpTypeInt = 21, OpTypeFloat = 22, OpTypeVector = 23,
|
||||
OpTypeMatrix = 24, OpTypeArray = 28, OpTypeStruct = 30, OpTypePointer = 32, OpConstant = 43, OpConstantComposite = 44,
|
||||
OpFunction = 54, OpVariable = 59, OpLoad = 61, OpAccessChain = 65, OpInBoundsAccessChain = 66, OpCopyObject = 83,
|
||||
DecBlock = 2, DecRowMajor = 4, DecArrayStride = 6, DecMatrixStride = 7, DecBinding = 33, DecDescriptorSet = 34, DecOffset = 35,
|
||||
StorageUniform = 2,
|
||||
};
|
||||
struct Type { uint32_t op = 0; std::vector<uint32_t> a; };
|
||||
struct Member { uint32_t offset = 0xFFFFFFFFu, matrixStride = 0; bool rowMajor = false; };
|
||||
struct Ptr { uint32_t set = 0, binding = 0, offset = 0, type = 0, matrixStride = 0; bool rowMajor = false; };
|
||||
|
||||
struct Module {
|
||||
std::map<uint32_t, Type> types;
|
||||
std::map<uint32_t, std::map<uint32_t, Member>> members; // struct id -> member -> layout
|
||||
std::map<uint32_t, uint32_t> arrayStride, set, binding, constU32;
|
||||
std::map<uint32_t, bool> block;
|
||||
std::map<uint32_t, Ptr> ptrs; // uniform member pointers reached by constant indices
|
||||
std::map<uint32_t, uint32_t> ptrPointee; // uniform pointer type -> pointee
|
||||
};
|
||||
|
||||
inline bool scan(const std::vector<uint32_t>& code, Module& m) {
|
||||
if (code.size() < 5 || code[0] != 0x07230203u) return false;
|
||||
for (size_t i = 5; i < code.size();) {
|
||||
const uint32_t words = code[i] >> 16, op = code[i] & 0xFFFF;
|
||||
if (!words || i + words > code.size()) return false;
|
||||
const uint32_t* w = &code[i];
|
||||
if (op == OpDecorate && words >= 3) {
|
||||
if (w[2] == DecDescriptorSet && words >= 4) m.set[w[1]] = w[3];
|
||||
else if (w[2] == DecBinding && words >= 4) m.binding[w[1]] = w[3];
|
||||
else if (w[2] == DecArrayStride && words >= 4) m.arrayStride[w[1]] = w[3];
|
||||
else if (w[2] == DecBlock) m.block[w[1]] = true;
|
||||
} else if (op == OpMemberDecorate && words >= 4) {
|
||||
Member& mm = m.members[w[1]][w[2]];
|
||||
if (w[3] == DecOffset && words >= 5) mm.offset = w[4];
|
||||
else if (w[3] == DecMatrixStride && words >= 5) mm.matrixStride = w[4];
|
||||
else if (w[3] == DecRowMajor) mm.rowMajor = true;
|
||||
} else if (op >= OpTypeBool && op <= OpTypePointer && words >= 2) {
|
||||
m.types[w[1]] = {op, std::vector<uint32_t>(w + 2, w + words)};
|
||||
if (op == OpTypePointer && words >= 4 && w[2] == StorageUniform) m.ptrPointee[w[1]] = w[3];
|
||||
} else if (op == OpConstant && words == 4) {
|
||||
const auto t = m.types.find(w[1]);
|
||||
if (t != m.types.end() && t->second.op == OpTypeInt && t->second.a.size() >= 1 && t->second.a[0] == 32) m.constU32[w[2]] = w[3];
|
||||
} else if (op == OpVariable && words >= 4 && w[3] == StorageUniform) {
|
||||
const auto pt = m.ptrPointee.find(w[1]);
|
||||
if (pt != m.ptrPointee.end() && m.block.count(pt->second) && m.set.count(w[2]) && m.binding.count(w[2]))
|
||||
m.ptrs[w[2]] = {m.set[w[2]], m.binding[w[2]], 0, pt->second, 0, false};
|
||||
} else if (op == OpFunction) {
|
||||
break;
|
||||
}
|
||||
i += words;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline uint32_t scalarBytes(const Module& m, uint32_t t) {
|
||||
const auto it = m.types.find(t);
|
||||
if (it == m.types.end()) return 0;
|
||||
if ((it->second.op == OpTypeFloat || it->second.op == OpTypeInt) && !it->second.a.empty() && it->second.a[0] == 32) return 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// One step down an access chain: (type, offset) of member / element `index`.
|
||||
inline bool step(const Module& m, Ptr& p, uint32_t index) {
|
||||
const auto it = m.types.find(p.type);
|
||||
if (it == m.types.end()) return false;
|
||||
const Type& t = it->second;
|
||||
if (t.op == OpTypeStruct) {
|
||||
if (index >= t.a.size()) return false;
|
||||
const auto ms = m.members.find(p.type);
|
||||
if (ms == m.members.end()) return false;
|
||||
const auto mm = ms->second.find(index);
|
||||
if (mm == ms->second.end() || mm->second.offset == 0xFFFFFFFFu) return false;
|
||||
p.offset += mm->second.offset;
|
||||
p.matrixStride = mm->second.matrixStride;
|
||||
p.rowMajor = mm->second.rowMajor;
|
||||
p.type = t.a[index];
|
||||
return true;
|
||||
}
|
||||
if (t.op == OpTypeArray) {
|
||||
const auto st = m.arrayStride.find(p.type);
|
||||
if (st == m.arrayStride.end()) return false;
|
||||
p.offset += index * st->second;
|
||||
p.type = t.a[0];
|
||||
return true;
|
||||
}
|
||||
if (t.op == OpTypeMatrix) {
|
||||
if (!p.matrixStride || p.rowMajor) return false;
|
||||
p.offset += index * p.matrixStride;
|
||||
p.type = t.a[0];
|
||||
return true;
|
||||
}
|
||||
if (t.op == OpTypeVector) {
|
||||
const uint32_t b = scalarBytes(m, t.a[0]);
|
||||
if (!b) return false;
|
||||
p.offset += index * b;
|
||||
p.type = t.a[0];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The bytes a value of type `type` at `p` spans (0 when it can't be built as a constant).
|
||||
inline uint32_t extent(const Module& m, const Ptr& p) {
|
||||
const auto it = m.types.find(p.type);
|
||||
if (it == m.types.end()) return 0;
|
||||
const Type& t = it->second;
|
||||
if (t.op == OpTypeFloat || t.op == OpTypeInt) return scalarBytes(m, p.type);
|
||||
if (t.op == OpTypeVector) return scalarBytes(m, t.a[0]) * (t.a.size() >= 2 ? t.a[1] : 0);
|
||||
uint32_t end = 0, n = 0;
|
||||
if (t.op == OpTypeMatrix) n = t.a.size() >= 2 ? t.a[1] : 0;
|
||||
else if (t.op == OpTypeArray) { const auto c = m.constU32.find(t.a.size() >= 2 ? t.a[1] : 0); n = c == m.constU32.end() ? 0 : c->second; }
|
||||
else if (t.op == OpTypeStruct) n = static_cast<uint32_t>(t.a.size());
|
||||
else return 0;
|
||||
for (uint32_t k = 0; k < n; ++k) {
|
||||
Ptr q = p;
|
||||
if (!step(m, q, k)) return 0;
|
||||
const uint32_t e = extent(m, q);
|
||||
if (!e) return 0;
|
||||
end = std::max(end, q.offset - p.offset + e);
|
||||
}
|
||||
return end;
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
// The blocks (set, binding) this shader loads constant-indexed members of: what a specialization depends on.
|
||||
inline std::vector<std::pair<uint32_t, uint32_t>> blocksRead(const std::vector<uint32_t>& code) {
|
||||
detail::Module m;
|
||||
std::vector<std::pair<uint32_t, uint32_t>> out;
|
||||
if (!detail::scan(code, m)) return out;
|
||||
for (const auto& p : m.ptrs) {
|
||||
const std::pair<uint32_t, uint32_t> sb = {p.second.set, p.second.binding};
|
||||
if (std::find(out.begin(), out.end(), sb) == out.end()) out.push_back(sb);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// `code` with the foldable loads replaced (integers, and floats that are 0, 1 or -1); `folded` counts them. The input
|
||||
// itself when nothing could be folded.
|
||||
inline std::vector<uint32_t> specialize(const std::vector<uint32_t>& code, const Fetch& fetch, uint32_t* folded = nullptr, int limit = -1,
|
||||
int floats = 2) {
|
||||
using namespace detail;
|
||||
uint32_t count = 0;
|
||||
if (!folded) folded = &count;
|
||||
*folded = 0;
|
||||
Module m;
|
||||
if (!scan(code, m)) return code;
|
||||
std::map<std::pair<uint32_t, uint32_t>, BlockBytes> blocks;
|
||||
std::map<std::pair<uint32_t, uint32_t>, bool> have;
|
||||
uint32_t bound = code[3];
|
||||
std::vector<uint32_t> consts; // new constants, before the first function
|
||||
std::map<std::pair<uint32_t, std::vector<uint32_t>>, uint32_t> made; // (type, words) -> id
|
||||
std::function<uint32_t(const Ptr&, const BlockBytes&)> build = [&](const Ptr& p, const BlockBytes& bb) -> uint32_t {
|
||||
const auto it = m.types.find(p.type);
|
||||
if (it == m.types.end()) return 0;
|
||||
const Type& t = it->second;
|
||||
std::vector<uint32_t> parts;
|
||||
uint32_t op = OpConstantComposite;
|
||||
if (t.op == OpTypeFloat || t.op == OpTypeInt) {
|
||||
if (scalarBytes(m, p.type) != 4 || p.offset + 4 > bb.size) return 0;
|
||||
uint32_t v;
|
||||
std::memcpy(&v, bb.data + p.offset, 4);
|
||||
// Floats only when exactly 0, 1 or -1: what they make dead (a multiply by 1, a layer mixed in by 0) goes
|
||||
// exactly, while folding any other value let the compiler round an expression its own way (a last bit
|
||||
// apart on 0.02% of Desert Terrace's pixels for its palms' colorIntensity), and saves next to nothing.
|
||||
if (t.op == OpTypeFloat && floats < 1) return 0;
|
||||
if (t.op == OpTypeFloat && (v & 0x7FFFFFFFu) != 0 && (floats < 2 || (v & 0x7FFFFFFFu) != 0x3F800000u)) return 0;
|
||||
parts.push_back(v);
|
||||
op = OpConstant;
|
||||
} else {
|
||||
uint32_t n = 0;
|
||||
if (t.op == OpTypeVector || t.op == OpTypeMatrix) n = t.a.size() >= 2 ? t.a[1] : 0;
|
||||
else if (t.op == OpTypeArray) { const auto c = m.constU32.find(t.a.size() >= 2 ? t.a[1] : 0); n = c == m.constU32.end() ? 0 : c->second; }
|
||||
else if (t.op == OpTypeStruct) n = static_cast<uint32_t>(t.a.size());
|
||||
if (!n || n > 1024) return 0;
|
||||
for (uint32_t k = 0; k < n; ++k) {
|
||||
Ptr q = p;
|
||||
if (!step(m, q, k)) return 0;
|
||||
const uint32_t id = build(q, bb);
|
||||
if (!id) return 0;
|
||||
parts.push_back(id);
|
||||
}
|
||||
}
|
||||
const auto key = std::make_pair(p.type * 2 + (op == OpConstant ? 1u : 0u), parts);
|
||||
if (const auto f = made.find(key); f != made.end()) return f->second;
|
||||
const uint32_t id = bound++;
|
||||
consts.push_back(((3u + static_cast<uint32_t>(parts.size())) << 16) | op);
|
||||
consts.push_back(p.type);
|
||||
consts.push_back(id);
|
||||
consts.insert(consts.end(), parts.begin(), parts.end());
|
||||
made[key] = id;
|
||||
return id;
|
||||
};
|
||||
std::vector<uint32_t> body; // everything from the first OpFunction on, rewritten
|
||||
size_t firstFunction = code.size();
|
||||
for (size_t i = 5; i < code.size();) {
|
||||
const uint32_t words = code[i] >> 16, op = code[i] & 0xFFFF;
|
||||
if (!words || i + words > code.size()) return code;
|
||||
if (op == OpFunction && firstFunction == code.size()) firstFunction = i;
|
||||
const uint32_t* w = &code[i];
|
||||
if (i >= firstFunction) {
|
||||
if ((op == OpAccessChain || op == OpInBoundsAccessChain) && words >= 4) {
|
||||
const auto base = m.ptrs.find(w[3]);
|
||||
if (base != m.ptrs.end()) {
|
||||
Ptr p = base->second;
|
||||
bool ok = true;
|
||||
for (uint32_t k = 4; k < words && ok; ++k) {
|
||||
const auto c = m.constU32.find(w[k]);
|
||||
ok = c != m.constU32.end() && step(m, p, c->second);
|
||||
}
|
||||
if (ok) m.ptrs[w[2]] = p;
|
||||
}
|
||||
} else if (op == OpLoad && words >= 4) {
|
||||
const auto pp = m.ptrs.find(w[3]);
|
||||
if (pp != m.ptrs.end() && pp->second.type == w[1] && (limit < 0 || static_cast<int>(*folded) < limit)) {
|
||||
const Ptr& p = pp->second;
|
||||
const auto sb = std::make_pair(p.set, p.binding);
|
||||
if (!have.count(sb)) { BlockBytes bb; have[sb] = fetch(p.set, p.binding, bb); blocks[sb] = bb; }
|
||||
const BlockBytes& bb = blocks[sb];
|
||||
const uint32_t ext = have[sb] ? extent(m, p) : 0;
|
||||
if (ext && p.offset + ext <= bb.size && (!bb.foldable || bb.foldable(p.offset, ext))) {
|
||||
if (const uint32_t id = build(p, bb)) {
|
||||
body.insert(body.end(), {(4u << 16) | OpCopyObject, w[1], w[2], id});
|
||||
++*folded;
|
||||
i += words;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
body.insert(body.end(), w, w + words);
|
||||
}
|
||||
i += words;
|
||||
}
|
||||
if (consts.empty()) return code;
|
||||
std::vector<uint32_t> out(code.begin(), code.begin() + static_cast<std::ptrdiff_t>(firstFunction));
|
||||
out.insert(out.end(), consts.begin(), consts.end());
|
||||
out.insert(out.end(), body.begin(), body.end());
|
||||
out[3] = bound;
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace spvspec
|
||||
@@ -0,0 +1,322 @@
|
||||
#pragma once
|
||||
// Surfaces made from a home's meshes (Studio's object Surfaces, Frame Home's mesh colliders) are for walking on and
|
||||
// bumping into, not for looking at: a sofa drawn with 50 000 triangles of stitching and cushion folds collides the same
|
||||
// as a few thousand. simplifySurfaceMesh clusters the vertices on a grid (2.5 cm by default, a quarter of the walkable
|
||||
// area's 10 cm cells, so nothing you can walk on, step up or bump into moves noticeably) and keeps one of each
|
||||
// triangle left: slivers collapsed to a line or a point go, and so does the back of a two-sided face (it lands on
|
||||
// the front). The vertices left are compacted. It's idempotent: simplifying a simplified mesh changes nothing.
|
||||
#include "core/world.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
constexpr float kSurfaceCell = 0.025f; // m
|
||||
|
||||
// cell is in the mesh's own units (a surface stored in its object's frame: kSurfaceCell / the frame's scale).
|
||||
inline void simplifySurfaceMesh(std::vector<float>& pos, std::vector<uint32_t>& idx, float cell) {
|
||||
const size_t nv = pos.size() / 3;
|
||||
if (!(cell > 0) || idx.size() < 3 || nv == 0) return;
|
||||
constexpr int64_t kOff = int64_t(1) << 20;
|
||||
auto key = [&](size_t v) -> uint64_t {
|
||||
const int64_t q[3] = {static_cast<int64_t>(std::floor(pos[v * 3] / cell)), static_cast<int64_t>(std::floor(pos[v * 3 + 1] / cell)),
|
||||
static_cast<int64_t>(std::floor(pos[v * 3 + 2] / cell))};
|
||||
uint64_t k = 0;
|
||||
for (int i = 0; i < 3; ++i) k = (k << 21) | static_cast<uint64_t>(std::clamp<int64_t>(q[i] + kOff, 0, (int64_t(1) << 21) - 1));
|
||||
return k;
|
||||
};
|
||||
// clusters: each the average of the vertices in its cell
|
||||
std::unordered_map<uint64_t, uint32_t> clusterOf;
|
||||
clusterOf.reserve(nv);
|
||||
std::vector<uint32_t> remap(nv);
|
||||
std::vector<double> sum;
|
||||
std::vector<uint32_t> count;
|
||||
for (size_t v = 0; v < nv; ++v) {
|
||||
auto [it, fresh] = clusterOf.try_emplace(key(v), static_cast<uint32_t>(count.size()));
|
||||
if (fresh) { sum.insert(sum.end(), {0.0, 0.0, 0.0}); count.push_back(0); }
|
||||
const uint32_t c = it->second;
|
||||
remap[v] = c;
|
||||
for (int k = 0; k < 3; ++k) sum[c * 3 + k] += pos[v * 3 + k];
|
||||
++count[c];
|
||||
}
|
||||
std::vector<float> cp(count.size() * 3);
|
||||
for (size_t c = 0; c < count.size(); ++c)
|
||||
for (int k = 0; k < 3; ++k) cp[c * 3 + k] = static_cast<float>(sum[c * 3 + k] / count[c]);
|
||||
// triangles: collapsed ones go, each one once (either winding)
|
||||
std::unordered_set<uint64_t> seen;
|
||||
seen.reserve(idx.size() / 3);
|
||||
std::vector<uint32_t> out;
|
||||
out.reserve(idx.size());
|
||||
const bool dedupe = count.size() < (size_t(1) << 21);
|
||||
const float minArea2 = cell * cell * 0.02f; // (twice the area)
|
||||
for (size_t t = 0; t + 2 < idx.size(); t += 3) {
|
||||
if (idx[t] >= nv || idx[t + 1] >= nv || idx[t + 2] >= nv) continue;
|
||||
const uint32_t a = remap[idx[t]], b = remap[idx[t + 1]], c = remap[idx[t + 2]];
|
||||
if (a == b || b == c || a == c) continue;
|
||||
const float* A = &cp[a * 3]; const float* B = &cp[b * 3]; const float* C = &cp[c * 3];
|
||||
const float e1[3] = {B[0] - A[0], B[1] - A[1], B[2] - A[2]}, e2[3] = {C[0] - A[0], C[1] - A[1], C[2] - A[2]};
|
||||
const float n[3] = {e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]};
|
||||
if (std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]) < minArea2) continue;
|
||||
if (dedupe) {
|
||||
uint32_t s[3] = {a, b, c};
|
||||
std::sort(s, s + 3);
|
||||
if (!seen.insert((uint64_t(s[0]) << 42) | (uint64_t(s[1]) << 21) | s[2]).second) continue;
|
||||
}
|
||||
out.insert(out.end(), {a, b, c});
|
||||
}
|
||||
// compact: only the clusters still used
|
||||
std::vector<uint32_t> used(count.size(), UINT32_MAX);
|
||||
std::vector<float> np;
|
||||
np.reserve(cp.size());
|
||||
for (uint32_t& v : out) {
|
||||
if (used[v] == UINT32_MAX) { used[v] = static_cast<uint32_t>(np.size() / 3); np.insert(np.end(), &cp[v * 3], &cp[v * 3] + 3); }
|
||||
v = used[v];
|
||||
}
|
||||
pos.swap(np);
|
||||
idx.swap(out);
|
||||
}
|
||||
|
||||
// The cell in a frame's own units: kSurfaceCell over the frame's (largest) scale, so a scaled-up object isn't coarser.
|
||||
inline float surfaceCellFor(const float* world) {
|
||||
float s = 0;
|
||||
for (int c = 0; c < 3; ++c) s = std::max(s, std::sqrt(world[c * 4] * world[c * 4] + world[c * 4 + 1] * world[c * 4 + 1] + world[c * 4 + 2] * world[c * 4 + 2]));
|
||||
return s > 1e-6f ? kSurfaceCell / s : kSurfaceCell;
|
||||
}
|
||||
|
||||
// The world's mesh colliders, simplified for walking (Meta's stand-ins are the entities' render meshes, and worlds an
|
||||
// older Studio saved copied whole meshes). Ones already simplified don't change.
|
||||
inline void simplifyWorldMeshes(HomeWorld& w) {
|
||||
size_t before = 0, after = 0;
|
||||
for (WorldCollider& c : w.colliders) {
|
||||
if (c.kind != WorldCollider::Mesh || c.simplified) continue;
|
||||
c.simplified = true;
|
||||
if (c.indices.size() <= 3000) continue;
|
||||
before += c.indices.size() / 3;
|
||||
simplifySurfaceMesh(c.positions, c.indices, surfaceCellFor(c.world));
|
||||
after += c.indices.size() / 3;
|
||||
}
|
||||
if (after < before) std::fprintf(stderr, "[world] mesh colliders simplified: %zu -> %zu triangles\n", before, after);
|
||||
}
|
||||
|
||||
// ── Surfaces you place (boxes and meshes not made from a home object): two that overlap become one ──
|
||||
|
||||
// A Box or Mesh collider's triangles in world space, 9 floats each (spheres and capsules: none).
|
||||
inline std::vector<float> colliderTriangles(const WorldCollider& c) {
|
||||
std::vector<float> out;
|
||||
auto xf = [&](float x, float y, float z, float* o) {
|
||||
for (int k = 0; k < 3; ++k) o[k] = c.world[k] * x + c.world[4 + k] * y + c.world[8 + k] * z + c.world[12 + k];
|
||||
};
|
||||
if (c.kind == WorldCollider::Box) {
|
||||
float v[8][3];
|
||||
for (int k = 0; k < 8; ++k) xf((k & 1 ? 1 : -1) * c.half[0], (k & 2 ? 1 : -1) * c.half[1], (k & 4 ? 1 : -1) * c.half[2], v[k]);
|
||||
static const int f[36] = {0, 2, 3, 0, 3, 1, 4, 5, 7, 4, 7, 6, 0, 1, 5, 0, 5, 4, 2, 6, 7, 2, 7, 3, 0, 4, 6, 0, 6, 2, 1, 3, 7, 1, 7, 5};
|
||||
for (int i : f) out.insert(out.end(), v[i], v[i] + 3);
|
||||
} else if (c.kind == WorldCollider::Mesh) {
|
||||
const size_t nv = c.positions.size() / 3;
|
||||
for (size_t t = 0; t + 2 < c.indices.size(); t += 3)
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const uint32_t i = c.indices[t + k];
|
||||
float p[3] = {0, 0, 0};
|
||||
if (i < nv) xf(c.positions[i * 3], c.positions[i * 3 + 1], c.positions[i * 3 + 2], p);
|
||||
out.insert(out.end(), p, p + 3);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace surface_detail {
|
||||
// Does segment p-q cross triangle abc? (Möller-Trumbore on the segment's length)
|
||||
inline bool segmentHits(const float* p, const float* q, const float* a, const float* b, const float* c) {
|
||||
const float d[3] = {q[0] - p[0], q[1] - p[1], q[2] - p[2]};
|
||||
const float e1[3] = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}, e2[3] = {c[0] - a[0], c[1] - a[1], c[2] - a[2]};
|
||||
const float h[3] = {d[1] * e2[2] - d[2] * e2[1], d[2] * e2[0] - d[0] * e2[2], d[0] * e2[1] - d[1] * e2[0]};
|
||||
const float det = e1[0] * h[0] + e1[1] * h[1] + e1[2] * h[2];
|
||||
if (std::fabs(det) < 1e-12f) return false;
|
||||
const float inv = 1 / det, s[3] = {p[0] - a[0], p[1] - a[1], p[2] - a[2]};
|
||||
const float u = (s[0] * h[0] + s[1] * h[1] + s[2] * h[2]) * inv;
|
||||
if (u < 0 || u > 1) return false;
|
||||
const float qv[3] = {s[1] * e1[2] - s[2] * e1[1], s[2] * e1[0] - s[0] * e1[2], s[0] * e1[1] - s[1] * e1[0]};
|
||||
const float v = (d[0] * qv[0] + d[1] * qv[1] + d[2] * qv[2]) * inv;
|
||||
if (v < 0 || u + v > 1) return false;
|
||||
const float t = (e2[0] * qv[0] + e2[1] * qv[1] + e2[2] * qv[2]) * inv;
|
||||
return t >= 0 && t <= 1;
|
||||
}
|
||||
// Is p inside the closed triangle soup? (crossings of a ray from p, slightly tilted so it misses edges)
|
||||
inline bool inside(const std::vector<float>& tris, const float* p) {
|
||||
const float far[3] = {p[0] + 1000.0f, p[1] + 0.731f, p[2] + 0.377f};
|
||||
int n = 0;
|
||||
for (size_t t = 0; t + 8 < tris.size(); t += 9) n += segmentHits(p, far, &tris[t], &tris[t + 3], &tris[t + 6]);
|
||||
return n & 1;
|
||||
}
|
||||
// The volume a soup encloses (0 for an open sheet: a flat plate has no inside to bury anything in).
|
||||
inline float volume(const std::vector<float>& t) {
|
||||
double v = 0;
|
||||
for (size_t i = 0; i + 8 < t.size(); i += 9)
|
||||
v += t[i] * (t[i + 4] * t[i + 8] - t[i + 5] * t[i + 7]) - t[i + 1] * (t[i + 3] * t[i + 8] - t[i + 5] * t[i + 6]) + t[i + 2] * (t[i + 3] * t[i + 7] - t[i + 4] * t[i + 6]);
|
||||
return static_cast<float>(std::fabs(v) / 6);
|
||||
}
|
||||
inline void bounds(const std::vector<float>& tris, float* lo, float* hi) {
|
||||
for (int k = 0; k < 3; ++k) { lo[k] = 1e30f; hi[k] = -1e30f; }
|
||||
for (size_t i = 0; i < tris.size(); ++i) { lo[i % 3] = std::min(lo[i % 3], tris[i]); hi[i % 3] = std::max(hi[i % 3], tris[i]); }
|
||||
}
|
||||
} // namespace surface_detail
|
||||
|
||||
// Do two triangle soups (colliderTriangles) touch: an edge of one through a face of the other, or one inside the other.
|
||||
inline bool surfacesTouch(const std::vector<float>& a, const std::vector<float>& b) {
|
||||
using namespace surface_detail;
|
||||
if (a.empty() || b.empty()) return false;
|
||||
float alo[3], ahi[3], blo[3], bhi[3];
|
||||
bounds(a, alo, ahi);
|
||||
bounds(b, blo, bhi);
|
||||
for (int k = 0; k < 3; ++k) if (alo[k] > bhi[k] + 1e-3f || blo[k] > ahi[k] + 1e-3f) return false;
|
||||
auto edgesThrough = [](const std::vector<float>& x, const std::vector<float>& y) {
|
||||
for (size_t t = 0; t + 8 < x.size(); t += 9)
|
||||
for (int e = 0; e < 3; ++e)
|
||||
for (size_t u = 0; u + 8 < y.size(); u += 9)
|
||||
if (segmentHits(&x[t + e * 3], &x[t + (e + 1) % 3 * 3], &y[u], &y[u + 3], &y[u + 6])) return true;
|
||||
return false;
|
||||
};
|
||||
return edgesThrough(a, b) || edgesThrough(b, a) || (volume(b) > 1e-6f && inside(b, &a[0])) || (volume(a) > 1e-6f && inside(a, &b[0]));
|
||||
}
|
||||
|
||||
namespace surface_detail {
|
||||
// The parts of triangle abc outside a box (its 6 planes cut it; what's inside all of them goes), as triangles. On a
|
||||
// face of the box counts as inside unless `onIsOut` (two surfaces sharing a face: one of them keeps it).
|
||||
inline void outsideBox(const float* tri, const WorldCollider& box, std::vector<float>& out, bool onIsOut) {
|
||||
using Poly = std::vector<std::array<float, 3>>;
|
||||
struct Plane { float u[3], c, h, sgn; };
|
||||
std::vector<Plane> planes;
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
float u[3] = {box.world[axis * 4], box.world[axis * 4 + 1], box.world[axis * 4 + 2]};
|
||||
const float L = std::sqrt(u[0] * u[0] + u[1] * u[1] + u[2] * u[2]);
|
||||
if (L < 1e-9f) { out.insert(out.end(), tri, tri + 9); return; }
|
||||
for (float& x : u) x /= L;
|
||||
const float c = u[0] * box.world[12] + u[1] * box.world[13] + u[2] * box.world[14], h = box.half[axis] * L;
|
||||
for (float sgn : {1.0f, -1.0f}) planes.push_back({{u[0], u[1], u[2]}, c, h, sgn});
|
||||
}
|
||||
const float eps = onIsOut ? -1e-5f : 1e-5f; // inside a side: distance past it <= eps
|
||||
auto dist = [&](const Plane& p, const float* x) { return p.sgn * (p.u[0] * x[0] + p.u[1] * x[1] + p.u[2] * x[2] - p.c) - p.h; };
|
||||
bool allIn = true;
|
||||
for (const Plane& p : planes) {
|
||||
int outN = 0;
|
||||
for (int k = 0; k < 3; ++k) outN += dist(p, tri + k * 3) > eps;
|
||||
if (outN == 3) { out.insert(out.end(), tri, tri + 9); return; } // wholly outside one side: kept whole
|
||||
allIn &= outN == 0;
|
||||
}
|
||||
if (allIn) return; // wholly inside: buried
|
||||
std::vector<Poly> inside{{{tri[0], tri[1], tri[2]}, {tri[3], tri[4], tri[5]}, {tri[6], tri[7], tri[8]}}};
|
||||
std::vector<Poly> keep;
|
||||
for (const Plane& pl : planes) {
|
||||
{ // the part outside this side is kept; the part inside goes on to the next side
|
||||
std::vector<Poly> next;
|
||||
for (const Poly& P : inside) {
|
||||
Poly in, outP;
|
||||
for (size_t i = 0; i < P.size(); ++i) {
|
||||
const auto& A = P[i];
|
||||
const auto& B = P[(i + 1) % P.size()];
|
||||
const float da = dist(pl, A.data()) - eps, db = dist(pl, B.data()) - eps;
|
||||
(da <= 0 ? in : outP).push_back(A);
|
||||
if ((da < 0 && db > 0) || (da > 0 && db < 0)) {
|
||||
const float t = da / (da - db);
|
||||
const std::array<float, 3> X = {A[0] + (B[0] - A[0]) * t, A[1] + (B[1] - A[1]) * t, A[2] + (B[2] - A[2]) * t};
|
||||
in.push_back(X);
|
||||
outP.push_back(X);
|
||||
}
|
||||
}
|
||||
if (outP.size() >= 3) keep.push_back(outP);
|
||||
if (in.size() >= 3) next.push_back(in);
|
||||
}
|
||||
inside.swap(next);
|
||||
}
|
||||
}
|
||||
for (const Poly& P : keep)
|
||||
for (size_t i = 1; i + 1 < P.size(); ++i)
|
||||
for (const auto* v : {&P[0], &P[i], &P[i + 1]}) out.insert(out.end(), v->begin(), v->end());
|
||||
}
|
||||
} // namespace surface_detail
|
||||
|
||||
// Two surfaces as one, in world space. Boxes (and combined surfaces' boxes, kept in WorldCollider::boxes) give the
|
||||
// exact outside of their union: each box's faces cut where they go into another box (a face two share is kept once,
|
||||
// the earlier box's). Any other mesh (a sheet, an imported shape) adds its faces less the ones wholly buried in a
|
||||
// solid part, and the result then can't be cut exactly again (no boxes kept). Welded.
|
||||
inline WorldCollider combineSurfaces(const WorldCollider& a, const WorldCollider& b) {
|
||||
using namespace surface_detail;
|
||||
std::vector<WorldCollider> boxes; // world frames
|
||||
std::vector<std::vector<float>> meshes; // other parts' world triangles
|
||||
for (const WorldCollider* x : {&a, &b}) {
|
||||
if (x->kind == WorldCollider::Box) { boxes.push_back(*x); continue; }
|
||||
if (x->boxes.empty()) { meshes.push_back(colliderTriangles(*x)); continue; }
|
||||
for (size_t i = 0; i + 19 <= x->boxes.size(); i += 19) {
|
||||
WorldCollider bx;
|
||||
bx.kind = WorldCollider::Box;
|
||||
float m[16];
|
||||
for (int r = 0; r < 4; ++r)
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
float v = 0;
|
||||
for (int k = 0; k < 4; ++k) v += x->world[k * 4 + r] * x->boxes[i + c * 4 + k];
|
||||
m[c * 4 + r] = v;
|
||||
}
|
||||
std::copy_n(m, 16, bx.world);
|
||||
std::copy_n(&x->boxes[i + 16], 3, bx.half);
|
||||
boxes.push_back(bx);
|
||||
}
|
||||
}
|
||||
WorldCollider c;
|
||||
c.name = a.name;
|
||||
c.kind = WorldCollider::Mesh;
|
||||
c.role = a.role;
|
||||
c.hidden = a.hidden && b.hidden;
|
||||
c.placed = true;
|
||||
auto add = [&](const float* t) {
|
||||
for (int k = 0; k < 9; ++k) c.positions.push_back(t[k]);
|
||||
const uint32_t i = static_cast<uint32_t>(c.indices.size());
|
||||
c.indices.insert(c.indices.end(), {i, i + 1, i + 2});
|
||||
};
|
||||
std::vector<std::vector<float>> boxTris(boxes.size());
|
||||
for (size_t i = 0; i < boxes.size(); ++i) boxTris[i] = colliderTriangles(boxes[i]);
|
||||
auto buriedIn = [&](const float* t, const std::vector<float>& other) { // wholly inside a solid soup
|
||||
if (volume(other) <= 1e-6f) return false;
|
||||
for (int k = 0; k < 3; ++k) { // its corners pulled a little towards the middle (a corner on the face isn't in)
|
||||
float p[3];
|
||||
for (int j = 0; j < 3; ++j) p[j] = t[k * 3 + j] * 0.98f + (t[j] + t[3 + j] + t[6 + j]) / 3 * 0.02f;
|
||||
if (!inside(other, p)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
for (size_t i = 0; i < boxes.size(); ++i) {
|
||||
std::vector<float> cur = boxTris[i];
|
||||
for (size_t j = 0; j < boxes.size() && !cur.empty(); ++j) {
|
||||
if (j == i) continue;
|
||||
std::vector<float> next;
|
||||
for (size_t t = 0; t + 8 < cur.size(); t += 9) outsideBox(&cur[t], boxes[j], next, i < j);
|
||||
cur.swap(next);
|
||||
}
|
||||
for (size_t t = 0; t + 8 < cur.size(); t += 9) {
|
||||
bool buried = false;
|
||||
for (const auto& m : meshes) buried |= buriedIn(&cur[t], m);
|
||||
if (!buried) add(&cur[t]);
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < meshes.size(); ++i) {
|
||||
std::vector<float> cur = meshes[i];
|
||||
for (size_t j = 0; j < boxes.size() && !cur.empty(); ++j) {
|
||||
std::vector<float> next;
|
||||
for (size_t t = 0; t + 8 < cur.size(); t += 9) outsideBox(&cur[t], boxes[j], next, false);
|
||||
cur.swap(next);
|
||||
}
|
||||
for (size_t t = 0; t + 8 < cur.size(); t += 9) {
|
||||
bool buried = false;
|
||||
for (size_t k = 0; k < meshes.size(); ++k) if (k != i) buried |= buriedIn(&cur[t], meshes[k]);
|
||||
if (!buried) add(&cur[t]);
|
||||
}
|
||||
}
|
||||
if (meshes.empty())
|
||||
for (const WorldCollider& bx : boxes) { c.boxes.insert(c.boxes.end(), bx.world, bx.world + 16); c.boxes.insert(c.boxes.end(), bx.half, bx.half + 3); }
|
||||
simplifySurfaceMesh(c.positions, c.indices, 0.001f); // shared corners welded
|
||||
return c;
|
||||
}
|
||||
+1985
File diff suppressed because it is too large.
Load diff
+171
@@ -0,0 +1,171 @@
|
||||
#pragma once
|
||||
// A disk cache of mip chains for textures shipped without one. The first time a home opens, such a texture is drawn
|
||||
// as before (decoded, shrunk to maxDecodedSide, RGBA mips: slow, heavy in memory) while its real ASTC chain is built
|
||||
// in the background (tex_mips.h: the full-size level exactly as shipped) and saved here; every later open uploads
|
||||
// that chain compressed, with nothing decoded. Entries are keyed by the texture's own bytes, so any home (APK, .fenv,
|
||||
// model) that carries the same texture shares one. Kept under 2 GB (the least recently used go first).
|
||||
#include "tex_mips.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#ifndef _WIN32
|
||||
#include <sys/resource.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace texcache {
|
||||
|
||||
// FRAME_TEX_CACHE, else %LOCALAPPDATA%\FrameStudio\cache\texmips (Windows) or ~/.cache/frame-home/texmips.
|
||||
inline std::filesystem::path dir() {
|
||||
if (const char* e = std::getenv("FRAME_TEX_CACHE")) return std::filesystem::u8path(e);
|
||||
#ifdef _WIN32
|
||||
if (const char* l = std::getenv("LOCALAPPDATA")) return std::filesystem::u8path(std::string(l) + "\\FrameStudio\\cache\\texmips");
|
||||
#endif
|
||||
if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::filesystem::u8path(std::string(x) + "/frame-home/texmips");
|
||||
const char* h = std::getenv("HOME");
|
||||
return std::filesystem::u8path(std::string(h ? h : "/tmp") + "/.cache/frame-home/texmips");
|
||||
}
|
||||
|
||||
inline uint64_t key(const TexSource& t) {
|
||||
uint64_t k = 1469598103934665603ull;
|
||||
auto mix = [&](uint64_t x) { k = (k ^ x) * 0x100000001B3ull; k ^= k >> 29; };
|
||||
mix(t.w); mix(t.h); mix(t.bw); mix(t.bh);
|
||||
const size_t n = std::min<size_t>(t.astc.size(), astcMip0Bytes(t.w, t.h, t.bw, t.bh));
|
||||
size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8) { uint64_t x; std::memcpy(&x, t.astc.data() + i, 8); mix(x); }
|
||||
for (; i < n; ++i) mix(t.astc[i]);
|
||||
mix(n);
|
||||
return k;
|
||||
}
|
||||
|
||||
inline std::filesystem::path pathFor(uint64_t k) {
|
||||
char name[32];
|
||||
std::snprintf(name, sizeof name, "%016llx.ftxm", static_cast<unsigned long long>(k));
|
||||
return dir() / name;
|
||||
}
|
||||
|
||||
struct Header { char magic[4]; uint32_t version, w, h, bw, bh, mips; uint64_t bytes; };
|
||||
|
||||
// The cached chain for `t`, or null.
|
||||
inline std::shared_ptr<TexSource> load(const TexSource& t) {
|
||||
if (!texmips::wants(t)) return nullptr;
|
||||
const std::filesystem::path p = pathFor(key(t));
|
||||
FILE* f = std::fopen(p.string().c_str(), "rb");
|
||||
if (!f) return nullptr;
|
||||
Header hd = {};
|
||||
std::shared_ptr<TexSource> out;
|
||||
if (std::fread(&hd, sizeof hd, 1, f) == 1 && std::memcmp(hd.magic, "FTXM", 4) == 0 && hd.version == 1 && hd.w == t.w && hd.h == t.h &&
|
||||
hd.bw == t.bw && hd.bh == t.bh && hd.mips == astcFullMipCount(t.w, t.h) && hd.bytes < (1ull << 31)) {
|
||||
auto s = std::make_shared<TexSource>();
|
||||
s->w = t.w; s->h = t.h; s->bw = t.bw; s->bh = t.bh; s->mips = hd.mips; s->hdr = t.hdr; s->faces = 1;
|
||||
s->astc.resize(static_cast<size_t>(hd.bytes));
|
||||
if (std::fread(s->astc.data(), 1, s->astc.size(), f) == s->astc.size()) out = std::move(s);
|
||||
}
|
||||
std::fclose(f);
|
||||
if (out) { // used: the last to be pruned
|
||||
std::error_code ec;
|
||||
std::filesystem::last_write_time(p, std::filesystem::file_time_type::clock::now(), ec);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline void prune(uint64_t limit) {
|
||||
std::error_code ec;
|
||||
std::vector<std::pair<std::filesystem::file_time_type, std::filesystem::path>> files;
|
||||
uint64_t total = 0;
|
||||
for (const auto& e : std::filesystem::directory_iterator(dir(), ec)) {
|
||||
if (e.path().extension() != ".ftxm") continue;
|
||||
const uint64_t s = e.file_size(ec);
|
||||
total += s;
|
||||
files.push_back({e.last_write_time(ec), e.path()});
|
||||
}
|
||||
if (total <= limit) return;
|
||||
std::sort(files.begin(), files.end());
|
||||
for (const auto& f : files) {
|
||||
if (total <= limit) break;
|
||||
const uint64_t s = std::filesystem::file_size(f.second, ec);
|
||||
if (std::filesystem::remove(f.second, ec)) total -= s;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool store(const TexSource& chain, uint64_t k) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dir(), ec);
|
||||
const std::filesystem::path p = pathFor(k), tmp = p.string() + ".part";
|
||||
FILE* f = std::fopen(tmp.string().c_str(), "wb");
|
||||
if (!f) return false;
|
||||
Header hd = {{'F', 'T', 'X', 'M'}, 1, chain.w, chain.h, chain.bw, chain.bh, chain.mips, chain.astc.size()};
|
||||
const bool ok = std::fwrite(&hd, sizeof hd, 1, f) == 1 && std::fwrite(chain.astc.data(), 1, chain.astc.size(), f) == chain.astc.size();
|
||||
if (std::fclose(f) != 0 || !ok) { std::filesystem::remove(tmp, ec); return false; }
|
||||
std::filesystem::rename(tmp, p, ec);
|
||||
if (ec) { std::filesystem::remove(tmp, ec); return false; }
|
||||
prune(2ull << 30);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Builds and saves `t`'s chain on a background thread at the lowest priority, one texture at a time, two encoder
|
||||
// threads (never in the way of a frame). Each texture once per run.
|
||||
class Builder {
|
||||
public:
|
||||
static Builder& get() { static Builder b; return b; }
|
||||
void add(std::shared_ptr<const TexSource> t) {
|
||||
if (!t || !texmips::wants(*t)) return;
|
||||
std::lock_guard<std::mutex> l(m_);
|
||||
if (!queued_.insert(t.get()).second) return;
|
||||
q_.push_back(std::move(t));
|
||||
if (!th_.joinable()) th_ = std::thread([this] { run(); });
|
||||
cv_.notify_one();
|
||||
}
|
||||
size_t pending() { std::lock_guard<std::mutex> l(m_); return q_.size() + (busy_ ? 1 : 0); }
|
||||
~Builder() { // never holds up an exit: a texture half encoded is left as a .part file
|
||||
{ std::lock_guard<std::mutex> l(m_); stop_ = true; q_.clear(); }
|
||||
cv_.notify_all();
|
||||
if (th_.joinable()) th_.detach();
|
||||
}
|
||||
|
||||
private:
|
||||
void run() {
|
||||
#ifdef _WIN32
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST);
|
||||
#else
|
||||
setpriority(PRIO_PROCESS, static_cast<id_t>(syscall(SYS_gettid)), 19); // (its encoder threads inherit it)
|
||||
#endif
|
||||
for (;;) {
|
||||
std::shared_ptr<const TexSource> t;
|
||||
{
|
||||
std::unique_lock<std::mutex> l(m_);
|
||||
cv_.wait(l, [&] { return stop_ || !q_.empty(); });
|
||||
if (stop_) return;
|
||||
t = std::move(q_.front());
|
||||
q_.pop_front();
|
||||
busy_ = true;
|
||||
}
|
||||
const uint64_t k = key(*t);
|
||||
std::error_code ec;
|
||||
if (t.use_count() > 1 && !std::filesystem::exists(pathFor(k), ec)) // (held only here: its home is closed)
|
||||
if (std::shared_ptr<TexSource> chain = texmips::withMips(*t, 2)) store(*chain, k);
|
||||
std::lock_guard<std::mutex> l(m_);
|
||||
busy_ = false;
|
||||
}
|
||||
}
|
||||
std::mutex m_;
|
||||
std::condition_variable cv_;
|
||||
std::deque<std::shared_ptr<const TexSource>> q_;
|
||||
std::set<const TexSource*> queued_;
|
||||
std::thread th_;
|
||||
bool stop_ = false, busy_ = false;
|
||||
};
|
||||
|
||||
} // namespace texcache
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
// Mip chains for textures that ship without one. Several Meta homes (Desert, Cascadia) carry 2048-8192 ASTC textures
|
||||
// with only their full-size level: drawn as they are they shimmer at a distance, so the renderer decodes them at load,
|
||||
// shrinks them to 2048 and builds the smaller levels in RGBA: blurrier than the original, slow to load, heavy in memory.
|
||||
// Baking a .fenv gives them a real chain instead: the full-size level stays exactly as shipped, and each smaller one is
|
||||
// box-filtered from the level above it and encoded to ASTC in the same block size. The GPU then samples them natively,
|
||||
// full sharpness up close and no shimmer far away.
|
||||
#include "core/types.h"
|
||||
#include "loaders/rendtxtr_parser.h"
|
||||
#include "astcenc.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace texmips {
|
||||
|
||||
// Worth a chain: an LDR 2D texture bigger than 256 with no smaller levels.
|
||||
inline bool wants(const TexSource& t) {
|
||||
return !t.hdr && t.faces == 1 && t.mips <= 1 && std::max(t.w, t.h) > 256 && astcDimsOk(t.w, t.h, t.bw, t.bh) &&
|
||||
t.astc.size() >= astcMip0Bytes(t.w, t.h, t.bw, t.bh);
|
||||
}
|
||||
|
||||
// `t` with its full chain, or null when it can't be made (the texture is then kept as it was). threads: encoder
|
||||
// threads (0 = every core).
|
||||
inline std::shared_ptr<TexSource> withMips(const TexSource& t, unsigned threads = 0) {
|
||||
if (!wants(t)) return nullptr;
|
||||
std::vector<u8> level;
|
||||
const u32 mip0 = astcMip0Bytes(t.w, t.h, t.bw, t.bh);
|
||||
if (!astc::decodeASTC(t.astc.data(), mip0, t.w, t.h, t.bw, t.bh, level, false, 0)) return nullptr;
|
||||
astcenc_config cfg = {};
|
||||
if (astcenc_config_init(ASTCENC_PRF_LDR, t.bw, t.bh, 1, ASTCENC_PRE_FAST, 0, &cfg) != ASTCENC_SUCCESS) return nullptr;
|
||||
unsigned n = threads ? threads : std::thread::hardware_concurrency();
|
||||
n = std::clamp(n, 1u, 16u);
|
||||
astcenc_context* ctx = nullptr;
|
||||
if (astcenc_context_alloc(&cfg, n, &ctx, nullptr) != ASTCENC_SUCCESS) return nullptr;
|
||||
auto out = std::make_shared<TexSource>(t);
|
||||
out->astc.assign(t.astc.begin(), t.astc.begin() + mip0); // the full-size level, exactly as shipped
|
||||
const u32 levels = astcFullMipCount(t.w, t.h);
|
||||
u32 w = t.w, h = t.h;
|
||||
bool ok = true;
|
||||
const astcenc_swizzle swz = {ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A};
|
||||
for (u32 l = 1; l < levels && ok; ++l) {
|
||||
// 2x2 box filter (an odd edge folds its last row / column in)
|
||||
const u32 nw = std::max(1u, w / 2), nh = std::max(1u, h / 2);
|
||||
std::vector<u8> next(static_cast<size_t>(nw) * nh * 4);
|
||||
for (u32 y = 0; y < nh; ++y)
|
||||
for (u32 x = 0; x < nw; ++x) {
|
||||
const u32 x0 = std::min(w - 1, x * 2), x1 = std::min(w - 1, x * 2 + 1), y0 = std::min(h - 1, y * 2), y1 = std::min(h - 1, y * 2 + 1);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
const unsigned s = level[(static_cast<size_t>(y0) * w + x0) * 4 + c] + level[(static_cast<size_t>(y0) * w + x1) * 4 + c] +
|
||||
level[(static_cast<size_t>(y1) * w + x0) * 4 + c] + level[(static_cast<size_t>(y1) * w + x1) * 4 + c];
|
||||
next[(static_cast<size_t>(y) * nw + x) * 4 + c] = static_cast<u8>((s + 2) / 4);
|
||||
}
|
||||
}
|
||||
level.swap(next);
|
||||
w = nw;
|
||||
h = nh;
|
||||
const size_t bytes = static_cast<size_t>((w + t.bw - 1) / t.bw) * ((h + t.bh - 1) / t.bh) * 16;
|
||||
const size_t at = out->astc.size();
|
||||
out->astc.resize(at + bytes);
|
||||
void* px = level.data();
|
||||
astcenc_image img = {};
|
||||
img.dim_x = w; img.dim_y = h; img.dim_z = 1;
|
||||
img.data_type = ASTCENC_TYPE_U8;
|
||||
img.data = &px;
|
||||
std::vector<astcenc_error> e(n, ASTCENC_SUCCESS);
|
||||
if (n > 1 && static_cast<size_t>(w) * h >= 256 * 256) { // big levels on every core, the small ones on one
|
||||
std::vector<std::thread> th;
|
||||
for (unsigned i = 0; i < n; ++i) th.emplace_back([&, i] { e[i] = astcenc_compress_image(ctx, &img, &swz, out->astc.data() + at, bytes, i); });
|
||||
for (auto& x : th) x.join();
|
||||
} else {
|
||||
// A context made for n threads waits for all n in compress: one thread uses a context of its own.
|
||||
astcenc_context* one = nullptr;
|
||||
if (astcenc_context_alloc(&cfg, 1, &one, nullptr) != ASTCENC_SUCCESS) { ok = false; break; }
|
||||
e[0] = astcenc_compress_image(one, &img, &swz, out->astc.data() + at, bytes, 0);
|
||||
astcenc_context_free(one);
|
||||
}
|
||||
for (astcenc_error x : e) ok = ok && x == ASTCENC_SUCCESS;
|
||||
astcenc_compress_reset(ctx);
|
||||
}
|
||||
astcenc_context_free(ctx);
|
||||
if (!ok) return nullptr;
|
||||
out->mips = levels;
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace texmips
|
||||
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
// The headset's temperature, for Frame Home's thermal governor: the hottest of the GPU and CPU sensors the kernel
|
||||
// lists in /sys/class/thermal (thermal_zone*/type "gpuss-*", "cpu*", "cpuss*"), in degrees C. The Steam Frame throttles
|
||||
// its GPU clock (903 MHz down to ~310) once these reach the mid-90s, and every frame then costs up to three times as
|
||||
// long: keeping them at the headset's ordinary level keeps the clock, and the frame rate, where they belong.
|
||||
// Elsewhere (the PC, a desktop Linux with no such zones) it reads nothing and the governor stays out of the way.
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ThermalSensors {
|
||||
public:
|
||||
ThermalSensors() {
|
||||
std::error_code ec;
|
||||
for (const auto& e : std::filesystem::directory_iterator("/sys/class/thermal", ec)) {
|
||||
const std::string name = e.path().filename().string();
|
||||
if (name.rfind("thermal_zone", 0) != 0) continue;
|
||||
std::ifstream t(e.path() / "type");
|
||||
std::string type;
|
||||
std::getline(t, type);
|
||||
if (type.rfind("gpuss", 0) == 0 || type.rfind("cpu", 0) == 0) zones_.push_back((e.path() / "temp").string());
|
||||
}
|
||||
}
|
||||
bool available() const { return !zones_.empty(); }
|
||||
// The hottest zone now, in degrees C (0 when there are none).
|
||||
float hottest() const {
|
||||
float best = 0;
|
||||
for (const std::string& z : zones_) {
|
||||
FILE* f = std::fopen(z.c_str(), "r");
|
||||
if (!f) continue;
|
||||
long milli = 0;
|
||||
if (std::fscanf(f, "%ld", &milli) == 1) best = std::max(best, milli / 1000.0f);
|
||||
std::fclose(f);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::string> zones_;
|
||||
};
|
||||
|
||||
// Holds the temperature at a limit by adding foveation strength (coarser toward the edges of view, where you aren't
|
||||
// looking: the cheapest work to give up) a little at a time while it's above, and handing it back once it's below.
|
||||
// The headset heats and cools over minutes, so it steps slowly and waits while the temperature is already falling
|
||||
// (the last step is still taking effect): otherwise it overshoots to the strongest setting and stays there. Smoothed so
|
||||
// a passing spike doesn't move it; each step small enough that the change isn't seen.
|
||||
class ThermalGovernor {
|
||||
public:
|
||||
// limit: degrees C to stay at or under (0: off). Called about once a second.
|
||||
float update(float tempC, float limit, float dtSec) {
|
||||
if (limit <= 0 || tempC <= 0) { extra_ = 0; return 0; }
|
||||
smooth_ = smooth_ <= 0 ? tempC : smooth_ + (tempC - smooth_) * std::min(1.0f, dtSec / 8.0f);
|
||||
trend_ = trend_ <= 0 ? smooth_ : trend_ + (smooth_ - trend_) * std::min(1.0f, dtSec / 30.0f); // ~30 s behind
|
||||
const bool falling = smooth_ < trend_ - 0.2f;
|
||||
since_ += dtSec;
|
||||
if (smooth_ > limit + 0.5f && !falling && since_ >= 6.0f) { // hot and not yet easing: a little more off the edges
|
||||
extra_ = std::min(kMax, extra_ + 0.0625f);
|
||||
since_ = 0;
|
||||
} else if (smooth_ < limit - 1.0f && extra_ > 0 && since_ >= 8.0f) { // under the limit again: give it back
|
||||
extra_ = std::max(0.0f, extra_ - 0.0625f);
|
||||
since_ = 0;
|
||||
}
|
||||
return extra_;
|
||||
}
|
||||
float extra() const { return extra_; }
|
||||
float smoothed() const { return smooth_; }
|
||||
static constexpr float kMax = 2.0f; // foveation strength it may add (on top of the setting)
|
||||
|
||||
private:
|
||||
float extra_ = 0, smooth_ = 0, trend_ = 0, since_ = 0;
|
||||
};
|
||||
@@ -0,0 +1,474 @@
|
||||
#pragma once
|
||||
// The mesh optimizer's visibility pass (Frame Studio's save, fenv-tool): geometry the player can never see is left out
|
||||
// of the baked scene. Seen from where: everywhere the player can be (every walkable floor of the walk grid and a band
|
||||
// past its edges for leaning and room-scale steps, spawns, seats, teleport spots), at eye heights from crouching to
|
||||
// tiptoe, in every direction (a cube of six 90-degree views per spot). From each spot the solid, still geometry is
|
||||
// drawn into a depth cube (a CPU rasterizer); a triangle is seen when its screen box, widened a pixel, reaches past
|
||||
// that depth anywhere (conservative: coverage is the box, depth its nearest corner, the occluders shrunk by a pixel).
|
||||
// What no spot sees goes: single triangles of still, solid meshes (the backs of walls, under floors, behind them),
|
||||
// and whole meshes of anything else (see-through, cut-out, moved by its shader) whose widened bounds no spot sees.
|
||||
// The seen triangles' neighbours stay too (a ring round everything seen). Never touched: the sky, animated, skinned
|
||||
// and edited meshes. Vertices aren't removed (a shader may number them): only index lists shrink.
|
||||
#include "hsl_scene.h"
|
||||
#include "walkgrid.h"
|
||||
#include "home_edits.h"
|
||||
#include "core/parallel.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct VisibilityResult {
|
||||
std::vector<std::vector<u32>> indices; // per mesh: what's kept (valid where changed[m])
|
||||
std::vector<char> changed;
|
||||
size_t samples = 0, trisBefore = 0, trisAfter = 0, meshesEmptied = 0, meshesTouched = 0;
|
||||
double ms = 0;
|
||||
std::string skipped; // why nothing was done ("" when it ran)
|
||||
};
|
||||
|
||||
namespace visbake {
|
||||
|
||||
// Whether a vertex shader puts vertices where the mesh data says (and leaves them there): it reads no clock, view or
|
||||
// other per-frame value (only clipFromWorld, the camera position for fog, reverseZ) and no texture (vertex animation
|
||||
// textures). A bone palette it may read (vista's ibl skins every mesh) is identity for a mesh nothing animates: the
|
||||
// caller rules out the animated ones. SPIR-V scan of its access chains.
|
||||
inline bool staticVertexShader(const std::vector<u32>& code) {
|
||||
if (code.size() < 5 || code[0] != 0x07230203u) return false;
|
||||
std::map<u32, u32> set, binding, pointee, varPtr, varStorage, constant;
|
||||
std::map<std::pair<u32, u32>, u32> memberOffset;
|
||||
for (size_t i = 5; i < code.size();) {
|
||||
const u32 words = code[i] >> 16, op = code[i] & 0xFFFF;
|
||||
if (!words || i + words > code.size()) return false;
|
||||
const u32* a = &code[i + 1];
|
||||
if ((op >= 87 && op <= 98) || (op >= 305 && op <= 320)) return false; // image sampling / fetch / read
|
||||
if (op == 71 && words >= 4 && a[1] == 34) set[a[0]] = a[2]; // DescriptorSet
|
||||
if (op == 71 && words >= 4 && a[1] == 33) binding[a[0]] = a[2]; // Binding
|
||||
if (op == 72 && words >= 5 && a[2] == 35) memberOffset[{a[0], a[1]}] = a[3]; // member Offset
|
||||
if (op == 32 && words >= 4) pointee[a[0]] = a[2]; // OpTypePointer
|
||||
if (op == 43 && words >= 4) constant[a[1]] = a[2]; // OpConstant
|
||||
if (op == 59 && words >= 4) { varPtr[a[1]] = a[0]; varStorage[a[1]] = a[2]; } // OpVariable
|
||||
i += words;
|
||||
}
|
||||
u32 globals = 0;
|
||||
for (const auto& v : varStorage)
|
||||
if (v.second == 2 && set.count(v.first) && set[v.first] == 0 && binding.count(v.first) && binding[v.first] == 0) globals = v.first;
|
||||
const u32 globalsStruct = globals ? pointee[varPtr[globals]] : 0;
|
||||
for (size_t i = 5; i < code.size();) {
|
||||
const u32 words = code[i] >> 16, op = code[i] & 0xFFFF;
|
||||
const u32* a = &code[i + 1];
|
||||
if ((op == 65 || op == 66) && words >= 4) { // OpAccessChain
|
||||
const u32 base = a[2];
|
||||
const auto st = varStorage.find(base);
|
||||
if (st != varStorage.end()) {
|
||||
if (base == globals) {
|
||||
if (words < 5 || !constant.count(a[3])) return false;
|
||||
const auto off = memberOffset.find({globalsStruct, constant[a[3]]});
|
||||
if (off == memberOffset.end()) return false;
|
||||
if (off->second != 0 && off->second != 384 && off->second != 604) return false; // clipFromWorld, worldCameraPos, reverseZ
|
||||
}
|
||||
}
|
||||
}
|
||||
i += words;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Whether a fragment shader leaves coverage and depth to the rasterizer (no discard, no depth or mask written): an
|
||||
// opaque mesh drawn with it hides what's behind it everywhere it covers.
|
||||
inline bool solidFragmentShader(const std::vector<u32>& code) {
|
||||
if (code.size() < 5 || code[0] != 0x07230203u) return false;
|
||||
for (size_t i = 5; i < code.size();) {
|
||||
const u32 words = code[i] >> 16, op = code[i] & 0xFFFF;
|
||||
if (!words || i + words > code.size()) return false;
|
||||
if (op == 252 || op == 4416 || op == 5380) return false;
|
||||
if ((op == 71 && words >= 4 && code[i + 2] == 11 && (code[i + 3] == 22 || code[i + 3] == 20)) ||
|
||||
(op == 72 && words >= 5 && code[i + 3] == 11 && (code[i + 4] == 22 || code[i + 4] == 20)))
|
||||
return false;
|
||||
i += words;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
constexpr float kNear = 0.02f;
|
||||
|
||||
// Cube face f (0..5: +X -X +Y -Y +Z -Z) of a point v relative to the eye: its depth along the face's axis, and the two
|
||||
// across (u, w). In view when |u|, |w| <= depth.
|
||||
inline float faceOf(const float* v, int f, float& u, float& w) {
|
||||
const int ax = f >> 1;
|
||||
u = v[(ax + 1) % 3];
|
||||
w = v[(ax + 2) % 3];
|
||||
return (f & 1) ? -v[ax] : v[ax];
|
||||
}
|
||||
|
||||
// One face's depth: the nearest occluder per pixel, then the "far" pyramid the tests read (level 0: the farthest of each
|
||||
// pixel's 3 x 3, so an occluder edge can't hide what a slightly different eye would see past it; then halves, farthest).
|
||||
struct FaceDepth {
|
||||
int n = 0, levels = 0;
|
||||
std::vector<float> z;
|
||||
std::vector<std::vector<float>> farZ;
|
||||
void reset(int size) {
|
||||
n = size;
|
||||
z.assign(static_cast<size_t>(n) * n, std::numeric_limits<float>::infinity());
|
||||
levels = 0;
|
||||
for (int s = n; s >= 1; s /= 2) ++levels;
|
||||
farZ.resize(levels);
|
||||
}
|
||||
void build() {
|
||||
farZ[0].resize(z.size());
|
||||
for (int y = 0; y < n; ++y)
|
||||
for (int x = 0; x < n; ++x) {
|
||||
float m = 0;
|
||||
for (int j = std::max(0, y - 1); j <= std::min(n - 1, y + 1); ++j)
|
||||
for (int i = std::max(0, x - 1); i <= std::min(n - 1, x + 1); ++i) m = std::max(m, z[static_cast<size_t>(j) * n + i]);
|
||||
farZ[0][static_cast<size_t>(y) * n + x] = m;
|
||||
}
|
||||
for (int l = 1, s = n / 2; l < levels; ++l, s /= 2) {
|
||||
const int ps = s * 2;
|
||||
farZ[l].resize(static_cast<size_t>(s) * s);
|
||||
for (int y = 0; y < s; ++y)
|
||||
for (int x = 0; x < s; ++x) {
|
||||
const float* a = &farZ[l - 1][static_cast<size_t>(y * 2) * ps + x * 2];
|
||||
farZ[l][static_cast<size_t>(y) * s + x] = std::max(std::max(a[0], a[1]), std::max(a[ps], a[ps + 1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Whether anything in pixels [x0, x1] x [y0, y1] lies farther than depth d (something there at d could be seen).
|
||||
bool farther(int x0, int y0, int x1, int y1, float d) const {
|
||||
int l = 0;
|
||||
while (l + 1 < levels && ((x1 >> l) - (x0 >> l) > 2 || (y1 >> l) - (y0 >> l) > 2)) ++l;
|
||||
const int s = n >> l;
|
||||
const float lim = d * (1.0f - 1e-4f) - 1e-4f;
|
||||
for (int y = y0 >> l; y <= (y1 >> l) && y < s; ++y)
|
||||
for (int x = x0 >> l; x <= (x1 >> l) && x < s; ++x)
|
||||
if (farZ[l][static_cast<size_t>(y) * s + x] > lim) return true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Whether points (eye-relative) are all off face f's view on the same side (in front of the eye too: depth >= kNear).
|
||||
inline bool outsideFace(const float (*v)[3], int count, int f) {
|
||||
int out[5] = {0, 0, 0, 0, 0};
|
||||
for (int k = 0; k < count; ++k) {
|
||||
float u, w;
|
||||
const float d = faceOf(v[k], f, u, w);
|
||||
out[0] += d < kNear; out[1] += u > d; out[2] += u < -d; out[3] += w > d; out[4] += w < -d;
|
||||
}
|
||||
for (int k = 0; k < 5; ++k) if (out[k] == count) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// A polygon (eye-relative points) clipped to depth >= kNear on face f, as screen points (pixels) and depths.
|
||||
inline int clipToFace(const float (*v)[3], int count, int f, int n, float (*out)[3]) {
|
||||
float in[8][3], d[8];
|
||||
for (int k = 0; k < count; ++k) {
|
||||
float u, w;
|
||||
d[k] = faceOf(v[k], f, u, w);
|
||||
in[k][0] = u; in[k][1] = w; in[k][2] = d[k];
|
||||
}
|
||||
int m = 0;
|
||||
for (int k = 0; k < count; ++k) {
|
||||
const int j = (k + 1) % count;
|
||||
const bool a = d[k] >= kNear, b = d[j] >= kNear;
|
||||
if (a) { std::memcpy(out[m], in[k], sizeof out[m]); ++m; }
|
||||
if (a != b) {
|
||||
const float t = (kNear - d[k]) / (d[j] - d[k]);
|
||||
for (int c = 0; c < 3; ++c) out[m][c] = in[k][c] + (in[j][c] - in[k][c]) * t;
|
||||
out[m][2] = kNear;
|
||||
++m;
|
||||
}
|
||||
}
|
||||
const float h = n * 0.5f;
|
||||
for (int k = 0; k < m; ++k) { out[k][0] = (out[k][0] / out[k][2] + 1) * h; out[k][1] = (out[k][1] / out[k][2] + 1) * h; }
|
||||
return m;
|
||||
}
|
||||
|
||||
// Fills an occluder triangle (eye-relative) into face f's nearest depths (pixel centres inside, depth from 1/z).
|
||||
inline void rasterOccluder(const float (*v)[3], int f, FaceDepth& fd) {
|
||||
float p[8][3];
|
||||
const int m = clipToFace(v, 3, f, fd.n, p);
|
||||
for (int k = 1; k + 1 < m; ++k) {
|
||||
const float* a = p[0];
|
||||
const float* b = p[k];
|
||||
const float* c = p[k + 1];
|
||||
float area = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
||||
if (std::fabs(area) < 1e-6f) continue;
|
||||
const int x0 = std::max(0, static_cast<int>(std::floor(std::min({a[0], b[0], c[0]})))), x1 = std::min(fd.n - 1, static_cast<int>(std::ceil(std::max({a[0], b[0], c[0]}))));
|
||||
const int y0 = std::max(0, static_cast<int>(std::floor(std::min({a[1], b[1], c[1]})))), y1 = std::min(fd.n - 1, static_cast<int>(std::ceil(std::max({a[1], b[1], c[1]}))));
|
||||
if (x0 > x1 || y0 > y1) continue;
|
||||
const float ia = 1 / area, za = 1 / a[2], zb = 1 / b[2], zc = 1 / c[2];
|
||||
for (int y = y0; y <= y1; ++y) {
|
||||
const float py = y + 0.5f;
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const float px = x + 0.5f;
|
||||
const float w0 = ((b[0] - px) * (c[1] - py) - (b[1] - py) * (c[0] - px)) * ia;
|
||||
const float w1 = ((c[0] - px) * (a[1] - py) - (c[1] - py) * (a[0] - px)) * ia;
|
||||
const float w2 = 1 - w0 - w1;
|
||||
if (w0 < 0 || w1 < 0 || w2 < 0) continue;
|
||||
const float iz = w0 * za + w1 * zb + w2 * zc;
|
||||
if (iz <= 0) continue;
|
||||
float& dst = fd.z[static_cast<size_t>(y) * fd.n + x];
|
||||
dst = std::min(dst, 1 / iz);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Whether a polygon (eye-relative: a triangle, or a box's 8 corners as a cloud) can be seen on face f: its screen box
|
||||
// widened a pixel reaches something farther than its nearest point. Partly behind the eye: seen if on the face at all.
|
||||
inline bool seenOnFace(const float (*v)[3], int count, int f, const FaceDepth& fd, bool cloud) {
|
||||
float u, w;
|
||||
float minD = std::numeric_limits<float>::infinity();
|
||||
bool behind = false;
|
||||
float lo[2] = {1e30f, 1e30f}, hi[2] = {-1e30f, -1e30f};
|
||||
const float h = fd.n * 0.5f;
|
||||
for (int k = 0; k < count; ++k) {
|
||||
const float d = faceOf(v[k], f, u, w);
|
||||
if (d < kNear) { behind = true; continue; }
|
||||
minD = std::min(minD, d);
|
||||
const float sx = (u / d + 1) * h, sy = (w / d + 1) * h;
|
||||
lo[0] = std::min(lo[0], sx); hi[0] = std::max(hi[0], sx);
|
||||
lo[1] = std::min(lo[1], sy); hi[1] = std::max(hi[1], sy);
|
||||
}
|
||||
if (behind) {
|
||||
if (cloud) return true; // a box round the eye, or reaching behind it: seen
|
||||
float p[8][3];
|
||||
const int m = clipToFace(v, count, f, fd.n, p);
|
||||
if (m < 3) return false;
|
||||
lo[0] = lo[1] = 1e30f; hi[0] = hi[1] = -1e30f;
|
||||
for (int k = 0; k < m; ++k) { lo[0] = std::min(lo[0], p[k][0]); hi[0] = std::max(hi[0], p[k][0]); lo[1] = std::min(lo[1], p[k][1]); hi[1] = std::max(hi[1], p[k][1]); }
|
||||
minD = kNear;
|
||||
}
|
||||
const int x0 = std::max(0, static_cast<int>(std::floor(lo[0])) - 1), x1 = std::min(fd.n - 1, static_cast<int>(std::floor(hi[0])) + 1);
|
||||
const int y0 = std::max(0, static_cast<int>(std::floor(lo[1])) - 1), y1 = std::min(fd.n - 1, static_cast<int>(std::floor(hi[1])) + 1);
|
||||
if (x0 > x1 || y0 > y1) return false;
|
||||
return fd.farther(x0, y0, x1, y1, minD);
|
||||
}
|
||||
|
||||
// Whether an eye at height y over cell (cx, cz) is out in the open: off the grid, or clear of every solid span there.
|
||||
inline bool eyeFree(const WalkGrid& g, int cx, int cz, float y) {
|
||||
if (!g.inside(cx, cz)) return true;
|
||||
for (const WalkSpan* s = g.begin(cx, cz); s != g.end(cx, cz); ++s)
|
||||
if (y >= s->lo - 0.08f && y <= s->hi + 0.08f) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace visbake
|
||||
|
||||
struct VisibilityOptions {
|
||||
float spacing = 0.35f; // between spots on the floor (m); more where the area ends
|
||||
float margin = 0.4f; // how far past the area's edges the head goes (leaning, a step in the room)
|
||||
float heights[4] = {0.35f, 0.9f, 1.45f, 2.0f}; // eye heights over the floor: crouching to tiptoe
|
||||
int face = 256; // depth cube face size (pixels; a power of two)
|
||||
size_t maxSpots = 9000; // more than this: wider spacing
|
||||
std::vector<int> pinned; // meshes kept whole besides the animated ones (all a CPU animation writes)
|
||||
};
|
||||
|
||||
// The pass. `grid`: the home's walkable area (walkWorld); `edits`: Frame Studio's object edits (their meshes are kept
|
||||
// whole and hide nothing). False (result.skipped says why) when it can't vouch for the home.
|
||||
inline bool bakeVisibility(const HslScene& scene, const HomeWorld& world, const WalkGrid* grid, const std::vector<ObjectEdit>* edits,
|
||||
VisibilityResult& out, const VisibilityOptions& opt = VisibilityOptions()) {
|
||||
using namespace visbake;
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
out = VisibilityResult();
|
||||
const size_t nm = scene.meshes.size();
|
||||
out.indices.resize(nm);
|
||||
out.changed.assign(nm, 0);
|
||||
if (!grid || grid->walkArea <= 0) { out.skipped = "no walkable area"; return false; }
|
||||
if (!world.gravity.empty()) { out.skipped = "gravity zones (you can stand on walls)"; return false; }
|
||||
|
||||
// What each mesh is: 2 still and solid (triangles go, and it hides what's behind it), 1 still but see-through or
|
||||
// cut-out (goes whole, hides nothing), 0 kept whole.
|
||||
std::vector<char> kind(nm, 0);
|
||||
std::vector<char> pinned(nm, 0);
|
||||
for (int m : scene.animatedMeshes) if (m >= 0 && static_cast<size_t>(m) < nm) pinned[m] = 1;
|
||||
for (int m : opt.pinned) if (m >= 0 && static_cast<size_t>(m) < nm) pinned[m] = 1;
|
||||
for (const HslAnimGroup& a : scene.anims) for (int m : a.meshes) if (m >= 0 && static_cast<size_t>(m) < nm) pinned[m] = 1;
|
||||
if (edits) for (const ObjectEdit& e : *edits) if (e.index >= 0 && static_cast<size_t>(e.index) < nm) pinned[e.index] = 1;
|
||||
std::map<std::string, std::pair<bool, bool>> progKind; // shader path -> (still vertices, solid fragments)
|
||||
for (size_t m = 0; m < nm; ++m) {
|
||||
const MeshData& md = scene.meshes[m];
|
||||
if (pinned[m] || md.isSkybox || md.overlayKind || md.dynamicVerts || md.hasVat || !md.skinJoints.empty() || md.indices.size() < 3 ||
|
||||
md.positions.empty())
|
||||
continue;
|
||||
auto it = progKind.find(md.shaderPath);
|
||||
if (it == progKind.end()) {
|
||||
const ShaderProgramSrc* p = findProgram(scene, md.shaderPath);
|
||||
std::string path = md.shaderPath;
|
||||
for (char& c : path) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
const bool vege = path.find("vege") != std::string::npos || path.find("veg_") != std::string::npos || path.find("billboard") != std::string::npos;
|
||||
it = progKind.emplace(md.shaderPath, std::make_pair(p && !vege && staticVertexShader(p->vert), p && solidFragmentShader(p->frag))).first;
|
||||
}
|
||||
if (!it->second.first) continue;
|
||||
const bool solid = !md.useBlend && !md.additive && !md.alphaTest && !md.alphaToCoverage && it->second.second;
|
||||
kind[m] = solid ? 2 : 1;
|
||||
}
|
||||
|
||||
// World-space triangles: the solid ones (occluders, tested one by one), boxes for the rest.
|
||||
struct Tri { float v[3][3]; u32 mesh, index; };
|
||||
std::vector<Tri> tris;
|
||||
std::vector<std::array<float, 6>> boxes(nm);
|
||||
std::vector<size_t> firstTri(nm + 1, 0);
|
||||
for (size_t m = 0; m < nm; ++m) {
|
||||
firstTri[m] = tris.size();
|
||||
const MeshData& md = scene.meshes[m];
|
||||
out.trisBefore += md.indices.size() / 3;
|
||||
if (!kind[m]) continue;
|
||||
float wm[16];
|
||||
if (md.hasWorldMatrix) std::memcpy(wm, md.worldMatrix, sizeof wm); else trsToMat4(md.transform, wm);
|
||||
auto xf = [&](u32 vi, float* o) {
|
||||
const float* p = &md.positions[static_cast<size_t>(vi) * 3];
|
||||
for (int j = 0; j < 3; ++j) o[j] = wm[j] * p[0] + wm[4 + j] * p[1] + wm[8 + j] * p[2] + wm[12 + j];
|
||||
};
|
||||
std::array<float, 6> bb = {1e30f, 1e30f, 1e30f, -1e30f, -1e30f, -1e30f};
|
||||
for (size_t t = 0; t + 2 < md.indices.size(); t += 3) {
|
||||
Tri tr;
|
||||
bool ok = true;
|
||||
for (int k = 0; k < 3 && ok; ++k) {
|
||||
const u32 vi = md.indices[t + k];
|
||||
if (static_cast<size_t>(vi) * 3 + 2 >= md.positions.size()) { ok = false; break; }
|
||||
xf(vi, tr.v[k]);
|
||||
for (int j = 0; j < 3; ++j) { bb[j] = std::min(bb[j], tr.v[k][j]); bb[3 + j] = std::max(bb[3 + j], tr.v[k][j]); }
|
||||
}
|
||||
if (!ok) { kind[m] = 0; break; }
|
||||
if (kind[m] == 2) { tr.mesh = static_cast<u32>(m); tr.index = static_cast<u32>(t); tris.push_back(tr); }
|
||||
}
|
||||
if (!kind[m]) { tris.resize(firstTri[m]); continue; }
|
||||
// a see-through / cut-out mesh's box, widened (a little sway, an offset the material adds)
|
||||
for (int j = 0; j < 3; ++j) { const float g = 0.25f + 0.1f * (bb[3 + j] - bb[j]); bb[j] -= g; bb[3 + j] += g; }
|
||||
boxes[m] = bb;
|
||||
}
|
||||
firstTri[nm] = tris.size();
|
||||
|
||||
// Where the eyes can be.
|
||||
const WalkParams& P = grid->p;
|
||||
std::vector<std::array<float, 3>> spots;
|
||||
auto addHeights = [&](float x, float y, float z, const float* hs, int n) { for (int k = 0; k < n; ++k) spots.push_back({x, y + hs[k], z}); };
|
||||
float spacing = opt.spacing;
|
||||
for (int pass = 0; pass < 6; ++pass) {
|
||||
spots.clear();
|
||||
const int step = std::max(1, static_cast<int>(std::lround(spacing / P.cell)));
|
||||
for (int cz = 0; cz < grid->nz; ++cz)
|
||||
for (int cx = 0; cx < grid->nx; ++cx)
|
||||
for (const WalkSpan* s = grid->begin(cx, cz); s != grid->end(cx, cz); ++s) {
|
||||
if (!(s->flags & kWalkOk)) continue;
|
||||
static const int nb[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
bool edge = false;
|
||||
for (const auto& d : nb)
|
||||
if (!grid->walkNear(cx + d[0], cz + d[1], s->hi, P.step, kWalkOk)) {
|
||||
edge = true;
|
||||
// past the edge: the head over it (a lean, a step in the room), as far as the margin goes
|
||||
// before the eye would be in something (a wall, the sofa: from inside, everything shows)
|
||||
if ((cx + cz) % std::max(1, step / 2) != 0) continue;
|
||||
const int reach = std::max(1, static_cast<int>(std::lround(opt.margin / P.cell)));
|
||||
for (float hh : opt.heights) {
|
||||
const float y = s->hi + hh;
|
||||
int k = 0;
|
||||
while (k < reach && eyeFree(*grid, cx + d[0] * (k + 1), cz + d[1] * (k + 1), y)) ++k;
|
||||
if (k > 0) spots.push_back({grid->centerX(cx + d[0] * k), y, grid->centerZ(cz + d[1] * k)});
|
||||
}
|
||||
}
|
||||
if (edge ? (cx + cz) % std::max(1, step / 2) == 0 : (cx % step == 0 && cz % step == 0))
|
||||
addHeights(grid->centerX(cx), s->hi, grid->centerZ(cz), opt.heights, 4);
|
||||
}
|
||||
if (spots.size() <= opt.maxSpots) break;
|
||||
spacing *= std::sqrt(static_cast<float>(spots.size()) / opt.maxSpots) * 1.05f;
|
||||
}
|
||||
static const float seatH[3] = {0.5f, 0.8f, 1.1f};
|
||||
for (const WorldPoint& p : world.seats) addHeights(p.pos[0], p.pos[1], p.pos[2], seatH, 3);
|
||||
for (const auto* list : {&world.spawns, &world.hotspots})
|
||||
for (const WorldPoint& p : *list) addHeights(p.pos[0], p.pos[1], p.pos[2], opt.heights, 4);
|
||||
if (const char* e = std::getenv("FH_VIS_SPOTS")) { // diagnostics: only the first n spots (spread evenly)
|
||||
const size_t n = std::max<size_t>(1, std::strtoul(e, nullptr, 10));
|
||||
if (n < spots.size()) { std::vector<std::array<float, 3>> few; for (size_t i = 0; i < n; ++i) few.push_back(spots[i * spots.size() / n]); spots.swap(few); }
|
||||
}
|
||||
out.samples = spots.size();
|
||||
if (spots.empty()) { out.skipped = "nowhere to stand"; return false; }
|
||||
|
||||
// From every spot, six faces: occluders in, then what's seen marked.
|
||||
std::unique_ptr<std::atomic<uint8_t>[]> seenTri(new std::atomic<uint8_t>[tris.size() ? tris.size() : 1]);
|
||||
std::unique_ptr<std::atomic<uint8_t>[]> seenMesh(new std::atomic<uint8_t>[nm ? nm : 1]);
|
||||
for (size_t i = 0; i < tris.size(); ++i) seenTri[i].store(0, std::memory_order_relaxed);
|
||||
for (size_t i = 0; i < nm; ++i) seenMesh[i].store(0, std::memory_order_relaxed);
|
||||
const int faceN = opt.face;
|
||||
par::forEach(spots.size(), [&](size_t si) {
|
||||
thread_local FaceDepth fds[6];
|
||||
const float* e = spots[si].data();
|
||||
for (int f = 0; f < 6; ++f) fds[f].reset(faceN);
|
||||
float v[8][3];
|
||||
for (const Tri& t : tris) {
|
||||
for (int k = 0; k < 3; ++k) for (int j = 0; j < 3; ++j) v[k][j] = t.v[k][j] - e[j];
|
||||
for (int f = 0; f < 6; ++f) if (!outsideFace(v, 3, f)) rasterOccluder(v, f, fds[f]);
|
||||
}
|
||||
for (int f = 0; f < 6; ++f) fds[f].build();
|
||||
for (size_t i = 0; i < tris.size(); ++i) {
|
||||
if (seenTri[i].load(std::memory_order_relaxed)) continue;
|
||||
for (int k = 0; k < 3; ++k) for (int j = 0; j < 3; ++j) v[k][j] = tris[i].v[k][j] - e[j];
|
||||
for (int f = 0; f < 6; ++f)
|
||||
if (!outsideFace(v, 3, f) && seenOnFace(v, 3, f, fds[f], false)) { seenTri[i].store(1, std::memory_order_relaxed); break; }
|
||||
}
|
||||
for (size_t m = 0; m < nm; ++m) {
|
||||
if (kind[m] != 1 || seenMesh[m].load(std::memory_order_relaxed)) continue;
|
||||
const auto& b = boxes[m];
|
||||
if (e[0] >= b[0] && e[0] <= b[3] && e[1] >= b[1] && e[1] <= b[4] && e[2] >= b[2] && e[2] <= b[5]) { seenMesh[m].store(1); continue; }
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
v[k][0] = b[(k & 1) ? 3 : 0] - e[0];
|
||||
v[k][1] = b[(k & 2) ? 4 : 1] - e[1];
|
||||
v[k][2] = b[(k & 4) ? 5 : 2] - e[2];
|
||||
}
|
||||
for (int f = 0; f < 6; ++f)
|
||||
if (!outsideFace(v, 8, f) && seenOnFace(v, 8, f, fds[f], true)) { seenMesh[m].store(1, std::memory_order_relaxed); break; }
|
||||
}
|
||||
});
|
||||
|
||||
// What's kept: the seen triangles and one ring round them (triangles touching a seen one's corner), seen meshes.
|
||||
for (size_t m = 0; m < nm; ++m) {
|
||||
const MeshData& md = scene.meshes[m];
|
||||
if (kind[m] == 1 && !seenMesh[m].load()) {
|
||||
out.changed[m] = 1;
|
||||
++out.meshesEmptied;
|
||||
continue;
|
||||
}
|
||||
if (kind[m] != 2) { out.trisAfter += md.indices.size() / 3; continue; }
|
||||
std::set<std::array<float, 3>> corners;
|
||||
bool all = true;
|
||||
for (size_t i = firstTri[m]; i < firstTri[m + 1]; ++i) {
|
||||
if (!seenTri[i].load()) { all = false; continue; }
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const float* p = &md.positions[static_cast<size_t>(md.indices[tris[i].index + k]) * 3];
|
||||
corners.insert({p[0], p[1], p[2]});
|
||||
}
|
||||
}
|
||||
if (all) { out.trisAfter += md.indices.size() / 3; continue; }
|
||||
std::vector<u32>& keep = out.indices[m];
|
||||
for (size_t i = firstTri[m]; i < firstTri[m + 1]; ++i) {
|
||||
const u32 t = tris[i].index;
|
||||
bool k = seenTri[i].load() != 0;
|
||||
static const bool noRing = std::getenv("FH_VIS_NORING") != nullptr; // diagnostics: the seen ones alone
|
||||
for (int c = 0; c < 3 && !k && !noRing; ++c) {
|
||||
const float* p = &md.positions[static_cast<size_t>(md.indices[t + c]) * 3];
|
||||
k = corners.count({p[0], p[1], p[2]}) != 0;
|
||||
}
|
||||
if (k) keep.insert(keep.end(), md.indices.begin() + t, md.indices.begin() + t + 3);
|
||||
}
|
||||
out.changed[m] = 1;
|
||||
out.trisAfter += keep.size() / 3;
|
||||
if (keep.empty()) ++out.meshesEmptied; else ++out.meshesTouched;
|
||||
}
|
||||
out.ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
|
||||
if (std::getenv("FH_VIS_DEBUG")) // per mesh: what it was taken for and what's left
|
||||
for (size_t m = 0; m < nm; ++m) {
|
||||
const MeshData& md = scene.meshes[m];
|
||||
std::fprintf(stderr, "[optimize] %-50s %s %6zu -> %6zu tris [%s]\n", md.name.c_str(), kind[m] == 2 ? "solid" : kind[m] == 1 ? "whole" : "kept ",
|
||||
md.indices.size() / 3, out.changed[m] ? out.indices[m].size() / 3 : md.indices.size() / 3, md.shaderPath.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+1601
File diff suppressed because it is too large.
Load diff
+1069
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,454 @@
|
||||
#pragma once
|
||||
// The world layer at run time: what the player stands on and bumps into. Built once per home from HomeWorld:
|
||||
// floors — upward faces of floor + walls surfaces, everything not too steep on walkable floors, box tops
|
||||
// walls — steep faces of floor + walls surfaces, every face of wall surfaces (a Wall part is all wall, whatever
|
||||
// its slope: Studio's Wall + Floor split leaves 40-45 degree faces in it), plus box / sphere / capsule
|
||||
// colliders that block
|
||||
// (each collider's role says which it gives: see WorldCollider::Role; triggers give neither).
|
||||
// Both sit in a 1 m grid over XZ, so a query touches a handful of triangles instead of hundreds of thousands.
|
||||
// A world with a walkable area (HomeWorld::walkGrid / walkGridCache, src/walkgrid.h) answers from it instead: floors,
|
||||
// walking, edges and teleport landings all come from the one baked area (seams bridged, furniture and ledges
|
||||
// blocked, the body's radius kept from walls), so Frame Home walks exactly what Frame Studio shows. Box / sphere /
|
||||
// capsule colliders still report which one a teleport arc hit (seats).
|
||||
// Frame Home walks and teleports with it; Frame Studio previews the same answers.
|
||||
#include "core/world.h"
|
||||
#include "walkgrid.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class WorldRuntime {
|
||||
public:
|
||||
struct Tri { float a[3], b[3], c[3]; float n[3]; bool solid = false; }; // solid: a floor that blocks from below
|
||||
|
||||
void build(const HomeWorld& w) {
|
||||
floors_.clear(); walls_.clear(); floorGrid_.clear(); wallGrid_.clear(); shapes_.clear();
|
||||
grid_ = w.walkGridCache;
|
||||
if (!grid_ && !w.walkGrid.empty()) grid_ = decodeWalkGrid(w.walkGrid);
|
||||
if (grid_ && grid_->any()) {
|
||||
for (const WorldCollider& c : w.colliders)
|
||||
if (c.kind != WorldCollider::Mesh && (c.role == WorldCollider::Solid || c.role == WorldCollider::Wall || c.role == WorldCollider::Object)) shapes_.push_back(c);
|
||||
hasFloor_ = true;
|
||||
return;
|
||||
}
|
||||
grid_.reset();
|
||||
// floorUp: how upright a face must be to stand on (0 = never floor); walls: steep faces block.
|
||||
// allWall: every face blocks (the Wall role), not just the steep ones.
|
||||
auto addMesh = [&](const std::vector<float>& p, const std::vector<u32>& idx, float floorUp, bool walls, bool solidFloor, bool allWall = false) {
|
||||
const size_t nv = p.size() / 3;
|
||||
for (size_t t = 0; t + 2 < idx.size(); t += 3) {
|
||||
if (idx[t] >= nv || idx[t + 1] >= nv || idx[t + 2] >= nv) continue;
|
||||
Tri tr;
|
||||
std::copy_n(&p[idx[t] * 3], 3, tr.a); std::copy_n(&p[idx[t + 1] * 3], 3, tr.b); std::copy_n(&p[idx[t + 2] * 3], 3, tr.c);
|
||||
const float e1[3] = {tr.b[0] - tr.a[0], tr.b[1] - tr.a[1], tr.b[2] - tr.a[2]}, e2[3] = {tr.c[0] - tr.a[0], tr.c[1] - tr.a[1], tr.c[2] - tr.a[2]};
|
||||
float n[3] = {e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]};
|
||||
const float L = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
|
||||
if (L < 1e-9f) continue;
|
||||
for (float& v : n) v /= L;
|
||||
std::copy_n(n, 3, tr.n);
|
||||
tr.solid = solidFloor;
|
||||
const float up = std::fabs(n[1]); // winding varies: a floor faces up either way
|
||||
if (floorUp > 0 && up > floorUp) insert(floors_, floorGrid_, tr);
|
||||
else if (walls && (allWall || up < 0.7f)) insert(walls_, wallGrid_, tr);
|
||||
}
|
||||
};
|
||||
if (!w.walkIndices.empty()) addMesh(w.walkPositions, w.walkIndices, 0.01f, false, false); // legacy walkable triangles
|
||||
for (const WorldCollider& c : w.colliders) {
|
||||
if (c.role == WorldCollider::Trigger) continue;
|
||||
const float floorUp = c.role == WorldCollider::Solid ? 0.7f : c.role == WorldCollider::Floor || c.role == WorldCollider::Walkable ? 0.5f : 0.0f;
|
||||
const bool blocks = c.role == WorldCollider::Solid || c.role == WorldCollider::Wall || c.role == WorldCollider::Object;
|
||||
if (c.kind == WorldCollider::Mesh) { addMesh(worldMeshPositions(c), c.indices, floorUp, blocks, blocks, c.role == WorldCollider::Wall || c.role == WorldCollider::Object); continue; }
|
||||
if (blocks) shapes_.push_back(c);
|
||||
if (c.kind == WorldCollider::Box && floorUp > 0) { // a box's top is floor (steps, platforms, rugs)
|
||||
std::vector<float> p;
|
||||
const float s[4][2] = {{-1, -1}, {1, -1}, {1, 1}, {-1, 1}};
|
||||
for (auto& q : s) {
|
||||
const float l[3] = {q[0] * c.half[0], c.half[1], q[1] * c.half[2]};
|
||||
for (int k = 0; k < 3; ++k) p.push_back(c.world[k] * l[0] + c.world[4 + k] * l[1] + c.world[8 + k] * l[2] + c.world[12 + k]);
|
||||
}
|
||||
addMesh(p, {0, 1, 2, 0, 2, 3}, 0.01f, false, blocks);
|
||||
}
|
||||
}
|
||||
hasFloor_ = !floors_.empty();
|
||||
}
|
||||
|
||||
bool hasFloor() const { return hasFloor_; }
|
||||
const WalkGrid* grid() const { return grid_.get(); } // the walkable area answering, if any
|
||||
|
||||
// Highest floor under (x, z) between y - maxDown and y + maxUp; false when there is none.
|
||||
bool floorAt(float x, float z, float y, float maxUp, float maxDown, float& out) const {
|
||||
if (grid_) return grid_->floorAt(x, z, y, maxUp, maxDown, out);
|
||||
bool found = false;
|
||||
float best = -1e30f;
|
||||
forCell(floorGrid_, x, z, [&](uint32_t i) {
|
||||
const Tri& t = floors_[i];
|
||||
float h;
|
||||
if (!heightOn(t, x, z, h) || h > y + maxUp || h < y - maxDown) return;
|
||||
if (h > best) { best = h; found = true; }
|
||||
});
|
||||
if (found) out = best;
|
||||
return found;
|
||||
}
|
||||
|
||||
// Moves a standing body (feet at y, `radius` wide, `height` tall) from (x, z) by (dx, dz), stopping at walls
|
||||
// and sliding along them.
|
||||
void move(float& x, float& z, float y, float dx, float dz, float radius = 0.2f, float height = 1.7f) const {
|
||||
if (grid_) { grid_->move(x, z, y, dx, dz); return; } // (the area keeps the body's radius from edges)
|
||||
for (int pass = 0; pass < 2; ++pass) { // try the full step, then slide
|
||||
const float nx = x + dx, nz = z + dz;
|
||||
float push[3];
|
||||
if (!blocked(nx, y, nz, radius, height, push)) { x = nx; z = nz; return; }
|
||||
// remove the component of the step that goes into the wall
|
||||
const float pl = std::sqrt(push[0] * push[0] + push[2] * push[2]);
|
||||
if (pl < 1e-6f) return;
|
||||
const float px = push[0] / pl, pz = push[2] / pl, into = dx * px + dz * pz;
|
||||
if (into >= 0) return;
|
||||
dx -= into * px; dz -= into * pz;
|
||||
}
|
||||
}
|
||||
|
||||
// First floor hit along a segment (teleport arcs are traced as short segments).
|
||||
bool raycastFloor(const float o[3], const float d[3], float maxT, float hit[3]) const {
|
||||
const float len = std::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
|
||||
if (len < 1e-9f) return false;
|
||||
const int steps = std::max(1, static_cast<int>(maxT * len / 0.5f));
|
||||
float bestT = 1e30f;
|
||||
for (int s = 0; s <= steps; ++s) { // visit the cells the segment crosses
|
||||
const float t = maxT * s / steps;
|
||||
const float x = o[0] + d[0] * t, z = o[2] + d[2] * t;
|
||||
forCell(floorGrid_, x, z, [&](uint32_t i) {
|
||||
float th;
|
||||
if (rayTri(o, d, floors_[i], th) && th >= 0 && th <= maxT && th < bestT) bestT = th;
|
||||
});
|
||||
}
|
||||
if (bestT > maxT) return false;
|
||||
for (int k = 0; k < 3; ++k) hit[k] = o[k] + d[k] * bestT;
|
||||
// a wall in front of the landing blocks it
|
||||
return !wallBetween(o, hit);
|
||||
}
|
||||
|
||||
// What a straight segment o + d t, t in [0, 1], meets first: a floor from above (Land), anything solid — walls,
|
||||
// ceilings and the undersides of solid floors, blocking shapes — (Block), or nothing. Teleport arcs are traced
|
||||
// with it segment by segment, so they stop at the first wall instead of flying through it. n: the surface's
|
||||
// normal facing the segment's start.
|
||||
enum Hit { None = 0, Land = 1, Block = 2 };
|
||||
// hitShape (optional): the box / sphere / capsule collider hit, when that's what stopped it.
|
||||
Hit trace(const float o[3], const float d[3], float& tOut, float n[3], const WorldCollider** hitShape = nullptr) const {
|
||||
if (hitShape) *hitShape = nullptr;
|
||||
float best = 2;
|
||||
Hit kind = None;
|
||||
if (grid_) {
|
||||
kind = static_cast<Hit>(grid_->trace(o, d, best, n));
|
||||
for (const WorldCollider& c : shapes_) { // which chair it was (seats)
|
||||
float th, sn[3];
|
||||
if (!rayShape(c, o, d, th, sn) || th > 1) continue;
|
||||
if (th < best - 1e-4f || (kind != Land && th < best)) { best = th; kind = Block; std::copy_n(sn, 3, n); if (hitShape) *hitShape = &c; }
|
||||
}
|
||||
tOut = best;
|
||||
return kind;
|
||||
}
|
||||
auto consider = [&](const Tri& tr, bool floor) {
|
||||
float th;
|
||||
if (!rayTri(o, d, tr, th) || th < 0 || th > 1 || th >= best) return;
|
||||
const float side = tr.n[0] * d[0] + tr.n[1] * d[1] + tr.n[2] * d[2] < 0 ? 1.0f : -1.0f;
|
||||
const float fn[3] = {tr.n[0] * side, tr.n[1] * side, tr.n[2] * side};
|
||||
Hit k = Block;
|
||||
if (floor) {
|
||||
if (fn[1] > 0) k = Land; // onto it from above
|
||||
else if (!tr.solid) return; // under a walkable area: it doesn't stop you
|
||||
}
|
||||
best = th; kind = k;
|
||||
std::copy_n(fn, 3, n);
|
||||
};
|
||||
walkCells(o, d, [&](int64_t key) {
|
||||
if (auto it = floorGrid_.find(key); it != floorGrid_.end()) for (uint32_t i : it->second) consider(floors_[i], true);
|
||||
if (auto it = wallGrid_.find(key); it != wallGrid_.end()) for (uint32_t i : it->second) consider(walls_[i], false);
|
||||
});
|
||||
for (const auto* g : {&floorGrid_, &wallGrid_}) {
|
||||
auto it = g->find(kBig);
|
||||
if (it == g->end()) continue;
|
||||
for (uint32_t i : it->second) consider(g == &floorGrid_ ? floors_[i] : walls_[i], g == &floorGrid_);
|
||||
}
|
||||
// Blocking shapes. A box's top is also a floor triangle: landing on it wins a tie.
|
||||
for (const WorldCollider& c : shapes_) {
|
||||
float th, sn[3];
|
||||
if (!rayShape(c, o, d, th, sn) || th > 1) continue;
|
||||
if (th < best - 1e-4f || (kind != Land && th < best)) { best = th; kind = Block; std::copy_n(sn, 3, n); if (hitShape) *hitShape = &c; }
|
||||
}
|
||||
tOut = best;
|
||||
return kind;
|
||||
}
|
||||
|
||||
// Where a teleport arc that met something at q (h: what it was, n: its normal facing the arc) puts you, into out:
|
||||
// a floor from above: there, or the nearest place you fit within 30 cm (an edge, a seam);
|
||||
// the top of something you can't stand on (a table, a sofa's back): the floor beside it;
|
||||
// a wall, a railing, furniture's side, at any height: the floor at its foot (in front of it);
|
||||
// a ceiling or a floor's underside: the floor under it.
|
||||
// Each with headroom over it. False: nowhere you fit within reach of it.
|
||||
bool landingFor(Hit h, const float q[3], const float n[3], float out[3]) const {
|
||||
auto standAt = [&](float x, float y, float z, float nudge) {
|
||||
if (!roomAt(x, y, z, nudge)) return false;
|
||||
float f;
|
||||
if (floorAt(x, z, y, 0.1f, 0.1f, f)) y = f;
|
||||
const float from[3] = {x, y + 0.05f, z}, up[3] = {0, 1.2f, 0};
|
||||
float t, hn[3];
|
||||
if (trace(from, up, t, hn) == Block) return false; // no headroom (under a table)
|
||||
out[0] = x; out[1] = y; out[2] = z;
|
||||
return true;
|
||||
};
|
||||
float f;
|
||||
if (h == Land) {
|
||||
if (standAt(q[0], q[1], q[2], 0.3f)) return true;
|
||||
return floorAt(q[0], q[2], q[1] - 0.2f, 0.0f, 2.0f, f) && standAt(q[0], f, q[2], 0.6f); // a top: the floor by it
|
||||
}
|
||||
if (h != Block) return false;
|
||||
if (n[1] < -0.7f) return floorAt(q[0], q[2], q[1] - 0.1f, 0.0f, 3.0f, f) && standAt(q[0], f, q[2], 0.3f); // under a ceiling
|
||||
const float hl = std::sqrt(n[0] * n[0] + n[2] * n[2]);
|
||||
if (hl < 1e-4f) return false;
|
||||
const float x = q[0] + n[0] / hl * 0.3f, z = q[2] + n[2] / hl * 0.3f; // a wall or a side: in front of its foot
|
||||
return floorAt(x, z, q[1], 0.0f, 3.0f, f) && standAt(x, f, z, 0.3f);
|
||||
}
|
||||
|
||||
// Somewhere to stand at (x, y, z): pushed up to `nudge` metres out of walls the body would be in (landing at the
|
||||
// foot of a wall or beside a sofa), still on a floor within a step. False: no room.
|
||||
bool roomAt(float& x, float y, float& z, float nudge = 0.3f) const {
|
||||
if (grid_) return grid_->roomAt(x, y, z, nudge);
|
||||
float push[3];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (!blocked(x, y, z, 0.2f, 1.7f, push)) return true;
|
||||
const float pl = std::sqrt(push[0] * push[0] + push[2] * push[2]);
|
||||
if (pl < 1e-6f) return false;
|
||||
const float nx = x + push[0] / pl * nudge / 3, nz = z + push[2] / pl * nudge / 3;
|
||||
float f;
|
||||
if (!floorAt(nx, nz, y, 0.1f, 0.1f, f)) return false;
|
||||
x = nx; z = nz;
|
||||
}
|
||||
return !blocked(x, y, z, 0.2f, 1.7f, push);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Tri> floors_, walls_;
|
||||
std::unordered_map<int64_t, std::vector<uint32_t>> floorGrid_, wallGrid_;
|
||||
std::vector<WorldCollider> shapes_;
|
||||
std::shared_ptr<const WalkGrid> grid_;
|
||||
bool hasFloor_ = false;
|
||||
|
||||
static int64_t key(int cx, int cz) { return (static_cast<int64_t>(cx) << 32) ^ static_cast<uint32_t>(cz); }
|
||||
static int cell(float v) { return static_cast<int>(std::floor(v)); }
|
||||
|
||||
static void insert(std::vector<Tri>& list, std::unordered_map<int64_t, std::vector<uint32_t>>& grid, const Tri& t) {
|
||||
const uint32_t id = static_cast<uint32_t>(list.size());
|
||||
list.push_back(t);
|
||||
const float x0 = std::min({t.a[0], t.b[0], t.c[0]}), x1 = std::max({t.a[0], t.b[0], t.c[0]});
|
||||
const float z0 = std::min({t.a[2], t.b[2], t.c[2]}), z1 = std::max({t.a[2], t.b[2], t.c[2]});
|
||||
if (x1 - x0 > 400 || z1 - z0 > 400) { grid[kBig].push_back(id); return; } // one huge ground quad: every query sees it
|
||||
for (int cx = cell(x0); cx <= cell(x1); ++cx)
|
||||
for (int cz = cell(z0); cz <= cell(z1); ++cz) grid[key(cx, cz)].push_back(id);
|
||||
}
|
||||
|
||||
static constexpr int64_t kBig = INT64_MIN; // triangles too big to grid
|
||||
template <typename F> static void forCell(const std::unordered_map<int64_t, std::vector<uint32_t>>& grid, float x, float z, F&& f) {
|
||||
auto it = grid.find(key(cell(x), cell(z)));
|
||||
if (it != grid.end()) for (uint32_t i : it->second) f(i);
|
||||
auto big = grid.find(kBig);
|
||||
if (big != grid.end()) for (uint32_t i : big->second) f(i);
|
||||
}
|
||||
|
||||
// Every grid cell the segment o + d t, t in [0, 1], crosses over XZ (Amanatides & Woo), each once.
|
||||
template <typename F> static void walkCells(const float o[3], const float d[3], F&& f) {
|
||||
int cx = cell(o[0]), cz = cell(o[2]);
|
||||
const int ex = cell(o[0] + d[0]), ez = cell(o[2] + d[2]);
|
||||
const int sx = d[0] > 0 ? 1 : -1, sz = d[2] > 0 ? 1 : -1;
|
||||
const float ix = std::fabs(d[0]) > 1e-9f ? 1 / std::fabs(d[0]) : 1e30f, iz = std::fabs(d[2]) > 1e-9f ? 1 / std::fabs(d[2]) : 1e30f;
|
||||
float tx = std::fabs(d[0]) > 1e-9f ? ((sx > 0 ? cx + 1 : cx) - o[0]) / d[0] : 1e30f;
|
||||
float tz = std::fabs(d[2]) > 1e-9f ? ((sz > 0 ? cz + 1 : cz) - o[2]) / d[2] : 1e30f;
|
||||
for (int guard = 0; guard < 4096; ++guard) {
|
||||
f(key(cx, cz));
|
||||
if (cx == ex && cz == ez) break;
|
||||
if (tx < tz) { if (tx > 1) break; tx += ix; cx += sx; }
|
||||
else { if (tz > 1) break; tz += iz; cz += sz; }
|
||||
}
|
||||
}
|
||||
|
||||
// A segment against a box / sphere / capsule collider (in its frame, as blocked() reads it).
|
||||
static bool rayShape(const WorldCollider& c, const float o[3], const float d[3], float& t, float n[3]) {
|
||||
float lo[3], ld[3], s2[3];
|
||||
const float rel[3] = {o[0] - c.world[12], o[1] - c.world[13], o[2] - c.world[14]};
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const float* ax = &c.world[a * 4];
|
||||
s2[a] = ax[0] * ax[0] + ax[1] * ax[1] + ax[2] * ax[2];
|
||||
const float inv = s2[a] > 1e-12f ? 1 / s2[a] : 0;
|
||||
lo[a] = (rel[0] * ax[0] + rel[1] * ax[1] + rel[2] * ax[2]) * inv;
|
||||
ld[a] = (d[0] * ax[0] + d[1] * ax[1] + d[2] * ax[2]) * inv;
|
||||
}
|
||||
float ln[3] = {0, 0, 0};
|
||||
t = 1e30f;
|
||||
auto sphere = [&](const float cc[3], float r) {
|
||||
const float oc[3] = {lo[0] - cc[0], lo[1] - cc[1], lo[2] - cc[2]};
|
||||
const float A = ld[0] * ld[0] + ld[1] * ld[1] + ld[2] * ld[2], B = oc[0] * ld[0] + oc[1] * ld[1] + oc[2] * ld[2];
|
||||
const float C = oc[0] * oc[0] + oc[1] * oc[1] + oc[2] * oc[2] - r * r, disc = B * B - A * C;
|
||||
if (A < 1e-12f || disc < 0 || C < 0) return; // (starting inside: not a hit)
|
||||
const float th = (-B - std::sqrt(disc)) / A;
|
||||
if (th < 0 || th >= t) return;
|
||||
t = th;
|
||||
for (int k = 0; k < 3; ++k) ln[k] = oc[k] + ld[k] * th;
|
||||
};
|
||||
if (c.kind == WorldCollider::Box) {
|
||||
float t0 = 0, t1 = 1;
|
||||
int axis = -1;
|
||||
float sgn = 0;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
if (std::fabs(ld[a]) < 1e-12f) { if (std::fabs(lo[a]) > c.half[a]) return false; continue; }
|
||||
float ta = (-c.half[a] - lo[a]) / ld[a], tb = (c.half[a] - lo[a]) / ld[a];
|
||||
float sa = -1;
|
||||
if (ta > tb) { std::swap(ta, tb); sa = 1; }
|
||||
if (ta > t0) { t0 = ta; axis = a; sgn = sa; }
|
||||
t1 = std::min(t1, tb);
|
||||
if (t0 > t1) return false;
|
||||
}
|
||||
if (axis < 0) return false; // starts inside
|
||||
t = t0;
|
||||
ln[axis] = sgn;
|
||||
} else if (c.kind == WorldCollider::Sphere) {
|
||||
const float z[3] = {0, 0, 0};
|
||||
sphere(z, c.radius);
|
||||
} else if (c.kind == WorldCollider::Capsule) {
|
||||
const float h = c.length * 0.5f, r = c.radius;
|
||||
const float top[3] = {0, h, 0}, bot[3] = {0, -h, 0};
|
||||
sphere(top, r); sphere(bot, r);
|
||||
const float A = ld[0] * ld[0] + ld[2] * ld[2], B = lo[0] * ld[0] + lo[2] * ld[2], C = lo[0] * lo[0] + lo[2] * lo[2] - r * r;
|
||||
const float disc = B * B - A * C;
|
||||
if (A > 1e-12f && disc >= 0 && C > 0) {
|
||||
const float th = (-B - std::sqrt(disc)) / A, y = lo[1] + ld[1] * th;
|
||||
if (th >= 0 && th < t && std::fabs(y) <= h) { t = th; ln[0] = lo[0] + ld[0] * th; ln[1] = 0; ln[2] = lo[2] + ld[2] * th; }
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (t > 1) return false;
|
||||
// the local normal back to world (scaled axes: divide by the scale)
|
||||
float wn[3] = {0, 0, 0};
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const float k = s2[a] > 1e-12f ? ln[a] / s2[a] : 0;
|
||||
for (int j = 0; j < 3; ++j) wn[j] += c.world[a * 4 + j] * k;
|
||||
}
|
||||
const float L = std::sqrt(wn[0] * wn[0] + wn[1] * wn[1] + wn[2] * wn[2]);
|
||||
for (int j = 0; j < 3; ++j) n[j] = L > 1e-9f ? wn[j] / L : (j == 1 ? 1.0f : 0.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool heightOn(const Tri& t, float x, float z, float& h) {
|
||||
const float d = (t.b[2] - t.c[2]) * (t.a[0] - t.c[0]) + (t.c[0] - t.b[0]) * (t.a[2] - t.c[2]);
|
||||
if (std::fabs(d) < 1e-12f) return false;
|
||||
const float l1 = ((t.b[2] - t.c[2]) * (x - t.c[0]) + (t.c[0] - t.b[0]) * (z - t.c[2])) / d;
|
||||
const float l2 = ((t.c[2] - t.a[2]) * (x - t.c[0]) + (t.a[0] - t.c[0]) * (z - t.c[2])) / d;
|
||||
const float l3 = 1 - l1 - l2;
|
||||
if (l1 < -1e-4f || l2 < -1e-4f || l3 < -1e-4f) return false;
|
||||
h = l1 * t.a[1] + l2 * t.b[1] + l3 * t.c[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool rayTri(const float o[3], const float d[3], const Tri& t, float& out) {
|
||||
const float e1[3] = {t.b[0] - t.a[0], t.b[1] - t.a[1], t.b[2] - t.a[2]}, e2[3] = {t.c[0] - t.a[0], t.c[1] - t.a[1], t.c[2] - t.a[2]};
|
||||
const float p[3] = {d[1] * e2[2] - d[2] * e2[1], d[2] * e2[0] - d[0] * e2[2], d[0] * e2[1] - d[1] * e2[0]};
|
||||
const float det = e1[0] * p[0] + e1[1] * p[1] + e1[2] * p[2];
|
||||
if (std::fabs(det) < 1e-12f) return false;
|
||||
const float inv = 1 / det, s[3] = {o[0] - t.a[0], o[1] - t.a[1], o[2] - t.a[2]};
|
||||
const float u = (s[0] * p[0] + s[1] * p[1] + s[2] * p[2]) * inv;
|
||||
if (u < 0 || u > 1) return false;
|
||||
const float q[3] = {s[1] * e1[2] - s[2] * e1[1], s[2] * e1[0] - s[0] * e1[2], s[0] * e1[1] - s[1] * e1[0]};
|
||||
const float v = (d[0] * q[0] + d[1] * q[1] + d[2] * q[2]) * inv;
|
||||
if (v < 0 || u + v > 1) return false;
|
||||
out = (e2[0] * q[0] + e2[1] * q[1] + e2[2] * q[2]) * inv;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Closest point on a triangle to p (Ericson), for body-vs-wall tests.
|
||||
static void closest(const Tri& t, const float p[3], float out[3]) {
|
||||
auto sub = [](const float* a, const float* b, float* o) { for (int k = 0; k < 3; ++k) o[k] = a[k] - b[k]; };
|
||||
auto dot = [](const float* a, const float* b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; };
|
||||
float ab[3], ac[3], ap[3], bp[3], cp[3];
|
||||
sub(t.b, t.a, ab); sub(t.c, t.a, ac); sub(p, t.a, ap);
|
||||
const float d1 = dot(ab, ap), d2 = dot(ac, ap);
|
||||
if (d1 <= 0 && d2 <= 0) { std::copy_n(t.a, 3, out); return; }
|
||||
sub(p, t.b, bp);
|
||||
const float d3 = dot(ab, bp), d4 = dot(ac, bp);
|
||||
if (d3 >= 0 && d4 <= d3) { std::copy_n(t.b, 3, out); return; }
|
||||
const float vc = d1 * d4 - d3 * d2;
|
||||
if (vc <= 0 && d1 >= 0 && d3 <= 0) { const float v = d1 / (d1 - d3); for (int k = 0; k < 3; ++k) out[k] = t.a[k] + v * ab[k]; return; }
|
||||
sub(p, t.c, cp);
|
||||
const float d5 = dot(ab, cp), d6 = dot(ac, cp);
|
||||
if (d6 >= 0 && d5 <= d6) { std::copy_n(t.c, 3, out); return; }
|
||||
const float vb = d5 * d2 - d1 * d6;
|
||||
if (vb <= 0 && d2 >= 0 && d6 <= 0) { const float w = d2 / (d2 - d6); for (int k = 0; k < 3; ++k) out[k] = t.a[k] + w * ac[k]; return; }
|
||||
const float va = d3 * d6 - d5 * d4;
|
||||
if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) {
|
||||
const float w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
||||
for (int k = 0; k < 3; ++k) out[k] = t.b[k] + w * (t.c[k] - t.b[k]);
|
||||
return;
|
||||
}
|
||||
const float den = 1 / (va + vb + vc), v = vb * den, w = vc * den;
|
||||
for (int k = 0; k < 3; ++k) out[k] = t.a[k] + ab[k] * v + ac[k] * w;
|
||||
}
|
||||
|
||||
// Is a body at (x, y..y+height, z) inside a wall? `push` = direction out of it (horizontal).
|
||||
bool blocked(float x, float y, float z, float r, float height, float push[3]) const {
|
||||
push[0] = push[1] = push[2] = 0;
|
||||
bool hit = false;
|
||||
const float knee = y + 0.35f; // low clutter (rugs, thresholds) is stepped over
|
||||
for (float hy : {knee, y + height * 0.55f, y + height - 0.1f}) {
|
||||
const float p[3] = {x, hy, z};
|
||||
for (int cx = cell(x - r); cx <= cell(x + r); ++cx)
|
||||
for (int cz = cell(z - r); cz <= cell(z + r); ++cz) {
|
||||
auto it = wallGrid_.find(key(cx, cz));
|
||||
if (it == wallGrid_.end()) continue;
|
||||
for (uint32_t i : it->second) {
|
||||
float q[3];
|
||||
closest(walls_[i], p, q);
|
||||
const float dx = p[0] - q[0], dy = p[1] - q[1], dz = p[2] - q[2];
|
||||
if (dx * dx + dy * dy + dz * dz < r * r) { push[0] += dx; push[2] += dz; hit = true; }
|
||||
}
|
||||
}
|
||||
for (const WorldCollider& c : shapes_) {
|
||||
float inv[3], l[3]; // NOLINT
|
||||
const float rel[3] = {p[0] - c.world[12], p[1] - c.world[13], p[2] - c.world[14]};
|
||||
for (int a = 0; a < 3; ++a) { // local coords (axes may be scaled)
|
||||
const float* ax = &c.world[a * 4];
|
||||
const float s2 = ax[0] * ax[0] + ax[1] * ax[1] + ax[2] * ax[2];
|
||||
inv[a] = s2 > 1e-12f ? 1 / s2 : 0;
|
||||
l[a] = (rel[0] * ax[0] + rel[1] * ax[1] + rel[2] * ax[2]) * inv[a];
|
||||
}
|
||||
const float sx = std::sqrt(c.world[0] * c.world[0] + c.world[1] * c.world[1] + c.world[2] * c.world[2]);
|
||||
const float rl = sx > 1e-6f ? r / sx : r;
|
||||
bool in = false;
|
||||
if (c.kind == WorldCollider::Box) in = std::fabs(l[0]) < c.half[0] + rl && std::fabs(l[1]) < c.half[1] + rl && std::fabs(l[2]) < c.half[2] + rl;
|
||||
else if (c.kind == WorldCollider::Sphere) in = l[0] * l[0] + l[1] * l[1] + l[2] * l[2] < (c.radius + rl) * (c.radius + rl);
|
||||
else if (c.kind == WorldCollider::Capsule) {
|
||||
const float yy = std::clamp(l[1], -c.length * 0.5f, c.length * 0.5f);
|
||||
in = l[0] * l[0] + (l[1] - yy) * (l[1] - yy) + l[2] * l[2] < (c.radius + rl) * (c.radius + rl);
|
||||
}
|
||||
if (in) { push[0] += p[0] - c.world[12]; push[2] += p[2] - c.world[14]; hit = true; } // (tested from the knee up: low steps pass)
|
||||
}
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
bool wallBetween(const float a[3], const float b[3]) const {
|
||||
const float lift[3] = {b[0], b[1] + 0.5f, b[2]}; // test from the target, half a metre up
|
||||
const float dl[3] = {a[0] - lift[0], a[1] - lift[1], a[2] - lift[2]};
|
||||
const float len = std::sqrt(dl[0] * dl[0] + dl[1] * dl[1] + dl[2] * dl[2]);
|
||||
if (len < 1e-6f) return false;
|
||||
const int steps = std::max(1, static_cast<int>(len / 0.5f));
|
||||
for (int s = 0; s <= steps; ++s) {
|
||||
const float t = static_cast<float>(s) / steps * 0.98f;
|
||||
const float x = lift[0] + dl[0] * t, z = lift[2] + dl[2] * t;
|
||||
bool hit = false;
|
||||
forCell(wallGrid_, x, z, [&](uint32_t i) { float th; if (!hit && rayTri(lift, dl, walls_[i], th) && th > 0.02f && th < 0.98f) hit = true; });
|
||||
if (hit) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
// A world's own render settings: what that one home runs best at on the Steam Frame, over the headset's settings.json.
|
||||
// A .fenv carries them in its manifest ("settings" in fenv.json); an APK home in envs/ in "<apk>.settings.json"
|
||||
// beside it. Every field is optional: unset, the headset's own setting applies. Frame Home applies them while the
|
||||
// world is shown (resolution, foveation strength and frame rate live; MSAA and filtering by restarting itself, as a
|
||||
// settings change does); `fenv-tool tune` and Frame Studio's Calculate Best Settings work them out on the headset.
|
||||
#include "perf_settings.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
struct WorldSettings {
|
||||
float resolution = 0; // a fixed resolution scale (0.5 .. 2); 0: the headset's (fixed or auto)
|
||||
int msaa = 0; // 1, 2, 4; 0: the headset's
|
||||
int anisotropy = 0; // 4, 8, 16; 0: the headset's
|
||||
float foveation = 0; // foveation strength, 1 gentle .. 3 strong, in between too; 0: the headset's level
|
||||
int frameRate = -1; // 0 the display's, 1 half; -1: the headset's
|
||||
std::string tuned; // how they were found ("" when set by hand)
|
||||
|
||||
bool empty() const { return resolution <= 0 && msaa <= 0 && anisotropy <= 0 && foveation <= 0 && frameRate < 0; }
|
||||
bool operator==(const WorldSettings& o) const {
|
||||
return std::abs(resolution - o.resolution) < 1e-3f && msaa == o.msaa && anisotropy == o.anisotropy &&
|
||||
std::abs(foveation - o.foveation) < 1e-3f && frameRate == o.frameRate && tuned == o.tuned;
|
||||
}
|
||||
bool operator!=(const WorldSettings& o) const { return !(*this == o); }
|
||||
|
||||
// The headset's settings with this world's on top.
|
||||
PerfSettings apply(PerfSettings p) const {
|
||||
if (resolution > 0) { p.autoResolution = false; p.resolution = resolution; }
|
||||
if (msaa > 0) p.msaa = msaa;
|
||||
if (anisotropy > 0) p.anisotropy = anisotropy;
|
||||
if (foveation > 0) p.foveationLevel = static_cast<int>(std::lround(foveation));
|
||||
if (frameRate >= 0) p.frameRate = frameRate;
|
||||
p.clamp();
|
||||
return p;
|
||||
}
|
||||
// The foveation strength to draw with: this world's, else the headset's level.
|
||||
float foveationStrength(const PerfSettings& headset) const { return foveation > 0 ? foveation : static_cast<float>(headset.foveationLevel); }
|
||||
|
||||
void clamp() {
|
||||
if (!std::isfinite(resolution) || resolution <= 0) resolution = 0;
|
||||
else resolution = std::clamp(resolution, 0.5f, 2.0f);
|
||||
msaa = msaa <= 0 ? 0 : msaa >= 4 ? 4 : msaa >= 2 ? 2 : 1;
|
||||
anisotropy = anisotropy <= 0 ? 0 : anisotropy >= 16 ? 16 : anisotropy >= 8 ? 8 : 4;
|
||||
if (!std::isfinite(foveation) || foveation <= 0) foveation = 0;
|
||||
else foveation = std::clamp(foveation, 1.0f, 3.0f);
|
||||
frameRate = frameRate < 0 ? -1 : std::min(frameRate, 1);
|
||||
}
|
||||
|
||||
// Only the fields that are set ("{}" when none).
|
||||
std::string toJson() const {
|
||||
std::string j;
|
||||
char b[96];
|
||||
auto add = [&](const char* k, const char* v) { j += std::string(j.empty() ? "" : ", ") + "\"" + k + "\": " + v; };
|
||||
if (resolution > 0) { std::snprintf(b, sizeof b, "%.2f", resolution); add("resolution", b); }
|
||||
if (msaa > 0) { std::snprintf(b, sizeof b, "%d", msaa); add("msaa", b); }
|
||||
if (anisotropy > 0) { std::snprintf(b, sizeof b, "%d", anisotropy); add("anisotropy", b); }
|
||||
if (foveation > 0) { std::snprintf(b, sizeof b, "%.3f", foveation); add("foveation", b); }
|
||||
if (frameRate >= 0) { std::snprintf(b, sizeof b, "%d", frameRate); add("frameRate", b); }
|
||||
if (!tuned.empty()) {
|
||||
std::string q = "\"";
|
||||
for (char c : tuned) {
|
||||
if (c == '"' || c == '\\') { q += '\\'; q += c; }
|
||||
else if (static_cast<unsigned char>(c) >= 0x20) q += c;
|
||||
}
|
||||
add("tuned", (q + "\"").c_str());
|
||||
}
|
||||
return "{" + j + "}";
|
||||
}
|
||||
bool fromValue(const tinyjson::Value& d) {
|
||||
if (!d.isObject()) return false;
|
||||
*this = WorldSettings();
|
||||
if (d.has("resolution")) resolution = static_cast<float>(d["resolution"].asFloat());
|
||||
if (d.has("msaa")) msaa = static_cast<int>(d["msaa"].asFloat());
|
||||
if (d.has("anisotropy")) anisotropy = static_cast<int>(d["anisotropy"].asFloat());
|
||||
if (d.has("foveation")) foveation = static_cast<float>(d["foveation"].asFloat());
|
||||
if (d.has("frameRate")) frameRate = static_cast<int>(d["frameRate"].asFloat());
|
||||
if (d.has("tuned")) tuned = d["tuned"].asString();
|
||||
clamp();
|
||||
return true;
|
||||
}
|
||||
bool fromJson(const std::string& j) {
|
||||
const size_t last = j.find_last_not_of(" \t\r\n");
|
||||
if (last == std::string::npos || j[last] != '}') return false;
|
||||
try { return fromValue(tinyjson::parse(j)); } catch (...) { return false; }
|
||||
}
|
||||
|
||||
// "1.40x, 2x MSAA, foveation 1.5" (what is set), or "the headset's settings".
|
||||
std::string describe() const {
|
||||
if (empty()) return "the headset's settings";
|
||||
std::string s;
|
||||
char b[64];
|
||||
auto add = [&](const char* t) { s += std::string(s.empty() ? "" : ", ") + t; };
|
||||
if (resolution > 0) { std::snprintf(b, sizeof b, "resolution %.0f%%", resolution * 100); add(b); }
|
||||
if (msaa > 0) { std::snprintf(b, sizeof b, "%dx MSAA", msaa); add(b); }
|
||||
if (anisotropy > 0) { std::snprintf(b, sizeof b, "%dx filtering", anisotropy); add(b); }
|
||||
if (foveation > 0) { std::snprintf(b, sizeof b, "foveation %.3g", foveation); add(b); }
|
||||
if (frameRate >= 0) add(frameRate ? "half frame rate" : "full frame rate");
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
// An APK home's settings file, beside it in envs/ (a .fenv keeps them in its manifest instead).
|
||||
inline std::string worldSettingsPathFor(const std::string& homePath) { return homePath + ".settings.json"; }
|
||||
@@ -0,0 +1,152 @@
|
||||
# Frame Studio — desktop app for Windows, Linux and macOS (same renderer as Frame Home, in a window with an editor
|
||||
# UI). Everything it needs is vendored: Vulkan is loaded at runtime through volk (MoltenVK on macOS); the window is
|
||||
# Win32 on Windows and GLFW elsewhere (studio/platform.h has the rest of the system). Presets: CMakePresets.json.
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(frame_studio LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
endif()
|
||||
|
||||
set(ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..")
|
||||
set(TP "${ROOT}/third_party")
|
||||
|
||||
if(WIN32)
|
||||
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN _CRT_SECURE_NO_WARNINGS VK_USE_PLATFORM_WIN32_KHR)
|
||||
endif()
|
||||
if(MSVC)
|
||||
add_compile_options(/utf-8 /bigobj /W3 /wd4244 /wd4267 /wd4305)
|
||||
endif()
|
||||
|
||||
add_library(volk STATIC "${TP}/volk/volk.c")
|
||||
target_include_directories(volk PUBLIC "${TP}/volk" "${TP}/vulkan-headers/include")
|
||||
|
||||
# LZMA SDK (public domain): the .fenv scene coder (src/fenv_lzma.cpp), with its threaded match finder.
|
||||
add_library(lzma STATIC "${TP}/lzma/LzmaEnc.c" "${TP}/lzma/LzmaDec.c" "${TP}/lzma/LzFind.c" "${TP}/lzma/LzFindOpt.c"
|
||||
"${TP}/lzma/LzFindMt.c" "${TP}/lzma/Threads.c" "${TP}/lzma/CpuArch.c")
|
||||
target_include_directories(lzma PUBLIC "${TP}/lzma")
|
||||
add_library(miniz STATIC "${TP}/miniz/miniz.c" "${TP}/miniz/miniz_tdef.c" "${TP}/miniz/miniz_tinfl.c" "${TP}/miniz/miniz_zip.c")
|
||||
target_include_directories(miniz PUBLIC "${TP}/miniz")
|
||||
# APKs are signed and intact; re-checksumming every extracted asset was ~20% of load time.
|
||||
target_compile_definitions(miniz PRIVATE MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS)
|
||||
add_library(smolv STATIC "${TP}/smolv/smolv.cpp")
|
||||
target_include_directories(smolv PUBLIC "${TP}/smolv")
|
||||
|
||||
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
||||
set(ASTCENC_CLI OFF CACHE BOOL "" FORCE)
|
||||
set(ASTCENC_DECOMPRESSOR OFF CACHE BOOL "" FORCE)
|
||||
set(ASTCENC_UNIVERSAL_BUILD OFF CACHE BOOL "" FORCE)
|
||||
# astcenc's SIMD: NEON on ARM (Windows on ARM, Apple Silicon, ARM Linux), SSE4.1 on x86-64.
|
||||
# (on macOS the architecture asked for decides: an Apple Silicon Mac builds the Intel version too)
|
||||
if(CMAKE_OSX_ARCHITECTURES)
|
||||
set(STUDIO_ARCH "${CMAKE_OSX_ARCHITECTURES}")
|
||||
else()
|
||||
set(STUDIO_ARCH "${CMAKE_SYSTEM_PROCESSOR}")
|
||||
endif()
|
||||
if(STUDIO_ARCH MATCHES "^(arm64|ARM64|aarch64|AARCH64)$")
|
||||
set(ASTCENC_ISA_NEON ON CACHE BOOL "" FORCE)
|
||||
set(STUDIO_ASTCENC astcenc-neon-static)
|
||||
else()
|
||||
set(ASTCENC_ISA_SSE41 ON CACHE BOOL "" FORCE)
|
||||
set(STUDIO_ASTCENC astcenc-sse4.1-static)
|
||||
endif()
|
||||
add_subdirectory("${TP}/astcenc" astcenc EXCLUDE_FROM_ALL)
|
||||
|
||||
# Ogg Opus ambience (Clay homes' _BACKGROUND_LOOP.ogg): Xiph libopus + libogg + opusfile (BSD), decode only.
|
||||
set(OPUS_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory("${TP}/opus" opus EXCLUDE_FROM_ALL)
|
||||
add_library(ogg STATIC "${TP}/ogg/src/bitwise.c" "${TP}/ogg/src/framing.c")
|
||||
target_include_directories(ogg PUBLIC "${TP}/ogg/include")
|
||||
add_library(opusfile STATIC "${TP}/opusfile/src/info.c" "${TP}/opusfile/src/internal.c"
|
||||
"${TP}/opusfile/src/opusfile.c" "${TP}/opusfile/src/stream.c")
|
||||
target_include_directories(opusfile PUBLIC "${TP}/opusfile/include" PRIVATE "${TP}/opusfile/src")
|
||||
target_compile_definitions(opusfile PRIVATE OP_DISABLE_HTTP)
|
||||
target_link_libraries(opusfile PUBLIC opus ogg)
|
||||
|
||||
# The window: Win32 on Windows; GLFW (X11 on Linux, Cocoa on macOS) elsewhere.
|
||||
if(WIN32)
|
||||
set(STUDIO_IMGUI_BACKEND "${TP}/imgui/backends/imgui_impl_win32.cpp")
|
||||
set(STUDIO_PLATFORM platform_win32.cpp)
|
||||
else()
|
||||
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
|
||||
set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
# (Wayland sessions run it through XWayland; GLFW's own Wayland backend needs wayland-scanner to build)
|
||||
set(GLFW_BUILD_WAYLAND OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory("${TP}/glfw" glfw EXCLUDE_FROM_ALL)
|
||||
set(STUDIO_IMGUI_BACKEND "${TP}/imgui/backends/imgui_impl_glfw.cpp")
|
||||
set(STUDIO_PLATFORM platform_posix.cpp "${TP}/tinyfiledialogs/tinyfiledialogs.c")
|
||||
endif()
|
||||
|
||||
add_library(imgui STATIC
|
||||
"${TP}/imgui/imgui.cpp" "${TP}/imgui/imgui_draw.cpp" "${TP}/imgui/imgui_tables.cpp" "${TP}/imgui/imgui_widgets.cpp"
|
||||
${STUDIO_IMGUI_BACKEND} "${TP}/imgui/backends/imgui_impl_vulkan.cpp"
|
||||
"${TP}/imguizmo/ImGuizmo.cpp")
|
||||
target_include_directories(imgui PUBLIC "${TP}/imgui" "${TP}/imguizmo")
|
||||
target_compile_definitions(imgui PUBLIC IMGUI_IMPL_VULKAN_USE_VOLK GLFW_INCLUDE_NONE)
|
||||
target_link_libraries(imgui PUBLIC volk)
|
||||
if(NOT WIN32)
|
||||
target_link_libraries(imgui PUBLIC glfw)
|
||||
endif()
|
||||
|
||||
add_executable(frame-studio WIN32 MACOSX_BUNDLE
|
||||
studio.cpp
|
||||
${STUDIO_PLATFORM}
|
||||
"${ROOT}/src/hsl_scene.cpp"
|
||||
"${ROOT}/src/scene_bake.cpp"
|
||||
"${ROOT}/src/anim_bake.cpp"
|
||||
"${ROOT}/src/fenv_lzma.cpp"
|
||||
"${ROOT}/src/model_import.cpp"
|
||||
"${TP}/ufbx/ufbx.c"
|
||||
"${ROOT}/src/hsl_renderer.cpp"
|
||||
"${TP}/qhe/cook/hzanim_acl.cpp"
|
||||
"${TP}/stb/stb_image_impl.c"
|
||||
"${TP}/stb/stb_vorbis_impl.c"
|
||||
"${TP}/miniaudio/miniaudio_impl.c")
|
||||
set_target_properties(frame-studio PROPERTIES OUTPUT_NAME "Frame Studio")
|
||||
# ACL's NEON code mixes vector types GCC keeps apart by default (as the Frame's own build: CMakeLists.txt)
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64|ARM64|AARCH64)$")
|
||||
set_source_files_properties("${TP}/qhe/cook/hzanim_acl.cpp" PROPERTIES COMPILE_OPTIONS "-flax-vector-conversions")
|
||||
endif()
|
||||
target_compile_definitions(frame-studio PRIVATE FH_USE_VOLK)
|
||||
target_include_directories(frame-studio PRIVATE
|
||||
"${ROOT}/src"
|
||||
"${TP}/cgltf"
|
||||
"${TP}/ufbx"
|
||||
"${TP}/qhe"
|
||||
"${TP}/stb"
|
||||
"${TP}/miniaudio"
|
||||
"${TP}/acl/includes"
|
||||
"${TP}/rtm/includes"
|
||||
"${TP}/astcenc/Source"
|
||||
"${TP}/tinyfiledialogs")
|
||||
target_link_libraries(frame-studio PRIVATE imgui volk miniz lzma smolv opusfile ${STUDIO_ASTCENC})
|
||||
if(WIN32)
|
||||
target_link_libraries(frame-studio PRIVATE dwmapi comdlg32 shell32)
|
||||
else()
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(frame-studio PRIVATE glfw Threads::Threads ${CMAKE_DL_LIBS} m)
|
||||
endif()
|
||||
if(APPLE)
|
||||
set_target_properties(frame-studio PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Frame Studio" MACOSX_BUNDLE_GUI_IDENTIFIER "com.framestudio.app")
|
||||
endif()
|
||||
# Frame Home's headset side (arm64 binaries + install.sh) ships beside the exe, in frame\: Frame > Install on Steam Frame.
|
||||
add_custom_command(TARGET frame-studio POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${ROOT}/frame/payload" "$<TARGET_FILE_DIR:frame-studio>/frame")
|
||||
if(MINGW) # MinGW builds (x64, x86 and ARM64): wWinMain entry point, no runtime DLLs to ship
|
||||
target_link_options(frame-studio PRIVATE -municode -static)
|
||||
endif()
|
||||
if(WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 4) # 32-bit: up to 4 GB of address space on 64-bit Windows instead of 2 (big homes)
|
||||
if(MSVC)
|
||||
target_link_options(frame-studio PRIVATE /LARGEADDRESSAWARE)
|
||||
else()
|
||||
target_link_options(frame-studio PRIVATE "LINKER:--large-address-aware")
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,34 @@
|
||||
@echo off
|
||||
REM Builds Frame Studio with MSVC (any Visual Studio with the C++ workload). Output: <build dir>\Frame Studio.exe
|
||||
REM Usage: build_windows.bat [x64 ^| x86 ^| arm64] [build dir]
|
||||
REM x64 (default): 64-bit PCs. x86: 32-bit Windows. arm64: Windows on ARM (Snapdragon laptops, Surface Pro X...).
|
||||
REM Build dir default: %LOCALAPPDATA%\FrameStudio\build-<arch> (local disk, fast).
|
||||
REM Building for x86 or arm64 needs Visual Studio's matching C++ tools ("MSVC ... x64/x86" is always there; for
|
||||
REM ARM64 add "MSVC ... ARM64/ARM64EC build tools" in the Visual Studio Installer).
|
||||
setlocal
|
||||
set "SRC=%~dp0"
|
||||
set "ARCH=%~1"
|
||||
if "%ARCH%"=="" set "ARCH=x64"
|
||||
set "BDIR=%~2"
|
||||
if "%BDIR%"=="" set "BDIR=%LOCALAPPDATA%\FrameStudio\build-%ARCH%"
|
||||
|
||||
REM vcvarsall's host_target: built on this PC's own architecture, for the one asked for
|
||||
set "HOSTARCH=amd64"
|
||||
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" set "HOSTARCH=arm64"
|
||||
if /i "%ARCH%"=="x64" set "VCARCH=%HOSTARCH%_amd64"
|
||||
if /i "%ARCH%"=="x86" set "VCARCH=%HOSTARCH%_x86"
|
||||
if /i "%ARCH%"=="arm64" set "VCARCH=%HOSTARCH%_arm64"
|
||||
if "%VCARCH%"=="" ( echo ERROR: unknown architecture "%ARCH%": x64, x86 or arm64 & exit /b 1 )
|
||||
if "%VCARCH%"=="amd64_amd64" set "VCARCH=amd64"
|
||||
if "%VCARCH%"=="arm64_arm64" set "VCARCH=arm64"
|
||||
|
||||
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSPATH=%%i"
|
||||
if "%VSPATH%"=="" ( echo ERROR: Visual Studio with the C++ toolset not found & exit /b 1 )
|
||||
call "%VSPATH%\VC\Auxiliary\Build\vcvarsall.bat" %VCARCH% >nul || ( echo ERROR: no MSVC tools for %ARCH% & exit /b 1 )
|
||||
where cmake >nul 2>nul || set "PATH=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin;%PATH%"
|
||||
where ninja >nul 2>nul || set "PATH=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja;%PATH%"
|
||||
|
||||
cmake -S "%SRC%." -B "%BDIR%" -G Ninja -DCMAKE_BUILD_TYPE=Release || exit /b 1
|
||||
cmake --build "%BDIR%" --target frame-studio || exit /b 1
|
||||
echo Built "%BDIR%\Frame Studio.exe" (%ARCH%)
|
||||
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
// Frame pacing for Frame Studio's window: one frame per display refresh while the window is in use, a few per
|
||||
// second when it's in the background, and none at all once nothing on screen can change (no input for a moment,
|
||||
// nothing animating, loading or saving) until the next input arrives.
|
||||
//
|
||||
// Without it the UI loop drew as fast as the GPU allowed (MAILBOX presents never block): ~75% of an RTX 3060 for a
|
||||
// home sitting still on a 164 Hz monitor.
|
||||
//
|
||||
// The wait happens before the frame reads input, so pacing adds no input latency: the frame starts as late as it can.
|
||||
// The window code (Win32 or GLFW) supplies what the pacer can't know: the display's rate, whether the window has
|
||||
// focus, and how to sleep until input arrives.
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
class FramePacer {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
// Frames per second while in the background (another window has focus).
|
||||
static constexpr double kBackgroundFps = 30.0;
|
||||
// After the last input, keep drawing at full rate this long (ImGui hover effects, tooltips and drags settle).
|
||||
static constexpr double kSettleSeconds = 1.0;
|
||||
// Idle waits wake at least this often, so background work's progress (saving, uploading) still shows.
|
||||
static constexpr double kIdleWakeSeconds = 0.25;
|
||||
|
||||
// isFocused: the window has the keyboard. waitForInput(seconds): sleep until input arrives or the time is up.
|
||||
FramePacer(std::function<bool()> isFocused, std::function<void(double)> waitForInput)
|
||||
: isFocused_(std::move(isFocused)), waitForInput_(std::move(waitForInput)) {
|
||||
#ifdef _WIN32
|
||||
// High-resolution waitable timer (Windows 10 1803+); a plain one (~1-15 ms granularity) otherwise.
|
||||
timer_ = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||
if (!timer_) timer_ = CreateWaitableTimerExW(nullptr, nullptr, 0, TIMER_ALL_ACCESS);
|
||||
#endif
|
||||
lastInput_ = nextFrame_ = Clock::now();
|
||||
}
|
||||
~FramePacer() {
|
||||
#ifdef _WIN32
|
||||
if (timer_) CloseHandle(timer_);
|
||||
#endif
|
||||
}
|
||||
FramePacer(const FramePacer&) = delete;
|
||||
FramePacer& operator=(const FramePacer&) = delete;
|
||||
|
||||
// FS_FPS=n overrides the display's rate (0: unlimited, the old behaviour, for comparisons).
|
||||
void setFpsOverride(double fps) { fpsOverride_ = fps; }
|
||||
|
||||
// The refresh rate of the display the window is on (the window code tells it again when the window moves).
|
||||
void setDisplayFps(double fps) { displayFps_ = fps > 1 ? fps : 60.0; }
|
||||
double displayFps() const { return displayFps_; }
|
||||
|
||||
// Call for every input from the user (keyboard, mouse, resize, focus, file drop).
|
||||
void noteInput() { lastInput_ = Clock::now(); }
|
||||
|
||||
// Blocks until the next frame is due. `sceneChanging`: something on screen moves by itself (animation playing,
|
||||
// a home loading, a save's progress, a fly-through). An idle wait ends early when input arrives.
|
||||
void waitForNextFrame(bool sceneChanging) {
|
||||
const bool focused = isFocused_();
|
||||
const bool settled = std::chrono::duration<double>(Clock::now() - lastInput_).count() > kSettleSeconds;
|
||||
if (settled && !sceneChanging) {
|
||||
// Nothing can change until the user does something: sleep until input.
|
||||
waitForInput_(kIdleWakeSeconds);
|
||||
nextFrame_ = Clock::now() - period(displayFps_); // the frame after waking starts at once
|
||||
return;
|
||||
}
|
||||
const double fps = fpsOverride_ >= 0 ? fpsOverride_ : focused ? displayFps_ : kBackgroundFps;
|
||||
if (fps <= 0) return;
|
||||
// A frame that ran late starts the next period from now instead of rushing to catch up.
|
||||
nextFrame_ = std::max(nextFrame_ + period(fps), Clock::now());
|
||||
// Input arriving meanwhile waits for the frame (at most one refresh): waking for each event would let a
|
||||
// 1000 Hz mouse drive the frame rate.
|
||||
sleepUntil(nextFrame_);
|
||||
}
|
||||
|
||||
private:
|
||||
static Clock::duration period(double fps) {
|
||||
return std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(1.0 / fps));
|
||||
}
|
||||
|
||||
void sleepUntil(Clock::time_point when) {
|
||||
const auto wait = when - Clock::now();
|
||||
if (wait <= Clock::duration::zero()) return;
|
||||
#ifdef _WIN32
|
||||
// Windows' Sleep has ~1-15 ms granularity: a high-resolution timer hits the refresh.
|
||||
LARGE_INTEGER due;
|
||||
due.QuadPart = -static_cast<LONGLONG>(std::chrono::duration_cast<std::chrono::nanoseconds>(wait).count() / 100); // relative, 100 ns units
|
||||
if (timer_ && SetWaitableTimer(timer_, &due, 0, nullptr, nullptr, FALSE)) { WaitForSingleObject(timer_, INFINITE); return; }
|
||||
#endif
|
||||
std::this_thread::sleep_until(when);
|
||||
}
|
||||
|
||||
std::function<bool()> isFocused_;
|
||||
std::function<void(double)> waitForInput_;
|
||||
#ifdef _WIN32
|
||||
HANDLE timer_ = nullptr;
|
||||
#endif
|
||||
double displayFps_ = 60;
|
||||
double fpsOverride_ = -1;
|
||||
Clock::time_point lastInput_, nextFrame_;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
// What Frame Studio needs from the operating system, behind one interface: platform_win32.cpp (Windows) and
|
||||
// platform_posix.cpp (Linux, macOS). Text and paths cross it as UTF-8 std::string (or fs::path); the rest of Studio
|
||||
// never includes a system header.
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace platform {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ── Text ──
|
||||
std::wstring widen(const std::string& utf8);
|
||||
std::string narrow(const std::wstring& wide);
|
||||
|
||||
// ── Places ──
|
||||
fs::path exeDir(); // the folder Frame Studio runs from (the Frame payload ships beside it)
|
||||
fs::path userDataDir(); // %APPDATA% / $XDG_DATA_HOME (~/.local/share) / ~/Library/Application Support
|
||||
fs::path documentsDir(); // Documents (the user's own; ~ when there's none)
|
||||
FILE* openFile(const fs::path& path, const char* mode); // fopen with a Unicode path everywhere
|
||||
std::tm localTime(std::time_t t); // (thread-safe localtime)
|
||||
|
||||
// ── Other programs ──
|
||||
// Runs a command line (no console window); stdout and stderr land in `out`. Returns its exit code (-1: not started).
|
||||
// The line is in the platform's shell syntax as far as quoting goes: "double quotes" around arguments work on both.
|
||||
int run(const std::string& commandLine, const fs::path& workingDir, std::string& out);
|
||||
// ssh / scp, ready to prefix arguments: quoted when it's a path, with a trailing space.
|
||||
std::string sshTool(const char* name);
|
||||
// Opens a folder in the file browser, or a file / URL in its usual program.
|
||||
void openPath(const std::string& pathOrUrl);
|
||||
|
||||
// ── Dialogs ── (modal; they block until answered)
|
||||
// The window dialogs belong to (Windows: its HWND), so they come up over it. Null: none.
|
||||
void setDialogOwner(void* nativeWindow);
|
||||
enum class Icon { Info, Warning, Error, Question };
|
||||
void message(const std::string& title, const std::string& text, Icon icon = Icon::Info);
|
||||
enum class Answer { Yes, No, Cancel };
|
||||
Answer ask(const std::string& title, const std::string& text, bool withCancel, Icon icon = Icon::Question);
|
||||
|
||||
// One line of a file dialog's type list: "Meta home" with {"*.apk"}. "All files" is always added after them.
|
||||
struct FileFilter {
|
||||
std::string description;
|
||||
std::vector<std::string> patterns;
|
||||
};
|
||||
// The files picked (empty: cancelled). `multiple`: several may be picked at once.
|
||||
std::vector<std::string> openFiles(const std::string& title, const std::vector<FileFilter>& filters, bool multiple);
|
||||
// Where to save (empty: cancelled). `suggestedName` is filled in; `extension` ("fenv", no dot) is added to a name
|
||||
// typed without one. Asks before replacing a file that exists.
|
||||
std::string saveFile(const std::string& title, const std::string& suggestedName, const std::vector<FileFilter>& filters,
|
||||
const std::string& extension);
|
||||
|
||||
} // namespace platform
|
||||
@@ -0,0 +1,240 @@
|
||||
// platform.h on Linux and macOS: dialogs through tinyfiledialogs (zenity / kdialog on Linux, the system's own panels
|
||||
// on macOS), programs through /bin/sh, folders after the XDG base directories (Linux) or ~/Library (macOS).
|
||||
#include "platform.h"
|
||||
|
||||
#include <tinyfiledialogs.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#ifdef __APPLE__
|
||||
#include <mach-o/dyld.h>
|
||||
#endif
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
extern char** environ;
|
||||
|
||||
namespace platform {
|
||||
|
||||
namespace {
|
||||
|
||||
fs::path home() {
|
||||
const char* h = std::getenv("HOME");
|
||||
return h && *h ? fs::path(h) : fs::temp_directory_path();
|
||||
}
|
||||
|
||||
// The text a dialog shows: tinyfiledialogs refuses quotes (it hands the text to a shell), so they become the
|
||||
// typographic ones ("Couldn't" reads the same as "Couldn’t").
|
||||
std::string dialogText(const std::string& s) {
|
||||
std::string out;
|
||||
for (char c : s) out += c == '\'' || c == '`' ? "’" : c == '"' ? "”" : std::string(1, c);
|
||||
return out;
|
||||
}
|
||||
|
||||
// One argument for /bin/sh: in single quotes, its own single quotes closed, escaped and reopened.
|
||||
std::string shellQuote(const std::string& s) {
|
||||
std::string q = "'";
|
||||
for (char c : s) q += c == '\'' ? std::string("'\\''") : std::string(1, c);
|
||||
return q + "'";
|
||||
}
|
||||
|
||||
const char* iconName(Icon icon) {
|
||||
switch (icon) {
|
||||
case Icon::Warning: return "warning";
|
||||
case Icon::Error: return "error";
|
||||
case Icon::Question: return "question";
|
||||
default: return "info";
|
||||
}
|
||||
}
|
||||
|
||||
// tinyfiledialogs takes one filter: every pattern of every line, described by the first line.
|
||||
struct OneFilter {
|
||||
std::vector<std::string> patterns;
|
||||
std::vector<const char*> pointers;
|
||||
std::string description;
|
||||
explicit OneFilter(const std::vector<FileFilter>& filters) {
|
||||
for (const FileFilter& f : filters) {
|
||||
if (description.empty()) description = f.description;
|
||||
patterns.insert(patterns.end(), f.patterns.begin(), f.patterns.end());
|
||||
}
|
||||
for (const std::string& p : patterns) pointers.push_back(p.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// wchar_t is UTF-32 here: decode and encode UTF-8 directly.
|
||||
std::wstring widen(const std::string& s) {
|
||||
std::wstring w;
|
||||
w.reserve(s.size());
|
||||
for (size_t i = 0; i < s.size();) {
|
||||
const unsigned char c = static_cast<unsigned char>(s[i]);
|
||||
const int extra = c < 0x80 ? 0 : c < 0xE0 ? 1 : c < 0xF0 ? 2 : 3;
|
||||
uint32_t cp = extra == 0 ? c : extra == 1 ? c & 0x1F : extra == 2 ? c & 0x0F : c & 0x07;
|
||||
if (extra > 0 && i + extra >= s.size()) { w += L'�'; break; } // a sequence cut short
|
||||
for (int k = 1; k <= extra; ++k) cp = (cp << 6) | (static_cast<unsigned char>(s[i + k]) & 0x3F);
|
||||
w += static_cast<wchar_t>(cp);
|
||||
i += 1 + extra;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
std::string narrow(const std::wstring& w) {
|
||||
std::string s;
|
||||
s.reserve(w.size());
|
||||
for (wchar_t wc : w) {
|
||||
const uint32_t cp = static_cast<uint32_t>(wc);
|
||||
if (cp < 0x80) s += static_cast<char>(cp);
|
||||
else if (cp < 0x800) { s += static_cast<char>(0xC0 | (cp >> 6)); s += static_cast<char>(0x80 | (cp & 0x3F)); }
|
||||
else if (cp < 0x10000) {
|
||||
s += static_cast<char>(0xE0 | (cp >> 12)); s += static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); s += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
s += static_cast<char>(0xF0 | (cp >> 18)); s += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
s += static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); s += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
fs::path exeDir() {
|
||||
std::error_code ec;
|
||||
#ifdef __APPLE__
|
||||
char buf[4096];
|
||||
uint32_t size = sizeof buf;
|
||||
if (_NSGetExecutablePath(buf, &size) == 0) return fs::canonical(buf, ec).parent_path();
|
||||
return fs::current_path(ec);
|
||||
#else
|
||||
const fs::path self = fs::read_symlink("/proc/self/exe", ec);
|
||||
return ec ? fs::current_path(ec) : self.parent_path();
|
||||
#endif
|
||||
}
|
||||
|
||||
fs::path userDataDir() {
|
||||
#ifdef __APPLE__
|
||||
return home() / "Library" / "Application Support";
|
||||
#else
|
||||
const char* xdg = std::getenv("XDG_DATA_HOME");
|
||||
return xdg && *xdg ? fs::path(xdg) : home() / ".local" / "share";
|
||||
#endif
|
||||
}
|
||||
|
||||
fs::path documentsDir() {
|
||||
static const fs::path found = [] {
|
||||
std::error_code ec;
|
||||
#ifndef __APPLE__
|
||||
// xdg-user-dirs' answer (localized folder names), when it's set up
|
||||
std::string out;
|
||||
if (run("xdg-user-dir DOCUMENTS", {}, out) == 0) {
|
||||
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
|
||||
if (!out.empty() && fs::is_directory(out, ec) && fs::path(out) != home()) return fs::path(out);
|
||||
}
|
||||
#endif
|
||||
const fs::path docs = home() / "Documents";
|
||||
return fs::is_directory(docs, ec) ? docs : home();
|
||||
}();
|
||||
return found;
|
||||
}
|
||||
|
||||
FILE* openFile(const fs::path& path, const char* mode) { return std::fopen(path.c_str(), mode); }
|
||||
|
||||
std::tm localTime(std::time_t t) {
|
||||
std::tm tm = {};
|
||||
localtime_r(&t, &tm);
|
||||
return tm;
|
||||
}
|
||||
|
||||
int run(const std::string& commandLine, const fs::path& workingDir, std::string& out) {
|
||||
// Both ends close on exec: a command started meanwhile by another thread (Send to Frame and Install run at once)
|
||||
// must not inherit this pipe, or its reader never sees the end. The child's own copies (dup2) stay open.
|
||||
int pipeFds[2];
|
||||
{
|
||||
static std::mutex creating; // (no pipe2 on macOS: made and marked as one step)
|
||||
std::lock_guard<std::mutex> lock(creating);
|
||||
if (::pipe(pipeFds) != 0) return -1;
|
||||
::fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC);
|
||||
::fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC);
|
||||
}
|
||||
posix_spawn_file_actions_t actions;
|
||||
posix_spawn_file_actions_init(&actions);
|
||||
posix_spawn_file_actions_addopen(&actions, 0, "/dev/null", O_RDONLY, 0);
|
||||
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], 1);
|
||||
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], 2);
|
||||
posix_spawn_file_actions_addclose(&actions, pipeFds[0]);
|
||||
posix_spawn_file_actions_addclose(&actions, pipeFds[1]);
|
||||
// (the working directory through the shell: posix_spawn_file_actions_addchdir isn't everywhere yet)
|
||||
const std::string line = workingDir.empty() ? commandLine : "cd " + shellQuote(workingDir.string()) + " && " + commandLine;
|
||||
const char* argv[] = {"/bin/sh", "-c", line.c_str(), nullptr};
|
||||
pid_t pid = -1;
|
||||
const int spawned = posix_spawn(&pid, "/bin/sh", &actions, nullptr, const_cast<char* const*>(argv), environ);
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
::close(pipeFds[1]);
|
||||
if (spawned != 0) { ::close(pipeFds[0]); out = "could not start: " + commandLine; return -1; }
|
||||
char buf[4096];
|
||||
for (ssize_t n; (n = ::read(pipeFds[0], buf, sizeof buf)) > 0;) out.append(buf, static_cast<size_t>(n));
|
||||
::close(pipeFds[0]);
|
||||
int status = 0;
|
||||
while (::waitpid(pid, &status, 0) < 0 && errno == EINTR) {}
|
||||
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
||||
}
|
||||
|
||||
std::string sshTool(const char* name) { return std::string(name) + " "; }
|
||||
|
||||
void openPath(const std::string& pathOrUrl) {
|
||||
#ifdef __APPLE__
|
||||
const std::string cmd = "open " + shellQuote(pathOrUrl) + " >/dev/null 2>&1 &";
|
||||
#else
|
||||
const std::string cmd = "xdg-open " + shellQuote(pathOrUrl) + " >/dev/null 2>&1 &";
|
||||
#endif
|
||||
std::string ignored;
|
||||
run(cmd, {}, ignored);
|
||||
}
|
||||
|
||||
void setDialogOwner(void*) {}
|
||||
|
||||
void message(const std::string& title, const std::string& text, Icon icon) {
|
||||
tinyfd_messageBox(dialogText(title).c_str(), dialogText(text).c_str(), "ok", iconName(icon), 1);
|
||||
}
|
||||
|
||||
Answer ask(const std::string& title, const std::string& text, bool withCancel, Icon icon) {
|
||||
const int r = tinyfd_messageBox(dialogText(title).c_str(), dialogText(text).c_str(), withCancel ? "yesnocancel" : "yesno", iconName(icon), 1);
|
||||
// tinyfiledialogs: 1 yes; yesnocancel: 2 no, 0 cancel; yesno: 0 no
|
||||
return r == 1 ? Answer::Yes : withCancel && r == 0 ? Answer::Cancel : Answer::No;
|
||||
}
|
||||
|
||||
std::vector<std::string> openFiles(const std::string& title, const std::vector<FileFilter>& filters, bool multiple) {
|
||||
const OneFilter f(filters);
|
||||
const char* picked = tinyfd_openFileDialog(dialogText(title).c_str(), nullptr, static_cast<int>(f.pointers.size()),
|
||||
f.pointers.empty() ? nullptr : f.pointers.data(),
|
||||
f.description.empty() ? nullptr : f.description.c_str(), multiple ? 1 : 0);
|
||||
std::vector<std::string> out;
|
||||
if (!picked) return out;
|
||||
// several files come back joined by '|'
|
||||
std::string all = picked;
|
||||
for (size_t at = 0; at <= all.size();) {
|
||||
const size_t bar = all.find('|', at);
|
||||
const std::string one = all.substr(at, bar == std::string::npos ? std::string::npos : bar - at);
|
||||
if (!one.empty()) out.push_back(one);
|
||||
if (bar == std::string::npos) break;
|
||||
at = bar + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string saveFile(const std::string& title, const std::string& suggestedName, const std::vector<FileFilter>& filters,
|
||||
const std::string& extension) {
|
||||
const OneFilter f(filters);
|
||||
const char* picked = tinyfd_saveFileDialog(dialogText(title).c_str(), suggestedName.c_str(), static_cast<int>(f.pointers.size()),
|
||||
f.pointers.empty() ? nullptr : f.pointers.data(),
|
||||
f.description.empty() ? nullptr : f.description.c_str());
|
||||
if (!picked) return {};
|
||||
std::string path = picked;
|
||||
if (!extension.empty() && fs::path(path).extension().empty()) path += "." + extension;
|
||||
return path;
|
||||
}
|
||||
|
||||
} // namespace platform
|
||||
@@ -0,0 +1,197 @@
|
||||
// platform.h on Windows: Win32 dialogs and message boxes, CreateProcess, the shell's folders.
|
||||
#include "platform.h"
|
||||
|
||||
#include <windows.h> // before the other Win32 headers
|
||||
#include <commdlg.h>
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
namespace platform {
|
||||
|
||||
namespace {
|
||||
HWND g_owner = nullptr;
|
||||
}
|
||||
|
||||
std::wstring widen(const std::string& s) {
|
||||
if (s.empty()) return {};
|
||||
const int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), nullptr, 0);
|
||||
std::wstring w(n, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), w.data(), n);
|
||||
return w;
|
||||
}
|
||||
|
||||
std::string narrow(const std::wstring& w) {
|
||||
if (w.empty()) return {};
|
||||
const int n = WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
|
||||
std::string s(n, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), s.data(), n, nullptr, nullptr);
|
||||
return s;
|
||||
}
|
||||
|
||||
fs::path exeDir() {
|
||||
std::wstring buf(MAX_PATH, L'\0');
|
||||
for (;;) {
|
||||
const DWORD n = GetModuleFileNameW(nullptr, buf.data(), static_cast<DWORD>(buf.size()));
|
||||
if (n < buf.size()) { buf.resize(n); break; }
|
||||
buf.resize(buf.size() * 2);
|
||||
}
|
||||
return fs::path(buf).parent_path();
|
||||
}
|
||||
|
||||
fs::path userDataDir() {
|
||||
wchar_t buf[MAX_PATH] = {};
|
||||
const DWORD n = GetEnvironmentVariableW(L"APPDATA", buf, MAX_PATH);
|
||||
return n ? fs::path(buf) : fs::temp_directory_path();
|
||||
}
|
||||
|
||||
fs::path documentsDir() {
|
||||
wchar_t buf[MAX_PATH] = {};
|
||||
return SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_PERSONAL, nullptr, 0, buf)) ? fs::path(buf) : userDataDir();
|
||||
}
|
||||
|
||||
FILE* openFile(const fs::path& path, const char* mode) { return _wfopen(path.c_str(), widen(mode).c_str()); }
|
||||
|
||||
std::tm localTime(std::time_t t) {
|
||||
std::tm tm = {};
|
||||
localtime_s(&tm, &t);
|
||||
return tm;
|
||||
}
|
||||
|
||||
int run(const std::string& commandLine, const fs::path& workingDir, std::string& out) {
|
||||
// Inheritable handles exist only between CreatePipe and the CloseHandle after CreateProcess. Send to Frame and
|
||||
// Install run commands from two threads: without this lock one child could inherit the other's pipe, whose
|
||||
// reader then never sees end-of-file until that child exits.
|
||||
static std::mutex spawn;
|
||||
std::unique_lock<std::mutex> spawning(spawn);
|
||||
SECURITY_ATTRIBUTES sa = {sizeof sa, nullptr, TRUE};
|
||||
HANDLE rd = nullptr, wr = nullptr;
|
||||
if (!CreatePipe(&rd, &wr, &sa, 0)) return -1;
|
||||
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
||||
HANDLE nul = CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, nullptr);
|
||||
STARTUPINFOW si = {sizeof si};
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.hStdInput = nul;
|
||||
si.hStdOutput = si.hStdError = wr;
|
||||
PROCESS_INFORMATION pi = {};
|
||||
std::wstring cmd = widen(commandLine);
|
||||
const std::wstring cwd = workingDir.wstring();
|
||||
const BOOL ok = CreateProcessW(nullptr, cmd.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr,
|
||||
cwd.empty() ? nullptr : cwd.c_str(), &si, &pi);
|
||||
CloseHandle(wr);
|
||||
if (nul != INVALID_HANDLE_VALUE) CloseHandle(nul);
|
||||
spawning.unlock();
|
||||
if (!ok) { CloseHandle(rd); out = "could not start: " + commandLine; return -1; }
|
||||
char buf[4096];
|
||||
DWORD n = 0;
|
||||
while (ReadFile(rd, buf, sizeof buf, &n, nullptr) && n) out.append(buf, n);
|
||||
CloseHandle(rd);
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
DWORD code = 1;
|
||||
GetExitCodeProcess(pi.hProcess, &code);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
return static_cast<int>(code);
|
||||
}
|
||||
|
||||
// Windows' OpenSSH when installed, else Git for Windows' copy, else whatever PATH finds.
|
||||
std::string sshTool(const char* name) {
|
||||
wchar_t found[MAX_PATH] = {};
|
||||
const std::wstring exe = widen(name) + L".exe";
|
||||
if (SearchPathW(nullptr, exe.c_str(), nullptr, MAX_PATH, found, nullptr)) return "\"" + narrow(found) + "\" ";
|
||||
wchar_t win[MAX_PATH] = {};
|
||||
GetSystemDirectoryW(win, MAX_PATH);
|
||||
std::vector<fs::path> tries = {fs::path(win) / L"OpenSSH" / exe};
|
||||
for (const wchar_t* pf : {L"ProgramW6432", L"ProgramFiles", L"LOCALAPPDATA"}) {
|
||||
wchar_t d[MAX_PATH] = {};
|
||||
if (GetEnvironmentVariableW(pf, d, MAX_PATH)) {
|
||||
tries.push_back(fs::path(d) / L"Git" / L"usr" / L"bin" / exe);
|
||||
tries.push_back(fs::path(d) / L"Programs" / L"Git" / L"usr" / L"bin" / exe);
|
||||
}
|
||||
}
|
||||
std::error_code ec;
|
||||
for (const fs::path& t : tries) if (fs::exists(t, ec)) return "\"" + narrow(t.wstring()) + "\" ";
|
||||
return std::string(name) + " ";
|
||||
}
|
||||
|
||||
void openPath(const std::string& pathOrUrl) {
|
||||
ShellExecuteW(nullptr, L"open", widen(pathOrUrl).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
}
|
||||
|
||||
void setDialogOwner(void* nativeWindow) { g_owner = static_cast<HWND>(nativeWindow); }
|
||||
|
||||
namespace {
|
||||
UINT iconFlag(Icon icon) {
|
||||
switch (icon) {
|
||||
case Icon::Warning: return MB_ICONWARNING;
|
||||
case Icon::Error: return MB_ICONERROR;
|
||||
case Icon::Question: return MB_ICONQUESTION;
|
||||
default: return MB_ICONINFORMATION;
|
||||
}
|
||||
}
|
||||
|
||||
// "Meta home (*.apk)\0*.apk\0...All files\0*.*\0\0", as OPENFILENAMEW wants it.
|
||||
std::wstring filterString(const std::vector<FileFilter>& filters) {
|
||||
std::wstring s;
|
||||
for (const FileFilter& f : filters) {
|
||||
std::wstring patterns;
|
||||
for (const std::string& p : f.patterns) patterns += (patterns.empty() ? L"" : L";") + widen(p);
|
||||
s += widen(f.description) + L" (" + patterns + L")" + std::wstring(1, L'\0') + patterns + std::wstring(1, L'\0');
|
||||
}
|
||||
s += L"All files";
|
||||
s += L'\0';
|
||||
s += L"*.*";
|
||||
s += L'\0';
|
||||
return s; // (the string's own terminator ends the list)
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void message(const std::string& title, const std::string& text, Icon icon) {
|
||||
MessageBoxW(g_owner, widen(text).c_str(), widen(title).c_str(), MB_OK | iconFlag(icon));
|
||||
}
|
||||
|
||||
Answer ask(const std::string& title, const std::string& text, bool withCancel, Icon icon) {
|
||||
const int r = MessageBoxW(g_owner, widen(text).c_str(), widen(title).c_str(), (withCancel ? MB_YESNOCANCEL : MB_YESNO) | iconFlag(icon));
|
||||
return r == IDYES ? Answer::Yes : r == IDNO ? Answer::No : Answer::Cancel;
|
||||
}
|
||||
|
||||
std::vector<std::string> openFiles(const std::string& title, const std::vector<FileFilter>& filters, bool multiple) {
|
||||
std::vector<wchar_t> buf(multiple ? 32768 : 4096, L'\0');
|
||||
const std::wstring filter = filterString(filters), wtitle = widen(title);
|
||||
OPENFILENAMEW ofn = {sizeof ofn};
|
||||
ofn.hwndOwner = g_owner;
|
||||
ofn.lpstrFilter = filter.c_str();
|
||||
ofn.lpstrFile = buf.data();
|
||||
ofn.nMaxFile = static_cast<DWORD>(buf.size());
|
||||
ofn.lpstrTitle = wtitle.c_str();
|
||||
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | (multiple ? OFN_ALLOWMULTISELECT | OFN_EXPLORER : 0);
|
||||
if (!GetOpenFileNameW(&ofn)) return {};
|
||||
// Multi-select returns "dir\0file1\0file2\0\0"; a single pick returns "full path\0".
|
||||
std::vector<std::string> picked;
|
||||
const wchar_t* p = buf.data();
|
||||
const std::wstring first = p;
|
||||
p += first.size() + 1;
|
||||
if (!*p) picked.push_back(narrow(first));
|
||||
for (; *p; p += std::wcslen(p) + 1) picked.push_back(narrow((fs::path(first) / p).wstring()));
|
||||
return picked;
|
||||
}
|
||||
|
||||
std::string saveFile(const std::string& title, const std::string& suggestedName, const std::vector<FileFilter>& filters,
|
||||
const std::string& extension) {
|
||||
std::vector<wchar_t> buf(4096, L'\0');
|
||||
const std::wstring name = widen(suggestedName), filter = filterString(filters), wtitle = widen(title), ext = widen(extension);
|
||||
std::wcsncpy(buf.data(), name.c_str(), buf.size() - 1);
|
||||
OPENFILENAMEW ofn = {sizeof ofn};
|
||||
ofn.hwndOwner = g_owner;
|
||||
ofn.lpstrFilter = filter.c_str();
|
||||
ofn.lpstrDefExt = ext.empty() ? nullptr : ext.c_str();
|
||||
ofn.lpstrFile = buf.data();
|
||||
ofn.nMaxFile = static_cast<DWORD>(buf.size());
|
||||
ofn.lpstrTitle = wtitle.c_str();
|
||||
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
|
||||
if (!GetSaveFileNameW(&ofn)) return {};
|
||||
return narrow(buf.data());
|
||||
}
|
||||
|
||||
} // namespace platform
|
||||
+8284
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,94 @@
|
||||
// Frame Home never puts your eyes inside things or your feet through a floor (Locomotion over a baked walkable area):
|
||||
// walking down an open staircase along its side, and walking / leaning into a wall for real.
|
||||
#include "locomotion.h"
|
||||
#include <cstdio>
|
||||
static int fails = 0;
|
||||
static void ck(bool ok, const char* w, float a = 0, float b = 0, float c = 0) { std::printf("%s %s (%.2f %.2f %.2f)\n", ok ? "PASS" : "FAIL", w, a, b, c); fails += !ok; }
|
||||
static WorldCollider box(float x0, float y0, float z0, float x1, float y1, float z1) {
|
||||
WorldCollider c; c.kind = WorldCollider::Box; c.role = WorldCollider::Solid;
|
||||
c.half[0] = (x1 - x0) / 2; c.half[1] = (y1 - y0) / 2; c.half[2] = (z1 - z0) / 2;
|
||||
c.world[12] = (x0 + x1) / 2; c.world[13] = (y0 + y1) / 2; c.world[14] = (z0 + z1) / 2;
|
||||
return c;
|
||||
}
|
||||
static void bake(HomeWorld& w) {
|
||||
std::vector<WalkSource> src;
|
||||
std::vector<std::vector<float>> P;
|
||||
std::vector<std::vector<uint32_t>> I;
|
||||
walkColliderSources(w, src, P, I);
|
||||
w.walkGridCache = buildWalkGrid(src, walkSeeds(w), WalkParams{}, nullptr);
|
||||
}
|
||||
int main() {
|
||||
{ // an open staircase: a 1.8 m landing, 8 treads 20 cm down, 30 cm deep, 1 m wide, the ground all round and under
|
||||
HomeWorld w;
|
||||
w.colliders.push_back(box(-8, -0.1f, -8, 8, 0, 8));
|
||||
w.colliders.push_back(box(-4, 0, -1, -2, 1.8f, 1));
|
||||
for (int i = 0; i < 8; ++i) w.colliders.push_back(box(-2 + 0.3f * i, 1.8f - 0.2f * (i + 1) - 0.05f, -0.5f, -2 + 0.3f * (i + 1), 1.8f - 0.2f * (i + 1), 0.5f));
|
||||
WorldPoint sp; sp.pos[0] = -3; sp.pos[1] = 1.8f; sp.pos[2] = 0.38f; sp.yaw = -1.5708f; // facing +X, down the stairs
|
||||
w.spawns.push_back(sp);
|
||||
WorldPoint g; g.pos[0] = 4; g.pos[1] = 0; g.pos[2] = 0; w.hotspots.push_back(g);
|
||||
bake(w);
|
||||
Locomotion loco;
|
||||
loco.setWorld(w);
|
||||
float head[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1};
|
||||
loco.testInput(0, 1, 0, 0);
|
||||
float worst = 1e9f, x = -3, y = 0;
|
||||
for (int i = 0; i < 900 && x < 0.5f; ++i) {
|
||||
loco.update(1.0f / 90, head);
|
||||
float R[16];
|
||||
loco.rig(R);
|
||||
x = R[0] * head[12] + R[4] * head[13] + R[8] * head[14] + R[12];
|
||||
y = R[1] * head[12] + R[5] * head[13] + R[9] * head[14] + R[13];
|
||||
float tread = 0; // the top under the eyes
|
||||
if (x < -2) tread = 1.8f;
|
||||
else if (x < 0.4f) tread = std::max(0.0f, 1.8f - 0.2f * (std::floor((x + 2) / 0.3f) + 1));
|
||||
worst = std::min(worst, (y - 1.7f) - tread);
|
||||
}
|
||||
loco.testInput(0, 0, 0, 0);
|
||||
for (int i = 0; i < 90; ++i) loco.update(1.0f / 90, head); // the last step eases down
|
||||
{ float R[16]; loco.rig(R); y = R[1] * head[12] + R[5] * head[13] + R[9] * head[14] + R[13]; }
|
||||
ck(x > 0.3f, "walked down the stairs along their open side", x, y);
|
||||
ck(worst > -0.25f, "the feet never sink more than a quarter step under the tread (never through it)", worst);
|
||||
ck(std::fabs(y - 1.7f) < 0.05f, "at the bottom, standing on the ground", y);
|
||||
}
|
||||
{ // a wall 1 m ahead: walk into it for real (no stick): the eyes stop short of it
|
||||
HomeWorld w;
|
||||
w.colliders.push_back(box(-8, -0.1f, -8, 8, 0, 8));
|
||||
w.colliders.push_back(box(0.9f, 0, -2, 1.1f, 2.5f, 2));
|
||||
WorldPoint sp; sp.pos[0] = 0; sp.pos[1] = 0; sp.pos[2] = 0; w.spawns.push_back(sp);
|
||||
bake(w);
|
||||
Locomotion loco;
|
||||
loco.setWorld(w);
|
||||
float head[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1};
|
||||
float most = -1e9f;
|
||||
for (int i = 0; i < 300; ++i) {
|
||||
head[12] = std::min(1.6f, i * 0.008f); // stepping forward 1.6 m in the room
|
||||
head[13] = 1.70f - (i > 200 ? 0.6f : 0.0f); // then bending down
|
||||
loco.update(1.0f / 90, head);
|
||||
float R[16];
|
||||
loco.rig(R);
|
||||
most = std::max(most, R[0] * head[12] + R[4] * head[13] + R[8] * head[14] + R[12]);
|
||||
}
|
||||
ck(most <= 0.9f - 0.14f, "walking into a wall for real, the eyes stay 15 cm off it", most);
|
||||
}
|
||||
{ // a coffee table (45 cm high) in the way: walking into it for real, you're kept at its edge
|
||||
HomeWorld w;
|
||||
w.colliders.push_back(box(-8, -0.1f, -8, 8, 0, 8));
|
||||
w.colliders.push_back(box(0.8f, 0, -0.5f, 1.6f, 0.45f, 0.5f));
|
||||
WorldPoint sp; sp.pos[0] = 0; sp.pos[1] = 0; sp.pos[2] = 0; w.spawns.push_back(sp);
|
||||
bake(w);
|
||||
Locomotion loco;
|
||||
loco.setWorld(w);
|
||||
float head[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1};
|
||||
float most = -1e9f;
|
||||
for (int i = 0; i < 250; ++i) {
|
||||
head[12] = std::min(1.2f, i * 0.008f);
|
||||
loco.update(1.0f / 90, head);
|
||||
float R[16];
|
||||
loco.rig(R);
|
||||
most = std::max(most, R[0] * head[12] + R[4] * head[13] + R[8] * head[14] + R[12]);
|
||||
}
|
||||
ck(most <= 0.85f, "walking into a coffee table for real, you stop at its edge", most);
|
||||
}
|
||||
std::printf("%d failed\n", fails);
|
||||
return fails != 0;
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
#include "locomotion.h"
|
||||
#include "hsl_scene.h"
|
||||
#include <cstdio>
|
||||
static int fails = 0;
|
||||
static void ck(bool ok, const char* w, float a = 0, float b = 0, float c = 0) { std::printf("%s %s (%.2f %.2f %.2f)\n", ok ? "PASS" : "FAIL", w, a, b, c); fails += !ok; }
|
||||
int main() {
|
||||
HomeWorld w;
|
||||
WorldCollider fl; fl.kind = WorldCollider::Mesh; fl.role = WorldCollider::Floor;
|
||||
fl.positions = {-10, 0, -10, 10, 0, -10, 10, 0, 10, -10, 0, 10}; fl.indices = {0, 2, 1, 0, 3, 2}; w.colliders.push_back(fl);
|
||||
WorldPoint seat; seat.name = "Armchair"; seat.pos[0] = 2; seat.pos[1] = 0.45f; seat.pos[2] = -1; seat.yaw = 1.5708f; w.seats.push_back(seat);
|
||||
Locomotion loco;
|
||||
loco.setWorld(w);
|
||||
float head[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1}; // standing, looking down -Z
|
||||
auto eye = [&](float* o) { float R[16]; loco.rig(R); for (int k = 0; k < 3; ++k) o[k] = R[k] * head[12] + R[4 + k] * head[13] + R[8 + k] * head[14] + R[12 + k]; };
|
||||
auto facing = [&]() { float R[16]; loco.rig(R); const float f[3] = {-(R[8]), 0, -(R[10])}; return std::atan2(-f[0], -f[2]); };
|
||||
float e[3];
|
||||
loco.testSit(0, head); eye(e);
|
||||
ck(loco.seated() == 0, "landing on a seat sits you in it");
|
||||
ck(std::fabs(e[0] - 2) < 1e-3f && std::fabs(e[2] + 1) < 1e-3f, "your head is over the seat", e[0], e[1], e[2]);
|
||||
ck(std::fabs(e[1] - (0.45f + 0.78f)) < 1e-3f, "your eyes are at seated height over it (standing in the room)", e[1]);
|
||||
ck(std::fabs(facing()) < 0.02f, "sitting keeps the way you face (no turn to the seat's front)", facing());
|
||||
for (int i = 0; i < 30; ++i) loco.update(1.0f / 90, head);
|
||||
eye(e);
|
||||
ck(loco.seated() == 0 && std::fabs(e[0] - 2) < 1e-3f, "sitting still keeps you in the seat", e[0], e[1], e[2]);
|
||||
head[13] = 1.20f; // you sit down for real
|
||||
for (int i = 0; i < 400; ++i) loco.update(1.0f / 90, head);
|
||||
eye(e);
|
||||
ck(std::fabs(e[1] - 1.23f) < 0.03f, "sitting down for real eases back to seated eye height", e[1]);
|
||||
float R0[16]; loco.rig(R0);
|
||||
head[12] = 0.3f; // leaning: stays seated
|
||||
loco.update(1.0f / 90, head);
|
||||
ck(loco.seated() == 0, "leaning keeps you seated");
|
||||
head[12] = 0.8f; // stepping away in the room
|
||||
loco.update(1.0f / 90, head);
|
||||
eye(e);
|
||||
ck(loco.seated() == -1, "stepping 0.8 m away stands you up", e[0], e[1], e[2]);
|
||||
ck(std::fabs(e[1] - 1.20f) < 0.02f, "standing again, the floor is the floor", e[1]);
|
||||
head[12] = 0; head[13] = 1.70f;
|
||||
loco.testSit(0, head);
|
||||
loco.testWalkOff(head);
|
||||
eye(e);
|
||||
ck(loco.seated() == -1 && std::fabs(e[0] - 1.5f) < 0.02f && std::fabs(e[2] + 1) < 0.02f && std::fabs(e[1] - 1.70f) < 0.02f,
|
||||
"walking off steps you out half a metre in front of the seat, standing", e[0], e[1], e[2]);
|
||||
{ // the stick in a seat (slide teleports): you dash out to its stand-up spot (none named: half a metre in front)
|
||||
Locomotion l2; l2.setWorld(w); l2.setOptions(1, 2, 0, 30, 2);
|
||||
float hd[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1};
|
||||
l2.testSit(0, hd);
|
||||
l2.testInput(0, 1, 0, 0);
|
||||
l2.update(1.0f / 90, hd);
|
||||
l2.testInput(0, 0, 0, 0);
|
||||
for (int i = 0; i < 90; ++i) l2.update(1.0f / 90, hd);
|
||||
float R2[16]; l2.rig(R2);
|
||||
const float ex = R2[0] * hd[12] + R2[4] * hd[13] + R2[8] * hd[14] + R2[12], ey = R2[1] * hd[12] + R2[5] * hd[13] + R2[9] * hd[14] + R2[13],
|
||||
ez = R2[2] * hd[12] + R2[6] * hd[13] + R2[10] * hd[14] + R2[14];
|
||||
ck(l2.seated() == -1 && std::fabs(ex - 1.5f) < 0.05f && std::fabs(ez + 1) < 0.05f && std::fabs(ey - 1.70f) < 0.05f,
|
||||
"the stick in a seat dashes you out to its stand-up spot, standing", ex, ey, ez);
|
||||
}
|
||||
{ // falling off a small platform (roomscale) in an ordinary home: you fall, then you're back at the spawn
|
||||
HomeWorld w2;
|
||||
WorldCollider pl; pl.kind = WorldCollider::Mesh; pl.role = WorldCollider::Solid;
|
||||
pl.positions = {-1, 0, -1, 1, 0, -1, 1, 0, 1, -1, 0, 1}; pl.indices = {0, 2, 1, 0, 3, 2}; w2.colliders.push_back(pl);
|
||||
w2.fallLimit = 15;
|
||||
Locomotion l2; l2.setWorld(w2);
|
||||
float hd[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1};
|
||||
for (int i = 0; i < 10; ++i) l2.update(1.0f / 90, hd);
|
||||
hd[12] = 2.5f; // step off the edge in the room
|
||||
int frames = 0;
|
||||
float lowest = 1e9f;
|
||||
while (l2.respawns() == 0 && frames < 900) { l2.update(1.0f / 90, hd); float R[16]; l2.rig(R); lowest = std::min(lowest, R[13]); ++frames; }
|
||||
ck(l2.respawns() == 1 && lowest < -14.0f, "stepping off a platform you fall, and after 15 m you're back at the spawn", frames / 90.0f, lowest);
|
||||
}
|
||||
{ // a home with a gravity zone goes to the gravity walker
|
||||
HomeWorld w3;
|
||||
WorldCollider s3; s3.kind = WorldCollider::Sphere; s3.role = WorldCollider::Solid; s3.radius = 10; w3.colliders.push_back(s3);
|
||||
WorldGravity g3; g3.kind = WorldGravity::Planet; g3.radius = 40; w3.gravity.push_back(g3);
|
||||
WorldPoint sp3; sp3.pos[1] = 10; w3.spawns.push_back(sp3);
|
||||
Locomotion l3; l3.setWorld(w3);
|
||||
float hd[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1};
|
||||
for (int i = 0; i < 30; ++i) l3.update(1.0f / 90, hd);
|
||||
float R[16]; l3.rig(R);
|
||||
const float ey = R[4] * hd[12] + R[5] * hd[13] + R[6] * hd[14] + R[13];
|
||||
ck(l3.gravityMode() && std::fabs(ey - 11.7f) < 0.05f, "a planet home stands you on the planet (gravity walker)", ey);
|
||||
}
|
||||
{ // movement options (setOptions), on a plain 20 m floor, nothing near the spawn
|
||||
HomeWorld w4;
|
||||
w4.colliders.push_back(fl);
|
||||
const float dt = 1.0f / 80;
|
||||
float hd[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1};
|
||||
auto eyeOf = [&](Locomotion& l, float* o) { float R[16]; l.rig(R); for (int k = 0; k < 3; ++k) o[k] = R[k] * hd[12] + R[4 + k] * hd[13] + R[8 + k] * hd[14] + R[12 + k]; };
|
||||
auto yawOf = [&](Locomotion& l) { float R[16]; l.rig(R); return std::atan2(R[8], R[10]); };
|
||||
auto run = [&](Locomotion& l, int n) { for (int i = 0; i < n; ++i) l.update(dt, hd); };
|
||||
// The right hand at chest height, pointing forward and 30 degrees down (row-major 3x4).
|
||||
const float aimDown[12] = {1, 0, 0, 0.2f, 0, 0.866f, 0.5f, 1.2f, 0, -0.5f, 0.866f, 0};
|
||||
float e0[3], e1[3], t[3];
|
||||
{
|
||||
Locomotion l; l.setWorld(w4);
|
||||
l.setOptions(0, 1, 0, 30, 2);
|
||||
l.testInput(0, 1, 0, 0); eyeOf(l, e0); run(l, 80); eyeOf(l, e1);
|
||||
ck(std::hypot(e1[0] - e0[0], e1[2] - e0[2]) < 1e-4f, "walk off: a full left stick for 1 s doesn't move you", e1[0], e1[2]);
|
||||
l.setOptions(1, 1, 0, 30, 3);
|
||||
run(l, 80); eyeOf(l, e1);
|
||||
// (0.15 s ramp from a standstill: 1 s at full stick covers 3 * (1 - 0.075) m)
|
||||
ck(std::fabs(e1[2] - e0[2] + 3 * (1 - grav::kAccel / 2)) < 0.05f && std::fabs(e1[0] - e0[0]) < 1e-3f, "walk on at 3 m/s: 1 s forward goes 3 m (less the ramp up)", e1[0], e1[2]);
|
||||
}
|
||||
{
|
||||
Locomotion l; l.setWorld(w4);
|
||||
l.setOptions(1, 1, 1, 30, 2);
|
||||
const float y0 = yawOf(l);
|
||||
eyeOf(l, e0);
|
||||
l.testInput(0, 0, 1, 0); run(l, 40); // 0.5 s full right
|
||||
const float y1 = yawOf(l);
|
||||
run(l, 40); // 1 s
|
||||
const float y2 = yawOf(l);
|
||||
eyeOf(l, e1);
|
||||
ck(std::fabs((y1 - y0) + 1.0472f) < 0.01f && std::fabs((y2 - y0) + 2.0944f) < 0.01f, "smooth turn: 60 degrees in 0.5 s, 120 in 1 s (right = clockwise)", y1 - y0, y2 - y0);
|
||||
ck(std::hypot(e1[0] - e0[0], e1[2] - e0[2]) < 1e-4f, "smooth turn spins about the head", e1[0], e1[2]);
|
||||
l.testInput(0, 0, 0.575f, 0); const float y3 = yawOf(l); run(l, 40);
|
||||
ck(std::fabs((yawOf(l) - y3) + 0.5236f) < 0.01f, "half deflection past the deadzone turns half as fast", yawOf(l) - y3);
|
||||
l.testInput(0, 0, 0.1f, 0); const float y4 = yawOf(l); run(l, 40);
|
||||
ck(std::fabs(yawOf(l) - y4) < 1e-6f, "inside the deadzone: no turn", yawOf(l) - y4);
|
||||
}
|
||||
{
|
||||
Locomotion l; l.setWorld(w4);
|
||||
l.setOptions(1, 1, 0, 45, 2);
|
||||
const float y0 = yawOf(l);
|
||||
l.testInput(0, 0, -1, 0); run(l, 20); // held: one snap
|
||||
const float y1 = yawOf(l);
|
||||
l.testInput(0, 0, 0, 0); run(l, 2); l.testInput(0, 0, -1, 0); run(l, 2);
|
||||
ck(std::fabs((y1 - y0) - 0.7854f) < 1e-4f && std::fabs((yawOf(l) - y0) - 1.5708f) < 1e-4f, "snap angle 45: a held flick turns 45 degrees once, the next another 45", y1 - y0, yawOf(l) - y0);
|
||||
}
|
||||
{
|
||||
Locomotion l; l.setWorld(w4);
|
||||
l.setOptions(1, 2, 0, 30, 2);
|
||||
l.testInput(0, 0, 0, 1, aimDown); run(l, 3);
|
||||
const bool aimed = l.testTarget(t);
|
||||
eyeOf(l, e0);
|
||||
ck(aimed && t[2] < -1.5f && std::fabs(t[1]) < 1e-3f, "aiming forward and down finds the floor ahead", t[0], t[1], t[2]);
|
||||
l.testInput(0, 0, 0, 0); run(l, 10); // released; 10 frames = 0.125 s = half the dash
|
||||
eyeOf(l, e1);
|
||||
const float frac = (e1[2] - e0[2]) / (t[2] - e0[2]);
|
||||
ck(l.sliding() && std::fabs(frac - 0.5f) < 0.02f, "slide: halfway through the dash, halfway there", frac);
|
||||
run(l, 8); const bool still = l.sliding();
|
||||
run(l, 4); eyeOf(l, e1);
|
||||
ck(still && !l.sliding() && std::fabs(e1[0] - t[0]) < 1e-3f && std::fabs(e1[2] - t[2]) < 1e-3f && std::fabs(e1[1] - 1.70f) < 1e-3f,
|
||||
"slide: after 0.25 s you're standing at the target", e1[0], e1[1], e1[2]);
|
||||
l.testInput(1, 0, 0, 0); run(l, 1); // the stick works again afterwards (sanity)
|
||||
}
|
||||
{
|
||||
Locomotion l; l.setWorld(w4);
|
||||
l.setOptions(1, 1, 0, 30, 2); // instant (no compositor here: no blink delay)
|
||||
l.testInput(0, 0, 0, 1, aimDown); run(l, 3); l.testTarget(t);
|
||||
l.testInput(0, 0, 0, 0); run(l, 1); eyeOf(l, e1);
|
||||
ck(std::fabs(e1[0] - t[0]) < 1e-3f && std::fabs(e1[2] - t[2]) < 1e-3f, "instant teleport lands on release", e1[0], e1[2]);
|
||||
}
|
||||
{
|
||||
Locomotion l; l.setWorld(w4);
|
||||
l.setOptions(1, 0, 0, 30, 2);
|
||||
eyeOf(l, e0);
|
||||
l.testInput(0, 0, 0, 1, aimDown); run(l, 10);
|
||||
const bool aimed = l.testTarget(t);
|
||||
l.testInput(0, 0, 0, 0); run(l, 30); eyeOf(l, e1);
|
||||
ck(!aimed && std::hypot(e1[0] - e0[0], e1[2] - e0[2]) < 1e-4f, "teleport off: the stick forward aims nothing and goes nowhere", e1[0], e1[2]);
|
||||
l.setOptions(1, 1, 0, 30, 2);
|
||||
l.testInput(0, 0, 0, 1, aimDown); run(l, 2);
|
||||
ck(l.testTarget(t), "teleport back on: aiming works again live");
|
||||
}
|
||||
{ // gravity homes honour them too
|
||||
HomeWorld w5;
|
||||
WorldCollider s5; s5.kind = WorldCollider::Sphere; s5.role = WorldCollider::Solid; s5.radius = 10; w5.colliders.push_back(s5);
|
||||
WorldGravity g5; g5.kind = WorldGravity::Planet; g5.radius = 40; w5.gravity.push_back(g5);
|
||||
WorldPoint sp5; sp5.pos[1] = 10; w5.spawns.push_back(sp5);
|
||||
Locomotion l; l.setWorld(w5);
|
||||
l.setOptions(0, 1, 1, 30, 2);
|
||||
run(l, 30); eyeOf(l, e0);
|
||||
l.testInput(0, 1, 0, 0); run(l, 80); eyeOf(l, e1);
|
||||
ck(std::hypot(e1[0] - e0[0], e1[2] - e0[2]) < 1e-3f, "gravity home, walk off: the stick doesn't move you", e1[0], e1[2]);
|
||||
l.setOptions(1, 1, 1, 30, 1);
|
||||
run(l, 40); eyeOf(l, e1);
|
||||
const float arc = 10.0f * std::atan2(std::hypot(e1[0], e1[2]), e1[1]);
|
||||
ck(std::fabs(arc - (0.5f - grav::kAccel / 2)) < 0.03f, "gravity home, walk on at 1 m/s: 0.5 s walks 0.5 m round the planet, less the ramp up", arc);
|
||||
float R0[16], R1[16]; l.rig(R0);
|
||||
l.testInput(0, 0, 1, 0); run(l, 40); l.rig(R1);
|
||||
const float cosA = R0[0] * R1[0] + R0[1] * R1[1] + R0[2] * R1[2];
|
||||
ck(std::fabs(std::acos(std::clamp(cosA, -1.0f, 1.0f)) - 1.0472f) < 0.02f, "gravity home, smooth turn: 60 degrees in 0.5 s", std::acos(std::clamp(cosA, -1.0f, 1.0f)));
|
||||
}
|
||||
}
|
||||
// ── Walking feel, teleport limits, seats ──
|
||||
auto handPose = [](const float p[3], float dx, float dy, float dz, float* m) { // at p, pointing along d (3x4 row-major)
|
||||
const float l = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
const float z[3] = {-dx / l, -dy / l, -dz / l};
|
||||
float x[3] = {z[2], 0, -z[0]}; // up x z
|
||||
const float xl = std::sqrt(x[0] * x[0] + x[2] * x[2]);
|
||||
if (xl < 1e-4f) { x[0] = 1; x[2] = 0; } else { x[0] /= xl; x[2] /= xl; }
|
||||
const float y[3] = {z[1] * x[2] - z[2] * x[1], z[2] * x[0] - z[0] * x[2], z[0] * x[1] - z[1] * x[0]};
|
||||
for (int r = 0; r < 3; ++r) { m[r * 4] = x[r]; m[r * 4 + 1] = y[r]; m[r * 4 + 2] = z[r]; m[r * 4 + 3] = p[r]; }
|
||||
};
|
||||
auto quadC = [](int role, std::vector<float> pts) {
|
||||
WorldCollider q; q.kind = WorldCollider::Mesh; q.role = role; q.positions = pts; q.indices = {0, 1, 2, 0, 2, 3}; return q;
|
||||
};
|
||||
const float dt = 1.0f / 80;
|
||||
float hd[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1};
|
||||
auto eyeOf = [&](Locomotion& l, float* o) { float R[16]; l.rig(R); for (int k = 0; k < 3; ++k) o[k] = R[k] * hd[12] + R[4 + k] * hd[13] + R[8 + k] * hd[14] + R[12 + k]; };
|
||||
auto run = [&](Locomotion& l, int n) { for (int i = 0; i < n; ++i) l.update(dt, hd); };
|
||||
HomeWorld flat;
|
||||
flat.colliders.push_back(quadC(WorldCollider::Floor, {-20, 0, -20, 20, 0, -20, 20, 0, 20, -20, 0, 20}));
|
||||
{
|
||||
Locomotion l; l.setWorld(flat); l.setOptions(1, 1, 0, 30, 2);
|
||||
float a[3], b[3];
|
||||
l.testInput(0, 1, 0, 0);
|
||||
eyeOf(l, a); run(l, 1); eyeOf(l, b);
|
||||
const float first = std::fabs(b[2] - a[2]) / dt;
|
||||
run(l, 5); eyeOf(l, a); run(l, 1); eyeOf(l, b);
|
||||
const float mid = std::fabs(b[2] - a[2]) / dt;
|
||||
run(l, 20); eyeOf(l, a); run(l, 1); eyeOf(l, b);
|
||||
const float full = std::fabs(b[2] - a[2]) / dt;
|
||||
ck(first < 0.3f && mid > 0.7f && mid < 1.6f && std::fabs(full - 2) < 0.01f, "walking eases up to speed in ~0.15 s (no instant start)", first, mid, full);
|
||||
l.testInput(0, 0, 0, 0);
|
||||
eyeOf(l, a); run(l, 1); eyeOf(l, b);
|
||||
const float after = std::fabs(b[2] - a[2]) / dt;
|
||||
run(l, 10); eyeOf(l, a); run(l, 1); eyeOf(l, b);
|
||||
ck(after > 1.5f && std::fabs(b[2] - a[2]) < 1e-5f, "letting go slows you to a stop within ~0.12 s (no instant stop)", after, std::fabs(b[2] - a[2]) / dt);
|
||||
l.testInput(0, 0.575f, 0, 0); run(l, 40); eyeOf(l, a); run(l, 1); eyeOf(l, b);
|
||||
const float half = std::fabs(b[2] - a[2]) / dt;
|
||||
ck(half > 0.5f && half < 0.8f, "half a push walks about a third of full speed (small push = slow)", half);
|
||||
l.testInput(0.1f, 0.1f, 0, 0); run(l, 40); eyeOf(l, a); run(l, 1); eyeOf(l, b);
|
||||
ck(std::hypot(b[0] - a[0], b[2] - a[2]) < 1e-6f, "inside the round deadzone: no drift");
|
||||
}
|
||||
{ // a 0.2 m step up 1 m ahead, and a 20 degree ramp beyond it
|
||||
HomeWorld w = flat;
|
||||
w.colliders.push_back(quadC(WorldCollider::Solid, {-5, 0.2f, -1, 5, 0.2f, -1, 5, 0.2f, -4, -5, 0.2f, -4}));
|
||||
const float rise = std::tan(20 * 3.14159f / 180) * 4;
|
||||
w.colliders.push_back(quadC(WorldCollider::Floor, {-5, 0.2f, -4, 5, 0.2f, -4, 5, 0.2f + rise, -8, -5, 0.2f + rise, -8}));
|
||||
Locomotion l; l.setWorld(w); l.setOptions(1, 1, 0, 30, 1);
|
||||
l.testInput(0, 1, 0, 0);
|
||||
float e[3], prev = 1.7f, maxJump = 0;
|
||||
float onRampErr = 0;
|
||||
for (int i = 0; i < 80 * 7; ++i) {
|
||||
run(l, 1); eyeOf(l, e);
|
||||
maxJump = std::max(maxJump, std::fabs(e[1] - prev));
|
||||
prev = e[1];
|
||||
if (e[2] < -4.6f && e[2] > -7.5f) {
|
||||
const float surface = 0.2f + rise * (-4 - e[2]) / 4;
|
||||
onRampErr = std::max(onRampErr, std::fabs(e[1] - 1.7f - surface));
|
||||
}
|
||||
}
|
||||
ck(maxJump < 0.025f, "a 0.2 m step is eased: never more than 2.5 cm in one frame", maxJump);
|
||||
ck(onRampErr < 0.01f, "a ramp is followed exactly: eyes stay 1.7 m over it walking up", onRampErr);
|
||||
}
|
||||
{ // teleporting: a wall 3 m tall across z = -3 on a 40 m floor
|
||||
HomeWorld w = flat;
|
||||
w.colliders.push_back(quadC(WorldCollider::Solid, {-10, 0, -3, 10, 0, -3, 10, 3, -3, -10, 3, -3}));
|
||||
Locomotion l; l.setWorld(w); l.setOptions(1, 1, 0, 30, 2);
|
||||
float m[12], t[3];
|
||||
const float hand[3] = {0.2f, 1.2f, 0};
|
||||
handPose(hand, 0, 0.25f, -1, m); // lobbed over the wall's top? no: into it
|
||||
l.testInput(0, 0, 0, 1, m); run(l, 2);
|
||||
const bool overWall = l.testTarget(t);
|
||||
ck(!overWall || t[2] > -3, "aiming at the floor behind a wall: nowhere (or in front of it), never behind", overWall, t[2]);
|
||||
ck(l.guide().aiming && l.guide().arc.size() >= 6 && l.guide().arc[l.guide().arc.size() - 1] > -3.01f, "the drawn arc stops at the wall", l.guide().arc[l.guide().arc.size() - 1]);
|
||||
handPose(hand, 0, 0.1f, -1, m); // at the wall, ~0.6 m up
|
||||
l.testInput(0, 0, 0, 1, m); run(l, 2);
|
||||
ck(l.testTarget(t) && t[2] > -3 && t[2] < -2.5f && std::fabs(t[1]) < 0.05f, "aiming at a wall: you land at its foot, in front of it", t[1], t[2]);
|
||||
handPose(hand, 0, 0, -1, m); // at its foot (hits 0.3 m up): lands in front
|
||||
l.testInput(0, 0, 0, 1, m); run(l, 2);
|
||||
ck(l.testTarget(t) && t[2] > -3 && t[2] < -2.6f, "aiming at a wall's foot: you land in front of it", t[2]);
|
||||
const float atWall[3] = {0.2f, 1.2f, -3.2f}; // standing at the wall, hand through it
|
||||
float hd2[16]; std::copy(hd, hd + 16, hd2); hd2[14] = -2.85f;
|
||||
handPose(atWall, 0, -0.5f, -1, m);
|
||||
l.testInput(0, 0, 0, 1, m); l.update(dt, hd2); l.update(dt, hd2);
|
||||
ck(!l.testTarget(t), "a hand reached through the wall aims nothing on the far side");
|
||||
l.testInput(0, 0, 0, 0); run(l, 2);
|
||||
}
|
||||
{ // a ceiling at 2.2 m with a floor on top (a storey above): aiming up never lands on it
|
||||
HomeWorld w = flat;
|
||||
w.colliders.push_back(quadC(WorldCollider::Solid, {-10, 2.2f, -10, 10, 2.2f, -10, 10, 2.2f, 10, -10, 2.2f, 10}));
|
||||
Locomotion l; l.setWorld(w); l.setOptions(1, 1, 0, 30, 2);
|
||||
float m[12], t[3];
|
||||
const float hand[3] = {0.2f, 1.2f, 0};
|
||||
handPose(hand, 0, 1, -1, m);
|
||||
l.testInput(0, 0, 0, 1, m); run(l, 2);
|
||||
ck(!l.testTarget(t) || t[1] < 0.1f, "aiming up into a ceiling: invalid, never onto the storey above", t[1]);
|
||||
}
|
||||
{ // low things: a 0.3 m kerb at z = -1.4 (the arc hits its face low), a table top, a box marked Wall
|
||||
const float hand[3] = {0.2f, 1.2f, 0};
|
||||
float m[12], t[3];
|
||||
handPose(hand, 0, -0.5f, -0.866f, m); // lands ~1.53 m ahead on bare floor
|
||||
{
|
||||
HomeWorld w = flat;
|
||||
w.colliders.push_back(quadC(WorldCollider::Solid, {-5, 0, -1.4f, 5, 0, -1.4f, 5, 0.3f, -1.4f, -5, 0.3f, -1.4f}));
|
||||
Locomotion l; l.setWorld(w); l.setOptions(1, 1, 0, 30, 2);
|
||||
l.testInput(0, 0, 0, 1, m); run(l, 2);
|
||||
ck(l.testTarget(t) && t[2] > -1.4f && t[2] < -1.0f, "the arc hits a kerb's face low: you land at its foot, in front", t[2]);
|
||||
}
|
||||
{
|
||||
HomeWorld w = flat; // a table top 0.7 m up over z -1.2..-2.2
|
||||
w.colliders.push_back(quadC(WorldCollider::Solid, {-1, 0.7f, -1.2f, 1, 0.7f, -1.2f, 1, 0.7f, -2.2f, -1, 0.7f, -2.2f}));
|
||||
Locomotion l; l.setWorld(w); l.setOptions(1, 1, 0, 30, 2);
|
||||
l.testInput(0, 0, 0, 1, m); run(l, 2);
|
||||
ck(!l.testTarget(t), "an arc landing under a table: no headroom, invalid");
|
||||
float m2[12];
|
||||
handPose(hand, 0, -0.15f, -1, m2); // flatter: comes down on the table top
|
||||
l.testInput(0, 0, 0, 1, m2); run(l, 2);
|
||||
ck(l.testTarget(t) && std::fabs(t[1] - 0.7f) < 1e-3f, "a solid table top is floor (the existing rule): you can land on it", t[1], t[2]);
|
||||
}
|
||||
{
|
||||
HomeWorld w = flat;
|
||||
WorldCollider b; b.kind = WorldCollider::Box; b.role = WorldCollider::Wall;
|
||||
b.world[12] = 0; b.world[13] = 0.4f; b.world[14] = -1.7f; b.half[0] = 1; b.half[1] = 0.4f; b.half[2] = 0.5f;
|
||||
w.colliders.push_back(b);
|
||||
Locomotion l; l.setWorld(w); l.setOptions(1, 1, 0, 30, 2);
|
||||
float m2[12];
|
||||
handPose(hand, 0, 0, -1, m2);
|
||||
l.testInput(0, 0, 0, 1, m2); run(l, 2);
|
||||
ck(!l.testTarget(t) || std::fabs(t[1]) < 0.05f, "a box marked Wall is never landed on (its top): only the floor round it", t[1], t[2]);
|
||||
const bool r = l.testTarget(t);
|
||||
ck(l.guide().arc.size() >= 6 && l.guide().arc[l.guide().arc.size() - 3 + 1] <= 0.81f, "the arc stops on the Wall box", r, l.guide().arc[l.guide().arc.size() - 2]);
|
||||
}
|
||||
}
|
||||
{ // seats: pointable, a trigger sits you, markers near you
|
||||
HomeWorld w = flat;
|
||||
WorldPoint s1; s1.name = "Sofa"; s1.pos[0] = 0; s1.pos[1] = 0.45f; s1.pos[2] = -3; s1.yaw = 0; w.seats.push_back(s1);
|
||||
WorldPoint s2; s2.name = "Far bench"; s2.pos[0] = 15; s2.pos[1] = 0.45f; s2.pos[2] = 0; w.seats.push_back(s2);
|
||||
Locomotion l; l.setWorld(w); l.setOptions(1, 1, 0, 30, 2);
|
||||
const float o[3] = {0.2f, 1.2f, 0}, toSeat[3] = {-0.2f / 3.076f, -0.65f / 3.076f, -3 / 3.076f};
|
||||
float t;
|
||||
ck(l.seatOnRay(o, toSeat, 10, t) == 0 && t > 2.5f && t < 3.1f, "a ray at the sofa hits its seat volume", t);
|
||||
const float away[3] = {1, 0, 0};
|
||||
ck(l.seatOnRay(o, away, 10, t) == -1, "a ray elsewhere hits no seat");
|
||||
run(l, 1);
|
||||
ck(l.guide().seats.empty(), "markers: only seats right by you (the sofa 3 m away and the bench 15 m away aren't shown)", static_cast<float>(l.guide().seats.size()));
|
||||
float m[12];
|
||||
const float lh[3] = {-0.2f, 1.2f, 0};
|
||||
handPose(lh, 0.2f, -0.65f, -3, m);
|
||||
l.testInput(0, 0, 0, 0);
|
||||
l.testButtons(false, false, m); run(l, 1);
|
||||
ck(l.pointedSeat(1) == 0 && l.guide().seats.size() == 1 && l.guide().seats[0].hot && l.guide().pointer, "pointing the left hand at it lights its marker and shows the pointer");
|
||||
l.testButtons(false, true, m); run(l, 2);
|
||||
float e[3]; eyeOf(l, e);
|
||||
ck(l.seated() == 0 && std::fabs(e[2] + 3) < 1e-3f && std::fabs(e[1] - (0.45f + 0.78f)) < 1e-3f, "the trigger while pointing sits you in it", e[0], e[1], e[2]);
|
||||
run(l, 1);
|
||||
ck(l.guide().seats.empty(), "seated: your own seat's marker is hidden");
|
||||
l.testButtons(false, false, nullptr);
|
||||
// standing again elsewhere, a slide setting dashes you into it
|
||||
Locomotion l2; l2.setWorld(w); l2.setOptions(1, 2, 0, 30, 2);
|
||||
l2.testInput(0, 0, 0, 0);
|
||||
l2.testButtons(false, false, m); run(l2, 1);
|
||||
l2.testButtons(false, true, m); run(l2, 1);
|
||||
const bool dashing = l2.sliding();
|
||||
run(l2, 30);
|
||||
ck(dashing && l2.seated() == 0, "with slide teleports the trigger dashes you into the seat", dashing, static_cast<float>(l2.seated()));
|
||||
// the teleport arc onto the seat ball
|
||||
Locomotion l3; l3.setWorld(w); l3.setOptions(1, 1, 0, 30, 2);
|
||||
bool seatAimed = false;
|
||||
for (float up = -0.4f; up <= 0.4f && !seatAimed; up += 0.025f) { // some aim at it (whatever the arc's speed)
|
||||
handPose(o, -0.065f, up, -1, m);
|
||||
l3.testInput(0, 0, 0, 1, m); run(l3, 2);
|
||||
seatAimed = l3.testTargetSeat() == 0 && l3.guide().seat;
|
||||
}
|
||||
ck(seatAimed, "the teleport arc hits the seat's volume: it targets the seat", static_cast<float>(l3.testTargetSeat()));
|
||||
}
|
||||
{ // a home with no world layer: floors and walls come from its drawn meshes (walkWorld -> geometryColliders)
|
||||
HslScene sc;
|
||||
auto mesh = [&](const char* name, std::vector<float> p) {
|
||||
MeshData md; md.name = name; md.positions = p; md.nVerts = 4; md.indices = {0, 1, 2, 0, 2, 3}; sc.meshes.push_back(md);
|
||||
};
|
||||
mesh("floor", {-20, 0, -20, 20, 0, -20, 20, 0, 20, -20, 0, 20});
|
||||
mesh("wall", {-10, 0, -3, 10, 0, -3, 10, 3, -3, -10, 3, -3});
|
||||
Locomotion l; l.setWorld(walkWorld(sc)); l.setOptions(1, 1, 0, 30, 2);
|
||||
float m[12], t[3];
|
||||
const float hand[3] = {0.2f, 1.2f, 0};
|
||||
handPose(hand, 0, 0.25f, -1, m);
|
||||
l.testInput(0, 0, 0, 1, m); run(l, 2);
|
||||
const bool ok = l.testTarget(t);
|
||||
ck(!ok || t[2] > -3, "geometry home: the drawn wall stops the arc (never lands behind it)", ok, t[2]);
|
||||
handPose(hand, 0, -0.5f, -0.866f, m);
|
||||
l.testInput(0, 0, 0, 1, m); run(l, 2);
|
||||
ck(l.testTarget(t) && t[2] < 0 && t[2] > -3 && std::fabs(t[1]) < 0.05f, "geometry home: the drawn floor in front is landable", t[1], t[2]);
|
||||
l.testInput(0, 1, 0, 0); run(l, 80 * 3);
|
||||
float e[3]; eyeOf(l, e);
|
||||
ck(e[2] > -3 && e[2] < -2.6f, "geometry home: walking into the drawn wall stops you at it", e[2]);
|
||||
}
|
||||
{ // a Wall-role part blocks at any slope: a 42 degree face (|n.y| 0.74) used to be neither floor nor wall
|
||||
HomeWorld w = flat;
|
||||
const float top = 2 * std::tan(42 * 3.14159f / 180);
|
||||
w.colliders.push_back(quadC(WorldCollider::Wall, {-5, 0, -1, 5, 0, -1, 5, top, -3, -5, top, -3}));
|
||||
Locomotion l; l.setWorld(w); l.setOptions(1, 1, 0, 30, 2);
|
||||
l.testInput(0, 1, 0, 0); run(l, 80 * 3);
|
||||
float e[3]; eyeOf(l, e);
|
||||
ck(e[2] > -1.6f, "walking into a 42 degree Wall-role face stops you", e[2]);
|
||||
float m[12], t[3];
|
||||
const float hand[3] = {0.2f, 1.2f, 0};
|
||||
Locomotion l2; l2.setWorld(w); l2.setOptions(1, 1, 0, 30, 2);
|
||||
handPose(hand, 0, 0.1f, -1, m);
|
||||
l2.testInput(0, 0, 0, 1, m); run(l2, 2);
|
||||
ck(!l2.testTarget(t) || t[2] > -1.0f, "and a teleport arc can't land on or behind it", t[2]);
|
||||
}
|
||||
{ // Meta's chairs carry a big ball collider: hitting it counts as pointing at the chair
|
||||
HomeWorld w = flat;
|
||||
WorldPoint s1; s1.name = "Chair"; s1.pos[0] = 0; s1.pos[1] = 0.45f; s1.pos[2] = -4; w.seats.push_back(s1);
|
||||
WorldCollider b; b.kind = WorldCollider::Sphere; b.role = WorldCollider::Solid; b.radius = 0.6f;
|
||||
b.world[0] = b.world[5] = b.world[10] = 2.45f; b.world[13] = 0.45f; b.world[14] = -4; // 1.47 m across in world
|
||||
w.colliders.push_back(b);
|
||||
Locomotion l; l.setWorld(w);
|
||||
const float o[3] = {0.2f, 1.2f, 0}, d[3] = {-0.2f / 4.05f, -0.65f / 4.05f, -4 / 4.05f};
|
||||
float t;
|
||||
ck(l.seatOnRay(o, d, 10, t) == 0 && t < 3.0f, "a ray stopped by the chair's own (big) collider points at the chair", t);
|
||||
}
|
||||
std::printf("%d failed\n", fails);
|
||||
return fails;
|
||||
}
|
||||
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
cmake_minimum_required(VERSION 3.2...3.25)
|
||||
project(acl CXX)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake")
|
||||
|
||||
include(CMakeUtils)
|
||||
include(CMakeCompiler)
|
||||
include(CMakePlatforms)
|
||||
|
||||
set(USE_AVX_INSTRUCTIONS false CACHE BOOL "Use AVX instructions")
|
||||
set(USE_POPCNT_INSTRUCTIONS false CACHE BOOL "Use POPCOUNT instructions")
|
||||
set(USE_SIMD_INSTRUCTIONS true CACHE BOOL "Use SIMD instructions")
|
||||
set(USE_SJSON true CACHE BOOL "Use SJSON")
|
||||
set(CPU_INSTRUCTION_SET false CACHE STRING "CPU instruction set")
|
||||
set(BUILD_BENCHMARK_EXE false CACHE BOOL "Enable the benchmark projects")
|
||||
set(ENABLE_ALL_WARNINGS false CACHE BOOL "Enable all warnings")
|
||||
set(IS_CROSS_COMPILING false CACHE BOOL "Whether or not we are cross-compiling")
|
||||
|
||||
set(TEST_DATA_DIR "" CACHE STRING "The full path of the regression test data directory")
|
||||
set(DECOMP_DATA_DIR "" CACHE STRING "The full path of the decompression data directory")
|
||||
|
||||
if(CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_CONFIGURATION_TYPES Debug Release)
|
||||
set(CMAKE_CONFIGURATION_TYPES "${CMAKE_CONFIGURATION_TYPES}" CACHE STRING "Reset the configurations to what we need" FORCE)
|
||||
endif()
|
||||
|
||||
# Determine if we should include unit tests
|
||||
set(INCLUDE_UNIT_TESTS true)
|
||||
|
||||
if(PLATFORM_IOS)
|
||||
# Catch2 2.13 doesn't compile on iOS with Xcode 8 and 9
|
||||
if(PLATFORM_XCODE_VERSION MATCHES "^8\\.")
|
||||
set(INCLUDE_UNIT_TESTS false)
|
||||
elseif(PLATFORM_XCODE_VERSION MATCHES "^9\\.")
|
||||
set(INCLUDE_UNIT_TESTS false)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Grab all of our include files
|
||||
file(GLOB_RECURSE ACL_INCLUDE_FILES LIST_DIRECTORIES false
|
||||
${PROJECT_SOURCE_DIR}/includes/*.h
|
||||
${PROJECT_SOURCE_DIR}/docs/*.md
|
||||
${PROJECT_SOURCE_DIR}/cmake/*.cmake
|
||||
${PROJECT_SOURCE_DIR}/test_data/configs/*.sjson
|
||||
${PROJECT_SOURCE_DIR}/tools/release_scripts/*.py
|
||||
)
|
||||
|
||||
create_source_groups("${ACL_INCLUDE_FILES}" ${PROJECT_SOURCE_DIR})
|
||||
|
||||
file(GLOB ACL_ROOT_FILES LIST_DIRECTORIES false
|
||||
${PROJECT_SOURCE_DIR}/*.md
|
||||
${PROJECT_SOURCE_DIR}/*.py
|
||||
${PROJECT_SOURCE_DIR}/.gitignore)
|
||||
|
||||
# Add every natvis file
|
||||
file(GLOB_RECURSE NATVIS_FILES LIST_DIRECTORIES false
|
||||
${PROJECT_SOURCE_DIR}/*.natvis)
|
||||
|
||||
if(MSVC)
|
||||
# With MSVC, the custom target creates a Utility project. Even though it isn't a proper C++ project
|
||||
# that compiles into something, MSVC will still parse the code and report intellisense information.
|
||||
# A utility project doesn't have an editable field in its properties to add an include directory
|
||||
# but adding it anyway like this is enough to make sure the headers are found by intellisense.
|
||||
include_directories("${PROJECT_SOURCE_DIR}/includes")
|
||||
endif()
|
||||
|
||||
# Create a dummy target so they show up in the IDE
|
||||
add_custom_target(${PROJECT_NAME} SOURCES ${ACL_INCLUDE_FILES} ${ACL_ROOT_FILES} ${NATVIS_FILES})
|
||||
|
||||
# Enable CTest
|
||||
enable_testing()
|
||||
|
||||
# Add other projects
|
||||
add_subdirectory("${PROJECT_SOURCE_DIR}/tools/acl_compressor")
|
||||
|
||||
if(BUILD_BENCHMARK_EXE)
|
||||
add_subdirectory("${PROJECT_SOURCE_DIR}/tools/acl_decompressor")
|
||||
endif()
|
||||
|
||||
if(INCLUDE_UNIT_TESTS)
|
||||
add_subdirectory("${PROJECT_SOURCE_DIR}/tests")
|
||||
endif()
|
||||
|
||||
# Custom regression testing (requires SJSON support)
|
||||
if(USE_SJSON)
|
||||
if(PLATFORM_ANDROID)
|
||||
add_subdirectory("${PROJECT_SOURCE_DIR}/tools/regression_tester_android")
|
||||
elseif(PLATFORM_IOS)
|
||||
add_subdirectory("${PROJECT_SOURCE_DIR}/tools/regression_tester_ios")
|
||||
endif()
|
||||
endif()
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Nicholas Frechette & Animation Compression Library contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/compressed_database.h"
|
||||
#include "acl/core/compressed_tracks.h"
|
||||
#include "acl/core/error_result.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
#include "acl/compression/compression_settings.h"
|
||||
#include "acl/compression/output_stats.h"
|
||||
#include "acl/compression/track_array.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Compresses a track array with uniform sampling.
|
||||
//
|
||||
// This compression algorithm is the simplest by far and as such it offers
|
||||
// the fastest compression and decompression. Every sample is retained and
|
||||
// every track has the same number of samples playing back at the same
|
||||
// sample rate. This means that when we sample at a particular time within
|
||||
// the clip, we can trivially calculate the offsets required to read the
|
||||
// desired data. All the data is sorted in order to ensure all reads are
|
||||
// as contiguous as possible for optimal cache locality during decompression.
|
||||
//
|
||||
// allocator: The allocator instance to use to allocate and free memory.
|
||||
// track_list: The track list to compress.
|
||||
// settings: The compression settings to use.
|
||||
// out_compressed_tracks: The resulting compressed tracks. The caller owns the returned memory and must free it.
|
||||
// out_stats: Stat output structure.
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
error_result compress_track_list(iallocator& allocator, const track_array& track_list, const compression_settings& settings,
|
||||
compressed_tracks*& out_compressed_tracks, output_stats& out_stats);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Compresses a transform track array using its additive base with uniform sampling.
|
||||
//
|
||||
// This compression algorithm is the simplest by far and as such it offers
|
||||
// the fastest compression and decompression. Every sample is retained and
|
||||
// every track has the same number of samples playing back at the same
|
||||
// sample rate. This means that when we sample at a particular time within
|
||||
// the clip, we can trivially calculate the offsets required to read the
|
||||
// desired data. All the data is sorted in order to ensure all reads are
|
||||
// as contiguous as possible for optimal cache locality during decompression.
|
||||
//
|
||||
// allocator: The allocator instance to use to allocate and free memory.
|
||||
// track_list: The track list to compress.
|
||||
// settings: The compression settings to use.
|
||||
// out_compressed_tracks: The resulting compressed tracks. The caller owns the returned memory and must free it.
|
||||
// out_stats: Stat output structure.
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
error_result compress_track_list(iallocator& allocator, const track_array_qvvf& track_list, const compression_settings& settings,
|
||||
const track_array_qvvf& additive_base_track_list, additive_clip_format8 additive_format,
|
||||
compressed_tracks*& out_compressed_tracks, output_stats& out_stats);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Takes a list of compressed track instances that contain the contributing error metadata and uses their data to build
|
||||
// a new database instance. Each compressed track instance will be duplicated and split between a new instance and the
|
||||
// database.
|
||||
//
|
||||
// allocator: The allocator instance to use
|
||||
// settings: The settings to use when creating the database
|
||||
// compressed_tracks_list: The list of compressed tracks to build the database with (must have contributing error metadata)
|
||||
// num_compressed_tracks: The number of compressed track instances in the above list
|
||||
// out_compressed_tracks: The output list of compressed tracks bound to the output database (array allocated by the caller (must be large enough); compressed_tracks instances allocated by the function)
|
||||
// out_database: The output database (allocated by the function)
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
error_result build_database(iallocator& allocator, const compression_database_settings& settings,
|
||||
const compressed_tracks* const* compressed_tracks_list, uint32_t num_compressed_tracks,
|
||||
compressed_tracks** out_compressed_tracks, compressed_database*& out_database);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Takes a compressed database with inline bulk data and duplicates it into
|
||||
// a new database instance where the bulk data lives in separate buffers.
|
||||
//
|
||||
// allocator: The allocator instance to use to allocate the new database and its bulk data.
|
||||
// database: The source database to split with inline bulk data.
|
||||
// out_split_database: The new database without inline bulk data.
|
||||
// out_bulk_data_medium: The new database's bulk data for the medium importance tier.
|
||||
// out_bulk_data_low: The new database's bulk data for the low importance tier.
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
error_result split_database_bulk_data(iallocator& allocator, const compressed_database& database, compressed_database*& out_split_database, uint8_t*& out_bulk_data_medium, uint8_t*& out_bulk_data_low);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Takes a compressed database and strips the specified quality tier from it.
|
||||
// The database is duplicated including its remaining bulk data (if inline).
|
||||
// Only the medium/lowest tiers can be stripped.
|
||||
//
|
||||
// allocator: The allocator instance to use to allocate the new database.
|
||||
// database: The source database to strip.
|
||||
// tier: The quality tier to strip.
|
||||
// out_stripped_database: The new database without the specified quality tier.
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
error_result strip_database_quality_tier(iallocator& allocator, const compressed_database& database, quality_tier tier, compressed_database*& out_stripped_database);
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
#include "acl/compression/impl/compress.database.impl.h"
|
||||
#include "acl/compression/impl/compress.scalar.impl.h"
|
||||
#include "acl/compression/impl/compress.transform.impl.h"
|
||||
#include "acl/compression/impl/compress.impl.h"
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/error.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// compression_level8 represents how aggressively we attempt to reduce the memory
|
||||
// footprint. Higher levels will try more permutations and bit rates. The higher
|
||||
// the level, the slower the compression but the smaller the memory footprint.
|
||||
enum class compression_level8 : uint8_t
|
||||
{
|
||||
lowest = 0, // Same as medium for now
|
||||
low = 1, // Same as medium for now
|
||||
medium = 2,
|
||||
high = 3,
|
||||
highest = 4,
|
||||
|
||||
// Automatic attempts to pick the best compression level based on various
|
||||
// properties of the input clip: clip length, longest transform chain, etc.
|
||||
// This offers a nice balance between size and speed for the majority of clips.
|
||||
automatic = 100,
|
||||
|
||||
//lossless = 255, // Not implemented, reserved
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Returns a string representing the compression level.
|
||||
// TODO: constexpr
|
||||
inline const char* get_compression_level_name(compression_level8 level);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Returns the compression level from its string representation.
|
||||
inline bool get_compression_level(const char* level_name, compression_level8& out_level);
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
#include "acl/compression/impl/compression_level.impl.h"
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,276 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2017 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
#include "acl/core/error_result.h"
|
||||
#include "acl/core/hash.h"
|
||||
#include "acl/core/track_formats.h"
|
||||
#include "acl/core/track_types.h"
|
||||
#include "acl/core/range_reduction_types.h"
|
||||
#include "acl/compression/compression_level.h"
|
||||
#include "acl/compression/transform_error_metrics.h"
|
||||
|
||||
#include <rtm/scalarf.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Encapsulates all the compression settings related to database usage.
|
||||
struct compression_database_settings
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// What proportions we should use when distributing our frames based on
|
||||
// their importance to the overall error contribution. If a sample doesn't
|
||||
// go into the medium or low importance tiers, it will end up in the high
|
||||
// importance tier stored within each compressed track instance.
|
||||
// Proportion values must be between 0.0 and 1.0 and their sum as well.
|
||||
// If the sum is less than 1.0, remaining frames are considered to have high
|
||||
// importance. A low importance proportion of 30% means that the least important
|
||||
// 30% of frames will end up in that corresponding database tier.
|
||||
// Note that only movable frames can end up in the database as some frames must remain
|
||||
// within the compressed track instance. A frame is movable if it isn't the first or last
|
||||
// frame of its segment.
|
||||
// Defaults to '0.0' (the medium importance tier is empty)
|
||||
float medium_importance_tier_proportion = 0.0F;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// See above for details.
|
||||
// Defaults to '0.5' (the least important 50% of frames are moved to the database)
|
||||
float low_importance_tier_proportion = 0.5F;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// How large should each chunk be, in bytes.
|
||||
// This value must be at least 4 KB and ideally it should be a multiple of
|
||||
// the virtual memory page size used on the platform that will decompress
|
||||
// from the database.
|
||||
// Defaults to '1 MB'
|
||||
uint32_t max_chunk_size = 1 * 1024 * 1024;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Calculates a hash from the internal state to uniquely identify a configuration.
|
||||
uint32_t get_hash() const;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Checks if everything is valid and if it isn't, returns an error string.
|
||||
// Returns nullptr if the settings are valid.
|
||||
error_result is_valid() const;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Encapsulates all the optional metadata compression settings.
|
||||
struct compression_metadata_settings
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Whether to include the optional metadata for the track list name
|
||||
// Defaults to 'false'
|
||||
bool include_track_list_name = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Whether to include the optional metadata for track names
|
||||
// Defaults to 'false'
|
||||
bool include_track_names = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Whether to include the optional metadata for parent track indices
|
||||
// Transform tracks only
|
||||
// Defaults to 'false'
|
||||
bool include_parent_track_indices = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Whether to include the optional metadata for track descriptions
|
||||
// For transforms, also enables the parent track indices metadata
|
||||
// Defaults to 'false'
|
||||
bool include_track_descriptions = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Whether to include the optional metadata for the contributing error
|
||||
// of each frame. These are sorted from lowest to largest error.
|
||||
// This is required when the compressed tracks will later be merged into
|
||||
// a database.
|
||||
// Transform tracks only
|
||||
// Defaults to 'false'
|
||||
bool include_contributing_error = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Calculates a hash from the internal state to uniquely identify a configuration.
|
||||
uint32_t get_hash() const;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Checks if everything is valid and if it isn't, returns an error string.
|
||||
// Returns nullptr if the settings are valid.
|
||||
error_result is_valid() const;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Encapsulates all frame stripping compression settings.
|
||||
// A keyframe represents all samples at a particular point in time.
|
||||
// Keyframe stripping is a destructive process and visual fidelity can degrade
|
||||
// considerably. If a keyframe is identified as a stripping candidate, it is
|
||||
// entirely removed and reconstructed through linear interpolation of its
|
||||
// remaining neighbors. This is destructive unless the keyframe can be reconstructed
|
||||
// below the desired precision threshold, in which case it is a trivial keyframe.
|
||||
//
|
||||
// Removing whole keyframes ensures that decompression remains very fast.
|
||||
// However, enabling keyframe stripping does add a fixed amount of overhead
|
||||
// during decompression. This overhead should be below 200 instructions and
|
||||
// be on the order of nanoseconds depending on the processor and its clock speed.
|
||||
//
|
||||
// Note that stripping is not always appropriate and it may yield unacceptable
|
||||
// results. Here are known problematic scenarios:
|
||||
// - high velocity animations can lose a lot of momentum, yielding a swimming-
|
||||
// like motion
|
||||
// - synchronized animations can become out of sync if keyframes are unevenly
|
||||
// removed leading to missed contacts
|
||||
// - contacts and motion apex can be missed if key keyframes are removed
|
||||
//
|
||||
// Transform tracks only.
|
||||
struct compression_keyframe_stripping_settings
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Returns whether or not keyframe stripping is enabled.
|
||||
// Keyframe stripping is enabled when either 'proportion' or 'threshold'
|
||||
// or both are enabled.
|
||||
// See [compression_keyframe_stripping_settings] for details.
|
||||
// Defaults to 'false'
|
||||
bool is_enabled() const;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// The minimum proportion of keyframes that should be stripped.
|
||||
// Proportion value must be between 0.0 and 1.0.
|
||||
// Defaults to '0.0' (no stripping)
|
||||
float proportion = 0.0F;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// The threshold error below which to strip keyframes.
|
||||
// Keyframes that yield an error below or equal to this threshold will
|
||||
// be removed. Keyframes that contribute more error than the threshold
|
||||
// are retained.
|
||||
// Defaults to '0.0' centimeters (no stripping)
|
||||
float threshold = 0.0F;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Whether or not to strip trivial keyframes.
|
||||
// If the error introduced by removing a keyframe is below the precision
|
||||
// threshold for every transform, it is trivial. It means that if we reconstruct
|
||||
// the stripped keyframe, the error introduced is below our precision threshold.
|
||||
//
|
||||
// Note that if you intend to use per-track rounding during decompression, you
|
||||
// should keep this feature disabled. See sample_rounding_policy::per_track for
|
||||
// details.
|
||||
//
|
||||
// Defaults to 'false' (no stripping)
|
||||
bool strip_trivial = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Calculates a hash from the internal state to uniquely identify a configuration.
|
||||
uint32_t get_hash() const;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Checks if everything is valid and if it isn't, returns an error string.
|
||||
// Returns nullptr if the settings are valid.
|
||||
error_result is_valid() const;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Encapsulates all the compression settings.
|
||||
struct compression_settings
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// The compression level determines how aggressively we attempt to reduce the memory
|
||||
// footprint. Higher levels will try more permutations and bit rates. The higher
|
||||
// the level, the slower the compression but the smaller the memory footprint.
|
||||
// Transform tracks only.
|
||||
compression_level8 level = compression_level8::low;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// The rotation, translation, and scale formats to use. See functions get_rotation_format(..) and get_vector_format(..)
|
||||
// Defaults to raw: 'quatf_full' and 'vector3f_full'
|
||||
// Transform tracks only.
|
||||
rotation_format8 rotation_format = rotation_format8::quatf_full;
|
||||
vector_format8 translation_format = vector_format8::vector3f_full;
|
||||
vector_format8 scale_format = vector_format8::vector3f_full;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// The error metric to use.
|
||||
// Defaults to 'null', this value must be set manually!
|
||||
// Transform tracks only.
|
||||
itransform_error_metric* error_metric = nullptr;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Whether or not to enable database support on the output compressed clip.
|
||||
// This enables the required metadata which will later be stripped once
|
||||
// the database is built.
|
||||
// Transform tracks only.
|
||||
bool enable_database_support = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Detects if the last sample matches the first for every sub-track.
|
||||
// If it is the case, it is removed and the wrap looping policy is used
|
||||
// otherwise we use the clamp policy and every sample is retained.
|
||||
// See `sample_looping_policy` for details.
|
||||
bool optimize_loops = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Keyframe stripping related settings. See [compression_keyframe_stripping_settings].
|
||||
// Transform tracks only.
|
||||
compression_keyframe_stripping_settings keyframe_stripping;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// These are optional metadata that can be added to compressed clips.
|
||||
compression_metadata_settings metadata;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Calculates a hash from the internal state to uniquely identify a configuration.
|
||||
uint32_t get_hash() const;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Checks if everything is valid and if it isn't, returns an error string.
|
||||
error_result is_valid() const;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Returns raw compression settings. No compression is performed and
|
||||
// samples are all retained with full precision.
|
||||
compression_settings get_raw_compression_settings();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Returns the recommended and default compression settings. These have
|
||||
// been tested in a wide range of scenarios and perform best overall.
|
||||
compression_settings get_default_compression_settings();
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
#include "acl/compression/impl/compression_settings.impl.h"
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2020 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/compression/track_array.h"
|
||||
#include "acl/core/compressed_tracks.h"
|
||||
#include "acl/core/error_result.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Convert a track array instance into a raw compressed tracks instance.
|
||||
// This is a lossless process.
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
error_result convert_track_list(iallocator& allocator, const track_array& track_list, compressed_tracks*& out_compressed_tracks);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Convert a compressed tracks instance into a track array instance.
|
||||
// This is a lossless process if all the metadata is present.
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
error_result convert_track_list(iallocator& allocator, const compressed_tracks& tracks, track_array& out_track_list);
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
#include "acl/compression/impl/convert.impl.h"
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2022 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
|
||||
// track_type8 needs to be fully defined to be able to specialize some templates
|
||||
#include "acl/core/track_types.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// This header provides forward declarations for all public ACL types.
|
||||
// Forward declaring symbols from a 3rd party library is a bad idea, use this
|
||||
// header instead.
|
||||
// See also: https://blog.libtorrent.org/2017/12/forward-declarations-and-abi/
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
enum class compression_level8 : uint8_t;
|
||||
|
||||
struct compression_database_settings;
|
||||
struct compression_metadata_settings;
|
||||
struct compression_settings;
|
||||
|
||||
enum class pre_process_actions;
|
||||
enum class pre_process_precision_policy;
|
||||
struct pre_process_settings_t;
|
||||
|
||||
enum class stat_logging;
|
||||
struct output_stats;
|
||||
|
||||
struct track_error;
|
||||
|
||||
class track_array;
|
||||
template<track_type8 track_type_> class track_array_typed;
|
||||
|
||||
using track_array_float1f = track_array_typed<track_type8::float1f>;
|
||||
using track_array_float2f = track_array_typed<track_type8::float2f>;
|
||||
using track_array_float3f = track_array_typed<track_type8::float3f>;
|
||||
using track_array_float4f = track_array_typed<track_type8::float4f>;
|
||||
using track_array_vector4f = track_array_typed<track_type8::vector4f>;
|
||||
using track_array_qvvf = track_array_typed<track_type8::qvvf>;
|
||||
|
||||
class track;
|
||||
template<track_type8 track_type_> class track_typed;
|
||||
|
||||
using track_float1f = track_typed<track_type8::float1f>;
|
||||
using track_float2f = track_typed<track_type8::float2f>;
|
||||
using track_float3f = track_typed<track_type8::float3f>;
|
||||
using track_float4f = track_typed<track_type8::float4f>;
|
||||
using track_vector4f = track_typed<track_type8::vector4f>;
|
||||
using track_qvvf = track_typed<track_type8::qvvf>;
|
||||
|
||||
enum class additive_clip_format8 : uint8_t;
|
||||
|
||||
class itransform_error_metric;
|
||||
class qvvf_transform_error_metric;
|
||||
class qvvf_precise_transform_error_metric;
|
||||
class qvvf_matrix3x4f_transform_error_metric;
|
||||
template<additive_clip_format8 additive_format> class additive_qvvf_transform_error_metric;
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2020 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/error.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
#include "acl/compression/impl/clip_context.h"
|
||||
#include "acl/compression/impl/segment_context.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
namespace acl_impl
|
||||
{
|
||||
// We use template arguments to support lambdas and avoid using std::function since they might allocate memory
|
||||
// without using our allocator.
|
||||
// group_filter_action_type = std::function<bool(animation_track_type8 group_type, uint32_t bone_index)>
|
||||
template<typename group_filter_action_type>
|
||||
inline void get_num_sub_tracks(const segment_context& segment,
|
||||
group_filter_action_type& group_filter_action,
|
||||
uint32_t& out_num_rotation_sub_tracks, uint32_t& out_num_translation_sub_tracks, uint32_t& out_num_scale_sub_tracks)
|
||||
{
|
||||
uint32_t num_rotation_sub_tracks = 0;
|
||||
uint32_t num_translation_sub_tracks = 0;
|
||||
uint32_t num_scale_sub_tracks = 0;
|
||||
for (uint32_t bone_index = 0; bone_index < segment.num_bones; ++bone_index)
|
||||
{
|
||||
const transform_streams& bone_stream = segment.bone_streams[bone_index];
|
||||
if (bone_stream.output_index == k_invalid_track_index)
|
||||
continue; // Stripped
|
||||
|
||||
if (group_filter_action(animation_track_type8::rotation, bone_index))
|
||||
num_rotation_sub_tracks++;
|
||||
|
||||
if (group_filter_action(animation_track_type8::translation, bone_index))
|
||||
num_translation_sub_tracks++;
|
||||
|
||||
if (group_filter_action(animation_track_type8::scale, bone_index))
|
||||
num_scale_sub_tracks++;
|
||||
}
|
||||
|
||||
out_num_rotation_sub_tracks = num_rotation_sub_tracks;
|
||||
out_num_translation_sub_tracks = num_translation_sub_tracks;
|
||||
out_num_scale_sub_tracks = num_scale_sub_tracks;
|
||||
}
|
||||
|
||||
inline void get_num_animated_sub_tracks(const segment_context& segment,
|
||||
uint32_t& out_num_animated_rotation_sub_tracks, uint32_t& out_num_animated_translation_sub_tracks, uint32_t& out_num_animated_scale_sub_tracks)
|
||||
{
|
||||
const auto animated_group_filter_action = [&segment](animation_track_type8 group_type, uint32_t bone_index)
|
||||
{
|
||||
const transform_streams& bone_stream = segment.bone_streams[bone_index];
|
||||
if (group_type == animation_track_type8::rotation)
|
||||
return !bone_stream.is_rotation_constant;
|
||||
else if (group_type == animation_track_type8::translation)
|
||||
return !bone_stream.is_translation_constant;
|
||||
else
|
||||
return !bone_stream.is_scale_constant;
|
||||
};
|
||||
|
||||
get_num_sub_tracks(segment, animated_group_filter_action, out_num_animated_rotation_sub_tracks, out_num_animated_translation_sub_tracks, out_num_animated_scale_sub_tracks);
|
||||
}
|
||||
|
||||
// We use template arguments to support lambdas and avoid using std::function since they might allocate memory
|
||||
// without using our allocator.
|
||||
// group_filter_action_type = std::function<bool(animation_track_type8 group_type, uint32_t bone_index)>
|
||||
// group_entry_action_type = std::function<void(animation_track_type8 group_type, uint32_t group_size, uint32_t bone_index)>
|
||||
// group_flush_action_type = std::function<void(animation_track_type8 group_type, uint32_t group_size)>
|
||||
template<typename group_filter_action_type, typename group_entry_action_type, typename group_flush_action_type>
|
||||
inline void group_writer(const segment_context& segment, const uint32_t* output_bone_mapping, uint32_t num_output_bones,
|
||||
group_filter_action_type& group_filter_action, group_entry_action_type& group_entry_action, group_flush_action_type& group_flush_action)
|
||||
{
|
||||
// Data is ordered in groups of 4 animated sub-tracks (e.g rot0, rot1, rot2, rot3)
|
||||
// Groups are sorted per sub-track type. All rotation groups come first followed by translations then scales.
|
||||
// The last group of each sub-track may or may not have padding. The last group might be less than 4 sub-tracks.
|
||||
|
||||
const auto group_writer_impl = [output_bone_mapping, num_output_bones, &group_filter_action, &group_entry_action, &group_flush_action](animation_track_type8 group_type)
|
||||
{
|
||||
uint32_t group_size = 0;
|
||||
|
||||
for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index)
|
||||
{
|
||||
const uint32_t bone_index = output_bone_mapping[output_index];
|
||||
|
||||
if (group_filter_action(group_type, bone_index))
|
||||
{
|
||||
group_entry_action(group_type, group_size, bone_index);
|
||||
group_size++;
|
||||
}
|
||||
|
||||
if (group_size == 4)
|
||||
{
|
||||
// Group full, write it out and move onto to the next group
|
||||
group_flush_action(group_type, group_size);
|
||||
group_size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If group has leftover tracks, write it out
|
||||
if (group_size != 0)
|
||||
group_flush_action(group_type, group_size);
|
||||
};
|
||||
|
||||
// Output sub-tracks in natural order: rot, trans, scale
|
||||
group_writer_impl(animation_track_type8::rotation);
|
||||
group_writer_impl(animation_track_type8::translation);
|
||||
|
||||
if (segment.clip->has_scale)
|
||||
group_writer_impl(animation_track_type8::scale);
|
||||
}
|
||||
|
||||
// We use template arguments to support lambdas and avoid using std::function since they might allocate memory
|
||||
// without using our allocator.
|
||||
// group_filter_action_type = std::function<bool(animation_track_type8 group_type, uint32_t bone_index)>
|
||||
// group_entry_action_type = std::function<void(animation_track_type8 group_type, uint32_t group_size, uint32_t bone_index)>
|
||||
// group_flush_action_type = std::function<void(animation_track_type8 group_type, uint32_t group_size)>
|
||||
template<typename group_filter_action_type, typename group_entry_action_type, typename group_flush_action_type>
|
||||
inline void animated_group_writer(const segment_context& segment, const uint32_t* output_bone_mapping, uint32_t num_output_bones,
|
||||
group_filter_action_type& group_filter_action, group_entry_action_type& group_entry_action, group_flush_action_type& group_flush_action)
|
||||
{
|
||||
const auto animated_group_filter_action = [&segment, &group_filter_action](animation_track_type8 group_type, uint32_t bone_index)
|
||||
{
|
||||
const transform_streams& bone_stream = segment.bone_streams[bone_index];
|
||||
if (group_type == animation_track_type8::rotation)
|
||||
return !bone_stream.is_rotation_constant && group_filter_action(group_type, bone_index);
|
||||
else if (group_type == animation_track_type8::translation)
|
||||
return !bone_stream.is_translation_constant && group_filter_action(group_type, bone_index);
|
||||
else
|
||||
return !bone_stream.is_scale_constant && group_filter_action(group_type, bone_index);
|
||||
};
|
||||
|
||||
group_writer(segment, output_bone_mapping, num_output_bones, animated_group_filter_action, group_entry_action, group_flush_action);
|
||||
}
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,259 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2017 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/additive_utils.h"
|
||||
#include "acl/core/bitset.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
#include "acl/core/iterator.h"
|
||||
#include "acl/core/error.h"
|
||||
#include "acl/core/sample_looping_policy.h"
|
||||
#include "acl/core/track_formats.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
#include "acl/compression/compression_settings.h"
|
||||
#include "acl/compression/track_array.h"
|
||||
#include "acl/compression/impl/segment_context.h"
|
||||
#include "acl/compression/impl/topology_metadata.h"
|
||||
|
||||
#include <rtm/quatf.h>
|
||||
#include <rtm/vector4f.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
namespace acl_impl
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Simple iterator utility class to allow easy looping
|
||||
class bone_chain_iterator
|
||||
{
|
||||
public:
|
||||
bone_chain_iterator(const uint32_t* chain, bitset_description chain_desc, uint32_t bone_index, uint32_t offset)
|
||||
: m_bone_chain(chain)
|
||||
, m_bone_chain_desc(chain_desc)
|
||||
, m_bone_index(bone_index)
|
||||
, m_offset(offset)
|
||||
{}
|
||||
|
||||
bone_chain_iterator& operator++()
|
||||
{
|
||||
ACL_ASSERT(m_offset <= m_bone_index, "Cannot increment the iterator, it is no longer valid");
|
||||
|
||||
// Skip the current bone
|
||||
m_offset++;
|
||||
|
||||
// Iterate until we find the next bone part of the chain or until we reach the end of the chain
|
||||
// TODO: Use clz or similar to find the next set bit starting at the current index
|
||||
while (m_offset < m_bone_index && !bitset_test(m_bone_chain, m_bone_chain_desc, m_offset))
|
||||
m_offset++;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
uint32_t operator*() const
|
||||
{
|
||||
ACL_ASSERT(m_offset <= m_bone_index, "Returned bone index doesn't belong to the bone chain");
|
||||
ACL_ASSERT(bitset_test(m_bone_chain, m_bone_chain_desc, m_offset), "Returned bone index doesn't belong to the bone chain");
|
||||
return m_offset;
|
||||
}
|
||||
|
||||
// We only compare the offset in the bone chain. Two iterators on the same bone index
|
||||
// from two different or equal chains will be equal.
|
||||
bool operator==(const bone_chain_iterator& other) const { return m_offset == other.m_offset; }
|
||||
bool operator!=(const bone_chain_iterator& other) const { return m_offset != other.m_offset; }
|
||||
|
||||
private:
|
||||
const uint32_t* m_bone_chain;
|
||||
bitset_description m_bone_chain_desc;
|
||||
uint32_t m_bone_index;
|
||||
uint32_t m_offset;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Simple bone chain container to allow easy looping
|
||||
//
|
||||
// A bone chain allows looping over all bones up to a specific bone starting
|
||||
// at the root bone.
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
struct bone_chain
|
||||
{
|
||||
bone_chain(const uint32_t* chain, bitset_description chain_desc, uint32_t bone_index)
|
||||
: m_bone_chain(chain)
|
||||
, m_bone_chain_desc(chain_desc)
|
||||
, m_bone_index(bone_index)
|
||||
{
|
||||
// We don't know where this bone chain starts, find the root bone
|
||||
// TODO: Use clz or similar to find the next set bit starting at the current index
|
||||
uint32_t root_index = 0;
|
||||
while (!bitset_test(chain, chain_desc, root_index))
|
||||
root_index++;
|
||||
|
||||
m_root_index = root_index;
|
||||
}
|
||||
|
||||
acl_impl::bone_chain_iterator begin() const { return acl_impl::bone_chain_iterator(m_bone_chain, m_bone_chain_desc, m_bone_index, m_root_index); }
|
||||
acl_impl::bone_chain_iterator end() const { return acl_impl::bone_chain_iterator(m_bone_chain, m_bone_chain_desc, m_bone_index, m_bone_index + 1); }
|
||||
|
||||
const uint32_t* m_bone_chain;
|
||||
bitset_description m_bone_chain_desc;
|
||||
uint32_t m_root_index;
|
||||
uint32_t m_bone_index;
|
||||
};
|
||||
|
||||
// Metadata per transform
|
||||
struct transform_metadata
|
||||
{
|
||||
// The transform chain this transform belongs to (points into leaf_transform_chains in owner context)
|
||||
const uint32_t* transform_chain = nullptr;
|
||||
|
||||
// Parent transform index of this transform, invalid if at the root
|
||||
uint32_t parent_index = k_invalid_track_index;
|
||||
|
||||
// The precision value from the track description for this transform
|
||||
float precision = 0.0F;
|
||||
|
||||
// The local space shell distance from the track description for this transform
|
||||
float shell_distance = 0.0F;
|
||||
};
|
||||
|
||||
// Rigid shell information per transform
|
||||
struct rigid_shell_metadata_t
|
||||
{
|
||||
// Dominant local space shell distance (from transform tip)
|
||||
float local_shell_distance; // TODO: rename this, remove 'local' from name, it is implied
|
||||
|
||||
// Precision required on the surface of the rigid shell
|
||||
float precision;
|
||||
|
||||
// The index of the dominant transform (current transform index for leaf transforms)
|
||||
uint32_t dominant_transform_index;
|
||||
};
|
||||
|
||||
// Represents the working space for a clip (raw or lossy)
|
||||
struct clip_context
|
||||
{
|
||||
// List of segments contained (num_segments present)
|
||||
// Raw contexts only have a single segment
|
||||
segment_context* segments = nullptr;
|
||||
|
||||
// List of clip wide range information for each transform (num_bones present)
|
||||
transform_range* ranges = nullptr;
|
||||
|
||||
// List of metadata for each transform (num_bones present)
|
||||
// TODO: Same for raw/lossy/additive clip context, can we share?
|
||||
transform_metadata* metadata = nullptr;
|
||||
|
||||
// List of bit sets for each leaf transform to track transform chains (num_leaf_transforms present)
|
||||
// TODO: Same for raw/lossy/additive clip context, can we share?
|
||||
uint32_t* leaf_transform_chains = nullptr;
|
||||
|
||||
// List of transform indices sorted by parent first then sibling transforms are sorted by their transform index (num_bones present)
|
||||
// TODO: Same for raw/lossy/additive clip context, can we share?
|
||||
uint32_t* sorted_transforms_parent_first = nullptr;
|
||||
|
||||
// List of shell metadata for each transform (num_bones present)
|
||||
// Data is aggregate of whole clip
|
||||
// Shared between all clip contexts, not owned
|
||||
const rigid_shell_metadata_t* clip_shell_metadata = nullptr;
|
||||
|
||||
// Topology metadata
|
||||
// Data is aggregate of whole clip
|
||||
// Shared between all clip contexts, not owned
|
||||
const clip_topology_t* topology = nullptr;
|
||||
|
||||
// Optional if we request it in the compression settings
|
||||
// Sorted by stripping order within this clip
|
||||
keyframe_stripping_metadata_t* contributing_error = nullptr;
|
||||
|
||||
uint32_t num_segments = 0;
|
||||
uint32_t num_bones = 0; // TODO: Rename num_transforms
|
||||
uint32_t num_samples_allocated = 0;
|
||||
uint32_t num_samples = 0;
|
||||
float sample_rate = 0.0F;
|
||||
|
||||
float duration = 0.0F;
|
||||
|
||||
sample_looping_policy looping_policy = sample_looping_policy::non_looping;
|
||||
additive_clip_format8 additive_format = additive_clip_format8::none;
|
||||
|
||||
bool are_rotations_normalized = false;
|
||||
bool are_translations_normalized = false;
|
||||
bool are_scales_normalized = false;
|
||||
bool has_scale = false;
|
||||
bool has_additive_base = false;
|
||||
bool has_stripped_keyframes = false;
|
||||
|
||||
uint32_t num_leaf_transforms = 0;
|
||||
|
||||
iallocator* allocator = nullptr; // Never null if the context is initialized
|
||||
|
||||
// Stat tracking
|
||||
uint32_t decomp_touched_bytes = 0;
|
||||
uint32_t decomp_touched_cache_lines = 0;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool is_initialized() const { return allocator != nullptr; }
|
||||
iterator<segment_context> segment_iterator() { return iterator<segment_context>(segments, num_segments); }
|
||||
const_iterator<segment_context> segment_iterator() const { return const_iterator<segment_context>(segments, num_segments); }
|
||||
|
||||
bone_chain get_bone_chain(uint32_t bone_index) const
|
||||
{
|
||||
ACL_ASSERT(bone_index < num_bones, "Invalid bone index: %u >= %u", bone_index, num_bones);
|
||||
const transform_metadata& meta = metadata[bone_index];
|
||||
return bone_chain(meta.transform_chain, bitset_description::make_from_num_bits(num_bones), bone_index);
|
||||
}
|
||||
};
|
||||
|
||||
template<class clip_adapter_t>
|
||||
void sort_transform_indices_parent_first(
|
||||
const clip_adapter_t& clip,
|
||||
uint32_t* sorted_transforms_parent_first,
|
||||
uint32_t num_transforms);
|
||||
|
||||
bool initialize_clip_context(
|
||||
iallocator& allocator,
|
||||
const track_array_qvvf& track_list,
|
||||
const compression_settings& settings,
|
||||
additive_clip_format8 additive_format,
|
||||
clip_context& out_clip_context);
|
||||
void destroy_clip_context(clip_context& context);
|
||||
|
||||
constexpr bool segment_context_has_scale(const segment_context& segment) { return segment.clip->has_scale; }
|
||||
constexpr bool bone_streams_has_scale(const transform_streams& bone_streams) { return segment_context_has_scale(*bone_streams.segment); }
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
#include "acl/compression/impl/clip_context.impl.h"
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,312 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2023 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/additive_utils.h"
|
||||
#include "acl/core/bitset.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
#include "acl/core/iterator.h"
|
||||
#include "acl/core/error.h"
|
||||
#include "acl/core/sample_looping_policy.h"
|
||||
#include "acl/core/track_formats.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
#include "acl/compression/compression_settings.h"
|
||||
#include "acl/compression/track_array.h"
|
||||
#include "acl/compression/impl/segment_context.h"
|
||||
#include "acl/compression/impl/transform_clip_adapters.h"
|
||||
#include "acl/math/quatf.h"
|
||||
|
||||
#include <rtm/quatf.h>
|
||||
#include <rtm/vector4f.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
namespace acl_impl
|
||||
{
|
||||
template<class clip_adapter_t>
|
||||
inline void sort_transform_indices_parent_first(
|
||||
const clip_adapter_t& clip,
|
||||
uint32_t* sorted_transforms_parent_first,
|
||||
uint32_t num_transforms)
|
||||
{
|
||||
static_assert(std::is_base_of<transform_clip_adapter_t, clip_adapter_t>::value, "Clip adapter must derive from transform_clip_adapter_t");
|
||||
|
||||
// We sort our transform indices by parent first
|
||||
// If two transforms have the same parent index, we sort them by their transform index
|
||||
const auto sort_predicate = [&clip](const uint32_t lhs_transform_index, const uint32_t rhs_transform_index)
|
||||
{
|
||||
const uint32_t lhs_parent_index = clip.get_transform_parent_index(lhs_transform_index);
|
||||
const uint32_t rhs_parent_index = clip.get_transform_parent_index(rhs_transform_index);
|
||||
|
||||
// If the transforms don't have the same parent, sort by the parent index
|
||||
// We add 1 to parent indices to cause the invalid index to wrap around to 0
|
||||
// since parents come first, they'll have the lowest value
|
||||
if (lhs_parent_index != rhs_parent_index)
|
||||
return (lhs_parent_index + 1) < (rhs_parent_index + 1);
|
||||
|
||||
// Both transforms have the same parent, sort by their index
|
||||
return lhs_transform_index < rhs_transform_index;
|
||||
};
|
||||
|
||||
std::sort(sorted_transforms_parent_first, sorted_transforms_parent_first + num_transforms, sort_predicate);
|
||||
}
|
||||
|
||||
inline bool initialize_clip_context(iallocator& allocator, const track_array_qvvf& track_list, const compression_settings& settings, additive_clip_format8 additive_format, clip_context& out_clip_context)
|
||||
{
|
||||
const uint32_t num_transforms = track_list.get_num_tracks();
|
||||
const uint32_t num_samples = track_list.get_num_samples_per_track();
|
||||
const float sample_rate = track_list.get_sample_rate();
|
||||
const sample_looping_policy looping_policy = track_list.get_looping_policy();
|
||||
|
||||
// Create a single segment with the whole clip
|
||||
out_clip_context.segments = allocate_type_array<segment_context>(allocator, 1);
|
||||
out_clip_context.ranges = nullptr;
|
||||
out_clip_context.metadata = allocate_type_array<transform_metadata>(allocator, num_transforms);
|
||||
out_clip_context.leaf_transform_chains = nullptr;
|
||||
out_clip_context.sorted_transforms_parent_first = allocate_type_array<uint32_t>(allocator, num_transforms);
|
||||
out_clip_context.clip_shell_metadata = nullptr;
|
||||
out_clip_context.topology = nullptr;
|
||||
out_clip_context.contributing_error = nullptr;
|
||||
out_clip_context.num_segments = 1;
|
||||
out_clip_context.num_bones = num_transforms;
|
||||
out_clip_context.num_samples_allocated = num_samples;
|
||||
out_clip_context.num_samples = num_samples;
|
||||
out_clip_context.sample_rate = sample_rate;
|
||||
out_clip_context.duration = track_list.get_finite_duration();
|
||||
out_clip_context.looping_policy = looping_policy;
|
||||
out_clip_context.additive_format = additive_format;
|
||||
out_clip_context.are_rotations_normalized = false;
|
||||
out_clip_context.are_translations_normalized = false;
|
||||
out_clip_context.are_scales_normalized = false;
|
||||
out_clip_context.has_additive_base = additive_format != additive_clip_format8::none;
|
||||
out_clip_context.num_leaf_transforms = 0;
|
||||
out_clip_context.allocator = &allocator;
|
||||
|
||||
bool are_samples_valid = true;
|
||||
|
||||
segment_context& segment = out_clip_context.segments[0];
|
||||
|
||||
transform_streams* bone_streams = allocate_type_array<transform_streams>(allocator, num_transforms);
|
||||
|
||||
for (uint32_t transform_index = 0; transform_index < num_transforms; ++transform_index)
|
||||
{
|
||||
const track_qvvf& track = track_list[transform_index];
|
||||
const track_desc_transformf& desc = track.get_description();
|
||||
|
||||
transform_streams& bone_stream = bone_streams[transform_index];
|
||||
|
||||
bone_stream.segment = &segment;
|
||||
bone_stream.bone_index = transform_index;
|
||||
bone_stream.parent_bone_index = desc.parent_index;
|
||||
bone_stream.output_index = desc.output_index;
|
||||
bone_stream.default_value = desc.default_value;
|
||||
|
||||
bone_stream.rotations = rotation_track_stream(allocator, num_samples, sizeof(rtm::quatf), sample_rate, rotation_format8::quatf_full);
|
||||
bone_stream.translations = translation_track_stream(allocator, num_samples, sizeof(rtm::vector4f), sample_rate, vector_format8::vector3f_full);
|
||||
bone_stream.scales = scale_track_stream(allocator, num_samples, sizeof(rtm::vector4f), sample_rate, vector_format8::vector3f_full);
|
||||
|
||||
// Constant and default detection is handled during sub-track compacting
|
||||
bone_stream.is_rotation_constant = false;
|
||||
bone_stream.is_rotation_default = false;
|
||||
bone_stream.is_translation_constant = false;
|
||||
bone_stream.is_translation_default = false;
|
||||
bone_stream.is_scale_constant = false;
|
||||
bone_stream.is_scale_default = false;
|
||||
|
||||
for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
|
||||
{
|
||||
const rtm::qvvf& transform = track[sample_index];
|
||||
|
||||
// If we request raw data and we are already normalized, retain the original value
|
||||
// otherwise we normalize for safety
|
||||
rtm::quatf rotation;
|
||||
if (settings.rotation_format != rotation_format8::quatf_full || !rtm::quat_is_normalized(transform.rotation))
|
||||
rotation = quat_normalize_stable(transform.rotation);
|
||||
else
|
||||
rotation = transform.rotation;
|
||||
|
||||
are_samples_valid &= rtm::quat_is_finite(rotation);
|
||||
are_samples_valid &= rtm::vector_is_finite3(transform.translation);
|
||||
are_samples_valid &= rtm::vector_is_finite3(transform.scale);
|
||||
|
||||
bone_stream.rotations.set_raw_sample(sample_index, rotation);
|
||||
bone_stream.translations.set_raw_sample(sample_index, transform.translation);
|
||||
bone_stream.scales.set_raw_sample(sample_index, transform.scale);
|
||||
}
|
||||
|
||||
transform_metadata& metadata = out_clip_context.metadata[transform_index];
|
||||
metadata.transform_chain = nullptr;
|
||||
metadata.parent_index = desc.parent_index;
|
||||
metadata.precision = desc.precision;
|
||||
metadata.shell_distance = desc.shell_distance;
|
||||
|
||||
out_clip_context.sorted_transforms_parent_first[transform_index] = transform_index;
|
||||
}
|
||||
|
||||
out_clip_context.has_scale = true; // Scale detection is handled during sub-track compacting
|
||||
out_clip_context.decomp_touched_bytes = 0;
|
||||
out_clip_context.decomp_touched_cache_lines = 0;
|
||||
|
||||
segment.bone_streams = bone_streams;
|
||||
segment.clip = &out_clip_context;
|
||||
segment.ranges = nullptr;
|
||||
segment.contributing_error = nullptr;
|
||||
segment.num_samples_allocated = num_samples;
|
||||
segment.num_samples = num_samples;
|
||||
segment.num_bones = num_transforms;
|
||||
segment.clip_sample_offset = 0;
|
||||
segment.segment_index = 0;
|
||||
segment.are_rotations_normalized = false;
|
||||
segment.are_translations_normalized = false;
|
||||
segment.are_scales_normalized = false;
|
||||
|
||||
segment.animated_rotation_bit_size = 0;
|
||||
segment.animated_translation_bit_size = 0;
|
||||
segment.animated_scale_bit_size = 0;
|
||||
segment.animated_pose_bit_size = 0;
|
||||
segment.animated_data_size = 0;
|
||||
segment.range_data_size = 0;
|
||||
segment.total_header_size = 0;
|
||||
|
||||
// Initialize our hierarchy information
|
||||
if (num_transforms != 0)
|
||||
{
|
||||
// Calculate which bones are leaf bones that have no children
|
||||
bitset_description bone_bitset_desc = bitset_description::make_from_num_bits(num_transforms);
|
||||
const size_t bitset_size = bone_bitset_desc.get_size();
|
||||
uint32_t* is_leaf_bitset = allocate_type_array<uint32_t>(allocator, bitset_size);
|
||||
bitset_reset(is_leaf_bitset, bone_bitset_desc, false);
|
||||
|
||||
// By default everything is marked as a leaf
|
||||
// We'll then iterate on every transform and mark their parent as non-leaf
|
||||
bitset_set_range(is_leaf_bitset, bone_bitset_desc, 0, num_transforms, true);
|
||||
|
||||
#if defined(ACL_HAS_ASSERT_CHECKS)
|
||||
uint32_t num_root_bones = 0;
|
||||
#endif
|
||||
|
||||
// Move and validate the input data
|
||||
for (uint32_t transform_index = 0; transform_index < num_transforms; ++transform_index)
|
||||
{
|
||||
const transform_metadata& metadata = out_clip_context.metadata[transform_index];
|
||||
|
||||
const bool is_root = metadata.parent_index == k_invalid_track_index;
|
||||
|
||||
// If we have a parent, mark it as not being a leaf bone (it has at least one child)
|
||||
if (!is_root)
|
||||
bitset_set(is_leaf_bitset, bone_bitset_desc, metadata.parent_index, false);
|
||||
|
||||
#if defined(ACL_HAS_ASSERT_CHECKS)
|
||||
if (is_root)
|
||||
num_root_bones++;
|
||||
#endif
|
||||
}
|
||||
|
||||
const uint32_t num_leaf_transforms = bitset_count_set_bits(is_leaf_bitset, bone_bitset_desc);
|
||||
out_clip_context.num_leaf_transforms = num_leaf_transforms;
|
||||
|
||||
// Build our transform chains
|
||||
// Each leaf transform is part of a unique chain
|
||||
// When a leaf is found, we assign it a new chain. We then iterate through the parent
|
||||
// transforms, adding them to the chain
|
||||
// Each non-leaf transform visited is assigned the first chain that contains it
|
||||
// This allows easy traversal between one transform and its parents
|
||||
uint32_t* leaf_transform_chains = allocate_type_array<uint32_t>(allocator, num_leaf_transforms * bitset_size);
|
||||
out_clip_context.leaf_transform_chains = leaf_transform_chains;
|
||||
|
||||
uint32_t leaf_index = 0;
|
||||
for (uint32_t transform_index = 0; transform_index < num_transforms; ++transform_index)
|
||||
{
|
||||
if (!bitset_test(is_leaf_bitset, bone_bitset_desc, transform_index))
|
||||
continue; // Skip non-leaf bones
|
||||
|
||||
uint32_t* bone_chain = leaf_transform_chains + (leaf_index * bitset_size);
|
||||
bitset_reset(bone_chain, bone_bitset_desc, false);
|
||||
|
||||
uint32_t chain_bone_index = transform_index;
|
||||
while (chain_bone_index != k_invalid_track_index)
|
||||
{
|
||||
bitset_set(bone_chain, bone_bitset_desc, chain_bone_index, true);
|
||||
|
||||
transform_metadata& metadata = out_clip_context.metadata[chain_bone_index];
|
||||
|
||||
// We assign a bone chain the first time we find a bone that isn't part of one already
|
||||
if (metadata.transform_chain == nullptr)
|
||||
metadata.transform_chain = bone_chain;
|
||||
|
||||
chain_bone_index = metadata.parent_index;
|
||||
}
|
||||
|
||||
leaf_index++;
|
||||
}
|
||||
|
||||
ACL_ASSERT(num_root_bones > 0, "No root bone found. The root bones must have a parent index = 0xFFFF");
|
||||
ACL_ASSERT(leaf_index == num_leaf_transforms, "Invalid number of leaf bone found");
|
||||
deallocate_type_array(allocator, is_leaf_bitset, bitset_size);
|
||||
|
||||
// We sort our transform indices by parent first
|
||||
// If two transforms have the same parent index, we sort them by their transform index
|
||||
sort_transform_indices_parent_first(
|
||||
transform_clip_context_adapter_t(out_clip_context),
|
||||
out_clip_context.sorted_transforms_parent_first,
|
||||
num_transforms);
|
||||
}
|
||||
|
||||
return are_samples_valid;
|
||||
}
|
||||
|
||||
inline void destroy_clip_context(clip_context& context)
|
||||
{
|
||||
if (context.allocator == nullptr)
|
||||
return; // Not initialized
|
||||
|
||||
iallocator& allocator = *context.allocator;
|
||||
|
||||
for (segment_context& segment : context.segment_iterator())
|
||||
destroy_segment_context(allocator, segment);
|
||||
|
||||
deallocate_type_array(allocator, context.segments, context.num_segments);
|
||||
deallocate_type_array(allocator, context.ranges, context.num_bones);
|
||||
deallocate_type_array(allocator, context.metadata, context.num_bones);
|
||||
|
||||
bitset_description bone_bitset_desc = bitset_description::make_from_num_bits(context.num_bones);
|
||||
deallocate_type_array(allocator, context.leaf_transform_chains, size_t(context.num_leaf_transforms) * bone_bitset_desc.get_size());
|
||||
|
||||
deallocate_type_array(allocator, context.sorted_transforms_parent_first, context.num_bones);
|
||||
deallocate_type_array(allocator, context.contributing_error, context.num_samples);
|
||||
}
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/bitset.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
#include "acl/core/track_desc.h"
|
||||
#include "acl/compression/impl/track_list_context.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
namespace acl_impl
|
||||
{
|
||||
inline bool is_scalarf_track_constant(const track& track_, const track_range& range)
|
||||
{
|
||||
const track_desc_scalarf& desc = track_.get_description<track_desc_scalarf>();
|
||||
return range.is_constant(desc.precision);
|
||||
}
|
||||
|
||||
inline void extract_constant_tracks(track_list_context& context)
|
||||
{
|
||||
ACL_ASSERT(context.is_valid(), "Invalid context");
|
||||
|
||||
const bitset_description bitset_desc = bitset_description::make_from_num_bits(context.num_tracks);
|
||||
|
||||
context.constant_tracks_bitset = allocate_type_array<uint32_t>(*context.allocator, bitset_desc.get_size());
|
||||
bitset_reset(context.constant_tracks_bitset, bitset_desc, false);
|
||||
|
||||
for (uint32_t track_index = 0; track_index < context.num_tracks; ++track_index)
|
||||
{
|
||||
const track& mut_track = context.track_list[track_index];
|
||||
const track_range& range = context.range_list[track_index];
|
||||
|
||||
bool is_constant = false;
|
||||
switch (range.category)
|
||||
{
|
||||
case track_category8::scalarf:
|
||||
is_constant = is_scalarf_track_constant(mut_track, range);
|
||||
break;
|
||||
case track_category8::scalard:
|
||||
case track_category8::transformf:
|
||||
case track_category8::transformd:
|
||||
default:
|
||||
ACL_ASSERT(false, "Invalid track category");
|
||||
break;
|
||||
}
|
||||
|
||||
bitset_set(context.constant_tracks_bitset, bitset_desc, track_index, is_constant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2020 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Included only once from compress.h
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/buffer_tag.h"
|
||||
#include "acl/core/compressed_tracks.h"
|
||||
#include "acl/core/error.h"
|
||||
#include "acl/core/error_result.h"
|
||||
#include "acl/core/floating_point_control.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
#include "acl/compression/compression_settings.h"
|
||||
#include "acl/compression/output_stats.h"
|
||||
#include "acl/compression/track_array.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
inline error_result compress_track_list(iallocator& allocator, const track_array& track_list, const compression_settings& settings, compressed_tracks*& out_compressed_tracks, output_stats& out_stats)
|
||||
{
|
||||
using namespace acl_impl;
|
||||
|
||||
error_result result = track_list.is_valid();
|
||||
if (result.any())
|
||||
return result;
|
||||
|
||||
// Disable floating point exceptions during compression because we leverage all SIMD lanes
|
||||
// and we might intentionally divide by zero, etc.
|
||||
scope_disable_fp_exceptions fp_off;
|
||||
|
||||
if (track_list.get_track_category() == track_category8::transformf)
|
||||
result = compress_transform_track_list(allocator, track_array_cast<track_array_qvvf>(track_list), settings, nullptr, additive_clip_format8::none, out_compressed_tracks, out_stats);
|
||||
else
|
||||
result = compress_scalar_track_list(allocator, track_list, settings, out_compressed_tracks, out_stats);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline error_result compress_track_list(iallocator& allocator, const track_array_qvvf& track_list, const compression_settings& settings, const track_array_qvvf& additive_base_track_list, additive_clip_format8 additive_format, compressed_tracks*& out_compressed_tracks, output_stats& out_stats)
|
||||
{
|
||||
using namespace acl_impl;
|
||||
|
||||
error_result result = track_list.is_valid();
|
||||
if (result.any())
|
||||
return result;
|
||||
|
||||
if (additive_format != additive_clip_format8::none)
|
||||
{
|
||||
result = additive_base_track_list.is_valid();
|
||||
if (result.any())
|
||||
return result;
|
||||
}
|
||||
|
||||
// Disable floating point exceptions during compression because we leverage all SIMD lanes
|
||||
// and we might intentionally divide by zero, etc.
|
||||
scope_disable_fp_exceptions fp_off;
|
||||
|
||||
return compress_transform_track_list(allocator, track_list, settings, &additive_base_track_list, additive_format, out_compressed_tracks, out_stats);
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,275 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2020 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Included only once from compress.h
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/buffer_tag.h"
|
||||
#include "acl/core/compressed_tracks.h"
|
||||
#include "acl/core/compressed_tracks_version.h"
|
||||
#include "acl/core/error.h"
|
||||
#include "acl/core/error_result.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
#include "acl/core/scope_profiler.h"
|
||||
#include "acl/core/impl/bit_cast.impl.h"
|
||||
#include "acl/compression/compression_settings.h"
|
||||
#include "acl/compression/output_stats.h"
|
||||
#include "acl/compression/track_array.h"
|
||||
#include "acl/compression/impl/track_list_context.h"
|
||||
#include "acl/compression/impl/compact.scalar.h"
|
||||
#include "acl/compression/impl/normalize.scalar.h"
|
||||
#include "acl/compression/impl/optimize_looping.scalar.h"
|
||||
#include "acl/compression/impl/quantize.scalar.h"
|
||||
#include "acl/compression/impl/track_range_impl.h"
|
||||
#include "acl/compression/impl/write_compression_stats_impl.h"
|
||||
#include "acl/compression/impl/write_track_data_impl.h"
|
||||
#include "acl/compression/impl/write_track_metadata.h"
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
#include <sjson/writer.h>
|
||||
#endif
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
namespace acl_impl
|
||||
{
|
||||
inline error_result compress_scalar_track_list(iallocator& allocator, const track_array& track_list, const compression_settings& settings, compressed_tracks*& out_compressed_tracks, output_stats& out_stats)
|
||||
{
|
||||
(void)out_stats;
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
scope_profiler compression_time;
|
||||
#endif
|
||||
|
||||
track_list_context context;
|
||||
if (!initialize_context(allocator, track_list, context))
|
||||
return error_result("Some samples are not finite");
|
||||
|
||||
// Wrap instead of clamp if we loop
|
||||
optimize_looping(context, settings);
|
||||
|
||||
// Extract our ranges now, we need it for compacting the constant tracks
|
||||
extract_track_ranges(context);
|
||||
|
||||
// Compact and collapse the constant tracks
|
||||
extract_constant_tracks(context);
|
||||
|
||||
// Normalize our samples into the track wide ranges per track
|
||||
normalize_tracks(context);
|
||||
|
||||
// Find how many bits we need per track and quantize everything
|
||||
quantize_tracks(context);
|
||||
|
||||
// Done transforming our input tracks, time to pack them into their final form
|
||||
const uint32_t per_track_metadata_size = write_track_metadata(context, nullptr);
|
||||
const uint32_t constant_values_size = write_track_constant_values(context, nullptr);
|
||||
const uint32_t range_values_size = write_track_range_values(context, nullptr);
|
||||
const uint32_t animated_num_bits = write_track_animated_values(context, nullptr);
|
||||
const uint32_t animated_values_size = (animated_num_bits + 7) / 8; // Round up to nearest byte
|
||||
const uint32_t num_bits_per_frame = context.num_samples != 0 ? (animated_num_bits / context.num_samples) : 0;
|
||||
|
||||
uint32_t buffer_size = 0;
|
||||
buffer_size += sizeof(raw_buffer_header); // Header
|
||||
buffer_size += sizeof(tracks_header); // Header
|
||||
buffer_size += sizeof(scalar_tracks_header); // Header
|
||||
ACL_ASSERT(is_aligned_to(buffer_size, alignof(track_metadata)), "Invalid alignment");
|
||||
buffer_size += per_track_metadata_size; // Per track metadata
|
||||
buffer_size = align_to(buffer_size, 4); // Align constant values
|
||||
buffer_size += constant_values_size; // Constant values
|
||||
ACL_ASSERT(is_aligned_to(buffer_size, 4), "Invalid alignment");
|
||||
buffer_size += range_values_size; // Range values
|
||||
ACL_ASSERT(is_aligned_to(buffer_size, 4), "Invalid alignment");
|
||||
buffer_size += animated_values_size; // Animated values
|
||||
|
||||
// Optional metadata
|
||||
const uint32_t metadata_start_offset = align_to(buffer_size, 4);
|
||||
const uint32_t metadata_track_list_name_size = settings.metadata.include_track_list_name ? write_track_list_name(track_list, nullptr) : 0;
|
||||
const uint32_t metadata_track_names_size = settings.metadata.include_track_names ? write_track_names(track_list, context.track_output_indices, context.num_output_tracks, nullptr) : 0;
|
||||
const uint32_t metadata_track_descriptions_size = settings.metadata.include_track_descriptions ? write_track_descriptions(track_list, context.track_output_indices, context.num_output_tracks, nullptr) : 0;
|
||||
|
||||
uint32_t metadata_size = 0;
|
||||
metadata_size += metadata_track_list_name_size;
|
||||
metadata_size = align_to(metadata_size, 4);
|
||||
metadata_size += metadata_track_names_size;
|
||||
metadata_size = align_to(metadata_size, 4);
|
||||
metadata_size += metadata_track_descriptions_size;
|
||||
|
||||
if (metadata_size != 0)
|
||||
{
|
||||
buffer_size = align_to(buffer_size, 4);
|
||||
buffer_size += metadata_size;
|
||||
|
||||
buffer_size = align_to(buffer_size, 4);
|
||||
buffer_size += sizeof(optional_metadata_header);
|
||||
}
|
||||
else
|
||||
buffer_size += 15; // Ensure we have sufficient padding for unaligned 16 byte loads
|
||||
|
||||
uint8_t* buffer = allocate_type_array_aligned<uint8_t>(allocator, buffer_size, alignof(compressed_tracks));
|
||||
std::memset(buffer, 0, buffer_size);
|
||||
|
||||
uint8_t* buffer_start = buffer;
|
||||
out_compressed_tracks = bit_cast<compressed_tracks*>(buffer);
|
||||
|
||||
raw_buffer_header* buffer_header = safe_ptr_cast<raw_buffer_header>(buffer);
|
||||
buffer += sizeof(raw_buffer_header);
|
||||
|
||||
tracks_header* header = safe_ptr_cast<tracks_header>(buffer);
|
||||
buffer += sizeof(tracks_header);
|
||||
|
||||
// Write our primary header
|
||||
header->tag = static_cast<uint32_t>(buffer_tag32::compressed_tracks);
|
||||
header->version = compressed_tracks_version16::latest;
|
||||
header->algorithm_type = algorithm_type8::uniformly_sampled;
|
||||
header->track_type = track_list.get_track_type();
|
||||
header->num_tracks = context.num_output_tracks;
|
||||
header->num_samples = context.num_output_tracks != 0 ? context.num_samples : 0;
|
||||
header->sample_rate = context.num_output_tracks != 0 ? context.sample_rate : 0.0F;
|
||||
header->set_is_wrap_optimized(context.looping_policy == sample_looping_policy::wrap);
|
||||
header->set_has_metadata(metadata_size != 0);
|
||||
|
||||
// Write our scalar tracks header
|
||||
scalar_tracks_header* scalars_header = safe_ptr_cast<scalar_tracks_header>(buffer);
|
||||
buffer += sizeof(scalar_tracks_header);
|
||||
|
||||
scalars_header->num_bits_per_frame = num_bits_per_frame;
|
||||
|
||||
const uint8_t* packed_data_start_offset = buffer - sizeof(scalar_tracks_header); // Relative to our header
|
||||
scalars_header->metadata_per_track = uint32_t(buffer - packed_data_start_offset);
|
||||
buffer += per_track_metadata_size;
|
||||
buffer = align_to(buffer, 4);
|
||||
scalars_header->track_constant_values = uint32_t(buffer - packed_data_start_offset);
|
||||
buffer += constant_values_size;
|
||||
scalars_header->track_range_values = uint32_t(buffer - packed_data_start_offset);
|
||||
buffer += range_values_size;
|
||||
scalars_header->track_animated_values = uint32_t(buffer - packed_data_start_offset);
|
||||
buffer += animated_values_size;
|
||||
|
||||
if (metadata_size != 0)
|
||||
{
|
||||
buffer = align_to(buffer, 4);
|
||||
buffer += metadata_size;
|
||||
|
||||
buffer = align_to(buffer, 4);
|
||||
buffer += sizeof(optional_metadata_header);
|
||||
}
|
||||
else
|
||||
buffer += 15;
|
||||
|
||||
(void)buffer_start; // Avoid VS2017 bug, it falsely reports this variable as unused even when asserts are enabled
|
||||
ACL_ASSERT((buffer_start + buffer_size) == buffer, "Buffer size and pointer mismatch");
|
||||
|
||||
// Write our compressed data
|
||||
track_metadata* per_track_metadata = scalars_header->get_track_metadata();
|
||||
write_track_metadata(context, per_track_metadata);
|
||||
|
||||
float* constant_values = scalars_header->get_track_constant_values();
|
||||
write_track_constant_values(context, constant_values);
|
||||
|
||||
float* range_values = scalars_header->get_track_range_values();
|
||||
write_track_range_values(context, range_values);
|
||||
|
||||
uint8_t* animated_values = scalars_header->get_track_animated_values();
|
||||
write_track_animated_values(context, animated_values);
|
||||
|
||||
// Optional metadata header is last
|
||||
uint32_t writter_metadata_track_list_name_size = 0;
|
||||
uint32_t written_metadata_track_names_size = 0;
|
||||
uint32_t written_metadata_track_descriptions_size = 0;
|
||||
if (metadata_size != 0)
|
||||
{
|
||||
optional_metadata_header* metadada_header = bit_cast<optional_metadata_header*>(buffer_start + buffer_size - sizeof(optional_metadata_header));
|
||||
uint32_t metadata_offset = metadata_start_offset;
|
||||
|
||||
if (settings.metadata.include_track_list_name)
|
||||
{
|
||||
metadada_header->track_list_name = metadata_offset;
|
||||
writter_metadata_track_list_name_size = write_track_list_name(track_list, metadada_header->get_track_list_name(*out_compressed_tracks));
|
||||
metadata_offset += writter_metadata_track_list_name_size;
|
||||
}
|
||||
else
|
||||
metadada_header->track_list_name = invalid_ptr_offset();
|
||||
|
||||
if (settings.metadata.include_track_names)
|
||||
{
|
||||
metadata_offset = align_to(metadata_offset, 4);
|
||||
metadada_header->track_name_offsets = metadata_offset;
|
||||
written_metadata_track_names_size = write_track_names(track_list, context.track_output_indices, context.num_output_tracks, metadada_header->get_track_name_offsets(*out_compressed_tracks));
|
||||
metadata_offset += written_metadata_track_names_size;
|
||||
}
|
||||
else
|
||||
metadada_header->track_name_offsets = invalid_ptr_offset();
|
||||
|
||||
metadada_header->parent_track_indices = invalid_ptr_offset(); // Not supported for scalar tracks
|
||||
|
||||
if (settings.metadata.include_track_descriptions)
|
||||
{
|
||||
metadata_offset = align_to(metadata_offset, 4);
|
||||
metadada_header->track_descriptions = metadata_offset;
|
||||
written_metadata_track_descriptions_size = write_track_descriptions(track_list, context.track_output_indices, context.num_output_tracks, metadada_header->get_track_descriptions(*out_compressed_tracks));
|
||||
metadata_offset += written_metadata_track_descriptions_size;
|
||||
}
|
||||
else
|
||||
metadada_header->track_descriptions = invalid_ptr_offset();
|
||||
}
|
||||
|
||||
ACL_ASSERT(writter_metadata_track_list_name_size == metadata_track_list_name_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_metadata_track_names_size == metadata_track_names_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_metadata_track_descriptions_size == metadata_track_descriptions_size, "Wrote too little or too much data");
|
||||
|
||||
// Finish the raw buffer header
|
||||
buffer_header->size = buffer_size;
|
||||
buffer_header->hash = hash32(safe_ptr_cast<const uint8_t>(header), buffer_size - sizeof(raw_buffer_header)); // Hash everything but the raw buffer header
|
||||
|
||||
#if defined(ACL_HAS_ASSERT_CHECKS)
|
||||
if (metadata_size == 0)
|
||||
{
|
||||
for (const uint8_t* padding = buffer - 15; padding < buffer; ++padding)
|
||||
ACL_ASSERT(*padding == 0, "Padding was overwritten");
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
compression_time.stop();
|
||||
|
||||
if (out_stats.logging != stat_logging::none)
|
||||
write_compression_stats(context, *out_compressed_tracks, compression_time, out_stats);
|
||||
#endif
|
||||
|
||||
return error_result();
|
||||
}
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,632 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2021 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Included only once from compress.h
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/buffer_tag.h"
|
||||
#include "acl/core/compressed_tracks.h"
|
||||
#include "acl/core/compressed_tracks_version.h"
|
||||
#include "acl/core/error.h"
|
||||
#include "acl/core/error_result.h"
|
||||
#include "acl/core/floating_point_control.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
#include "acl/core/scope_profiler.h"
|
||||
#include "acl/core/impl/bit_cast.impl.h"
|
||||
#include "acl/compression/compression_settings.h"
|
||||
#include "acl/compression/output_stats.h"
|
||||
#include "acl/compression/track_array.h"
|
||||
#include "acl/compression/impl/clip_context.h"
|
||||
#include "acl/compression/impl/compression_stats.h"
|
||||
#include "acl/compression/impl/track_stream.h"
|
||||
#include "acl/compression/impl/convert_rotation.transform.h"
|
||||
#include "acl/compression/impl/compact.transform.h"
|
||||
#include "acl/compression/impl/keyframe_stripping.h"
|
||||
#include "acl/compression/impl/normalize.transform.h"
|
||||
#include "acl/compression/impl/optimize_looping.transform.h"
|
||||
#include "acl/compression/impl/quantize.transform.h"
|
||||
#include "acl/compression/impl/segment.transform.h"
|
||||
#include "acl/compression/impl/topology_metadata.h"
|
||||
#include "acl/compression/impl/write_segment_data.h"
|
||||
#include "acl/compression/impl/write_stats.h"
|
||||
#include "acl/compression/impl/write_stream_data.h"
|
||||
#include "acl/compression/impl/write_sub_track_types.h"
|
||||
#include "acl/compression/impl/write_track_metadata.h"
|
||||
|
||||
#include <rtm/quatf.h>
|
||||
#include <rtm/vector4f.h>
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
#include <sjson/writer.h>
|
||||
#endif
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
namespace acl_impl
|
||||
{
|
||||
inline bool has_trivial_default_values(const track_array_qvvf& track_list, additive_clip_format8 additive_format, const clip_context& lossy_clip_context)
|
||||
{
|
||||
// Additive clips and regular clips can have zero/one scale and be trivial
|
||||
bool all_scales_zero = true;
|
||||
bool all_scales_one = true;
|
||||
|
||||
const rtm::vector4f zero = rtm::vector_zero();
|
||||
const rtm::vector4f one = rtm::vector_set(1.0F);
|
||||
|
||||
for (const track_qvvf& track : track_list)
|
||||
{
|
||||
const track_desc_transformf& desc = track.get_description();
|
||||
|
||||
if (!rtm::quat_near_identity(desc.default_value.rotation, 0.0F))
|
||||
return false; // Not the identity
|
||||
|
||||
if (!rtm::vector_all_near_equal3(desc.default_value.translation, zero, 0.0F))
|
||||
return false; // Not zero
|
||||
|
||||
if (!rtm::vector_all_near_equal3(desc.default_value.scale, zero, 0.0F))
|
||||
all_scales_zero = false; // Not zero
|
||||
if (!rtm::vector_all_near_equal3(desc.default_value.scale, one, 0.0F))
|
||||
all_scales_one = false; // Not one
|
||||
}
|
||||
|
||||
if (lossy_clip_context.has_scale)
|
||||
{
|
||||
if (additive_format == additive_clip_format8::additive1 && !all_scales_zero)
|
||||
return false; // Not zero
|
||||
else if (!all_scales_one)
|
||||
return false; // Not one
|
||||
}
|
||||
|
||||
// Every default value was trivial
|
||||
return true;
|
||||
}
|
||||
|
||||
// These values are hard-coded
|
||||
// They were selected based on empirical data
|
||||
inline compression_level8 find_best_compression_level(const clip_context& lossy_clip_context)
|
||||
{
|
||||
// Very long clips stay need to be fast
|
||||
if (lossy_clip_context.num_samples > 500)
|
||||
return compression_level8::medium;
|
||||
|
||||
uint32_t longest_chain_length = 0;
|
||||
const bitset_description transform_bitset_desc = bitset_description::make_from_num_bits(lossy_clip_context.num_bones);
|
||||
for (uint32_t leaf_index = 0; leaf_index < lossy_clip_context.num_leaf_transforms; ++leaf_index)
|
||||
{
|
||||
const uint32_t* transform_chain = lossy_clip_context.leaf_transform_chains + (leaf_index * transform_bitset_desc.get_size());
|
||||
const uint32_t num_chain_transforms = bitset_count_set_bits(transform_chain, transform_bitset_desc);
|
||||
longest_chain_length = std::max<uint32_t>(longest_chain_length, num_chain_transforms);
|
||||
}
|
||||
|
||||
// Clips with shorter transform chains are fast and can try harder
|
||||
if (longest_chain_length < 18)
|
||||
return compression_level8::highest;
|
||||
else if (longest_chain_length < 25)
|
||||
return compression_level8::high;
|
||||
else
|
||||
return compression_level8::medium;
|
||||
}
|
||||
|
||||
inline error_result compress_transform_track_list(iallocator& allocator, const track_array_qvvf& track_list, compression_settings settings,
|
||||
const track_array_qvvf* additive_base_track_list, additive_clip_format8 additive_format,
|
||||
compressed_tracks*& out_compressed_tracks, output_stats& out_stats)
|
||||
{
|
||||
error_result result = settings.is_valid();
|
||||
if (result.any())
|
||||
return result;
|
||||
|
||||
compression_stats_t compression_stats;
|
||||
(void)compression_stats;
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
scope_profiler compression_time;
|
||||
scope_profiler initialization_time;
|
||||
#endif
|
||||
|
||||
// Segmenting settings are an implementation detail
|
||||
compression_segmenting_settings segmenting_settings;
|
||||
|
||||
// If we enable database support or keyframe stripping, include the metadata we need
|
||||
bool remove_contributing_error = false;
|
||||
if (settings.enable_database_support)
|
||||
settings.metadata.include_contributing_error = true;
|
||||
else if (settings.keyframe_stripping.is_enabled())
|
||||
{
|
||||
// If we only enable the contributing error for keyframe stripping, make sure to strip it afterwards
|
||||
remove_contributing_error = !settings.metadata.include_contributing_error;
|
||||
settings.metadata.include_contributing_error = true;
|
||||
}
|
||||
|
||||
// If every track is retains full precision, we disable segmenting since it provides no benefit
|
||||
if (!is_rotation_format_variable(settings.rotation_format) && !is_vector_format_variable(settings.translation_format) && !is_vector_format_variable(settings.scale_format))
|
||||
{
|
||||
if (settings.metadata.include_contributing_error)
|
||||
return error_result("Raw tracks have no contributing error");
|
||||
|
||||
segmenting_settings.ideal_num_samples = 0xFFFFFFFF;
|
||||
segmenting_settings.max_num_samples = 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
if (settings.metadata.include_contributing_error && segmenting_settings.max_num_samples > 32)
|
||||
return error_result("Cannot have more than 32 samples per segment when calculating the contributing error per frame");
|
||||
|
||||
// If we want the optional track descriptions, make sure to include the parent track indices
|
||||
if (settings.metadata.include_track_descriptions)
|
||||
settings.metadata.include_parent_track_indices = true;
|
||||
|
||||
ACL_ASSERT(settings.is_valid().empty(), "Invalid compression settings");
|
||||
ACL_ASSERT(segmenting_settings.is_valid().empty(), "Invalid segmenting settings");
|
||||
|
||||
// Variable bit rate tracks need range reduction
|
||||
// Full precision tracks do not need range reduction since samples are stored raw
|
||||
range_reduction_flags8 range_reduction = range_reduction_flags8::none;
|
||||
if (is_rotation_format_variable(settings.rotation_format))
|
||||
range_reduction |= range_reduction_flags8::rotations;
|
||||
|
||||
if (is_vector_format_variable(settings.translation_format))
|
||||
range_reduction |= range_reduction_flags8::translations;
|
||||
|
||||
if (is_vector_format_variable(settings.scale_format))
|
||||
range_reduction |= range_reduction_flags8::scales;
|
||||
|
||||
// If we have no additive base, our additive format is always none
|
||||
if (additive_base_track_list == nullptr || additive_base_track_list->is_empty())
|
||||
additive_format = additive_clip_format8::none;
|
||||
|
||||
clip_context raw_clip_context;
|
||||
if (!initialize_clip_context(allocator, track_list, settings, additive_format, raw_clip_context))
|
||||
return error_result("Some samples are not finite");
|
||||
|
||||
clip_context lossy_clip_context;
|
||||
initialize_clip_context(allocator, track_list, settings, additive_format, lossy_clip_context);
|
||||
|
||||
const bool is_additive = additive_format != additive_clip_format8::none;
|
||||
clip_context additive_base_clip_context;
|
||||
if (is_additive && !initialize_clip_context(allocator, *additive_base_track_list, settings, additive_format, additive_base_clip_context))
|
||||
return error_result("Some base samples are not finite");
|
||||
|
||||
if (settings.level == compression_level8::automatic)
|
||||
settings.level = find_best_compression_level(lossy_clip_context);
|
||||
|
||||
// Clip topology metadata, not specific to clip context
|
||||
clip_topology_t clip_topology;
|
||||
build_clip_topology(allocator, track_list, clip_topology);
|
||||
|
||||
raw_clip_context.topology = &clip_topology;
|
||||
lossy_clip_context.topology = &clip_topology;
|
||||
if (is_additive)
|
||||
additive_base_clip_context.topology = &clip_topology;
|
||||
|
||||
// Topology dependent data, not specific to clip context
|
||||
const uint32_t num_input_transforms = raw_clip_context.num_bones;
|
||||
rigid_shell_metadata_t* clip_shell_metadata = compute_clip_shell_distances(
|
||||
allocator,
|
||||
transform_clip_context_adapter_t(raw_clip_context),
|
||||
transform_clip_context_adapter_t(additive_base_clip_context));
|
||||
|
||||
#if defined(ACL_IMPL_DEBUG_ENABLE_DOMINANT_DESCENDANTS)
|
||||
compute_dominance_map(
|
||||
allocator,
|
||||
transform_clip_context_adapter_t(raw_clip_context),
|
||||
transform_clip_context_adapter_t(additive_base_clip_context),
|
||||
*settings.error_metric,
|
||||
clip_topology);
|
||||
#endif
|
||||
|
||||
raw_clip_context.clip_shell_metadata = clip_shell_metadata;
|
||||
lossy_clip_context.clip_shell_metadata = clip_shell_metadata;
|
||||
if (is_additive)
|
||||
additive_base_clip_context.clip_shell_metadata = clip_shell_metadata;
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
compression_stats.initialization_elapsed_seconds = initialization_time.get_elapsed_seconds();
|
||||
#endif
|
||||
|
||||
// Wrap instead of clamp if we loop
|
||||
optimize_looping(lossy_clip_context, additive_base_clip_context, settings, compression_stats);
|
||||
|
||||
// Convert our rotations if we need to
|
||||
convert_rotation_streams(allocator, lossy_clip_context, settings.rotation_format, compression_stats);
|
||||
|
||||
// Extract our clip ranges now, we need it for compacting the constant streams
|
||||
extract_clip_bone_ranges(allocator, lossy_clip_context, compression_stats);
|
||||
|
||||
// Compact and collapse the constant streams
|
||||
compact_constant_streams(allocator, lossy_clip_context, additive_base_clip_context, track_list, settings, compression_stats);
|
||||
|
||||
uint32_t clip_range_data_size = 0;
|
||||
if (range_reduction != range_reduction_flags8::none)
|
||||
{
|
||||
// Normalize our samples into the clip wide ranges per bone
|
||||
normalize_clip_streams(lossy_clip_context, range_reduction, compression_stats);
|
||||
clip_range_data_size = get_clip_range_data_size(lossy_clip_context, range_reduction, settings.rotation_format);
|
||||
}
|
||||
|
||||
segment_streams(allocator, lossy_clip_context, segmenting_settings, compression_stats);
|
||||
|
||||
// If we have a single segment, skip segment range reduction since it won't help
|
||||
if (range_reduction != range_reduction_flags8::none && lossy_clip_context.num_segments > 1)
|
||||
{
|
||||
// Extract and fixup our segment wide ranges per bone
|
||||
extract_segment_bone_ranges(allocator, lossy_clip_context, compression_stats);
|
||||
|
||||
// Normalize our samples into the segment wide ranges per bone
|
||||
normalize_segment_streams(lossy_clip_context, range_reduction, compression_stats);
|
||||
}
|
||||
|
||||
// Find how many bits we need per sub-track and quantize everything
|
||||
quantize_streams(allocator, lossy_clip_context, settings, segmenting_settings, raw_clip_context, additive_base_clip_context, out_stats, compression_stats);
|
||||
|
||||
uint32_t num_output_bones = 0;
|
||||
uint32_t* output_bone_mapping = create_output_track_mapping(allocator, track_list, num_output_bones);
|
||||
|
||||
// Calculate the pose size, we need it to estimate savings when stripping keyframes
|
||||
calculate_animated_data_size(lossy_clip_context, output_bone_mapping, num_output_bones);
|
||||
|
||||
// Remove whole keyframes as needed
|
||||
strip_keyframes(lossy_clip_context, settings, compression_stats);
|
||||
|
||||
// Compression is done! Time to pack things.
|
||||
#if defined(ACL_USE_SJSON)
|
||||
scope_profiler output_packing_time;
|
||||
#endif
|
||||
|
||||
if (remove_contributing_error)
|
||||
settings.metadata.include_contributing_error = false;
|
||||
|
||||
const bool has_trivial_defaults = has_trivial_default_values(track_list, additive_format, lossy_clip_context);
|
||||
|
||||
const uint32_t constant_data_size = get_constant_data_size(lossy_clip_context);
|
||||
|
||||
uint32_t num_animated_variable_sub_tracks_padded = 0;
|
||||
const uint32_t format_per_track_data_size = get_format_per_track_data_size(lossy_clip_context, settings.rotation_format, settings.translation_format, settings.scale_format, &num_animated_variable_sub_tracks_padded);
|
||||
|
||||
const uint32_t num_sub_tracks_per_bone = lossy_clip_context.has_scale ? 3U : 2U;
|
||||
|
||||
// Calculate how many sub-track packed entries we have
|
||||
// Each sub-track is 2 bits packed within a 32 bit entry
|
||||
// For each sub-track type, we round up to simplify bookkeeping
|
||||
// For example, if we have 3 tracks made up of rotation/translation we'll have one entry for each with unused padding
|
||||
// All rotation types come first, followed by all translation types, and with scale types at the end when present
|
||||
const uint32_t num_sub_track_entries = ((num_output_bones + k_num_sub_tracks_per_packed_entry - 1) / k_num_sub_tracks_per_packed_entry) * num_sub_tracks_per_bone;
|
||||
const uint32_t packed_sub_track_buffer_size = num_sub_track_entries * sizeof(packed_sub_track_types);
|
||||
|
||||
// Adding an extra index at the end to delimit things, the index is always invalid: 0xFFFFFFFF
|
||||
const uint32_t segment_start_indices_size = lossy_clip_context.num_segments > 1 ? (uint32_t(sizeof(uint32_t)) * (lossy_clip_context.num_segments + 1)) : 0;
|
||||
const uint32_t segment_header_type_size = lossy_clip_context.has_stripped_keyframes ? sizeof(stripped_segment_header_t) : sizeof(segment_header);
|
||||
const uint32_t segment_headers_size = segment_header_type_size * lossy_clip_context.num_segments;
|
||||
|
||||
uint32_t buffer_size = 0;
|
||||
// Per clip data
|
||||
buffer_size += sizeof(raw_buffer_header); // Header
|
||||
buffer_size += sizeof(tracks_header); // Header
|
||||
buffer_size += sizeof(transform_tracks_header); // Header
|
||||
|
||||
const uint32_t clip_header_size = buffer_size;
|
||||
|
||||
buffer_size = align_to(buffer_size, 4); // Align segment start indices
|
||||
buffer_size += segment_start_indices_size; // Segment start indices
|
||||
buffer_size = align_to(buffer_size, 4); // Align segment headers
|
||||
buffer_size += segment_headers_size; // Segment headers
|
||||
buffer_size = align_to(buffer_size, 4); // Align sub-track types
|
||||
|
||||
const uint32_t clip_segment_header_size = buffer_size - clip_header_size;
|
||||
|
||||
buffer_size += packed_sub_track_buffer_size; // Packed sub-track types sorted by type
|
||||
buffer_size = align_to(buffer_size, 4); // Align constant track data
|
||||
buffer_size += constant_data_size; // Constant track data
|
||||
buffer_size = align_to(buffer_size, 4); // Align range data
|
||||
buffer_size += clip_range_data_size; // Range data
|
||||
|
||||
const uint32_t clip_data_size = buffer_size - clip_segment_header_size - clip_header_size;
|
||||
|
||||
if (are_all_enum_flags_set(out_stats.logging, stat_logging::detailed))
|
||||
{
|
||||
constexpr uint32_t k_cache_line_byte_size = 64;
|
||||
lossy_clip_context.decomp_touched_bytes = clip_header_size + clip_data_size;
|
||||
lossy_clip_context.decomp_touched_bytes += sizeof(uint32_t) * 4; // We touch at most 4 segment start indices
|
||||
lossy_clip_context.decomp_touched_bytes += segment_header_type_size * 2; // We touch at most 2 segment headers
|
||||
lossy_clip_context.decomp_touched_cache_lines = align_to(clip_header_size, k_cache_line_byte_size) / k_cache_line_byte_size;
|
||||
lossy_clip_context.decomp_touched_cache_lines += align_to(clip_data_size, k_cache_line_byte_size) / k_cache_line_byte_size;
|
||||
lossy_clip_context.decomp_touched_cache_lines += 1; // All 4 segment start indices should fit in a cache line
|
||||
lossy_clip_context.decomp_touched_cache_lines += 1; // Both segment headers should fit in a cache line
|
||||
}
|
||||
|
||||
// Per segment data
|
||||
for (segment_context& segment : lossy_clip_context.segment_iterator())
|
||||
{
|
||||
const uint32_t header_start = buffer_size;
|
||||
|
||||
buffer_size += format_per_track_data_size; // Format per track data
|
||||
|
||||
// TODO: Alignment only necessary with 16bit per component (segment constant tracks), need to fix scalar decoding path
|
||||
buffer_size = align_to(buffer_size, 2); // Align range data
|
||||
buffer_size += segment.range_data_size; // Range data
|
||||
|
||||
const uint32_t header_end = buffer_size;
|
||||
|
||||
// TODO: Variable bit rate doesn't need alignment
|
||||
buffer_size = align_to(buffer_size, 4); // Align animated data
|
||||
buffer_size += segment.animated_data_size; // Animated track data
|
||||
|
||||
segment.segment_data_size = buffer_size - header_start;
|
||||
segment.total_header_size = header_end - header_start;
|
||||
}
|
||||
|
||||
const uint32_t segment_data_size = buffer_size - clip_data_size - clip_segment_header_size - clip_header_size;
|
||||
|
||||
// Optional metadata
|
||||
const uint32_t metadata_start_offset = align_to(buffer_size, 4);
|
||||
const uint32_t metadata_track_list_name_size = settings.metadata.include_track_list_name ? write_track_list_name(track_list, nullptr) : 0;
|
||||
const uint32_t metadata_track_names_size = settings.metadata.include_track_names ? write_track_names(track_list, output_bone_mapping, num_output_bones, nullptr) : 0;
|
||||
const uint32_t metadata_parent_track_indices_size = settings.metadata.include_parent_track_indices ? write_parent_track_indices(track_list, output_bone_mapping, num_output_bones, nullptr) : 0;
|
||||
const uint32_t metadata_track_descriptions_size = settings.metadata.include_track_descriptions ? write_track_descriptions(track_list, output_bone_mapping, num_output_bones, nullptr) : 0;
|
||||
const uint32_t metadata_contributing_error_size = settings.metadata.include_contributing_error ? write_contributing_error(lossy_clip_context, nullptr) : 0;
|
||||
|
||||
uint32_t metadata_size = 0;
|
||||
metadata_size += metadata_track_list_name_size;
|
||||
metadata_size = align_to(metadata_size, 4);
|
||||
metadata_size += metadata_track_names_size;
|
||||
metadata_size = align_to(metadata_size, 4);
|
||||
metadata_size += metadata_parent_track_indices_size;
|
||||
metadata_size = align_to(metadata_size, 4);
|
||||
metadata_size += metadata_track_descriptions_size;
|
||||
metadata_size = align_to(metadata_size, 4);
|
||||
metadata_size += metadata_contributing_error_size;
|
||||
|
||||
if (metadata_size != 0)
|
||||
{
|
||||
buffer_size = align_to(buffer_size, 4);
|
||||
buffer_size += metadata_size;
|
||||
|
||||
buffer_size = align_to(buffer_size, 4);
|
||||
buffer_size += sizeof(optional_metadata_header);
|
||||
}
|
||||
else
|
||||
buffer_size += 15; // Ensure we have sufficient padding for unaligned 16 byte loads
|
||||
|
||||
uint8_t* buffer = allocate_type_array_aligned<uint8_t>(allocator, buffer_size, alignof(compressed_tracks));
|
||||
std::memset(buffer, 0, buffer_size);
|
||||
|
||||
uint8_t* buffer_start = buffer;
|
||||
out_compressed_tracks = bit_cast<compressed_tracks*>(buffer);
|
||||
|
||||
raw_buffer_header* buffer_header = safe_ptr_cast<raw_buffer_header>(buffer);
|
||||
buffer += sizeof(raw_buffer_header);
|
||||
|
||||
tracks_header* header = safe_ptr_cast<tracks_header>(buffer);
|
||||
buffer += sizeof(tracks_header);
|
||||
|
||||
// Write our primary header
|
||||
header->tag = static_cast<uint32_t>(buffer_tag32::compressed_tracks);
|
||||
header->version = compressed_tracks_version16::latest;
|
||||
header->algorithm_type = algorithm_type8::uniformly_sampled;
|
||||
header->track_type = track_list.get_track_type();
|
||||
header->num_tracks = num_output_bones;
|
||||
header->num_samples = num_output_bones != 0 ? lossy_clip_context.num_samples : 0;
|
||||
header->sample_rate = num_output_bones != 0 ? lossy_clip_context.sample_rate : 0.0F;
|
||||
header->set_rotation_format(settings.rotation_format);
|
||||
header->set_translation_format(settings.translation_format);
|
||||
header->set_scale_format(settings.scale_format);
|
||||
header->set_has_scale(lossy_clip_context.has_scale);
|
||||
// Our default scale is 1.0 if we have no additive base or if we don't use 'additive1', otherwise it is 0.0
|
||||
header->set_default_scale(!is_additive || additive_format != additive_clip_format8::additive1 ? 1U : 0U);
|
||||
header->set_has_database(false);
|
||||
header->set_has_trivial_default_values(has_trivial_defaults);
|
||||
header->set_has_stripped_keyframes(lossy_clip_context.has_stripped_keyframes);
|
||||
header->set_is_wrap_optimized(lossy_clip_context.looping_policy == sample_looping_policy::wrap);
|
||||
header->set_has_metadata(metadata_size != 0);
|
||||
|
||||
// Write our transform tracks header
|
||||
transform_tracks_header* transforms_header = safe_ptr_cast<transform_tracks_header>(buffer);
|
||||
buffer += sizeof(transform_tracks_header);
|
||||
|
||||
transforms_header->num_segments = lossy_clip_context.num_segments;
|
||||
transforms_header->num_animated_variable_sub_tracks = num_animated_variable_sub_tracks_padded;
|
||||
get_num_constant_samples(lossy_clip_context, transforms_header->num_constant_rotation_samples, transforms_header->num_constant_translation_samples, transforms_header->num_constant_scale_samples);
|
||||
get_num_animated_sub_tracks(lossy_clip_context.segments[0],
|
||||
transforms_header->num_animated_rotation_sub_tracks, transforms_header->num_animated_translation_sub_tracks, transforms_header->num_animated_scale_sub_tracks);
|
||||
|
||||
const uint32_t segment_start_indices_offset = align_to<uint32_t>(sizeof(transform_tracks_header), 4); // Relative to the start of our transform_tracks_header
|
||||
transforms_header->database_header_offset = invalid_ptr_offset();
|
||||
transforms_header->segment_headers_offset = align_to(segment_start_indices_offset + segment_start_indices_size, 4);
|
||||
transforms_header->sub_track_types_offset = align_to(transforms_header->segment_headers_offset + segment_headers_size, 4);
|
||||
transforms_header->constant_track_data_offset = align_to(transforms_header->sub_track_types_offset + packed_sub_track_buffer_size, 4);
|
||||
transforms_header->clip_range_data_offset = align_to(transforms_header->constant_track_data_offset + constant_data_size, 4);
|
||||
|
||||
uint32_t written_segment_start_indices_size = 0;
|
||||
if (lossy_clip_context.num_segments > 1)
|
||||
written_segment_start_indices_size = write_segment_start_indices(lossy_clip_context, transforms_header->get_segment_start_indices());
|
||||
|
||||
const uint32_t segment_data_start_offset = transforms_header->clip_range_data_offset + clip_range_data_size;
|
||||
const uint32_t written_segment_headers_size = write_segment_headers(lossy_clip_context, settings, transforms_header->get_segment_headers(), segment_data_start_offset);
|
||||
|
||||
uint32_t written_sub_track_buffer_size = 0;
|
||||
written_sub_track_buffer_size += write_packed_sub_track_types(lossy_clip_context, transforms_header->get_sub_track_types(), output_bone_mapping, num_output_bones);
|
||||
|
||||
uint32_t written_constant_data_size = 0;
|
||||
if (constant_data_size != 0)
|
||||
written_constant_data_size = write_constant_track_data(lossy_clip_context, settings.rotation_format, transforms_header->get_constant_track_data(), constant_data_size, output_bone_mapping, num_output_bones);
|
||||
|
||||
uint32_t written_clip_range_data_size = 0;
|
||||
if (range_reduction != range_reduction_flags8::none)
|
||||
written_clip_range_data_size = write_clip_range_data(lossy_clip_context, range_reduction, transforms_header->get_clip_range_data(), clip_range_data_size, output_bone_mapping, num_output_bones);
|
||||
|
||||
const uint32_t written_segment_data_size = write_segment_data(lossy_clip_context, settings, range_reduction, transforms_header->get_segment_headers(), *transforms_header, output_bone_mapping, num_output_bones);
|
||||
|
||||
// Optional metadata header is last
|
||||
uint32_t writter_metadata_track_list_name_size = 0;
|
||||
uint32_t written_metadata_track_names_size = 0;
|
||||
uint32_t written_metadata_parent_track_indices_size = 0;
|
||||
uint32_t written_metadata_track_descriptions_size = 0;
|
||||
uint32_t written_metadata_contributing_error_size = 0;
|
||||
if (metadata_size != 0)
|
||||
{
|
||||
optional_metadata_header* metadata_header = bit_cast<optional_metadata_header*>(buffer_start + buffer_size - sizeof(optional_metadata_header));
|
||||
uint32_t metadata_offset = metadata_start_offset; // Relative to the start of our compressed_tracks
|
||||
|
||||
if (settings.metadata.include_track_list_name)
|
||||
{
|
||||
metadata_header->track_list_name = metadata_offset;
|
||||
writter_metadata_track_list_name_size = write_track_list_name(track_list, metadata_header->get_track_list_name(*out_compressed_tracks));
|
||||
metadata_offset += writter_metadata_track_list_name_size;
|
||||
}
|
||||
else
|
||||
metadata_header->track_list_name = invalid_ptr_offset();
|
||||
|
||||
if (settings.metadata.include_track_names)
|
||||
{
|
||||
metadata_offset = align_to(metadata_offset, 4);
|
||||
metadata_header->track_name_offsets = metadata_offset;
|
||||
written_metadata_track_names_size = write_track_names(track_list, output_bone_mapping, num_output_bones, metadata_header->get_track_name_offsets(*out_compressed_tracks));
|
||||
metadata_offset += written_metadata_track_names_size;
|
||||
}
|
||||
else
|
||||
metadata_header->track_name_offsets = invalid_ptr_offset();
|
||||
|
||||
if (settings.metadata.include_parent_track_indices)
|
||||
{
|
||||
metadata_offset = align_to(metadata_offset, 4);
|
||||
metadata_header->parent_track_indices = metadata_offset;
|
||||
written_metadata_parent_track_indices_size = write_parent_track_indices(track_list, output_bone_mapping, num_output_bones, metadata_header->get_parent_track_indices(*out_compressed_tracks));
|
||||
metadata_offset += written_metadata_parent_track_indices_size;
|
||||
}
|
||||
else
|
||||
metadata_header->parent_track_indices = invalid_ptr_offset();
|
||||
|
||||
if (settings.metadata.include_track_descriptions)
|
||||
{
|
||||
metadata_offset = align_to(metadata_offset, 4);
|
||||
metadata_header->track_descriptions = metadata_offset;
|
||||
written_metadata_track_descriptions_size = write_track_descriptions(track_list, output_bone_mapping, num_output_bones, metadata_header->get_track_descriptions(*out_compressed_tracks));
|
||||
metadata_offset += written_metadata_track_descriptions_size;
|
||||
}
|
||||
else
|
||||
metadata_header->track_descriptions = invalid_ptr_offset();
|
||||
|
||||
if (settings.metadata.include_contributing_error)
|
||||
{
|
||||
metadata_offset = align_to(metadata_offset, 4);
|
||||
metadata_header->contributing_error = metadata_offset;
|
||||
written_metadata_contributing_error_size = write_contributing_error(lossy_clip_context, metadata_header->get_contributing_error(*out_compressed_tracks));
|
||||
metadata_offset += written_metadata_contributing_error_size;
|
||||
}
|
||||
else
|
||||
metadata_header->contributing_error = invalid_ptr_offset();
|
||||
}
|
||||
|
||||
// Finish the compressed tracks raw buffer header
|
||||
buffer_header->size = buffer_size;
|
||||
buffer_header->hash = hash32(safe_ptr_cast<const uint8_t>(header), buffer_size - sizeof(raw_buffer_header)); // Hash everything but the raw buffer header
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
compression_stats.output_packing_elapsed_seconds = output_packing_time.get_elapsed_seconds();
|
||||
#endif
|
||||
|
||||
#if defined(ACL_HAS_ASSERT_CHECKS)
|
||||
{
|
||||
// Make sure we wrote the right amount of data
|
||||
buffer = align_to(buffer, 4); // Align segment start indices
|
||||
buffer += written_segment_start_indices_size;
|
||||
buffer = align_to(buffer, 4); // Align segment headers
|
||||
buffer += written_segment_headers_size;
|
||||
buffer = align_to(buffer, 4); // Align sub-track types
|
||||
buffer += written_sub_track_buffer_size;
|
||||
buffer = align_to(buffer, 4); // Align constant track data
|
||||
buffer += written_constant_data_size;
|
||||
buffer = align_to(buffer, 4); // Align range data
|
||||
buffer += written_clip_range_data_size;
|
||||
buffer += written_segment_data_size;
|
||||
|
||||
if (metadata_size != 0)
|
||||
{
|
||||
buffer = align_to(buffer, 4);
|
||||
buffer += metadata_size;
|
||||
|
||||
buffer = align_to(buffer, 4);
|
||||
buffer += sizeof(optional_metadata_header);
|
||||
}
|
||||
else
|
||||
buffer += 15; // Ensure we have sufficient padding for unaligned 16 byte loads
|
||||
|
||||
(void)buffer_start; // Avoid VS2017 bug, it falsely reports this variable as unused even when asserts are enabled
|
||||
ACL_ASSERT(written_segment_start_indices_size == segment_start_indices_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_segment_headers_size == segment_headers_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_segment_data_size == segment_data_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_sub_track_buffer_size == packed_sub_track_buffer_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_constant_data_size == constant_data_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_clip_range_data_size == clip_range_data_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(writter_metadata_track_list_name_size == metadata_track_list_name_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_metadata_track_names_size == metadata_track_names_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_metadata_parent_track_indices_size == metadata_parent_track_indices_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_metadata_track_descriptions_size == metadata_track_descriptions_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(written_metadata_contributing_error_size == metadata_contributing_error_size, "Wrote too little or too much data");
|
||||
ACL_ASSERT(uint32_t(buffer - buffer_start) == buffer_size, "Wrote too little or too much data");
|
||||
|
||||
if (metadata_size == 0)
|
||||
{
|
||||
for (const uint8_t* padding = buffer - 15; padding < buffer; ++padding)
|
||||
ACL_ASSERT(*padding == 0, "Padding was overwritten");
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)written_segment_start_indices_size;
|
||||
(void)written_segment_headers_size;
|
||||
(void)written_sub_track_buffer_size;
|
||||
(void)written_constant_data_size;
|
||||
(void)written_clip_range_data_size;
|
||||
(void)written_segment_data_size;
|
||||
(void)segment_data_size;
|
||||
(void)buffer_start;
|
||||
#endif
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
compression_stats.total_elapsed_seconds = compression_time.stop().get_elapsed_seconds();
|
||||
|
||||
if (out_stats.logging != stat_logging::none)
|
||||
write_stats(allocator, track_list, lossy_clip_context, *out_compressed_tracks, settings, segmenting_settings, range_reduction, raw_clip_context, additive_base_clip_context, compression_stats, out_stats);
|
||||
#endif
|
||||
|
||||
deallocate_type_array(allocator, output_bone_mapping, num_output_bones);
|
||||
deallocate_type_array(allocator, clip_shell_metadata, num_input_transforms);
|
||||
destroy_clip_context(lossy_clip_context);
|
||||
destroy_clip_context(raw_clip_context);
|
||||
destroy_clip_context(additive_base_clip_context);
|
||||
|
||||
return error_result();
|
||||
}
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2023 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Included only once from compression_level.h
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
inline const char* get_compression_level_name(compression_level8 level)
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case compression_level8::lowest: return "lowest";
|
||||
case compression_level8::low: return "low";
|
||||
case compression_level8::medium: return "medium";
|
||||
case compression_level8::high: return "high";
|
||||
case compression_level8::highest: return "highest";
|
||||
case compression_level8::automatic: return "automatic";
|
||||
default: return "<Invalid>";
|
||||
}
|
||||
}
|
||||
|
||||
inline bool get_compression_level(const char* level_name, compression_level8& out_level)
|
||||
{
|
||||
ACL_ASSERT(level_name != nullptr, "Level name cannot be null");
|
||||
if (level_name == nullptr)
|
||||
return false;
|
||||
|
||||
const char* level_lowest = "Lowest"; // ACL_DEPRECATED Legacy name, keep for backwards compatibility, remove in 3.0
|
||||
const char* level_lowest_new = "lowest";
|
||||
if (std::strncmp(level_name, level_lowest, std::strlen(level_lowest)) == 0
|
||||
|| std::strncmp(level_name, level_lowest_new, std::strlen(level_lowest_new)) == 0)
|
||||
{
|
||||
out_level = compression_level8::lowest;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* level_low = "Low"; // ACL_DEPRECATED Legacy name, keep for backwards compatibility, remove in 3.0
|
||||
const char* level_low_new = "low";
|
||||
if (std::strncmp(level_name, level_low, std::strlen(level_low)) == 0
|
||||
|| std::strncmp(level_name, level_low_new, std::strlen(level_low_new)) == 0)
|
||||
{
|
||||
out_level = compression_level8::low;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* level_medium = "Medium"; // ACL_DEPRECATED Legacy name, keep for backwards compatibility, remove in 3.0
|
||||
const char* level_medium_new = "medium";
|
||||
if (std::strncmp(level_name, level_medium, std::strlen(level_medium)) == 0
|
||||
|| std::strncmp(level_name, level_medium_new, std::strlen(level_medium_new)) == 0)
|
||||
{
|
||||
out_level = compression_level8::medium;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* level_highest = "Highest"; // ACL_DEPRECATED Legacy name, keep for backwards compatibility, remove in 3.0
|
||||
const char* level_highest_new = "highest";
|
||||
if (std::strncmp(level_name, level_highest, std::strlen(level_highest)) == 0
|
||||
|| std::strncmp(level_name, level_highest_new, std::strlen(level_highest_new)) == 0)
|
||||
{
|
||||
out_level = compression_level8::highest;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* level_high = "High"; // ACL_DEPRECATED Legacy name, keep for backwards compatibility, remove in 3.0
|
||||
const char* level_high_new = "high";
|
||||
if (std::strncmp(level_name, level_high, std::strlen(level_high)) == 0
|
||||
|| std::strncmp(level_name, level_high_new, std::strlen(level_high_new)) == 0)
|
||||
{
|
||||
out_level = compression_level8::high;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* level_automatic = "automatic";
|
||||
if (std::strncmp(level_name, level_automatic, std::strlen(level_automatic)) == 0)
|
||||
{
|
||||
out_level = compression_level8::automatic;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,175 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2020 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Included only once from compression_settings.h
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/error_result.h"
|
||||
#include "acl/core/hash.h"
|
||||
#include "acl/core/track_formats.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
inline uint32_t compression_database_settings::get_hash() const
|
||||
{
|
||||
uint32_t hash_value = 0;
|
||||
hash_value = hash_combine(hash_value, hash32(max_chunk_size));
|
||||
hash_value = hash_combine(hash_value, hash32(medium_importance_tier_proportion));
|
||||
hash_value = hash_combine(hash_value, hash32(low_importance_tier_proportion));
|
||||
return hash_value;
|
||||
}
|
||||
|
||||
inline error_result compression_database_settings::is_valid() const
|
||||
{
|
||||
if (max_chunk_size < 4 * 1024)
|
||||
return error_result("max_chunk_size must be greater or equal to 4 KB");
|
||||
|
||||
if (align_to(max_chunk_size, 4 * 1024) != max_chunk_size)
|
||||
return error_result("max_chunk_size must be a multiple of 4 KB");
|
||||
|
||||
if (!rtm::scalar_is_finite(medium_importance_tier_proportion) || medium_importance_tier_proportion < 0.0F || medium_importance_tier_proportion > 1.0F)
|
||||
return error_result("medium_importance_tier_proportion must be in the range [0.0, 1.0]");
|
||||
|
||||
if (!rtm::scalar_is_finite(low_importance_tier_proportion) || low_importance_tier_proportion < 0.0F || low_importance_tier_proportion > 1.0F)
|
||||
return error_result("low_importance_tier_proportion must be in the range [0.0, 1.0]");
|
||||
|
||||
// Add an epsilon to account for arithmetic imprecision
|
||||
const float database_proportion = low_importance_tier_proportion + medium_importance_tier_proportion;
|
||||
const float epsilon = 1.0e-5F;
|
||||
if (database_proportion < epsilon || database_proportion > (1.0F + epsilon))
|
||||
return error_result("The sum of medium_importance_tier_proportion + low_importance_tier_proportion must be in the range [0.0, 1.0]");
|
||||
|
||||
return error_result();
|
||||
}
|
||||
|
||||
inline uint32_t compression_metadata_settings::get_hash() const
|
||||
{
|
||||
uint32_t hash_value = 0;
|
||||
hash_value = hash_combine(hash_value, hash32(include_track_list_name));
|
||||
hash_value = hash_combine(hash_value, hash32(include_track_names));
|
||||
hash_value = hash_combine(hash_value, hash32(include_parent_track_indices));
|
||||
hash_value = hash_combine(hash_value, hash32(include_track_descriptions));
|
||||
hash_value = hash_combine(hash_value, hash32(include_contributing_error));
|
||||
|
||||
return hash_value;
|
||||
}
|
||||
|
||||
inline error_result compression_metadata_settings::is_valid() const
|
||||
{
|
||||
return error_result();
|
||||
}
|
||||
|
||||
inline bool compression_keyframe_stripping_settings::is_enabled() const
|
||||
{
|
||||
return proportion > 0.0F || threshold > 0.0F || strip_trivial;
|
||||
}
|
||||
|
||||
inline uint32_t compression_keyframe_stripping_settings::get_hash() const
|
||||
{
|
||||
uint32_t hash_value = 0;
|
||||
hash_value = hash_combine(hash_value, hash32(proportion));
|
||||
hash_value = hash_combine(hash_value, hash32(threshold));
|
||||
hash_value = hash_combine(hash_value, hash32(strip_trivial));
|
||||
|
||||
return hash_value;
|
||||
}
|
||||
|
||||
inline error_result compression_keyframe_stripping_settings::is_valid() const
|
||||
{
|
||||
if (!rtm::scalar_is_finite(proportion) || proportion < 0.0F || proportion > 1.0F)
|
||||
return error_result("proportion must be in the range [0.0, 1.0]");
|
||||
|
||||
if (!rtm::scalar_is_finite(threshold) || threshold < 0.0F)
|
||||
return error_result("threshold must be positive definite");
|
||||
|
||||
return error_result();
|
||||
}
|
||||
|
||||
inline uint32_t compression_settings::get_hash() const
|
||||
{
|
||||
uint32_t hash_value = 0;
|
||||
hash_value = hash_combine(hash_value, hash32(level));
|
||||
hash_value = hash_combine(hash_value, hash32(rotation_format));
|
||||
hash_value = hash_combine(hash_value, hash32(translation_format));
|
||||
hash_value = hash_combine(hash_value, hash32(scale_format));
|
||||
|
||||
if (error_metric != nullptr)
|
||||
hash_value = hash_combine(hash_value, error_metric->get_hash());
|
||||
|
||||
hash_value = hash_combine(hash_value, enable_database_support);
|
||||
hash_value = hash_combine(hash_value, optimize_loops);
|
||||
hash_value = hash_combine(hash_value, keyframe_stripping.get_hash());
|
||||
hash_value = hash_combine(hash_value, metadata.get_hash());
|
||||
|
||||
return hash_value;
|
||||
}
|
||||
|
||||
inline error_result compression_settings::is_valid() const
|
||||
{
|
||||
if (error_metric == nullptr)
|
||||
return error_result("error_metric cannot be NULL");
|
||||
|
||||
const error_result keyframe_stripping_result = keyframe_stripping.is_valid();
|
||||
if (keyframe_stripping_result.any())
|
||||
return keyframe_stripping_result;
|
||||
|
||||
const error_result metadata_result = metadata.is_valid();
|
||||
if (metadata_result.any())
|
||||
return metadata_result;
|
||||
|
||||
if (keyframe_stripping.is_enabled() && enable_database_support)
|
||||
return error_result("Cannot enable keyframe stripping with database support");
|
||||
|
||||
return error_result();
|
||||
}
|
||||
|
||||
inline compression_settings get_raw_compression_settings()
|
||||
{
|
||||
return compression_settings();
|
||||
}
|
||||
|
||||
inline compression_settings get_default_compression_settings()
|
||||
{
|
||||
compression_settings settings;
|
||||
settings.level = compression_level8::automatic;
|
||||
settings.rotation_format = rotation_format8::quatf_drop_w_variable;
|
||||
settings.translation_format = vector_format8::vector3f_variable;
|
||||
settings.scale_format = vector_format8::vector3f_variable;
|
||||
settings.optimize_loops = true;
|
||||
settings.keyframe_stripping.strip_trivial = true;
|
||||
return settings;
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2024 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
namespace acl_impl
|
||||
{
|
||||
// Holds compression timing stats for diagnostics
|
||||
struct compression_stats_t
|
||||
{
|
||||
#if defined(ACL_USE_SJSON)
|
||||
// Total compression time
|
||||
double total_elapsed_seconds = 0.0;
|
||||
|
||||
// High level passes
|
||||
double initialization_elapsed_seconds = 0.0;
|
||||
double optimize_looping_elapsed_seconds = 0.0;
|
||||
double convert_rotations_elapsed_seconds = 0.0;
|
||||
double extract_clip_ranges_elapsed_seconds = 0.0;
|
||||
double compact_constant_sub_tracks_elapsed_seconds = 0.0;
|
||||
double normalize_clip_elapsed_seconds = 0.0;
|
||||
double segmenting_elapsed_seconds = 0.0;
|
||||
double extract_segment_ranges_elapsed_seconds = 0.0;
|
||||
double normalize_segment_elapsed_seconds = 0.0;
|
||||
double bit_rate_optimization_elapsed_seconds = 0.0;
|
||||
double keyframe_stripping_elapsed_seconds = 0.0;
|
||||
double output_packing_elapsed_seconds = 0.0;
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,259 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2020 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Included only once from convert.h
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/compression/compress.h"
|
||||
#include "acl/compression/track_array.h"
|
||||
#include "acl/core/compressed_tracks.h"
|
||||
#include "acl/core/error_result.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
#include "acl/core/track_formats.h"
|
||||
#include "acl/core/impl/bit_cast.impl.h"
|
||||
#include "acl/core/impl/debug_track_writer.h"
|
||||
#include "acl/decompression/decompress.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
namespace acl_impl
|
||||
{
|
||||
struct raw_sampling_decompression_settings final : public decompression_settings
|
||||
{
|
||||
// Disable normalization. This is only safe if we know the input data is already normalized!
|
||||
static constexpr rotation_normalization_policy_t get_rotation_normalization_policy() { return rotation_normalization_policy_t::never; }
|
||||
|
||||
// Only support raw formats. This ensures we don't interpolate when we can avoid it to maintain the original
|
||||
// accuracy and it strips the code we don't need for faster decompression.
|
||||
static constexpr bool is_rotation_format_supported(rotation_format8 format) { return format == rotation_format8::quatf_full; }
|
||||
static constexpr bool is_translation_format_supported(vector_format8 format) { return format == vector_format8::vector3f_full; }
|
||||
static constexpr bool is_scale_format_supported(vector_format8 format) { return format == vector_format8::vector3f_full; }
|
||||
};
|
||||
}
|
||||
|
||||
inline error_result convert_track_list(iallocator& allocator, const track_array& track_list, compressed_tracks*& out_compressed_tracks)
|
||||
{
|
||||
compression_settings settings = get_raw_compression_settings();
|
||||
|
||||
qvvf_transform_error_metric error_metric;
|
||||
settings.error_metric = &error_metric;
|
||||
|
||||
// Include all the metadata
|
||||
settings.metadata.include_track_list_name = true;
|
||||
settings.metadata.include_track_names = true;
|
||||
settings.metadata.include_parent_track_indices = true;
|
||||
settings.metadata.include_track_descriptions = true;
|
||||
|
||||
output_stats stats;
|
||||
return compress_track_list(allocator, track_list, settings, out_compressed_tracks, stats);
|
||||
}
|
||||
|
||||
inline error_result convert_track_list(iallocator& allocator, const compressed_tracks& tracks, track_array& out_track_list)
|
||||
{
|
||||
error_result error = tracks.is_valid(false);
|
||||
if (error.any())
|
||||
return error;
|
||||
|
||||
const uint32_t num_tracks = tracks.get_num_tracks();
|
||||
const track_type8 track_type = tracks.get_track_type();
|
||||
const uint32_t num_samples = tracks.get_num_samples_per_track();
|
||||
const float sample_rate = tracks.get_sample_rate();
|
||||
const float duration = tracks.get_finite_duration();
|
||||
|
||||
track_array result(allocator, num_tracks);
|
||||
result.set_name(string(allocator, tracks.get_name()));
|
||||
|
||||
// Setup our track metadata and allocate memory
|
||||
bool success = true;
|
||||
for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
|
||||
{
|
||||
track& track_ = result[track_index];
|
||||
|
||||
track_desc_scalarf desc_scalar;
|
||||
track_desc_transformf desc_transform;
|
||||
|
||||
bool got_description = false;
|
||||
switch (track_type)
|
||||
{
|
||||
case track_type8::float1f:
|
||||
got_description = tracks.get_track_description(track_index, desc_scalar);
|
||||
track_ = track_float1f::make_reserve(desc_scalar, allocator, num_samples, sample_rate);
|
||||
break;
|
||||
case track_type8::float2f:
|
||||
got_description = tracks.get_track_description(track_index, desc_scalar);
|
||||
track_ = track_float2f::make_reserve(desc_scalar, allocator, num_samples, sample_rate);
|
||||
break;
|
||||
case track_type8::float3f:
|
||||
got_description = tracks.get_track_description(track_index, desc_scalar);
|
||||
track_ = track_float3f::make_reserve(desc_scalar, allocator, num_samples, sample_rate);
|
||||
break;
|
||||
case track_type8::float4f:
|
||||
got_description = tracks.get_track_description(track_index, desc_scalar);
|
||||
track_ = track_float4f::make_reserve(desc_scalar, allocator, num_samples, sample_rate);
|
||||
break;
|
||||
case track_type8::vector4f:
|
||||
got_description = tracks.get_track_description(track_index, desc_scalar);
|
||||
track_ = track_vector4f::make_reserve(desc_scalar, allocator, num_samples, sample_rate);
|
||||
break;
|
||||
case track_type8::qvvf:
|
||||
got_description = tracks.get_track_description(track_index, desc_transform);
|
||||
track_ = track_qvvf::make_reserve(desc_transform, allocator, num_samples, sample_rate);
|
||||
break;
|
||||
default:
|
||||
ACL_ASSERT(false, "Unexpected track type");
|
||||
break;
|
||||
}
|
||||
|
||||
success &= got_description;
|
||||
|
||||
track_.set_name(string(allocator, tracks.get_track_name(track_index)));
|
||||
}
|
||||
|
||||
if (!success)
|
||||
return error_result("Metadata was missing from the input");
|
||||
|
||||
// Disable floating point exceptions since decompression assumes it
|
||||
scope_disable_fp_exceptions fp_off;
|
||||
|
||||
// Decompress and populate our track data
|
||||
const acl_impl::tracks_header& header = acl_impl::get_tracks_header(tracks);
|
||||
if (track_type == track_type8::qvvf &&
|
||||
header.get_rotation_format() == rotation_format8::quatf_full &&
|
||||
header.get_translation_format() == vector_format8::vector3f_full &&
|
||||
header.get_scale_format() == vector_format8::vector3f_full)
|
||||
{
|
||||
// Our input transform data uses full precision, retain it
|
||||
decompression_context<acl_impl::raw_sampling_decompression_settings> context;
|
||||
context.initialize(tracks);
|
||||
|
||||
acl_impl::debug_track_writer writer(allocator, track_type, num_tracks);
|
||||
writer.initialize_with_defaults(result);
|
||||
|
||||
for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
|
||||
{
|
||||
const float sample_time = rtm::scalar_min(float(sample_index) / sample_rate, duration);
|
||||
|
||||
// Round to nearest to land directly on a sample
|
||||
context.seek(sample_time, sample_rounding_policy::nearest);
|
||||
|
||||
context.decompress_tracks(writer);
|
||||
|
||||
for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
|
||||
{
|
||||
track& track_ = result[track_index];
|
||||
|
||||
switch (track_type)
|
||||
{
|
||||
case track_type8::float1f:
|
||||
*acl_impl::bit_cast<float*>(track_[sample_index]) = writer.read_float1(track_index);
|
||||
break;
|
||||
case track_type8::float2f:
|
||||
rtm::vector_store2(writer.read_float2(track_index), acl_impl::bit_cast<rtm::float2f*>(track_[sample_index]));
|
||||
break;
|
||||
case track_type8::float3f:
|
||||
rtm::vector_store3(writer.read_float3(track_index), acl_impl::bit_cast<rtm::float3f*>(track_[sample_index]));
|
||||
break;
|
||||
case track_type8::float4f:
|
||||
rtm::vector_store(writer.read_float4(track_index), acl_impl::bit_cast<rtm::float4f*>(track_[sample_index]));
|
||||
break;
|
||||
case track_type8::vector4f:
|
||||
*acl_impl::bit_cast<rtm::vector4f*>(track_[sample_index]) = writer.read_vector4(track_index);
|
||||
break;
|
||||
case track_type8::qvvf:
|
||||
*acl_impl::bit_cast<rtm::qvvf*>(track_[sample_index]) = writer.read_qvv(track_index);
|
||||
break;
|
||||
default:
|
||||
ACL_ASSERT(false, "Unexpected track type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
decompression_context<decompression_settings> context;
|
||||
context.initialize(tracks);
|
||||
|
||||
acl_impl::debug_track_writer writer(allocator, track_type, num_tracks);
|
||||
|
||||
if (track_type == track_type8::qvvf)
|
||||
writer.initialize_with_defaults(result);
|
||||
|
||||
for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
|
||||
{
|
||||
const float sample_time = rtm::scalar_min(float(sample_index) / sample_rate, duration);
|
||||
|
||||
// Round to nearest to land directly on a sample
|
||||
context.seek(sample_time, sample_rounding_policy::nearest);
|
||||
|
||||
context.decompress_tracks(writer);
|
||||
|
||||
for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
|
||||
{
|
||||
track& track_ = result[track_index];
|
||||
|
||||
switch (track_type)
|
||||
{
|
||||
case track_type8::float1f:
|
||||
*acl_impl::bit_cast<float*>(track_[sample_index]) = writer.read_float1(track_index);
|
||||
break;
|
||||
case track_type8::float2f:
|
||||
rtm::vector_store2(writer.read_float2(track_index), acl_impl::bit_cast<rtm::float2f*>(track_[sample_index]));
|
||||
break;
|
||||
case track_type8::float3f:
|
||||
rtm::vector_store3(writer.read_float3(track_index), acl_impl::bit_cast<rtm::float3f*>(track_[sample_index]));
|
||||
break;
|
||||
case track_type8::float4f:
|
||||
rtm::vector_store(writer.read_float4(track_index), acl_impl::bit_cast<rtm::float4f*>(track_[sample_index]));
|
||||
break;
|
||||
case track_type8::vector4f:
|
||||
*acl_impl::bit_cast<rtm::vector4f*>(track_[sample_index]) = writer.read_vector4(track_index);
|
||||
break;
|
||||
case track_type8::qvvf:
|
||||
*acl_impl::bit_cast<rtm::qvvf*>(track_[sample_index]) = writer.read_qvv(track_index);
|
||||
break;
|
||||
default:
|
||||
ACL_ASSERT(false, "Unexpected track type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out_track_list = std::move(result);
|
||||
return error_result();
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2017 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/iallocator.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
#include "acl/core/error.h"
|
||||
#include "acl/core/scope_profiler.h"
|
||||
#include "acl/core/track_formats.h"
|
||||
#include "acl/compression/impl/clip_context.h"
|
||||
#include "acl/compression/impl/compression_stats.h"
|
||||
|
||||
#include <rtm/quatf.h>
|
||||
#include <rtm/vector4f.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
namespace acl_impl
|
||||
{
|
||||
inline rtm::vector4f RTM_SIMD_CALL convert_rotation(rtm::vector4f_arg0 rotation, rotation_format8 from, rotation_format8 to)
|
||||
{
|
||||
ACL_ASSERT(from == rotation_format8::quatf_full, "Source rotation format must be a full precision quaternion");
|
||||
(void)from;
|
||||
|
||||
const rotation_format8 high_precision_format = get_rotation_variant(to) == rotation_variant8::quat ? rotation_format8::quatf_full : rotation_format8::quatf_drop_w_full;
|
||||
switch (high_precision_format)
|
||||
{
|
||||
case rotation_format8::quatf_full:
|
||||
// Original format, nothing to do
|
||||
return rotation;
|
||||
case rotation_format8::quatf_drop_w_full:
|
||||
// Drop W, we just ensure it is positive and write it back, the W component can be ignored afterwards
|
||||
return rtm::quat_to_vector(rtm::quat_ensure_positive_w(rtm::vector_to_quat(rotation)));
|
||||
case rotation_format8::quatf_drop_w_variable:
|
||||
default:
|
||||
ACL_ASSERT(false, "Invalid or unsupported rotation format: " ACL_ASSERT_STRING_FORMAT_SPECIFIER, get_rotation_format_name(to));
|
||||
return rotation;
|
||||
}
|
||||
}
|
||||
|
||||
inline void convert_rotation_streams(iallocator& allocator, segment_context& segment, rotation_format8 rotation_format)
|
||||
{
|
||||
const rotation_format8 high_precision_format = get_rotation_variant(rotation_format) == rotation_variant8::quat ? rotation_format8::quatf_full : rotation_format8::quatf_drop_w_full;
|
||||
|
||||
for (transform_streams& bone_stream : segment.bone_iterator())
|
||||
{
|
||||
// We convert our rotation stream in place. We assume that the original format is quatf_full stored as rtm::quatf
|
||||
// For all other formats, we keep the same sample size and either keep Quat_32 or use rtm::vector4f
|
||||
ACL_ASSERT(bone_stream.rotations.get_sample_size() == sizeof(rtm::quatf), "Unexpected rotation sample size. %u != %zu", bone_stream.rotations.get_sample_size(), sizeof(rtm::quatf));
|
||||
|
||||
const uint32_t num_samples = bone_stream.rotations.get_num_samples();
|
||||
const float sample_rate = bone_stream.rotations.get_sample_rate();
|
||||
rotation_track_stream converted_stream(allocator, num_samples, sizeof(rtm::quatf), sample_rate, high_precision_format);
|
||||
|
||||
for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
|
||||
{
|
||||
rtm::quatf rotation = bone_stream.rotations.get_raw_sample<rtm::quatf>(sample_index);
|
||||
|
||||
switch (high_precision_format)
|
||||
{
|
||||
case rotation_format8::quatf_full:
|
||||
// Original format, nothing to do
|
||||
break;
|
||||
case rotation_format8::quatf_drop_w_full:
|
||||
// Drop W, we just ensure it is positive and write it back, the W component can be ignored afterwards
|
||||
rotation = rtm::quat_ensure_positive_w(rotation);
|
||||
break;
|
||||
case rotation_format8::quatf_drop_w_variable:
|
||||
default:
|
||||
ACL_ASSERT(false, "Invalid or unsupported rotation format: " ACL_ASSERT_STRING_FORMAT_SPECIFIER, get_rotation_format_name(high_precision_format));
|
||||
break;
|
||||
}
|
||||
|
||||
converted_stream.set_raw_sample(sample_index, rotation);
|
||||
}
|
||||
|
||||
bone_stream.rotations = std::move(converted_stream);
|
||||
}
|
||||
}
|
||||
|
||||
inline void convert_rotation_streams(
|
||||
iallocator& allocator,
|
||||
clip_context& context,
|
||||
rotation_format8 rotation_format,
|
||||
compression_stats_t& compression_stats)
|
||||
{
|
||||
(void)compression_stats;
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
scope_profiler convert_rotations_time;
|
||||
#endif
|
||||
|
||||
for (segment_context& segment : context.segment_iterator())
|
||||
convert_rotation_streams(allocator, segment, rotation_format);
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
compression_stats.convert_rotations_elapsed_seconds = convert_rotations_time.get_elapsed_seconds();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
@@ -0,0 +1,151 @@
|
||||
#pragma once
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2023 Nicholas Frechette & Animation Compression Library contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "acl/version.h"
|
||||
#include "acl/core/scope_profiler.h"
|
||||
#include "acl/core/impl/compiler_utils.h"
|
||||
#include "acl/compression/compression_settings.h"
|
||||
#include "acl/compression/impl/compression_stats.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_PUSH
|
||||
|
||||
namespace acl
|
||||
{
|
||||
ACL_IMPL_VERSION_NAMESPACE_BEGIN
|
||||
|
||||
namespace acl_impl
|
||||
{
|
||||
inline void strip_keyframes(clip_context& lossy_clip_context, const compression_settings& settings, compression_stats_t& compression_stats)
|
||||
{
|
||||
if (!settings.keyframe_stripping.is_enabled())
|
||||
return; // We don't want to strip keyframes, nothing to do
|
||||
|
||||
(void)compression_stats;
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
scope_profiler keyframe_stripping_time;
|
||||
#endif
|
||||
|
||||
const bitset_description hard_keyframes_desc = bitset_description::make_from_num_bits<32>();
|
||||
const uint32_t num_keyframes = lossy_clip_context.num_samples;
|
||||
|
||||
const keyframe_stripping_metadata_t* contributing_error_per_keyframe = lossy_clip_context.contributing_error;
|
||||
|
||||
// Initialize which keyframes are retained, we'll strip them later
|
||||
for (segment_context& segment : lossy_clip_context.segment_iterator())
|
||||
bitset_set_range(&segment.hard_keyframes, hard_keyframes_desc, 0, segment.num_samples, true);
|
||||
|
||||
// First determine how many we wish to strip based on proportion
|
||||
// A frame is movable if it isn't the first or last frame of a segment
|
||||
// If we have more than 1 frame, we can remove 2 frames per segment
|
||||
// We know that the only way to get a segment with 1 frame is if the whole clip contains
|
||||
// a single frame and thus has one segment
|
||||
// If we have 0 or 1 frame, none are movable
|
||||
const uint32_t num_movable_frames = num_keyframes >= 2 ? (num_keyframes - (lossy_clip_context.num_segments * 2)) : 0;
|
||||
|
||||
uint32_t num_keyframes_to_strip = 0;
|
||||
|
||||
// First, we strip the trivial keyframes
|
||||
if (settings.keyframe_stripping.strip_trivial)
|
||||
{
|
||||
// We only strip the ones that have the lowest error
|
||||
// Since the error depends on the order in which keyframes are removed,
|
||||
// a trivial keyframe might only become trivial after another keyframe that
|
||||
// isn't trivial is removed.
|
||||
for (; num_keyframes_to_strip < num_movable_frames; ++num_keyframes_to_strip)
|
||||
{
|
||||
if (!contributing_error_per_keyframe[num_keyframes_to_strip].is_keyframe_trivial)
|
||||
break; // Found the first non-trivial keyframe
|
||||
}
|
||||
}
|
||||
|
||||
// Then scan starting until we find our threshold if its above the proportion
|
||||
// We removed trivial keyframes above, meaning the error should be below our desired
|
||||
// threshold or it's low enough not to matter
|
||||
// Since the error depends on the order in which keyframes are removed, we process
|
||||
// the threshold first as it could decrease
|
||||
for (; num_keyframes_to_strip < num_movable_frames; ++num_keyframes_to_strip)
|
||||
{
|
||||
if (contributing_error_per_keyframe[num_keyframes_to_strip].stripping_error > settings.keyframe_stripping.threshold)
|
||||
break; // The error exceeds our threshold, stop stripping keyframes
|
||||
}
|
||||
|
||||
// Make sure to at least remove as many as the proportion we want
|
||||
const uint32_t num_proportion_keyframes_to_strip = std::min<uint32_t>(num_movable_frames, uint32_t(settings.keyframe_stripping.proportion * float(num_keyframes)));
|
||||
num_keyframes_to_strip = std::max<uint32_t>(num_keyframes_to_strip, num_proportion_keyframes_to_strip);
|
||||
|
||||
ACL_ASSERT(num_keyframes_to_strip <= num_movable_frames, "Cannot strip more than the number of movable keyframes");
|
||||
|
||||
// Check if stripping our keyframes actually saves any memory
|
||||
// Stripping requires us to add a small header per segment, we need to offset that cost
|
||||
uint32_t num_bits_saved = 0;
|
||||
for (uint32_t index = 0; index < num_keyframes_to_strip; ++index)
|
||||
{
|
||||
const keyframe_stripping_metadata_t& contributing_error = contributing_error_per_keyframe[index];
|
||||
const segment_context& segment = lossy_clip_context.segments[contributing_error.segment_index];
|
||||
|
||||
num_bits_saved += segment.animated_pose_bit_size;
|
||||
}
|
||||
|
||||
const uint32_t num_bytes_segment_overhead = sizeof(stripped_segment_header_t) - sizeof(segment_header);
|
||||
const uint32_t num_bytes_stripping_overhead = lossy_clip_context.num_segments * num_bytes_segment_overhead;
|
||||
const uint32_t num_bytes_saved = num_bits_saved / 8; // Round down to a full byte to be conservative
|
||||
|
||||
// If we don't save enough memory, don't strip anything
|
||||
if (num_bytes_saved <= num_bytes_stripping_overhead)
|
||||
num_keyframes_to_strip = 0;
|
||||
|
||||
// Now that we know how many keyframes to strip, remove them
|
||||
for (uint32_t index = 0; index < num_keyframes_to_strip; ++index)
|
||||
{
|
||||
const keyframe_stripping_metadata_t& contributing_error = contributing_error_per_keyframe[index];
|
||||
segment_context& segment = lossy_clip_context.segments[contributing_error.segment_index];
|
||||
|
||||
const uint32_t segment_keyframe_index = contributing_error.keyframe_index - segment.clip_sample_offset;
|
||||
ACL_ASSERT(segment_keyframe_index != 0 && segment_keyframe_index < (segment.num_samples - 1), "Cannot strip the first and last sample of a segment");
|
||||
|
||||
bitset_set(&segment.hard_keyframes, hard_keyframes_desc, segment_keyframe_index, false);
|
||||
|
||||
// Update how much animated we have
|
||||
const uint32_t num_stored_samples = bitset_count_set_bits(&segment.hard_keyframes, hard_keyframes_desc);
|
||||
const uint32_t num_animated_data_bits = segment.animated_pose_bit_size * num_stored_samples;
|
||||
segment.animated_data_size = align_to(num_animated_data_bits, 8) / 8;
|
||||
}
|
||||
|
||||
lossy_clip_context.has_stripped_keyframes = num_keyframes_to_strip != 0;
|
||||
|
||||
#if defined(ACL_USE_SJSON)
|
||||
compression_stats.keyframe_stripping_elapsed_seconds = keyframe_stripping_time.get_elapsed_seconds();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
ACL_IMPL_VERSION_NAMESPACE_END
|
||||
}
|
||||
|
||||
ACL_IMPL_FILE_PRAGMA_POP
|
||||
Loaded 100 of 1176 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user