Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70db96a52e | ||
|
|
1c9b1b1969 |
No files matched your search
@@ -1,10 +0,0 @@
|
|||||||
*.bmp filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.anim filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.ogg filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.dll filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.so filter=lfs diff=lfs merge=lfs -text
|
|
||||||
@@ -1,416 +1,62 @@
|
|||||||
## Ignore Visual Studio temporary files, build results, and
|
# Built application files
|
||||||
## files generated by popular Visual Studio add-ons.
|
*.apk
|
||||||
##
|
*.ap_
|
||||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
*.aab
|
||||||
|
|
||||||
.vscode/
|
# Files for the ART/Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
# User-specific files
|
# Java class files
|
||||||
*.rsuser
|
*.class
|
||||||
*.suo
|
|
||||||
*.user
|
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
# Generated files
|
||||||
*.userprefs
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
|
||||||
# Mono auto generated files
|
# Gradle files
|
||||||
mono_crash.*
|
.gradle/
|
||||||
.........................................................................
|
build/
|
||||||
# Build results
|
|
||||||
[Dd]ebug/
|
|
||||||
[Dd]ebugPublic/
|
|
||||||
[Rr]elease/
|
|
||||||
[Rr]eleases/
|
|
||||||
x64/
|
|
||||||
x86/
|
|
||||||
[Ww][Ii][Nn]32/
|
|
||||||
[Aa][Rr][Mm]/
|
|
||||||
[Aa][Rr][Mm]64/
|
|
||||||
bld/
|
|
||||||
[Bb]in/
|
|
||||||
[Oo]bj/
|
|
||||||
[Ll]og/
|
|
||||||
[Ll]ogs/
|
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory
|
|
||||||
.vs/
|
|
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
|
||||||
#wwwroot/
|
|
||||||
|
|
||||||
# Visual Studio 2017 auto generated files
|
|
||||||
Generated\ Files/
|
|
||||||
|
|
||||||
# MSTest test Results
|
|
||||||
[Tt]est[Rr]esult*/
|
|
||||||
[Bb]uild[Ll]og.*
|
|
||||||
|
|
||||||
# NUnit
|
|
||||||
*.VisualState.xml
|
|
||||||
TestResult.xml
|
|
||||||
nunit-*.xml
|
|
||||||
|
|
||||||
# Build Results of an ATL Project
|
|
||||||
[Dd]ebugPS/
|
|
||||||
[Rr]eleasePS/
|
|
||||||
dlldata.c
|
|
||||||
|
|
||||||
# Benchmark Results
|
|
||||||
BenchmarkDotNet.Artifacts/
|
|
||||||
|
|
||||||
# .NET Core
|
|
||||||
project.lock.json
|
|
||||||
project.fragment.lock.json
|
|
||||||
artifacts/
|
|
||||||
|
|
||||||
# ASP.NET Scaffolding
|
|
||||||
ScaffoldingReadMe.txt
|
|
||||||
|
|
||||||
# StyleCop
|
|
||||||
StyleCopReport.xml
|
|
||||||
|
|
||||||
# Files built by Visual Studio
|
|
||||||
*_i.c
|
|
||||||
*_p.c
|
|
||||||
*_h.h
|
|
||||||
*.ilk
|
|
||||||
*.meta
|
|
||||||
*.obj
|
|
||||||
*.iobj
|
|
||||||
*.pch
|
|
||||||
*.pdb
|
|
||||||
*.ipdb
|
|
||||||
*.pgc
|
|
||||||
*.pgd
|
|
||||||
*.rsp
|
|
||||||
*.sbr
|
|
||||||
*.tlb
|
|
||||||
*.tli
|
|
||||||
*.tlh
|
|
||||||
*.tmp
|
|
||||||
*.tmp_proj
|
|
||||||
*_wpftmp.csproj
|
|
||||||
*.log
|
|
||||||
*.tlog
|
|
||||||
*.vspscc
|
|
||||||
*.vssscc
|
|
||||||
.builds
|
|
||||||
*.pidb
|
|
||||||
*.svclog
|
|
||||||
*.scc
|
|
||||||
|
|
||||||
# Chutzpah Test files
|
|
||||||
_Chutzpah*
|
|
||||||
|
|
||||||
# Visual C++ cache files
|
|
||||||
ipch/
|
|
||||||
*.aps
|
|
||||||
*.ncb
|
|
||||||
*.opendb
|
|
||||||
*.opensdf
|
|
||||||
*.sdf
|
|
||||||
*.cachefile
|
|
||||||
*.VC.db
|
|
||||||
*.VC.VC.opendb
|
|
||||||
|
|
||||||
# Visual Studio profiler
|
|
||||||
*.psess
|
|
||||||
*.vsp
|
|
||||||
*.vspx
|
|
||||||
*.sap
|
|
||||||
|
|
||||||
# Visual Studio Trace Files
|
|
||||||
*.e2e
|
|
||||||
|
|
||||||
# TFS 2012 Local Workspace
|
|
||||||
$tf/
|
|
||||||
|
|
||||||
# Guidance Automation Toolkit
|
|
||||||
*.gpState
|
|
||||||
|
|
||||||
# ReSharper is a .NET coding add-in
|
|
||||||
_ReSharper*/
|
|
||||||
*.[Rr]e[Ss]harper
|
|
||||||
*.DotSettings.user
|
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
|
||||||
_TeamCity*
|
|
||||||
|
|
||||||
# DotCover is a Code Coverage Tool
|
|
||||||
*.dotCover
|
|
||||||
|
|
||||||
# AxoCover is a Code Coverage Tool
|
|
||||||
.axoCover/*
|
|
||||||
!.axoCover/settings.json
|
|
||||||
|
|
||||||
# Coverlet is a free, cross platform Code Coverage Tool
|
|
||||||
coverage*.json
|
|
||||||
coverage*.xml
|
|
||||||
coverage*.info
|
|
||||||
|
|
||||||
# Visual Studio code coverage results
|
|
||||||
*.coverage
|
|
||||||
*.coveragexml
|
|
||||||
|
|
||||||
# NCrunch
|
|
||||||
_NCrunch_*
|
|
||||||
.*crunch*.local.xml
|
|
||||||
nCrunchTemp_*
|
|
||||||
|
|
||||||
# MightyMoose
|
|
||||||
*.mm.*
|
|
||||||
AutoTest.Net/
|
|
||||||
|
|
||||||
# Web workbench (sass)
|
|
||||||
.sass-cache/
|
|
||||||
|
|
||||||
# Installshield output folder
|
|
||||||
[Ee]xpress/
|
|
||||||
|
|
||||||
# DocProject is a documentation generator add-in
|
|
||||||
DocProject/buildhelp/
|
|
||||||
DocProject/Help/*.HxT
|
|
||||||
DocProject/Help/*.HxC
|
|
||||||
DocProject/Help/*.hhc
|
|
||||||
DocProject/Help/*.hhk
|
|
||||||
DocProject/Help/*.hhp
|
|
||||||
DocProject/Help/Html2
|
|
||||||
DocProject/Help/html
|
|
||||||
|
|
||||||
# Click-Once directory
|
|
||||||
publish/
|
|
||||||
|
|
||||||
# Publish Web Output
|
|
||||||
*.[Pp]ublish.xml
|
|
||||||
*.azurePubxml
|
|
||||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
|
||||||
*.pubxml
|
|
||||||
*.publishproj
|
|
||||||
|
|
||||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
|
||||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
|
||||||
# in these scripts will be unencrypted
|
|
||||||
PublishScripts/
|
|
||||||
|
|
||||||
# NuGet Packages
|
|
||||||
*.nupkg
|
|
||||||
# NuGet Symbol Packages
|
|
||||||
*.snupkg
|
|
||||||
# The packages folder can be ignored because of Package Restore
|
|
||||||
**/[Pp]ackages/*
|
|
||||||
# except build/, which is used as an MSBuild target.
|
|
||||||
!**/[Pp]ackages/build/
|
|
||||||
# Uncomment if necessary however generally it will be regenerated when needed
|
|
||||||
#!**/[Pp]ackages/repositories.config
|
|
||||||
# NuGet v3's project.json files produces more ignorable files
|
|
||||||
*.nuget.props
|
|
||||||
*.nuget.targets
|
|
||||||
|
|
||||||
# Nuget personal access tokens and Credentials
|
|
||||||
nuget.config
|
|
||||||
|
|
||||||
# Microsoft Azure Build Output
|
|
||||||
csx/
|
|
||||||
*.build.csdef
|
|
||||||
|
|
||||||
# Microsoft Azure Emulator
|
|
||||||
ecf/
|
|
||||||
rcf/
|
|
||||||
|
|
||||||
# Windows Store app package directories and files
|
|
||||||
AppPackages/
|
|
||||||
BundleArtifacts/
|
|
||||||
Package.StoreAssociation.xml
|
|
||||||
_pkginfo.txt
|
|
||||||
*.appx
|
|
||||||
*.appxbundle
|
|
||||||
*.appxupload
|
|
||||||
|
|
||||||
# Visual Studio cache files
|
|
||||||
# files ending in .cache can be ignored
|
|
||||||
*.[Cc]ache
|
|
||||||
# but keep track of directories ending in .cache
|
|
||||||
!?*.[Cc]ache/
|
|
||||||
|
|
||||||
# Others
|
|
||||||
ClientBin/
|
|
||||||
~$*
|
|
||||||
*~
|
|
||||||
*.dbmdl
|
|
||||||
*.dbproj.schemaview
|
|
||||||
*.jfm
|
|
||||||
*.pfx
|
|
||||||
*.publishsettings
|
|
||||||
orleans.codegen.cs
|
|
||||||
|
|
||||||
# Including strong name files can present a security risk
|
|
||||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
|
||||||
#*.snk
|
|
||||||
|
|
||||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
|
||||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
|
||||||
#bower_components/
|
|
||||||
|
|
||||||
# RIA/Silverlight projects
|
|
||||||
Generated_Code/
|
|
||||||
|
|
||||||
# Backup & report files from converting an old project file
|
|
||||||
# to a newer Visual Studio version. Backup files are not needed,
|
|
||||||
# because we have git ;-)
|
|
||||||
_UpgradeReport_Files/
|
|
||||||
Backup*/
|
|
||||||
UpgradeLog*.XML
|
|
||||||
UpgradeLog*.htm
|
|
||||||
ServiceFabricBackup/
|
|
||||||
*.rptproj.bak
|
|
||||||
|
|
||||||
# SQL Server files
|
|
||||||
*.mdf
|
|
||||||
*.ldf
|
|
||||||
*.ndf
|
|
||||||
|
|
||||||
# Business Intelligence projects
|
|
||||||
*.rdl.data
|
|
||||||
*.bim.layout
|
|
||||||
*.bim_*.settings
|
|
||||||
*.rptproj.rsuser
|
|
||||||
*- [Bb]ackup.rdl
|
|
||||||
*- [Bb]ackup ([0-9]).rdl
|
|
||||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
|
||||||
|
|
||||||
# Microsoft Fakes
|
|
||||||
FakesAssemblies/
|
|
||||||
|
|
||||||
# GhostDoc plugin setting file
|
|
||||||
*.GhostDoc.xml
|
|
||||||
|
|
||||||
# Node.js Tools for Visual Studio
|
|
||||||
.ntvs_analysis.dat
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Visual Studio 6 build log
|
|
||||||
*.plg
|
|
||||||
|
|
||||||
# Visual Studio 6 workspace options file
|
|
||||||
*.opt
|
|
||||||
|
|
||||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
|
||||||
*.vbw
|
|
||||||
|
|
||||||
# Visual Studio LightSwitch build output
|
|
||||||
**/*.HTMLClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/ModelManifest.xml
|
|
||||||
**/*.Server/GeneratedArtifacts
|
|
||||||
**/*.Server/ModelManifest.xml
|
|
||||||
_Pvt_Extensions
|
|
||||||
|
|
||||||
# Paket dependency manager
|
|
||||||
.paket/paket.exe
|
|
||||||
paket-files/
|
|
||||||
|
|
||||||
# FAKE - F# Make
|
|
||||||
.fake/
|
|
||||||
|
|
||||||
# CodeRush personal settings
|
|
||||||
.cr/personal
|
|
||||||
|
|
||||||
# Python Tools for Visual Studio (PTVS)
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Cake - Uncomment if you are using it
|
|
||||||
# tools/**
|
|
||||||
# !tools/packages.config
|
|
||||||
|
|
||||||
# Tabs Studio
|
|
||||||
*.tss
|
|
||||||
|
|
||||||
# Telerik's JustMock configuration file
|
|
||||||
*.jmconfig
|
|
||||||
|
|
||||||
# BizTalk build output
|
|
||||||
*.btp.cs
|
|
||||||
*.btm.cs
|
|
||||||
*.odx.cs
|
|
||||||
*.xsd.cs
|
|
||||||
|
|
||||||
# OpenCover UI analysis results
|
|
||||||
OpenCover/
|
|
||||||
|
|
||||||
# Azure Stream Analytics local run output
|
|
||||||
ASALocalRun/
|
|
||||||
|
|
||||||
# MSBuild Binary and Structured Log
|
|
||||||
*.binlog
|
|
||||||
|
|
||||||
# NVidia Nsight GPU debugger configuration file
|
|
||||||
*.nvuser
|
|
||||||
|
|
||||||
# MFractors (Xamarin productivity tool) working folder
|
|
||||||
.mfractor/
|
|
||||||
|
|
||||||
# Local History for Visual Studio
|
|
||||||
.localhistory/
|
|
||||||
|
|
||||||
# BeatPulse healthcheck temp database
|
|
||||||
healthchecksdb
|
|
||||||
|
|
||||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
|
||||||
MigrationBackup/
|
|
||||||
|
|
||||||
# Ionide (cross platform F# VS Code tools) working folder
|
|
||||||
.ionide/
|
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
|
||||||
FodyWeavers.xsd
|
|
||||||
|
|
||||||
# VS Code files for those working on multiple tools
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
*.code-workspace
|
|
||||||
|
|
||||||
# Local History for Visual Studio Code
|
|
||||||
.history/
|
|
||||||
|
|
||||||
# Windows Installer files from build outputs
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msix
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
|
|
||||||
# JetBrains Rider
|
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
sdl_app
|
# CMake build files
|
||||||
|
CMakeFiles/
|
||||||
|
cmake_install.cmake
|
||||||
|
CMakeCache.txt
|
||||||
|
Makefile
|
||||||
|
*.cmake
|
||||||
|
|
||||||
*.sln.iml
|
# Android Studio
|
||||||
|
*.iml
|
||||||
|
.idea/
|
||||||
|
.cxx/
|
||||||
|
.gradle/
|
||||||
|
.DS_Store
|
||||||
|
/captures/
|
||||||
|
.externalNativeBuild/
|
||||||
|
|
||||||
images.zip
|
# NDK
|
||||||
script.py
|
.obj/
|
||||||
|
*.o
|
||||||
|
*.so
|
||||||
|
|
||||||
jumpingbird.*
|
# SDL2 (если используешь)
|
||||||
jumpingbird.data
|
libs/
|
||||||
build
|
obj/
|
||||||
build-emcmake
|
|
||||||
thirdparty
|
|
||||||
|
|
||||||
proj-web/build
|
# Временные файлы
|
||||||
proj-windows/build
|
*.swp
|
||||||
public
|
*.swo
|
||||||
web_resources/
|
*~
|
||||||
pc_resources/
|
.thumbs.db
|
||||||
resources_hd/
|
desktop.ini
|
||||||
web_resources_x2/
|
|
||||||
android_resources/
|
|
||||||
|
|
||||||
.artifacts/
|
# Проектные специфичные
|
||||||
|
app/build/
|
||||||
*.zip
|
*.log
|
||||||
|
app/jni/libpng
|
||||||
|
app/jni/SDL
|
||||||
|
app/jni/zlib
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Character Presentation Interpolation
|
|
||||||
|
|
||||||
The authoritative game simulation still advances in exact 100 ms ticks. Character
|
|
||||||
interpolation is a rendering-only feature and does not modify `CharacterState`,
|
|
||||||
network commands, tick IDs, state hashes, or deterministic lockstep behavior.
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
Each runtime `Character` keeps two presentation samples:
|
|
||||||
|
|
||||||
- `previous`: the visual values received at the preceding completed simulation tick;
|
|
||||||
- `current`: the visual values received at the latest completed simulation tick.
|
|
||||||
|
|
||||||
Rendering calculates an alpha from the real elapsed time since the latest completed
|
|
||||||
tick, clamped to the 100 ms simulation interval. It renders the delayed
|
|
||||||
`previous -> current` transition. This introduces at most one simulation tick of
|
|
||||||
presentation latency, but it avoids predicting a state that has not arrived from
|
|
||||||
the authoritative simulation.
|
|
||||||
|
|
||||||
The following values are interpolated:
|
|
||||||
|
|
||||||
- Position uses linear interpolation.
|
|
||||||
- Facing angle uses the shortest angular path, including wraparound at ±π.
|
|
||||||
- Animation frame remains fractional and is interpolated when both samples use
|
|
||||||
the same animation state. Looping clips treat the last frame to first frame
|
|
||||||
transition as a cyclic interval instead of interpolating backward through the
|
|
||||||
entire clip.
|
|
||||||
|
|
||||||
The interpolated values are used by mesh, weapon, shadow, night-lighting, name,
|
|
||||||
and health-bar rendering. The local camera and shadow-map center follow the same
|
|
||||||
interpolated player position; otherwise the camera would jump to the newest
|
|
||||||
authoritative position while the player model was still in its presentation
|
|
||||||
transition. Gameplay queries, commands, callbacks, collision, and presentation
|
|
||||||
events continue to use the authoritative snapshot.
|
|
||||||
|
|
||||||
## Other presentation animations
|
|
||||||
|
|
||||||
Teleport-zone star particles are advanced by `Location::updatePresentation()`
|
|
||||||
once per render frame using the real frame delta. They are no longer advanced
|
|
||||||
only from the 100 ms simulation-tick callback, so their visual motion remains
|
|
||||||
continuous without affecting teleport activation or other authoritative state.
|
|
||||||
|
|
||||||
Dialogue typewriter text and cutscene presentation timers use the same
|
|
||||||
per-render-frame path. This keeps character reveals, subtitle timing, fades, and
|
|
||||||
cutscene image poses smooth instead of stepping in 100 ms increments.
|
|
||||||
|
|
||||||
Interactive objects retain authoritative move, rotate, scale, and fade tasks on
|
|
||||||
the simulation tick. Their previous and current visual transforms are sampled
|
|
||||||
at tick boundaries and interpolated only while drawing, so doors and similar
|
|
||||||
objects remain logically deterministic while their presentation is smooth.
|
|
||||||
|
|
||||||
The same render-only sampling is used for `NpcCar` taxi visuals. Its position,
|
|
||||||
body rotation, wheel rotation, and steering angle are interpolated for drawing;
|
|
||||||
the authoritative taxi movement and waypoint logic remain fixed-step.
|
|
||||||
|
|
||||||
## Animation skinning
|
|
||||||
|
|
||||||
GPU and CPU bone interpolation receive the fractional render frame instead of an
|
|
||||||
integer-truncated frame. Bone positions are linearly interpolated and bone
|
|
||||||
rotations use spherical interpolation as before. Frames at the end of a clip are
|
|
||||||
clamped to the final keyframe.
|
|
||||||
|
|
||||||
## Discontinuities
|
|
||||||
|
|
||||||
The presentation buffer is reset instead of blended when a character is first
|
|
||||||
observed, changes animation state or enabled state, or has not received a tick
|
|
||||||
for a sufficiently long gap. This prevents stale positions, animation clips, or
|
|
||||||
disabled characters from producing visible interpolation artifacts.
|
|
||||||
|
|
||||||
## Local and networked games
|
|
||||||
|
|
||||||
Local games continue accumulating wall-clock remainder time and executing exact
|
|
||||||
100 ms simulation updates. Networked games continue consuming only contiguous
|
|
||||||
server tick messages. Both paths publish completed snapshots to the same runtime
|
|
||||||
presentation objects; interpolation only changes how those snapshots are drawn
|
|
||||||
between updates.
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
# Cutscene System
|
|
||||||
|
|
||||||
Cutscenes are defined in JSON and loaded by `CutsceneDatabase`. Each cutscene is a self-contained object with an array of animated image layers and optional subtitle lines.
|
|
||||||
|
|
||||||
The file can contain multiple cutscenes:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"cutscenes": [
|
|
||||||
{ "id": "intro", ... },
|
|
||||||
{ "id": "ending", ... }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Cutscenes and dialogues are loaded from **separate files**:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
dialogueSystem.loadDatabase("resources/dialogue/uni_interior.json"); // dialogues
|
|
||||||
dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes.json"); // cutscenes
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cutscene object
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "intro_cutscene",
|
|
||||||
"skippable": true,
|
|
||||||
"durationMs": 8000,
|
|
||||||
"fadeOutMs": 500,
|
|
||||||
"fadeInMs": 500,
|
|
||||||
"endFadeOutMs": 500,
|
|
||||||
"endFadeInMs": 500,
|
|
||||||
"onFadeInCallback": "",
|
|
||||||
"imageSegments": [ ... ],
|
|
||||||
"lines": [ ... ]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `id` | string | — | Unique identifier used to start the cutscene from C++ or dialogue (**required**) |
|
|
||||||
| `skippable` | bool | `true` | Whether the player can skip by holding LMB / touch |
|
|
||||||
| `durationMs` | int | `0` | Minimum content duration in ms. The cutscene will not end before this time even if all subtitle lines have finished. `0` means duration is determined solely by subtitle lines or `imageSegments.endMs` |
|
|
||||||
| `fadeOutMs` | int | `0` | Duration of the **opening fade** — game world fades to black before the cutscene images appear |
|
|
||||||
| `fadeInMs` | int | `0` | Duration of the **opening reveal** — cutscene images fade in from black after `fadeOutMs` |
|
|
||||||
| `endFadeOutMs` | int | `0` | Duration of the **closing fade** — cutscene fades to black at the end of content |
|
|
||||||
| `endFadeInMs` | int | `0` | Duration of the **closing reveal** — game world fades back in from black |
|
|
||||||
| `onFadeInCallback` | string | `""` | Lua function name called once the opening fade-in completes (fired after `fadeOutMs + fadeInMs` ms) |
|
|
||||||
| `imageSegments` | array | `[]` | Image layers with motion — see [Image segments](#image-segments) |
|
|
||||||
| `lines` | array | `[]` | Subtitle lines shown sequentially — see [Subtitle lines](#subtitle-lines) |
|
|
||||||
|
|
||||||
### Timing model
|
|
||||||
|
|
||||||
The total cutscene duration is:
|
|
||||||
|
|
||||||
```
|
|
||||||
contentDuration = max(durationMs, max(segment.endMs for all segments))
|
|
||||||
totalDuration = contentDuration + endFadeOutMs + endFadeInMs
|
|
||||||
```
|
|
||||||
|
|
||||||
The full timeline looks like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
|-- fadeOutMs --|-- fadeInMs --|--- content plays (images + subtitles) ---|-- endFadeOutMs --|-- endFadeInMs --|
|
|
||||||
world→black black→images images→black black→world
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Image segments
|
|
||||||
|
|
||||||
Each entry in `imageSegments` describes one image layer: when it is visible, how it fades in/out, and how it animates from a start pose to an end pose.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/bg_layer.png",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 8000,
|
|
||||||
"fadeInMs": 300,
|
|
||||||
"fadeOutMs": 300,
|
|
||||||
"easing": "EaseInOutSine",
|
|
||||||
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
|
|
||||||
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `path` | string | — | Path to the PNG image (**required**) |
|
|
||||||
| `width` | int | `0` | Logical width used for all UV and aspect-ratio math. `0` uses the actual texture pixel width |
|
|
||||||
| `height` | int | `0` | Logical height. `0` uses the actual texture pixel height |
|
|
||||||
| `startMs` | int | `0` | Time (ms from cutscene start) when this layer becomes active |
|
|
||||||
| `endMs` | int | `0` | Time (ms) when this layer stops being active. Must be > `startMs` |
|
|
||||||
| `fadeInMs` | int | `0` | Alpha fades from 0 → 1 over this many ms after `startMs`. `0` = instant |
|
|
||||||
| `fadeOutMs` | int | `0` | Alpha fades from 1 → 0 over this many ms before `endMs`. `0` = instant |
|
|
||||||
| `easing` | string | `"Linear"` | Easing applied to the pose interpolation — see [Easing types](#easing-types) |
|
|
||||||
| `from` | pose object | center/1.0 | Pose at `startMs` — see [Image pose](#image-pose) |
|
|
||||||
| `to` | pose object | same as `from` | Pose at `endMs`. If omitted, the layer stays at `from` the whole time |
|
|
||||||
|
|
||||||
Multiple segments can be active at the same time. They are rendered **in declaration order** (first = bottom layer, last = top layer), which enables parallax layering.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Image pose
|
|
||||||
|
|
||||||
A pose defines how an image is framed on screen at a given moment.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `centerX` | float | `0.5` | Normalized X position (0 = left edge of image, 1 = right edge) of the point that is placed at the horizontal center of the screen |
|
|
||||||
| `centerY` | float | `0.5` | Normalized Y position (0 = top edge, 1 = bottom edge) placed at the screen center |
|
|
||||||
| `scale` | float | `1.0` | Zoom level. `1.0` = the image fills the screen exactly (aspect-ratio corrected). `2.0` = zoomed in 2×, showing half the image area |
|
|
||||||
|
|
||||||
The runtime interpolates all three values independently from `from` to `to` using the chosen easing.
|
|
||||||
|
|
||||||
**Coordinate clamping:** `centerX`/`centerY` are automatically clamped so the viewport never shows area outside the image. For a zoomed-in segment (`scale > 1`) you therefore have more freedom to pan; for `scale = 1.0` the center is locked to `0.5/0.5`.
|
|
||||||
|
|
||||||
### Pose intuition
|
|
||||||
|
|
||||||
| Goal | Config |
|
|
||||||
|---|---|
|
|
||||||
| Centered, no zoom | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }` |
|
|
||||||
| Slightly zoomed in on center | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }` |
|
|
||||||
| Pan left to right | `from: { "centerX": 0.3, "scale": 1.2 }` → `to: { "centerX": 0.7, "scale": 1.2 }` |
|
|
||||||
| Zoom out from close-up | `from: { "scale": 1.8 }` → `to: { "scale": 1.0 }` |
|
|
||||||
| Look at top portion | `{ "centerY": 0.2, "scale": 1.3 }` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Easing types
|
|
||||||
|
|
||||||
Controls the interpolation curve applied to pose animation between `from` and `to`.
|
|
||||||
|
|
||||||
| Value | Description |
|
|
||||||
|---|---|
|
|
||||||
| `"Linear"` | Constant speed (default) |
|
|
||||||
| `"EaseInSine"` | Slow start, fast end |
|
|
||||||
| `"EaseOutSine"` | Fast start, slow end |
|
|
||||||
| `"EaseInOutSine"` | Slow start and end, fast middle |
|
|
||||||
| `"EaseInQuad"` | Quadratic slow start |
|
|
||||||
| `"EaseOutQuad"` | Quadratic slow end |
|
|
||||||
| `"EaseInOutQuad"` | Quadratic slow start and end |
|
|
||||||
| `"EaseInCubic"` | Cubic slow start |
|
|
||||||
| `"EaseOutCubic"` | Cubic slow end |
|
|
||||||
| `"EaseInOutCubic"` | Cubic slow start and end |
|
|
||||||
|
|
||||||
For cinematic camera motion `"EaseInOutSine"` or `"EaseInOutCubic"` give the most natural feel.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Subtitle lines
|
|
||||||
|
|
||||||
Lines are displayed sequentially on top of the cutscene images. Each line shows until its duration expires (or until the player advances, if `waitForConfirm` is set).
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"speaker": "Аида Дженибековна",
|
|
||||||
"text": "Здравствуйте, студенты.",
|
|
||||||
"durationMs": 3000,
|
|
||||||
"waitForConfirm": false,
|
|
||||||
"luaCallback": ""
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `speaker` | string | `""` | Speaker name shown above the subtitle text. Empty = no name bar |
|
|
||||||
| `text` | string | `""` | Subtitle text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
|
|
||||||
| `durationMs` | int | `0` | How long this line is displayed in ms. `0` = auto-computed from text length (~17 chars/sec, minimum 1500 ms) |
|
|
||||||
| `waitForConfirm` | bool | `false` | When `true`, the line waits for player input (tap/click/Enter) before advancing. No timer runs |
|
|
||||||
| `luaCallback` | string | `""` | Lua function name called when this line begins. Useful for triggering SFX, spawning effects, etc. |
|
|
||||||
|
|
||||||
Subtitle lines run on their own timer that is **independent** of the image segments. The cutscene ends when **both** subtitle lines are exhausted **and** `contentDuration` has elapsed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## C++ API
|
|
||||||
|
|
||||||
### Starting a cutscene
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Standalone cutscene (not part of a dialogue):
|
|
||||||
dialogueSystem.startCutscene("intro_cutscene");
|
|
||||||
|
|
||||||
// Skip the currently playing cutscene:
|
|
||||||
dialogueSystem.skipCutscene();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Callbacks
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Called when a cutscene begins:
|
|
||||||
dialogueSystem.setOnCutsceneStarted([]() { /* hide HUD, etc. */ });
|
|
||||||
|
|
||||||
// Called when a cutscene ends (receives the cutscene id):
|
|
||||||
dialogueSystem.setOnCutsceneFinished([](const std::string& id) {
|
|
||||||
// id == "intro_cutscene"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Called when a subtitle line begins (receives luaCallback value):
|
|
||||||
dialogueSystem.setOnCutsceneLineStarted([](const std::string& fn) {
|
|
||||||
scriptEngine.callActivateFunction(fn);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Called when the opening fade-in completes (receives onFadeInCallback value):
|
|
||||||
dialogueSystem.setOnCutsceneFadeInComplete([](const std::string& fn) {
|
|
||||||
scriptEngine.callActivateFunction(fn);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Triggering from dialogue
|
|
||||||
|
|
||||||
A dialogue node of type `CutsceneStart` embeds a cutscene mid-conversation. Dialogue resumes at `next` when the cutscene ends.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "node_cutscene",
|
|
||||||
"type": "CutsceneStart",
|
|
||||||
"cutsceneId": "intro_cutscene",
|
|
||||||
"next": "node_after_cutscene"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Full examples
|
|
||||||
|
|
||||||
### Minimal — static image, timed
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "simple",
|
|
||||||
"durationMs": 4000,
|
|
||||||
"fadeOutMs": 300,
|
|
||||||
"fadeInMs": 300,
|
|
||||||
"endFadeOutMs": 300,
|
|
||||||
"endFadeInMs": 300,
|
|
||||||
"imageSegments": [
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/city.png",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 4000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Two-layer parallax pan
|
|
||||||
|
|
||||||
Background moves slowly left-to-right; foreground character moves faster, creating depth.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "classroom_intro",
|
|
||||||
"durationMs": 8000,
|
|
||||||
"fadeOutMs": 500,
|
|
||||||
"fadeInMs": 500,
|
|
||||||
"endFadeOutMs": 500,
|
|
||||||
"endFadeInMs": 500,
|
|
||||||
"imageSegments": [
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/classroom_bg.png",
|
|
||||||
"width": 1920,
|
|
||||||
"height": 1080,
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 8000,
|
|
||||||
"fadeInMs": 400,
|
|
||||||
"easing": "EaseInOutSine",
|
|
||||||
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
|
|
||||||
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/classroom_teacher.png",
|
|
||||||
"width": 1920,
|
|
||||||
"height": 1080,
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 8000,
|
|
||||||
"easing": "EaseInOutSine",
|
|
||||||
"from": { "centerX": 0.35, "centerY": 0.5, "scale": 1.0 },
|
|
||||||
"to": { "centerX": 0.65, "centerY": 0.5, "scale": 1.0 }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lines": [
|
|
||||||
{
|
|
||||||
"speaker": "Аида Дженибековна",
|
|
||||||
"text": "Здравствуйте, студенты.",
|
|
||||||
"durationMs": 3000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker": "Аида Дженибековна",
|
|
||||||
"text": "Рассаживайтесь.",
|
|
||||||
"durationMs": 2500
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Zoom-in reveal with a second image appearing mid-way
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "letter_reveal",
|
|
||||||
"durationMs": 7000,
|
|
||||||
"fadeOutMs": 400,
|
|
||||||
"fadeInMs": 600,
|
|
||||||
"endFadeOutMs": 600,
|
|
||||||
"endFadeInMs": 400,
|
|
||||||
"imageSegments": [
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/desk_bg.png",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 7000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/letter_closeup.png",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"startMs": 2000,
|
|
||||||
"endMs": 7000,
|
|
||||||
"fadeInMs": 800,
|
|
||||||
"easing": "EaseOutCubic",
|
|
||||||
"from": { "centerX": 0.5, "centerY": 0.5, "scale": 2.5 },
|
|
||||||
"to": { "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lines": [
|
|
||||||
{
|
|
||||||
"text": "Среди бумаг на столе лежит конверт.",
|
|
||||||
"durationMs": 2500
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker": "Главный герой",
|
|
||||||
"text": "«Явитесь в деканат немедленно».",
|
|
||||||
"durationMs": 3000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
# Game State Architecture
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
The game separates authoritative gameplay state from rendering and runtime
|
|
||||||
resources. The same snapshot-and-command boundary supports the in-process
|
|
||||||
single-player server and a WebSocket deterministic-lockstep client/server.
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
The architecture has six layers:
|
|
||||||
|
|
||||||
1. `GameState` is the aggregate root for persistent, authoritative state.
|
|
||||||
2. `InternalGameServer` owns `GameState`, advances the local simulation, handles commands, and produces immutable snapshots.
|
|
||||||
3. `ClientLocationState` owns per-client `LocationLocalState` records, including camera orientation. Graphical hosts own this state; it is never replicated.
|
|
||||||
4. Runtime objects hold rendering resources. `Character` and `Location` read immutable server snapshots by stable ID, while `InteractiveObject` still uses a transitional non-owning binding to server-owned state. `Location` also translates local input, scripts, and collision results into semantic server commands.
|
|
||||||
5. Client-facing systems such as `MenuManager` read immutable snapshots and submit typed commands through `IGameServer`.
|
|
||||||
6. Protocol-v2 WebSocket hosts assign preconfigured player slots and exchange
|
|
||||||
one initial state followed by ordered 100 ms tick bundles.
|
|
||||||
|
|
||||||
```text
|
|
||||||
Input / UI
|
|
||||||
| typed GameCommand
|
|
||||||
v
|
|
||||||
IGameServer
|
|
||||||
|
|
|
||||||
v
|
|
||||||
InternalGameServer ---- owns ----> GameState
|
|
||||||
| |
|
|
||||||
| update | stable state objects
|
|
||||||
v v
|
|
||||||
Location / Character / InteractiveObject runtime
|
|
||||||
|
|
|
||||||
v
|
|
||||||
Renderer
|
|
||||||
|
|
||||||
IGameServer ---- immutable GameSnapshot ----> UI and other clients
|
|
||||||
```
|
|
||||||
|
|
||||||
## Authoritative State Model
|
|
||||||
|
|
||||||
### `GameState`
|
|
||||||
|
|
||||||
`GameState` is the top-level state object. It contains:
|
|
||||||
|
|
||||||
- Campaign ID and current location ID.
|
|
||||||
- Day, night, dawn, and darklands flags.
|
|
||||||
- A map of campaign-level players keyed by stable player ID.
|
|
||||||
- A map of typed `LocationGameState` records keyed by location ID.
|
|
||||||
- Inventory, money, player-health HUD values, quests, tutorial progress, chat data, taxi state, and script-global values.
|
|
||||||
|
|
||||||
Live `Location` pointers and rendering objects are no longer stored in `GameState`.
|
|
||||||
Embedded state models such as `Inventory` and `QuestJournal` contain only value
|
|
||||||
data and deterministic operations; they do not retain runtime callbacks.
|
|
||||||
|
|
||||||
### Campaign-level players
|
|
||||||
|
|
||||||
Player `CharacterState` objects are stored in `GameState::players`. The current single-player game uses the ID `"player"`.
|
|
||||||
|
|
||||||
The player state is no longer owned by a `Character` or duplicated as a separately authoritative state for each loaded location. This permits additional player IDs to be introduced later without changing the aggregate ownership model.
|
|
||||||
|
|
||||||
Every player, including the one controlled by the current client, remains in this
|
|
||||||
same authoritative collection. `IGameServer::localPlayerId()` identifies which
|
|
||||||
entry is local to a particular client connection; it is not a separate state
|
|
||||||
object.
|
|
||||||
|
|
||||||
`LocationGameState` does not retain a player pointer or back-reference to `GameState`.
|
|
||||||
Its update boundary receives `GameState::players` by reference, allowing character
|
|
||||||
lookup and simulation to treat every campaign player by stable ID.
|
|
||||||
|
|
||||||
### `LocationGameState`
|
|
||||||
|
|
||||||
Each location record owns:
|
|
||||||
|
|
||||||
- One `LocationNetworkState`.
|
|
||||||
- NPC `CharacterState` objects keyed by configured ID.
|
|
||||||
- `InteractiveObjectState` objects keyed by configured ID.
|
|
||||||
- Trigger-zone enabled/inside flags.
|
|
||||||
- Teleport-zone active flags.
|
|
||||||
- Serialized dialogue runtime state.
|
|
||||||
- Serialized Lua runtime globals.
|
|
||||||
|
|
||||||
World-mode flags (`darklands`, `night`, and `dawn`) exist only at the aggregate
|
|
||||||
`GameState` level. Location rendering and scripts read those values from the
|
|
||||||
top-level snapshot rather than maintaining per-location mirrors.
|
|
||||||
|
|
||||||
State objects use `shared_ptr` internally to keep addresses stable for the
|
|
||||||
remaining local runtime bindings. These pointers never appear in public snapshots.
|
|
||||||
|
|
||||||
### Stable identity
|
|
||||||
|
|
||||||
- Locations use campaign configuration IDs.
|
|
||||||
- Players use keys from `GameState::players`.
|
|
||||||
- NPCs use required location-local configuration IDs.
|
|
||||||
- Interactive objects use required location-local configuration IDs.
|
|
||||||
- Trigger and teleport zones use their configured IDs.
|
|
||||||
|
|
||||||
Missing, empty, duplicate, and reserved IDs are content errors. Runtime references,
|
|
||||||
Lua APIs, snapshots, and persistence all use these IDs rather than configuration
|
|
||||||
array positions.
|
|
||||||
|
|
||||||
## Server Interface
|
|
||||||
|
|
||||||
`IGameServer` is the public state boundary. It exposes:
|
|
||||||
|
|
||||||
- `snapshot()` for an immutable, revisioned `GameSnapshot`.
|
|
||||||
- `submit()` for typed `GameCommand` values.
|
|
||||||
- `update()` for advancing a local fixed tick or polling a remote implementation.
|
|
||||||
- `save()` and `load()` for server-side persistence.
|
|
||||||
|
|
||||||
No public method returns a mutable state pointer or reference.
|
|
||||||
|
|
||||||
### Snapshots
|
|
||||||
|
|
||||||
`GameSnapshot` and its nested snapshot types are value-only read models. They contain no renderer objects, runtime `Location` pointers, mutable references, or active callbacks.
|
|
||||||
|
|
||||||
Snapshots include:
|
|
||||||
|
|
||||||
- Campaign and world-mode state.
|
|
||||||
- All players.
|
|
||||||
- Locations, NPCs, interactive objects, triggers, and teleports.
|
|
||||||
- Inventory item IDs.
|
|
||||||
- Quest views.
|
|
||||||
- Tutorial, chat, taxi, money, and health data.
|
|
||||||
- Script-global values.
|
|
||||||
|
|
||||||
Snapshots have a monotonically increasing revision. `InternalGameServer` caches the current immutable snapshot and rebuilds it only after the authoritative state changes.
|
|
||||||
|
|
||||||
### Commands
|
|
||||||
|
|
||||||
`GameCommand` is a C++17 `std::variant` of semantic command types. Current commands cover:
|
|
||||||
|
|
||||||
- World mode and current location.
|
|
||||||
- Player health and money.
|
|
||||||
- Tutorial progression and flags.
|
|
||||||
- Chat history and unread state.
|
|
||||||
- Taxi state.
|
|
||||||
- Inventory additions and removals.
|
|
||||||
- Quest status, objective completion/visibility, and active-objective changes.
|
|
||||||
- Script globals.
|
|
||||||
- Character transforms, attack targets, navigation destinations, stopping,
|
|
||||||
facing, replanning, health, damage, death reset, attack combos, and animation
|
|
||||||
metadata. Character position, facing angle, home position, face target, and
|
|
||||||
enabled state use separate semantic commands; no whole-character commit
|
|
||||||
command is exposed.
|
|
||||||
- Interactive-object activation.
|
|
||||||
- Trigger-zone state.
|
|
||||||
- Quest-definition reloads.
|
|
||||||
|
|
||||||
The remote implementation serializes destination intent from clients, while
|
|
||||||
the server emits connection-lifecycle presence commands. The server derives the
|
|
||||||
character and location from the assigned connection slot; arbitrary state
|
|
||||||
mutation commands are not accepted from the network.
|
|
||||||
`SetCharacterEnabledCommand` is consumed by the server and replicated to every
|
|
||||||
client in the ordered tick bundle when a slot connects or disconnects.
|
|
||||||
|
|
||||||
### Presentation events
|
|
||||||
|
|
||||||
`IGameServer::drainEvents()` returns value-only events produced by successful
|
|
||||||
authoritative mutations. Inventory add/remove and quest unlock/complete/fail/
|
|
||||||
objective-complete events contain stable IDs only. `Game` drains these events and
|
|
||||||
performs local tutorial, toast, and audio presentation through `MenuManager`.
|
|
||||||
Loading and persistence restoration clear pending events and remain silent.
|
|
||||||
|
|
||||||
## Internal Server
|
|
||||||
|
|
||||||
`InternalGameServer` is the current `IGameServer` implementation. It:
|
|
||||||
|
|
||||||
- Exclusively owns the aggregate `GameState`.
|
|
||||||
- Executes typed commands against authoritative state.
|
|
||||||
- Advances the active location simulation in exact 100 ms ticks.
|
|
||||||
- Builds and caches immutable snapshots.
|
|
||||||
- Delegates save/load serialization to the aggregate state model.
|
|
||||||
|
|
||||||
`LocalGameServerHost` is the graphical client's integration adapter. It owns loaded
|
|
||||||
runtime `Location` objects, forwards simulation events to them, and synchronizes
|
|
||||||
dialogue, Lua, trigger, and teleport auxiliary state. `Game` uses the explicit
|
|
||||||
`IGameServerHost` lifecycle interface and is no longer a friend of
|
|
||||||
`InternalGameServer`. The standalone server does not compile this adapter.
|
|
||||||
|
|
||||||
Location loading has two phases. `LocationContentLoader` parses renderer-free
|
|
||||||
`LocationDefinition` data, and `LocationGameState::setup()` creates/reconciles
|
|
||||||
authoritative NPC, interactive-object, navigation, taxi, trigger, teleport, and
|
|
||||||
animation state. The graphical `Location::setup()` runs afterward and only builds
|
|
||||||
presentation objects from that definition and the prepared state. Lua and dialogue
|
|
||||||
runtime behavior remains client-side.
|
|
||||||
|
|
||||||
### Animation and combat
|
|
||||||
|
|
||||||
Logical animation frame clocks live in `CharacterState`. The renderer-free content
|
|
||||||
loader reads animation frame metadata before authoritative entities are created.
|
|
||||||
Runtime `Character` construction only loads models for presentation and does not
|
|
||||||
submit metadata mutations back to the server.
|
|
||||||
|
|
||||||
`CharacterState::update()` returns value-only events containing arrival Lua function
|
|
||||||
names, attack impacts, navigation requests, and death-animation completion.
|
|
||||||
`LocationGameState` applies queued damage after every character has advanced.
|
|
||||||
Navigation replans are consumed by `InternalGameServer` as deterministic
|
|
||||||
simulation consequences rather than being resubmitted by presentation code.
|
|
||||||
Runtime `Character` objects only pose loaded models and produce audio, particles,
|
|
||||||
and presentation callbacks.
|
|
||||||
|
|
||||||
### Character state operations
|
|
||||||
|
|
||||||
Gameplay operations mutate the authoritative `CharacterState`, not the runtime
|
|
||||||
`Character`:
|
|
||||||
|
|
||||||
- `setTarget()` normalizes the destination, avoids redundant requests, stores the
|
|
||||||
optional Lua arrival function name, and asks the authoritative `LocationNetworkState` to
|
|
||||||
call `planPathExceptMyself()`.
|
|
||||||
- `forceReplan()` recalculates the current requested destination using the same
|
|
||||||
location navigation state.
|
|
||||||
- `stopInPlace()` clears navigation and any pending arrival function.
|
|
||||||
- `stopAndRotateToTarget()`, `resetPlayerAfterDeath()`, `setHp()`, and
|
|
||||||
`tryAttackCombo()` update their respective state directly.
|
|
||||||
|
|
||||||
`InternalGameServer::submit()` resolves the character and location, then invokes
|
|
||||||
these operations for the corresponding semantic command. The runtime `Location`
|
|
||||||
provides convenience methods for submitting those commands from input, scripts,
|
|
||||||
collision handling, and presentation-event processing.
|
|
||||||
|
|
||||||
## Runtime and Visual Object Bindings
|
|
||||||
|
|
||||||
Runtime visual objects do not own their authoritative state objects.
|
|
||||||
|
|
||||||
- `Location` keeps its `IGameServer` reference, stable location ID, and a shared
|
|
||||||
`LocationLocalState` supplied by the graphical host. It resolves its immutable
|
|
||||||
`LocationNetworkState` view from `IGameServer::snapshot()`, while rendering and
|
|
||||||
camera input access the local state directly.
|
|
||||||
- `Character` keeps only its `IGameServer` reference plus stable entity and location IDs. It resolves the player-first character view from `IGameServer::snapshot()`.
|
|
||||||
- `InteractiveObject` keeps a private non-owning reference to its `InteractiveObjectState`; its configured object name is its stable ID.
|
|
||||||
|
|
||||||
`Character::stateSnapshot()` and `Location::stateSnapshot()` return aliasing
|
|
||||||
`shared_ptr` views. Each pointer addresses a state copy inside a `GameSnapshot`,
|
|
||||||
while shared ownership keeps that whole immutable snapshot alive. Update, input,
|
|
||||||
editor, and draw operations capture one such pointer and use it consistently
|
|
||||||
throughout the operation. A missing or unloaded entity produces an empty pointer
|
|
||||||
and presentation or input work is skipped.
|
|
||||||
|
|
||||||
The authoritative state objects are owned by `GameState` or `LocationGameState`.
|
|
||||||
The internal server advances them before publishing a new snapshot revision.
|
|
||||||
Runtime input, scripts, and collision handling submit semantic commands that invoke
|
|
||||||
authoritative character and location operations. Location commands cover pending
|
|
||||||
interaction targets, navigation switching and area changes, editor obstacles, and
|
|
||||||
taxi reset. Camera orientation is deliberately not a server command. No
|
|
||||||
whole-location commit command is exposed.
|
|
||||||
|
|
||||||
Navigation target selection, replanning, stopping, facing, death reset, health
|
|
||||||
assignment, and attack-combo clock changes are implemented by `CharacterState`.
|
|
||||||
`Character` no longer exposes gameplay mutation helpers or a generic state mutation
|
|
||||||
callback. Character construction is presentation-only; frame ranges are already
|
|
||||||
present in `CharacterState` after content setup. It focuses on model posing,
|
|
||||||
rendering, audio, particles, and runtime callbacks.
|
|
||||||
|
|
||||||
Death-animation and health-change callbacks remain runtime-only on `Character`.
|
|
||||||
Arrival behavior is represented solely by the serialized Lua function name in
|
|
||||||
`CharacterState`; `Location` resolves and invokes that name through `ScriptEngine`
|
|
||||||
when it consumes the arrival event.
|
|
||||||
|
|
||||||
`Location` owns presentation-only runtime characters for all non-local entries in
|
|
||||||
`GameSnapshot::players`. It reconciles them by stable ID as snapshots change,
|
|
||||||
reuses the local-player assets and animation set, and includes them in all render,
|
|
||||||
shadow, lighting, animation, damage, and name-tag passes. The local entry is
|
|
||||||
excluded using `IGameServer::localPlayerId()` because its visual is already owned
|
|
||||||
by `Game`.
|
|
||||||
|
|
||||||
`GameObjectLoader` now:
|
|
||||||
|
|
||||||
1. Receives immutable object/NPC definitions parsed by `LocationContentLoader`.
|
|
||||||
2. Resolves already-prepared authoritative entities by stable ID.
|
|
||||||
3. Constructs meshes, textures, VBOs, `InteractiveObject`, and `Character`
|
|
||||||
presentation wrappers without creating or overwriting authoritative state.
|
|
||||||
|
|
||||||
Configuration remains the source of visual assets, animation paths, meshes, textures, and fixed tuning data.
|
|
||||||
|
|
||||||
## State Update Flow
|
|
||||||
|
|
||||||
Render frames and simulation ticks are separate:
|
|
||||||
|
|
||||||
1. SDL input and UI presentation use the real elapsed render-frame time.
|
|
||||||
2. A local host accumulates `steady_clock` elapsed time and runs zero or more
|
|
||||||
exact 100 ms simulation ticks. A remote host polls transport every frame but
|
|
||||||
advances only from contiguous server tick messages.
|
|
||||||
3. Commands assigned to tick `N` are applied in server sequence order before
|
|
||||||
simulation tick `N`.
|
|
||||||
4. The server freezes location character observations and updates every player,
|
|
||||||
NPC, taxi, and interactive object with exactly 100 ms. An empty player
|
|
||||||
collection remains valid.
|
|
||||||
5. Attack impacts are queued and applied only after all characters advance.
|
|
||||||
Navigation recovery is applied inside the deterministic state path.
|
|
||||||
6. The server invalidates its cached snapshot and advances its revision.
|
|
||||||
7. Runtime locations consume the completed tick's value-only events and perform
|
|
||||||
rendering, audio, particles, and supported presentation callbacks.
|
|
||||||
8. Rendering can occur between ticks and never changes authoritative tick time.
|
|
||||||
|
|
||||||
Commands that update bound character or object state do not reload the whole location. This avoids replaying dialogue-node effects or Lua callbacks.
|
|
||||||
|
|
||||||
## Campaign and Location Lifecycle
|
|
||||||
|
|
||||||
Only one location is normally loaded as a runtime object.
|
|
||||||
|
|
||||||
### Campaign loading
|
|
||||||
|
|
||||||
When a campaign starts:
|
|
||||||
|
|
||||||
- Existing campaign state and runtime locations are cleared.
|
|
||||||
- Quest definitions are loaded.
|
|
||||||
- The graphical single-player host requests a campaign-level `"player"` state and
|
|
||||||
creates its visual wrapper.
|
|
||||||
- A standalone host may deliberately create no players; locations continue to tick.
|
|
||||||
- The battle-zone test bot is an explicit host option rather than an unconditional
|
|
||||||
campaign side effect.
|
|
||||||
|
|
||||||
### Location loading
|
|
||||||
|
|
||||||
When a location is loaded:
|
|
||||||
|
|
||||||
- `LocationContentLoader` parses a renderer-free `LocationDefinition`.
|
|
||||||
- `LocationGameState::setup()` creates or reconciles navigation, taxi, NPC,
|
|
||||||
interactive-object, trigger, teleport, and animation state.
|
|
||||||
- Saved mutable fields are overlaid on configuration defaults by stable ID.
|
|
||||||
- `Location::setup()` builds static and entity visuals from the prepared definition
|
|
||||||
without mutating authoritative state.
|
|
||||||
- The shared campaign player is attached.
|
|
||||||
- Runtime visuals are created for other player IDs currently present in the
|
|
||||||
snapshot and are subsequently spawned or removed as snapshots change.
|
|
||||||
- Dialogue, Lua, trigger/teleport visuals, and presentation callbacks are initialized.
|
|
||||||
- The runtime `Location` is owned and registered by `LocalGameServerHost`;
|
|
||||||
`InternalGameServer` remains headless.
|
|
||||||
|
|
||||||
### Location unloading
|
|
||||||
|
|
||||||
When leaving a location:
|
|
||||||
|
|
||||||
- Auxiliary runtime state is captured.
|
|
||||||
- The visual/runtime `Location` is destroyed.
|
|
||||||
- Its typed `LocationGameState` remains in `GameState`.
|
|
||||||
|
|
||||||
The player state remains campaign-level and survives location changes.
|
|
||||||
|
|
||||||
## Save and Load Schema
|
|
||||||
|
|
||||||
Authoritative persistence uses one strict, versioned schema. `schemaVersion` is
|
|
||||||
required and unsupported versions are rejected rather than migrated. A
|
|
||||||
single-player save is a separate versioned envelope containing `networkState`
|
|
||||||
(the authoritative `GameState` JSON) and `localState` (the client-local location
|
|
||||||
map). Lockstep sends that authoritative document only in `initial_state`; later
|
|
||||||
messages contain numbered command bundles.
|
|
||||||
|
|
||||||
- Campaign players are stored once in an ID-keyed `players` object.
|
|
||||||
- Locations are keyed by location ID.
|
|
||||||
- Each location stores NPC and interactive-object state in ID-keyed objects.
|
|
||||||
- Pending interaction targets and character references use stable IDs.
|
|
||||||
- Immutable content such as meshes, initial taxi configuration, and object
|
|
||||||
definitions is reconstructed from campaign configuration.
|
|
||||||
- Configuration is loaded first and mutable saved state is overlaid in place.
|
|
||||||
Snapshot-backed runtime objects then resolve the resulting state by stable ID.
|
|
||||||
|
|
||||||
Old user save slots are intentionally unsupported. Bundled start-state snapshots
|
|
||||||
must be regenerated with `Game::saveInitialState()` whenever the schema changes.
|
|
||||||
|
|
||||||
## UI, Scripts, Dialogue, and Quests
|
|
||||||
|
|
||||||
### UI
|
|
||||||
|
|
||||||
`MenuManager` no longer holds `GameState&` or a mutable `Inventory*`.
|
|
||||||
|
|
||||||
It now:
|
|
||||||
|
|
||||||
- Reads inventory, quests, tutorial state, chat, money, health, location, and world mode from `GameSnapshot`.
|
|
||||||
- Submits typed commands for mutations.
|
|
||||||
- Continues to own presentation-only UI trees, screen state, textures, and callbacks.
|
|
||||||
|
|
||||||
### Scripts
|
|
||||||
|
|
||||||
Lua remains an in-process single-player runtime. Character, inventory, quest, and
|
|
||||||
campaign-global bindings submit semantic commands through `Location`; the server
|
|
||||||
invokes operations on centrally owned state. Snapshot queries provide global
|
|
||||||
values, inventory counts, item presence, and synchronous quest validation for the
|
|
||||||
existing Lua API. NPC,
|
|
||||||
interactive-object, trigger-zone, and teleport-zone bindings address entities by
|
|
||||||
configured ID. Other object and global-state bindings remain trusted local
|
|
||||||
integrations.
|
|
||||||
|
|
||||||
Each location script registers initialization, enter, and exit callbacks.
|
|
||||||
Initialization runs only when the location has no retained state; loading the Lua
|
|
||||||
file again for rendering does not repeat state-resetting setup. Enter and exit
|
|
||||||
callbacks continue to run for every location transition.
|
|
||||||
|
|
||||||
The scalar Lua runtime state for each location is stored under `LocationGameState::scriptGlobals`. Multiplayer location scripts are expected to be empty, so remote script execution is intentionally outside the current scope.
|
|
||||||
|
|
||||||
### Dialogue and cutscenes
|
|
||||||
|
|
||||||
Dialogue runtime state is captured in each `LocationGameState`. Dialogue global
|
|
||||||
flags use snapshot getter and command setter callbacks rather than retaining a
|
|
||||||
mutable map pointer. Dialogue and cutscene rendering remain local presentation
|
|
||||||
systems.
|
|
||||||
|
|
||||||
Dialogues and cutscenes are outside the planned multiplayer gameplay mode, but their existing single-player save and callback behavior is retained.
|
|
||||||
|
|
||||||
### Quests, inventory, and tutorial
|
|
||||||
|
|
||||||
These callback-free state systems remain part of `GameState` for single-player
|
|
||||||
compatibility. UI access, mutations, and presentation notifications cross the
|
|
||||||
server interface. They can remain empty or disabled in multiplayer mode as planned.
|
|
||||||
|
|
||||||
## Multiplayer Lockstep
|
|
||||||
|
|
||||||
Protocol v2 uses server-sequenced deterministic lockstep:
|
|
||||||
|
|
||||||
- The server assigns a connection to a preconfigured campaign player slot.
|
|
||||||
- Player slots are disabled in the authoritative initial state. A successful
|
|
||||||
connection schedules `SetCharacterEnabledCommand(enabled=true)` for the next
|
|
||||||
lockstep tick; disconnect schedules the corresponding `enabled=false` command.
|
|
||||||
- Connection receives one authoritative state at a completed tick plus its
|
|
||||||
assigned player ID, tick duration, input delay, and compatibility fingerprint.
|
|
||||||
- Destination input is normalized and scheduled two ticks in the future.
|
|
||||||
- The server broadcasts every numbered tick, including empty ticks. Clients
|
|
||||||
buffer out-of-order messages and advance only contiguous tick IDs.
|
|
||||||
- All replicas apply the same globally sequenced commands before one exact
|
|
||||||
100 ms update.
|
|
||||||
- Post-tick canonical hashes exclude persistence timestamps and local
|
|
||||||
presentation state. A mismatch is diagnosed but not automatically repaired.
|
|
||||||
|
|
||||||
No wall-clock timestamp participates in simulation. `steady_clock` paces the
|
|
||||||
server and local render accumulator only, so different machine clocks do not
|
|
||||||
affect command placement or state. The current missing-packet policy is to stall;
|
|
||||||
there is no prediction or rollback.
|
|
||||||
|
|
||||||
Current boundaries remain:
|
|
||||||
|
|
||||||
- client intent is limited to `SetCharacterDestinationCommand`; server presence
|
|
||||||
changes use `SetCharacterEnabledCommand` in tick bundles;
|
|
||||||
- player characters must already exist in the initial state and campaign slot
|
|
||||||
list; dynamic spawning and reconnect history are not implemented;
|
|
||||||
- authentication and automatic checkpoint resynchronization are absent;
|
|
||||||
- multiplayer Lua/dialogue shared behavior is unsupported;
|
|
||||||
- deterministic compatibility targets identical executable and content builds.
|
|
||||||
Floating-point simulation is not guaranteed bit-identical across platforms.
|
|
||||||
|
|
||||||
## Platform Build Integration
|
|
||||||
|
|
||||||
The new state and server files are registered in:
|
|
||||||
|
|
||||||
- `proj-windows/CMakeLists.txt`
|
|
||||||
- `proj-linux/CMakeLists.txt`
|
|
||||||
- `proj-web/CMakeLists.txt`
|
|
||||||
- `proj-android/app/jni/src/CMakeLists.txt`
|
|
||||||
|
|
||||||
The refactor uses portable C++17 and introduces no Windows-only gameplay code.
|
|
||||||
|
|
||||||
The standalone target lives under `server/`. It links the authoritative state,
|
|
||||||
campaign, content-definition, and portable resource sources only; SDL, OpenGL,
|
|
||||||
audio, Lua, dialogue presentation, and rendering sources are absent. It can prepare
|
|
||||||
a selected campaign/location with zero players. The Windows server adds
|
|
||||||
Boost.Beast transport and runs a fixed 100 ms lockstep loop; other platforms
|
|
||||||
continue to compile the portable state core without that transport.
|
|
||||||
|
|
||||||
The Windows Debug target was verified with:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -- /m
|
|
||||||
```
|
|
||||||
|
|
||||||
## Important Constraints
|
|
||||||
|
|
||||||
- Runtime C++ callbacks are not part of `CharacterState` or public snapshots;
|
|
||||||
serializable Lua arrival function names are state data.
|
|
||||||
- `InteractiveObjectState` animation callbacks are also removed from public snapshots.
|
|
||||||
- Character target and attacker references use stable IDs (`"player"` or a unique
|
|
||||||
location-local NPC ID).
|
|
||||||
- Some top-level values remain mirrors for compatibility, notably player health used by the HUD and scripts.
|
|
||||||
- Dialogue and Lua are synchronized as auxiliary location state rather than fully networked subsystems.
|
|
||||||
- `IGameServerLifecycle` is the graphics-free campaign/location administration
|
|
||||||
boundary. `LocalGameServerHost` adds client-only runtime construction and
|
|
||||||
auxiliary-state synchronization without making `Game` a server friend.
|
|
||||||
- Empty player maps are valid. Player-specific interactions remain inactive until
|
|
||||||
a player entity is added.
|
|
||||||
- Pathfinding priority queues use cell index as a stable secondary key.
|
|
||||||
- Gameplay randomness must be seeded and serialized; the `random_device` battle
|
|
||||||
bot is disabled for multiplayer sessions.
|
|
||||||
- A lockstep compatibility fingerprint must match before the client accepts the
|
|
||||||
initial state.
|
|
||||||
|
|
||||||
## Main Files
|
|
||||||
|
|
||||||
- `src/GameState.h` / `src/GameState.cpp` — aggregate state and top-level persistence.
|
|
||||||
- `src/LocationState.h` / `src/LocationState.cpp` — separate local camera and authoritative network location records, plus single-player persistence composition.
|
|
||||||
- `src/LocationGameState.h` / `src/LocationGameState.cpp` — authoritative typed location state, simulation, and ID-keyed persistence.
|
|
||||||
- `src/content/LocationDefinition.h` / `.cpp` — renderer-free content definitions,
|
|
||||||
parsing, and animation metadata loading.
|
|
||||||
- `src/game_server/IGameServer.h` — public snapshot and command contract.
|
|
||||||
- `src/game_server/IGameServerLifecycle.h` — graphics-free campaign/location lifecycle.
|
|
||||||
- `src/game_server/InternalGameServer.h` / `.cpp` — in-process authoritative implementation.
|
|
||||||
- `src/game_server/LocalGameServerHost.h` / `.cpp` — graphical-client runtime adapter.
|
|
||||||
- `src/game_server/NetworkProtocol.h` / `.cpp` — protocol-v2 messages,
|
|
||||||
lockstep timeline, validation, and canonical hashes.
|
|
||||||
- `src/game_server/WebsocketGameServer.h` / `.cpp` and
|
|
||||||
`WebsocketGameClient.h` / `.cpp` — slot assignment, input scheduling, tick
|
|
||||||
broadcasting, buffering, and replica advancement.
|
|
||||||
- `src/Game.h` / `src/Game.cpp` — application host, campaign lifecycle, and runtime wiring.
|
|
||||||
- `src/Location.h` / `src/Location.cpp` — loaded location runtime, command routing,
|
|
||||||
interactions, collisions, scripts, and presentation-event consumption.
|
|
||||||
- `src/Character.h` / `src/Character.cpp` — passive character presentation resolving
|
|
||||||
immutable `CharacterState` snapshots by stable ID.
|
|
||||||
- `src/CharacterState.h` / `src/CharacterState.cpp` — deterministic character
|
|
||||||
simulation, navigation operations, combat state, and logical animation clock.
|
|
||||||
- `src/items/InteractiveObject.h` / `.cpp` — visual object runtime bound to central object state.
|
|
||||||
- `src/items/GameObjectLoader.h` / `.cpp` — presentation builders for prepared definitions.
|
|
||||||
- `src/MenuManager.h` / `src/MenuManager.cpp` — snapshot-driven UI and command submission.
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
# Pathfinding System
|
|
||||||
|
|
||||||
This document describes the grid-based pathfinding used for the player and all NPCs, including the collision avoidance and movement quality improvements.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
1. [Grid Representation](#1-grid-representation)
|
|
||||||
2. [Building the Walkable Grid](#2-building-the-walkable-grid)
|
|
||||||
3. [A\* Path Search](#3-a-path-search)
|
|
||||||
4. [Path Smoothing](#4-path-smoothing)
|
|
||||||
5. [Approaching Unreachable Destinations](#5-approaching-unreachable-destinations)
|
|
||||||
6. [Dynamic Obstacles](#6-dynamic-obstacles)
|
|
||||||
7. [Path Following](#7-path-following)
|
|
||||||
8. [Character Collision Resolution](#8-character-collision-resolution)
|
|
||||||
9. [Dynamic Replanning](#9-dynamic-replanning)
|
|
||||||
10. [Key Constants Reference](#10-key-constants-reference)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Grid Representation
|
|
||||||
|
|
||||||
The world is divided into a uniform 2D grid in the XZ plane (Y is ignored during pathfinding; all characters walk on a flat floor at `floorY`).
|
|
||||||
|
|
||||||
Each cell is either **walkable** (`1`) or **blocked** (`0`). The grid is stored as a flat `std::vector<unsigned char>` indexed by `z * gridWidth + x`.
|
|
||||||
|
|
||||||
**Parameters** (all configurable in the JSON config file):
|
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `cellSize` | 0.4 m | Width and depth of one cell |
|
|
||||||
| `agentRadius` | 0.45 m | Half-width of a character — used to erode free space |
|
|
||||||
| `objectPadding` | 0.25 m | Extra clearance added around obstacle polygons |
|
|
||||||
| `boundaryPadding` | 0.0 m | Inward erosion from the edges of navigation areas |
|
|
||||||
| `floorY` | 0.0 | Y coordinate placed on every path waypoint |
|
|
||||||
|
|
||||||
**Grid bounds** are computed from the union of all navigation area polygons plus a padding margin of `cellSize * 2 + agentRadius + objectPadding` on every side.
|
|
||||||
|
|
||||||
**Cell coordinate conversion:**
|
|
||||||
|
|
||||||
```
|
|
||||||
cell.x = floor((worldX - minX) / cellSize)
|
|
||||||
cell.z = floor((worldZ - minZ) / cellSize)
|
|
||||||
|
|
||||||
cellCenter.x = minX + (cell.x + 0.5) * cellSize
|
|
||||||
cellCenter.z = minZ + (cell.z + 0.5) * cellSize
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Building the Walkable Grid
|
|
||||||
|
|
||||||
The grid can be loaded in two ways.
|
|
||||||
|
|
||||||
### 2a. Pre-computed grid (`.txt` file)
|
|
||||||
|
|
||||||
A plain-text file with a small header followed by rows of `1`/`0` characters:
|
|
||||||
|
|
||||||
```
|
|
||||||
cellSize 0.4
|
|
||||||
agentRadius 0.45
|
|
||||||
floorY 0.0
|
|
||||||
...
|
|
||||||
minX -5.0
|
|
||||||
minZ -5.0
|
|
||||||
gridWidth 50
|
|
||||||
gridDepth 50
|
|
||||||
11111111...
|
|
||||||
10000001...
|
|
||||||
```
|
|
||||||
|
|
||||||
This format is generated by `PathFinder::saveGrid()` after building from polygons and can be loaded much faster than recomputing from geometry.
|
|
||||||
|
|
||||||
### 2b. Polygon-based config (`.json` file)
|
|
||||||
|
|
||||||
The JSON file lists **navigation areas** (convex or concave walkable regions) and **obstacle polygons** (impassable zones within those regions):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"cellSize": 0.4,
|
|
||||||
"areas": [
|
|
||||||
{ "name": "main_room", "available": true, "polygon": [[x,z], ...] }
|
|
||||||
],
|
|
||||||
"obstacles": [
|
|
||||||
{ "name": "table", "polygon": [[x,z], ...] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Build steps:**
|
|
||||||
|
|
||||||
1. **Mark available areas walkable** — every cell whose center lies inside any `available` navigation area polygon gets `walkable = 1`. If `boundaryPadding > 0`, cells too close to the outer edge of the area are left blocked.
|
|
||||||
2. **Mark obstacle polygons blocked** — cells whose center lies inside an obstacle polygon, or within `agentRadius + objectPadding` of its edges, are set to `0`.
|
|
||||||
|
|
||||||
Navigation areas can be toggled at runtime via `PathFinder::setAreaAvailable()`, which rebuilds the entire grid. This is used to open or close doors, gated areas, etc.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. A\* Path Search
|
|
||||||
|
|
||||||
`PathFinder::findPath(start, end)` runs a standard A\* on the walkable grid.
|
|
||||||
|
|
||||||
**Neighbor connectivity:** 8-directional (cardinal + diagonal). Diagonal moves are blocked if either of the two adjacent cardinal cells is unwalkable (no corner-cutting).
|
|
||||||
|
|
||||||
**Step costs:** `1.0` for cardinal, `√2 ≈ 1.414` for diagonal.
|
|
||||||
|
|
||||||
**Heuristic:** Euclidean distance in cell units to the end cell.
|
|
||||||
|
|
||||||
**Start/end snapping:** If the exact cell for `start` or `end` is not walkable, `findNearestWalkableCell` expands a square ring outward (up to radius 8 m) to find the nearest walkable cell. This makes clicking slightly outside the nav mesh still produce a valid path.
|
|
||||||
|
|
||||||
**Path reconstruction:** After A\* completes, the cell chain is walked via `cameFrom[]` from `end` back to `start`, reversed, then smoothed (see §4).
|
|
||||||
|
|
||||||
**First-waypoint trimming:** If the first waypoint is within `cellSize × 0.75` of `start`, it is dropped (the character is already close enough).
|
|
||||||
|
|
||||||
**Last-waypoint precision:** If the requested `end` maps to the same cell as the snapped end cell, the last waypoint is replaced with the exact `end` world position rather than the cell centre.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Path Smoothing
|
|
||||||
|
|
||||||
Raw A\* paths follow the grid diagonals and produce staircase-shaped routes. A **string-pulling** (line-of-sight) pass compresses them:
|
|
||||||
|
|
||||||
```
|
|
||||||
anchor = path[0]
|
|
||||||
result = [anchor]
|
|
||||||
while anchor is not the last cell:
|
|
||||||
find the furthest cell 'next' from anchor with unobstructed line of sight
|
|
||||||
result.append(next)
|
|
||||||
anchor = next
|
|
||||||
```
|
|
||||||
|
|
||||||
Line-of-sight is checked by stepping along the segment in increments of `cellSize / 2` and verifying that each sampled cell is walkable. The result is a minimal set of waypoints connected by straight, obstacle-free segments.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Approaching Unreachable Destinations
|
|
||||||
|
|
||||||
When a player clicks on a point in a disconnected region (e.g., across a thin wall), the original `findPath` returns an empty path and the character does not move. This is surprising — a click on a solid wall sensibly moves the character to the nearest reachable point, but a click into an inaccessible room does nothing.
|
|
||||||
|
|
||||||
**`findPathToNearest`** fixes this with a three-step cascade:
|
|
||||||
|
|
||||||
1. Try `findPath` with dynamic obstacles (stationary characters are avoided).
|
|
||||||
2. If empty, retry `findPath` without dynamic obstacles (an NPC blocking a doorway is ignored).
|
|
||||||
3. If still empty (destination genuinely unreachable), run **nearest-reachable A\***.
|
|
||||||
|
|
||||||
**Nearest-reachable A\***, implemented in `findNearestReachableImpl`:
|
|
||||||
|
|
||||||
- Runs the identical A\* loop against the static walkable grid.
|
|
||||||
- While processing cells, tracks `bestIndex` — the already-visited cell with the smallest Euclidean distance (in cell units) to the end cell.
|
|
||||||
- If A\* exhausts all reachable space without finding `end`, it reconstructs and returns a path to `bestIndex`.
|
|
||||||
- If `bestIndex` is still the start cell (character is completely isolated), an empty path is returned and the character stays put.
|
|
||||||
|
|
||||||
The net effect: clicking anywhere in the world always moves the character as close as possible to the target, matching the behaviour of clicking on a solid wall.
|
|
||||||
|
|
||||||
`findPathToNearest` replaces the direct `findPath` call in `Location::setupNavigation`'s path planner lambda, so it applies equally to the player and all NPCs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Dynamic Obstacles
|
|
||||||
|
|
||||||
When a path is planned, other characters can temporarily mark cells as blocked to make the character walk around them rather than through them.
|
|
||||||
|
|
||||||
**How it works:**
|
|
||||||
|
|
||||||
In `Location::setupNavigation`, every character is given a `PathPlanner` closure. Before calling `findPath`, the closure builds a list of `PathFinder::DynamicObstacle` entries (position + radius) representing nearby characters. `findPath` copies the static walkable grid, stamps zeros in circles around each obstacle, then runs A\* on the modified copy. The static grid is never mutated.
|
|
||||||
|
|
||||||
**Which characters become obstacles:**
|
|
||||||
|
|
||||||
A character is added as a dynamic obstacle only when **all** of these are true:
|
|
||||||
|
|
||||||
- It is not the character currently planning the path (`self`).
|
|
||||||
- It is alive and enabled.
|
|
||||||
- **It is not moving** — a moving character is transparent to pathfinding, so it does not block narrow corridors that it is actively passing through.
|
|
||||||
- Its position lies within `kDynamicObstacleInfluenceDist = 6 m` of the direct line segment from `start` to `end` (distant characters do not affect the search).
|
|
||||||
|
|
||||||
**Obstacle radius:** `character.collisionRadius × 0.6`. Using 60 % of the physical collision radius makes path planning less conservative; physical separation at full radius is still enforced by collision resolution (§8).
|
|
||||||
|
|
||||||
**Fallback when dynamic obstacles block the only path:**
|
|
||||||
|
|
||||||
If step 1 of `findPathToNearest` (with dynamic obstacles) returns empty, step 2 retries without any dynamic obstacles. This handles the common case of an NPC standing in a doorway: the player paths through the NPC's position, and the nudge logic (§8) pushes the NPC aside as the player passes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Path Following
|
|
||||||
|
|
||||||
`Character::setTarget(destination, onArrived)` sets a new walk target. It calls the path planner to generate a waypoint list. The result is stored in `pathWaypoints`; the final destination is also stored in `walkTarget` and `requestedWalkTarget`.
|
|
||||||
|
|
||||||
Each frame in `Character::update`:
|
|
||||||
|
|
||||||
1. **Active target** — if `pathWaypoints` is non-empty, the character moves toward `pathWaypoints[currentWaypointIndex]`; otherwise it moves toward `walkTarget`.
|
|
||||||
2. **Movement** — the character advances along the XZ direction at `walkSpeed` m/s and rotates smoothly toward the movement direction at `rotationSpeed` rad/s.
|
|
||||||
3. **Waypoint advance** — when the character is within `WALK_THRESHOLD = 0.05 m` of the current waypoint, it advances to the next one. When the last waypoint is reached, `pathWaypoints` is cleared and the optional `onArrived` callback is fired.
|
|
||||||
4. **State machine** — the animation state switches between `STAND` and `WALK` based on whether the character is moving.
|
|
||||||
|
|
||||||
`Character::isMoving()` returns `true` if `pathWaypoints` is non-empty or the distance to `walkTarget` exceeds `WALK_THRESHOLD`. This is used by dynamic obstacle filtering and collision nudging.
|
|
||||||
|
|
||||||
**Stopping in place:** `Character::stopInPlace()` sets `walkTarget` and `requestedWalkTarget` to the current position and clears `pathWaypoints`. It is called when an external force (collision resolution) displaces a stationary player so that the player does not walk back to their previous target position.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Character Collision Resolution
|
|
||||||
|
|
||||||
Pathfinding alone does not prevent two characters from occupying the same space — it only steers paths around stationary characters. Physical separation is handled separately each frame by `Location::resolveCharacterCollisions`.
|
|
||||||
|
|
||||||
**Algorithm** (3 iterations per frame):
|
|
||||||
|
|
||||||
For every pair `(A, B)` of living, enabled characters:
|
|
||||||
|
|
||||||
1. Compute the overlap: `penetration = (collisionRadius_A + collisionRadius_B) - distance(A, B)`.
|
|
||||||
2. If `penetration > 0`, compute a push direction (A-to-B normal) and a push magnitude of `penetration / 2` per character.
|
|
||||||
3. Compute candidate new positions `newA` and `newB`.
|
|
||||||
4. Validate against the navigation grid (`PathFinder::isWalkable`). If a pushed position is unwalkable, only the other character is moved.
|
|
||||||
5. **Player stays put:** if the player was not moving (`!isMoving()`) before the push, `stopInPlace()` is called after the push so the player does not walk back to the old target.
|
|
||||||
6. **NPC yielding:** if one character was moving and the other was standing, `nudgeCharacterAside` is called on the standing character.
|
|
||||||
|
|
||||||
**`nudgeCharacterAside(standing, awayFrom)`:**
|
|
||||||
|
|
||||||
Gives the standing NPC a short walk target so it steps out of the way:
|
|
||||||
|
|
||||||
1. Compute the direction from `awayFrom` to the NPC's current position.
|
|
||||||
2. Try four candidate targets at distance `1.2 m` in directions: straight away, +90°, −90°, 180°.
|
|
||||||
3. Use the first candidate that is walkable (per `PathFinder::isWalkable`).
|
|
||||||
4. Call `standing->setTarget(candidate)` — the NPC takes a small step aside, then stands at the new spot.
|
|
||||||
5. The player is never nudged; combat NPCs can be nudged, but their attack AI immediately overrides the yield target on the next tick.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Dynamic Replanning
|
|
||||||
|
|
||||||
When characters move they can displace each other or enter each other's planned paths. `Location::updateDynamicReplans` handles this:
|
|
||||||
|
|
||||||
**Every frame:**
|
|
||||||
|
|
||||||
1. Measure how much each character moved since the last frame. Characters that moved more than `kMovedEps = 0.05 m` are collected as **movers**.
|
|
||||||
2. For each mover, find other characters that are currently walking. If the mover's position is within `kReplanTriggerDist = 1.8 m` of the segment `[walker.position → walker.nextWaypoint]`, trigger a replan for the walker via `forceReplan()`.
|
|
||||||
3. A per-character cooldown of `kReplanCooldownMs = 500 ms` prevents the same character from replanning more often than twice per second.
|
|
||||||
|
|
||||||
**`Character::forceReplan()`** re-runs the path planner from the character's current position to its stored `requestedWalkTarget`, updating `pathWaypoints` in place. If the replanned path is empty, the character stops at its current position.
|
|
||||||
|
|
||||||
The relatively generous trigger distance (1.8 m vs the old 1.1 m) and cooldown (500 ms vs 300 ms) prevent micro-jitter: small position corrections from collision resolution no longer spam replanning events.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Key Constants Reference
|
|
||||||
|
|
||||||
| Constant | Location | Value | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `cellSize` | `PathFinder` config | 0.4 m | Grid cell size |
|
|
||||||
| `agentRadius` | `PathFinder` config | 0.45 m | Character half-width for grid erosion |
|
|
||||||
| `objectPadding` | `PathFinder` config | 0.25 m | Extra clearance around obstacles |
|
|
||||||
| `WALK_THRESHOLD` | `Character.h` | 0.05 m | Distance below which a waypoint is considered reached |
|
|
||||||
| `TARGET_REPLAN_THRESHOLD` | `Character.h` | 0.25 m | Deduplication threshold in `setTarget` |
|
|
||||||
| `kDynamicObstacleInfluenceDist` | `Location.cpp` | 6.0 m | Max distance from path for a character to become an obstacle |
|
|
||||||
| `kDynamicObstacleRadiusFraction` | `Location.cpp` | 0.6× | Fraction of collision radius used for dynamic obstacle footprint |
|
|
||||||
| `kNudgeDist` | `Location.cpp` | 1.2 m | Distance an NPC steps aside when yielding |
|
|
||||||
| `kReplanTriggerDist` | `Location.cpp` | 1.8 m | Mover must be this close to a walker's path to trigger replan |
|
|
||||||
| `kReplanCooldownMs` | `Location.cpp` | 500 ms | Minimum interval between replans for any one character |
|
|
||||||
| `NPC_TALK_DISTANCE` | `Location.cpp` | 1.35 m | Distance at which walking-to-NPC interaction fires |
|
|
||||||
| `kIterations` (collision) | `Location.cpp` | 3 | Push-apart iterations per frame |
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
### Перед запуском в папке ```app/jni/```(рядом с src) нужно создать три папки с исходниками библиотек ```libpng```, ```SDL```, ```zlib```
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
# Windows
|
|
||||||
|
|
||||||
download from https://cmake.org/download/
|
|
||||||
|
|
||||||
|
|
||||||
Windows x64 Installer: cmake-4.2.0-windows-x86_64.msi
|
|
||||||
|
|
||||||
|
|
||||||
download from https://github.com/libsdl-org/SDL/releases/tag/release-2.32.10
|
|
||||||
|
|
||||||
SDL2-2.32.10-win32-x64.zip
|
|
||||||
|
|
||||||
SDL2-2.32.10:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd C:\..\SDL-realese-2.32.10
|
|
||||||
mkdir build
|
|
||||||
cd build
|
|
||||||
cmake -G "Visual Studio 18 2026" -DCMAKE_INSTALL_PREFIX=install ..
|
|
||||||
cmake --build . --config Debug
|
|
||||||
cmake --install . --config Debug
|
|
||||||
```
|
|
||||||
|
|
||||||
download from https://www.zlib.net/
|
|
||||||
|
|
||||||
zlib source code, version 1.3.1, zipfile format (1616K, SHA-256 hash 72af66d44fcc14c22013b46b814d5d2514673dda3d115e64b690c1ad636e7b17):
|
|
||||||
US (zlib.net)
|
|
||||||
|
|
||||||
zlib-1.3.1:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd C:\..\zlib-1.3.1
|
|
||||||
mkdir build
|
|
||||||
cd build
|
|
||||||
cmake -G "Visual Studio 18 2026" -DCMAKE_INSTALL_PREFIX=install ..
|
|
||||||
cmake --build . --config Debug
|
|
||||||
cmake --install . --config Debug
|
|
||||||
```
|
|
||||||
|
|
||||||
download from https://github.com/pnggroup/libpng/releases/tag/v1.6.51
|
|
||||||
|
|
||||||
Source code (zip)
|
|
||||||
|
|
||||||
libpng-1.6.51:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd C:\..\libpng-1.6.51
|
|
||||||
mkdir build
|
|
||||||
cd build
|
|
||||||
```
|
|
||||||
|
|
||||||
To build libpng, you need to specify the path to the zlib installation directory as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
cmake -DCMAKE_PREFIX_PATH="../zlib-1.3.1/build/install" -DCMAKE_INSTALL_PREFIX=install -G "Visual Studio 18 2026" ..
|
|
||||||
cmake --build . --config Debug
|
|
||||||
cmake --install . --config Debug
|
|
||||||
```
|
|
||||||
|
|
||||||
Настройка проекта в Visual Studio:
|
|
||||||
|
|
||||||
Перейдите в Project Properties (правый клик на проект, "Properties").
|
|
||||||
|
|
||||||
C/C++ - ОБЩИЕ; Дополнительные каталоги включаемых файлов, проверить чтобы был добавлен путь к папке include:(пример)
|
|
||||||
|
|
||||||
..\SDL-release-2.32.10\include;..\libpng-1.6.51\build\install\include;C:\Work\OpenAL 1.1 SDK\include;..\Projects\libogg\include;..\vorbis\include
|
|
||||||
|
|
||||||
|
|
||||||
Компоновщик - ОБЩИЕ; Доподнительные каталоги библиотек (пример)
|
|
||||||
|
|
||||||
..\SDL-release-2.32.10\build\install\lib;..\libpng-1.6.51\build\install\lib;..\zlib-1.3.1\build\install\lib
|
|
||||||
|
|
||||||
Компоновщик - ВВОД; Дополнительные зависимости, добавить zlibstaticd.lib (пример)
|
|
||||||
|
|
||||||
zlibstaticd.lib;libpng16_staticd.lib;SDL2d.lib;SDL2maind.lib;opengl32.lib;glu32.lib;shell32.lib;opengl32.lib;glu32.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib
|
|
||||||
|
|
||||||
|
|
||||||
в папку ..\projectGAME01\x64\Debug добавить файл SDL2d.dll
|
|
||||||
который можно скопировать из папки ..\SDL-release-2.32.10\build\Debug
|
|
||||||
|
|
||||||
|
|
||||||
# Script to run:
|
|
||||||
|
|
||||||
```
|
|
||||||
C:\Work\Projects\emsdk\emsdk.bat activate latest
|
|
||||||
C:\Work\Projects\emsdk\emsdk_env.bat
|
|
||||||
emcc main.cpp Game.cpp Math.cpp Physics.cpp Renderer.cpp ShaderManager.cpp TextureManager.cpp Utils.cpp OpenGlExtensions.cpp -O2 -std=c++14 -sTOTAL_MEMORY=33554432 -sUSE_SDL_IMAGE=2 -sSDL2_IMAGE_FORMATS="[""png""]" -sUSE_SDL=2 --preload-file background.bmp --preload-file bird.bmp32 --preload-file default.fragment --preload-file default.vertex --preload-file game_over.bmp32 --preload-file pipe.bmp32 -o space-game001.html
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
zlib-1.3.1:
|
|
||||||
|
|
||||||
mkdir build
|
|
||||||
cd build
|
|
||||||
cmake -DCMAKE_INSTALL_PREFIX=install ..
|
|
||||||
|
|
||||||
then run ALL_BUILD and INSTALL in Visual Studio
|
|
||||||
|
|
||||||
lpng1645:
|
|
||||||
|
|
||||||
mkdir build
|
|
||||||
cd build
|
|
||||||
cmake -DCMAKE_INSTALL_PREFIX=install -DZLIB_ROOT=C:\Work\Projects\zlib-1.3.1\build\install ..
|
|
||||||
|
|
||||||
then run ALL_BUILD and INSTALL in Visual Studio
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
https://github.com/Bly7/OBJ-Loader/blob/master/Source/OBJ_Loader.h
|
|
||||||
|
|
||||||
|
|
||||||
https://github.com/gametutorials/tutorials/blob/master/OpenGL/MD3%20Animation/Main.cpp
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
linux:
|
|
||||||
```
|
|
||||||
g++ Game.cpp main.cpp Math.cpp OpenGlExtensions.cpp Physics.cpp Renderer.cpp ShaderManager.cpp TextureManager.cpp Utils.cpp BoneAnimatedModel.cpp ObjLoader.cpp cmakeaudioplayer/src/AudioPlayer.cpp TextModel.cpp Inventory.cpp -o sdl_app -O2 -std=c++17 \
|
|
||||||
-I cmakeaudioplayer/include \
|
|
||||||
$(pkg-config --cflags --libs sdl2 gl) \
|
|
||||||
$(pkg-config --cflags --libs vorbis vorbisfile ogg) \
|
|
||||||
-lopenal
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Linux new:
|
|
||||||
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install build-essential cmake pkg-config \
|
|
||||||
libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev \
|
|
||||||
libgl1-mesa-dev libpng-dev libz-dev libzip-dev \
|
|
||||||
libboost-dev libeigen3-dev liblua5.4-dev
|
|
||||||
|
|
||||||
sudo apt-get install libglu1-mesa-dev
|
|
||||||
|
|
||||||
|
|
||||||
# Emscripten new
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
cd build-emcmake/
|
|
||||||
|
|
||||||
emcmake cmake -DCMAKE_INSTALL_PREFIX=install ..
|
|
||||||
cmake --build .
|
|
||||||
|
|
||||||
cmake --install .
|
|
||||||
|
|
||||||
|
|
||||||
emcc main.cpp Game.cpp Environment.cpp GameObjectManager.cpp BoneAnimatedModel.cpp GameWorld.cpp InputManager.cpp Inventory.cpp ObjLoader.cpp QuestScripts.cpp RenderSystem.cpp Math.cpp Physics.cpp Renderer.cpp TextModel.cpp ShaderManager.cpp TextureManager.cpp Utils.cpp OpenGlExtensions.cpp -O2 -std=c++14 -IC:\Users\ASUS\Desktop\fishrungame2\ZeptoLabTest1\thirdparty\libzip-1.11.3\build-emcmake\install\include -LC:\Users\ASUS\Desktop\fishrungame2\ZeptoLabTest1\thirdparty\libzip-1.11.3\build-emcmake\install\lib -lzip -sTOTAL_MEMORY=33554432 -sUSE_SDL_IMAGE=2 -sSDL2_IMAGE_FORMATS="[""png""]" -sUSE_SDL=2 --preload-file background.bmp --preload-file bird.bmp32 --preload-file default.fragment --preload-file default.vertex --preload-file game_over.bmp32 --preload-file pipe.bmp32 -o jumpingbird.html
|
|
||||||
|
|
||||||
emcc main.cpp Game.cpp Environment.cpp GameObjectManager.cpp BoneAnimatedModel.cpp GameWorld.cpp InputManager.cpp Inventory.cpp ObjLoader.cpp QuestScripts.cpp RenderSystem.cpp Math.cpp Physics.cpp Renderer.cpp TextModel.cpp ShaderManager.cpp TextureManager.cpp Utils.cpp OpenGlExtensions.cpp -O2 -std=c++14 -pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4 -sTOTAL_MEMORY=4294967296 -sINITIAL_MEMORY=3221225472 -sMAXIMUM_MEMORY=4294967296 -sALLOW_MEMORY_GROWTH=1 -I./thirdparty/libzip-1.11.3/build-emcmake/install/include -I./thirdparty/zlib-1.3.1/install/include -L./thirdparty/libzip-1.11.3/build-emcmake/install/lib -L./thirdparty/zlib-1.3.1/install/lib -lzip -lz -sUSE_SDL_IMAGE=2 -sSDL2_IMAGE_FORMATS='["png"]' -sUSE_SDL=2 --preload-file data.zip -o jumpingbird.html
|
|
||||||
|
|
||||||
|
|
||||||
# Windows:
|
|
||||||
emcc --clear-cache
|
|
||||||
embuilder build sdl2 sdl2_ttf sdl2_image sdl2_image_jpg sdl2_image_png
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
emcc main.cpp Game.cpp Environment.cpp BoneAnimatedModel.cpp ZLMath.cpp Renderer.cpp TextModel.cpp ShaderManager.cpp TextureManager.cpp Utils.cpp OpenGlExtensions.cpp -O2 -std=c++14 -pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4 -sTOTAL_MEMORY=4294967296 -sINITIAL_MEMORY=3221225472 -sMAXIMUM_MEMORY=4294967296 -sALLOW_MEMORY_GROWTH=1 -I./thirdparty/libzip-1.11.3/build-emcmake/install/include -L./thirdparty/libzip-1.11.3/build-emcmake/install/lib -lzip -lz -sUSE_SDL_IMAGE=2 -sUSE_SDL=2 -sUSE_LIBPNG=1 --preload-file space-game001.zip -o space-game001.html
|
|
||||||
|
|
||||||
emrun --no_browser --port 8080 .
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
# Emscripten new
|
|
||||||
```
|
|
||||||
emcc src/main.cpp src/Game.cpp src/Environment.cpp src/BoneAnimatedModel.cpp src/TextModel.cpp src/Projectile.cpp src/SparkEmitter.cpp src/UiManager.cpp src/render/Renderer.cpp src/render/ShaderManager.cpp src/render/TextureManager.cpp src/render/FrameBuffer.cpp src/render/OpenGlExtensions.cpp src/utils/Utils.cpp src/utils/TaskManager.cpp src/utils/Perlin.cpp src/planet/PlanetData.cpp src/planet/PlanetObject.cpp src/planet/StoneObject.cpp -O2 -std=c++17 -pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4 -sTOTAL_MEMORY=4294967296 -sINITIAL_MEMORY=3221225472 -sMAXIMUM_MEMORY=4294967296 -sALLOW_MEMORY_GROWTH=1 -fexceptions -I./thirdparty1/eigen-5.0.0 -I./src -I./thirdparty/libzip-1.11.3/build-emcmake/install/include -IC:/Boost/include/boost-1_84 -L./thirdparty/libzip-1.11.3/build-emcmake/install/lib -lzip -lz -sUSE_SDL_IMAGE=2 -sUSE_SDL=2 -sUSE_LIBPNG=1 --preload-file space-game001.zip -o space-game001.html
|
|
||||||
```
|
|
||||||
|
|
||||||
# License
|
|
||||||
Code: MIT
|
|
||||||
|
|
||||||
Art: CC-BY
|
|
||||||
|
|
||||||
# Cmake Run Linux
|
|
||||||
Run using cmakelist
|
|
||||||
make -j$(nproc) -C build #Компилируем
|
|
||||||
./build/sdl_app #Запускаем
|
|
||||||
|
|
||||||
Для постройки без звука
|
|
||||||
rm -rf build #Очищаем build папку
|
|
||||||
cmake -B build -DAUDIO=1 #Пересоздаём конфигурацию CMake
|
|
||||||
|
|
||||||
|
|
||||||
# Cmake Build NSIS and Portable for Windows:
|
|
||||||
|
|
||||||
```
|
|
||||||
cmake --build . --config Release
|
|
||||||
cpack -C Release
|
|
||||||
```
|
|
||||||
|
|
||||||
Если есть такая ошибка:
|
|
||||||
|
|
||||||
CPack Error: Cannot find NSIS compiler makensis: likely it is not installed, or not in your PATH
|
|
||||||
CPack Error: Could not read NSIS registry value. This is usually caused by NSIS not being installed. Please install NSIS from http://nsis.sourceforge.net
|
|
||||||
CPack Error: Cannot initialize the generator NSIS
|
|
||||||
|
|
||||||
|
|
||||||
То нужно установить nsis отсюда: https://nsis.sourceforge.io/Download
|
|
||||||
|
|
||||||
|
|
||||||
# Steam windows
|
|
||||||
|
|
||||||
```
|
|
||||||
cmake -DSTEAMSDK=ON ..
|
|
||||||
cmake --build . --config Release
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
# Steam Linux
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run -it --rm -v "${PWD}:/work2" -w /work2 registry.gitlab.steamos.cloud/steamrt/sniper/sdk:latest bash
|
|
||||||
|
|
||||||
|
|
||||||
apt-get update
|
|
||||||
apt-get install libboost-dev libeigen3-dev liblua5.4-dev libzip-dev libglu1-mesa-dev
|
|
||||||
|
|
||||||
|
|
||||||
cmake -DSTEAMSDK=ON -DCMAKE_BUILD_TYPE=Release ..
|
|
||||||
cmake --build . -j 4
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -1,527 +0,0 @@
|
|||||||
# UI System
|
|
||||||
|
|
||||||
UI layouts are defined in JSON files and loaded at runtime by `UiManager`. Each file has a single `"root"` node that is the top-level container.
|
|
||||||
|
|
||||||
The coordinate system has the origin at the **bottom-left** of the screen. Y increases upward.
|
|
||||||
The virtual canvas size is defined by `Environment::projectionWidth` × `Environment::projectionHeight`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"root": { ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Node Properties
|
|
||||||
|
|
||||||
These properties are available on every node type.
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `name` | string | `""` | Unique name used to find the node from C++ code |
|
|
||||||
| `x` | float | `0` | Horizontal offset from the parent's origin (or gravity-adjusted position) |
|
|
||||||
| `y` | float | `0` | Vertical offset |
|
|
||||||
| `width` | float \| `"match_parent"` | `0` | Width in virtual pixels. `"match_parent"` fills the parent |
|
|
||||||
| `height` | float \| `"match_parent"` | `0` | Height in virtual pixels |
|
|
||||||
| `horizontal_gravity` | `"left"` \| `"center"` \| `"right"` | `"left"` | Positions the node horizontally inside a **FrameLayout** parent |
|
|
||||||
| `vertical_gravity` | `"bottom"` \| `"center"` \| `"top"` | `"bottom"` | Positions the node vertically inside a **FrameLayout** parent |
|
|
||||||
| `visible` | bool | `true` | Whether the node (and all its children) are rendered and interactive. Can be toggled at runtime via `setNodeVisible` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Containers
|
|
||||||
|
|
||||||
### FrameLayout
|
|
||||||
|
|
||||||
Children are positioned using absolute `x`/`y` offsets and/or `horizontal_gravity` / `vertical_gravity`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "FrameLayout",
|
|
||||||
"name": "hud_root",
|
|
||||||
"width": "match_parent",
|
|
||||||
"height": "match_parent",
|
|
||||||
"children": [ ... ]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `children` | array | `[]` | Child nodes |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### LinearLayout
|
|
||||||
|
|
||||||
Children are stacked automatically in a row or column. Gravity and align properties control the layout of the block and its children.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "LinearLayout",
|
|
||||||
"orientation": "vertical",
|
|
||||||
"vertical_align": "center",
|
|
||||||
"horizontal_align": "center",
|
|
||||||
"spacing": 10,
|
|
||||||
"width": 400,
|
|
||||||
"height": 600,
|
|
||||||
"children": [ ... ]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `orientation` | `"vertical"` \| `"horizontal"` | `"vertical"` | Direction children are stacked |
|
|
||||||
| `spacing` | float | `0` | Gap in pixels between consecutive children |
|
|
||||||
| `vertical_align` | `"top"` \| `"center"` \| `"bottom"` | `"top"` | **Vertical** alignment of the child block inside this layout. For vertical orientation, controls how the whole stack is aligned; for horizontal orientation, controls each child's cross-axis alignment |
|
|
||||||
| `horizontal_align` | `"left"` \| `"center"` \| `"right"` | `"left"` | **Horizontal** alignment of the child block. For horizontal orientation, controls how the whole row is aligned; for vertical orientation, controls each child's cross-axis alignment |
|
|
||||||
| `children` | array | `[]` | Child nodes, laid out in order |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Widgets
|
|
||||||
|
|
||||||
### Button
|
|
||||||
|
|
||||||
An image-only clickable button. Swaps textures on hover/press.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "Button",
|
|
||||||
"name": "closeButton",
|
|
||||||
"width": 90,
|
|
||||||
"height": 90,
|
|
||||||
"x": 580,
|
|
||||||
"y": 240,
|
|
||||||
"horizontal_gravity": "center",
|
|
||||||
"vertical_gravity": "center",
|
|
||||||
"textures": {
|
|
||||||
"normal": "resources/w/ui/img/Close001_State=Default.png",
|
|
||||||
"hover": "resources/w/ui/img/Close001_State=Selected.png",
|
|
||||||
"pressed": "resources/w/ui/img/Close001_State=Tap.png",
|
|
||||||
"disabled": "resources/w/ui/img/Close001_State=Disabled.png"
|
|
||||||
},
|
|
||||||
"border": 4,
|
|
||||||
"clickZoneWidth": 80,
|
|
||||||
"clickZoneHeight": 80
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `textures.normal` | string | — | Texture path shown in the default state (**required**) |
|
|
||||||
| `textures.hover` | string | — | Texture path shown when the mouse hovers |
|
|
||||||
| `textures.pressed` | string | — | Texture path shown while pressed |
|
|
||||||
| `textures.disabled` | string | — | Texture path shown when the button is disabled |
|
|
||||||
| `border` | float | `0` | Inset (pixels) applied to the hit-test zone on all sides |
|
|
||||||
| `clickZoneWidth` | float | `0` | Explicit hit-test width; `0` uses the widget width |
|
|
||||||
| `clickZoneHeight` | float | `0` | Explicit hit-test height; `0` uses the widget height |
|
|
||||||
|
|
||||||
**C++ callbacks:**
|
|
||||||
```cpp
|
|
||||||
uiManager.setButtonCallback("closeButton", [](const std::string&) { /* click */ });
|
|
||||||
uiManager.setButtonPressCallback("closeButton", [](const std::string&) { /* press */ });
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### TextButton
|
|
||||||
|
|
||||||
A button that renders a text label on top of an optional background texture.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "item1name",
|
|
||||||
"width": 270,
|
|
||||||
"height": 60,
|
|
||||||
"text": "Main Quest",
|
|
||||||
"fontSize": 32,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"topAligned": false,
|
|
||||||
"textPaddingX": 12,
|
|
||||||
"textPaddingY": -8,
|
|
||||||
"wrap": true,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": {
|
|
||||||
"normal": "resources/w/red.png",
|
|
||||||
"hover": "resources/w/red.png",
|
|
||||||
"pressed": "resources/w/red.png"
|
|
||||||
},
|
|
||||||
"border": 0,
|
|
||||||
"clickZoneWidth": 0,
|
|
||||||
"clickZoneHeight": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `text` | string | `""` | Label text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
|
|
||||||
| `fontSize` | int | `32` | Font size in pixels |
|
|
||||||
| `fontPath` | string | `"resources/fonts/DroidSans.ttf"` | Path to the TTF font file |
|
|
||||||
| `textCentered` | bool | `true` | Horizontally centers the text within the widget. When `false`, text starts at `textPaddingX` from the left edge |
|
|
||||||
| `topAligned` | bool | `false` | When `true`, the first text line is placed near the top of the widget; when `false`, the text is vertically centered |
|
|
||||||
| `textPaddingX` | float | `12` | Left padding when `textCentered` is `false`; also used to compute the wrapping width |
|
|
||||||
| `textPaddingY` | float | `0` | Vertical offset applied to the text baseline |
|
|
||||||
| `wrap` | bool | `false` | Wraps text that exceeds `width - textPaddingX * 2` pixels |
|
|
||||||
| `color` | [R, G, B, A] | `[1,1,1,1]` | Text color, each channel 0..1 |
|
|
||||||
| `textures.*` | string | — | Background textures (all optional — button can be text-only) |
|
|
||||||
| `border` | float | `0` | Hit-test inset |
|
|
||||||
| `clickZoneWidth` / `clickZoneHeight` | float | `0` | Explicit hit-test size; `0` uses the widget size |
|
|
||||||
|
|
||||||
**C++ callbacks:**
|
|
||||||
```cpp
|
|
||||||
uiManager.setTextButtonCallback("item1name", [](const std::string&) { /* click */ });
|
|
||||||
uiManager.setTextButtonPressCallback("item1name", [](const std::string&) { /* press */ });
|
|
||||||
|
|
||||||
// Programmatic updates
|
|
||||||
uiManager.setTextButtonText("item1name", "New Quest Name");
|
|
||||||
uiManager.setTextButtonColor("item1name", {1.f, 0.f, 0.f, 1.f});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### TextView
|
|
||||||
|
|
||||||
A non-interactive text display widget.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "TextView",
|
|
||||||
"name": "quest_description",
|
|
||||||
"x": 170,
|
|
||||||
"y": 390,
|
|
||||||
"width": 1000,
|
|
||||||
"height": 300,
|
|
||||||
"text": "Long description here.",
|
|
||||||
"fontSize": 32,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"topAligned": true,
|
|
||||||
"wrap": true,
|
|
||||||
"paddingX": 0,
|
|
||||||
"paddingY": 4,
|
|
||||||
"maxLines": 10,
|
|
||||||
"color": [1.0, 1.0, 0.0, 1.0]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `text` | string | `""` | Display text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
|
|
||||||
| `fontSize` | int | `32` | Font size in pixels |
|
|
||||||
| `fontPath` | string | `"resources/fonts/DroidSans.ttf"` | Path to the TTF font file |
|
|
||||||
| `textCentered` | bool | `true` | Horizontally centers the text when `true`; left-aligns from `paddingX` when `false` |
|
|
||||||
| `topAligned` | bool | `false` | When `true`, the first line is placed near the top edge; when `false`, text is vertically centered |
|
|
||||||
| `wrap` | bool | `false` | Wraps text at `width - paddingX * 2` pixels |
|
|
||||||
| `paddingX` | float | `0` | Left/right padding used for alignment and wrap width |
|
|
||||||
| `paddingY` | float | `0` | Vertical inset applied when `topAligned` is `true` |
|
|
||||||
| `maxLines` | int | `0` | Maximum number of lines to display; `0` means unlimited. Truncated text gets `...` |
|
|
||||||
| `color` | [R, G, B, A] | `[1,1,1,1]` | Text color |
|
|
||||||
|
|
||||||
> **Legacy note:** If none of `wrap`, `topAligned`, `paddingX`, `paddingY`, or `maxLines` are set, the text is drawn centered on `(x + width/2, y + height/2)` for backward compatibility.
|
|
||||||
|
|
||||||
**C++ updates:**
|
|
||||||
```cpp
|
|
||||||
uiManager.setText("quest_description", "New text here.");
|
|
||||||
uiManager.setTextColor("quest_description", {1.f, 1.f, 0.f, 1.f});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### TextField
|
|
||||||
|
|
||||||
An interactive single-line text input field. Receives keyboard input when focused.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "TextField",
|
|
||||||
"name": "playerName",
|
|
||||||
"x": 100,
|
|
||||||
"y": 300,
|
|
||||||
"width": 400,
|
|
||||||
"height": 50,
|
|
||||||
"placeholder": "Enter name...",
|
|
||||||
"fontSize": 28,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"maxLength": 64,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"placeholderColor": [0.5, 0.5, 0.5, 1.0],
|
|
||||||
"backgroundColor": [0.2, 0.2, 0.2, 1.0],
|
|
||||||
"borderColor": [0.5, 0.5, 0.5, 1.0]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `placeholder` | string | `""` | Text shown when the field is empty |
|
|
||||||
| `fontSize` | int | `32` | Font size in pixels |
|
|
||||||
| `fontPath` | string | `"resources/fonts/DroidSans.ttf"` | Path to the TTF font file |
|
|
||||||
| `maxLength` | int | `256` | Maximum number of characters |
|
|
||||||
| `color` | [R, G, B, A] | `[1,1,1,1]` | Input text color |
|
|
||||||
| `placeholderColor` | [R, G, B, A] | `[0.5,0.5,0.5,1]` | Placeholder text color |
|
|
||||||
| `backgroundColor` | [R, G, B, A] | `[0.2,0.2,0.2,1]` | Field background color |
|
|
||||||
| `borderColor` | [R, G, B, A] | `[0.5,0.5,0.5,1]` | Border color |
|
|
||||||
|
|
||||||
**C++ callbacks and queries:**
|
|
||||||
```cpp
|
|
||||||
uiManager.setTextFieldCallback("playerName", [](const std::string& name, const std::string& value) {
|
|
||||||
// called on every keystroke
|
|
||||||
});
|
|
||||||
std::string current = uiManager.getTextFieldValue("playerName");
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Slider
|
|
||||||
|
|
||||||
A draggable slider that returns a normalized value in the range `[0, 1]`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "Slider",
|
|
||||||
"name": "volumeSlider",
|
|
||||||
"x": 100,
|
|
||||||
"y": 200,
|
|
||||||
"width": 40,
|
|
||||||
"height": 300,
|
|
||||||
"orientation": "vertical",
|
|
||||||
"value": 0.75,
|
|
||||||
"textures": {
|
|
||||||
"track": "resources/w/ui/img/slider_track.png",
|
|
||||||
"knob": "resources/w/ui/img/slider_knob.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `textures.track` | string | — | Texture for the slider track |
|
|
||||||
| `textures.knob` | string | — | Texture for the draggable knob |
|
|
||||||
| `orientation` | `"vertical"` \| `"horizontal"` | `"vertical"` | Drag direction |
|
|
||||||
| `value` | float | `0` | Initial normalized value `[0, 1]` |
|
|
||||||
|
|
||||||
**C++ callbacks:**
|
|
||||||
```cpp
|
|
||||||
uiManager.setSliderCallback("volumeSlider", [](const std::string& name, float value) {
|
|
||||||
// value is 0..1
|
|
||||||
});
|
|
||||||
uiManager.setSliderValue("volumeSlider", 0.5f);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### StaticImage
|
|
||||||
|
|
||||||
A non-interactive image. Supports optional fade-in and pulse-scale animations.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "StaticImage",
|
|
||||||
"name": "background",
|
|
||||||
"width": 1266,
|
|
||||||
"height": 585,
|
|
||||||
"horizontal_gravity": "center",
|
|
||||||
"vertical_gravity": "center",
|
|
||||||
"texture": "resources/w/ui/img/journal/QuestJournal003.png",
|
|
||||||
"fadeIn": {
|
|
||||||
"durationMs": 600
|
|
||||||
},
|
|
||||||
"pulse": {
|
|
||||||
"minScale": 0.92,
|
|
||||||
"maxScale": 1.08,
|
|
||||||
"periodMs": 1500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `texture` | string | — | Path to the PNG texture |
|
|
||||||
| `fadeIn.durationMs` | float | — | If present, the image fades in over this many milliseconds each time the UI is shown |
|
|
||||||
| `pulse.minScale` | float | `0.9` | Minimum scale during the pulse cycle |
|
|
||||||
| `pulse.maxScale` | float | `1.1` | Maximum scale during the pulse cycle |
|
|
||||||
| `pulse.periodMs` | float | `1000` | Duration of one full pulse cycle in milliseconds |
|
|
||||||
|
|
||||||
**C++ pop-in animation** (scales the node from 0 → 1, ease-out quad):
|
|
||||||
```cpp
|
|
||||||
uiManager.startPopIn("background", 300.0f); // duration in milliseconds
|
|
||||||
```
|
|
||||||
Typically called immediately after making a node visible. The node is automatically removed from the animation list when the scale reaches 1.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Touch / Click Priority
|
|
||||||
|
|
||||||
All `Button` and `TextButton` nodes in a layout are collected into a single ordered list during `collectButtonsAndSliders` (depth-first traversal of the node tree, which matches JSON declaration order). When a touch or mouse-down event arrives, the list is scanned **in reverse** — later-declared nodes are checked first — and the **first hit wins**. At most one element (button or textButton, regardless of type) fires per touch.
|
|
||||||
|
|
||||||
**Practical rule:** place background "catch-all" elements (e.g. a full-screen transparent exit button) **early** in the JSON, and foreground interactive elements **later**. The later-declared element will always win when they overlap.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "FrameLayout",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "Button",
|
|
||||||
"name": "phoneExitButton", // declared first → lowest priority
|
|
||||||
"width": "match_parent",
|
|
||||||
"height": "match_parent",
|
|
||||||
"textures": { "normal": "resources/transparent.png", ... }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "chat2button", // declared later → wins over phoneExitButton
|
|
||||||
...
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This behaviour is consistent across `Button` and `TextButton` — there is no inherent type priority, only declaration order matters.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Animations
|
|
||||||
|
|
||||||
Animations can be defined on **Button** and **TextButton** nodes and started from C++ code. Each animation is a named sequence of steps.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "Button",
|
|
||||||
"name": "myButton",
|
|
||||||
"width": 100,
|
|
||||||
"height": 100,
|
|
||||||
"textures": { "normal": "resources/w/btn.png" },
|
|
||||||
"animations": {
|
|
||||||
"bounce": {
|
|
||||||
"repeat": false,
|
|
||||||
"steps": [
|
|
||||||
{ "type": "move", "to": [0, 20], "duration": 0.15, "easing": "easeout" },
|
|
||||||
{ "type": "move", "to": [0, 0], "duration": 0.15, "easing": "easein" },
|
|
||||||
{ "type": "wait", "duration": 0.1 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pulse": {
|
|
||||||
"repeat": true,
|
|
||||||
"steps": [
|
|
||||||
{ "type": "scale", "to": [1.1, 1.1], "duration": 0.4, "easing": "easeout" },
|
|
||||||
{ "type": "scale", "to": [1.0, 1.0], "duration": 0.4, "easing": "easein" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Animation sequence properties
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `repeat` | bool | `false` | Whether the sequence loops after the last step |
|
|
||||||
| `steps` | array | — | Ordered list of animation steps |
|
|
||||||
|
|
||||||
### Step properties
|
|
||||||
|
|
||||||
| Property | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `type` | `"move"` \| `"scale"` \| `"wait"` | Step kind |
|
|
||||||
| `to` | [x, y] | Target offset (`move`) or scale factors (`scale`) |
|
|
||||||
| `duration` | float (seconds) | Duration of the step. `0` applies the target instantly |
|
|
||||||
| `easing` | `"linear"` \| `"easein"` \| `"easeout"` | Interpolation curve (default `"linear"`) |
|
|
||||||
|
|
||||||
**C++ control:**
|
|
||||||
```cpp
|
|
||||||
uiManager.startAnimationOnNode("myButton", "bounce");
|
|
||||||
uiManager.stopAnimationOnNode("myButton", "bounce");
|
|
||||||
uiManager.setAnimationCallback("myButton", "bounce", []() {
|
|
||||||
// called when the non-repeating sequence finishes
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## C++ API Quick Reference
|
|
||||||
|
|
||||||
### Loading and navigation
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
uiManager.loadFromFile("resources/w/ui/screen.json", renderer);
|
|
||||||
uiManager.pushMenuFromFile("resources/w/ui/popup.json", renderer); // push on stack
|
|
||||||
uiManager.popMenu(); // restore previous UI
|
|
||||||
uiManager.clearMenuStack();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Finding nodes
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
auto node = uiManager.findNode("myNode");
|
|
||||||
auto btn = uiManager.findButton("myButton");
|
|
||||||
auto tbtn = uiManager.findTextButton("item1name");
|
|
||||||
auto tv = uiManager.findTextView("quest_description");
|
|
||||||
auto img = uiManager.findStaticImage("background");
|
|
||||||
auto slider = uiManager.findSlider("volumeSlider");
|
|
||||||
auto tf = uiManager.findTextField("playerName");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Visibility
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
uiManager.setNodeVisible("hint5", false);
|
|
||||||
bool visible = uiManager.getNodeVisible("hint5");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pop-in animation
|
|
||||||
|
|
||||||
Scales a node from 0 to 1 using an ease-out curve. Useful for chat bubble reveals and similar "appear" effects.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
uiManager.startPopIn("messageBubble", 300.0f); // node name, duration ms
|
|
||||||
```
|
|
||||||
|
|
||||||
Set the node's `scaleX`/`scaleY` to `0` and call `setNodeVisible` before calling `startPopIn` to avoid a one-frame flash at full size.
|
|
||||||
|
|
||||||
### Dynamic node repositioning
|
|
||||||
|
|
||||||
`node->localY` (and `localX`) can be modified directly on a node pointer, then a layout recalculation applied:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
auto node = uiManager.findNode("messageBubble");
|
|
||||||
node->localY = 350.0f; // new bottom-Y (for vertical_gravity: bottom nodes)
|
|
||||||
uiManager.updateAllLayouts(); // recomputes screenRect and rebuilds meshes
|
|
||||||
```
|
|
||||||
|
|
||||||
This is how the phone chat manager repositions bubbles as new messages arrive.
|
|
||||||
|
|
||||||
### Per-frame update
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
uiManager.update(deltaMs); // advance animations and fade-ins
|
|
||||||
uiManager.draw(renderer); // render everything
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dialogue → UI integration (phone chat bubbles)
|
|
||||||
|
|
||||||
Dialogue nodes in JSON can carry a `"bubbleSlot"` field naming a `StaticImage` UI node. When the dialogue runtime presents that line, it fires the `onBubbleSlotReady` callback with the slot name, which the game uses to reveal the corresponding bubble image.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "line_1",
|
|
||||||
"type": "Line",
|
|
||||||
"speaker": "Айпери",
|
|
||||||
"text": "...",
|
|
||||||
"next": "line_2",
|
|
||||||
"bubbleSlot": "message01in"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Lines without `"bubbleSlot"` (or with an empty value) do not trigger any UI change — useful for internal monologue lines that have no corresponding chat image.
|
|
||||||
|
|
||||||
**C++ wiring:**
|
|
||||||
```cpp
|
|
||||||
dialogueSystem.setOnBubbleSlotReady([](const std::string& slotName) {
|
|
||||||
// slotName == "message01in" etc.
|
|
||||||
menuManager.revealPhoneChatBubble(slotName);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
# WebSocket Deterministic Lockstep Networking
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
The Windows WebSocket transport uses Boost.Asio and Boost.Beast to run a
|
|
||||||
server-sequenced deterministic lockstep simulation. A client receives one
|
|
||||||
authoritative state at a completed tick boundary. After that, the server sends
|
|
||||||
only numbered tick messages containing ordered destination and presence
|
|
||||||
commands (or an empty command list).
|
|
||||||
|
|
||||||
Client intent is intentionally limited to `SetCharacterDestinationCommand`.
|
|
||||||
The server also sends `SetCharacterEnabledCommand` in tick bundles when a
|
|
||||||
configured player slot connects or disconnects.
|
|
||||||
|
|
||||||
## Runtime Architecture
|
|
||||||
|
|
||||||
### Server
|
|
||||||
|
|
||||||
`WebsocketGameServer` wraps the headless `InternalGameServer`.
|
|
||||||
|
|
||||||
- The simulation tick is fixed at 100 ms.
|
|
||||||
- Each accepted connection is assigned the first free character ID from the
|
|
||||||
campaign's `networkPlayerSlots`; the connection is rejected when all slots
|
|
||||||
are occupied.
|
|
||||||
- Player slots start disabled. The server consumes and broadcasts an enabled
|
|
||||||
command on the next tick for each connection and disconnection, including to
|
|
||||||
the newly connected client.
|
|
||||||
- The initial message contains the state, completed snapshot tick, assigned
|
|
||||||
player ID, tick duration, two-tick input delay, and compatibility fingerprint.
|
|
||||||
- Destination inputs cross from the Asio thread through a queue. The simulation
|
|
||||||
thread validates client sequence numbers, replaces client identity with the
|
|
||||||
assigned slot, and schedules commands two ticks in the future.
|
|
||||||
- Every tick is broadcast, including ticks with no commands. No periodic state
|
|
||||||
snapshots are sent.
|
|
||||||
|
|
||||||
### Client
|
|
||||||
|
|
||||||
`WebsocketGameClient` implements `IGameServer`.
|
|
||||||
|
|
||||||
- It connects asynchronously to `ws://127.0.0.1:8080/`.
|
|
||||||
- Received messages are queued by the network thread and applied on the game
|
|
||||||
thread from `update()`.
|
|
||||||
- It maintains an internal `InternalGameServer` replica so existing snapshot
|
|
||||||
consumers and transitional runtime-state bindings continue to work.
|
|
||||||
- Camera orientation lives in host-owned `LocationLocalState`, so incoming
|
|
||||||
authoritative snapshots cannot overwrite camera movement.
|
|
||||||
- `submit()` serializes destination intent only. Other command variants remain
|
|
||||||
local no-ops for a remote connection.
|
|
||||||
- Tick bundles are buffered by ID. The replica advances only while the next
|
|
||||||
contiguous tick is available, applying its commands before one exact 100 ms
|
|
||||||
simulation step.
|
|
||||||
- Completed simulation events are passed to the runtime `Location` for
|
|
||||||
presentation, and ordinary server events are drainable through `IGameServer`.
|
|
||||||
- Client-side save and load calls are ignored because persistence remains
|
|
||||||
server-side.
|
|
||||||
|
|
||||||
`RemoteGameServerHost` prepares local presentation resources and runtime
|
|
||||||
locations against the replica and presents completed-tick events. It never
|
|
||||||
advances simulation from a render-frame delta.
|
|
||||||
|
|
||||||
## Campaign Routing
|
|
||||||
|
|
||||||
The graphical game uses stable router objects for `IGameServer` and
|
|
||||||
`IGameServerHost`, allowing existing UI and runtime references to remain valid.
|
|
||||||
|
|
||||||
When `NETWORK` is defined:
|
|
||||||
|
|
||||||
- `battle_zone_multiplayer` uses `WebsocketGameClient` and `RemoteGameServerHost`.
|
|
||||||
- `main`, `battle_zone`, and every other campaign use `InternalGameServer` and
|
|
||||||
`LocalGameServerHost`.
|
|
||||||
|
|
||||||
The WebSocket client is created only when `battle_zone_multiplayer` is selected
|
|
||||||
and is destroyed when that campaign is unloaded. The single-player and
|
|
||||||
multiplayer Battle Zone entries share location content and the start-state path,
|
|
||||||
so the state can be regenerated through the single-player entry. At startup, the
|
|
||||||
standalone server loads the authoritative half of that shared save and rewrites
|
|
||||||
its campaign ID to `battle_zone_multiplayer`; each remote client restores only
|
|
||||||
the local presentation half. The multiplayer campaign currently declares the
|
|
||||||
pre-existing `player` and `player2` state entries as connection
|
|
||||||
slots; the random test-bot controller is disabled.
|
|
||||||
The campaign's `networkCompatibility` token is the fingerprint; release builds
|
|
||||||
must bump it whenever deterministic content changes.
|
|
||||||
|
|
||||||
If DNS resolution, TCP connection, the WebSocket handshake, or the active socket
|
|
||||||
fails, the client unloads the remote campaign and returns to the campaign
|
|
||||||
selection screen.
|
|
||||||
|
|
||||||
## JSON Protocol v2
|
|
||||||
|
|
||||||
Every message uses a versioned JSON envelope:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"protocolVersion": 2,
|
|
||||||
"type": "tick",
|
|
||||||
"payload": {
|
|
||||||
"tickId": 81,
|
|
||||||
"commands": [],
|
|
||||||
"stateHash": "0123456789abcdef"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Message types are:
|
|
||||||
|
|
||||||
- `initial_state` (server to client): one state snapshot and lockstep metadata;
|
|
||||||
- `destination_input` (client to server): monotonic client sequence and a
|
|
||||||
centimetre-normalized finite target;
|
|
||||||
- `tick` (server to client): tick ID, globally sequenced destination and
|
|
||||||
character-presence commands, and post-tick diagnostic state hash;
|
|
||||||
- `error` (server to client): invalid input, incompatibility, or full server.
|
|
||||||
|
|
||||||
Protocol v1 is not compatible. Malformed payloads, non-finite or out-of-range
|
|
||||||
coordinates, duplicate client sequences, and unsupported commands cannot enter
|
|
||||||
the authoritative command stream.
|
|
||||||
|
|
||||||
WebSocket ordering is not used as simulation time. Integer tick IDs are the
|
|
||||||
only authoritative clock. The server uses `steady_clock` only to pace its loop,
|
|
||||||
and clients may have different wall clocks without affecting the result. A
|
|
||||||
client stalls on a missing tick instead of predicting or rolling back.
|
|
||||||
|
|
||||||
Single-player persistence remains a separate envelope combining authoritative
|
|
||||||
and client-local state. `savedAt`, camera state, and presentation objects are
|
|
||||||
excluded from the diagnostic deterministic hash.
|
|
||||||
|
|
||||||
## Build Configuration
|
|
||||||
|
|
||||||
Networking is enabled only for Windows:
|
|
||||||
|
|
||||||
- `proj-windows/CMakeLists.txt` defines `NETWORK`, compiles the WebSocket client
|
|
||||||
and remote host, and links `ws2_32` and `mswsock`.
|
|
||||||
- `server/CMakeLists.txt` defines `NETWORK` for the Windows `GameServer`, compiles
|
|
||||||
the WebSocket server, and links the Windows socket libraries.
|
|
||||||
- Linux, Android, and Web builds do not define `NETWORK` and do not compile the
|
|
||||||
network transport.
|
|
||||||
|
|
||||||
The previous Web-build `NETWORK` definition was removed.
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
Build the server:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cmake -S server -B server/build
|
|
||||||
cmake --build server/build --config Debug --target GameServer
|
|
||||||
```
|
|
||||||
|
|
||||||
Run it from the repository root:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\server\build\Debug\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
|
|
||||||
```
|
|
||||||
|
|
||||||
The listener defaults to `0.0.0.0:8080`. It can be changed with:
|
|
||||||
|
|
||||||
```text
|
|
||||||
--address IP
|
|
||||||
--port N
|
|
||||||
```
|
|
||||||
|
|
||||||
Launch the Windows game and select `battle_zone_multiplayer`. The client connects to
|
|
||||||
`127.0.0.1:8080`. Selecting another campaign continues to use the local server.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
CTest covers protocol-v2 round trips and validation, input-delay ordering,
|
|
||||||
fixed-tick replica behavior, campaign slot metadata, initial connection and
|
|
||||||
slot assignment, connection/disconnection presence propagation, destination
|
|
||||||
submission, and contiguous tick consumption.
|
|
||||||
|
|
||||||
## Current Limitations
|
|
||||||
|
|
||||||
- Only preconfigured player slots can connect; dynamic join/spawn commands are
|
|
||||||
not implemented.
|
|
||||||
- There is no authentication or reconnect/replay history.
|
|
||||||
- A hash mismatch is logged but does not yet trigger checkpoint resynchronization.
|
|
||||||
- The endpoint is hardcoded in the graphical client.
|
|
||||||
- Multiplayer Lua and dialogue behavior remains unsupported.
|
|
||||||
- Determinism is supported for identical executable/content builds. The
|
|
||||||
floating-point simulation is not guaranteed bit-identical across platforms
|
|
||||||
or compilers.
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY')
|
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY');
|
||||||
def buildAsApplication = !buildAsLibrary
|
def buildAsApplication = !buildAsLibrary
|
||||||
if (buildAsApplication) {
|
if (buildAsApplication) {
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
@@ -9,33 +9,30 @@ else {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
if (buildAsApplication) {
|
if (buildAsApplication) {
|
||||||
namespace "fishrungames.shadowoverbishkek"
|
namespace "org.libsdl.app"
|
||||||
}
|
}
|
||||||
compileSdkVersion 37
|
compileSdkVersion 34
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 21
|
minSdkVersion 19
|
||||||
targetSdkVersion 37
|
targetSdkVersion 34
|
||||||
versionCode 4
|
versionCode 1
|
||||||
versionName "1.0.7"
|
versionName "1.0"
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
/*ndkBuild {
|
/*ndkBuild {
|
||||||
arguments "APP_PLATFORM=android-19"
|
arguments "APP_PLATFORM=android-19"
|
||||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||||
}*/
|
}*/
|
||||||
cmake {
|
cmake {
|
||||||
arguments /*"-DANDROID_APP_PLATFORM=android-19",*/ "-DANDROID_STL=c++_static"
|
arguments "-DANDROID_APP_PLATFORM=android-19", "-DANDROID_STL=c++_static"
|
||||||
cppFlags "-std=c++17 -frtti -fexceptions"
|
cppFlags "-std=c++11 -frtti -fexceptions"
|
||||||
abiFilters 'arm64-v8a', 'x86_64'
|
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// Enables code optimizations.
|
minifyEnabled false
|
||||||
minifyEnabled = true
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
// Enables resource shrinking.
|
|
||||||
shrinkResources = true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
@@ -63,9 +60,11 @@ android {
|
|||||||
|
|
||||||
if (buildAsLibrary) {
|
if (buildAsLibrary) {
|
||||||
libraryVariants.all { variant ->
|
libraryVariants.all { variant ->
|
||||||
variant.outputs.all { output ->
|
variant.outputs.each { output ->
|
||||||
if (output.outputFileName != null && output.outputFileName.endsWith(".aar")) {
|
def outputFile = output.outputFile
|
||||||
output.outputFileName = "fishrungames.shadowoverbishkek.aar"
|
if (outputFile != null && outputFile.name.endsWith(".aar")) {
|
||||||
|
def fileName = "org.libsdl.app.aar";
|
||||||
|
output.outputFile = new File(outputFile.parent, fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,6 +72,5 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.6)
|
||||||
|
|
||||||
|
project(GAME)
|
||||||
|
|
||||||
|
if(POLICY CMP0079)
|
||||||
|
cmake_policy(SET CMP0079 NEW)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Копируем pnglibconf.h.prebuilt
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/libpng/scripts/pnglibconf.h.prebuilt
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/libpng/pnglibconf.h
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сначала zlib
|
||||||
|
add_subdirectory(zlib)
|
||||||
|
|
||||||
|
# ВАЖНО: Установите опции ДО add_subdirectory(libpng)
|
||||||
|
# Libpng создает ДВЕ цели: png_shared (shared) и png_static (static)
|
||||||
|
# Мы хотим статическую библиотеку для Android
|
||||||
|
set(PNG_STATIC ON CACHE BOOL "Build static library" FORCE)
|
||||||
|
set(PNG_SHARED OFF CACHE BOOL "Don't build shared library" FORCE)
|
||||||
|
set(PNG_TESTS OFF CACHE BOOL "Disable tests" FORCE)
|
||||||
|
set(PNG_TOOLS OFF CACHE BOOL "Disable tools" FORCE)
|
||||||
|
set(PNG_EXECUTABLES OFF CACHE BOOL "Disable executables" FORCE)
|
||||||
|
set(PNG_DEBUG OFF CACHE BOOL "Disable debug" FORCE)
|
||||||
|
set(SKIP_INSTALL_ALL ON CACHE BOOL "Skip installation" FORCE)
|
||||||
|
|
||||||
|
# Для Android отключаем оптимизации
|
||||||
|
set(PNG_HARDWARE_OPTIMIZATIONS OFF CACHE BOOL "Disable hardware optimizations" FORCE)
|
||||||
|
set(PNG_ARM_NEON "off" CACHE STRING "Disable ARM NEON" FORCE)
|
||||||
|
|
||||||
|
# Добавляем libpng
|
||||||
|
add_subdirectory(libpng)
|
||||||
|
|
||||||
|
# Затем SDL
|
||||||
|
add_subdirectory(SDL)
|
||||||
|
|
||||||
|
# И ваш код
|
||||||
|
add_subdirectory(src)
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
#include "render/Renderer.h"
|
#include "Renderer.h"
|
||||||
#include "render/TextureManager.h"
|
#include "TextureManager.h"
|
||||||
|
|
||||||
namespace FRG
|
namespace ZL
|
||||||
{
|
{
|
||||||
|
|
||||||
struct MeshGroup
|
struct MeshGroup
|
||||||
@@ -0,0 +1,719 @@
|
|||||||
|
#include "BoneAnimatedModel.h"
|
||||||
|
#include <regex>
|
||||||
|
#include <string>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace ZL
|
||||||
|
{
|
||||||
|
|
||||||
|
int getIndexByValue(const std::string& name, const std::vector<std::string>& words)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < words.size(); i++)
|
||||||
|
{
|
||||||
|
if (words[i] == name)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void BoneSystem::LoadFromFile(const std::string& fileName, const std::string& ZIPFileName)
|
||||||
|
{
|
||||||
|
std::ifstream filestream;
|
||||||
|
std::istringstream zipStream;
|
||||||
|
|
||||||
|
if (!ZIPFileName.empty())
|
||||||
|
{
|
||||||
|
std::vector<char> fileData = readFileFromZIP(fileName, ZIPFileName);
|
||||||
|
std::string fileContents(fileData.begin(), fileData.end());
|
||||||
|
zipStream.str(fileContents);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
filestream.open(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::istream& f = (!ZIPFileName.empty()) ? static_cast<std::istream&>(zipStream) : static_cast<std::istream&>(filestream);
|
||||||
|
|
||||||
|
//Skip first 5 lines
|
||||||
|
std::string tempLine;
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
}
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
static const std::regex pattern_count(R"(\d+)");
|
||||||
|
static const std::regex pattern_float(R"([-]?\d+\.\d+)");
|
||||||
|
static const std::regex pattern_int(R"([-]?\d+)");
|
||||||
|
static const std::regex pattern_boneChildren(R"(\'([^\']+)\')");
|
||||||
|
static const std::regex pattern_bone_weight(R"(\'([^\']+)\'.*?([-]?\d+\.\d+))");
|
||||||
|
|
||||||
|
std::smatch match;
|
||||||
|
|
||||||
|
int numberBones;
|
||||||
|
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
numberBones = std::stoi(number_str);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Bone> bones;
|
||||||
|
std::vector<std::string> boneNames;
|
||||||
|
std::vector<std::string> boneParentNames;
|
||||||
|
std::unordered_map<std::string, std::vector<std::string>> boneChildren;
|
||||||
|
|
||||||
|
bones.resize(numberBones);
|
||||||
|
boneNames.resize(numberBones);
|
||||||
|
boneParentNames.resize(numberBones);
|
||||||
|
|
||||||
|
for (int i = 0; i < numberBones; i++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
std::string boneName = tempLine.substr(6);
|
||||||
|
|
||||||
|
boneNames[i] = boneName;
|
||||||
|
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
std::vector<float> floatValues;
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
bones[i].boneStartWorld = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
|
||||||
|
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //skip tail
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //len
|
||||||
|
|
||||||
|
if (std::regex_search(tempLine, match, pattern_float)) {
|
||||||
|
std::string len_str = match.str();
|
||||||
|
bones[i].boneLength = std::stof(len_str);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
//---------- matrix begin
|
||||||
|
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
b = tempLine.cbegin();
|
||||||
|
e = tempLine.cend();
|
||||||
|
floatValues.clear();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
bones[i].boneMatrixWorld.m[0] = floatValues[0];
|
||||||
|
bones[i].boneMatrixWorld.m[0 + 1 * 3] = floatValues[1];
|
||||||
|
bones[i].boneMatrixWorld.m[0 + 2 * 3] = floatValues[2];
|
||||||
|
|
||||||
|
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
b = tempLine.cbegin();
|
||||||
|
e = tempLine.cend();
|
||||||
|
floatValues.clear();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
bones[i].boneMatrixWorld.m[1] = floatValues[0];
|
||||||
|
bones[i].boneMatrixWorld.m[1 + 1 * 3] = floatValues[1];
|
||||||
|
bones[i].boneMatrixWorld.m[1 + 2 * 3] = floatValues[2];
|
||||||
|
|
||||||
|
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
b = tempLine.cbegin();
|
||||||
|
e = tempLine.cend();
|
||||||
|
floatValues.clear();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
bones[i].boneMatrixWorld.m[2] = floatValues[0];
|
||||||
|
bones[i].boneMatrixWorld.m[2 + 1 * 3] = floatValues[1];
|
||||||
|
bones[i].boneMatrixWorld.m[2 + 2 * 3] = floatValues[2];
|
||||||
|
|
||||||
|
//----------- matrix end
|
||||||
|
std::getline(f, tempLine); //parent
|
||||||
|
|
||||||
|
if (tempLine == " Parent: None")
|
||||||
|
{
|
||||||
|
bones[i].parent = -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::string boneParent = tempLine.substr(10);
|
||||||
|
boneParentNames[i] = boneParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //children
|
||||||
|
|
||||||
|
b = tempLine.cbegin();
|
||||||
|
e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_boneChildren)) {
|
||||||
|
|
||||||
|
boneChildren[boneName].push_back(match.str(1));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//Now process all the bones:
|
||||||
|
for (int i = 0; i < numberBones; i++)
|
||||||
|
{
|
||||||
|
std::string boneName = boneNames[i];
|
||||||
|
std::string boneParent = boneParentNames[i];
|
||||||
|
if (boneParent == "")
|
||||||
|
{
|
||||||
|
bones[i].parent = -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bones[i].parent = getIndexByValue(boneParent, boneNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < boneChildren[boneName].size(); j++)
|
||||||
|
{
|
||||||
|
bones[i].children.push_back(getIndexByValue(boneChildren[boneName][j], boneNames));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*if (boneName == "Bone.020")
|
||||||
|
{
|
||||||
|
std::cout << i << std::endl;
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
startBones = bones;
|
||||||
|
currentBones = bones;
|
||||||
|
|
||||||
|
///std::cout << "Hello!" << std::endl;
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //vertice count
|
||||||
|
int numberVertices;
|
||||||
|
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
numberVertices = std::stoi(number_str);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Vector3f> vertices;
|
||||||
|
|
||||||
|
vertices.resize(numberVertices);
|
||||||
|
for (int i = 0; i < numberVertices; i++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
std::vector<float> floatValues;
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
vertices[i] = Vector3f{floatValues[0], floatValues[1], floatValues[2]};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//==== process uv and normals begin
|
||||||
|
|
||||||
|
std::cout << "Hello x1" << std::endl;
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //===UV Coordinates:
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //triangle count
|
||||||
|
int numberTriangles;
|
||||||
|
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
numberTriangles = std::stoi(number_str);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Now process UVs
|
||||||
|
std::vector<std::array<Vector2f, 3>> uvCoords;
|
||||||
|
|
||||||
|
uvCoords.resize(numberTriangles);
|
||||||
|
|
||||||
|
for (int i = 0; i < numberTriangles; i++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine); //Face 0
|
||||||
|
|
||||||
|
int uvCount;
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
uvCount = std::stoi(number_str);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uvCount != 3)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("more than 3 uvs");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> floatValues;
|
||||||
|
|
||||||
|
for (int j = 0; j < 3; j++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine); //UV <Vector (-0.3661, -1.1665)>
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
floatValues.clear();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (floatValues.size() != 2)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("more than 2 uvs---");
|
||||||
|
}
|
||||||
|
|
||||||
|
uvCoords[i][j] = Vector2f{ floatValues[0],floatValues[1] };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Hello eee" << std::endl;
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //===Normals:
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<Vector3f> normals;
|
||||||
|
|
||||||
|
normals.resize(numberVertices);
|
||||||
|
for (int i = 0; i < numberVertices; i++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
std::vector<float> floatValues;
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
normals[i] = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
|
||||||
|
}
|
||||||
|
|
||||||
|
//==== process uv and normals end
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //triangle count.
|
||||||
|
//numberTriangles; //Need to check if new value is the same as was read before
|
||||||
|
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
numberTriangles = std::stoi(number_str);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::array<int, 3>> triangles;
|
||||||
|
|
||||||
|
triangles.resize(numberTriangles);
|
||||||
|
for (int i = 0; i < numberTriangles; i++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
std::vector<int> intValues;
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_int)) {
|
||||||
|
intValues.push_back(std::stoi(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
triangles[i] = { intValues[0], intValues[1], intValues[2] };
|
||||||
|
}
|
||||||
|
|
||||||
|
std::getline(f, tempLine);//=== Vertex Weights ===
|
||||||
|
std::vector<std::array<BoneWeight, MAX_BONE_COUNT>> localVerticesBoneWeight;
|
||||||
|
localVerticesBoneWeight.resize(numberVertices);
|
||||||
|
|
||||||
|
for (int i = 0; i < numberVertices; i++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine); //skip Vertex 0:
|
||||||
|
std::getline(f, tempLine); //vertex group count
|
||||||
|
int boneCount;
|
||||||
|
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
boneCount = std::stoi(number_str);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boneCount > MAX_BONE_COUNT)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("more than 5 bones");
|
||||||
|
}
|
||||||
|
|
||||||
|
float sumWeights = 0;
|
||||||
|
|
||||||
|
for (int j = 0; j < boneCount; j++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine); //Group: 'Bone', Weight: 0.9929084181785583
|
||||||
|
if (std::regex_search(tempLine, match, pattern_bone_weight)) {
|
||||||
|
// ��������� ����� (��� �������)
|
||||||
|
std::string word = match.str(1);
|
||||||
|
double weight = std::stod(match.str(2));
|
||||||
|
|
||||||
|
int boneNumber = getIndexByValue(word, boneNames);
|
||||||
|
localVerticesBoneWeight[i][j].boneIndex = boneNumber;
|
||||||
|
localVerticesBoneWeight[i][j].weight = weight;
|
||||||
|
sumWeights += weight;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("No match found in the input string.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Normalize weights:
|
||||||
|
for (int j = 0; j < boneCount; j++)
|
||||||
|
{
|
||||||
|
localVerticesBoneWeight[i][j].weight = localVerticesBoneWeight[i][j].weight / sumWeights;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
std::getline(f, tempLine);//=== Animation Keyframes ===
|
||||||
|
std::getline(f, tempLine);//=== Bone Transforms per Keyframe ===
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
int numberKeyFrames;
|
||||||
|
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
numberKeyFrames = std::stoi(number_str);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
animations.resize(1);
|
||||||
|
|
||||||
|
animations[0].keyFrames.resize(numberKeyFrames);
|
||||||
|
|
||||||
|
for (int i = 0; i < numberKeyFrames; i++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
int numberFrame;
|
||||||
|
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
numberFrame = std::stoi(number_str);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
animations[0].keyFrames[i].frame = numberFrame;
|
||||||
|
|
||||||
|
animations[0].keyFrames[i].bones.resize(numberBones);
|
||||||
|
|
||||||
|
for (int j = 0; j < numberBones; j++)
|
||||||
|
{
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
std::string boneName = tempLine.substr(8);
|
||||||
|
int boneNumber = getIndexByValue(boneName, boneNames);
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber] = startBones[boneNumber];
|
||||||
|
|
||||||
|
std::getline(f, tempLine); // Location: <Vector (0.0000, 0.0000, -0.0091)>
|
||||||
|
|
||||||
|
std::vector<float> floatValues;
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneStartWorld = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
|
||||||
|
|
||||||
|
std::getline(f, tempLine); // Rotation
|
||||||
|
std::getline(f, tempLine); // Matrix
|
||||||
|
|
||||||
|
//=============== Matrix begin ==================
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
|
||||||
|
b = tempLine.cbegin();
|
||||||
|
e = tempLine.cend();
|
||||||
|
floatValues.clear();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0] = floatValues[0];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0 + 1 * 4] = floatValues[1];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0 + 2 * 4] = floatValues[2];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0 + 3 * 4] = floatValues[3];
|
||||||
|
|
||||||
|
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
b = tempLine.cbegin();
|
||||||
|
e = tempLine.cend();
|
||||||
|
floatValues.clear();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1] = floatValues[0];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1 + 1 * 4] = floatValues[1];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1 + 2 * 4] = floatValues[2];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1 + 3 * 4] = floatValues[3];
|
||||||
|
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
b = tempLine.cbegin();
|
||||||
|
e = tempLine.cend();
|
||||||
|
floatValues.clear();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2] = floatValues[0];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2 + 1 * 4] = floatValues[1];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2 + 2 * 4] = floatValues[2];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2 + 3 * 4] = floatValues[3];
|
||||||
|
|
||||||
|
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
b = tempLine.cbegin();
|
||||||
|
e = tempLine.cend();
|
||||||
|
floatValues.clear();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3] = floatValues[0];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3 + 1 * 4] = floatValues[1];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3 + 2 * 4] = floatValues[2];
|
||||||
|
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3 + 3 * 4] = floatValues[3];
|
||||||
|
|
||||||
|
//std::getline(f, tempLine);// ignore last matrix line
|
||||||
|
|
||||||
|
//=============== Matrix end ==================
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now let's process bone weights and vertices
|
||||||
|
|
||||||
|
for (int i = 0; i < numberTriangles; i++)
|
||||||
|
{
|
||||||
|
|
||||||
|
mesh.PositionData.push_back(vertices[triangles[i][0]]);
|
||||||
|
mesh.PositionData.push_back(vertices[triangles[i][1]]);
|
||||||
|
mesh.PositionData.push_back(vertices[triangles[i][2]]);
|
||||||
|
|
||||||
|
verticesBoneWeight.push_back(localVerticesBoneWeight[triangles[i][0]]);
|
||||||
|
verticesBoneWeight.push_back(localVerticesBoneWeight[triangles[i][1]]);
|
||||||
|
verticesBoneWeight.push_back(localVerticesBoneWeight[triangles[i][2]]);
|
||||||
|
|
||||||
|
mesh.TexCoordData.push_back(uvCoords[i][0]);
|
||||||
|
mesh.TexCoordData.push_back(uvCoords[i][1]);
|
||||||
|
mesh.TexCoordData.push_back(uvCoords[i][2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
startMesh = mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoneSystem::Interpolate(int frame)
|
||||||
|
{
|
||||||
|
int startingFrame = -1;
|
||||||
|
for (int i = 0; i < animations[0].keyFrames.size() - 1; i++)
|
||||||
|
{
|
||||||
|
int oldFrame = animations[0].keyFrames[i].frame;
|
||||||
|
int nextFrame = animations[0].keyFrames[i + 1].frame;
|
||||||
|
if (frame >= oldFrame && frame < nextFrame)
|
||||||
|
{
|
||||||
|
startingFrame = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startingFrame == -1)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Exception here");
|
||||||
|
}
|
||||||
|
|
||||||
|
int modifiedFrameNumber = frame - animations[0].keyFrames[startingFrame].frame;
|
||||||
|
|
||||||
|
int diffFrames = animations[0].keyFrames[startingFrame + 1].frame - animations[0].keyFrames[startingFrame].frame;
|
||||||
|
|
||||||
|
float t = (modifiedFrameNumber + 0.f) / diffFrames;
|
||||||
|
|
||||||
|
std::vector<Bone>& oneFrameBones = animations[0].keyFrames[startingFrame].bones;
|
||||||
|
std::vector<Bone>& nextFrameBones = animations[0].keyFrames[startingFrame+1].bones;
|
||||||
|
|
||||||
|
std::vector<Matrix4f> skinningMatrixForEachBone;
|
||||||
|
//std::vector<Matrix3f> skinningMatrixForEachBone;
|
||||||
|
skinningMatrixForEachBone.resize(currentBones.size());
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < currentBones.size(); i++)
|
||||||
|
{
|
||||||
|
currentBones[i].boneStartWorld.v[0] = oneFrameBones[i].boneStartWorld.v[0] + t * (nextFrameBones[i].boneStartWorld.v[0] - oneFrameBones[i].boneStartWorld.v[0]);
|
||||||
|
currentBones[i].boneStartWorld.v[1] = oneFrameBones[i].boneStartWorld.v[1] + t * (nextFrameBones[i].boneStartWorld.v[1] - oneFrameBones[i].boneStartWorld.v[1]);
|
||||||
|
currentBones[i].boneStartWorld.v[2] = oneFrameBones[i].boneStartWorld.v[2] + t * (nextFrameBones[i].boneStartWorld.v[2] - oneFrameBones[i].boneStartWorld.v[2]);
|
||||||
|
|
||||||
|
Matrix3f oneFrameBonesMatrix;
|
||||||
|
|
||||||
|
oneFrameBonesMatrix.m[0] = oneFrameBones[i].boneMatrixWorld.m[0];
|
||||||
|
oneFrameBonesMatrix.m[1] = oneFrameBones[i].boneMatrixWorld.m[1];
|
||||||
|
oneFrameBonesMatrix.m[2] = oneFrameBones[i].boneMatrixWorld.m[2];
|
||||||
|
|
||||||
|
oneFrameBonesMatrix.m[3] = oneFrameBones[i].boneMatrixWorld.m[0 + 1*4];
|
||||||
|
oneFrameBonesMatrix.m[4] = oneFrameBones[i].boneMatrixWorld.m[1 + 1*4];
|
||||||
|
oneFrameBonesMatrix.m[5] = oneFrameBones[i].boneMatrixWorld.m[2 + 1*4];
|
||||||
|
|
||||||
|
oneFrameBonesMatrix.m[6] = oneFrameBones[i].boneMatrixWorld.m[0 + 2*4];
|
||||||
|
oneFrameBonesMatrix.m[7] = oneFrameBones[i].boneMatrixWorld.m[1 + 2*4];
|
||||||
|
oneFrameBonesMatrix.m[8] = oneFrameBones[i].boneMatrixWorld.m[2 + 2*4];
|
||||||
|
|
||||||
|
Matrix3f nextFrameBonesMatrix;
|
||||||
|
|
||||||
|
nextFrameBonesMatrix.m[0] = nextFrameBones[i].boneMatrixWorld.m[0];
|
||||||
|
nextFrameBonesMatrix.m[1] = nextFrameBones[i].boneMatrixWorld.m[1];
|
||||||
|
nextFrameBonesMatrix.m[2] = nextFrameBones[i].boneMatrixWorld.m[2];
|
||||||
|
|
||||||
|
nextFrameBonesMatrix.m[3] = nextFrameBones[i].boneMatrixWorld.m[0 + 1 * 4];
|
||||||
|
nextFrameBonesMatrix.m[4] = nextFrameBones[i].boneMatrixWorld.m[1 + 1 * 4];
|
||||||
|
nextFrameBonesMatrix.m[5] = nextFrameBones[i].boneMatrixWorld.m[2 + 1 * 4];
|
||||||
|
|
||||||
|
nextFrameBonesMatrix.m[6] = nextFrameBones[i].boneMatrixWorld.m[0 + 2 * 4];
|
||||||
|
nextFrameBonesMatrix.m[7] = nextFrameBones[i].boneMatrixWorld.m[1 + 2 * 4];
|
||||||
|
nextFrameBonesMatrix.m[8] = nextFrameBones[i].boneMatrixWorld.m[2 + 2 * 4];
|
||||||
|
|
||||||
|
Vector4f q1 = MatrixToQuat(oneFrameBonesMatrix);
|
||||||
|
Vector4f q2 = MatrixToQuat(nextFrameBonesMatrix);
|
||||||
|
Vector4f q1_norm = q1.normalized();
|
||||||
|
Vector4f q2_norm = q2.normalized();
|
||||||
|
|
||||||
|
Vector4f result = slerp(q1_norm, q2_norm, t);
|
||||||
|
|
||||||
|
Matrix3f boneMatrixWorld3 = QuatToMatrix(result);
|
||||||
|
|
||||||
|
currentBones[i].boneMatrixWorld = MakeMatrix4x4(boneMatrixWorld3, currentBones[i].boneStartWorld);
|
||||||
|
|
||||||
|
Matrix4f currentBoneMatrixWorld4 = currentBones[i].boneMatrixWorld;
|
||||||
|
Matrix4f startBoneMatrixWorld4 = animations[0].keyFrames[0].bones[i].boneMatrixWorld;
|
||||||
|
|
||||||
|
Matrix4f inverstedStartBoneMatrixWorld4 = InverseMatrix(startBoneMatrixWorld4);
|
||||||
|
|
||||||
|
skinningMatrixForEachBone[i] = MultMatrixMatrix(currentBoneMatrixWorld4, inverstedStartBoneMatrixWorld4);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
for (int i = 0; i < currentBones.size(); i++)
|
||||||
|
{
|
||||||
|
currentBones[i].boneStartWorld = oneFrameBones[i].boneStartWorld;
|
||||||
|
currentBones[i].boneMatrixWorld = oneFrameBones[i].boneMatrixWorld;
|
||||||
|
//Matrix4f currentBoneMatrixWorld4 = MakeMatrix4x4(currentBones[i].boneMatrixWorld, currentBones[i].boneStartWorld);
|
||||||
|
//Matrix4f startBoneMatrixWorld4 = MakeMatrix4x4(animations[0].keyFrames[0].bones[i].boneMatrixWorld, animations[0].keyFrames[0].bones[i].boneStartWorld);
|
||||||
|
Matrix4f currentBoneMatrixWorld4 = currentBones[i].boneMatrixWorld;
|
||||||
|
Matrix4f startBoneMatrixWorld4 = animations[0].keyFrames[0].bones[i].boneMatrixWorld;
|
||||||
|
Matrix4f inverstedStartBoneMatrixWorld4 = InverseMatrix(startBoneMatrixWorld4);
|
||||||
|
skinningMatrixForEachBone[i] = MultMatrixMatrix(currentBoneMatrixWorld4, inverstedStartBoneMatrixWorld4);
|
||||||
|
|
||||||
|
if (i == 10)
|
||||||
|
{
|
||||||
|
std::cout << i << std::endl;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
for (int i = 0; i < mesh.PositionData.size(); i++)
|
||||||
|
{
|
||||||
|
Vector4f originalPos = {
|
||||||
|
startMesh.PositionData[i].v[0],
|
||||||
|
startMesh.PositionData[i].v[1],
|
||||||
|
startMesh.PositionData[i].v[2], 1.0};
|
||||||
|
|
||||||
|
Vector4f finalPos = Vector4f{0.f, 0.f, 0.f, 0.f};
|
||||||
|
|
||||||
|
bool vMoved = false;
|
||||||
|
//Vector3f finalPos = Vector3f{ 0.f, 0.f, 0.f };
|
||||||
|
|
||||||
|
for (int j = 0; j < MAX_BONE_COUNT; j++)
|
||||||
|
{
|
||||||
|
if (verticesBoneWeight[i][j].weight != 0)
|
||||||
|
{
|
||||||
|
vMoved = true;
|
||||||
|
//finalPos = finalPos + MultVectorMatrix(originalPos, skinningMatrixForEachBone[verticesBoneWeight[i][j].boneIndex]) * verticesBoneWeight[i][j].weight;
|
||||||
|
finalPos = finalPos + MultMatrixVector(skinningMatrixForEachBone[verticesBoneWeight[i][j].boneIndex], originalPos) * verticesBoneWeight[i][j].weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs(finalPos.v[0] - originalPos.v[0]) > 1 || abs(finalPos.v[1] - originalPos.v[1]) > 1 || abs(finalPos.v[2] - originalPos.v[2]) > 1)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vMoved)
|
||||||
|
{
|
||||||
|
finalPos = originalPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh.PositionData[i].v[0] = finalPos.v[0];
|
||||||
|
mesh.PositionData[i].v[1] = finalPos.v[1];
|
||||||
|
mesh.PositionData[i].v[2] = finalPos.v[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "Math.h"
|
||||||
|
#include "Renderer.h"
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
|
||||||
|
namespace ZL
|
||||||
|
{
|
||||||
|
constexpr int MAX_BONE_COUNT = 6;
|
||||||
|
struct Bone
|
||||||
|
{
|
||||||
|
Vector3f boneStartWorld;
|
||||||
|
float boneLength;
|
||||||
|
Matrix4f boneMatrixWorld;
|
||||||
|
// boneVector = boneLength * (0, 1, 0) � ���� ��������
|
||||||
|
// Then multiply by boneMatrixWorld � �� �������� �������� �����
|
||||||
|
|
||||||
|
int parent;
|
||||||
|
std::vector<int> children;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BoneWeight
|
||||||
|
{
|
||||||
|
int boneIndex = -1;
|
||||||
|
float weight = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AnimationKeyFrame
|
||||||
|
{
|
||||||
|
int frame;
|
||||||
|
std::vector<Bone> bones;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Animation
|
||||||
|
{
|
||||||
|
std::vector<AnimationKeyFrame> keyFrames;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BoneSystem
|
||||||
|
{
|
||||||
|
VertexDataStruct mesh;
|
||||||
|
VertexDataStruct startMesh;
|
||||||
|
std::vector<std::array<BoneWeight, MAX_BONE_COUNT>> verticesBoneWeight;
|
||||||
|
|
||||||
|
Matrix4f armatureMatrix;
|
||||||
|
|
||||||
|
std::vector<Bone> startBones;
|
||||||
|
std::vector<Bone> currentBones;
|
||||||
|
|
||||||
|
std::vector<Animation> animations;
|
||||||
|
|
||||||
|
void LoadFromFile(const std::string& fileName, const std::string& ZIPFileName = "");
|
||||||
|
|
||||||
|
void Interpolate(int frame);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.6)
|
||||||
|
|
||||||
|
project(MY_APP CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_library(main SHARED
|
||||||
|
SDL_android_main.c
|
||||||
|
main.cpp
|
||||||
|
|
||||||
|
Utils.h
|
||||||
|
Utils.cpp
|
||||||
|
TextureManager.h
|
||||||
|
TextureManager.cpp
|
||||||
|
TextModel.h
|
||||||
|
TextModel.cpp
|
||||||
|
ShaderManager.h
|
||||||
|
ShaderManager.cpp
|
||||||
|
Renderer.h
|
||||||
|
Renderer.cpp
|
||||||
|
Math.h
|
||||||
|
Math.cpp
|
||||||
|
Environment.h
|
||||||
|
Environment.cpp
|
||||||
|
BoneAnimatedModel.h
|
||||||
|
BoneAnimatedModel.cpp
|
||||||
|
AnimatedModel.h
|
||||||
|
Game.h
|
||||||
|
Game.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Подключаем заголовки
|
||||||
|
target_include_directories(main PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../SDL/include
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../zlib
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../libpng
|
||||||
|
)
|
||||||
|
|
||||||
|
# ВАЖНО: Линкуемся с png_static (статика) или png_shared (динамика)
|
||||||
|
# Так как мы установили PNG_STATIC=ON и PNG_SHARED=OFF,
|
||||||
|
# должна создаться цель png_static
|
||||||
|
target_link_libraries(main
|
||||||
|
png_static # ← ЭТО ПРАВИЛЬНОЕ ИМЯ ЦЕЛИ!
|
||||||
|
z
|
||||||
|
SDL2
|
||||||
|
)
|
||||||
|
|
||||||
|
if(ANDROID)
|
||||||
|
# OpenGL ES 2.0 или 3.0 для Android
|
||||||
|
find_library(OPENGLES2_LIB GLESv2)
|
||||||
|
find_library(OPENGLES1_LIB GLESv1_CM)
|
||||||
|
|
||||||
|
target_link_libraries(main
|
||||||
|
${OPENGLES2_LIB} # OpenGL ES 2.0/3.0
|
||||||
|
${OPENGLES1_LIB} # OpenGL ES 1.x (если нужно)
|
||||||
|
log
|
||||||
|
android
|
||||||
|
OpenSLES
|
||||||
|
dl
|
||||||
|
)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#include "Environment.h"
|
||||||
|
|
||||||
|
#include "Utils.h"
|
||||||
|
//#include <GL/gl.h>
|
||||||
|
#include <GLES2/gl2.h>
|
||||||
|
#include <GLES2/gl2ext.h>
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
int Environment::windowHeaderHeight = 0;
|
||||||
|
int Environment::width = 0;
|
||||||
|
int Environment::height = 0;
|
||||||
|
float Environment::zoom = 20.f;
|
||||||
|
|
||||||
|
bool Environment::leftPressed = false;
|
||||||
|
bool Environment::rightPressed = false;
|
||||||
|
bool Environment::upPressed = false;
|
||||||
|
bool Environment::downPressed = false;
|
||||||
|
|
||||||
|
Vector3f Environment::cameraShift = {0, 0, 0};
|
||||||
|
Vector3f Environment::characterPos = {0, 0, 0};
|
||||||
|
|
||||||
|
float Environment::cameraPhi = 0.f;
|
||||||
|
float Environment::cameraAlpha = 0.3*M_PI / 2.0;
|
||||||
|
|
||||||
|
bool Environment::settings_inverseVertical = false;
|
||||||
|
|
||||||
|
SDL_Window* Environment::window = nullptr;
|
||||||
|
|
||||||
|
bool Environment::showMouse = false;
|
||||||
|
|
||||||
|
bool Environment::exitGameLoop = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace ZL
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "Math.h"
|
||||||
|
#ifdef __linux__
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#endif
|
||||||
|
//#include "OpenGlExtensions.h"
|
||||||
|
#include <GLES2/gl2.h>
|
||||||
|
#include <GLES2/gl2ext.h>
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
class Environment {
|
||||||
|
public:
|
||||||
|
static int windowHeaderHeight;
|
||||||
|
static int width;
|
||||||
|
static int height;
|
||||||
|
static float zoom;
|
||||||
|
|
||||||
|
static bool leftPressed;
|
||||||
|
static bool rightPressed;
|
||||||
|
static bool upPressed;
|
||||||
|
static bool downPressed;
|
||||||
|
|
||||||
|
static Vector3f cameraShift;
|
||||||
|
static Vector3f characterPos;
|
||||||
|
static float cameraPhi;
|
||||||
|
static float cameraAlpha;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static bool settings_inverseVertical;
|
||||||
|
|
||||||
|
static SDL_Window* window;
|
||||||
|
|
||||||
|
static bool showMouse;
|
||||||
|
static bool exitGameLoop;
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZL
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
#include "Game.h"
|
||||||
|
#include "AnimatedModel.h"
|
||||||
|
#include "BoneAnimatedModel.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
#include <iostream>
|
||||||
|
#include "TextureManager.h"
|
||||||
|
#include "TextModel.h"
|
||||||
|
#include <GLES2/gl2.h>
|
||||||
|
#include <GLES2/gl2ext.h>
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
#include <android/log.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
const char *CONST_ZIP_FILE = "";
|
||||||
|
|
||||||
|
Game::Game()
|
||||||
|
: window(nullptr), glContext(nullptr), newTickCount(0), lastTickCount(0),
|
||||||
|
resourcesLoaded(false), modelLoaded(false) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Game::~Game() {
|
||||||
|
if (glContext) {
|
||||||
|
SDL_GL_DeleteContext(glContext);
|
||||||
|
}
|
||||||
|
if (window) {
|
||||||
|
SDL_DestroyWindow(window);
|
||||||
|
}
|
||||||
|
SDL_Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setup() {
|
||||||
|
glContext = SDL_GL_CreateContext(ZL::Environment::window);
|
||||||
|
|
||||||
|
// Initialize renderer
|
||||||
|
renderer.InitOpenGL();
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "Game", "Start for Android");
|
||||||
|
|
||||||
|
const char* testFiles[] = {
|
||||||
|
"resources/spaceship004.txt",
|
||||||
|
"shaders/default.vertex",
|
||||||
|
"shaders/default_android.fragment",
|
||||||
|
"shaders/defaultColor.vertex",
|
||||||
|
"shaders/defaultColor_android.fragment",
|
||||||
|
"resources/sship001x.bmp",
|
||||||
|
nullptr
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; testFiles[i] != nullptr; i++) {
|
||||||
|
SDL_RWops* file = SDL_RWFromFile(testFiles[i], "rb");
|
||||||
|
if (file) {
|
||||||
|
Sint64 size = SDL_RWsize(file);
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "Game", "Found: %s (size: %lld)", testFiles[i], size);
|
||||||
|
SDL_RWclose(file);
|
||||||
|
} else {
|
||||||
|
__android_log_print(ANDROID_LOG_WARN, "Game", "Not found: %s (SDL error: %s)",
|
||||||
|
testFiles[i], SDL_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "Game", "Shaders...");
|
||||||
|
|
||||||
|
renderer.shaderManager.AddShaderFromFiles("default",
|
||||||
|
"shaders/default.vertex",
|
||||||
|
"shaders/default_android.fragment",
|
||||||
|
CONST_ZIP_FILE);
|
||||||
|
|
||||||
|
renderer.shaderManager.AddShaderFromFiles("defaultColor",
|
||||||
|
"shaders/defaultColor.vertex",
|
||||||
|
"shaders/defaultColor_android.fragment",
|
||||||
|
CONST_ZIP_FILE);
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "Game", "Textures...");
|
||||||
|
try {
|
||||||
|
spaceshipTexture = std::make_shared<Texture>(
|
||||||
|
CreateTextureDataFromBmp32("resources/sship001x.bmp")
|
||||||
|
);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
spaceshipTexture = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "Game", "Model...");
|
||||||
|
|
||||||
|
std::string modelPaths[] = {
|
||||||
|
"resources/spaceship004.txt",
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
bool modelLoadSuccess = false;
|
||||||
|
for (int i = 0; !modelLoadSuccess && !modelPaths[i].empty(); i++) {
|
||||||
|
try {
|
||||||
|
spaceshipBase = LoadFromTextFile02(modelPaths[i]);
|
||||||
|
spaceshipBase.RotateByMatrix(QuatToMatrix(QuatFromRotateAroundY(M_PI / 2.0)));
|
||||||
|
spaceship.AssignFrom(spaceshipBase);
|
||||||
|
spaceship.RefreshVBO();
|
||||||
|
modelLoaded = true;
|
||||||
|
modelLoadSuccess = true;
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "Game", "Model loaded successfully from: %s", modelPaths[i].c_str());
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
__android_log_print(ANDROID_LOG_WARN, "Game", "Failed to load model from %s: %s",
|
||||||
|
modelPaths[i].c_str(), e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourcesLoaded = (spaceshipTexture != nullptr && modelLoaded);
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "Game", "Setup failed: %s", e.what());
|
||||||
|
resourcesLoaded = false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Десктопная версия
|
||||||
|
renderer.shaderManager.AddShaderFromFiles("default",
|
||||||
|
"./shaders/default.vertex",
|
||||||
|
"./shaders/default_desktop.fragment",
|
||||||
|
CONST_ZIP_FILE);
|
||||||
|
renderer.shaderManager.AddShaderFromFiles("defaultColor",
|
||||||
|
"./shaders/defaultColor.vertex",
|
||||||
|
"./shaders/defaultColor_desktop.fragment",
|
||||||
|
CONST_ZIP_FILE);
|
||||||
|
|
||||||
|
//Load texture
|
||||||
|
spaceshipTexture = std::make_shared<Texture>(
|
||||||
|
CreateTextureDataFromPng("./resources/sship001x.png"));
|
||||||
|
spaceshipBase = LoadFromTextFile02("./resources/spaceship004.txt");
|
||||||
|
spaceshipBase.RotateByMatrix(QuatToMatrix(QuatFromRotateAroundY(M_PI / 2.0)));
|
||||||
|
spaceship.AssignFrom(spaceshipBase);
|
||||||
|
spaceship.RefreshVBO();
|
||||||
|
resourcesLoaded = true;
|
||||||
|
modelLoaded = true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::drawScene() {
|
||||||
|
static const std::string defaultShaderName = "default";
|
||||||
|
static const std::string vPositionName = "vPosition";
|
||||||
|
static const std::string vTexCoordName = "vTexCoord";
|
||||||
|
static const std::string textureUniformName = "Texture";
|
||||||
|
|
||||||
|
glClearColor(0.0f, 0.5f, 1.0f, 1.0f);
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
glViewport(0, 0, Environment::width, Environment::height);
|
||||||
|
|
||||||
|
if (!resourcesLoaded) {
|
||||||
|
glClearColor(0.2f, 0.3f, 0.8f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.shaderManager.PushShader(defaultShaderName);
|
||||||
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
|
renderer.EnableVertexAttribArray(vPositionName);
|
||||||
|
renderer.EnableVertexAttribArray(vTexCoordName);
|
||||||
|
|
||||||
|
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
||||||
|
static_cast<float>(Environment::width) /
|
||||||
|
static_cast<float>(Environment::height),
|
||||||
|
1, 1000);
|
||||||
|
renderer.PushMatrix();
|
||||||
|
|
||||||
|
renderer.LoadIdentity();
|
||||||
|
renderer.TranslateMatrix({0, 0, -1.0f * Environment::zoom});
|
||||||
|
renderer.RotateMatrix(QuatFromRotateAroundX(M_PI / 6.0));
|
||||||
|
//renderer.RotateMatrix(QuatFromRotateAroundX(Environment::cameraAlpha));
|
||||||
|
|
||||||
|
if (spaceshipTexture) {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
||||||
|
renderer.DrawVertexRenderStruct(spaceship);
|
||||||
|
} else {
|
||||||
|
renderer.DrawVertexRenderStruct(spaceship);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.PopMatrix();
|
||||||
|
renderer.PopProjectionMatrix();
|
||||||
|
renderer.DisableVertexAttribArray(vPositionName);
|
||||||
|
renderer.DisableVertexAttribArray(vTexCoordName);
|
||||||
|
|
||||||
|
renderer.shaderManager.PopShader();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::processTickCount() {
|
||||||
|
|
||||||
|
if (lastTickCount == 0) {
|
||||||
|
lastTickCount = SDL_GetTicks64();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newTickCount = SDL_GetTicks64();
|
||||||
|
if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) {
|
||||||
|
size_t delta = (newTickCount - lastTickCount > CONST_MAX_TIME_INTERVAL) ?
|
||||||
|
CONST_MAX_TIME_INTERVAL : newTickCount - lastTickCount;
|
||||||
|
|
||||||
|
//gameObjects.updateScene(delta);
|
||||||
|
|
||||||
|
Environment::cameraAlpha = Environment::cameraAlpha + delta * M_PI / 10000.f;
|
||||||
|
|
||||||
|
lastTickCount = newTickCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::render() {
|
||||||
|
SDL_GL_MakeCurrent(ZL::Environment::window, glContext);
|
||||||
|
|
||||||
|
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
drawScene();
|
||||||
|
processTickCount();
|
||||||
|
|
||||||
|
SDL_GL_SwapWindow(ZL::Environment::window);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::update() {
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
if (event.type == SDL_QUIT) {
|
||||||
|
Environment::exitGameLoop = true;
|
||||||
|
|
||||||
|
} else if (event.type == SDL_MOUSEWHEEL) {
|
||||||
|
static const float zoomstep = 2.0f;
|
||||||
|
if (event.wheel.y > 0) {
|
||||||
|
Environment::zoom -= zoomstep;
|
||||||
|
} else if (event.wheel.y < 0) {
|
||||||
|
Environment::zoom += zoomstep;
|
||||||
|
}
|
||||||
|
if (Environment::zoom < zoomstep) {
|
||||||
|
Environment::zoom = zoomstep;
|
||||||
|
}
|
||||||
|
/*if (Environment::zoom > 4) {
|
||||||
|
Environment::zoom = 4;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
//this->modelMeshRender.data.Scale(0.5);
|
||||||
|
//this->modelMeshRender.RefreshVBO();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZL
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Renderer.h"
|
||||||
|
#include "Environment.h"
|
||||||
|
#include "TextureManager.h"
|
||||||
|
#include <GLES2/gl2.h>
|
||||||
|
#include <GLES2/gl2ext.h>
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
class Game {
|
||||||
|
public:
|
||||||
|
Game();
|
||||||
|
|
||||||
|
~Game();
|
||||||
|
|
||||||
|
void setup();
|
||||||
|
|
||||||
|
void update();
|
||||||
|
|
||||||
|
void render();
|
||||||
|
|
||||||
|
bool shouldExit() const { return Environment::exitGameLoop; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void processTickCount();
|
||||||
|
|
||||||
|
void drawScene();
|
||||||
|
|
||||||
|
SDL_Window *window;
|
||||||
|
SDL_GLContext glContext;
|
||||||
|
Renderer renderer;
|
||||||
|
|
||||||
|
size_t newTickCount;
|
||||||
|
size_t lastTickCount;
|
||||||
|
|
||||||
|
bool resourcesLoaded;
|
||||||
|
bool modelLoaded;
|
||||||
|
|
||||||
|
static const size_t CONST_TIMER_INTERVAL = 10;
|
||||||
|
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
|
||||||
|
|
||||||
|
std::shared_ptr <Texture> spaceshipTexture;
|
||||||
|
VertexDataStruct spaceshipBase;
|
||||||
|
VertexRenderStruct spaceship;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZL
|
||||||
@@ -0,0 +1,734 @@
|
|||||||
|
#include "Math.h"
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
Vector2f operator+(const Vector2f& x, const Vector2f& y)
|
||||||
|
{
|
||||||
|
Vector2f result;
|
||||||
|
|
||||||
|
result.v[0] = x.v[0] + y.v[0];
|
||||||
|
result.v[1] = x.v[1] + y.v[1];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2f operator-(const Vector2f& x, const Vector2f& y)
|
||||||
|
{
|
||||||
|
Vector2f result;
|
||||||
|
|
||||||
|
result.v[0] = x.v[0] - y.v[0];
|
||||||
|
result.v[1] = x.v[1] - y.v[1];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3f operator+(const Vector3f& x, const Vector3f& y)
|
||||||
|
{
|
||||||
|
Vector3f result;
|
||||||
|
|
||||||
|
result.v[0] = x.v[0] + y.v[0];
|
||||||
|
result.v[1] = x.v[1] + y.v[1];
|
||||||
|
result.v[2] = x.v[2] + y.v[2];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3f operator-(const Vector3f& x, const Vector3f& y)
|
||||||
|
{
|
||||||
|
Vector3f result;
|
||||||
|
|
||||||
|
result.v[0] = x.v[0] - y.v[0];
|
||||||
|
result.v[1] = x.v[1] - y.v[1];
|
||||||
|
result.v[2] = x.v[2] - y.v[2];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4f operator+(const Vector4f& x, const Vector4f& y)
|
||||||
|
{
|
||||||
|
Vector4f result;
|
||||||
|
|
||||||
|
result.v[0] = x.v[0] + y.v[0];
|
||||||
|
result.v[1] = x.v[1] + y.v[1];
|
||||||
|
result.v[2] = x.v[2] + y.v[2];
|
||||||
|
result.v[3] = x.v[3] + y.v[3];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4f operator-(const Vector4f& x, const Vector4f& y)
|
||||||
|
{
|
||||||
|
Vector4f result;
|
||||||
|
|
||||||
|
result.v[0] = x.v[0] - y.v[0];
|
||||||
|
result.v[1] = x.v[1] - y.v[1];
|
||||||
|
result.v[2] = x.v[2] - y.v[2];
|
||||||
|
result.v[3] = x.v[3] - y.v[3];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix3f Matrix3f::Identity()
|
||||||
|
{
|
||||||
|
Matrix3f r;
|
||||||
|
|
||||||
|
r.m[0] = 1.f;
|
||||||
|
r.m[1] = 0.f;
|
||||||
|
r.m[2] = 0.f;
|
||||||
|
|
||||||
|
r.m[3] = 0.f;
|
||||||
|
r.m[4] = 1.f;
|
||||||
|
r.m[5] = 0.f;
|
||||||
|
|
||||||
|
r.m[6] = 0.f;
|
||||||
|
r.m[7] = 0.f;
|
||||||
|
r.m[8] = 1.f;
|
||||||
|
|
||||||
|
return r;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix4f Matrix4f::Identity()
|
||||||
|
{
|
||||||
|
Matrix4f r;
|
||||||
|
|
||||||
|
r.m[0] = 1.f;
|
||||||
|
r.m[1] = 0.f;
|
||||||
|
r.m[2] = 0.f;
|
||||||
|
r.m[3] = 0.f;
|
||||||
|
|
||||||
|
r.m[4] = 0.f;
|
||||||
|
r.m[5] = 1.f;
|
||||||
|
r.m[6] = 0.f;
|
||||||
|
r.m[7] = 0.f;
|
||||||
|
|
||||||
|
r.m[8] = 0.f;
|
||||||
|
r.m[9] = 0.f;
|
||||||
|
r.m[10] = 1.f;
|
||||||
|
r.m[11] = 0.f;
|
||||||
|
|
||||||
|
r.m[12] = 0.f;
|
||||||
|
r.m[13] = 0.f;
|
||||||
|
r.m[14] = 0.f;
|
||||||
|
r.m[15] = 1.f;
|
||||||
|
|
||||||
|
return r;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix4f operator*(const Matrix4f& m1, const Matrix4f& m2)
|
||||||
|
{
|
||||||
|
Matrix4f r;
|
||||||
|
|
||||||
|
r.m[0] = m1.m[0] * m2.m[0] + m1.m[4] * m2.m[1] + m1.m[8] * m2.m[2] + m1.m[12] * m2.m[3];
|
||||||
|
r.m[1] = m1.m[1] * m2.m[0] + m1.m[5] * m2.m[1] + m1.m[9] * m2.m[2] + m1.m[13] * m2.m[3];
|
||||||
|
r.m[2] = m1.m[2] * m2.m[0] + m1.m[6] * m2.m[1] + m1.m[10] * m2.m[2] + m1.m[14] * m2.m[3];
|
||||||
|
r.m[3] = m1.m[3] * m2.m[0] + m1.m[7] * m2.m[1] + m1.m[11] * m2.m[2] + m1.m[15] * m2.m[3];
|
||||||
|
|
||||||
|
r.m[4] = m1.m[0] * m2.m[4] + m1.m[4] * m2.m[5] + m1.m[8] * m2.m[6] + m1.m[12] * m2.m[7];
|
||||||
|
r.m[5] = m1.m[1] * m2.m[4] + m1.m[5] * m2.m[5] + m1.m[9] * m2.m[6] + m1.m[13] * m2.m[7];
|
||||||
|
r.m[6] = m1.m[2] * m2.m[4] + m1.m[6] * m2.m[5] + m1.m[10] * m2.m[6] + m1.m[14] * m2.m[7];
|
||||||
|
r.m[7] = m1.m[3] * m2.m[4] + m1.m[7] * m2.m[5] + m1.m[11] * m2.m[6] + m1.m[15] * m2.m[7];
|
||||||
|
|
||||||
|
|
||||||
|
r.m[8] = m1.m[0] * m2.m[8] + m1.m[4] * m2.m[9] + m1.m[8] * m2.m[10] + m1.m[12] * m2.m[11];
|
||||||
|
r.m[9] = m1.m[1] * m2.m[8] + m1.m[5] * m2.m[9] + m1.m[9] * m2.m[10] + m1.m[13] * m2.m[11];
|
||||||
|
r.m[10] = m1.m[2] * m2.m[8] + m1.m[6] * m2.m[9] + m1.m[10] * m2.m[10] + m1.m[14] * m2.m[11];
|
||||||
|
r.m[11] = m1.m[3] * m2.m[8] + m1.m[7] * m2.m[9] + m1.m[11] * m2.m[10] + m1.m[15] * m2.m[11];
|
||||||
|
|
||||||
|
r.m[12] = m1.m[0] * m2.m[12] + m1.m[4] * m2.m[13] + m1.m[8] * m2.m[14] + m1.m[12] * m2.m[15];
|
||||||
|
r.m[13] = m1.m[1] * m2.m[12] + m1.m[5] * m2.m[13] + m1.m[9] * m2.m[14] + m1.m[13] * m2.m[15];
|
||||||
|
r.m[14] = m1.m[2] * m2.m[12] + m1.m[6] * m2.m[13] + m1.m[10] * m2.m[14] + m1.m[14] * m2.m[15];
|
||||||
|
r.m[15] = m1.m[3] * m2.m[12] + m1.m[7] * m2.m[13] + m1.m[11] * m2.m[14] + m1.m[15] * m2.m[15];
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix4f MakeOrthoMatrix(float width, float height, float zNear, float zFar)
|
||||||
|
{
|
||||||
|
float depthRange = zFar - zNear;
|
||||||
|
|
||||||
|
if (depthRange <= 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("zFar must be greater than zNear");
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix4f r;
|
||||||
|
|
||||||
|
r.m[0] = 2.f / width;
|
||||||
|
r.m[1] = 0;
|
||||||
|
r.m[2] = 0;
|
||||||
|
r.m[3] = 0;
|
||||||
|
|
||||||
|
r.m[4] = 0;
|
||||||
|
r.m[5] = 2.f / height;
|
||||||
|
r.m[6] = 0;
|
||||||
|
r.m[7] = 0;
|
||||||
|
|
||||||
|
r.m[8] = 0;
|
||||||
|
r.m[9] = 0;
|
||||||
|
r.m[10] = -1 / depthRange;
|
||||||
|
r.m[11] = 0;
|
||||||
|
|
||||||
|
r.m[12] = -1;
|
||||||
|
r.m[13] = -1;
|
||||||
|
r.m[14] = zNear / depthRange;
|
||||||
|
r.m[15] = 1;
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix4f MakePerspectiveMatrix(float fovY, float aspectRatio, float zNear, float zFar)
|
||||||
|
{
|
||||||
|
float tanHalfFovy = tan(fovY / 2.f);
|
||||||
|
Matrix4f r;
|
||||||
|
|
||||||
|
if (zNear >= zFar || aspectRatio == 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Invalid perspective parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
r.m[0] = 1.f / (aspectRatio * tanHalfFovy);
|
||||||
|
r.m[1] = 0;
|
||||||
|
r.m[2] = 0;
|
||||||
|
r.m[3] = 0;
|
||||||
|
|
||||||
|
r.m[4] = 0;
|
||||||
|
r.m[5] = 1.f / (tanHalfFovy);
|
||||||
|
r.m[6] = 0;
|
||||||
|
r.m[7] = 0;
|
||||||
|
|
||||||
|
r.m[8] = 0;
|
||||||
|
r.m[9] = 0;
|
||||||
|
r.m[10] = -(zFar + zNear) / (zFar - zNear);
|
||||||
|
r.m[11] = -1;
|
||||||
|
|
||||||
|
r.m[12] = 0;
|
||||||
|
r.m[13] = 0;
|
||||||
|
r.m[14] = -(2.f * zFar * zNear) / (zFar - zNear);
|
||||||
|
r.m[15] = 0;
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix3f QuatToMatrix(const Vector4f& q)
|
||||||
|
{
|
||||||
|
Matrix3f m;
|
||||||
|
|
||||||
|
float wx, wy, wz, xx, yy, yz, xy, xz, zz, s, x2, y2, z2;
|
||||||
|
|
||||||
|
s = 2.0f / (q.v[0] * q.v[0] + q.v[1] * q.v[1] + q.v[2] * q.v[2] + q.v[3] * q.v[3]);
|
||||||
|
|
||||||
|
|
||||||
|
x2 = q.v[0] * s;
|
||||||
|
y2 = q.v[1] * s;
|
||||||
|
z2 = q.v[2] * s;
|
||||||
|
|
||||||
|
wx = q.v[3] * x2; wy = q.v[3] * y2; wz = q.v[3] * z2;
|
||||||
|
xx = q.v[0] * x2; xy = q.v[1] * x2; xz = q.v[2] * x2;
|
||||||
|
yy = q.v[1] * y2; yz = q.v[2] * y2;
|
||||||
|
zz = q.v[2] * z2;
|
||||||
|
|
||||||
|
m.m[0] = 1.0f - (yy + zz);
|
||||||
|
m.m[1] = xy + wz;
|
||||||
|
m.m[2] = xz - wy;
|
||||||
|
|
||||||
|
m.m[3] = xy - wz;
|
||||||
|
m.m[4] = 1.0f - (xx + zz);
|
||||||
|
m.m[5] = yz + wx;
|
||||||
|
|
||||||
|
m.m[6] = xz + wy;
|
||||||
|
m.m[7] = yz - wx;
|
||||||
|
m.m[8] = 1.0f - (xx + yy);
|
||||||
|
|
||||||
|
return m;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4f MatrixToQuat(const Matrix3f& m)
|
||||||
|
{
|
||||||
|
Vector4f r;
|
||||||
|
float trace = m.m[0] + m.m[4] + m.m[8];
|
||||||
|
|
||||||
|
if (trace > 0)
|
||||||
|
{
|
||||||
|
float s = 0.5f / sqrtf(trace + 1.0f);
|
||||||
|
r.v[3] = 0.25f / s;
|
||||||
|
r.v[0] = (m.m[5] - m.m[7]) * s;
|
||||||
|
r.v[1] = (m.m[6] - m.m[2]) * s;
|
||||||
|
r.v[2] = (m.m[1] - m.m[3]) * s;
|
||||||
|
}
|
||||||
|
else if (m.m[0] > m.m[4] && m.m[0] > m.m[8])
|
||||||
|
{
|
||||||
|
float s = 2.0f * sqrtf(1.0f + m.m[0] - m.m[4] - m.m[8]);
|
||||||
|
r.v[3] = (m.m[5] - m.m[7]) / s;
|
||||||
|
r.v[0] = 0.25f * s;
|
||||||
|
r.v[1] = (m.m[1] + m.m[3]) / s;
|
||||||
|
r.v[2] = (m.m[6] + m.m[2]) / s;
|
||||||
|
}
|
||||||
|
else if (m.m[4] > m.m[8])
|
||||||
|
{
|
||||||
|
float s = 2.0f * sqrtf(1.0f + m.m[4] - m.m[0] - m.m[8]);
|
||||||
|
r.v[3] = (m.m[6] - m.m[2]) / s;
|
||||||
|
r.v[0] = (m.m[1] + m.m[3]) / s;
|
||||||
|
r.v[1] = 0.25f * s;
|
||||||
|
r.v[2] = (m.m[5] + m.m[7]) / s;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float s = 2.0f * sqrtf(1.0f + m.m[8] - m.m[0] - m.m[4]);
|
||||||
|
r.v[3] = (m.m[1] - m.m[3]) / s;
|
||||||
|
r.v[0] = (m.m[6] + m.m[2]) / s;
|
||||||
|
r.v[1] = (m.m[5] + m.m[7]) / s;
|
||||||
|
r.v[2] = 0.25f * s;
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.normalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4f QuatFromRotateAroundX(float angle)
|
||||||
|
{
|
||||||
|
Vector4f result;
|
||||||
|
|
||||||
|
result.v[0] = sinf(angle * 0.5f);
|
||||||
|
result.v[1] = 0.f;
|
||||||
|
result.v[2] = 0.f;
|
||||||
|
result.v[3] = cosf(angle * 0.5f);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4f QuatFromRotateAroundY(float angle)
|
||||||
|
{
|
||||||
|
Vector4f result;
|
||||||
|
|
||||||
|
result.v[0] = 0.f;
|
||||||
|
result.v[1] = sinf(angle * 0.5f);
|
||||||
|
result.v[2] = 0.f;
|
||||||
|
result.v[3] = cosf(angle * 0.5f);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4f QuatFromRotateAroundZ(float angle)
|
||||||
|
{
|
||||||
|
Vector4f result;
|
||||||
|
|
||||||
|
result.v[0] = 0.f;
|
||||||
|
result.v[1] = 0.f;
|
||||||
|
result.v[2] = sinf(angle * 0.5f);
|
||||||
|
result.v[3] = cosf(angle * 0.5f);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix3f TransposeMatrix(const Matrix3f& m)
|
||||||
|
{
|
||||||
|
Matrix3f r;
|
||||||
|
r.m[0] = m.m[0];
|
||||||
|
r.m[1] = m.m[3];
|
||||||
|
r.m[2] = m.m[6];
|
||||||
|
r.m[3] = m.m[1];
|
||||||
|
r.m[4] = m.m[4];
|
||||||
|
r.m[5] = m.m[7];
|
||||||
|
r.m[6] = m.m[2];
|
||||||
|
r.m[7] = m.m[5];
|
||||||
|
r.m[8] = m.m[8];
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix3f InverseMatrix(const Matrix3f& m)
|
||||||
|
{
|
||||||
|
float d;
|
||||||
|
Matrix3f r;
|
||||||
|
|
||||||
|
d = m.m[0] * (m.m[4] * m.m[8] - m.m[5] * m.m[7]);
|
||||||
|
d -= m.m[1] * (m.m[3] * m.m[8] - m.m[6] * m.m[5]);
|
||||||
|
d += m.m[2] * (m.m[3] * m.m[7] - m.m[6] * m.m[4]);
|
||||||
|
|
||||||
|
if (fabs(d) < 0.01f)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Error: matrix cannot be inversed!");
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
r.m[0] = (m.m[4] * m.m[8] - m.m[5] * m.m[7]) / d;
|
||||||
|
r.m[1] = -(m.m[1] * m.m[8] - m.m[2] * m.m[7]) / d;
|
||||||
|
r.m[2] = (m.m[1] * m.m[5] - m.m[2] * m.m[4]) / d;
|
||||||
|
|
||||||
|
r.m[3] = -(m.m[3] * m.m[8] - m.m[5] * m.m[6]) / d;
|
||||||
|
r.m[4] = (m.m[0] * m.m[8] - m.m[2] * m.m[6]) / d;
|
||||||
|
r.m[5] = -(m.m[0] * m.m[5] - m.m[2] * m.m[3]) / d;
|
||||||
|
|
||||||
|
r.m[6] = (m.m[3] * m.m[7] - m.m[6] * m.m[4]) / d;
|
||||||
|
r.m[7] = -(m.m[0] * m.m[7] - m.m[6] * m.m[1]) / d;
|
||||||
|
r.m[8] = (m.m[0] * m.m[4] - m.m[1] * m.m[3]) / d;
|
||||||
|
|
||||||
|
};
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Matrix4f InverseMatrix(const Matrix4f& mat)
|
||||||
|
{
|
||||||
|
Matrix4f inv;
|
||||||
|
float det;
|
||||||
|
|
||||||
|
inv.m[0] = mat.m[5] * mat.m[10] * mat.m[15] -
|
||||||
|
mat.m[5] * mat.m[11] * mat.m[14] -
|
||||||
|
mat.m[9] * mat.m[6] * mat.m[15] +
|
||||||
|
mat.m[9] * mat.m[7] * mat.m[14] +
|
||||||
|
mat.m[13] * mat.m[6] * mat.m[11] -
|
||||||
|
mat.m[13] * mat.m[7] * mat.m[10];
|
||||||
|
|
||||||
|
inv.m[4] = -mat.m[4] * mat.m[10] * mat.m[15] +
|
||||||
|
mat.m[4] * mat.m[11] * mat.m[14] +
|
||||||
|
mat.m[8] * mat.m[6] * mat.m[15] -
|
||||||
|
mat.m[8] * mat.m[7] * mat.m[14] -
|
||||||
|
mat.m[12] * mat.m[6] * mat.m[11] +
|
||||||
|
mat.m[12] * mat.m[7] * mat.m[10];
|
||||||
|
|
||||||
|
inv.m[8] = mat.m[4] * mat.m[9] * mat.m[15] -
|
||||||
|
mat.m[4] * mat.m[11] * mat.m[13] -
|
||||||
|
mat.m[8] * mat.m[5] * mat.m[15] +
|
||||||
|
mat.m[8] * mat.m[7] * mat.m[13] +
|
||||||
|
mat.m[12] * mat.m[5] * mat.m[11] -
|
||||||
|
mat.m[12] * mat.m[7] * mat.m[9];
|
||||||
|
|
||||||
|
inv.m[12] = -mat.m[4] * mat.m[9] * mat.m[14] +
|
||||||
|
mat.m[4] * mat.m[10] * mat.m[13] +
|
||||||
|
mat.m[8] * mat.m[5] * mat.m[14] -
|
||||||
|
mat.m[8] * mat.m[6] * mat.m[13] -
|
||||||
|
mat.m[12] * mat.m[5] * mat.m[10] +
|
||||||
|
mat.m[12] * mat.m[6] * mat.m[9];
|
||||||
|
|
||||||
|
inv.m[1] = -mat.m[1] * mat.m[10] * mat.m[15] +
|
||||||
|
mat.m[1] * mat.m[11] * mat.m[14] +
|
||||||
|
mat.m[9] * mat.m[2] * mat.m[15] -
|
||||||
|
mat.m[9] * mat.m[3] * mat.m[14] -
|
||||||
|
mat.m[13] * mat.m[2] * mat.m[11] +
|
||||||
|
mat.m[13] * mat.m[3] * mat.m[10];
|
||||||
|
|
||||||
|
inv.m[5] = mat.m[0] * mat.m[10] * mat.m[15] -
|
||||||
|
mat.m[0] * mat.m[11] * mat.m[14] -
|
||||||
|
mat.m[8] * mat.m[2] * mat.m[15] +
|
||||||
|
mat.m[8] * mat.m[3] * mat.m[14] +
|
||||||
|
mat.m[12] * mat.m[2] * mat.m[11] -
|
||||||
|
mat.m[12] * mat.m[3] * mat.m[10];
|
||||||
|
|
||||||
|
inv.m[9] = -mat.m[0] * mat.m[9] * mat.m[15] +
|
||||||
|
mat.m[0] * mat.m[11] * mat.m[13] +
|
||||||
|
mat.m[8] * mat.m[1] * mat.m[15] -
|
||||||
|
mat.m[8] * mat.m[3] * mat.m[13] -
|
||||||
|
mat.m[12] * mat.m[1] * mat.m[11] +
|
||||||
|
mat.m[12] * mat.m[3] * mat.m[9];
|
||||||
|
|
||||||
|
inv.m[13] = mat.m[0] * mat.m[9] * mat.m[14] -
|
||||||
|
mat.m[0] * mat.m[10] * mat.m[13] -
|
||||||
|
mat.m[8] * mat.m[1] * mat.m[14] +
|
||||||
|
mat.m[8] * mat.m[2] * mat.m[13] +
|
||||||
|
mat.m[12] * mat.m[1] * mat.m[10] -
|
||||||
|
mat.m[12] * mat.m[2] * mat.m[9];
|
||||||
|
|
||||||
|
inv.m[2] = mat.m[1] * mat.m[6] * mat.m[15] -
|
||||||
|
mat.m[1] * mat.m[7] * mat.m[14] -
|
||||||
|
mat.m[5] * mat.m[2] * mat.m[15] +
|
||||||
|
mat.m[5] * mat.m[3] * mat.m[14] +
|
||||||
|
mat.m[13] * mat.m[2] * mat.m[7] -
|
||||||
|
mat.m[13] * mat.m[3] * mat.m[6];
|
||||||
|
|
||||||
|
inv.m[6] = -mat.m[0] * mat.m[6] * mat.m[15] +
|
||||||
|
mat.m[0] * mat.m[7] * mat.m[14] +
|
||||||
|
mat.m[4] * mat.m[2] * mat.m[15] -
|
||||||
|
mat.m[4] * mat.m[3] * mat.m[14] -
|
||||||
|
mat.m[12] * mat.m[2] * mat.m[7] +
|
||||||
|
mat.m[12] * mat.m[3] * mat.m[6];
|
||||||
|
|
||||||
|
inv.m[10] = mat.m[0] * mat.m[5] * mat.m[15] -
|
||||||
|
mat.m[0] * mat.m[7] * mat.m[13] -
|
||||||
|
mat.m[4] * mat.m[1] * mat.m[15] +
|
||||||
|
mat.m[4] * mat.m[3] * mat.m[13] +
|
||||||
|
mat.m[12] * mat.m[1] * mat.m[7] -
|
||||||
|
mat.m[12] * mat.m[3] * mat.m[5];
|
||||||
|
|
||||||
|
inv.m[14] = -mat.m[0] * mat.m[5] * mat.m[14] +
|
||||||
|
mat.m[0] * mat.m[6] * mat.m[13] +
|
||||||
|
mat.m[4] * mat.m[1] * mat.m[14] -
|
||||||
|
mat.m[4] * mat.m[2] * mat.m[13] -
|
||||||
|
mat.m[12] * mat.m[1] * mat.m[6] +
|
||||||
|
mat.m[12] * mat.m[2] * mat.m[5];
|
||||||
|
|
||||||
|
inv.m[3] = -mat.m[1] * mat.m[6] * mat.m[11] +
|
||||||
|
mat.m[1] * mat.m[7] * mat.m[10] +
|
||||||
|
mat.m[5] * mat.m[2] * mat.m[11] -
|
||||||
|
mat.m[5] * mat.m[3] * mat.m[10] -
|
||||||
|
mat.m[9] * mat.m[2] * mat.m[7] +
|
||||||
|
mat.m[9] * mat.m[3] * mat.m[6];
|
||||||
|
|
||||||
|
inv.m[7] = mat.m[0] * mat.m[6] * mat.m[11] -
|
||||||
|
mat.m[0] * mat.m[7] * mat.m[10] -
|
||||||
|
mat.m[4] * mat.m[2] * mat.m[11] +
|
||||||
|
mat.m[4] * mat.m[3] * mat.m[10] +
|
||||||
|
mat.m[8] * mat.m[2] * mat.m[7] -
|
||||||
|
mat.m[8] * mat.m[3] * mat.m[6];
|
||||||
|
|
||||||
|
inv.m[11] = -mat.m[0] * mat.m[5] * mat.m[11] +
|
||||||
|
mat.m[0] * mat.m[7] * mat.m[9] +
|
||||||
|
mat.m[4] * mat.m[1] * mat.m[11] -
|
||||||
|
mat.m[4] * mat.m[3] * mat.m[9] -
|
||||||
|
mat.m[8] * mat.m[1] * mat.m[7] +
|
||||||
|
mat.m[8] * mat.m[3] * mat.m[5];
|
||||||
|
|
||||||
|
inv.m[15] = mat.m[0] * mat.m[5] * mat.m[10] -
|
||||||
|
mat.m[0] * mat.m[6] * mat.m[9] -
|
||||||
|
mat.m[4] * mat.m[1] * mat.m[10] +
|
||||||
|
mat.m[4] * mat.m[2] * mat.m[9] +
|
||||||
|
mat.m[8] * mat.m[1] * mat.m[6] -
|
||||||
|
mat.m[8] * mat.m[2] * mat.m[5];
|
||||||
|
|
||||||
|
// ��������� �����������
|
||||||
|
det = mat.m[0] * inv.m[0] + mat.m[1] * inv.m[4] + mat.m[2] * inv.m[8] + mat.m[3] * inv.m[12];
|
||||||
|
|
||||||
|
if (std::fabs(det) < 0.01f)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Error: matrix cannot be inversed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ����� ��� �������� �� �����������
|
||||||
|
det = 1.0f / det;
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
inv.m[i] *= det;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inv;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix3f CreateZRotationMatrix(float angle)
|
||||||
|
{
|
||||||
|
Matrix3f result = Matrix3f::Identity();
|
||||||
|
|
||||||
|
result.m[0] = cosf(angle);
|
||||||
|
result.m[1] = -sinf(angle);
|
||||||
|
result.m[3] = sinf(angle);
|
||||||
|
result.m[4] = cosf(angle);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix4f MultMatrixMatrix(const Matrix4f& m1, const Matrix4f& m2)
|
||||||
|
{
|
||||||
|
Matrix4f rx;
|
||||||
|
|
||||||
|
rx.m[0] = m1.m[0] * m2.m[0] + m1.m[4] * m2.m[1] + m1.m[8] * m2.m[2] + m1.m[12] * m2.m[3];
|
||||||
|
rx.m[1] = m1.m[1] * m2.m[0] + m1.m[5] * m2.m[1] + m1.m[9] * m2.m[2] + m1.m[13] * m2.m[3];
|
||||||
|
rx.m[2] = m1.m[2] * m2.m[0] + m1.m[6] * m2.m[1] + m1.m[10] * m2.m[2] + m1.m[14] * m2.m[3];
|
||||||
|
rx.m[3] = m1.m[3] * m2.m[0] + m1.m[7] * m2.m[1] + m1.m[11] * m2.m[2] + m1.m[15] * m2.m[3];
|
||||||
|
|
||||||
|
rx.m[4] = m1.m[0] * m2.m[4] + m1.m[4] * m2.m[5] + m1.m[8] * m2.m[6] + m1.m[12] * m2.m[7];
|
||||||
|
rx.m[5] = m1.m[1] * m2.m[4] + m1.m[5] * m2.m[5] + m1.m[9] * m2.m[6] + m1.m[13] * m2.m[7];
|
||||||
|
rx.m[6] = m1.m[2] * m2.m[4] + m1.m[6] * m2.m[5] + m1.m[10] * m2.m[6] + m1.m[14] * m2.m[7];
|
||||||
|
rx.m[7] = m1.m[3] * m2.m[4] + m1.m[7] * m2.m[5] + m1.m[11] * m2.m[6] + m1.m[15] * m2.m[7];
|
||||||
|
|
||||||
|
|
||||||
|
rx.m[8] = m1.m[0] * m2.m[8] + m1.m[4] * m2.m[9] + m1.m[8] * m2.m[10] + m1.m[12] * m2.m[11];
|
||||||
|
rx.m[9] = m1.m[1] * m2.m[8] + m1.m[5] * m2.m[9] + m1.m[9] * m2.m[10] + m1.m[13] * m2.m[11];
|
||||||
|
rx.m[10] = m1.m[2] * m2.m[8] + m1.m[6] * m2.m[9] + m1.m[10] * m2.m[10] + m1.m[14] * m2.m[11];
|
||||||
|
rx.m[11] = m1.m[3] * m2.m[8] + m1.m[7] * m2.m[9] + m1.m[11] * m2.m[10] + m1.m[15] * m2.m[11];
|
||||||
|
|
||||||
|
rx.m[12] = m1.m[0] * m2.m[12] + m1.m[4] * m2.m[13] + m1.m[8] * m2.m[14] + m1.m[12] * m2.m[15];
|
||||||
|
rx.m[13] = m1.m[1] * m2.m[12] + m1.m[5] * m2.m[13] + m1.m[9] * m2.m[14] + m1.m[13] * m2.m[15];
|
||||||
|
rx.m[14] = m1.m[2] * m2.m[12] + m1.m[6] * m2.m[13] + m1.m[10] * m2.m[14] + m1.m[14] * m2.m[15];
|
||||||
|
rx.m[15] = m1.m[3] * m2.m[12] + m1.m[7] * m2.m[13] + m1.m[11] * m2.m[14] + m1.m[15] * m2.m[15];
|
||||||
|
|
||||||
|
return rx;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix3f MultMatrixMatrix(const Matrix3f& m1, const Matrix3f& m2)
|
||||||
|
{
|
||||||
|
Matrix3f r;
|
||||||
|
|
||||||
|
r.m[0] = m1.m[0] * m2.m[0] + m1.m[3] * m2.m[1] + m1.m[6] * m2.m[2];
|
||||||
|
r.m[1] = m1.m[1] * m2.m[0] + m1.m[4] * m2.m[1] + m1.m[7] * m2.m[2];
|
||||||
|
r.m[2] = m1.m[2] * m2.m[0] + m1.m[5] * m2.m[1] + m1.m[8] * m2.m[2];
|
||||||
|
|
||||||
|
r.m[3] = m1.m[0] * m2.m[3] + m1.m[3] * m2.m[4] + m1.m[6] * m2.m[5];
|
||||||
|
r.m[4] = m1.m[1] * m2.m[3] + m1.m[4] * m2.m[4] + m1.m[7] * m2.m[5];
|
||||||
|
r.m[5] = m1.m[2] * m2.m[3] + m1.m[5] * m2.m[4] + m1.m[8] * m2.m[5];
|
||||||
|
|
||||||
|
r.m[6] = m1.m[0] * m2.m[6] + m1.m[3] * m2.m[7] + m1.m[6] * m2.m[8] ;
|
||||||
|
r.m[7] = m1.m[1] * m2.m[6] + m1.m[4] * m2.m[7] + m1.m[7] * m2.m[8];
|
||||||
|
r.m[8] = m1.m[2] * m2.m[6] + m1.m[5] * m2.m[7] + m1.m[8] * m2.m[8];
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix3f MakeTranslationMatrix(const Vector3f& p)
|
||||||
|
{
|
||||||
|
Matrix3f r = Matrix3f::Identity();
|
||||||
|
|
||||||
|
r.m[12] = p.v[0];
|
||||||
|
r.m[13] = p.v[1];
|
||||||
|
r.m[14] = p.v[2];
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix3f MakeScaleMatrix(float scale)
|
||||||
|
{
|
||||||
|
Matrix3f r = Matrix3f::Identity();
|
||||||
|
|
||||||
|
r.m[0] = scale;
|
||||||
|
r.m[5] = scale;
|
||||||
|
r.m[10] = scale;
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix3f MakeRotationMatrix(const Vector3f& p)
|
||||||
|
{
|
||||||
|
Matrix3f r = Matrix3f::Identity();
|
||||||
|
|
||||||
|
r.m[12] = p.v[0];
|
||||||
|
r.m[13] = p.v[1];
|
||||||
|
r.m[14] = p.v[2];
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3f operator*(Vector3f v, float scale)
|
||||||
|
{
|
||||||
|
Vector3f r = v;
|
||||||
|
|
||||||
|
r.v[0] = v.v[0] * scale;
|
||||||
|
r.v[1] = v.v[1] * scale;
|
||||||
|
r.v[2] = v.v[2] * scale;
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4f operator*(Vector4f v, float scale)
|
||||||
|
{
|
||||||
|
Vector4f r = v;
|
||||||
|
|
||||||
|
r.v[0] = v.v[0] * scale;
|
||||||
|
r.v[1] = v.v[1] * scale;
|
||||||
|
r.v[2] = v.v[2] * scale;
|
||||||
|
r.v[3] = v.v[3] * scale;
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3f MultVectorMatrix(Vector3f v, Matrix3f mt)
|
||||||
|
{
|
||||||
|
Vector3f r;
|
||||||
|
|
||||||
|
r.v[0] = v.v[0] * mt.m[0] + v.v[1] * mt.m[1] + v.v[2] * mt.m[2];
|
||||||
|
r.v[1] = v.v[0] * mt.m[3] + v.v[1] * mt.m[4] + v.v[2] * mt.m[5];
|
||||||
|
r.v[2] = v.v[0] * mt.m[6] + v.v[1] * mt.m[7] + v.v[2] * mt.m[8];
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4f MultVectorMatrix(Vector4f v, Matrix4f mt)
|
||||||
|
{
|
||||||
|
Vector4f r;
|
||||||
|
|
||||||
|
r.v[0] = v.v[0] * mt.m[0] + v.v[1] * mt.m[1] + v.v[2] * mt.m[2] + v.v[3] * mt.m[3];
|
||||||
|
r.v[1] = v.v[0] * mt.m[4] + v.v[1] * mt.m[5] + v.v[2] * mt.m[6] + v.v[3] * mt.m[7];
|
||||||
|
r.v[2] = v.v[0] * mt.m[8] + v.v[1] * mt.m[9] + v.v[2] * mt.m[10] + v.v[3] * mt.m[11];
|
||||||
|
r.v[3] = v.v[0] * mt.m[12] + v.v[1] * mt.m[13] + v.v[2] * mt.m[14] + v.v[3] * mt.m[15];
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4f MultMatrixVector(Matrix4f mt, Vector4f v)
|
||||||
|
{
|
||||||
|
Vector4f r;
|
||||||
|
|
||||||
|
r.v[0] = v.v[0] * mt.m[0] + v.v[1] * mt.m[4] + v.v[2] * mt.m[8] + v.v[3] * mt.m[12];
|
||||||
|
r.v[1] = v.v[0] * mt.m[1] + v.v[1] * mt.m[5] + v.v[2] * mt.m[9] + v.v[3] * mt.m[13];
|
||||||
|
r.v[2] = v.v[0] * mt.m[2] + v.v[1] * mt.m[6] + v.v[2] * mt.m[10] + v.v[3] * mt.m[14];
|
||||||
|
r.v[3] = v.v[0] * mt.m[3] + v.v[1] * mt.m[7] + v.v[2] * mt.m[11] + v.v[3] * mt.m[15];
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4f slerp(const Vector4f& q1, const Vector4f& q2, float t)
|
||||||
|
{
|
||||||
|
const float epsilon = 1e-6f;
|
||||||
|
|
||||||
|
// Нормализация входных кватернионов
|
||||||
|
Vector4f q1_norm = q1.normalized();
|
||||||
|
Vector4f q2_norm = q2.normalized();
|
||||||
|
|
||||||
|
float cosTheta = q1_norm.dot(q2_norm);
|
||||||
|
|
||||||
|
// Если q1 и q2 близки к противоположным направлениям, корректируем q2
|
||||||
|
Vector4f q2_adjusted = q2_norm;
|
||||||
|
if (cosTheta < 0.0f) {
|
||||||
|
q2_adjusted.v[0] = -q2_adjusted.v[0];
|
||||||
|
q2_adjusted.v[1] = -q2_adjusted.v[1];
|
||||||
|
q2_adjusted.v[2] = -q2_adjusted.v[2];
|
||||||
|
q2_adjusted.v[3] = -q2_adjusted.v[3];
|
||||||
|
cosTheta = -cosTheta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если кватернионы близки, используем линейную интерполяцию
|
||||||
|
if (cosTheta > 1.0f - epsilon) {
|
||||||
|
Vector4f result;
|
||||||
|
|
||||||
|
result.v[0] = q1_norm.v[0] + t * (q2_adjusted.v[0] - q1_norm.v[0]);
|
||||||
|
result.v[1] = q1_norm.v[1] + t * (q2_adjusted.v[1] - q1_norm.v[1]);
|
||||||
|
result.v[2] = q1_norm.v[2] + t * (q2_adjusted.v[2] - q1_norm.v[2]);
|
||||||
|
result.v[3] = q1_norm.v[3] + t * (q2_adjusted.v[3] - q1_norm.v[3]);
|
||||||
|
|
||||||
|
return result.normalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Иначе используем сферическую интерполяцию
|
||||||
|
float theta = std::acos(cosTheta);
|
||||||
|
float sinTheta = std::sin(theta);
|
||||||
|
|
||||||
|
float coeff1 = std::sin((1.0f - t) * theta) / sinTheta;
|
||||||
|
float coeff2 = std::sin(t * theta) / sinTheta;
|
||||||
|
|
||||||
|
Vector4f result;
|
||||||
|
|
||||||
|
result.v[0] = coeff1 * q1_norm.v[0] + coeff2 * q2_adjusted.v[0];
|
||||||
|
result.v[1] = coeff1 * q1_norm.v[1] + coeff2 * q2_adjusted.v[1];
|
||||||
|
result.v[2] = coeff1 * q1_norm.v[2] + coeff2 * q2_adjusted.v[2];
|
||||||
|
result.v[3] = coeff1 * q1_norm.v[3] + coeff2 * q2_adjusted.v[3];
|
||||||
|
|
||||||
|
return result.normalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix4f MakeMatrix4x4(const Matrix3f& m, const Vector3f pos)
|
||||||
|
{
|
||||||
|
Matrix4f r;
|
||||||
|
|
||||||
|
r.m[0] = m.m[0];
|
||||||
|
r.m[1] = m.m[1];
|
||||||
|
r.m[2] = m.m[2];
|
||||||
|
r.m[3] = 0;
|
||||||
|
|
||||||
|
r.m[4] = m.m[3];
|
||||||
|
r.m[5] = m.m[4];
|
||||||
|
r.m[6] = m.m[5];
|
||||||
|
r.m[7] = 0;
|
||||||
|
|
||||||
|
r.m[8] = m.m[6];
|
||||||
|
r.m[9] = m.m[7];
|
||||||
|
r.m[10] = m.m[8];
|
||||||
|
r.m[11] = 0;
|
||||||
|
|
||||||
|
r.m[12] = pos.v[0];
|
||||||
|
r.m[13] = pos.v[1];
|
||||||
|
r.m[14] = pos.v[2];
|
||||||
|
r.m[15] = 1.0;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <exception>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
struct Vector4f
|
||||||
|
{
|
||||||
|
std::array<float, 4> v = { 0.f, 0.f, 0.f, 0.f };
|
||||||
|
|
||||||
|
Vector4f normalized() const {
|
||||||
|
double norm = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2] + v[3] * v[3]);
|
||||||
|
Vector4f r;
|
||||||
|
|
||||||
|
r.v[0] = v[0] / norm;
|
||||||
|
r.v[1] = v[1] / norm;
|
||||||
|
r.v[2] = v[2] / norm;
|
||||||
|
r.v[3] = v[3] / norm;
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
double dot(const Vector4f& other) const {
|
||||||
|
return v[0] * other.v[0] + v[1] * other.v[1] + v[2] * other.v[2] + v[3] * other.v[3];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Vector3f
|
||||||
|
{
|
||||||
|
std::array<float, 3> v = { 0.f, 0.f, 0.f };
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Vector2f
|
||||||
|
{
|
||||||
|
std::array<float, 2> v = {0.f, 0.f};
|
||||||
|
};
|
||||||
|
|
||||||
|
Vector2f operator+(const Vector2f& x, const Vector2f& y);
|
||||||
|
|
||||||
|
Vector2f operator-(const Vector2f& x, const Vector2f& y);
|
||||||
|
|
||||||
|
Vector3f operator+(const Vector3f& x, const Vector3f& y);
|
||||||
|
|
||||||
|
Vector3f operator-(const Vector3f& x, const Vector3f& y);
|
||||||
|
Vector4f operator+(const Vector4f& x, const Vector4f& y);
|
||||||
|
|
||||||
|
Vector4f operator-(const Vector4f& x, const Vector4f& y);
|
||||||
|
|
||||||
|
|
||||||
|
struct Matrix3f
|
||||||
|
{
|
||||||
|
std::array<float, 9> m = { 0.f, 0.f, 0.f,
|
||||||
|
0.f, 0.f, 0.f,
|
||||||
|
0.f, 0.f, 0.f, };
|
||||||
|
|
||||||
|
static Matrix3f Identity();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Matrix4f
|
||||||
|
{
|
||||||
|
std::array<float, 16> m = { 0.f, 0.f, 0.f, 0.f,
|
||||||
|
0.f, 0.f, 0.f, 0.f,
|
||||||
|
0.f, 0.f, 0.f, 0.f,
|
||||||
|
0.f, 0.f, 0.f, 0.f };
|
||||||
|
|
||||||
|
static Matrix4f Identity();
|
||||||
|
|
||||||
|
float& operator()(int row, int col) {
|
||||||
|
//return m[row * 4 + col]; //OpenGL specific
|
||||||
|
return m[col * 4 + row];
|
||||||
|
}
|
||||||
|
|
||||||
|
const float& operator()(int row, int col) const {
|
||||||
|
//return m[row * 4 + col];
|
||||||
|
return m[col * 4 + row];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Matrix4f operator*(const Matrix4f& m1, const Matrix4f& m2);
|
||||||
|
|
||||||
|
Matrix4f MakeOrthoMatrix(float width, float height, float zNear, float zFar);
|
||||||
|
|
||||||
|
Matrix4f MakePerspectiveMatrix(float fovY, float aspectRatio, float zNear, float zFar);
|
||||||
|
|
||||||
|
Matrix3f QuatToMatrix(const Vector4f& q);
|
||||||
|
|
||||||
|
Vector4f MatrixToQuat(const Matrix3f& m);
|
||||||
|
|
||||||
|
Vector4f QuatFromRotateAroundX(float angle);
|
||||||
|
Vector4f QuatFromRotateAroundY(float angle);
|
||||||
|
Vector4f QuatFromRotateAroundZ(float angle);
|
||||||
|
|
||||||
|
Vector3f operator*(Vector3f v, float scale);
|
||||||
|
Vector4f operator*(Vector4f v, float scale);
|
||||||
|
|
||||||
|
Vector3f MultVectorMatrix(Vector3f v, Matrix3f mt);
|
||||||
|
Vector4f MultVectorMatrix(Vector4f v, Matrix4f mt);
|
||||||
|
Vector4f MultMatrixVector(Matrix4f mt, Vector4f v);
|
||||||
|
|
||||||
|
Vector4f slerp(const Vector4f& q1, const Vector4f& q2, float t);
|
||||||
|
Matrix3f InverseMatrix(const Matrix3f& m);
|
||||||
|
Matrix4f InverseMatrix(const Matrix4f& m);
|
||||||
|
Matrix3f MultMatrixMatrix(const Matrix3f& m1, const Matrix3f& m2);
|
||||||
|
Matrix4f MultMatrixMatrix(const Matrix4f& m1, const Matrix4f& m2);
|
||||||
|
Matrix4f MakeMatrix4x4(const Matrix3f& m, const Vector3f pos);
|
||||||
|
|
||||||
|
};
|
||||||
@@ -1,103 +1,7 @@
|
|||||||
#include "render/Renderer.h"
|
#include "Renderer.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
namespace FRG {
|
namespace ZL {
|
||||||
|
|
||||||
Matrix4f MakeOrthoMatrix(float width, float height, float zNear, float zFar)
|
|
||||||
{
|
|
||||||
float depthRange = zFar - zNear;
|
|
||||||
|
|
||||||
if (depthRange <= 0)
|
|
||||||
{
|
|
||||||
throw std::runtime_error("zFar must be greater than zNear");
|
|
||||||
}
|
|
||||||
|
|
||||||
Matrix4f r;
|
|
||||||
|
|
||||||
r.data()[0] = 2.f / width;
|
|
||||||
r.data()[1] = 0;
|
|
||||||
r.data()[2] = 0;
|
|
||||||
r.data()[3] = 0;
|
|
||||||
|
|
||||||
r.data()[4] = 0;
|
|
||||||
r.data()[5] = 2.f / height;
|
|
||||||
r.data()[6] = 0;
|
|
||||||
r.data()[7] = 0;
|
|
||||||
|
|
||||||
r.data()[8] = 0;
|
|
||||||
r.data()[9] = 0;
|
|
||||||
r.data()[10] = -1 / depthRange;
|
|
||||||
r.data()[11] = 0;
|
|
||||||
|
|
||||||
r.data()[12] = -1;
|
|
||||||
r.data()[13] = -1;
|
|
||||||
r.data()[14] = zNear / depthRange;
|
|
||||||
r.data()[15] = 1;
|
|
||||||
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
Matrix4f MakeOrthoMatrix(float xmin, float xmax, float ymin, float ymax, float zNear, float zFar)
|
|
||||||
{
|
|
||||||
float width = xmax - xmin;
|
|
||||||
float height = ymax - ymin;
|
|
||||||
float depthRange = zFar - zNear;
|
|
||||||
|
|
||||||
if (width <= 0 || height <= 0 || depthRange <= 0)
|
|
||||||
{
|
|
||||||
throw std::runtime_error("Invalid dimensions for orthogonal matrix");
|
|
||||||
}
|
|
||||||
|
|
||||||
Matrix4f r;
|
|
||||||
|
|
||||||
r.data()[0] = 2.f / width;
|
|
||||||
r.data()[5] = 2.f / height;
|
|
||||||
r.data()[10] = -1.f / depthRange;
|
|
||||||
|
|
||||||
r.data()[1] = r.data()[2] = r.data()[3] = 0;
|
|
||||||
r.data()[4] = r.data()[6] = r.data()[7] = 0;
|
|
||||||
r.data()[8] = r.data()[9] = r.data()[11] = 0;
|
|
||||||
|
|
||||||
r.data()[12] = -(xmax + xmin) / width;
|
|
||||||
r.data()[13] = -(ymax + ymin) / height;
|
|
||||||
r.data()[14] = zNear / depthRange;
|
|
||||||
r.data()[15] = 1.f;
|
|
||||||
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
Matrix4f MakePerspectiveMatrix(float fovY, float aspectRatio, float zNear, float zFar)
|
|
||||||
{
|
|
||||||
float tanHalfFovy = tan(fovY / 2.f);
|
|
||||||
Matrix4f r;
|
|
||||||
|
|
||||||
if (zNear >= zFar || aspectRatio == 0)
|
|
||||||
{
|
|
||||||
throw std::runtime_error("Invalid perspective parameters");
|
|
||||||
}
|
|
||||||
|
|
||||||
r.data()[0] = 1.f / (aspectRatio * tanHalfFovy);
|
|
||||||
r.data()[1] = 0;
|
|
||||||
r.data()[2] = 0;
|
|
||||||
r.data()[3] = 0;
|
|
||||||
|
|
||||||
r.data()[4] = 0;
|
|
||||||
r.data()[5] = 1.f / (tanHalfFovy);
|
|
||||||
r.data()[6] = 0;
|
|
||||||
r.data()[7] = 0;
|
|
||||||
|
|
||||||
r.data()[8] = 0;
|
|
||||||
r.data()[9] = 0;
|
|
||||||
r.data()[10] = -(zFar + zNear) / (zFar - zNear);
|
|
||||||
r.data()[11] = -1;
|
|
||||||
|
|
||||||
r.data()[12] = 0;
|
|
||||||
r.data()[13] = 0;
|
|
||||||
r.data()[14] = -(2.f * zFar * zNear) / (zFar - zNear);
|
|
||||||
r.data()[15] = 0;
|
|
||||||
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
VBOHolder::VBOHolder()
|
VBOHolder::VBOHolder()
|
||||||
{
|
{
|
||||||
@@ -117,8 +21,10 @@ namespace FRG {
|
|||||||
VAOHolder::VAOHolder()
|
VAOHolder::VAOHolder()
|
||||||
{
|
{
|
||||||
#ifndef EMSCRIPTEN
|
#ifndef EMSCRIPTEN
|
||||||
#ifndef ANDROID
|
#if defined(_WIN32) || defined(__linux__) && !defined(__ANDROID__)
|
||||||
glGenVertexArrays(1, &vao);
|
glGenVertexArrays(1, &vao);
|
||||||
|
#else
|
||||||
|
vao = 0;
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -126,13 +32,8 @@ namespace FRG {
|
|||||||
VAOHolder::~VAOHolder()
|
VAOHolder::~VAOHolder()
|
||||||
{
|
{
|
||||||
#ifndef EMSCRIPTEN
|
#ifndef EMSCRIPTEN
|
||||||
#ifndef ANDROID
|
#if defined(_WIN32) || defined(__linux__) && !defined(__ANDROID__)
|
||||||
#ifdef __linux__
|
glDeleteVertexArrays(1, &vao);
|
||||||
glDeleteVertexArrays(1, &vao);
|
|
||||||
#else
|
|
||||||
//Windows
|
|
||||||
glDeleteVertexArrays(1, &vao);
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -149,10 +50,10 @@ namespace FRG {
|
|||||||
|
|
||||||
Vector2f posTo = center + halfWidthHeight;
|
Vector2f posTo = center + halfWidthHeight;
|
||||||
|
|
||||||
Vector3f pos1 = { posFrom(0), posFrom(1), zLevel };
|
Vector3f pos1 = { posFrom.v[0], posFrom.v[1], zLevel };
|
||||||
Vector3f pos2 = { posFrom(0), posTo(1), zLevel };
|
Vector3f pos2 = { posFrom.v[0], posTo.v[1], zLevel };
|
||||||
Vector3f pos3 = { posTo(0), posTo(1), zLevel };
|
Vector3f pos3 = { posTo.v[0], posTo.v[1], zLevel };
|
||||||
Vector3f pos4 = { posTo(0), posFrom(1), zLevel };
|
Vector3f pos4 = { posTo.v[0], posFrom.v[1], zLevel };
|
||||||
|
|
||||||
|
|
||||||
Vector2f texCoordPos1 = { 0.0f, 0.0f };
|
Vector2f texCoordPos1 = { 0.0f, 0.0f };
|
||||||
@@ -179,40 +80,22 @@ namespace FRG {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
VertexDataStruct CreatePolygonFloor(const std::vector<Eigen::Vector2f>& polygon, float yLevel, const Vector3f& color)
|
|
||||||
{
|
|
||||||
VertexDataStruct result;
|
|
||||||
if (polygon.size() < 3) return result;
|
|
||||||
|
|
||||||
for (size_t i = 1; i + 1 < polygon.size(); ++i) {
|
|
||||||
result.PositionData.push_back({ polygon[0].x(), yLevel, polygon[0].y() });
|
|
||||||
result.PositionData.push_back({ polygon[i].x(), yLevel, polygon[i].y() });
|
|
||||||
result.PositionData.push_back({ polygon[i + 1].x(), yLevel, polygon[i + 1].y() });
|
|
||||||
|
|
||||||
result.ColorData.push_back(color);
|
|
||||||
result.ColorData.push_back(color);
|
|
||||||
result.ColorData.push_back(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
VertexDataStruct CreateRectHorizontalSections2D(Vector2f center, Vector2f halfWidthHeight, float zLevel, size_t sectionCount)
|
VertexDataStruct CreateRectHorizontalSections2D(Vector2f center, Vector2f halfWidthHeight, float zLevel, size_t sectionCount)
|
||||||
{
|
{
|
||||||
Vector2f posFrom = center - halfWidthHeight;
|
Vector2f posFrom = center - halfWidthHeight;
|
||||||
|
|
||||||
Vector2f posTo = center + halfWidthHeight;
|
Vector2f posTo = center + halfWidthHeight;
|
||||||
|
|
||||||
float sectionWidth = halfWidthHeight(0) * 2.f;
|
float sectionWidth = halfWidthHeight.v[0] * 2.f;
|
||||||
|
|
||||||
VertexDataStruct result;
|
VertexDataStruct result;
|
||||||
|
|
||||||
for (size_t i = 0; i < sectionCount; i++)
|
for (size_t i = 0; i < sectionCount; i++)
|
||||||
{
|
{
|
||||||
Vector3f pos1 = { posFrom(0)+sectionWidth*i, posFrom(1), zLevel };
|
Vector3f pos1 = { posFrom.v[0]+sectionWidth*i, posFrom.v[1], zLevel };
|
||||||
Vector3f pos2 = { posFrom(0) + sectionWidth * i, posTo(1), zLevel };
|
Vector3f pos2 = { posFrom.v[0] + sectionWidth * i, posTo.v[1], zLevel };
|
||||||
Vector3f pos3 = { posTo(0) + sectionWidth * i, posTo(1), zLevel };
|
Vector3f pos3 = { posTo.v[0] + sectionWidth * i, posTo.v[1], zLevel };
|
||||||
Vector3f pos4 = { posTo(0) + sectionWidth * i, posFrom(1), zLevel };
|
Vector3f pos4 = { posTo.v[0] + sectionWidth * i, posFrom.v[1], zLevel };
|
||||||
|
|
||||||
result.PositionData.push_back(pos1);
|
result.PositionData.push_back(pos1);
|
||||||
result.PositionData.push_back(pos2);
|
result.PositionData.push_back(pos2);
|
||||||
@@ -313,83 +196,17 @@ namespace FRG {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
VertexDataStruct CreateCubemap(float scale)
|
|
||||||
{
|
|
||||||
VertexDataStruct cubemapVertexDataStruct;
|
|
||||||
|
|
||||||
// +x
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
|
|
||||||
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, scale });
|
|
||||||
|
|
||||||
// -x
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, scale });
|
|
||||||
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, scale });
|
|
||||||
|
|
||||||
|
|
||||||
// +y
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
|
|
||||||
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, scale });
|
|
||||||
|
|
||||||
|
|
||||||
// -y
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, scale });
|
|
||||||
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, scale });
|
|
||||||
|
|
||||||
|
|
||||||
// +z
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
|
|
||||||
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, scale });
|
|
||||||
|
|
||||||
// -z
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, -scale });
|
|
||||||
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, -scale });
|
|
||||||
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, -scale });
|
|
||||||
|
|
||||||
return cubemapVertexDataStruct;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void VertexRenderStruct::RefreshVBO()
|
void VertexRenderStruct::RefreshVBO()
|
||||||
{
|
{
|
||||||
//Check if main thread, check if data is not empty...
|
//Check if main thread, check if data is not empty...
|
||||||
|
|
||||||
#ifndef EMSCRIPTEN
|
#ifndef EMSCRIPTEN
|
||||||
#ifndef __ANDROID__
|
#if defined(_WIN32) || defined(__linux__) && !defined(__ANDROID__)
|
||||||
if (!vao)
|
if (!vao)
|
||||||
{
|
{
|
||||||
vao = std::make_shared<VAOHolder>();
|
vao = std::make_shared<VAOHolder>();
|
||||||
}
|
}
|
||||||
|
glBindVertexArray(vao->getBuffer());
|
||||||
glBindVertexArray(vao->getBuffer());
|
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
if (!positionVBO)
|
if (!positionVBO)
|
||||||
@@ -399,7 +216,7 @@ namespace FRG {
|
|||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, positionVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, positionVBO->getBuffer());
|
||||||
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, data.PositionData.size() * 12, &data.PositionData[0], GL_DYNAMIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, data.PositionData.size() * 12, &data.PositionData[0], GL_STATIC_DRAW);
|
||||||
|
|
||||||
if (data.TexCoordData.size() > 0)
|
if (data.TexCoordData.size() > 0)
|
||||||
{
|
{
|
||||||
@@ -410,9 +227,10 @@ namespace FRG {
|
|||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, texCoordVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, texCoordVBO->getBuffer());
|
||||||
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, data.TexCoordData.size() * 8, &data.TexCoordData[0], GL_DYNAMIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, data.TexCoordData.size() * 8, &data.TexCoordData[0], GL_STATIC_DRAW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (data.NormalData.size() > 0)
|
if (data.NormalData.size() > 0)
|
||||||
{
|
{
|
||||||
if (!normalVBO)
|
if (!normalVBO)
|
||||||
@@ -422,7 +240,7 @@ namespace FRG {
|
|||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, normalVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, normalVBO->getBuffer());
|
||||||
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, data.NormalData.size() * 12, &data.NormalData[0], GL_DYNAMIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, data.NormalData.size() * 12, &data.NormalData[0], GL_STATIC_DRAW);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.TangentData.size() > 0)
|
if (data.TangentData.size() > 0)
|
||||||
@@ -434,7 +252,7 @@ namespace FRG {
|
|||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, tangentVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, tangentVBO->getBuffer());
|
||||||
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, data.TangentData.size() * 12, &data.TangentData[0], GL_DYNAMIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, data.TangentData.size() * 12, &data.TangentData[0], GL_STATIC_DRAW);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.BinormalData.size() > 0)
|
if (data.BinormalData.size() > 0)
|
||||||
@@ -446,7 +264,7 @@ namespace FRG {
|
|||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, binormalVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, binormalVBO->getBuffer());
|
||||||
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, data.BinormalData.size() * 12, &data.BinormalData[0], GL_DYNAMIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, data.BinormalData.size() * 12, &data.BinormalData[0], GL_STATIC_DRAW);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.ColorData.size() > 0)
|
if (data.ColorData.size() > 0)
|
||||||
@@ -458,7 +276,7 @@ namespace FRG {
|
|||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, colorVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, colorVBO->getBuffer());
|
||||||
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, data.ColorData.size() * 12, &data.ColorData[0], GL_DYNAMIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, data.ColorData.size() * 12, &data.ColorData[0], GL_STATIC_DRAW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,9 +299,9 @@ namespace FRG {
|
|||||||
{
|
{
|
||||||
for (int i = 0; i < PositionData.size(); i++)
|
for (int i = 0; i < PositionData.size(); i++)
|
||||||
{
|
{
|
||||||
auto value = PositionData[i](1);
|
auto value = PositionData[i].v[1];
|
||||||
PositionData[i](1) = PositionData[i](2);
|
PositionData[i].v[1] = PositionData[i].v[2];
|
||||||
PositionData[i](2) = value;
|
PositionData[i].v[2] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,22 +311,22 @@ namespace FRG {
|
|||||||
|
|
||||||
for (int i = 0; i < PositionData.size(); i++)
|
for (int i = 0; i < PositionData.size(); i++)
|
||||||
{
|
{
|
||||||
PositionData[i] = PositionData[i].transpose() * m;
|
PositionData[i] = MultVectorMatrix(PositionData[i], m);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < NormalData.size(); i++)
|
for (int i = 0; i < NormalData.size(); i++)
|
||||||
{
|
{
|
||||||
NormalData[i] = NormalData[i].transpose() * m;
|
NormalData[i] = MultVectorMatrix(NormalData[i], m);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < TangentData.size(); i++)
|
for (int i = 0; i < TangentData.size(); i++)
|
||||||
{
|
{
|
||||||
TangentData[i] = TangentData[i].transpose() * m;
|
TangentData[i] = MultVectorMatrix(TangentData[i], m);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < BinormalData.size(); i++)
|
for (int i = 0; i < BinormalData.size(); i++)
|
||||||
{
|
{
|
||||||
BinormalData[i] = BinormalData[i].transpose() * m;
|
BinormalData[i] = MultVectorMatrix(BinormalData[i], m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,14 +347,14 @@ namespace FRG {
|
|||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
|
||||||
#ifndef EMSCRIPTEN
|
#ifndef EMSCRIPTEN
|
||||||
#ifndef __ANDROID__
|
#if defined(_WIN32) || defined(__linux__) && !defined(__ANDROID__)
|
||||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
glDepthFunc(GL_LEQUAL);
|
glDepthFunc(GL_LEQUAL);
|
||||||
|
|
||||||
CheckGlError(__FILE__, __LINE__);
|
//CheckGlError();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::PushProjectionMatrix(float width, float height, float zNear, float zFar)
|
void Renderer::PushProjectionMatrix(float width, float height, float zNear, float zFar)
|
||||||
@@ -550,18 +368,6 @@ namespace FRG {
|
|||||||
throw std::runtime_error("Projection matrix stack overflow!!!!");
|
throw std::runtime_error("Projection matrix stack overflow!!!!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::PushProjectionMatrix(float xmin, float xmax, float ymin, float ymax, float zNear, float zFar)
|
|
||||||
{
|
|
||||||
Matrix4f m = MakeOrthoMatrix(xmin, xmax, ymin, ymax, zNear, zFar);
|
|
||||||
ProjectionMatrixStack.push(m);
|
|
||||||
SetMatrix();
|
|
||||||
|
|
||||||
if (ProjectionMatrixStack.size() > CONST_MATRIX_STACK_SIZE)
|
|
||||||
{
|
|
||||||
throw std::runtime_error("Projection matrix stack overflow!!!!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::PushPerspectiveProjectionMatrix(float fovY, float aspectRatio, float zNear, float zFar)
|
void Renderer::PushPerspectiveProjectionMatrix(float fovY, float aspectRatio, float zNear, float zFar)
|
||||||
{
|
{
|
||||||
@@ -591,12 +397,6 @@ namespace FRG {
|
|||||||
return ProjectionModelViewMatrix;
|
return ProjectionModelViewMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
Matrix4f Renderer::GetCurrentModelViewMatrix()
|
|
||||||
{
|
|
||||||
return ModelviewMatrixStack.top();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void Renderer::SetMatrix()
|
void Renderer::SetMatrix()
|
||||||
{
|
{
|
||||||
if (ProjectionMatrixStack.size() <= 0)
|
if (ProjectionMatrixStack.size() <= 0)
|
||||||
@@ -609,16 +409,15 @@ namespace FRG {
|
|||||||
throw std::runtime_error("Modelview matrix stack out!");
|
throw std::runtime_error("Modelview matrix stack out!");
|
||||||
}
|
}
|
||||||
|
|
||||||
Matrix4f& modelViewMatrix = ModelviewMatrixStack.top();
|
ProjectionModelViewMatrix = ProjectionMatrixStack.top() * ModelviewMatrixStack.top();
|
||||||
ProjectionModelViewMatrix = ProjectionMatrixStack.top() * modelViewMatrix;
|
|
||||||
|
|
||||||
static const std::string ProjectionModelViewMatrixName = "ProjectionModelViewMatrix";
|
static const std::string ProjectionModelViewMatrixName = "ProjectionModelViewMatrix";
|
||||||
|
|
||||||
RenderUniformMatrix4fv(ProjectionModelViewMatrixName, false, ProjectionModelViewMatrix.data());
|
//static const std::string ProjectionMatrixName = "ProjectionMatrix";
|
||||||
|
|
||||||
static const std::string ModelViewMatrixName = "ModelViewMatrix";
|
RenderUniformMatrix4fv(ProjectionModelViewMatrixName, false, &ProjectionModelViewMatrix.m[0]);
|
||||||
|
|
||||||
RenderUniformMatrix4fv(ModelViewMatrixName, false, modelViewMatrix.data());
|
//RenderUniformMatrix4fv(ProjectionMatrixName, false, &ProjectionMatrixStack.top().m[0]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,7 +434,6 @@ namespace FRG {
|
|||||||
{
|
{
|
||||||
throw std::runtime_error("Modelview matrix stack overflow!!!!");
|
throw std::runtime_error("Modelview matrix stack overflow!!!!");
|
||||||
}
|
}
|
||||||
SetMatrix();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::LoadIdentity()
|
void Renderer::LoadIdentity()
|
||||||
@@ -655,9 +453,9 @@ namespace FRG {
|
|||||||
{
|
{
|
||||||
|
|
||||||
Matrix4f m = Matrix4f::Identity();
|
Matrix4f m = Matrix4f::Identity();
|
||||||
m.data()[12] = p(0);
|
m.m[12] = p.v[0];
|
||||||
m.data()[13] = p(1);
|
m.m[13] = p.v[1];
|
||||||
m.data()[14] = p(2);
|
m.m[14] = p.v[2];
|
||||||
|
|
||||||
m = ModelviewMatrixStack.top() * m;
|
m = ModelviewMatrixStack.top() * m;
|
||||||
|
|
||||||
@@ -675,9 +473,9 @@ namespace FRG {
|
|||||||
void Renderer::ScaleMatrix(float scale)
|
void Renderer::ScaleMatrix(float scale)
|
||||||
{
|
{
|
||||||
Matrix4f m = Matrix4f::Identity();
|
Matrix4f m = Matrix4f::Identity();
|
||||||
m.data()[0] = scale;
|
m.m[0] = scale;
|
||||||
m.data()[5] = scale;
|
m.m[5] = scale;
|
||||||
m.data()[10] = scale;
|
m.m[10] = scale;
|
||||||
|
|
||||||
m = ModelviewMatrixStack.top() * m;
|
m = ModelviewMatrixStack.top() * m;
|
||||||
|
|
||||||
@@ -695,9 +493,9 @@ namespace FRG {
|
|||||||
void Renderer::ScaleMatrix(const Vector3f& scale)
|
void Renderer::ScaleMatrix(const Vector3f& scale)
|
||||||
{
|
{
|
||||||
Matrix4f m = Matrix4f::Identity();
|
Matrix4f m = Matrix4f::Identity();
|
||||||
m.data()[0] = scale(0);
|
m.m[0] = scale.v[0];
|
||||||
m.data()[5] = scale(1);
|
m.m[5] = scale.v[1];
|
||||||
m.data()[10] = scale(2);
|
m.m[10] = scale.v[2];
|
||||||
|
|
||||||
m = ModelviewMatrixStack.top() * m;
|
m = ModelviewMatrixStack.top() * m;
|
||||||
|
|
||||||
@@ -712,13 +510,22 @@ namespace FRG {
|
|||||||
SetMatrix();
|
SetMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::RotateMatrix(const Eigen::Quaternionf& q)
|
void Renderer::RotateMatrix(const Vector4f& q)
|
||||||
{
|
{
|
||||||
|
|
||||||
Matrix3f m3 = q.toRotationMatrix();
|
Matrix3f m3 = QuatToMatrix(q);
|
||||||
Matrix4f m = Matrix4f::Identity();
|
Matrix4f m = Matrix4f::Identity();
|
||||||
|
m.m[0] = m3.m[0];
|
||||||
|
m.m[1] = m3.m[1];
|
||||||
|
m.m[2] = m3.m[2];
|
||||||
|
|
||||||
m.block<3, 3>(0, 0) = m3;
|
m.m[4] = m3.m[3];
|
||||||
|
m.m[5] = m3.m[4];
|
||||||
|
m.m[6] = m3.m[5];
|
||||||
|
|
||||||
|
m.m[8] = m3.m[6];
|
||||||
|
m.m[9] = m3.m[7];
|
||||||
|
m.m[10] = m3.m[8];
|
||||||
|
|
||||||
m = ModelviewMatrixStack.top() * m;
|
m = ModelviewMatrixStack.top() * m;
|
||||||
|
|
||||||
@@ -734,26 +541,6 @@ namespace FRG {
|
|||||||
SetMatrix();
|
SetMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::RotateMatrix(const Matrix3f& m3)
|
|
||||||
{
|
|
||||||
Matrix4f m = Matrix4f::Identity();
|
|
||||||
|
|
||||||
m.block<3, 3>(0, 0) = m3;
|
|
||||||
|
|
||||||
m = ModelviewMatrixStack.top() * m;
|
|
||||||
|
|
||||||
if (ModelviewMatrixStack.size() == 0)
|
|
||||||
{
|
|
||||||
throw std::runtime_error("Modelview matrix stack underflow!!!!");
|
|
||||||
}
|
|
||||||
|
|
||||||
ModelviewMatrixStack.pop();
|
|
||||||
ModelviewMatrixStack.push(m);
|
|
||||||
|
|
||||||
|
|
||||||
SetMatrix();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::PushSpecialMatrix(const Matrix4f& m)
|
void Renderer::PushSpecialMatrix(const Matrix4f& m)
|
||||||
{
|
{
|
||||||
if (ModelviewMatrixStack.size() > 64)
|
if (ModelviewMatrixStack.size() > 64)
|
||||||
@@ -776,23 +563,27 @@ namespace FRG {
|
|||||||
SetMatrix();
|
SetMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::RenderUniformMatrix3fv(const std::string& uniformName, bool transpose, const float* value)
|
|
||||||
|
void Renderer::EnableVertexAttribArray(const std::string& attribName)
|
||||||
|
{
|
||||||
|
|
||||||
|
auto shader = shaderManager.GetCurrentShader();
|
||||||
|
if (shader->attribList.find(attribName) != shader->attribList.end())
|
||||||
|
glEnableVertexAttribArray(shader->attribList[attribName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::DisableVertexAttribArray(const std::string& attribName)
|
||||||
{
|
{
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
auto shader = shaderManager.GetCurrentShader();
|
||||||
|
if (shader->attribList.find(attribName) != shader->attribList.end())
|
||||||
auto uniform = shader->uniformList.find(uniformName);
|
glDisableVertexAttribArray(shader->attribList[attribName]);
|
||||||
|
|
||||||
if (uniform != shader->uniformList.end())
|
|
||||||
{
|
|
||||||
glUniformMatrix3fv(uniform->second, 1, transpose, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Renderer::RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value)
|
void Renderer::RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value)
|
||||||
{
|
{
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
auto shader = shaderManager.GetCurrentShader();
|
||||||
|
|
||||||
auto uniform = shader->uniformList.find(uniformName);
|
auto uniform = shader->uniformList.find(uniformName);
|
||||||
|
|
||||||
if (uniform != shader->uniformList.end())
|
if (uniform != shader->uniformList.end())
|
||||||
@@ -801,38 +592,6 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::RenderUniformMatrix4fvArray(const std::string& uniformName, int count, bool transpose, const float* value)
|
|
||||||
{
|
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
|
||||||
|
|
||||||
auto uniform = shader->uniformList.find(uniformName);
|
|
||||||
|
|
||||||
if (uniform != shader->uniformList.end())
|
|
||||||
{
|
|
||||||
glUniformMatrix4fv(uniform->second, count, transpose, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::RenderUniform2fv(const std::string& uniformName, const float* value)
|
|
||||||
{
|
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
|
||||||
auto uniform = shader->uniformList.find(uniformName);
|
|
||||||
if (uniform != shader->uniformList.end())
|
|
||||||
{
|
|
||||||
glUniform2fv(uniform->second, 1, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::RenderUniform2fvArray(const std::string& uniformName, int count, const float* value)
|
|
||||||
{
|
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
|
||||||
auto uniform = shader->uniformList.find(uniformName);
|
|
||||||
if (uniform != shader->uniformList.end())
|
|
||||||
{
|
|
||||||
glUniform2fv(uniform->second, count, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::RenderUniform3fv(const std::string& uniformName, const float* value)
|
void Renderer::RenderUniform3fv(const std::string& uniformName, const float* value)
|
||||||
{
|
{
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
auto shader = shaderManager.GetCurrentShader();
|
||||||
@@ -845,25 +604,6 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::RenderUniform3fvArray(const std::string& uniformName, int count, const float* value)
|
|
||||||
{
|
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
|
||||||
auto uniform = shader->uniformList.find(uniformName);
|
|
||||||
if (uniform != shader->uniformList.end())
|
|
||||||
{
|
|
||||||
glUniform3fv(uniform->second, count, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::RenderUniform4fv(const std::string& uniformName, const float* value)
|
|
||||||
{
|
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
|
||||||
auto uniform = shader->uniformList.find(uniformName);
|
|
||||||
if (uniform != shader->uniformList.end()) {
|
|
||||||
glUniform4fv(uniform->second, 1, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::RenderUniform1i(const std::string& uniformName, const int value)
|
void Renderer::RenderUniform1i(const std::string& uniformName, const int value)
|
||||||
{
|
{
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
auto shader = shaderManager.GetCurrentShader();
|
||||||
@@ -877,18 +617,6 @@ namespace FRG {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::RenderUniform1f(const std::string& uniformName, float value)
|
|
||||||
{
|
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
|
||||||
|
|
||||||
auto uniform = shader->uniformList.find(uniformName);
|
|
||||||
|
|
||||||
if (uniform != shader->uniformList.end())
|
|
||||||
{
|
|
||||||
glUniform1fv(uniform->second, 1, &value);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void Renderer::VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer)
|
void Renderer::VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer)
|
||||||
@@ -896,164 +624,60 @@ namespace FRG {
|
|||||||
auto shader = shaderManager.GetCurrentShader();
|
auto shader = shaderManager.GetCurrentShader();
|
||||||
if (shader->attribList.find(attribName) != shader->attribList.end())
|
if (shader->attribList.find(attribName) != shader->attribList.end())
|
||||||
glVertexAttribPointer(shader->attribList[attribName], 2, GL_FLOAT, GL_FALSE, stride, pointer);
|
glVertexAttribPointer(shader->attribList[attribName], 2, GL_FLOAT, GL_FALSE, stride, pointer);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::VertexAttribPointer3fv(const std::string& attribName, int stride, const char* pointer)
|
void Renderer::VertexAttribPointer3fv(const std::string& attribName, int stride, const char* pointer)
|
||||||
{
|
{
|
||||||
|
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
auto shader = shaderManager.GetCurrentShader();
|
||||||
if (shader->attribList.find(attribName) != shader->attribList.end())
|
if (shader->attribList.find(attribName) != shader->attribList.end())
|
||||||
glVertexAttribPointer(shader->attribList[attribName], 3, GL_FLOAT, GL_FALSE, stride, pointer);
|
glVertexAttribPointer(shader->attribList[attribName], 3, GL_FLOAT, GL_FALSE, stride, pointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::VertexAttribPointer4fv(const std::string& attribName, int stride, const char* pointer)
|
|
||||||
{
|
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
|
||||||
if (shader->attribList.find(attribName) != shader->attribList.end())
|
|
||||||
glVertexAttribPointer(shader->attribList[attribName], 4, GL_FLOAT, GL_FALSE, stride, pointer);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::DisableVertexAttribArray(const std::string& attribName)
|
|
||||||
{
|
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
|
||||||
auto it = shader->attribList.find(attribName);
|
|
||||||
if (it != shader->attribList.end())
|
|
||||||
glDisableVertexAttribArray(it->second);
|
|
||||||
}
|
|
||||||
|
|
||||||
void InstancedBatch::RefreshInstanceVBO()
|
|
||||||
{
|
|
||||||
if (instances.empty())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!instanceVBO)
|
|
||||||
{
|
|
||||||
instanceVBO = std::make_shared<VBOHolder>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matrix4f is column-major with no padding: 16 floats per instance.
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO->getBuffer());
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, instances.size() * sizeof(Matrix4f), instances.data(), GL_STATIC_DRAW);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::DrawVertexRenderStruct(const VertexRenderStruct& VertexRenderStruct)
|
void Renderer::DrawVertexRenderStruct(const VertexRenderStruct& VertexRenderStruct)
|
||||||
{
|
{
|
||||||
BindVertexRenderStructAttribs(VertexRenderStruct);
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(VertexRenderStruct.data.PositionData.size()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::DrawInstanced(const InstancedBatch& batch)
|
|
||||||
{
|
|
||||||
if (batch.instances.empty() || !batch.instanceVBO || !batch.mesh.positionVBO)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
BindVertexRenderStructAttribs(batch.mesh);
|
|
||||||
|
|
||||||
static const char* const instanceAttribNames[4] = { "iModel0", "iModel1", "iModel2", "iModel3" };
|
|
||||||
|
|
||||||
auto shader = shaderManager.GetCurrentShader();
|
|
||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, batch.instanceVBO->getBuffer());
|
|
||||||
for (int i = 0; i < 4; i++)
|
|
||||||
{
|
|
||||||
auto it = shader->attribList.find(instanceAttribNames[i]);
|
|
||||||
if (it == shader->attribList.end())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
glVertexAttribPointer(it->second, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f),
|
|
||||||
reinterpret_cast<const void*>(static_cast<size_t>(i) * 4 * sizeof(float)));
|
|
||||||
glVertexAttribDivisor(it->second, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
glDrawArraysInstanced(GL_TRIANGLES, 0,
|
|
||||||
static_cast<GLsizei>(batch.mesh.data.PositionData.size()),
|
|
||||||
static_cast<GLsizei>(batch.instances.size()));
|
|
||||||
|
|
||||||
// The divisor is vertex array state: reset it so that later non-instanced draws
|
|
||||||
// with these attribute locations are not affected.
|
|
||||||
for (int i = 0; i < 4; i++)
|
|
||||||
{
|
|
||||||
auto it = shader->attribList.find(instanceAttribNames[i]);
|
|
||||||
if (it != shader->attribList.end())
|
|
||||||
{
|
|
||||||
glVertexAttribDivisor(it->second, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Renderer::BindVertexRenderStructAttribs(const VertexRenderStruct& VertexRenderStruct)
|
|
||||||
{
|
|
||||||
#ifndef EMSCRIPTEN
|
|
||||||
#ifndef __ANDROID__
|
|
||||||
if (VertexRenderStruct.vao) {
|
|
||||||
glBindVertexArray(VertexRenderStruct.vao->getBuffer());
|
|
||||||
shaderManager.EnableVertexAttribArrays();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static const std::string vNormal("vNormal");
|
static const std::string vNormal("vNormal");
|
||||||
static const std::string vTangent("vTangent");
|
static const std::string vTangent("vTangent");
|
||||||
static const std::string vBinormal("vBinormal");
|
static const std::string vBinormal("vBinormal");
|
||||||
static const std::string vColor("vColor");
|
static const std::string vColor("vColor");
|
||||||
static const std::string vTexCoord("vTexCoord");
|
static const std::string vTexCoord("vTexCoord");
|
||||||
static const std::string vPosition("vPosition");
|
static const std::string vPosition("vPosition");
|
||||||
|
|
||||||
|
//glBindVertexArray(VertexRenderStruct.vao->getBuffer());
|
||||||
|
|
||||||
// On WebGL (and when not using VAO), vertex attribute arrays must be explicitly
|
//Check if main thread, check if data is not empty...
|
||||||
// enabled before drawing. Desktop with VAO can rely on stored state; WebGL cannot.
|
|
||||||
if (VertexRenderStruct.data.NormalData.size() > 0)
|
if (VertexRenderStruct.data.NormalData.size() > 0)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.normalVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.normalVBO->getBuffer());
|
||||||
VertexAttribPointer3fv(vNormal, 0, NULL);
|
VertexAttribPointer3fv(vNormal, 0, NULL);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
DisableVertexAttribArray(vNormal);
|
|
||||||
}
|
|
||||||
if (VertexRenderStruct.data.TangentData.size() > 0)
|
if (VertexRenderStruct.data.TangentData.size() > 0)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.tangentVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.tangentVBO->getBuffer());
|
||||||
VertexAttribPointer3fv(vTangent, 0, NULL);
|
VertexAttribPointer3fv(vTangent, 0, NULL);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
DisableVertexAttribArray(vTangent);
|
|
||||||
}
|
|
||||||
if (VertexRenderStruct.data.BinormalData.size() > 0)
|
if (VertexRenderStruct.data.BinormalData.size() > 0)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.binormalVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.binormalVBO->getBuffer());
|
||||||
VertexAttribPointer3fv(vBinormal, 0, NULL);
|
VertexAttribPointer3fv(vBinormal, 0, NULL);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
DisableVertexAttribArray(vBinormal);
|
|
||||||
}
|
|
||||||
if (VertexRenderStruct.data.ColorData.size() > 0)
|
if (VertexRenderStruct.data.ColorData.size() > 0)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.colorVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.colorVBO->getBuffer());
|
||||||
VertexAttribPointer3fv(vColor, 0, NULL);
|
VertexAttribPointer3fv(vColor, 0, NULL);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
DisableVertexAttribArray(vColor);
|
|
||||||
}
|
|
||||||
if (VertexRenderStruct.data.TexCoordData.size() > 0)
|
if (VertexRenderStruct.data.TexCoordData.size() > 0)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.texCoordVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.texCoordVBO->getBuffer());
|
||||||
VertexAttribPointer2fv(vTexCoord, 0, NULL);
|
VertexAttribPointer2fv(vTexCoord, 0, NULL);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
DisableVertexAttribArray(vTexCoord);
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer());
|
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer());
|
||||||
VertexAttribPointer3fv(vPosition, 0, NULL);
|
VertexAttribPointer3fv(vPosition, 0, NULL);
|
||||||
|
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(VertexRenderStruct.data.PositionData.size()));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void worldToScreenCoordinates(Vector3f objectPos,
|
void worldToScreenCoordinates(Vector3f objectPos,
|
||||||
@@ -1061,11 +685,11 @@ namespace FRG {
|
|||||||
int screenWidth, int screenHeight,
|
int screenWidth, int screenHeight,
|
||||||
int& screenX, int& screenY) {
|
int& screenX, int& screenY) {
|
||||||
|
|
||||||
Vector4f inx = { objectPos(0), objectPos(1), objectPos(2), 1.0f };
|
Vector4f inx = { objectPos.v[0], objectPos.v[1], objectPos.v[2], 1.0f };
|
||||||
Vector4f clipCoords = projectionModelView * inx;
|
Vector4f clipCoords = MultMatrixVector(projectionModelView, inx);
|
||||||
|
|
||||||
float ndcX = clipCoords(0) / clipCoords(3);
|
float ndcX = clipCoords.v[0] / clipCoords.v[3];
|
||||||
float ndcY = clipCoords(1) / clipCoords(3);
|
float ndcY = clipCoords.v[1] / clipCoords.v[3];
|
||||||
|
|
||||||
screenX = (int)((ndcX + 1.0f) * 0.5f * screenWidth);
|
screenX = (int)((ndcX + 1.0f) * 0.5f * screenWidth);
|
||||||
screenY = (int)((1.0f + ndcY) * 0.5f * screenHeight);
|
screenY = (int)((1.0f + ndcY) * 0.5f * screenHeight);
|
||||||
@@ -1,27 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "render/OpenGlExtensions.h"
|
|
||||||
|
|
||||||
|
//#include "OpenGlExtensions.h"
|
||||||
|
#include "Math.h"
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include "ShaderManager.h"
|
#include "ShaderManager.h"
|
||||||
#include "TextureManager.h"
|
#include <GLES2/gl2.h>
|
||||||
#include <Eigen/Dense>
|
#include <GLES2/gl2ext.h>
|
||||||
|
|
||||||
namespace FRG {
|
|
||||||
|
|
||||||
using Eigen::Vector2f;
|
|
||||||
using Eigen::Vector3f;
|
|
||||||
using Eigen::Vector4f;
|
|
||||||
|
|
||||||
using Eigen::Matrix3f;
|
|
||||||
using Eigen::Matrix4f;
|
|
||||||
|
|
||||||
Matrix4f MakeOrthoMatrix(float width, float height, float zNear, float zFar);
|
|
||||||
Matrix4f MakeOrthoMatrix(float xmin, float xmax, float ymin, float ymax, float zNear, float zFar);
|
|
||||||
|
|
||||||
Matrix4f MakePerspectiveMatrix(float fovY, float aspectRatio, float zNear, float zFar);
|
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
constexpr size_t CONST_MATRIX_STACK_SIZE = 64;
|
constexpr size_t CONST_MATRIX_STACK_SIZE = 64;
|
||||||
|
|
||||||
@@ -87,27 +74,9 @@ namespace FRG {
|
|||||||
void AssignFrom(const VertexDataStruct& v);
|
void AssignFrom(const VertexDataStruct& v);
|
||||||
};
|
};
|
||||||
|
|
||||||
// One mesh drawn many times with a per-instance model matrix (glDrawArraysInstanced).
|
|
||||||
// The mesh is stored in local space; each instance matrix is fed to the *_inst shaders
|
|
||||||
// as four vec4 attributes (iModel0..iModel3, divisor 1).
|
|
||||||
struct InstancedBatch
|
|
||||||
{
|
|
||||||
std::string name;
|
|
||||||
VertexRenderStruct mesh;
|
|
||||||
std::shared_ptr<Texture> texture;
|
|
||||||
std::shared_ptr<Texture> textureDarklands;
|
|
||||||
|
|
||||||
std::vector<Matrix4f> instances;
|
|
||||||
std::shared_ptr<VBOHolder> instanceVBO;
|
|
||||||
|
|
||||||
void RefreshInstanceVBO();
|
|
||||||
};
|
|
||||||
|
|
||||||
VertexDataStruct CreateRect2D(Vector2f center, Vector2f halfWidthHeight, float zLevel);
|
VertexDataStruct CreateRect2D(Vector2f center, Vector2f halfWidthHeight, float zLevel);
|
||||||
VertexDataStruct CreatePolygonFloor(const std::vector<Eigen::Vector2f>& polygon, float yLevel, const Vector3f& color);
|
|
||||||
VertexDataStruct CreateRectHorizontalSections2D(Vector2f center, Vector2f halfWidthHeight, float zLevel, size_t sectionCount);
|
VertexDataStruct CreateRectHorizontalSections2D(Vector2f center, Vector2f halfWidthHeight, float zLevel, size_t sectionCount);
|
||||||
VertexDataStruct CreateCube3D(float scale);
|
VertexDataStruct CreateCube3D(float scale);
|
||||||
VertexDataStruct CreateCubemap(float scale = 1000.f);
|
|
||||||
|
|
||||||
|
|
||||||
class Renderer
|
class Renderer
|
||||||
@@ -121,12 +90,10 @@ namespace FRG {
|
|||||||
public:
|
public:
|
||||||
|
|
||||||
ShaderManager shaderManager;
|
ShaderManager shaderManager;
|
||||||
TextureManager textureManager;
|
|
||||||
|
|
||||||
void InitOpenGL();
|
void InitOpenGL();
|
||||||
|
|
||||||
void PushProjectionMatrix(float width, float height, float zNear = 0.f, float zFar = 1.f);
|
void PushProjectionMatrix(float width, float height, float zNear = 0.f, float zFar = 1.f);
|
||||||
void PushProjectionMatrix(float xmin, float xmax, float ymin, float ymax, float zNear, float zFar);
|
|
||||||
void PushPerspectiveProjectionMatrix(float fovY, float aspectRatio, float zNear, float zFar);
|
void PushPerspectiveProjectionMatrix(float fovY, float aspectRatio, float zNear, float zFar);
|
||||||
void PopProjectionMatrix();
|
void PopProjectionMatrix();
|
||||||
|
|
||||||
@@ -135,43 +102,32 @@ namespace FRG {
|
|||||||
void TranslateMatrix(const Vector3f& p);
|
void TranslateMatrix(const Vector3f& p);
|
||||||
void ScaleMatrix(float scale);
|
void ScaleMatrix(float scale);
|
||||||
void ScaleMatrix(const Vector3f& scale);
|
void ScaleMatrix(const Vector3f& scale);
|
||||||
void RotateMatrix(const Eigen::Quaternionf& q);
|
void RotateMatrix(const Vector4f& q);
|
||||||
void RotateMatrix(const Matrix3f& m3);
|
|
||||||
void PushSpecialMatrix(const Matrix4f& m);
|
void PushSpecialMatrix(const Matrix4f& m);
|
||||||
void PopMatrix();
|
void PopMatrix();
|
||||||
|
|
||||||
|
|
||||||
Matrix4f GetProjectionModelViewMatrix();
|
Matrix4f GetProjectionModelViewMatrix();
|
||||||
Matrix4f GetCurrentModelViewMatrix();
|
|
||||||
|
|
||||||
void SetMatrix();
|
void SetMatrix();
|
||||||
|
|
||||||
void RenderUniformMatrix3fv(const std::string& uniformName, bool transpose, const float* value);
|
|
||||||
|
|
||||||
|
void EnableVertexAttribArray(const std::string& attribName);
|
||||||
|
|
||||||
|
void DisableVertexAttribArray(const std::string& attribName);
|
||||||
|
|
||||||
|
|
||||||
void RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value);
|
void RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value);
|
||||||
void RenderUniformMatrix4fvArray(const std::string& uniformName, int count, bool transpose, const float* value);
|
|
||||||
void RenderUniform1i(const std::string& uniformName, const int value);
|
void RenderUniform1i(const std::string& uniformName, const int value);
|
||||||
void RenderUniform2fv(const std::string& uniformName, const float* value);
|
|
||||||
void RenderUniform2fvArray(const std::string& uniformName, int count, const float* value);
|
|
||||||
void RenderUniform3fv(const std::string& uniformName, const float* value);
|
void RenderUniform3fv(const std::string& uniformName, const float* value);
|
||||||
void RenderUniform3fvArray(const std::string& uniformName, int count, const float* value);
|
|
||||||
void RenderUniform4fv(const std::string& uniformName, const float* value);
|
|
||||||
void RenderUniform1f(const std::string& uniformName, float value);
|
|
||||||
|
|
||||||
void VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer);
|
void VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer);
|
||||||
|
|
||||||
void VertexAttribPointer3fv(const std::string& attribName, int stride, const char* pointer);
|
void VertexAttribPointer3fv(const std::string& attribName, int stride, const char* pointer);
|
||||||
|
|
||||||
void VertexAttribPointer4fv(const std::string& attribName, int stride, const char* pointer);
|
|
||||||
|
|
||||||
void DisableVertexAttribArray(const std::string& attribName);
|
|
||||||
|
|
||||||
void DrawVertexRenderStruct(const VertexRenderStruct& VertexRenderStruct);
|
void DrawVertexRenderStruct(const VertexRenderStruct& VertexRenderStruct);
|
||||||
|
|
||||||
// The current shader must be an instancing variant (compiled with INSTANCED).
|
|
||||||
void DrawInstanced(const InstancedBatch& batch);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void BindVertexRenderStructAttribs(const VertexRenderStruct& VertexRenderStruct);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "ShaderManager.h"
|
#include "ShaderManager.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
@@ -6,18 +6,15 @@
|
|||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace FRG {
|
namespace ZL {
|
||||||
|
|
||||||
ShaderResource::ShaderResource(const std::string &vertexCode, const std::string &fragmentCode) {
|
ShaderResource::ShaderResource(const std::string &vertexCode, const std::string &fragmentCode) {
|
||||||
|
|
||||||
logger() << "Started creating shader resource" << std::endl;
|
|
||||||
|
|
||||||
const int CONST_INFOLOG_LENGTH = 256;
|
const int CONST_INFOLOG_LENGTH = 256;
|
||||||
|
|
||||||
char infoLog[CONST_INFOLOG_LENGTH];
|
char infoLog[CONST_INFOLOG_LENGTH];
|
||||||
int infoLogLength;
|
int infoLogLength;
|
||||||
char infoLog2[CONST_INFOLOG_LENGTH];
|
|
||||||
int infoLogLength2;
|
|
||||||
int vertexShaderCompiled;
|
int vertexShaderCompiled;
|
||||||
int fragmentShaderCompiled;
|
int fragmentShaderCompiled;
|
||||||
int programLinked;
|
int programLinked;
|
||||||
@@ -43,36 +40,24 @@ namespace FRG {
|
|||||||
|
|
||||||
glCompileShader(fragmentShader);
|
glCompileShader(fragmentShader);
|
||||||
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompiled);
|
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompiled);
|
||||||
glGetShaderInfoLog(fragmentShader, CONST_INFOLOG_LENGTH, &infoLogLength2, infoLog2);
|
glGetShaderInfoLog(fragmentShader, CONST_INFOLOG_LENGTH, &infoLogLength, infoLog);
|
||||||
|
|
||||||
//logger() << "Creating shader resource step 1" << std::endl;
|
|
||||||
|
|
||||||
|
|
||||||
if (!vertexShaderCompiled) {
|
if (!vertexShaderCompiled) {
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "ShaderManager",
|
__android_log_print(ANDROID_LOG_ERROR, "ShaderManager",
|
||||||
"Failed to compile vertex shader: %s", infoLog);
|
"Failed to compile vertex shader: %s", infoLog);
|
||||||
#endif
|
#endif
|
||||||
logger() << "Vertex shader compilation failed: " << infoLog << std::endl;
|
|
||||||
throw std::runtime_error("Failed to compile vertex shader code!");
|
throw std::runtime_error("Failed to compile vertex shader code!");
|
||||||
}
|
}
|
||||||
|
|
||||||
//logger() << "Creating shader resource step 2" << std::endl;
|
|
||||||
|
|
||||||
|
|
||||||
if (!fragmentShaderCompiled) {
|
if (!fragmentShaderCompiled) {
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "ShaderManager",
|
__android_log_print(ANDROID_LOG_ERROR, "ShaderManager",
|
||||||
"Failed to compile fragment shader: %s", infoLog);
|
"Failed to compile fragment shader: %s", infoLog);
|
||||||
#endif
|
#endif
|
||||||
logger() << "Fragment shader compilation failed: " << infoLog << std::endl;
|
|
||||||
|
|
||||||
throw std::runtime_error("Failed to compile fragment shader code!");
|
throw std::runtime_error("Failed to compile fragment shader code!");
|
||||||
}
|
}
|
||||||
|
|
||||||
//logger() << "Creating shader resource step 4" << std::endl;
|
|
||||||
|
|
||||||
|
|
||||||
shaderProgram = glCreateProgram();
|
shaderProgram = glCreateProgram();
|
||||||
|
|
||||||
glAttachShader(shaderProgram, vertexShader);
|
glAttachShader(shaderProgram, vertexShader);
|
||||||
@@ -86,21 +71,15 @@ namespace FRG {
|
|||||||
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programLinked);
|
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programLinked);
|
||||||
glGetProgramInfoLog(shaderProgram, CONST_INFOLOG_LENGTH, &infoLogLength, infoLog);
|
glGetProgramInfoLog(shaderProgram, CONST_INFOLOG_LENGTH, &infoLogLength, infoLog);
|
||||||
|
|
||||||
//logger() << "Creating shader resource step 4" << std::endl;
|
|
||||||
|
|
||||||
if (!programLinked) {
|
if (!programLinked) {
|
||||||
shaderProgram = 0;
|
shaderProgram = 0;
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "ShaderManager",
|
__android_log_print(ANDROID_LOG_ERROR, "ShaderManager",
|
||||||
"Failed to link shader program: %s", infoLog);
|
"Failed to link shader program: %s", infoLog);
|
||||||
#endif
|
#endif
|
||||||
logger() << "Failed to link shader program: " << infoLog << std::endl;
|
|
||||||
throw std::runtime_error("Failed to link shader program!");
|
throw std::runtime_error("Failed to link shader program!");
|
||||||
}
|
}
|
||||||
|
|
||||||
//logger() << "Creating shader resource step 5" << std::endl;
|
|
||||||
|
|
||||||
|
|
||||||
//================= Parsing all uniforms ================
|
//================= Parsing all uniforms ================
|
||||||
|
|
||||||
int dummySize; //Dummy
|
int dummySize; //Dummy
|
||||||
@@ -121,9 +100,6 @@ namespace FRG {
|
|||||||
uniformList[uniformName] = glGetUniformLocation(shaderProgram, uniformName);
|
uniformList[uniformName] = glGetUniformLocation(shaderProgram, uniformName);
|
||||||
}
|
}
|
||||||
|
|
||||||
//logger() << "Creating shader resource step 6" << std::endl;
|
|
||||||
|
|
||||||
|
|
||||||
//================= Parsing all attributes ================
|
//================= Parsing all attributes ================
|
||||||
int activeAttribs;
|
int activeAttribs;
|
||||||
|
|
||||||
@@ -138,12 +114,9 @@ namespace FRG {
|
|||||||
attribList[attribName] = glGetAttribLocation(shaderProgram, attribName);
|
attribList[attribName] = glGetAttribLocation(shaderProgram, attribName);
|
||||||
}
|
}
|
||||||
|
|
||||||
//logger() << "Creating shader resource step 7" << std::endl;
|
|
||||||
|
|
||||||
|
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
|
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
|
||||||
"Shader created successfully, program ID: %u", shaderProgram);
|
"Shader created successfully, program ID: %u", shaderProgram);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,47 +133,19 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static std::string insertDefinesAfterVersion(const std::string &source, const std::string &defines) {
|
|
||||||
if (defines.empty()) {
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t versionPos = source.find("#version");
|
|
||||||
if (versionPos == std::string::npos) {
|
|
||||||
return defines + source;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t lineEnd = source.find('\n', versionPos);
|
|
||||||
if (lineEnd == std::string::npos) {
|
|
||||||
return source + "\n" + defines;
|
|
||||||
}
|
|
||||||
|
|
||||||
return source.substr(0, lineEnd + 1) + defines + source.substr(lineEnd + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderManager::AddShaderFromFiles(const std::string &shaderName,
|
void ShaderManager::AddShaderFromFiles(const std::string &shaderName,
|
||||||
const std::string &vertexShaderFileName,
|
const std::string &vertexShaderFileName,
|
||||||
const std::string &fragmentShaderFileName,
|
const std::string &fragmentShaderFileName,
|
||||||
const std::string &ZIPFileName) {
|
const std::string &ZIPFileName) {
|
||||||
AddShaderFromFiles(shaderName, vertexShaderFileName, fragmentShaderFileName, ZIPFileName, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderManager::AddShaderFromFiles(const std::string &shaderName,
|
|
||||||
const std::string &vertexShaderFileName,
|
|
||||||
const std::string &fragmentShaderFileName,
|
|
||||||
const std::string &ZIPFileName,
|
|
||||||
const std::string &vertexDefines) {
|
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
|
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
|
||||||
"Loading shader: %s", shaderName.c_str());
|
"Loading shader: %s", shaderName.c_str());
|
||||||
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
|
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
|
||||||
"Vertex shader: %s", vertexShaderFileName.c_str());
|
"Vertex shader: %s", vertexShaderFileName.c_str());
|
||||||
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
|
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
|
||||||
"Fragment shader: %s", fragmentShaderFileName.c_str());
|
"Fragment shader: %s", fragmentShaderFileName.c_str());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
logger() <<"Loading shader: " << shaderName << " " << vertexShaderFileName << " " << fragmentShaderFileName <<std::endl;
|
|
||||||
|
|
||||||
std::string vertexShader;
|
std::string vertexShader;
|
||||||
std::string fragmentShader;
|
std::string fragmentShader;
|
||||||
|
|
||||||
@@ -220,8 +165,7 @@ namespace FRG {
|
|||||||
fragmentShader = readTextFile(fragmentShaderFileName);
|
fragmentShader = readTextFile(fragmentShaderFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
vertexShader = insertDefinesAfterVersion(vertexShader, vertexDefines);
|
///std::cout << "Shader: "<< vertexShader << std::endl;
|
||||||
|
|
||||||
shaderResourceMap[shaderName] = std::make_shared<ShaderResource>(vertexShader,
|
shaderResourceMap[shaderName] = std::make_shared<ShaderResource>(vertexShader,
|
||||||
fragmentShader);
|
fragmentShader);
|
||||||
}
|
}
|
||||||
@@ -232,30 +176,12 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shaderResourceMap.find(shaderName) == shaderResourceMap.end()) {
|
if (shaderResourceMap.find(shaderName) == shaderResourceMap.end()) {
|
||||||
logger() << "Shader does not exist: " << shaderName << std::endl;
|
|
||||||
throw std::runtime_error("Shader does not exist!");
|
throw std::runtime_error("Shader does not exist!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shaderStack.size() > 0)
|
|
||||||
{
|
|
||||||
auto& prevShaderAttribList = shaderResourceMap[shaderStack.top()]->attribList;
|
|
||||||
for (auto& attrib : prevShaderAttribList)
|
|
||||||
{
|
|
||||||
glDisableVertexAttribArray(attrib.second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shaderStack.push(shaderName);
|
shaderStack.push(shaderName);
|
||||||
|
|
||||||
auto& shaderResource = shaderResourceMap[shaderName];
|
glUseProgram(shaderResourceMap[shaderName]->getShaderProgram());
|
||||||
|
|
||||||
glUseProgram(shaderResource->getShaderProgram());
|
|
||||||
|
|
||||||
auto& shaderAttribList = shaderResource->attribList;
|
|
||||||
for (auto& attrib : shaderAttribList)
|
|
||||||
{
|
|
||||||
glEnableVertexAttribArray(attrib.second);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -264,39 +190,12 @@ namespace FRG {
|
|||||||
throw std::runtime_error("Shader stack underflow!");
|
throw std::runtime_error("Shader stack underflow!");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& prevShaderAttribList = shaderResourceMap[shaderStack.top()]->attribList;
|
|
||||||
for (auto& attrib : prevShaderAttribList)
|
|
||||||
{
|
|
||||||
glDisableVertexAttribArray(attrib.second);
|
|
||||||
}
|
|
||||||
|
|
||||||
shaderStack.pop();
|
shaderStack.pop();
|
||||||
|
|
||||||
if (shaderStack.size() == 0) {
|
if (shaderStack.size() == 0) {
|
||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
} else {
|
} else {
|
||||||
auto& shaderResource = shaderResourceMap[shaderStack.top()];
|
glUseProgram(shaderResourceMap[shaderStack.top()]->getShaderProgram());
|
||||||
|
|
||||||
glUseProgram(shaderResource->getShaderProgram());
|
|
||||||
|
|
||||||
auto& shaderAttribList = shaderResource->attribList;
|
|
||||||
for (auto& attrib : shaderAttribList)
|
|
||||||
{
|
|
||||||
glEnableVertexAttribArray(attrib.second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderManager::EnableVertexAttribArrays()
|
|
||||||
{
|
|
||||||
if (shaderStack.size() != 0) {
|
|
||||||
auto& shaderResource = shaderResourceMap[shaderStack.top()];
|
|
||||||
|
|
||||||
auto& shaderAttribList = shaderResource->attribList;
|
|
||||||
for (auto& attrib : shaderAttribList)
|
|
||||||
{
|
|
||||||
glEnableVertexAttribArray(attrib.second);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
//#include "OpenGlExtensions.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
#include <GLES2/gl2.h>
|
||||||
|
#include <GLES2/gl2ext.h>
|
||||||
|
|
||||||
#include "render/OpenGlExtensions.h"
|
namespace ZL {
|
||||||
#include "utils/Utils.h"
|
|
||||||
|
|
||||||
namespace FRG {
|
|
||||||
|
|
||||||
|
|
||||||
constexpr size_t CONST_MAX_SHADER_STACK_SIZE = 16;
|
constexpr size_t CONST_MAX_SHADER_STACK_SIZE = 16;
|
||||||
@@ -14,6 +15,8 @@ namespace FRG {
|
|||||||
GLuint shaderProgram;
|
GLuint shaderProgram;
|
||||||
|
|
||||||
std::unordered_map<std::string, GLuint> uniformList;
|
std::unordered_map<std::string, GLuint> uniformList;
|
||||||
|
|
||||||
|
//std::unordered_map<std::string, std::pair<bool, size_t>> UniformList;
|
||||||
std::map<std::string, GLuint> attribList;
|
std::map<std::string, GLuint> attribList;
|
||||||
|
|
||||||
|
|
||||||
@@ -38,13 +41,8 @@ namespace FRG {
|
|||||||
public:
|
public:
|
||||||
void AddShaderFromFiles(const std::string& shaderName, const std::string& vertexShaderFileName, const std::string& fragmentShaderFileName, const std::string& ZIPFileName = "");
|
void AddShaderFromFiles(const std::string& shaderName, const std::string& vertexShaderFileName, const std::string& fragmentShaderFileName, const std::string& ZIPFileName = "");
|
||||||
|
|
||||||
// Same as above, but vertexDefines (e.g. "#define INSTANCED\n") is inserted into the
|
|
||||||
// vertex shader source right after its #version line (or at the very top if it has none).
|
|
||||||
void AddShaderFromFiles(const std::string& shaderName, const std::string& vertexShaderFileName, const std::string& fragmentShaderFileName, const std::string& ZIPFileName, const std::string& vertexDefines);
|
|
||||||
|
|
||||||
void PushShader(const std::string& shaderName);
|
void PushShader(const std::string& shaderName);
|
||||||
void PopShader();
|
void PopShader();
|
||||||
void EnableVertexAttribArrays();
|
|
||||||
|
|
||||||
std::shared_ptr<ShaderResource> GetCurrentShader();
|
std::shared_ptr<ShaderResource> GetCurrentShader();
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
#include "TextModel.h"
|
||||||
|
#include <regex>
|
||||||
|
#include <string>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
#include <android/log.h>
|
||||||
|
#endif
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
|
||||||
|
VertexDataStruct LoadFromTextFile(const std::string &fileName, const std::string &ZIPFileName) {
|
||||||
|
VertexDataStruct result;
|
||||||
|
std::ifstream filestream;
|
||||||
|
std::istringstream zipStream;
|
||||||
|
|
||||||
|
if (!ZIPFileName.empty()) {
|
||||||
|
std::vector<char> fileData = readFileFromZIP(fileName, ZIPFileName);
|
||||||
|
std::string fileContents(fileData.begin(), fileData.end());
|
||||||
|
zipStream.str(fileContents);
|
||||||
|
} else {
|
||||||
|
filestream.open(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем ссылку f на нужный поток – после этого код ниже остается без изменений
|
||||||
|
std::istream &f = (!ZIPFileName.empty()) ? static_cast<std::istream &>(zipStream)
|
||||||
|
: static_cast<std::istream &>(filestream);
|
||||||
|
|
||||||
|
|
||||||
|
//Skip first 5 lines
|
||||||
|
std::string tempLine;
|
||||||
|
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
static const std::regex pattern_count(R"(\d+)");
|
||||||
|
static const std::regex pattern_float(R"([-]?\d+\.\d+)");
|
||||||
|
static const std::regex pattern_int(R"([-]?\d+)");
|
||||||
|
|
||||||
|
|
||||||
|
std::smatch match;
|
||||||
|
|
||||||
|
int numberVertices;
|
||||||
|
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
numberVertices = std::stoi(number_str);
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector <Vector3f> vertices;
|
||||||
|
|
||||||
|
vertices.resize(numberVertices);
|
||||||
|
for (int i = 0; i < numberVertices; i++) {
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
std::vector<float> floatValues;
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
vertices[i] = Vector3f{floatValues[0], floatValues[1], floatValues[2]};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "UV Coordinates" << std::endl;
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //===UV Coordinates:
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //triangle count
|
||||||
|
int numberTriangles;
|
||||||
|
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
numberTriangles = std::stoi(number_str);
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Now process UVs
|
||||||
|
std::vector <std::array<Vector2f, 3>> uvCoords;
|
||||||
|
|
||||||
|
uvCoords.resize(numberTriangles);
|
||||||
|
|
||||||
|
for (int i = 0; i < numberTriangles; i++) {
|
||||||
|
std::getline(f, tempLine); //Face 0
|
||||||
|
|
||||||
|
int uvCount;
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
std::string number_str = match.str();
|
||||||
|
uvCount = std::stoi(number_str);
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("No number found in the input string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uvCount != 3) {
|
||||||
|
throw std::runtime_error("more than 3 uvs");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> floatValues;
|
||||||
|
|
||||||
|
for (int j = 0; j < 3; j++) {
|
||||||
|
std::getline(f, tempLine); //UV <Vector (-0.3661, -1.1665)>
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
floatValues.clear();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (floatValues.size() != 2) {
|
||||||
|
throw std::runtime_error("more than 2 uvs---");
|
||||||
|
}
|
||||||
|
|
||||||
|
uvCoords[i][j] = Vector2f{floatValues[0], floatValues[1]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Normals go" << std::endl;
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //===Normals:
|
||||||
|
|
||||||
|
|
||||||
|
std::vector <Vector3f> normals;
|
||||||
|
|
||||||
|
normals.resize(numberVertices);
|
||||||
|
for (int i = 0; i < numberVertices; i++) {
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
std::vector<float> floatValues;
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
normals[i] = Vector3f{floatValues[0], floatValues[1], floatValues[2]};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Triangles go:" << std::endl;
|
||||||
|
|
||||||
|
std::getline(f, tempLine); //===Triangles: 3974
|
||||||
|
|
||||||
|
|
||||||
|
std::vector <std::array<int, 3>> triangles;
|
||||||
|
|
||||||
|
triangles.resize(numberTriangles);
|
||||||
|
for (int i = 0; i < numberTriangles; i++) {
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
|
||||||
|
std::vector<int> intValues;
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_int)) {
|
||||||
|
intValues.push_back(std::stoi(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
triangles[i] = {intValues[0], intValues[1], intValues[2]};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::cout << "Process vertices" << std::endl;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Now let's process vertices
|
||||||
|
|
||||||
|
for (int i = 0; i < numberTriangles; i++) {
|
||||||
|
|
||||||
|
result.PositionData.push_back(vertices[triangles[i][0]]);
|
||||||
|
result.PositionData.push_back(vertices[triangles[i][1]]);
|
||||||
|
result.PositionData.push_back(vertices[triangles[i][2]]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
result.NormalData.push_back(normals[triangles[i][0]]);
|
||||||
|
result.NormalData.push_back(normals[triangles[i][1]]);
|
||||||
|
result.NormalData.push_back(normals[triangles[i][2]]);
|
||||||
|
*/
|
||||||
|
result.TexCoordData.push_back(uvCoords[i][0]);
|
||||||
|
result.TexCoordData.push_back(uvCoords[i][1]);
|
||||||
|
result.TexCoordData.push_back(uvCoords[i][2]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//Swap from Blender format to OpenGL format
|
||||||
|
for (int i = 0; i < result.PositionData.size(); i++) {
|
||||||
|
Vector3f tempVec = result.PositionData[i];
|
||||||
|
result.PositionData[i].v[0] = tempVec.v[1];
|
||||||
|
result.PositionData[i].v[1] = tempVec.v[2];
|
||||||
|
result.PositionData[i].v[2] = tempVec.v[0];
|
||||||
|
|
||||||
|
/*
|
||||||
|
tempVec = result.NormalData[i];
|
||||||
|
result.NormalData[i].v[0] = tempVec.v[1];
|
||||||
|
result.NormalData[i].v[1] = tempVec.v[2];
|
||||||
|
result.NormalData[i].v[2] = tempVec.v[0];*/
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
VertexDataStruct
|
||||||
|
LoadFromTextFile02(const std::string &fileName, const std::string &ZIPFileName) {
|
||||||
|
VertexDataStruct result;
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
// Для Android используем SDL_RWops для чтения файлов
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "TextModel",
|
||||||
|
"LoadFromTextFile02 called for Android: %s", fileName.c_str());
|
||||||
|
|
||||||
|
// Читаем файл с помощью readTextFile
|
||||||
|
std::string fileContent = readTextFile(fileName);
|
||||||
|
if (fileContent.empty()) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "TextModel",
|
||||||
|
"Failed to read file: %s", fileName.c_str());
|
||||||
|
throw std::runtime_error("Failed to read file: " + fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "TextModel",
|
||||||
|
"File read successfully, size: %zu", fileContent.size());
|
||||||
|
|
||||||
|
std::istringstream f(fileContent);
|
||||||
|
#else
|
||||||
|
// Оригинальный код для десктопа
|
||||||
|
std::ifstream filestream;
|
||||||
|
std::istringstream zipStream;
|
||||||
|
|
||||||
|
if (!ZIPFileName.empty()) {
|
||||||
|
std::vector<char> fileData = readFileFromZIP(fileName, ZIPFileName);
|
||||||
|
std::string fileContents(fileData.begin(), fileData.end());
|
||||||
|
zipStream.str(fileContents);
|
||||||
|
f = static_cast<std::istream &>(zipStream);
|
||||||
|
} else {
|
||||||
|
filestream.open(fileName);
|
||||||
|
if (!filestream.is_open()) {
|
||||||
|
throw std::runtime_error("Failed to open file: " + fileName);
|
||||||
|
}
|
||||||
|
f = static_cast<std::istream &>(filestream);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::string tempLine;
|
||||||
|
std::smatch match;
|
||||||
|
|
||||||
|
// Обновленные регулярки
|
||||||
|
static const std::regex pattern_count(R"(\d+)");
|
||||||
|
static const std::regex pattern_float(R"([-]?\d+(\.\d+)?)");
|
||||||
|
static const std::regex pattern_int(R"([-]?\d+)");
|
||||||
|
|
||||||
|
// --- 2. Парсинг Вершин (Pos + Norm + UV) ---
|
||||||
|
|
||||||
|
// Ищем заголовок ===Vertices
|
||||||
|
while (std::getline(f, tempLine)) {
|
||||||
|
if (tempLine.find("===Vertices") != std::string::npos) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int numberVertices = 0;
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
numberVertices = std::stoi(match.str());
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("Vertices header not found or invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Временные буферы для хранения "уникальных" вершин перед разверткой по индексам
|
||||||
|
std::vector <Vector3f> tempPositions(numberVertices);
|
||||||
|
std::vector <Vector3f> tempNormals(numberVertices);
|
||||||
|
std::vector <Vector2f> tempUVs(numberVertices);
|
||||||
|
|
||||||
|
for (int i = 0; i < numberVertices; i++) {
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
// Строка вида: V 0: Pos(x, y, z) Norm(x, y, z) UV(u, v)
|
||||||
|
|
||||||
|
std::vector<float> floatValues;
|
||||||
|
floatValues.reserve(9); // ID + 3 pos + 3 norm + 2 uv
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_float)) {
|
||||||
|
floatValues.push_back(std::stof(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка, что у нас достаточно данных
|
||||||
|
if (floatValues.size() < 9) {
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
__android_log_print(ANDROID_LOG_WARN, "TextModel",
|
||||||
|
"Warning: vertex line has only %zu values, expected 9",
|
||||||
|
floatValues.size());
|
||||||
|
#endif
|
||||||
|
// Попробуем с меньшим количеством, возможно данные упрощенные
|
||||||
|
if (floatValues.size() >= 8) {
|
||||||
|
// Без ID
|
||||||
|
tempPositions[i] = Vector3f{floatValues[0], floatValues[1], floatValues[2]};
|
||||||
|
tempNormals[i] = Vector3f{floatValues[3], floatValues[4], floatValues[5]};
|
||||||
|
tempUVs[i] = Vector2f{floatValues[6], floatValues[7]};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw std::runtime_error("Malformed vertex line at index " + std::to_string(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - floatValues[0]
|
||||||
|
// Pos - [1], [2], [3]
|
||||||
|
// Norm - [4], [5], [6]
|
||||||
|
// UV - [7], [8]
|
||||||
|
tempPositions[i] = Vector3f{floatValues[1], floatValues[2], floatValues[3]};
|
||||||
|
tempNormals[i] = Vector3f{floatValues[4], floatValues[5], floatValues[6]};
|
||||||
|
tempUVs[i] = Vector2f{floatValues[7], floatValues[8]};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. Парсинг Треугольников (Индексов) ---
|
||||||
|
|
||||||
|
// Пропускаем пустые строки до заголовка треугольников
|
||||||
|
while (std::getline(f, tempLine)) {
|
||||||
|
if (tempLine.find("===Triangles") != std::string::npos) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int numberTriangles = 0;
|
||||||
|
if (std::regex_search(tempLine, match, pattern_count)) {
|
||||||
|
numberTriangles = std::stoi(match.str());
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("Triangles header not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Резервируем память в result, чтобы избежать лишних аллокаций
|
||||||
|
result.PositionData.reserve(numberTriangles * 3);
|
||||||
|
result.NormalData.reserve(numberTriangles * 3);
|
||||||
|
result.TexCoordData.reserve(numberTriangles * 3);
|
||||||
|
|
||||||
|
for (int i = 0; i < numberTriangles; i++) {
|
||||||
|
std::getline(f, tempLine);
|
||||||
|
// Строка вида: Tri: 0 1 2
|
||||||
|
|
||||||
|
std::vector<int> indices;
|
||||||
|
indices.reserve(3);
|
||||||
|
|
||||||
|
auto b = tempLine.cbegin();
|
||||||
|
auto e = tempLine.cend();
|
||||||
|
while (std::regex_search(b, e, match, pattern_int)) {
|
||||||
|
indices.push_back(std::stoi(match.str()));
|
||||||
|
b = match.suffix().first;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indices.size() != 3) {
|
||||||
|
throw std::runtime_error("Malformed triangle line at index " + std::to_string(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. Заполнение VertexDataStruct (Flattening) ---
|
||||||
|
for (int k = 0; k < 3; k++) {
|
||||||
|
int idx = indices[k];
|
||||||
|
result.PositionData.push_back(tempPositions[idx]);
|
||||||
|
result.NormalData.push_back(tempNormals[idx]);
|
||||||
|
result.TexCoordData.push_back(tempUVs[idx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 5. Конвертация координат (Blender -> OpenGL/Engine) ---
|
||||||
|
for (size_t i = 0; i < result.PositionData.size(); i++) {
|
||||||
|
Vector3f originalPos = result.PositionData[i];
|
||||||
|
result.PositionData[i].v[0] = originalPos.v[1]; // New X = Old Y
|
||||||
|
result.PositionData[i].v[1] = originalPos.v[2]; // New Y = Old Z
|
||||||
|
result.PositionData[i].v[2] = originalPos.v[0]; // New Z = Old X
|
||||||
|
|
||||||
|
Vector3f originalNorm = result.NormalData[i];
|
||||||
|
result.NormalData[i].v[0] = originalNorm.v[1];
|
||||||
|
result.NormalData[i].v[1] = originalNorm.v[2];
|
||||||
|
result.NormalData[i].v[2] = originalNorm.v[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "TextModel",
|
||||||
|
"Model loaded successfully: %d verts, %d tris",
|
||||||
|
numberVertices, numberTriangles);
|
||||||
|
#else
|
||||||
|
std::cout << "Model loaded: " << numberVertices << " verts, " << numberTriangles << " tris."
|
||||||
|
<< std::endl;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Math.h"
|
||||||
|
#include "Renderer.h"
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
#include <android/log.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
VertexDataStruct
|
||||||
|
LoadFromTextFile(const std::string &fileName, const std::string &ZIPFileName = "");
|
||||||
|
|
||||||
|
VertexDataStruct
|
||||||
|
LoadFromTextFile02(const std::string &fileName, const std::string &ZIPFileName = "");
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
#include "TextureManager.h"
|
||||||
|
#ifdef PNG_ENABLED
|
||||||
|
#include "png.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ZL
|
||||||
|
{
|
||||||
|
|
||||||
|
Texture::Texture(const TextureDataStruct& texData)
|
||||||
|
{
|
||||||
|
|
||||||
|
width = texData.width;
|
||||||
|
height = texData.height;
|
||||||
|
|
||||||
|
glGenTextures(1, &texID);
|
||||||
|
|
||||||
|
if (texID == 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("glGenTextures did not work");
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texID);
|
||||||
|
|
||||||
|
//CheckGlError();
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
//CheckGlError();
|
||||||
|
|
||||||
|
//This should be only for Windows
|
||||||
|
//glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
|
||||||
|
|
||||||
|
//CheckGlError();
|
||||||
|
|
||||||
|
if (texData.bitSize == TextureDataStruct::BS_24BIT)
|
||||||
|
{
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, static_cast<GLsizei>(texData.width), static_cast<GLsizei>(texData.height), 0, GL_RGB, GL_UNSIGNED_BYTE, &texData.data[0]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<GLsizei>(texData.width), static_cast<GLsizei>(texData.height), 0, GL_RGBA, GL_UNSIGNED_BYTE, &texData.data[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//CheckGlError();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Texture::~Texture()
|
||||||
|
{
|
||||||
|
glDeleteTextures(1, &texID);
|
||||||
|
texID = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint Texture::getTexID()
|
||||||
|
{
|
||||||
|
return texID;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Texture::getWidth()
|
||||||
|
{
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Texture::getHeight()
|
||||||
|
{
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
TextureDataStruct CreateTextureDataFromBmp24(const std::string& fullFileName, const std::string& ZIPFileName)
|
||||||
|
{
|
||||||
|
|
||||||
|
TextureDataStruct texData;
|
||||||
|
std::vector<char> fileArr;
|
||||||
|
|
||||||
|
fileArr = !ZIPFileName.empty() ? readFileFromZIP(fullFileName, ZIPFileName) : readFile(fullFileName);
|
||||||
|
|
||||||
|
size_t fileSize = fileArr.size();
|
||||||
|
|
||||||
|
if (fileSize < 22)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("File is too short or not correct!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//This refers to BITMAPV5HEADER
|
||||||
|
texData.width = *reinterpret_cast<uint32_t*>(&fileArr[18]);
|
||||||
|
texData.height = *reinterpret_cast<uint32_t*>(&fileArr[22]);
|
||||||
|
|
||||||
|
texData.bitSize = TextureDataStruct::BS_24BIT;
|
||||||
|
|
||||||
|
size_t dataSize = texData.width * texData.height * 3;
|
||||||
|
|
||||||
|
texData.data.resize(dataSize);
|
||||||
|
|
||||||
|
size_t pos = *reinterpret_cast<uint32_t*>(&fileArr[10]);
|
||||||
|
size_t x = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < texData.width; i++)
|
||||||
|
for (size_t j = 0; j < texData.height; j++)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (pos + 3 > fileSize)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("File is too short!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
x = (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j);
|
||||||
|
|
||||||
|
texData.data[x + 2] = fileArr[pos++];
|
||||||
|
texData.data[x + 1] = fileArr[pos++];
|
||||||
|
texData.data[x + 0] = fileArr[pos++];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return texData;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureDataStruct CreateTextureDataFromBmp32(const std::string& fullFileName, const std::string& ZIPFileName)
|
||||||
|
{
|
||||||
|
|
||||||
|
TextureDataStruct texData;
|
||||||
|
std::vector<char> fileArr;
|
||||||
|
|
||||||
|
fileArr = !ZIPFileName.empty() ? readFileFromZIP(fullFileName, ZIPFileName) : readFile(fullFileName);
|
||||||
|
|
||||||
|
size_t fileSize = fileArr.size();
|
||||||
|
|
||||||
|
if (fileSize < 22)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("File is too short or not correct!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//This refers to BITMAPV5HEADER
|
||||||
|
texData.width = *reinterpret_cast<uint32_t*>(&fileArr[18]);
|
||||||
|
texData.height = *reinterpret_cast<uint32_t*>(&fileArr[22]);
|
||||||
|
|
||||||
|
texData.bitSize = TextureDataStruct::BS_32BIT;
|
||||||
|
|
||||||
|
size_t dataSize = texData.width * texData.height * 4;
|
||||||
|
|
||||||
|
texData.data.resize(dataSize);
|
||||||
|
|
||||||
|
size_t pos = *reinterpret_cast<uint32_t*>(&fileArr[10]);
|
||||||
|
size_t x = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < texData.width; i++)
|
||||||
|
for (size_t j = 0; j < texData.height; j++)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (pos + 4 > fileSize)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("File is too short!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
x = (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j);
|
||||||
|
|
||||||
|
texData.data[x + 2] = fileArr[pos++];
|
||||||
|
texData.data[x + 1] = fileArr[pos++];
|
||||||
|
texData.data[x + 0] = fileArr[pos++];
|
||||||
|
texData.data[x + 3] = fileArr[pos++];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return texData;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef PNG_ENABLED
|
||||||
|
|
||||||
|
TextureDataStruct CreateTextureDataFromPng(const std::string& fullFileName)
|
||||||
|
{
|
||||||
|
TextureDataStruct texData;
|
||||||
|
|
||||||
|
FILE* file = fopen(fullFileName.c_str(), "rb");
|
||||||
|
if (!file) {
|
||||||
|
fclose(file);
|
||||||
|
throw std::runtime_error("Could not open file " + fullFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||||
|
if (!png) {
|
||||||
|
fclose(file);
|
||||||
|
throw std::runtime_error("Could not create PNG read structure");
|
||||||
|
}
|
||||||
|
|
||||||
|
png_infop info = png_create_info_struct(png);
|
||||||
|
if (!info) {
|
||||||
|
fclose(file);
|
||||||
|
png_destroy_read_struct(&png, nullptr, nullptr);
|
||||||
|
throw std::runtime_error("Could not create PNG info structure");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setjmp(png_jmpbuf(png))) {
|
||||||
|
png_destroy_read_struct(&png, &info, nullptr);
|
||||||
|
fclose(file);
|
||||||
|
throw std::runtime_error("Error during PNG read");
|
||||||
|
}
|
||||||
|
|
||||||
|
png_init_io(png, file);
|
||||||
|
png_read_info(png, info);
|
||||||
|
|
||||||
|
texData.width = png_get_image_width(png, info);
|
||||||
|
texData.height = png_get_image_height(png, info);
|
||||||
|
png_byte color_type = png_get_color_type(png, info);
|
||||||
|
png_byte bit_depth = png_get_bit_depth(png, info);
|
||||||
|
|
||||||
|
if (bit_depth == 16)
|
||||||
|
png_set_strip_16(png);
|
||||||
|
|
||||||
|
if (color_type == PNG_COLOR_TYPE_PALETTE)
|
||||||
|
png_set_palette_to_rgb(png);
|
||||||
|
|
||||||
|
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
|
||||||
|
png_set_expand_gray_1_2_4_to_8(png);
|
||||||
|
|
||||||
|
if (png_get_valid(png, info, PNG_INFO_tRNS))
|
||||||
|
png_set_tRNS_to_alpha(png);
|
||||||
|
|
||||||
|
if (color_type == PNG_COLOR_TYPE_RGB ||
|
||||||
|
color_type == PNG_COLOR_TYPE_GRAY ||
|
||||||
|
color_type == PNG_COLOR_TYPE_PALETTE)
|
||||||
|
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
|
||||||
|
|
||||||
|
if (color_type == PNG_COLOR_TYPE_GRAY ||
|
||||||
|
color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
|
||||||
|
png_set_gray_to_rgb(png);
|
||||||
|
|
||||||
|
png_read_update_info(png, info);
|
||||||
|
|
||||||
|
|
||||||
|
png_bytep* row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * texData.height);
|
||||||
|
for (int y = 0; y < texData.height; y++) {
|
||||||
|
row_pointers[y] = (png_byte*)malloc(png_get_rowbytes(png, info));
|
||||||
|
}
|
||||||
|
|
||||||
|
png_read_image(png, row_pointers);
|
||||||
|
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
bool has_alpha = (color_type & PNG_COLOR_MASK_ALPHA) || (png_get_valid(png, info, PNG_INFO_tRNS));
|
||||||
|
|
||||||
|
size_t dataSize;
|
||||||
|
|
||||||
|
if (has_alpha)
|
||||||
|
{
|
||||||
|
texData.bitSize = TextureDataStruct::BS_32BIT;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
texData.bitSize = TextureDataStruct::BS_24BIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int channels = has_alpha ? 4 : 3;
|
||||||
|
|
||||||
|
dataSize = texData.width * texData.height * channels;
|
||||||
|
texData.data.resize(dataSize);
|
||||||
|
|
||||||
|
|
||||||
|
//for (int y = 0; y < texData.height; y++) { //Go in reverse because of UV coord start point
|
||||||
|
for (int y = texData.height-1; y >= 0; y--) {
|
||||||
|
//png_bytep row = row_pointers[y];//Go in reverse because of UV coord start point
|
||||||
|
png_bytep row = row_pointers[texData.height - 1 - y];
|
||||||
|
for (int x = 0; x < texData.width; x++) {
|
||||||
|
png_bytep px = &(row[x * 4]);
|
||||||
|
texData.data[(y * texData.width + x) * channels + 0] = px[0]; // R
|
||||||
|
texData.data[(y * texData.width + x) * channels + 1] = px[1]; // G
|
||||||
|
texData.data[(y * texData.width + x) * channels + 2] = px[2]; // B
|
||||||
|
if (has_alpha) {
|
||||||
|
texData.data[(y * texData.width + x) * channels + 3] = px[3]; // A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//free(row_pointers[y]);//Go in reverse because of UV coord start point
|
||||||
|
free(row_pointers[texData.height - 1 - y]);//Go in reverse because of UV coord start point
|
||||||
|
}
|
||||||
|
free(row_pointers);
|
||||||
|
|
||||||
|
png_destroy_read_struct(&png, &info, nullptr);
|
||||||
|
|
||||||
|
return texData;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
//#include "OpenGlExtensions.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
#include <GLES2/gl2.h>
|
||||||
|
#include <GLES2/gl2ext.h>
|
||||||
|
|
||||||
|
namespace ZL
|
||||||
|
{
|
||||||
|
|
||||||
|
struct TextureDataStruct
|
||||||
|
{
|
||||||
|
size_t width;
|
||||||
|
size_t height;
|
||||||
|
std::vector<char> data;
|
||||||
|
enum BitSize {
|
||||||
|
BS_24BIT,
|
||||||
|
BS_32BIT
|
||||||
|
};
|
||||||
|
|
||||||
|
BitSize bitSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Texture
|
||||||
|
{
|
||||||
|
size_t width = 0;
|
||||||
|
size_t height = 0;
|
||||||
|
GLuint texID = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
Texture(const TextureDataStruct& texData);
|
||||||
|
|
||||||
|
~Texture();
|
||||||
|
|
||||||
|
GLuint getTexID();
|
||||||
|
|
||||||
|
size_t getWidth();
|
||||||
|
size_t getHeight();
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
TextureDataStruct CreateTextureDataFromBmp24(const std::string& fullFileName, const std::string& ZIPFileName="");
|
||||||
|
TextureDataStruct CreateTextureDataFromBmp32(const std::string& fullFileName, const std::string& ZIPFileName="");
|
||||||
|
#ifdef PNG_ENABLED
|
||||||
|
TextureDataStruct CreateTextureDataFromPng(const std::string& fullFileName);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#include "Utils.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include <iterator>
|
||||||
|
#include <vector>
|
||||||
|
#include <iostream>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <fstream>
|
||||||
|
//#include <zip.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
#include <android/log.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
std::string readTextFile(const std::string &filename) {
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
SDL_RWops* file = SDL_RWFromFile(filename.c_str(), "rb");
|
||||||
|
if (!file) {
|
||||||
|
#ifdef DEBUG
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "Utils",
|
||||||
|
"Failed to open: %s", filename.c_str());
|
||||||
|
#endif
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Sint64 size = SDL_RWsize(file);
|
||||||
|
if (size <= 0) {
|
||||||
|
SDL_RWclose(file);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content(size, '\0');
|
||||||
|
if (SDL_RWread(file, &content[0], size, 1) != 1) {
|
||||||
|
SDL_RWclose(file);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_RWclose(file);
|
||||||
|
return content;
|
||||||
|
#else
|
||||||
|
std::ifstream f(filename);
|
||||||
|
if (!f.is_open()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return std::string((std::istreambuf_iterator<char>(f)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> readFile(const std::string &filename) {
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
SDL_RWops* file = SDL_RWFromFile(filename.c_str(), "rb");
|
||||||
|
if (!file) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Sint64 size = SDL_RWsize(file);
|
||||||
|
if (size <= 0) {
|
||||||
|
SDL_RWclose(file);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> data(size);
|
||||||
|
if (SDL_RWread(file, data.data(), size, 1) != 1) {
|
||||||
|
SDL_RWclose(file);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_RWclose(file);
|
||||||
|
return data;
|
||||||
|
#else
|
||||||
|
std::ifstream file(filename, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::streamsize size = file.tellg();
|
||||||
|
if (size <= 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
std::vector<char> buffer(size);
|
||||||
|
|
||||||
|
if (!file.read(buffer.data(), size)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> readFileFromZIP(const std::string &filename, const std::string &zipfilename) {
|
||||||
|
/*const std::string zipPath = zipfilename;
|
||||||
|
int zipErr;
|
||||||
|
zip_t* archive = zip_open(zipPath.c_str(), ZIP_RDONLY, &zipErr);
|
||||||
|
if (!archive) {
|
||||||
|
throw std::runtime_error("Ошибка открытия ZIP: " + zipPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cleanFilename = filename;
|
||||||
|
if (cleanFilename.rfind("./", 0) == 0) {
|
||||||
|
cleanFilename = cleanFilename.substr(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Ищем в ZIP: " << cleanFilename << std::endl;
|
||||||
|
|
||||||
|
zip_file_t* zipFile = zip_fopen(archive, cleanFilename.c_str(), 0);
|
||||||
|
if (!zipFile) {
|
||||||
|
zip_close(archive);
|
||||||
|
throw std::runtime_error("Файл не найден в ZIP: " + cleanFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
zip_stat_t fileStat;
|
||||||
|
if (zip_stat(archive, cleanFilename.c_str(), 0, &fileStat) != 0) {
|
||||||
|
zip_fclose(zipFile);
|
||||||
|
zip_close(archive);
|
||||||
|
throw std::runtime_error("Ошибка чтения ZIP-статистики.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> fileData;
|
||||||
|
fileData.resize(fileStat.size);
|
||||||
|
|
||||||
|
zip_fread(zipFile, fileData.data(), fileData.size());
|
||||||
|
|
||||||
|
zip_fclose(zipFile);
|
||||||
|
zip_close(archive);
|
||||||
|
|
||||||
|
return fileData;*/
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool findString(const char *in, char *list) {
|
||||||
|
size_t thisLength = strlen(in);
|
||||||
|
while (*list != 0) {
|
||||||
|
size_t length = strcspn(list, " ");
|
||||||
|
if (thisLength == length && !strncmp(in, list, length))
|
||||||
|
return true;
|
||||||
|
list += length + 1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <exception>
|
||||||
|
#include <map>
|
||||||
|
#include <stack>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
namespace ZL
|
||||||
|
{
|
||||||
|
std::string readTextFile(const std::string& filename);
|
||||||
|
|
||||||
|
std::vector<char> readFile(const std::string& filename);
|
||||||
|
|
||||||
|
std::vector<char> readFileFromZIP(const std::string& filename, const std::string& zipfilename);
|
||||||
|
|
||||||
|
bool findString(const char* in, char* list);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
#include "Game.h"
|
||||||
|
#include "Environment.h"
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
#include <android/log.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ZL::Game game;
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
|
||||||
|
extern "C" int SDL_main(int argc, char* argv[]) {
|
||||||
|
#else
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
// Инициализация SDL перед получением информации о дисплее
|
||||||
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "Game", "SDL init failed: %s", SDL_GetError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DisplayMode displayMode;
|
||||||
|
if (SDL_GetCurrentDisplayMode(0, &displayMode) != 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GetCurrentDisplayMode failed: %s", SDL_GetError());
|
||||||
|
SDL_Quit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
ZL::Environment::width = displayMode.w;
|
||||||
|
ZL::Environment::height = displayMode.h;
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "Game", "Display resolution: %dx%d",
|
||||||
|
ZL::Environment::width, ZL::Environment::height);
|
||||||
|
#else
|
||||||
|
constexpr int CONST_WIDTH = 1280;
|
||||||
|
constexpr int CONST_HEIGHT = 720;
|
||||||
|
ZL::Environment::width = CONST_WIDTH;
|
||||||
|
ZL::Environment::height = CONST_HEIGHT;
|
||||||
|
|
||||||
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
|
||||||
|
SDL_Log("SDL init failed: %s", SDL_GetError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef EMSCRIPTEN
|
||||||
|
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
|
||||||
|
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||||
|
|
||||||
|
SDL_Window* win = SDL_CreateWindow("Space Ship Game",
|
||||||
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||||
|
ZL::Environment::width, ZL::Environment::height,
|
||||||
|
SDL_WINDOW_OPENGL);
|
||||||
|
|
||||||
|
if (!win) {
|
||||||
|
std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GLContext glContext = SDL_GL_CreateContext(win);
|
||||||
|
if (!glContext) {
|
||||||
|
std::cerr << "SDL_GL_CreateContext failed: " << SDL_GetError() << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GL_MakeCurrent(win, glContext);
|
||||||
|
ZL::Environment::window = win;
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||||
|
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||||
|
|
||||||
|
|
||||||
|
ZL::Environment::window = SDL_CreateWindow(
|
||||||
|
"Space Ship Game",
|
||||||
|
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
|
||||||
|
ZL::Environment::width, ZL::Environment::height,
|
||||||
|
SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
|
||||||
|
);
|
||||||
|
#else
|
||||||
|
// Для десктопа
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
||||||
|
|
||||||
|
ZL::Environment::window = SDL_CreateWindow(
|
||||||
|
"Space Ship Game",
|
||||||
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||||
|
ZL::Environment::width, ZL::Environment::height,
|
||||||
|
SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!ZL::Environment::window) {
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "Game", "Failed to create window: %s", SDL_GetError());
|
||||||
|
#else
|
||||||
|
SDL_Log("Failed to create window: %s", SDL_GetError());
|
||||||
|
#endif
|
||||||
|
SDL_Quit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window);
|
||||||
|
if (!ctx) {
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GL_CreateContext failed: %s", SDL_GetError());
|
||||||
|
#else
|
||||||
|
SDL_Log("SDL_GL_CreateContext failed: %s", SDL_GetError());
|
||||||
|
#endif
|
||||||
|
SDL_DestroyWindow(ZL::Environment::window);
|
||||||
|
SDL_Quit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SDL_GL_MakeCurrent(ZL::Environment::window, ctx) != 0) {
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GL_MakeCurrent failed: %s", SDL_GetError());
|
||||||
|
#else
|
||||||
|
SDL_Log("SDL_GL_MakeCurrent failed: %s", SDL_GetError());
|
||||||
|
#endif
|
||||||
|
SDL_GL_DeleteContext(ctx);
|
||||||
|
SDL_DestroyWindow(ZL::Environment::window);
|
||||||
|
SDL_Quit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настройка VSync
|
||||||
|
if (SDL_GL_SetSwapInterval(1) < 0) {
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
__android_log_print(ANDROID_LOG_WARN, "Game", "Unable to set VSync: %s", SDL_GetError());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Настройка игры
|
||||||
|
try {
|
||||||
|
game.setup();
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "Game", "Game setup failed: %s", e.what());
|
||||||
|
#else
|
||||||
|
std::cerr << "Game setup failed: " << e.what() << std::endl;
|
||||||
|
#endif
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef EMSCRIPTEN
|
||||||
|
emscripten_set_main_loop(MainLoop, 0, 1);
|
||||||
|
#else
|
||||||
|
bool running = true;
|
||||||
|
while (running && !game.shouldExit()) {
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
if (event.type == SDL_QUIT) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
game.update();
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
SDL_Delay(16);
|
||||||
|
#else
|
||||||
|
SDL_Delay(2);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -16,9 +16,6 @@
|
|||||||
# public *;
|
# public *;
|
||||||
#}
|
#}
|
||||||
|
|
||||||
# SDL2 specific rules
|
|
||||||
-keep class org.libsdl.app.** { *; }
|
|
||||||
|
|
||||||
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLInputConnection {
|
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLInputConnection {
|
||||||
void nativeCommitText(java.lang.String, int);
|
void nativeCommitText(java.lang.String, int);
|
||||||
void nativeGenerateScancodeForUnichar(char);
|
void nativeGenerateScancodeForUnichar(char);
|
||||||
@@ -99,6 +96,3 @@
|
|||||||
void hapticRun(int, float, int);
|
void hapticRun(int, float, int);
|
||||||
void hapticStop(int);
|
void hapticStop(int);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keep our main activity
|
|
||||||
-keep class fishrungames.shadowoverbishkek.ShadowOverBishkekActivity { *; }
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
|
|
||||||
<!-- OpenGL ES 2.0 -->
|
<!-- OpenGL ES 2.0 -->
|
||||||
<uses-feature android:glEsVersion="0x00030000" />
|
<uses-feature android:glEsVersion="0x00020000" />
|
||||||
|
|
||||||
<!-- Touchscreen support -->
|
<!-- Touchscreen support -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->
|
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->
|
||||||
|
|
||||||
<!-- Create a Java class extending SDLActivity and place it in a
|
<!-- Create a Java class extending SDLActivity and place it in a
|
||||||
directory under shadowoverbishkek/main/java matching the package, e.g. shadowoverbishkek/main/java/com/gamemaker/game/MyGame.java
|
directory under app/src/main/java matching the package, e.g. app/src/main/java/com/gamemaker/game/MyGame.java
|
||||||
|
|
||||||
then replace "SDLActivity" with the name of your class (e.g. "MyGame")
|
then replace "SDLActivity" with the name of your class (e.g. "MyGame")
|
||||||
in the XML below.
|
in the XML below.
|
||||||
@@ -62,14 +62,14 @@
|
|||||||
<application android:label="@string/app_name"
|
<application android:label="@string/app_name"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:theme="@style/Theme.App.Starting"
|
android:theme="@style/AppTheme"
|
||||||
android:hardwareAccelerated="true" >
|
android:hardwareAccelerated="true" >
|
||||||
|
|
||||||
<!-- Example of setting SDL hints from AndroidManifest.xml:
|
<!-- Example of setting SDL hints from AndroidManifest.xml:
|
||||||
<meta-data android:name="SDL_ENV.SDL_ACCELEROMETER_AS_JOYSTICK" android:value="0"/>
|
<meta-data android:name="SDL_ENV.SDL_ACCELEROMETER_AS_JOYSTICK" android:value="0"/>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<activity android:name=".ShadowOverBishkekActivity"
|
<activity android:name="SDLActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:alwaysRetainTaskState="true"
|
android:alwaysRetainTaskState="true"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
|
After Width: | Height: | Size: 653 KiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 599 KiB |
@@ -1,11 +1,11 @@
|
|||||||
attribute vec3 vPosition;
|
attribute vec3 vPosition;
|
||||||
attribute vec2 vTexCoord;
|
attribute vec3 vColor;
|
||||||
varying vec2 texCoord;
|
varying vec3 color;
|
||||||
|
|
||||||
uniform mat4 ProjectionModelViewMatrix;
|
uniform mat4 ProjectionModelViewMatrix;
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
gl_Position = ProjectionModelViewMatrix * vec4(vPosition.xyz, 1.0);
|
gl_Position = ProjectionModelViewMatrix * vec4(vPosition.xyz, 1.0);
|
||||||
texCoord = vTexCoord;
|
color = vColor;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
precision mediump float;
|
||||||
|
varying vec3 color;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_FragColor = vec4(color, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//precision mediump float;
|
||||||
|
varying vec3 color;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
//gl_FragColor = vec4(color, 1.0);
|
||||||
|
gl_FragColor = vec4(1.0, 1.0, 0.5, 1.0);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
precision mediump float;
|
||||||
|
varying vec3 color;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
//gl_FragColor = vec4(color, 1.0);
|
||||||
|
gl_FragColor = vec4(1.0, 1.0, 0.5, 1.0);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
precision mediump float;
|
precision mediump float;
|
||||||
uniform sampler2D Texture;
|
uniform sampler2D Texture;
|
||||||
uniform float uAlpha;
|
|
||||||
varying vec2 texCoord;
|
varying vec2 texCoord;
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
vec4 color = texture2D(Texture, texCoord).rgba;
|
vec4 color = texture2D(Texture, texCoord);
|
||||||
color.a *= uAlpha;
|
|
||||||
gl_FragColor = color;
|
gl_FragColor = color;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//precision mediump float;
|
||||||
|
uniform sampler2D Texture;
|
||||||
|
varying vec2 texCoord;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec4 color = texture2D(Texture,texCoord).rgba;
|
||||||
|
gl_FragColor = vec4(color.rgb*0.9 + vec3(0.1, 0.1, 0.1), 1.0);
|
||||||
|
|
||||||
|
//gl_FragColor = color;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,5 +5,8 @@ varying vec2 texCoord;
|
|||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
vec4 color = texture2D(Texture,texCoord).rgba;
|
vec4 color = texture2D(Texture,texCoord).rgba;
|
||||||
|
//gl_FragColor = vec4(color.rgb*0.1 + vec3(0.9, 0.9, 0.9), 1.0);
|
||||||
|
|
||||||
gl_FragColor = color;
|
gl_FragColor = color;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Game</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="android:Theme.NoTitleBar.Fullscreen">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
def append_layered_action_5_0(source_obj_name, target_obj_name):
|
|
||||||
source_obj = bpy.data.objects.get(source_obj_name)
|
|
||||||
target_obj = bpy.data.objects.get(target_obj_name)
|
|
||||||
|
|
||||||
if not (source_obj and target_obj):
|
|
||||||
print("Ошибка: Объекты не найдены")
|
|
||||||
return
|
|
||||||
|
|
||||||
src_action = source_obj.animation_data.action
|
|
||||||
tgt_action = target_obj.animation_data.action
|
|
||||||
|
|
||||||
# 1. Получаем слои (обычно первый)
|
|
||||||
src_layer = src_action.layers[0]
|
|
||||||
tgt_layer = tgt_action.layers[0]
|
|
||||||
|
|
||||||
# 2. Получаем стрипы
|
|
||||||
src_strip = src_layer.strips[0]
|
|
||||||
tgt_strip = tgt_layer.strips[0]
|
|
||||||
|
|
||||||
# Смещение (опираемся на конец диапазона целевого экшена)
|
|
||||||
offset = tgt_action.frame_range[1]
|
|
||||||
|
|
||||||
# 3. Итерируемся по channelbags в исходном стрипе
|
|
||||||
for src_bag in src_strip.channelbags:
|
|
||||||
# Ищем или создаем соответствующий bag в целевом стрипе
|
|
||||||
# Обычно они сопоставляются по имени или типу (например, 'Keyframe Channel Bag')
|
|
||||||
# В простейшем случае берем первый или сопоставляем по индексу
|
|
||||||
dst_bag = None
|
|
||||||
if len(tgt_strip.channelbags) > 0:
|
|
||||||
# Пытаемся найти по названию (если оно есть) или берем тот же индекс
|
|
||||||
dst_bag = tgt_strip.channelbags[0]
|
|
||||||
|
|
||||||
if not dst_bag:
|
|
||||||
# Если в целевом стрипе нет сумок, это странно, но можно создать
|
|
||||||
# (Метод создания может зависеть от конкретного подтипа стрипа в 5.0)
|
|
||||||
continue
|
|
||||||
|
|
||||||
#print(f"Обработка channelbag: {src_bag.name}, кривых: {len(src_bag.fcurves)}")
|
|
||||||
|
|
||||||
# 4. Итерируемся по fcurves внутри сумки
|
|
||||||
for src_fcurve in src_bag.fcurves:
|
|
||||||
dst_fcurve = dst_bag.fcurves.find(src_fcurve.data_path, index=src_fcurve.array_index)
|
|
||||||
|
|
||||||
if not dst_fcurve:
|
|
||||||
dst_fcurve = dst_bag.fcurves.new(data_path=src_fcurve.data_path, index=src_fcurve.array_index)
|
|
||||||
|
|
||||||
# 5. Копируем ключи с офсетом
|
|
||||||
for keyframe in src_fcurve.keyframe_points:
|
|
||||||
new_frame = keyframe.co[0] + offset
|
|
||||||
new_value = keyframe.co[1]
|
|
||||||
|
|
||||||
new_kp = dst_fcurve.keyframe_points.insert(new_frame, new_value, options={'FAST'})
|
|
||||||
new_kp.interpolation = keyframe.interpolation
|
|
||||||
|
|
||||||
# Обновляем интерполяцию
|
|
||||||
for fc in dst_bag.fcurves:
|
|
||||||
fc.update()
|
|
||||||
|
|
||||||
print(f"Анимация успешно дозаписана. Новый конец: {tgt_action.frame_range[1]}")
|
|
||||||
|
|
||||||
append_layered_action_5_0('Armature.001', 'Armature')
|
|
||||||
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import bmesh
|
|
||||||
|
|
||||||
# Имена mesh и арматуры
|
|
||||||
mesh_name = "arm"
|
|
||||||
armature_name = "Reference"
|
|
||||||
|
|
||||||
# Находим объект mesh по имени
|
|
||||||
mesh_obj = bpy.data.objects.get(mesh_name)
|
|
||||||
|
|
||||||
# Находим объект арматуры по имени
|
|
||||||
armature_obj = bpy.data.objects.get(armature_name)
|
|
||||||
|
|
||||||
# Устанавливаем текущий кадр на 0
|
|
||||||
bpy.context.scene.frame_set(0)
|
|
||||||
|
|
||||||
# Принудительно обновляем сцену, чтобы применить анимацию
|
|
||||||
bpy.context.view_layer.update()
|
|
||||||
|
|
||||||
# Открываем файл для записи
|
|
||||||
with open("C:\\Work\\Projects\\witcher001\\resources\\w\\zombie002.txt", "w") as file:
|
|
||||||
# Обработка арматуры и анимации
|
|
||||||
if armature_obj and armature_obj.type == 'ARMATURE':
|
|
||||||
file.write("=== Armature Matrix ===\n")
|
|
||||||
for row in armature_obj.matrix_world:
|
|
||||||
file.write(f"{row}\n")
|
|
||||||
file.write(f"=== Armature Bones: {len(armature_obj.data.bones)}\n")
|
|
||||||
for bone in armature_obj.data.bones:
|
|
||||||
# Записываем имя кости, длину и связи
|
|
||||||
file.write(f"Bone: {bone.name}\n")
|
|
||||||
file.write(f" HEAD_LOCAL: {bone.head_local}\n")
|
|
||||||
file.write(f" TAIL_LOCAL: {bone.tail_local}\n")
|
|
||||||
file.write(f" Length: {(bone.tail_local - bone.head_local).length}\n")
|
|
||||||
for row in bone.matrix:
|
|
||||||
file.write(f" {row}\n")
|
|
||||||
file.write(f" Parent: {bone.parent.name if bone.parent else 'None'}\n")
|
|
||||||
file.write(f" Children: {[child.name for child in bone.children]}\n")
|
|
||||||
|
|
||||||
# Обработка mesh
|
|
||||||
if mesh_obj and mesh_obj.type == 'MESH':
|
|
||||||
# Создаем копию mesh, чтобы не изменять оригинал
|
|
||||||
mesh_copy = mesh_obj.copy()
|
|
||||||
mesh_copy.data = mesh_obj.data.copy()
|
|
||||||
bpy.context.collection.objects.link(mesh_copy)
|
|
||||||
|
|
||||||
# Убедимся, что объект активен
|
|
||||||
bpy.context.view_layer.objects.active = mesh_copy
|
|
||||||
mesh_copy.select_set(True)
|
|
||||||
|
|
||||||
# Применяем модификатор Armature (если он есть)
|
|
||||||
for modifier in mesh_copy.modifiers:
|
|
||||||
if modifier.type == 'ARMATURE':
|
|
||||||
# Включаем модификатор, если он отключен
|
|
||||||
if not modifier.show_viewport:
|
|
||||||
modifier.show_viewport = True
|
|
||||||
if not modifier.show_render:
|
|
||||||
modifier.show_render = True
|
|
||||||
|
|
||||||
# Проверяем, что модификатор связан с арматурой
|
|
||||||
if modifier.object is None:
|
|
||||||
print(f"Модификатор Armature на объекте {mesh_copy.name} не связан с арматурой. Пропускаем.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Временно применяем модификатор, чтобы получить правильные координаты вершин
|
|
||||||
try:
|
|
||||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
|
||||||
except RuntimeError as e:
|
|
||||||
print(f"Ошибка при применении модификатора Armature: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Переходим в режим редактирования
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
# Получаем BMesh представление mesh
|
|
||||||
bm = bmesh.from_edit_mesh(mesh_copy.data)
|
|
||||||
|
|
||||||
# Записываем список вершин
|
|
||||||
file.write(f"===Vertices: {len(bm.verts)}\n")
|
|
||||||
for vertex in bm.verts:
|
|
||||||
file.write(f"Vertex {vertex.index}: {vertex.co}\n")
|
|
||||||
|
|
||||||
# Убедимся, что у меша есть UV слой
|
|
||||||
uv_layer = bm.loops.layers.uv.active
|
|
||||||
if not uv_layer:
|
|
||||||
file.write("UV слой не найден.\n")
|
|
||||||
|
|
||||||
if uv_layer:
|
|
||||||
file.write(f"===UV Coordinates:\n")
|
|
||||||
file.write(f"Face count: {len(bm.faces)}\n")
|
|
||||||
|
|
||||||
for face in bm.faces:
|
|
||||||
file.write(f"Face {face.index}\n")
|
|
||||||
file.write(f"UV Count: {len(face.loops)}\n")
|
|
||||||
for loop in face.loops:
|
|
||||||
uv_coords = loop[uv_layer].uv
|
|
||||||
file.write(f" UV {uv_coords}\n")
|
|
||||||
|
|
||||||
# Записываем нормали
|
|
||||||
file.write(f"===Normals:\n")
|
|
||||||
for vertex in bm.verts:
|
|
||||||
file.write(f"Vertex {vertex.index}: Normal {vertex.normal}\n")
|
|
||||||
|
|
||||||
# Записываем треугольники (индексы вершин)
|
|
||||||
file.write(f"===Triangles: {len(bm.faces)}\n")
|
|
||||||
for face in bm.faces:
|
|
||||||
if len(face.verts) == 3: # Проверяем, что это треугольник
|
|
||||||
verts_indices = [vert.index for vert in face.verts]
|
|
||||||
file.write(f"Triangle: {verts_indices}\n")
|
|
||||||
|
|
||||||
# Возвращаемся в объектный режим
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
# Записываем веса вершин
|
|
||||||
file.write("=== Vertex Weights ===\n")
|
|
||||||
for vertex in mesh_copy.data.vertices:
|
|
||||||
file.write(f"Vertex {vertex.index}:\n")
|
|
||||||
file.write(f"Vertex groups: {len(vertex.groups)}\n")
|
|
||||||
for group in vertex.groups:
|
|
||||||
group_name = mesh_copy.vertex_groups[group.group].name
|
|
||||||
file.write(f" Group: '{group_name}', Weight: {group.weight}\n")
|
|
||||||
|
|
||||||
# Удаляем временную копию mesh
|
|
||||||
bpy.data.objects.remove(mesh_copy)
|
|
||||||
else:
|
|
||||||
file.write(f"Объект с именем '{mesh_name}' не найден или не является mesh.\n")
|
|
||||||
|
|
||||||
# Обработка арматуры и анимации
|
|
||||||
if armature_obj and armature_obj.type == 'ARMATURE':
|
|
||||||
|
|
||||||
# Получаем все ключевые кадры для арматуры
|
|
||||||
file.write("=== Animation Keyframes ===\n")
|
|
||||||
if armature_obj.animation_data and armature_obj.animation_data.action:
|
|
||||||
action = armature_obj.animation_data.action
|
|
||||||
|
|
||||||
# Собираем все уникальные ключевые кадры
|
|
||||||
keyframes = set()
|
|
||||||
|
|
||||||
# Логика для Blender 5.0 (Strip-based / ChannelBag structure)
|
|
||||||
if hasattr(action, "layers"):
|
|
||||||
for layer in action.layers:
|
|
||||||
if hasattr(layer, "strips"):
|
|
||||||
for strip in layer.strips:
|
|
||||||
# Проверяем наличие channelbags (согласно вашему dir(strip))
|
|
||||||
if hasattr(strip, "channelbags"):
|
|
||||||
for bag in strip.channelbags:
|
|
||||||
for fcurve in bag.fcurves:
|
|
||||||
for keyframe in fcurve.keyframe_points:
|
|
||||||
keyframes.add(int(keyframe.co[0]))
|
|
||||||
|
|
||||||
# На случай, если в этой версии используется единственное число
|
|
||||||
elif hasattr(strip, "channelbag") and strip.channelbag:
|
|
||||||
for fcurve in strip.channelbag.fcurves:
|
|
||||||
for keyframe in fcurve.keyframe_points:
|
|
||||||
keyframes.add(int(keyframe.co[0]))
|
|
||||||
|
|
||||||
# Фоллбек для Legacy экшенов
|
|
||||||
if not keyframes and hasattr(action, "fcurves"):
|
|
||||||
for fcurve in action.fcurves:
|
|
||||||
for keyframe in fcurve.keyframe_points:
|
|
||||||
keyframes.add(int(keyframe.co[0]))
|
|
||||||
|
|
||||||
keyframes = sorted(keyframes)
|
|
||||||
|
|
||||||
# Сортируем ключевые кадры
|
|
||||||
keyframes = sorted(keyframes)
|
|
||||||
|
|
||||||
# Сохраняем координаты и матрицы поворота для каждой кости на каждом ключевом кадре
|
|
||||||
file.write("=== Bone Transforms per Keyframe ===\n")
|
|
||||||
file.write(f"Keyframes: {len(keyframes)}\n")
|
|
||||||
for frame in keyframes:
|
|
||||||
# Устанавливаем текущий кадр
|
|
||||||
bpy.context.scene.frame_set(frame)
|
|
||||||
bpy.context.view_layer.update() # Обновляем сцену
|
|
||||||
|
|
||||||
file.write(f"Frame: {frame}\n")
|
|
||||||
for bone in armature_obj.pose.bones:
|
|
||||||
# Получаем координаты и матрицу поворота кости в мировом пространстве
|
|
||||||
matrix = bone.matrix
|
|
||||||
location = matrix.translation
|
|
||||||
rotation = matrix.to_euler()
|
|
||||||
|
|
||||||
# Записываем данные
|
|
||||||
file.write(f" Bone: {bone.name}\n")
|
|
||||||
file.write(f" Location: {location}\n")
|
|
||||||
file.write(f" Rotation: {rotation}\n")
|
|
||||||
file.write(f" Matrix:\n")
|
|
||||||
for row in matrix:
|
|
||||||
file.write(f" {row}\n")
|
|
||||||
else:
|
|
||||||
file.write(f"Объект с именем '{armature_name}' не найден или не является арматурой.\n")
|
|
||||||
|
|
||||||
print("Данные сохранены в файл 'mesh_armature_and_animation_data.txt'")
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import bmesh
|
|
||||||
|
|
||||||
#!
|
|
||||||
|
|
||||||
# Имена mesh и арматуры
|
|
||||||
mesh_name = "Joined"
|
|
||||||
armature_name = "Armature"
|
|
||||||
|
|
||||||
# Находим объект mesh по имени
|
|
||||||
mesh_obj = bpy.data.objects.get(mesh_name)
|
|
||||||
|
|
||||||
# Находим объект арматуры по имени
|
|
||||||
armature_obj = bpy.data.objects.get(armature_name)
|
|
||||||
|
|
||||||
# Устанавливаем текущий кадр на 0
|
|
||||||
bpy.context.scene.frame_set(0)
|
|
||||||
|
|
||||||
# Принудительно обновляем сцену, чтобы применить анимацию
|
|
||||||
bpy.context.view_layer.update()
|
|
||||||
|
|
||||||
# Открываем файл для записи
|
|
||||||
with open("C:\\Work\\Media\\witcher\\2026-04-13\\output\\gg_stand_idle001.txt", "w") as file:
|
|
||||||
# Обработка арматуры и анимации
|
|
||||||
if armature_obj and armature_obj.type == 'ARMATURE':
|
|
||||||
file.write("=== Armature Matrix ===\n")
|
|
||||||
for row in armature_obj.matrix_world:
|
|
||||||
file.write(f"{row}\n")
|
|
||||||
file.write(f"=== Armature Bones: {len(armature_obj.data.bones)}\n")
|
|
||||||
for bone in armature_obj.data.bones:
|
|
||||||
# Записываем имя кости, длину и связи
|
|
||||||
file.write(f"Bone: {bone.name}\n")
|
|
||||||
file.write(f" HEAD_LOCAL: {bone.head_local}\n")
|
|
||||||
file.write(f" TAIL_LOCAL: {bone.tail_local}\n")
|
|
||||||
file.write(f" Length: {(bone.tail_local - bone.head_local).length}\n")
|
|
||||||
for row in bone.matrix:
|
|
||||||
file.write(f" {row}\n")
|
|
||||||
file.write(f" Parent: {bone.parent.name if bone.parent else 'None'}\n")
|
|
||||||
file.write(f" Children: {[child.name for child in bone.children]}\n")
|
|
||||||
|
|
||||||
# Обработка mesh
|
|
||||||
if mesh_obj and mesh_obj.type == 'MESH':
|
|
||||||
# Создаем копию mesh, чтобы не изменять оригинал
|
|
||||||
mesh_copy = mesh_obj.copy()
|
|
||||||
mesh_copy.data = mesh_obj.data.copy()
|
|
||||||
bpy.context.collection.objects.link(mesh_copy)
|
|
||||||
|
|
||||||
# Убедимся, что объект активен
|
|
||||||
bpy.context.view_layer.objects.active = mesh_copy
|
|
||||||
mesh_copy.select_set(True)
|
|
||||||
|
|
||||||
# Применяем модификатор Armature (если он есть)
|
|
||||||
for modifier in mesh_copy.modifiers:
|
|
||||||
if modifier.type == 'ARMATURE':
|
|
||||||
# Включаем модификатор, если он отключен
|
|
||||||
if not modifier.show_viewport:
|
|
||||||
modifier.show_viewport = True
|
|
||||||
if not modifier.show_render:
|
|
||||||
modifier.show_render = True
|
|
||||||
|
|
||||||
# Проверяем, что модификатор связан с арматурой
|
|
||||||
if modifier.object is None:
|
|
||||||
print(f"Модификатор Armature на объекте {mesh_copy.name} не связан с арматурой. Пропускаем.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Временно применяем модификатор, чтобы получить правильные координаты вершин
|
|
||||||
try:
|
|
||||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
|
||||||
except RuntimeError as e:
|
|
||||||
print(f"Ошибка при применении модификатора Armature: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Переходим в режим редактирования
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
# Получаем BMesh представление mesh
|
|
||||||
bm = bmesh.from_edit_mesh(mesh_copy.data)
|
|
||||||
|
|
||||||
# Записываем список вершин
|
|
||||||
file.write(f"===Vertices: {len(bm.verts)}\n")
|
|
||||||
for vertex in bm.verts:
|
|
||||||
file.write(f"Vertex {vertex.index}: {vertex.co}\n")
|
|
||||||
|
|
||||||
# Убедимся, что у меша есть UV слой
|
|
||||||
uv_layer = bm.loops.layers.uv.active
|
|
||||||
if not uv_layer:
|
|
||||||
file.write("UV слой не найден.\n")
|
|
||||||
|
|
||||||
if uv_layer:
|
|
||||||
file.write(f"===UV Coordinates:\n")
|
|
||||||
file.write(f"Face count: {len(bm.faces)}\n")
|
|
||||||
|
|
||||||
for face in bm.faces:
|
|
||||||
file.write(f"Face {face.index}\n")
|
|
||||||
file.write(f"UV Count: {len(face.loops)}\n")
|
|
||||||
for loop in face.loops:
|
|
||||||
uv_coords = loop[uv_layer].uv
|
|
||||||
file.write(f" UV {uv_coords}\n")
|
|
||||||
|
|
||||||
# Записываем нормали
|
|
||||||
file.write(f"===Normals:\n")
|
|
||||||
for vertex in bm.verts:
|
|
||||||
file.write(f"Vertex {vertex.index}: Normal {vertex.normal}\n")
|
|
||||||
|
|
||||||
# Записываем треугольники (индексы вершин)
|
|
||||||
file.write(f"===Triangles: {len(bm.faces)}\n")
|
|
||||||
for face in bm.faces:
|
|
||||||
if len(face.verts) == 3: # Проверяем, что это треугольник
|
|
||||||
verts_indices = [vert.index for vert in face.verts]
|
|
||||||
file.write(f"Triangle: {verts_indices}\n")
|
|
||||||
|
|
||||||
# Возвращаемся в объектный режим
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
# Записываем веса вершин
|
|
||||||
file.write("=== Vertex Weights (Max 5 bones per vertex) ===\n")
|
|
||||||
|
|
||||||
MAX_BONES = 5
|
|
||||||
|
|
||||||
for vertex in mesh_copy.data.vertices:
|
|
||||||
# Извлекаем все группы и веса для текущей вершины
|
|
||||||
all_weights = []
|
|
||||||
for group_element in vertex.groups:
|
|
||||||
all_weights.append({
|
|
||||||
'index': group_element.group,
|
|
||||||
'weight': group_element.weight
|
|
||||||
})
|
|
||||||
|
|
||||||
# Если костей больше лимита, фильтруем и перераспределяем
|
|
||||||
if len(all_weights) > MAX_BONES:
|
|
||||||
# Сортируем по весу (от большего к меньшему)
|
|
||||||
all_weights.sort(key=lambda x: x['weight'], reverse=True)
|
|
||||||
|
|
||||||
# Берем только топ-5
|
|
||||||
kept_weights = all_weights[:MAX_BONES]
|
|
||||||
|
|
||||||
# Считаем сумму весов оставшихся костей для нормализации
|
|
||||||
total_weight = sum(gw['weight'] for gw in kept_weights)
|
|
||||||
|
|
||||||
if total_weight > 0:
|
|
||||||
for gw in kept_weights:
|
|
||||||
gw['weight'] /= total_weight
|
|
||||||
else:
|
|
||||||
# На случай, если у всех веса были по 0.0 (редкий баг меша)
|
|
||||||
kept_weights[0]['weight'] = 1.0
|
|
||||||
|
|
||||||
final_weights = kept_weights
|
|
||||||
else:
|
|
||||||
final_weights = all_weights
|
|
||||||
|
|
||||||
file.write(f"Vertex {vertex.index}:\n")
|
|
||||||
file.write(f"Vertex groups: {len(final_weights)}\n")
|
|
||||||
|
|
||||||
for gw in final_weights:
|
|
||||||
group_name = mesh_copy.vertex_groups[gw['index']].name
|
|
||||||
file.write(f" Group: '{group_name}', Weight: {gw['weight']:.6f}\n")
|
|
||||||
|
|
||||||
# Удаляем временную копию mesh
|
|
||||||
bpy.data.objects.remove(mesh_copy)
|
|
||||||
else:
|
|
||||||
file.write(f"Объект с именем '{mesh_name}' не найден или не является mesh.\n")
|
|
||||||
|
|
||||||
# Обработка арматуры и анимации
|
|
||||||
if armature_obj and armature_obj.type == 'ARMATURE':
|
|
||||||
|
|
||||||
# Получаем все ключевые кадры для арматуры
|
|
||||||
file.write("=== Animation Keyframes ===\n")
|
|
||||||
if armature_obj.animation_data and armature_obj.animation_data.action:
|
|
||||||
action = armature_obj.animation_data.action
|
|
||||||
|
|
||||||
# Собираем все уникальные ключевые кадры
|
|
||||||
keyframes = set()
|
|
||||||
|
|
||||||
# Логика для Blender 5.0 (Strip-based / ChannelBag structure)
|
|
||||||
if hasattr(action, "layers"):
|
|
||||||
for layer in action.layers:
|
|
||||||
if hasattr(layer, "strips"):
|
|
||||||
for strip in layer.strips:
|
|
||||||
# Проверяем наличие channelbags (согласно вашему dir(strip))
|
|
||||||
if hasattr(strip, "channelbags"):
|
|
||||||
for bag in strip.channelbags:
|
|
||||||
for fcurve in bag.fcurves:
|
|
||||||
for keyframe in fcurve.keyframe_points:
|
|
||||||
keyframes.add(int(keyframe.co[0]))
|
|
||||||
|
|
||||||
# На случай, если в этой версии используется единственное число
|
|
||||||
elif hasattr(strip, "channelbag") and strip.channelbag:
|
|
||||||
for fcurve in strip.channelbag.fcurves:
|
|
||||||
for keyframe in fcurve.keyframe_points:
|
|
||||||
keyframes.add(int(keyframe.co[0]))
|
|
||||||
|
|
||||||
# Фоллбек для Legacy экшенов
|
|
||||||
if not keyframes and hasattr(action, "fcurves"):
|
|
||||||
for fcurve in action.fcurves:
|
|
||||||
for keyframe in fcurve.keyframe_points:
|
|
||||||
keyframes.add(int(keyframe.co[0]))
|
|
||||||
|
|
||||||
keyframes = sorted(keyframes)
|
|
||||||
|
|
||||||
# Сортируем ключевые кадры
|
|
||||||
keyframes = sorted(keyframes)
|
|
||||||
|
|
||||||
# Сохраняем координаты и матрицы поворота для каждой кости на каждом ключевом кадре
|
|
||||||
file.write("=== Bone Transforms per Keyframe ===\n")
|
|
||||||
file.write(f"Keyframes: {len(keyframes)}\n")
|
|
||||||
for frame in keyframes:
|
|
||||||
# Устанавливаем текущий кадр
|
|
||||||
bpy.context.scene.frame_set(frame)
|
|
||||||
bpy.context.view_layer.update() # Обновляем сцену
|
|
||||||
|
|
||||||
file.write(f"Frame: {frame}\n")
|
|
||||||
for bone in armature_obj.pose.bones:
|
|
||||||
# Получаем координаты и матрицу поворота кости в мировом пространстве
|
|
||||||
matrix = bone.matrix
|
|
||||||
location = matrix.translation
|
|
||||||
rotation = matrix.to_euler()
|
|
||||||
|
|
||||||
# Записываем данные
|
|
||||||
file.write(f" Bone: {bone.name}\n")
|
|
||||||
file.write(f" Location: {location}\n")
|
|
||||||
file.write(f" Rotation: {rotation}\n")
|
|
||||||
file.write(f" Matrix:\n")
|
|
||||||
for row in matrix:
|
|
||||||
file.write(f" {row}\n")
|
|
||||||
else:
|
|
||||||
file.write(f"Объект с именем '{armature_name}' не найден или не является арматурой.\n")
|
|
||||||
|
|
||||||
print("Данные сохранены в файл 'mesh_armature_and_animation_data.txt'")
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import bmesh
|
|
||||||
import mathutils
|
|
||||||
import random
|
|
||||||
import math
|
|
||||||
|
|
||||||
class SolidTreeGenerator:
|
|
||||||
def __init__(self, levels=5, length=3.0, radius=0.3):
|
|
||||||
self.levels = levels
|
|
||||||
self.base_length = length
|
|
||||||
self.base_radius = radius
|
|
||||||
|
|
||||||
# Хранилище для данных: (start_pos, end_pos, radius_start, radius_end)
|
|
||||||
self.branches_data = []
|
|
||||||
|
|
||||||
def calculate_tree(self, start_pos, direction, length, radius, level):
|
|
||||||
if level <= 0 or length < 0.1:
|
|
||||||
return
|
|
||||||
|
|
||||||
end_pos = start_pos + direction * length
|
|
||||||
# Сохраняем данные сегмента
|
|
||||||
self.branches_data.append({
|
|
||||||
'start': start_pos.copy(),
|
|
||||||
'end': end_pos.copy(),
|
|
||||||
'r_start': radius,
|
|
||||||
'r_end': radius * 0.7
|
|
||||||
})
|
|
||||||
|
|
||||||
# 1. Основной ствол (продолжение)
|
|
||||||
trunk_dir = (direction + self.get_random_vector(0.1)).normalized()
|
|
||||||
self.calculate_tree(end_pos, trunk_dir, length * 0.8, radius * 0.7, level - 1)
|
|
||||||
|
|
||||||
# 2. Боковые ветки (ветвление)
|
|
||||||
if level > 1:
|
|
||||||
num_sides = random.randint(2, 3) # Минимум 2 ветки для видимости
|
|
||||||
for _ in range(num_sides):
|
|
||||||
# Создаем вектор, сильно отклоненный от ствола (30-60 градусов)
|
|
||||||
axis = self.get_random_vector(1.0).normalized()
|
|
||||||
angle = math.radians(random.uniform(30, 60))
|
|
||||||
|
|
||||||
side_dir = direction.copy()
|
|
||||||
side_dir.rotate(mathutils.Quaternion(axis, angle))
|
|
||||||
|
|
||||||
# Боковые ветки короче
|
|
||||||
self.calculate_tree(end_pos, side_dir, length * 0.6, radius * 0.5, level - 1)
|
|
||||||
|
|
||||||
def get_random_vector(self, intensity):
|
|
||||||
return mathutils.Vector((
|
|
||||||
random.uniform(-intensity, intensity),
|
|
||||||
random.uniform(-intensity, intensity),
|
|
||||||
random.uniform(-intensity, intensity)
|
|
||||||
))
|
|
||||||
|
|
||||||
def build_mesh(self):
|
|
||||||
mesh = bpy.data.meshes.new("TreeMesh")
|
|
||||||
obj = bpy.data.objects.new("Tree", mesh)
|
|
||||||
bpy.context.collection.objects.link(obj)
|
|
||||||
|
|
||||||
bm = bmesh.new()
|
|
||||||
skin_layer = bm.verts.layers.skin.verify()
|
|
||||||
|
|
||||||
# Словарь для предотвращения дублирования вершин в одной точке
|
|
||||||
# Ключ - кортеж координат, Значение - объект вершины BMesh
|
|
||||||
vert_map = {}
|
|
||||||
|
|
||||||
for b in self.branches_data:
|
|
||||||
# Превращаем координаты в кортежи для словаря
|
|
||||||
s_key = tuple(round(v, 4) for v in b['start'])
|
|
||||||
e_key = tuple(round(v, 4) for v in b['end'])
|
|
||||||
|
|
||||||
# Получаем или создаем начальную вершину
|
|
||||||
if s_key not in vert_map:
|
|
||||||
v_start = bm.verts.new(b['start'])
|
|
||||||
v_start[skin_layer].radius = (b['r_start'], b['r_start'])
|
|
||||||
vert_map[s_key] = v_start
|
|
||||||
else:
|
|
||||||
v_start = vert_map[s_key]
|
|
||||||
|
|
||||||
# Получаем или создаем конечную вершину
|
|
||||||
if e_key not in vert_map:
|
|
||||||
v_end = bm.verts.new(b['end'])
|
|
||||||
v_end[skin_layer].radius = (b['r_end'], b['r_end'])
|
|
||||||
vert_map[e_key] = v_end
|
|
||||||
else:
|
|
||||||
v_end = vert_map[e_key]
|
|
||||||
|
|
||||||
# Создаем ребро, если его еще нет
|
|
||||||
if not bm.edges.get((v_start, v_end)):
|
|
||||||
bm.edges.new((v_start, v_end))
|
|
||||||
|
|
||||||
# Находим корень (самую нижнюю точку) и помечаем его
|
|
||||||
root_v = min(bm.verts, key=lambda v: v.co.z)
|
|
||||||
root_v[skin_layer].use_root = True
|
|
||||||
|
|
||||||
bm.to_mesh(mesh)
|
|
||||||
bm.free()
|
|
||||||
|
|
||||||
# Модификаторы
|
|
||||||
obj.modifiers.new(name="Skin", type='SKIN')
|
|
||||||
sub = obj.modifiers.new(name="Subdiv", type='SUBSURF')
|
|
||||||
sub.levels = 1 # Для начала 1, чтобы не тормозило
|
|
||||||
|
|
||||||
# Очистка сцены
|
|
||||||
bpy.ops.object.select_all(action='SELECT')
|
|
||||||
bpy.ops.object.delete()
|
|
||||||
|
|
||||||
# Запуск
|
|
||||||
generator = SolidTreeGenerator(levels=5, length=3.0, radius=0.4)
|
|
||||||
generator.calculate_tree(mathutils.Vector((0,0,0)), mathutils.Vector((0,0,1)), 3.0, 0.4, 5)
|
|
||||||
generator.build_mesh()
|
|
||||||
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import bmesh
|
|
||||||
|
|
||||||
mesh_name = "chassis_low"
|
|
||||||
output_path = "C:\\Work\\Projects\\space-game001\\blender scripts\\output\\spaceship005.txt"
|
|
||||||
|
|
||||||
mesh_obj = bpy.data.objects.get(mesh_name)
|
|
||||||
|
|
||||||
if mesh_obj and mesh_obj.type == 'MESH':
|
|
||||||
# Обязательно применяем трансформации, чтобы координаты были актуальными
|
|
||||||
# Но для чистоты эксперимента берем mesh data как есть
|
|
||||||
|
|
||||||
# Работаем с копией меша, чтобы применить триангуляцию (если нужно)
|
|
||||||
# и не испортить сцену, но пока используем bmesh напрямую
|
|
||||||
bm = bmesh.new()
|
|
||||||
bm.from_mesh(mesh_obj.data)
|
|
||||||
|
|
||||||
# Получаем слой UV
|
|
||||||
uv_layer = bm.loops.layers.uv.active
|
|
||||||
|
|
||||||
# Словарь для хранения уникальных вершин:
|
|
||||||
# Ключ: (x, y, z, nx, ny, nz, u, v) -> Значение: новый_индекс
|
|
||||||
unique_verts_map = {}
|
|
||||||
final_vertices = [] # Список для записи в файл
|
|
||||||
final_indices = [] # Список индексов треугольников
|
|
||||||
|
|
||||||
# Проходим по всем фейсам
|
|
||||||
for face in bm.faces:
|
|
||||||
face_indices = []
|
|
||||||
|
|
||||||
# Проходим по углам (loops) фейса
|
|
||||||
for loop in face.loops:
|
|
||||||
vert = loop.vert
|
|
||||||
|
|
||||||
# 1. Координаты (округляем для надежности сравнения float)
|
|
||||||
co = (round(vert.co.x, 6), round(vert.co.y, 6), round(vert.co.z, 6))
|
|
||||||
|
|
||||||
# 2. Нормаль (если используете Smooth shading, берите vert.normal, если Flat - face.normal)
|
|
||||||
# Для простоты берем нормаль вершины
|
|
||||||
no = (round(vert.normal.x, 6), round(vert.normal.y, 6), round(vert.normal.z, 6))
|
|
||||||
|
|
||||||
# 3. UV координаты
|
|
||||||
if uv_layer:
|
|
||||||
raw_uv = loop[uv_layer].uv
|
|
||||||
uv = (round(raw_uv.x, 6), round(raw_uv.y, 6))
|
|
||||||
else:
|
|
||||||
uv = (0.0, 0.0)
|
|
||||||
|
|
||||||
# Собираем уникальный ключ данных вершины
|
|
||||||
vert_data_key = (co, no, uv)
|
|
||||||
|
|
||||||
# Проверяем, есть ли такая комбинация уже
|
|
||||||
if vert_data_key in unique_verts_map:
|
|
||||||
index = unique_verts_map[vert_data_key]
|
|
||||||
else:
|
|
||||||
index = len(final_vertices)
|
|
||||||
unique_verts_map[vert_data_key] = index
|
|
||||||
final_vertices.append(vert_data_key)
|
|
||||||
|
|
||||||
face_indices.append(index)
|
|
||||||
|
|
||||||
# Триангуляция "на лету" (если фейс - квадрат, делим на два треугольника)
|
|
||||||
# Простейший метод fan (0, 1, 2), (0, 2, 3)...
|
|
||||||
for i in range(1, len(face_indices) - 1):
|
|
||||||
final_indices.append(face_indices[0])
|
|
||||||
final_indices.append(face_indices[i])
|
|
||||||
final_indices.append(face_indices[i+1])
|
|
||||||
|
|
||||||
# --- ЗАПИСЬ В ФАЙЛ ---
|
|
||||||
with open(output_path, "w") as file:
|
|
||||||
file.write(f"===Vertices (Split by UV/Normal): {len(final_vertices)}\n")
|
|
||||||
# Формат строки: ID: X Y Z | NX NY NZ | U V
|
|
||||||
for idx, v_data in enumerate(final_vertices):
|
|
||||||
co, no, uv = v_data
|
|
||||||
file.write(f"V {idx}: Pos({co[0]}, {co[1]}, {co[2]}) Norm({no[0]}, {no[1]}, {no[2]}) UV({uv[0]}, {uv[1]})\n")
|
|
||||||
|
|
||||||
file.write(f"\n===Triangles (Indices): {len(final_indices) // 3}\n")
|
|
||||||
# Записываем тройками
|
|
||||||
for i in range(0, len(final_indices), 3):
|
|
||||||
file.write(f"Tri: {final_indices[i]} {final_indices[i+1]} {final_indices[i+2]}\n")
|
|
||||||
|
|
||||||
bm.free()
|
|
||||||
print(f"Export done. Original verts: {len(mesh_obj.data.vertices)}, Split verts: {len(final_vertices)}")
|
|
||||||
|
|
||||||
else:
|
|
||||||
print("Mesh not found")
|
|
||||||
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:9.3.0'
|
classpath 'com.android.tools.build:gradle:8.1.1'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# cmake/FetchDependencies.cmake
|
|
||||||
|
|
||||||
set(THIRDPARTY_DIR "${CMAKE_CURRENT_LIST_DIR}/../thirdparty")
|
|
||||||
|
|
||||||
if(NOT EXISTS "${THIRDPARTY_DIR}")
|
|
||||||
file(MAKE_DIRECTORY "${THIRDPARTY_DIR}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
macro(check_and_download URL ARCHIVE_NAME EXTRACTED_DIR_NAME CHECK_FILE)
|
|
||||||
set(ARCHIVE_PATH "${THIRDPARTY_DIR}/${ARCHIVE_NAME}")
|
|
||||||
set(SRC_PATH "${THIRDPARTY_DIR}/${EXTRACTED_DIR_NAME}")
|
|
||||||
|
|
||||||
if(NOT EXISTS "${ARCHIVE_PATH}")
|
|
||||||
message(STATUS "Downloading ${ARCHIVE_NAME}...")
|
|
||||||
file(DOWNLOAD "${URL}" "${ARCHIVE_PATH}" SHOW_PROGRESS)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT EXISTS "${SRC_PATH}/${CHECK_FILE}")
|
|
||||||
message(STATUS "Extracting ${ARCHIVE_NAME}...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E tar xvf "${ARCHIVE_PATH}"
|
|
||||||
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
endmacro()
|
|
||||||
|
|
||||||
# 1) ZLIB (Нужна только для инклудов, если не используете emscripten порты)
|
|
||||||
check_and_download("https://github.com/madler/zlib/releases/download/v1.3.2/zlib132.zip" "zlib132.zip" "zlib-1.3.2" "CMakeLists.txt")
|
|
||||||
|
|
||||||
# 2) SDL2
|
|
||||||
check_and_download("https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.32.10.zip" "release-2.32.10.zip" "SDL-release-2.32.10" "CMakeLists.txt")
|
|
||||||
|
|
||||||
# 3) LibPNG
|
|
||||||
check_and_download("https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.51.zip" "v1.6.51.zip" "libpng-1.6.51" "CMakeLists.txt")
|
|
||||||
|
|
||||||
# 4) LibZip
|
|
||||||
check_and_download("https://github.com/nih-at/libzip/archive/refs/tags/v1.11.4.zip" "v1.11.4.zip" "libzip-1.11.4" "CMakeLists.txt")
|
|
||||||
|
|
||||||
# 5) Eigen
|
|
||||||
check_and_download("https://gitlab.com/libeigen/eigen/-/archive/5.0.0/eigen-5.0.0.zip" "eigen-5.0.0.zip" "eigen-5.0.0" "signature_of_eigen3_matrix_library")
|
|
||||||
|
|
||||||
# 6) Boost
|
|
||||||
check_and_download("https://archives.boost.io/release/1.90.0/source/boost_1_90_0.zip" "boost_1_90_0.zip" "boost_1_90_0" "boost")
|
|
||||||
|
|
||||||
# 7) FreeType
|
|
||||||
check_and_download("https://download.savannah.gnu.org/releases/freetype/freetype-2.14.1.tar.gz" "freetype-2.14.1.tar.gz" "freetype-2.14.1" "CMakeLists.txt")
|
|
||||||
|
|
||||||
# 8) SDL_ttf
|
|
||||||
check_and_download("https://github.com/libsdl-org/SDL_ttf/archive/refs/tags/release-2.24.0.zip" "release-2.24.0.zip" "SDL_ttf-release-2.24.0" "CMakeLists.txt")
|
|
||||||
|
|
||||||
# 9) Lua
|
|
||||||
check_and_download("https://github.com/lua/lua/archive/refs/tags/v5.4.8.zip" "lua-v5.4.8.zip" "lua-5.4.8" "lapi.c")
|
|
||||||
|
|
||||||
# 10) sol2 (header-only C++ bindings for Lua)
|
|
||||||
check_and_download("https://github.com/ThePhD/sol2/archive/refs/tags/v3.3.0.zip" "sol2-v3.3.0.zip" "sol2-3.3.0" "include/sol/sol.hpp")
|
|
||||||
|
|
||||||
# 11) SDL2_mixer
|
|
||||||
check_and_download("https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.8.0.zip" "SDL_mixer-release-2.8.0.zip" "SDL_mixer-release-2.8.0" "CMakeLists.txt")
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# cmake/FetchDependenciesLinux.cmake
|
|
||||||
|
|
||||||
set(THIRDPARTY_DIR "${CMAKE_CURRENT_LIST_DIR}/../thirdparty")
|
|
||||||
|
|
||||||
if(NOT EXISTS "${THIRDPARTY_DIR}")
|
|
||||||
file(MAKE_DIRECTORY "${THIRDPARTY_DIR}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
macro(check_and_download URL ARCHIVE_NAME EXTRACTED_DIR_NAME CHECK_FILE)
|
|
||||||
set(ARCHIVE_PATH "${THIRDPARTY_DIR}/${ARCHIVE_NAME}")
|
|
||||||
set(SRC_PATH "${THIRDPARTY_DIR}/${EXTRACTED_DIR_NAME}")
|
|
||||||
|
|
||||||
if(NOT EXISTS "${ARCHIVE_PATH}")
|
|
||||||
message(STATUS "Downloading ${ARCHIVE_NAME}...")
|
|
||||||
file(DOWNLOAD "${URL}" "${ARCHIVE_PATH}" SHOW_PROGRESS)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT EXISTS "${SRC_PATH}/${CHECK_FILE}")
|
|
||||||
message(STATUS "Extracting ${ARCHIVE_NAME}...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E tar xvf "${ARCHIVE_PATH}"
|
|
||||||
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
endmacro()
|
|
||||||
|
|
||||||
# 1) sol2 (header-only C++ bindings for Lua)
|
|
||||||
check_and_download("https://github.com/ThePhD/sol2/archive/refs/tags/v3.3.0.zip" "sol2-v3.3.0.zip" "sol2-3.3.0" "include/sol/sol.hpp")
|
|
||||||
@@ -1,728 +0,0 @@
|
|||||||
# cmake/ThirdParty.cmake
|
|
||||||
|
|
||||||
include("${CMAKE_CURRENT_LIST_DIR}/FetchDependencies.cmake")
|
|
||||||
|
|
||||||
macro(log msg)
|
|
||||||
message(STATUS "[ThirdParty] ${msg}")
|
|
||||||
endmacro()
|
|
||||||
|
|
||||||
set(BUILD_CONFIGS Debug Release)
|
|
||||||
|
|
||||||
# Map MinSizeRel and RelWithDebInfo to Release libs for all imported targets.
|
|
||||||
# Without this CMake warns that IMPORTED_LOCATION is missing for those configs.
|
|
||||||
set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL Release)
|
|
||||||
set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release)
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 1) ZLIB (zlib131.zip → zlib-1.3.2) - без изменений
|
|
||||||
# ===========================================
|
|
||||||
set(ZLIB_SRC_DIR "${THIRDPARTY_DIR}/zlib-1.3.2")
|
|
||||||
set(ZLIB_BUILD_DIR "${ZLIB_SRC_DIR}/build")
|
|
||||||
set(ZLIB_INSTALL_DIR "${ZLIB_SRC_DIR}/install")
|
|
||||||
|
|
||||||
|
|
||||||
file(MAKE_DIRECTORY "${ZLIB_BUILD_DIR}")
|
|
||||||
|
|
||||||
# проверяем, собран ли уже zlib
|
|
||||||
set(_have_zlib FALSE)
|
|
||||||
foreach(candidate
|
|
||||||
"${ZLIB_INSTALL_DIR}/lib/zlibstatic.lib"
|
|
||||||
)
|
|
||||||
if(EXISTS "${candidate}")
|
|
||||||
set(_have_zlib TRUE)
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
|
|
||||||
if(NOT _have_zlib)
|
|
||||||
log("Configuring zlib ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
-G "${CMAKE_GENERATOR}"
|
|
||||||
-S "${ZLIB_SRC_DIR}"
|
|
||||||
-B "${ZLIB_BUILD_DIR}"
|
|
||||||
-DCMAKE_INSTALL_PREFIX=${ZLIB_INSTALL_DIR}
|
|
||||||
RESULT_VARIABLE _zlib_cfg_res
|
|
||||||
)
|
|
||||||
if(NOT _zlib_cfg_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "zlib configure failed")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
log("Building ZLIB (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--build "${ZLIB_BUILD_DIR}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _zlib_build_res
|
|
||||||
)
|
|
||||||
if(NOT _zlib_build_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "ZLIB build failed for configuration ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Installing ZLIB (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--install "${ZLIB_BUILD_DIR}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _zlib_inst_res
|
|
||||||
)
|
|
||||||
if(NOT _zlib_inst_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "ZLIB install failed for configuration ${cfg}")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# ИСПРАВЛЕНИЕ: Используем свойства для конкретных конфигураций
|
|
||||||
add_library(zlib_external_lib UNKNOWN IMPORTED GLOBAL)
|
|
||||||
set_target_properties(zlib_external_lib PROPERTIES
|
|
||||||
# Динамическая линковка (если zlib.lib - это импорт-библиотека для zlibd.dll)
|
|
||||||
#IMPORTED_LOCATION_DEBUG "${ZLIB_INSTALL_DIR}/lib/zlibd.lib"
|
|
||||||
#IMPORTED_LOCATION_RELEASE "${ZLIB_INSTALL_DIR}/lib/zlib.lib"
|
|
||||||
|
|
||||||
# Можно также указать статические библиотеки, если вы хотите их использовать
|
|
||||||
IMPORTED_LOCATION_DEBUG "${ZLIB_INSTALL_DIR}/lib/zsd.lib"
|
|
||||||
IMPORTED_LOCATION_RELEASE "${ZLIB_INSTALL_DIR}/lib/zs.lib"
|
|
||||||
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INSTALL_DIR}/include"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 2) SDL2 (release-2.32.10.zip → SDL-release-2.32.10) - без изменений
|
|
||||||
# ===========================================
|
|
||||||
set(SDL2_SRC_DIR "${THIRDPARTY_DIR}/SDL-release-2.32.10")
|
|
||||||
set(SDL2_BUILD_DIR "${SDL2_SRC_DIR}/build")
|
|
||||||
set(SDL2_INSTALL_DIR "${SDL2_SRC_DIR}/install")
|
|
||||||
|
|
||||||
file(MAKE_DIRECTORY "${SDL2_BUILD_DIR}")
|
|
||||||
|
|
||||||
set(_have_sdl2 FALSE)
|
|
||||||
foreach(candidate
|
|
||||||
"${SDL2_INSTALL_DIR}/lib/SDL2.lib"
|
|
||||||
"${SDL2_INSTALL_DIR}/lib/SDL2-static.lib"
|
|
||||||
"${SDL2_INSTALL_DIR}/lib/SDL2d.lib"
|
|
||||||
)
|
|
||||||
if(EXISTS "${candidate}")
|
|
||||||
set(_have_sdl2 TRUE)
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(NOT _have_sdl2)
|
|
||||||
log("Configuring SDL2 ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
-G "${CMAKE_GENERATOR}"
|
|
||||||
-S "${SDL2_SRC_DIR}"
|
|
||||||
-B "${SDL2_BUILD_DIR}"
|
|
||||||
-DCMAKE_INSTALL_PREFIX=${SDL2_INSTALL_DIR}
|
|
||||||
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR} # путь к zlib для SDL2
|
|
||||||
RESULT_VARIABLE _sdl_cfg_res
|
|
||||||
)
|
|
||||||
if(NOT _sdl_cfg_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "SDL2 configure failed")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# --- ИЗМЕНЕНИЕ: Цикл по конфигурациям Debug и Release ---
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
log("Building SDL2 (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--build "${SDL2_BUILD_DIR}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _sdl_build_res
|
|
||||||
)
|
|
||||||
if(NOT _sdl_build_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "SDL2 build failed for configuration ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Installing SDL2 (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--install "${SDL2_BUILD_DIR}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _sdl_inst_res
|
|
||||||
)
|
|
||||||
if(NOT _sdl_inst_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "SDL2 install failed for configuration ${cfg}")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
# ------------------------------------------------------
|
|
||||||
endif()
|
|
||||||
|
|
||||||
|
|
||||||
# ИСПРАВЛЕНИЕ: SDL2: Используем свойства для конкретных конфигураций
|
|
||||||
add_library(SDL2_external_lib UNKNOWN IMPORTED GLOBAL)
|
|
||||||
set_target_properties(SDL2_external_lib PROPERTIES
|
|
||||||
# Динамическая линковка SDL2
|
|
||||||
IMPORTED_LOCATION_DEBUG "${SDL2_INSTALL_DIR}/lib/SDL2d.lib"
|
|
||||||
IMPORTED_LOCATION_RELEASE "${SDL2_INSTALL_DIR}/lib/SDL2.lib"
|
|
||||||
# Оба include-пути: и include, и include/SDL2
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INSTALL_DIR}/include;${SDL2_INSTALL_DIR}/include/SDL2"
|
|
||||||
)
|
|
||||||
|
|
||||||
# SDL2main (обычно статическая)
|
|
||||||
add_library(SDL2main_external_lib UNKNOWN IMPORTED GLOBAL)
|
|
||||||
set_target_properties(SDL2main_external_lib PROPERTIES
|
|
||||||
# ИСПРАВЛЕНО: Указываем пути для Debug и Release, используя
|
|
||||||
# соглашение, что Debug имеет суффикс 'd', а Release — нет.
|
|
||||||
IMPORTED_LOCATION_DEBUG "${SDL2_INSTALL_DIR}/lib/SDL2maind.lib"
|
|
||||||
IMPORTED_LOCATION_RELEASE "${SDL2_INSTALL_DIR}/lib/SDL2main.lib"
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INSTALL_DIR}/include"
|
|
||||||
)
|
|
||||||
|
|
||||||
log("-----${SDL2_INSTALL_DIR}/lib/SDL2maind.lib")
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 3) libpng (v1.6.51.zip → libpng-1.6.51) - без изменений
|
|
||||||
# ===========================================
|
|
||||||
set(LIBPNG_SRC_DIR "${THIRDPARTY_DIR}/libpng-1.6.51")
|
|
||||||
set(LIBPNG_BUILD_DIR "${LIBPNG_SRC_DIR}/build")
|
|
||||||
set(LIBPNG_INSTALL_DIR "${LIBPNG_SRC_DIR}/install") # на будущее
|
|
||||||
|
|
||||||
file(MAKE_DIRECTORY "${LIBPNG_BUILD_DIR}")
|
|
||||||
|
|
||||||
# Проверяем, есть ли уже .lib (build/Debug или install/lib)
|
|
||||||
set(_libpng_candidates
|
|
||||||
"${LIBPNG_BUILD_DIR}/Debug/libpng16_staticd.lib"
|
|
||||||
"${LIBPNG_BUILD_DIR}/Release/libpng16_static.lib"
|
|
||||||
)
|
|
||||||
|
|
||||||
set(_have_png FALSE)
|
|
||||||
foreach(candidate IN LISTS _libpng_candidates)
|
|
||||||
if(EXISTS "${candidate}")
|
|
||||||
set(_have_png TRUE)
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(NOT _have_png)
|
|
||||||
log("Configuring libpng ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
-G "${CMAKE_GENERATOR}"
|
|
||||||
-S "${LIBPNG_SRC_DIR}"
|
|
||||||
-B "${LIBPNG_BUILD_DIR}"
|
|
||||||
-DCMAKE_INSTALL_PREFIX=${LIBPNG_INSTALL_DIR}
|
|
||||||
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR}
|
|
||||||
-DZLIB_ROOT=${ZLIB_INSTALL_DIR}
|
|
||||||
RESULT_VARIABLE _png_cfg_res
|
|
||||||
)
|
|
||||||
if(NOT _png_cfg_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "libpng configure failed")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# --- ИЗМЕНЕНИЕ: Цикл по конфигурациям Debug и Release ---
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
log("Building libpng (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--build "${LIBPNG_BUILD_DIR}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _png_build_res
|
|
||||||
)
|
|
||||||
if(NOT _png_build_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "libpng build failed for configuration ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Поскольку вы не используете "cmake --install" для libpng,
|
|
||||||
# здесь нет необходимости в дополнительном шаге установки.
|
|
||||||
# Файлы .lib будут сгенерированы в подкаталоге ${LIBPNG_BUILD_DIR}/${cfg} (например, build/Debug или build/Release).
|
|
||||||
|
|
||||||
endforeach()
|
|
||||||
# ------------------------------------------------------
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_library(libpng_external_lib UNKNOWN IMPORTED GLOBAL)
|
|
||||||
set_target_properties(libpng_external_lib PROPERTIES
|
|
||||||
# Предполагая, что libpng использует статический вариант
|
|
||||||
IMPORTED_LOCATION_DEBUG "${LIBPNG_BUILD_DIR}/Debug/libpng16_staticd.lib"
|
|
||||||
IMPORTED_LOCATION_RELEASE "${LIBPNG_BUILD_DIR}/Release/libpng16_static.lib"
|
|
||||||
|
|
||||||
# png.h, pngconf.h – в SRC, pnglibconf.h – в BUILD
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES "${LIBPNG_SRC_DIR};${LIBPNG_BUILD_DIR}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 4) libzip (v1.11.4.zip → libzip-1.11.4) - НОВАЯ ЗАВИСИМОСТЬ
|
|
||||||
# ===========================================
|
|
||||||
set(LIBZIP_SRC_DIR "${THIRDPARTY_DIR}/libzip-1.11.4")
|
|
||||||
set(LIBZIP_BUILD_DIR "${LIBZIP_SRC_DIR}/build")
|
|
||||||
#set(LIBZIP_INSTALL_DIR "${LIBZIP_SRC_DIR}/install")
|
|
||||||
set(LIBZIP_BASE_DIR "${LIBZIP_SRC_DIR}/install")
|
|
||||||
|
|
||||||
file(MAKE_DIRECTORY "${LIBZIP_BUILD_DIR}")
|
|
||||||
|
|
||||||
# Проверяем, собран ли уже libzip
|
|
||||||
set(_have_zip FALSE)
|
|
||||||
foreach(candidate
|
|
||||||
"${LIBZIP_BASE_DIR}-Debug/lib/zip.lib"
|
|
||||||
"${LIBZIP_BASE_DIR}-Release/lib/zip.lib"
|
|
||||||
)
|
|
||||||
if(EXISTS "${candidate}")
|
|
||||||
set(_have_zip TRUE)
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(NOT _have_zip)
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
log("Configuring libzip (${cfg})...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
-G "${CMAKE_GENERATOR}"
|
|
||||||
-S "${LIBZIP_SRC_DIR}"
|
|
||||||
-B "${LIBZIP_SRC_DIR}/build-${cfg}"
|
|
||||||
-DCMAKE_INSTALL_PREFIX=${LIBZIP_BASE_DIR}-${cfg}
|
|
||||||
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR}
|
|
||||||
-DZLIB_ROOT=${ZLIB_INSTALL_DIR}
|
|
||||||
-DENABLE_COMMONCRYPTO=OFF
|
|
||||||
-DENABLE_GNUTLS=OFF
|
|
||||||
-DENABLE_MBEDTLS=OFF
|
|
||||||
-DENABLE_OPENSSL=OFF
|
|
||||||
-DENABLE_WINDOWS_CRYPTO=OFF
|
|
||||||
-DENABLE_FUZZ=OFF
|
|
||||||
RESULT_VARIABLE _zip_cfg_res
|
|
||||||
)
|
|
||||||
if(NOT _zip_cfg_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "libzip configure failed")
|
|
||||||
endif()
|
|
||||||
log("Building libzip (${cfg}) ...")
|
|
||||||
|
|
||||||
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND} --build "${LIBZIP_SRC_DIR}/build-${cfg}" --config ${cfg} -v
|
|
||||||
RESULT_VARIABLE _zip_build_res
|
|
||||||
)
|
|
||||||
if(NOT _zip_build_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "libzip build failed for configuration ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Installing libzip (${cfg}) ...")
|
|
||||||
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND} --install "${LIBZIP_SRC_DIR}/build-${cfg}" --config ${cfg} -v
|
|
||||||
RESULT_VARIABLE _zip_inst_res
|
|
||||||
)
|
|
||||||
if(NOT _zip_inst_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "libzip install failed for configuration ${cfg}")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
|
|
||||||
add_library(libzip_external_lib UNKNOWN IMPORTED GLOBAL)
|
|
||||||
set_target_properties(libzip_external_lib PROPERTIES
|
|
||||||
IMPORTED_LOCATION_DEBUG "${LIBZIP_BASE_DIR}-Debug/lib/zip.lib" # ИСПРАВЛЕНО
|
|
||||||
IMPORTED_LOCATION_RELEASE "${LIBZIP_BASE_DIR}-Release/lib/zip.lib" # ИСПРАВЛЕНО
|
|
||||||
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES "$<IF:$<CONFIG:Debug>,${LIBZIP_BASE_DIR}-Debug/include,${LIBZIP_BASE_DIR}-Release/include>"
|
|
||||||
# libzip требует zlib для линковки
|
|
||||||
INTERFACE_LINK_LIBRARIES zlib_external_lib
|
|
||||||
)
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 5) FreeType (2.14.1) - dependency for SDL_ttf
|
|
||||||
# ===========================================
|
|
||||||
set(FREETYPE_SRC_DIR "${THIRDPARTY_DIR}/freetype-2.14.1")
|
|
||||||
set(FREETYPE_BASE_DIR "${FREETYPE_SRC_DIR}/install")
|
|
||||||
|
|
||||||
set(_have_freetype TRUE)
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
if(NOT EXISTS "${FREETYPE_BASE_DIR}-${cfg}/lib/freetype.lib" AND
|
|
||||||
NOT EXISTS "${FREETYPE_BASE_DIR}-${cfg}/lib/freetyped.lib")
|
|
||||||
set(_have_freetype FALSE)
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(NOT _have_freetype)
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
log("Configuring FreeType (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
-G "${CMAKE_GENERATOR}"
|
|
||||||
-S "${FREETYPE_SRC_DIR}"
|
|
||||||
-B "${FREETYPE_SRC_DIR}/build-${cfg}"
|
|
||||||
-DCMAKE_INSTALL_PREFIX=${FREETYPE_BASE_DIR}-${cfg}
|
|
||||||
-DCMAKE_PREFIX_PATH="${ZLIB_INSTALL_DIR};${LIBPNG_INSTALL_DIR}"
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_HarfBuzz=TRUE
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_BZip2=TRUE
|
|
||||||
-DFT_DISABLE_BROTLI=ON
|
|
||||||
-DBUILD_SHARED_LIBS=OFF
|
|
||||||
RESULT_VARIABLE _ft_cfg_res
|
|
||||||
)
|
|
||||||
if(NOT _ft_cfg_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "FreeType configure failed for ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Building FreeType (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--build "${FREETYPE_SRC_DIR}/build-${cfg}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _ft_build_res
|
|
||||||
)
|
|
||||||
if(NOT _ft_build_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "FreeType build failed for ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Installing FreeType (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--install "${FREETYPE_SRC_DIR}/build-${cfg}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _ft_inst_res
|
|
||||||
)
|
|
||||||
if(NOT _ft_inst_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "FreeType install failed for ${cfg}")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(_ft_debug_lib "")
|
|
||||||
foreach(cand
|
|
||||||
"${FREETYPE_BASE_DIR}-Debug/lib/freetyped.lib"
|
|
||||||
"${FREETYPE_BASE_DIR}-Debug/lib/freetype.lib"
|
|
||||||
)
|
|
||||||
if(EXISTS "${cand}")
|
|
||||||
set(_ft_debug_lib "${cand}")
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
set(_ft_release_lib "")
|
|
||||||
foreach(cand
|
|
||||||
"${FREETYPE_BASE_DIR}-Release/lib/freetype.lib"
|
|
||||||
)
|
|
||||||
if(EXISTS "${cand}")
|
|
||||||
set(_ft_release_lib "${cand}")
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(_ft_debug_lib STREQUAL "" OR _ft_release_lib STREQUAL "")
|
|
||||||
message(FATAL_ERROR "FreeType libs not found in ${FREETYPE_BASE_DIR}-Debug/Release")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_library(freetype_external_lib UNKNOWN IMPORTED GLOBAL)
|
|
||||||
set_target_properties(freetype_external_lib PROPERTIES
|
|
||||||
IMPORTED_LOCATION_DEBUG "${_ft_debug_lib}"
|
|
||||||
IMPORTED_LOCATION_RELEASE "${_ft_release_lib}"
|
|
||||||
)
|
|
||||||
target_include_directories(freetype_external_lib INTERFACE
|
|
||||||
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include/freetype2,${FREETYPE_BASE_DIR}-Release/include/freetype2>"
|
|
||||||
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include,${FREETYPE_BASE_DIR}-Release/include>"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 6) SDL_ttf (2.24.0)
|
|
||||||
# ===========================================
|
|
||||||
set(SDL2TTF_SRC_DIR "${THIRDPARTY_DIR}/SDL_ttf-release-2.24.0")
|
|
||||||
set(SDL2TTF_BASE_DIR "${SDL2TTF_SRC_DIR}/install")
|
|
||||||
|
|
||||||
set(_have_sdl2ttf TRUE)
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
if(NOT EXISTS "${SDL2TTF_BASE_DIR}-${cfg}/lib/SDL2_ttf.lib" AND
|
|
||||||
NOT EXISTS "${SDL2TTF_BASE_DIR}-${cfg}/lib/SDL2_ttfd.lib")
|
|
||||||
set(_have_sdl2ttf FALSE)
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(NOT _have_sdl2ttf)
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
|
|
||||||
if(cfg STREQUAL "Debug")
|
|
||||||
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2d.lib")
|
|
||||||
else()
|
|
||||||
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2.lib")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(_FT_PREFIX "${FREETYPE_BASE_DIR}-${cfg}")
|
|
||||||
|
|
||||||
set(_FT_LIB "")
|
|
||||||
foreach(cand
|
|
||||||
"${_FT_PREFIX}/lib/freetyped.lib"
|
|
||||||
"${_FT_PREFIX}/lib/freetype.lib"
|
|
||||||
)
|
|
||||||
if(EXISTS "${cand}")
|
|
||||||
set(_FT_LIB "${cand}")
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(_FT_LIB STREQUAL "")
|
|
||||||
message(FATAL_ERROR "FreeType library not found for ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Configuring SDL_ttf (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
-G "${CMAKE_GENERATOR}"
|
|
||||||
-S "${SDL2TTF_SRC_DIR}"
|
|
||||||
-B "${SDL2TTF_SRC_DIR}/build-${cfg}"
|
|
||||||
-DCMAKE_INSTALL_PREFIX=${SDL2TTF_BASE_DIR}-${cfg}
|
|
||||||
-DCMAKE_PREFIX_PATH=${_FT_PREFIX};${SDL2_INSTALL_DIR}
|
|
||||||
-DSDL2_LIBRARY=${_SDL2_LIB}
|
|
||||||
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
|
|
||||||
-DFREETYPE_LIBRARY=${_FT_LIB}
|
|
||||||
-DFREETYPE_INCLUDE_DIR=${_FT_PREFIX}/include/freetype2
|
|
||||||
-DFREETYPE_INCLUDE_DIRS=${_FT_PREFIX}/include/freetype2
|
|
||||||
-DSDL2TTF_VENDORED=OFF
|
|
||||||
-DSDL2TTF_SAMPLES=OFF
|
|
||||||
RESULT_VARIABLE _ttf_cfg_res
|
|
||||||
)
|
|
||||||
if(NOT _ttf_cfg_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "SDL_ttf configure failed for ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Building SDL_ttf (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--build "${SDL2TTF_SRC_DIR}/build-${cfg}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _ttf_build_res
|
|
||||||
)
|
|
||||||
if(NOT _ttf_build_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "SDL_ttf build failed for ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Installing SDL_ttf (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--install "${SDL2TTF_SRC_DIR}/build-${cfg}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _ttf_inst_res
|
|
||||||
)
|
|
||||||
if(NOT _ttf_inst_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "SDL_ttf install failed for ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
endforeach()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(_ttf_debug_lib "")
|
|
||||||
foreach(cand
|
|
||||||
"${SDL2TTF_BASE_DIR}-Debug/lib/SDL2_ttfd.lib"
|
|
||||||
"${SDL2TTF_BASE_DIR}-Debug/lib/SDL2_ttf.lib"
|
|
||||||
)
|
|
||||||
if(EXISTS "${cand}")
|
|
||||||
set(_ttf_debug_lib "${cand}")
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
set(_ttf_release_lib "")
|
|
||||||
foreach(cand
|
|
||||||
"${SDL2TTF_BASE_DIR}-Release/lib/SDL2_ttf.lib"
|
|
||||||
)
|
|
||||||
if(EXISTS "${cand}")
|
|
||||||
set(_ttf_release_lib "${cand}")
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(_ttf_debug_lib STREQUAL "" OR _ttf_release_lib STREQUAL "")
|
|
||||||
message(FATAL_ERROR "SDL_ttf libs not found in install-Debug / install-Release")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_library(SDL2_ttf_external_lib UNKNOWN IMPORTED GLOBAL)
|
|
||||||
set_target_properties(SDL2_ttf_external_lib PROPERTIES
|
|
||||||
IMPORTED_LOCATION_DEBUG "${_ttf_debug_lib}"
|
|
||||||
IMPORTED_LOCATION_RELEASE "${_ttf_release_lib}"
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES
|
|
||||||
"$<IF:$<CONFIG:Debug>,${SDL2TTF_BASE_DIR}-Debug/include,${SDL2TTF_BASE_DIR}-Release/include>;$<IF:$<CONFIG:Debug>,${SDL2TTF_BASE_DIR}-Debug/include/SDL2,${SDL2TTF_BASE_DIR}-Release/include/SDL2>"
|
|
||||||
INTERFACE_LINK_LIBRARIES
|
|
||||||
"SDL2_external_lib;freetype_external_lib"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 7) Eigen (5.0.0.zip → eigen-5.0.0) - HEADER-ONLY
|
|
||||||
# ===========================================
|
|
||||||
set(EIGEN_SRC_DIR "${THIRDPARTY_DIR}/eigen-5.0.0")
|
|
||||||
|
|
||||||
if(NOT TARGET eigen_external_lib)
|
|
||||||
add_library(eigen_external_lib INTERFACE)
|
|
||||||
target_include_directories(eigen_external_lib INTERFACE "${EIGEN_SRC_DIR}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 8) Boost (1.90.0) - HEADER-ONLY
|
|
||||||
# ===========================================
|
|
||||||
set(BOOST_VERSION "1.90.0")
|
|
||||||
set(BOOST_ARCHIVE_NAME "boost_1_90_0.zip")
|
|
||||||
set(BOOST_ARCHIVE "${THIRDPARTY_DIR}/${BOOST_ARCHIVE_NAME}")
|
|
||||||
# Внутри архива папка называется boost_1_90_0
|
|
||||||
set(BOOST_SRC_DIR "${THIRDPARTY_DIR}/boost_1_90_0")
|
|
||||||
|
|
||||||
if(NOT TARGET boost_external_lib)
|
|
||||||
add_library(boost_external_lib INTERFACE)
|
|
||||||
# Boost заголовки находятся непосредственно в корне распакованной папки
|
|
||||||
target_include_directories(boost_external_lib INTERFACE "${BOOST_SRC_DIR}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 9) Lua (5.5.0) - embedded scripting language
|
|
||||||
# ===========================================
|
|
||||||
set(LUA_SRC_DIR "${THIRDPARTY_DIR}/lua-5.4.8")
|
|
||||||
|
|
||||||
if(NOT TARGET lua_static)
|
|
||||||
file(GLOB LUA_SOURCES "${LUA_SRC_DIR}/*.c")
|
|
||||||
# Exclude the standalone interpreter, compiler, and unity-build wrapper.
|
|
||||||
# onelua.c #includes all other .c files — compiling it alongside them
|
|
||||||
# causes every symbol to be defined twice.
|
|
||||||
list(REMOVE_ITEM LUA_SOURCES
|
|
||||||
"${LUA_SRC_DIR}/lua.c"
|
|
||||||
"${LUA_SRC_DIR}/luac.c"
|
|
||||||
"${LUA_SRC_DIR}/onelua.c"
|
|
||||||
)
|
|
||||||
|
|
||||||
add_library(lua_static STATIC ${LUA_SOURCES})
|
|
||||||
target_include_directories(lua_static PUBLIC "${LUA_SRC_DIR}")
|
|
||||||
target_compile_definitions(lua_static PRIVATE _CRT_SECURE_NO_WARNINGS)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 10) sol2 (3.3.0) - header-only C++ bindings for Lua
|
|
||||||
# ===========================================
|
|
||||||
set(SOL2_SRC_DIR "${THIRDPARTY_DIR}/sol2-3.3.0")
|
|
||||||
|
|
||||||
# Apply patch for Clang/Emscripten compatibility in optional<T&>::emplace().
|
|
||||||
# The sentinel file prevents re-applying on subsequent cmake runs.
|
|
||||||
set(_sol2_sentinel "${SOL2_SRC_DIR}/.patched")
|
|
||||||
if(NOT EXISTS "${_sol2_sentinel}")
|
|
||||||
find_package(Git QUIET)
|
|
||||||
if(GIT_FOUND)
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${GIT_EXECUTABLE} apply --ignore-whitespace
|
|
||||||
"${CMAKE_CURRENT_LIST_DIR}/patches/sol2-3.3.0-clang-optional.patch"
|
|
||||||
WORKING_DIRECTORY "${SOL2_SRC_DIR}"
|
|
||||||
RESULT_VARIABLE _sol2_patch_res
|
|
||||||
)
|
|
||||||
if(_sol2_patch_res EQUAL 0)
|
|
||||||
file(WRITE "${_sol2_sentinel}" "patched\n")
|
|
||||||
message(STATUS "Applied sol2 Clang optional patch")
|
|
||||||
else()
|
|
||||||
message(WARNING "sol2 patch failed (exit ${_sol2_patch_res}) — Clang/Emscripten builds may not compile")
|
|
||||||
endif()
|
|
||||||
else()
|
|
||||||
message(WARNING "Git not found — cannot apply sol2 patch automatically. "
|
|
||||||
"Apply cmake/patches/sol2-3.3.0-clang-optional.patch manually.")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT TARGET sol2_external_lib)
|
|
||||||
add_library(sol2_external_lib INTERFACE)
|
|
||||||
target_include_directories(sol2_external_lib INTERFACE "${SOL2_SRC_DIR}/include")
|
|
||||||
target_link_libraries(sol2_external_lib INTERFACE lua_static)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 11) SDL2_mixer (2.8.0) – сборка из исходников
|
|
||||||
# ===========================================
|
|
||||||
set(SDL2MIXER_SRC_DIR "${THIRDPARTY_DIR}/SDL_mixer-release-2.8.0")
|
|
||||||
set(SDL2MIXER_BASE_DIR "${SDL2MIXER_SRC_DIR}/install")
|
|
||||||
set(SDL2MIXER_BASE_DIR "${SDL2MIXER_BASE_DIR}" CACHE PATH "SDL2_mixer install base directory" FORCE)
|
|
||||||
|
|
||||||
set(_have_sdl2mixer TRUE)
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
if(NOT EXISTS "${SDL2MIXER_BASE_DIR}-${cfg}/lib/SDL2_mixer.lib" AND
|
|
||||||
NOT EXISTS "${SDL2MIXER_BASE_DIR}-${cfg}/lib/SDL2_mixerd.lib")
|
|
||||||
set(_have_sdl2mixer FALSE)
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(NOT _have_sdl2mixer)
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
if(cfg STREQUAL "Debug")
|
|
||||||
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2d.lib")
|
|
||||||
else()
|
|
||||||
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2.lib")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Configuring SDL2_mixer (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
-G "${CMAKE_GENERATOR}"
|
|
||||||
-S "${SDL2MIXER_SRC_DIR}"
|
|
||||||
-B "${SDL2MIXER_SRC_DIR}/build-${cfg}"
|
|
||||||
-DCMAKE_INSTALL_PREFIX=${SDL2MIXER_BASE_DIR}-${cfg}
|
|
||||||
-DCMAKE_PREFIX_PATH=${SDL2_INSTALL_DIR}
|
|
||||||
-DSDL2_LIBRARY=${_SDL2_LIB}
|
|
||||||
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
|
|
||||||
-DSDL2MIXER_DEPS_SHARED=OFF
|
|
||||||
-DSDL2MIXER_VENDORED=ON
|
|
||||||
-DSDL2MIXER_SAMPLES=OFF
|
|
||||||
-DSDL2MIXER_MUSIC_CMD=OFF
|
|
||||||
-DSDL2MIXER_MOD=OFF
|
|
||||||
-DSDL2MIXER_MIDI=OFF
|
|
||||||
-DSDL2MIXER_OPUS=OFF
|
|
||||||
-DSDL2MIXER_WAVPACK=OFF
|
|
||||||
-DSDL2MIXER_MP3_MPG123=OFF
|
|
||||||
-DSDL2MIXER_MP3_DRMP3=ON
|
|
||||||
-DSDL2MIXER_FLAC_DRFLAC=ON
|
|
||||||
-DSDL2MIXER_OGG_STB=ON
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_OGG=TRUE
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_Vorbis=TRUE
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_FLAC=TRUE
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_MPG123=TRUE
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_LibModPlug=TRUE
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_FluidLite=TRUE
|
|
||||||
RESULT_VARIABLE _mixer_cfg_res
|
|
||||||
OUTPUT_VARIABLE _mixer_cfg_out
|
|
||||||
ERROR_VARIABLE _mixer_cfg_err
|
|
||||||
)
|
|
||||||
if(NOT _mixer_cfg_res EQUAL 0)
|
|
||||||
message(STATUS "SDL2_mixer configure stdout: ${_mixer_cfg_out}")
|
|
||||||
message(STATUS "SDL2_mixer configure stderr: ${_mixer_cfg_err}")
|
|
||||||
message(FATAL_ERROR "SDL2_mixer configure failed for ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Building SDL2_mixer (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--build "${SDL2MIXER_SRC_DIR}/build-${cfg}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _mixer_build_res
|
|
||||||
)
|
|
||||||
if(NOT _mixer_build_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "SDL2_mixer build failed for ${cfg}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
log("Installing SDL2_mixer (${cfg}) ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
--install "${SDL2MIXER_SRC_DIR}/build-${cfg}" --config ${cfg}
|
|
||||||
RESULT_VARIABLE _mixer_inst_res
|
|
||||||
)
|
|
||||||
if(NOT _mixer_inst_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "SDL2_mixer install failed for ${cfg}")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(_mixer_debug_lib "")
|
|
||||||
foreach(cand
|
|
||||||
"${SDL2MIXER_BASE_DIR}-Debug/lib/SDL2_mixerd.lib"
|
|
||||||
"${SDL2MIXER_BASE_DIR}-Debug/lib/SDL2_mixer.lib"
|
|
||||||
)
|
|
||||||
if(EXISTS "${cand}")
|
|
||||||
set(_mixer_debug_lib "${cand}")
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
set(_mixer_release_lib "")
|
|
||||||
foreach(cand
|
|
||||||
"${SDL2MIXER_BASE_DIR}-Release/lib/SDL2_mixer.lib"
|
|
||||||
)
|
|
||||||
if(EXISTS "${cand}")
|
|
||||||
set(_mixer_release_lib "${cand}")
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(_mixer_debug_lib STREQUAL "" OR _mixer_release_lib STREQUAL "")
|
|
||||||
message(FATAL_ERROR "SDL2_mixer libs not found in ${SDL2MIXER_BASE_DIR}-Debug/Release")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_library(SDL2_mixer_external_lib UNKNOWN IMPORTED GLOBAL)
|
|
||||||
set_target_properties(SDL2_mixer_external_lib PROPERTIES
|
|
||||||
IMPORTED_LOCATION_DEBUG "${_mixer_debug_lib}"
|
|
||||||
IMPORTED_LOCATION_RELEASE "${_mixer_release_lib}"
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES
|
|
||||||
"$<IF:$<CONFIG:Debug>,${SDL2MIXER_BASE_DIR}-Debug/include,${SDL2MIXER_BASE_DIR}-Release/include>"
|
|
||||||
INTERFACE_LINK_LIBRARIES
|
|
||||||
"SDL2_external_lib"
|
|
||||||
)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
--- a/include/sol/optional_implementation.hpp
|
|
||||||
+++ b/include/sol/optional_implementation.hpp
|
|
||||||
@@ -2189,7 +2189,10 @@
|
|
||||||
template <class... Args>
|
|
||||||
T& emplace(Args&&... args) noexcept {
|
|
||||||
static_assert(std::is_constructible<T, Args&&...>::value, "T must be constructible with Args");
|
|
||||||
|
|
||||||
*this = nullopt;
|
|
||||||
- this->construct(std::forward<Args>(args)...);
|
|
||||||
+ // Reference specialization stores a pointer; set it directly.
|
|
||||||
+ // construct() only exists in the non-reference specialization.
|
|
||||||
+ m_value = std::addressof(std::forward<Args>(args)...);
|
|
||||||
+ return *m_value;
|
|
||||||
}
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Convert a text-based bone animation file to the BSAF binary format.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python convert_anim_to_binary.py <input.txt> <output.bin>
|
|
||||||
|
|
||||||
Binary format (BSAF v2) -- all values little-endian:
|
|
||||||
|
|
||||||
HEADER
|
|
||||||
4 bytes magic "BSAF"
|
|
||||||
uint32 version (2)
|
|
||||||
|
|
||||||
BONES
|
|
||||||
uint32 numBones
|
|
||||||
per bone:
|
|
||||||
3 x float boneStartWorld (from HEAD_LOCAL)
|
|
||||||
float boneLength
|
|
||||||
9 x float 3x3 rotation matrix (row-major)
|
|
||||||
int32 parentIndex (-1 if none)
|
|
||||||
uint32 numChildren
|
|
||||||
numChildren x int32 childIndices
|
|
||||||
|
|
||||||
BONE NAMES (v2+)
|
|
||||||
per bone:
|
|
||||||
uint32 nameLen
|
|
||||||
nameLen bytes UTF-8 name (no terminator)
|
|
||||||
|
|
||||||
VERTICES
|
|
||||||
uint32 numVertices
|
|
||||||
numVertices x 3 x float positions
|
|
||||||
|
|
||||||
UV COORDINATES
|
|
||||||
uint32 numFaces
|
|
||||||
numFaces x 6 x float 3 UV pairs per face (u0,v0,u1,v1,u2,v2)
|
|
||||||
|
|
||||||
NORMALS
|
|
||||||
numVertices x 3 x float normals
|
|
||||||
|
|
||||||
TRIANGLES
|
|
||||||
uint32 numTriangles
|
|
||||||
numTriangles x 3 x int32 vertex indices
|
|
||||||
|
|
||||||
VERTEX WEIGHTS
|
|
||||||
per vertex (numVertices):
|
|
||||||
uint32 numGroups
|
|
||||||
numGroups x (int32 boneIndex, float weight)
|
|
||||||
|
|
||||||
ANIMATION KEYFRAMES
|
|
||||||
uint32 numKeyframes
|
|
||||||
per keyframe:
|
|
||||||
int32 frameNumber
|
|
||||||
per bone (numBones, in index order 0..N-1):
|
|
||||||
3 x float location
|
|
||||||
16 x float 4x4 matrix (row-major)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import struct
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def parse_floats(line):
|
|
||||||
return [float(x) for x in re.findall(r'[-]?\d+\.\d+', line)]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_first_int(line):
|
|
||||||
m = re.search(r'\d+', line)
|
|
||||||
if m:
|
|
||||||
return int(m.group())
|
|
||||||
raise ValueError(f"No integer found in: {line}")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_children(line):
|
|
||||||
return re.findall(r"'([^']+)'", line)
|
|
||||||
|
|
||||||
|
|
||||||
def convert(input_path, output_path):
|
|
||||||
with open(input_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
idx = 0
|
|
||||||
|
|
||||||
def next_line():
|
|
||||||
nonlocal idx
|
|
||||||
line = lines[idx].rstrip()
|
|
||||||
idx += 1
|
|
||||||
return line
|
|
||||||
|
|
||||||
# --- Skip armature matrix (5 lines) ---
|
|
||||||
for _ in range(5):
|
|
||||||
next_line()
|
|
||||||
|
|
||||||
# --- Bone count ---
|
|
||||||
line = next_line() # "=== Armature Bones: 65"
|
|
||||||
num_bones = parse_first_int(line)
|
|
||||||
|
|
||||||
bone_names = []
|
|
||||||
bones = []
|
|
||||||
bone_parent_names = []
|
|
||||||
bone_children_names = []
|
|
||||||
|
|
||||||
for _ in range(num_bones):
|
|
||||||
bone = {}
|
|
||||||
|
|
||||||
# "Bone: mixamorig:Hips"
|
|
||||||
line = next_line()
|
|
||||||
bone_name = line[6:]
|
|
||||||
bone_names.append(bone_name)
|
|
||||||
|
|
||||||
# " HEAD_LOCAL: <Vector (x, y, z)>"
|
|
||||||
line = next_line()
|
|
||||||
bone['head'] = parse_floats(line)[:3]
|
|
||||||
|
|
||||||
# " TAIL_LOCAL: ..." -- skip
|
|
||||||
next_line()
|
|
||||||
|
|
||||||
# " Length: 0.123"
|
|
||||||
line = next_line()
|
|
||||||
bone['length'] = parse_floats(line)[0]
|
|
||||||
|
|
||||||
# 3x3 matrix (3 rows)
|
|
||||||
mat = []
|
|
||||||
for _ in range(3):
|
|
||||||
mat.extend(parse_floats(next_line()))
|
|
||||||
bone['matrix_3x3'] = mat
|
|
||||||
|
|
||||||
# " Parent: None" or " Parent: boneName"
|
|
||||||
line = next_line()
|
|
||||||
if line == " Parent: None":
|
|
||||||
bone_parent_names.append(None)
|
|
||||||
else:
|
|
||||||
bone_parent_names.append(line[10:])
|
|
||||||
|
|
||||||
# " Children: ['a', 'b'] or []"
|
|
||||||
line = next_line()
|
|
||||||
bone_children_names.append(parse_children(line))
|
|
||||||
|
|
||||||
bones.append(bone)
|
|
||||||
|
|
||||||
# Build name -> index map
|
|
||||||
name_to_idx = {name: i for i, name in enumerate(bone_names)}
|
|
||||||
|
|
||||||
# Resolve parent / child indices
|
|
||||||
for i in range(num_bones):
|
|
||||||
if bone_parent_names[i] is None:
|
|
||||||
bones[i]['parent'] = -1
|
|
||||||
else:
|
|
||||||
bones[i]['parent'] = name_to_idx[bone_parent_names[i]]
|
|
||||||
bones[i]['children'] = [name_to_idx[c] for c in bone_children_names[i]]
|
|
||||||
|
|
||||||
# --- Vertices ---
|
|
||||||
line = next_line() # "===Vertices: 5140"
|
|
||||||
num_vertices = parse_first_int(line)
|
|
||||||
|
|
||||||
vertices = []
|
|
||||||
for _ in range(num_vertices):
|
|
||||||
vertices.append(parse_floats(next_line())[:3])
|
|
||||||
|
|
||||||
# --- UV Coordinates ---
|
|
||||||
next_line() # "===UV Coordinates:"
|
|
||||||
line = next_line() # "Face count: 8602"
|
|
||||||
num_faces = parse_first_int(line)
|
|
||||||
|
|
||||||
uvs = []
|
|
||||||
for _ in range(num_faces):
|
|
||||||
next_line() # "Face N"
|
|
||||||
next_line() # "UV Count: 3"
|
|
||||||
face_uvs = []
|
|
||||||
for _ in range(3):
|
|
||||||
face_uvs.extend(parse_floats(next_line())[:2])
|
|
||||||
uvs.append(face_uvs) # 6 floats
|
|
||||||
|
|
||||||
# --- Normals ---
|
|
||||||
next_line() # "===Normals:"
|
|
||||||
|
|
||||||
normals = []
|
|
||||||
for _ in range(num_vertices):
|
|
||||||
normals.append(parse_floats(next_line())[:3])
|
|
||||||
|
|
||||||
# --- Triangles ---
|
|
||||||
line = next_line() # "===Triangles: 8602"
|
|
||||||
num_triangles = parse_first_int(line)
|
|
||||||
|
|
||||||
triangles = []
|
|
||||||
for _ in range(num_triangles):
|
|
||||||
line = next_line()
|
|
||||||
ints = [int(x) for x in re.findall(r'[-]?\d+', line)]
|
|
||||||
triangles.append(ints[:3])
|
|
||||||
|
|
||||||
# --- Vertex Weights ---
|
|
||||||
next_line() # "=== Vertex Weights ..."
|
|
||||||
|
|
||||||
vertex_weights = []
|
|
||||||
for _ in range(num_vertices):
|
|
||||||
next_line() # "Vertex N:"
|
|
||||||
line = next_line() # "Vertex groups: 2"
|
|
||||||
num_groups = parse_first_int(line)
|
|
||||||
|
|
||||||
groups = []
|
|
||||||
for _ in range(num_groups):
|
|
||||||
line = next_line()
|
|
||||||
m = re.search(r"'([^']+)'.*?([-]?\d+\.\d+)", line)
|
|
||||||
bone_name = m.group(1)
|
|
||||||
weight = float(m.group(2))
|
|
||||||
groups.append((name_to_idx[bone_name], weight))
|
|
||||||
|
|
||||||
vertex_weights.append(groups)
|
|
||||||
|
|
||||||
# --- Animation Keyframes ---
|
|
||||||
next_line() # "=== Animation Keyframes ==="
|
|
||||||
next_line() # "=== Bone Transforms per Keyframe ==="
|
|
||||||
line = next_line() # "Keyframes: 32"
|
|
||||||
num_keyframes = parse_first_int(line)
|
|
||||||
|
|
||||||
keyframes = []
|
|
||||||
for _ in range(num_keyframes):
|
|
||||||
line = next_line() # "Frame: 0"
|
|
||||||
frame_number = parse_first_int(line)
|
|
||||||
|
|
||||||
bone_data = {}
|
|
||||||
for _ in range(num_bones):
|
|
||||||
line = next_line() # " Bone: mixamorig:Hips"
|
|
||||||
bone_name = line.strip()
|
|
||||||
if bone_name.startswith("Bone: "):
|
|
||||||
bone_name = bone_name[6:]
|
|
||||||
bone_idx = name_to_idx[bone_name]
|
|
||||||
|
|
||||||
# Location
|
|
||||||
location = parse_floats(next_line())[:3]
|
|
||||||
|
|
||||||
# Rotation (skip)
|
|
||||||
next_line()
|
|
||||||
|
|
||||||
# " Matrix:" (skip header)
|
|
||||||
next_line()
|
|
||||||
|
|
||||||
# 4 rows of 4 floats
|
|
||||||
matrix = []
|
|
||||||
for _ in range(4):
|
|
||||||
matrix.extend(parse_floats(next_line()))
|
|
||||||
|
|
||||||
bone_data[bone_idx] = {
|
|
||||||
'location': location,
|
|
||||||
'matrix': matrix,
|
|
||||||
}
|
|
||||||
|
|
||||||
keyframes.append((frame_number, bone_data))
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# Write binary file
|
|
||||||
# ================================================================
|
|
||||||
with open(output_path, 'wb') as out:
|
|
||||||
# Header
|
|
||||||
out.write(b'BSAF')
|
|
||||||
out.write(struct.pack('<I', 2))
|
|
||||||
|
|
||||||
# Bones
|
|
||||||
out.write(struct.pack('<I', num_bones))
|
|
||||||
for i in range(num_bones):
|
|
||||||
b = bones[i]
|
|
||||||
out.write(struct.pack('<3f', *b['head']))
|
|
||||||
out.write(struct.pack('<f', b['length']))
|
|
||||||
out.write(struct.pack('<9f', *b['matrix_3x3']))
|
|
||||||
out.write(struct.pack('<i', b['parent']))
|
|
||||||
out.write(struct.pack('<I', len(b['children'])))
|
|
||||||
for c in b['children']:
|
|
||||||
out.write(struct.pack('<i', c))
|
|
||||||
|
|
||||||
# Bone names (v2+)
|
|
||||||
for name in bone_names:
|
|
||||||
name_bytes = name.encode('utf-8')
|
|
||||||
out.write(struct.pack('<I', len(name_bytes)))
|
|
||||||
out.write(name_bytes)
|
|
||||||
|
|
||||||
# Vertices
|
|
||||||
out.write(struct.pack('<I', num_vertices))
|
|
||||||
for v in vertices:
|
|
||||||
out.write(struct.pack('<3f', *v))
|
|
||||||
|
|
||||||
# UV Coordinates
|
|
||||||
out.write(struct.pack('<I', num_faces))
|
|
||||||
for uv in uvs:
|
|
||||||
out.write(struct.pack('<6f', *uv))
|
|
||||||
|
|
||||||
# Normals
|
|
||||||
for n in normals:
|
|
||||||
out.write(struct.pack('<3f', *n))
|
|
||||||
|
|
||||||
# Triangles
|
|
||||||
out.write(struct.pack('<I', num_triangles))
|
|
||||||
for t in triangles:
|
|
||||||
out.write(struct.pack('<3i', *t))
|
|
||||||
|
|
||||||
# Vertex Weights
|
|
||||||
for vw in vertex_weights:
|
|
||||||
out.write(struct.pack('<I', len(vw)))
|
|
||||||
for bone_idx, weight in vw:
|
|
||||||
out.write(struct.pack('<if', bone_idx, weight))
|
|
||||||
|
|
||||||
# Animation Keyframes
|
|
||||||
out.write(struct.pack('<I', num_keyframes))
|
|
||||||
for frame_num, bone_data in keyframes:
|
|
||||||
out.write(struct.pack('<i', frame_num))
|
|
||||||
for i in range(num_bones):
|
|
||||||
bd = bone_data[i]
|
|
||||||
out.write(struct.pack('<3f', *bd['location']))
|
|
||||||
out.write(struct.pack('<16f', *bd['matrix']))
|
|
||||||
|
|
||||||
input_size = sum(len(l) for l in lines)
|
|
||||||
import os
|
|
||||||
output_size = os.path.getsize(output_path)
|
|
||||||
print(f"Converted: {input_path} ({input_size:,} bytes text) -> {output_path} ({output_size:,} bytes binary)")
|
|
||||||
print(f" Bones: {num_bones}, Vertices: {num_vertices}, Faces: {num_faces}, "
|
|
||||||
f"Triangles: {num_triangles}, Keyframes: {num_keyframes}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) != 3:
|
|
||||||
print(f"Usage: {sys.argv[0]} <input.txt> <output.bin>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
convert(sys.argv[1], sys.argv[2])
|
|
||||||
@@ -1,370 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Convert a text-based multi-mesh bone animation file to the BSMF binary format.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python convert_anim_to_binary_new.py <input.txt> <output.bin>
|
|
||||||
|
|
||||||
Binary format (BSMF v2) -- all values little-endian:
|
|
||||||
|
|
||||||
HEADER
|
|
||||||
4 bytes magic "BSMF"
|
|
||||||
uint32 version (2)
|
|
||||||
|
|
||||||
ARMATURE MATRIX
|
|
||||||
16 x float 4x4 matrix (row-major)
|
|
||||||
|
|
||||||
BONES
|
|
||||||
uint32 numBones
|
|
||||||
per bone:
|
|
||||||
3 x float boneStartWorld (from HEAD_LOCAL)
|
|
||||||
float boneLength
|
|
||||||
9 x float 3x3 rotation matrix (row-major)
|
|
||||||
int32 parentIndex (-1 if none)
|
|
||||||
uint32 numChildren
|
|
||||||
numChildren x int32 childIndices
|
|
||||||
|
|
||||||
BONE NAMES
|
|
||||||
per bone:
|
|
||||||
uint32 nameLen
|
|
||||||
nameLen bytes UTF-8 name (no terminator)
|
|
||||||
|
|
||||||
MESHES
|
|
||||||
uint32 numMeshes
|
|
||||||
per mesh:
|
|
||||||
uint32 nameLength
|
|
||||||
nameLength x char meshName (UTF-8, no null terminator)
|
|
||||||
|
|
||||||
VERTICES
|
|
||||||
uint32 numVertices
|
|
||||||
numVertices x 3 x float positions
|
|
||||||
|
|
||||||
UV COORDINATES
|
|
||||||
uint32 numFaces
|
|
||||||
numFaces x 6 x float 3 UV pairs per face (u0,v0,u1,v1,u2,v2)
|
|
||||||
|
|
||||||
NORMALS
|
|
||||||
numVertices x 3 x float normals
|
|
||||||
|
|
||||||
TRIANGLES
|
|
||||||
uint32 numTriangles
|
|
||||||
numTriangles x 3 x int32 vertex indices
|
|
||||||
|
|
||||||
VERTEX WEIGHTS
|
|
||||||
per vertex (numVertices):
|
|
||||||
uint32 numGroups
|
|
||||||
numGroups x (int32 boneIndex, float weight)
|
|
||||||
|
|
||||||
ANIMATION KEYFRAMES
|
|
||||||
uint32 numKeyframes
|
|
||||||
per keyframe:
|
|
||||||
int32 frameNumber
|
|
||||||
per bone (numBones, in index order 0..N-1):
|
|
||||||
3 x float location
|
|
||||||
16 x float 4x4 matrix (row-major)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import struct
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def parse_floats(line):
|
|
||||||
return [float(x) for x in re.findall(r'[-]?\d+\.\d+', line)]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_first_int(line):
|
|
||||||
m = re.search(r'\d+', line)
|
|
||||||
if m:
|
|
||||||
return int(m.group())
|
|
||||||
raise ValueError(f"No integer found in: {line}")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_children(line):
|
|
||||||
return re.findall(r"'([^']+)'", line)
|
|
||||||
|
|
||||||
|
|
||||||
def convert(input_path, output_path):
|
|
||||||
with open(input_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
idx = 0
|
|
||||||
|
|
||||||
def next_line():
|
|
||||||
nonlocal idx
|
|
||||||
line = lines[idx].rstrip()
|
|
||||||
idx += 1
|
|
||||||
return line
|
|
||||||
|
|
||||||
# --- Armature matrix header + 4 rows ---
|
|
||||||
next_line() # "=== Armature Matrix ==="
|
|
||||||
armature_matrix = []
|
|
||||||
for _ in range(4):
|
|
||||||
armature_matrix.extend(parse_floats(next_line())[:4])
|
|
||||||
|
|
||||||
# --- Bone count ---
|
|
||||||
line = next_line() # "=== Armature Bones: 65"
|
|
||||||
num_bones = parse_first_int(line)
|
|
||||||
|
|
||||||
bone_names = []
|
|
||||||
bones = []
|
|
||||||
bone_parent_names = []
|
|
||||||
bone_children_names = []
|
|
||||||
|
|
||||||
for _ in range(num_bones):
|
|
||||||
bone = {}
|
|
||||||
|
|
||||||
# "Bone: mixamorig:Hips"
|
|
||||||
line = next_line()
|
|
||||||
bone_name = line[6:]
|
|
||||||
bone_names.append(bone_name)
|
|
||||||
|
|
||||||
# " HEAD_LOCAL: <Vector (x, y, z)>"
|
|
||||||
line = next_line()
|
|
||||||
bone['head'] = parse_floats(line)[:3]
|
|
||||||
|
|
||||||
# " TAIL_LOCAL: ..." -- skip
|
|
||||||
next_line()
|
|
||||||
|
|
||||||
# " Length: 0.123"
|
|
||||||
line = next_line()
|
|
||||||
bone['length'] = parse_floats(line)[0]
|
|
||||||
|
|
||||||
# 3x3 matrix (3 rows)
|
|
||||||
mat = []
|
|
||||||
for _ in range(3):
|
|
||||||
mat.extend(parse_floats(next_line()))
|
|
||||||
bone['matrix_3x3'] = mat
|
|
||||||
|
|
||||||
# " Parent: None" or " Parent: boneName"
|
|
||||||
line = next_line()
|
|
||||||
if line == " Parent: None":
|
|
||||||
bone_parent_names.append(None)
|
|
||||||
else:
|
|
||||||
bone_parent_names.append(line[10:])
|
|
||||||
|
|
||||||
# " Children: ['a', 'b'] or []"
|
|
||||||
line = next_line()
|
|
||||||
bone_children_names.append(parse_children(line))
|
|
||||||
|
|
||||||
bones.append(bone)
|
|
||||||
|
|
||||||
# Build name -> index map
|
|
||||||
name_to_idx = {name: i for i, name in enumerate(bone_names)}
|
|
||||||
|
|
||||||
# Resolve parent / child indices
|
|
||||||
for i in range(num_bones):
|
|
||||||
if bone_parent_names[i] is None:
|
|
||||||
bones[i]['parent'] = -1
|
|
||||||
else:
|
|
||||||
bones[i]['parent'] = name_to_idx[bone_parent_names[i]]
|
|
||||||
bones[i]['children'] = [name_to_idx[c] for c in bone_children_names[i]]
|
|
||||||
|
|
||||||
# --- Multi-mesh header ---
|
|
||||||
line = next_line() # "=== TOTAL MESHES TO EXPORT: 7 ==="
|
|
||||||
num_meshes = parse_first_int(line)
|
|
||||||
|
|
||||||
meshes = []
|
|
||||||
|
|
||||||
for _ in range(num_meshes):
|
|
||||||
# "=== Mesh Object: Name ==="
|
|
||||||
line = next_line()
|
|
||||||
m = re.match(r"===\s*Mesh Object:\s*(.+?)\s*===$", line)
|
|
||||||
if not m:
|
|
||||||
raise ValueError(f"Invalid mesh header: {line}")
|
|
||||||
mesh_name = m.group(1)
|
|
||||||
|
|
||||||
# --- Vertices ---
|
|
||||||
line = next_line() # "===Vertices: N"
|
|
||||||
num_vertices = parse_first_int(line)
|
|
||||||
|
|
||||||
vertices = []
|
|
||||||
for _ in range(num_vertices):
|
|
||||||
vertices.append(parse_floats(next_line())[:3])
|
|
||||||
|
|
||||||
# --- UV Coordinates ---
|
|
||||||
next_line() # "===UV Coordinates:"
|
|
||||||
line = next_line() # "Face count: M"
|
|
||||||
num_faces = parse_first_int(line)
|
|
||||||
|
|
||||||
uvs = []
|
|
||||||
for _ in range(num_faces):
|
|
||||||
next_line() # "Face N"
|
|
||||||
next_line() # "UV Count: 3"
|
|
||||||
face_uvs = []
|
|
||||||
for _ in range(3):
|
|
||||||
face_uvs.extend(parse_floats(next_line())[:2])
|
|
||||||
uvs.append(face_uvs)
|
|
||||||
|
|
||||||
# --- Normals ---
|
|
||||||
next_line() # "===Normals:"
|
|
||||||
normals = []
|
|
||||||
for _ in range(num_vertices):
|
|
||||||
normals.append(parse_floats(next_line())[:3])
|
|
||||||
|
|
||||||
# --- Triangles ---
|
|
||||||
line = next_line() # "===Triangles: M"
|
|
||||||
num_triangles = parse_first_int(line)
|
|
||||||
|
|
||||||
triangles = []
|
|
||||||
for _ in range(num_triangles):
|
|
||||||
line = next_line()
|
|
||||||
ints = [int(x) for x in re.findall(r'[-]?\d+', line)]
|
|
||||||
triangles.append(ints[:3])
|
|
||||||
|
|
||||||
# --- Vertex Weights ---
|
|
||||||
next_line() # "=== Vertex Weights (Max 5 bones per vertex) ==="
|
|
||||||
vertex_weights = []
|
|
||||||
for _ in range(num_vertices):
|
|
||||||
next_line() # "Vertex N:"
|
|
||||||
line = next_line() # "Vertex groups: K"
|
|
||||||
num_groups = parse_first_int(line)
|
|
||||||
|
|
||||||
groups = []
|
|
||||||
for _ in range(num_groups):
|
|
||||||
line = next_line()
|
|
||||||
m = re.search(r"'([^']+)'.*?([-]?\d+\.\d+)", line)
|
|
||||||
bone_name = m.group(1)
|
|
||||||
weight = float(m.group(2))
|
|
||||||
groups.append((name_to_idx[bone_name], weight))
|
|
||||||
|
|
||||||
vertex_weights.append(groups)
|
|
||||||
|
|
||||||
meshes.append({
|
|
||||||
'name': mesh_name,
|
|
||||||
'num_vertices': num_vertices,
|
|
||||||
'vertices': vertices,
|
|
||||||
'num_faces': num_faces,
|
|
||||||
'uvs': uvs,
|
|
||||||
'normals': normals,
|
|
||||||
'num_triangles': num_triangles,
|
|
||||||
'triangles': triangles,
|
|
||||||
'vertex_weights': vertex_weights,
|
|
||||||
})
|
|
||||||
|
|
||||||
# --- Animation Keyframes ---
|
|
||||||
next_line() # "=== Animation Keyframes ==="
|
|
||||||
next_line() # "=== Bone Transforms per Keyframe ==="
|
|
||||||
line = next_line() # "Keyframes: N"
|
|
||||||
num_keyframes = parse_first_int(line)
|
|
||||||
|
|
||||||
keyframes = []
|
|
||||||
for _ in range(num_keyframes):
|
|
||||||
line = next_line() # "Frame: N"
|
|
||||||
frame_number = parse_first_int(line)
|
|
||||||
|
|
||||||
bone_data = {}
|
|
||||||
for _ in range(num_bones):
|
|
||||||
line = next_line() # " Bone: mixamorig:Hips"
|
|
||||||
bone_name = line.strip()
|
|
||||||
if bone_name.startswith("Bone: "):
|
|
||||||
bone_name = bone_name[6:]
|
|
||||||
bone_idx = name_to_idx[bone_name]
|
|
||||||
|
|
||||||
# Location
|
|
||||||
location = parse_floats(next_line())[:3]
|
|
||||||
|
|
||||||
# Rotation (skip)
|
|
||||||
next_line()
|
|
||||||
|
|
||||||
# " Matrix:" (skip header)
|
|
||||||
next_line()
|
|
||||||
|
|
||||||
# 4 rows of 4 floats
|
|
||||||
matrix = []
|
|
||||||
for _ in range(4):
|
|
||||||
matrix.extend(parse_floats(next_line()))
|
|
||||||
|
|
||||||
bone_data[bone_idx] = {
|
|
||||||
'location': location,
|
|
||||||
'matrix': matrix,
|
|
||||||
}
|
|
||||||
|
|
||||||
keyframes.append((frame_number, bone_data))
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# Write binary file
|
|
||||||
# ================================================================
|
|
||||||
with open(output_path, 'wb') as out:
|
|
||||||
# Header
|
|
||||||
out.write(b'BSMF')
|
|
||||||
out.write(struct.pack('<I', 2))
|
|
||||||
|
|
||||||
# Armature matrix (16 floats, row-major)
|
|
||||||
out.write(struct.pack('<16f', *armature_matrix))
|
|
||||||
|
|
||||||
# Bones
|
|
||||||
out.write(struct.pack('<I', num_bones))
|
|
||||||
for i in range(num_bones):
|
|
||||||
b = bones[i]
|
|
||||||
out.write(struct.pack('<3f', *b['head']))
|
|
||||||
out.write(struct.pack('<f', b['length']))
|
|
||||||
out.write(struct.pack('<9f', *b['matrix_3x3']))
|
|
||||||
out.write(struct.pack('<i', b['parent']))
|
|
||||||
out.write(struct.pack('<I', len(b['children'])))
|
|
||||||
for c in b['children']:
|
|
||||||
out.write(struct.pack('<i', c))
|
|
||||||
|
|
||||||
# Bone names
|
|
||||||
for name in bone_names:
|
|
||||||
name_bytes = name.encode('utf-8')
|
|
||||||
out.write(struct.pack('<I', len(name_bytes)))
|
|
||||||
out.write(name_bytes)
|
|
||||||
|
|
||||||
# Meshes
|
|
||||||
out.write(struct.pack('<I', num_meshes))
|
|
||||||
for md in meshes:
|
|
||||||
name_bytes = md['name'].encode('utf-8')
|
|
||||||
out.write(struct.pack('<I', len(name_bytes)))
|
|
||||||
out.write(name_bytes)
|
|
||||||
|
|
||||||
# Vertices
|
|
||||||
out.write(struct.pack('<I', md['num_vertices']))
|
|
||||||
for v in md['vertices']:
|
|
||||||
out.write(struct.pack('<3f', *v))
|
|
||||||
|
|
||||||
# UV Coordinates
|
|
||||||
out.write(struct.pack('<I', md['num_faces']))
|
|
||||||
for uv in md['uvs']:
|
|
||||||
out.write(struct.pack('<6f', *uv))
|
|
||||||
|
|
||||||
# Normals
|
|
||||||
for n in md['normals']:
|
|
||||||
out.write(struct.pack('<3f', *n))
|
|
||||||
|
|
||||||
# Triangles
|
|
||||||
out.write(struct.pack('<I', md['num_triangles']))
|
|
||||||
for t in md['triangles']:
|
|
||||||
out.write(struct.pack('<3i', *t))
|
|
||||||
|
|
||||||
# Vertex weights
|
|
||||||
for vw in md['vertex_weights']:
|
|
||||||
out.write(struct.pack('<I', len(vw)))
|
|
||||||
for bone_idx, weight in vw:
|
|
||||||
out.write(struct.pack('<if', bone_idx, weight))
|
|
||||||
|
|
||||||
# Animation Keyframes
|
|
||||||
out.write(struct.pack('<I', num_keyframes))
|
|
||||||
for frame_num, bone_data in keyframes:
|
|
||||||
out.write(struct.pack('<i', frame_num))
|
|
||||||
for i in range(num_bones):
|
|
||||||
bd = bone_data[i]
|
|
||||||
out.write(struct.pack('<3f', *bd['location']))
|
|
||||||
out.write(struct.pack('<16f', *bd['matrix']))
|
|
||||||
|
|
||||||
input_size = sum(len(l) for l in lines)
|
|
||||||
import os
|
|
||||||
output_size = os.path.getsize(output_path)
|
|
||||||
print(f"Converted: {input_path} ({input_size:,} bytes text) -> {output_path} ({output_size:,} bytes binary)")
|
|
||||||
print(f" Bones: {num_bones}, Meshes: {num_meshes}, Keyframes: {num_keyframes}")
|
|
||||||
for md in meshes:
|
|
||||||
print(f" - {md['name']}: {md['num_vertices']} verts, "
|
|
||||||
f"{md['num_faces']} faces, {md['num_triangles']} tris")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) != 3:
|
|
||||||
print(f"Usage: {sys.argv[0]} <input.txt> <output.bin>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
convert(sys.argv[1], sys.argv[2])
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
convert_config_meshes.py - Bulk-convert 3D mesh files referenced in a game object JSON config.
|
|
||||||
|
|
||||||
Reads <src_config>, converts every .txt mesh to binary (.txt.bin via BSMF format),
|
|
||||||
and writes the updated config to <dst_config> with meshPath values pointing to the
|
|
||||||
.bin files. Works for both regular game object configs and interactive object configs.
|
|
||||||
|
|
||||||
Mesh paths in the JSON are relative to the current working directory — run this
|
|
||||||
script from the project root.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python convert_config_meshes.py <src_config.json> <dst_config.json>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from convert_model_to_binary import convert
|
|
||||||
|
|
||||||
|
|
||||||
def convert_config(src_path: str, dst_path: str) -> None:
|
|
||||||
with open(src_path, 'r', encoding='utf-8') as f:
|
|
||||||
config = json.load(f)
|
|
||||||
|
|
||||||
objects = config.get("objects", [])
|
|
||||||
|
|
||||||
# Track already-converted paths so shared meshes are only processed once.
|
|
||||||
cache: dict[str, str | None] = {}
|
|
||||||
|
|
||||||
for obj in objects:
|
|
||||||
mesh_path = obj.get("meshPath")
|
|
||||||
if not mesh_path or not mesh_path.lower().endswith(".txt"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if mesh_path not in cache:
|
|
||||||
if not os.path.isfile(mesh_path):
|
|
||||||
print(f" WARNING: mesh not found, skipping: {mesh_path}")
|
|
||||||
cache[mesh_path] = None
|
|
||||||
else:
|
|
||||||
bin_path = mesh_path + ".bin"
|
|
||||||
convert(mesh_path, bin_path)
|
|
||||||
cache[mesh_path] = bin_path
|
|
||||||
|
|
||||||
if cache[mesh_path] is not None:
|
|
||||||
obj["meshPath"] = cache[mesh_path]
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(os.path.abspath(dst_path)), exist_ok=True)
|
|
||||||
with open(dst_path, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(config, f, indent=4, ensure_ascii=False)
|
|
||||||
|
|
||||||
converted_count = sum(1 for v in cache.values() if v is not None)
|
|
||||||
print(f"Saved: {dst_path} ({converted_count} mesh(es) converted, "
|
|
||||||
f"{len(cache) - converted_count} skipped)")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) != 3:
|
|
||||||
print(f"Usage: {sys.argv[0]} <src_config.json> <dst_config.json>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
convert_config(sys.argv[1], sys.argv[2])
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Convert a text-based static mesh file (.txt) to binary format (.txt.bin).
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python convert_model_to_binary.py <input.txt> [<output.bin>]
|
|
||||||
|
|
||||||
If the output path is not given it is derived by appending ".bin" to the input path,
|
|
||||||
e.g. resources/w/firebox.txt -> resources/w/firebox.txt.bin
|
|
||||||
|
|
||||||
Binary format (BSMF v1) -- all values little-endian:
|
|
||||||
|
|
||||||
HEADER
|
|
||||||
4 bytes magic "BSMF"
|
|
||||||
uint32 version (1)
|
|
||||||
uint32 numVertices
|
|
||||||
uint32 numTriangles
|
|
||||||
|
|
||||||
VERTICES (numVertices entries):
|
|
||||||
3 x float position x, y, z -- engine coordinate space
|
|
||||||
3 x float normal x, y, z -- engine coordinate space
|
|
||||||
2 x float UV u, v
|
|
||||||
|
|
||||||
TRIANGLES (numTriangles entries):
|
|
||||||
3 x uint32 vertex indices i0, i1, i2
|
|
||||||
|
|
||||||
The same Blender->engine axis swap applied by LoadFromTextFile02 is baked in here,
|
|
||||||
so the C++ binary loader can read coordinates directly without any post-processing:
|
|
||||||
engine_x = blender_y
|
|
||||||
engine_y = blender_z
|
|
||||||
engine_z = blender_x
|
|
||||||
"""
|
|
||||||
|
|
||||||
import struct
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_floats(text):
|
|
||||||
return [float(x) for x in re.findall(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?', text)]
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_ints(text):
|
|
||||||
return [int(x) for x in re.findall(r'[-]?\d+', text)]
|
|
||||||
|
|
||||||
|
|
||||||
def _swap_axes(x, y, z):
|
|
||||||
"""Blender Y-up -> engine coordinate system (mirrors LoadFromTextFile02)."""
|
|
||||||
return y, z, x
|
|
||||||
|
|
||||||
|
|
||||||
def convert(input_path, output_path):
|
|
||||||
with open(input_path, 'r', encoding='utf-8') as f:
|
|
||||||
lines = [line.rstrip('\n') for line in f]
|
|
||||||
|
|
||||||
idx = 0
|
|
||||||
|
|
||||||
def next_line():
|
|
||||||
nonlocal idx
|
|
||||||
while idx < len(lines):
|
|
||||||
line = lines[idx]
|
|
||||||
idx += 1
|
|
||||||
return line
|
|
||||||
raise EOFError("Unexpected end of file while parsing: " + input_path)
|
|
||||||
|
|
||||||
# --- Vertices ---
|
|
||||||
while True:
|
|
||||||
line = next_line()
|
|
||||||
if '===Vertices' in line:
|
|
||||||
break
|
|
||||||
|
|
||||||
m = re.search(r'\d+', line)
|
|
||||||
if not m:
|
|
||||||
raise ValueError("Could not parse vertex count from: " + line)
|
|
||||||
num_vertices = int(m.group())
|
|
||||||
|
|
||||||
positions = []
|
|
||||||
normals = []
|
|
||||||
uvs = []
|
|
||||||
|
|
||||||
for i in range(num_vertices):
|
|
||||||
line = next_line()
|
|
||||||
# V N: Pos(x, y, z) Norm(nx, ny, nz) UV(u, v)
|
|
||||||
nums = _parse_floats(line)
|
|
||||||
# nums[0] = vertex index (float-parsed), then 3 pos, 3 norm, 2 uv
|
|
||||||
if len(nums) < 9:
|
|
||||||
raise ValueError(f"Malformed vertex line {i}: {line}")
|
|
||||||
px, py, pz = _swap_axes(nums[1], nums[2], nums[3])
|
|
||||||
nx, ny, nz = _swap_axes(nums[4], nums[5], nums[6])
|
|
||||||
positions.append((px, py, pz))
|
|
||||||
normals.append((nx, ny, nz))
|
|
||||||
uvs.append((nums[7], nums[8]))
|
|
||||||
|
|
||||||
# --- Triangles ---
|
|
||||||
while True:
|
|
||||||
line = next_line()
|
|
||||||
if '===Triangles' in line:
|
|
||||||
break
|
|
||||||
|
|
||||||
m = re.search(r'\d+', line)
|
|
||||||
if not m:
|
|
||||||
raise ValueError("Could not parse triangle count from: " + line)
|
|
||||||
num_triangles = int(m.group())
|
|
||||||
|
|
||||||
triangles = []
|
|
||||||
for i in range(num_triangles):
|
|
||||||
line = next_line()
|
|
||||||
ints = _parse_ints(line)
|
|
||||||
if len(ints) != 3:
|
|
||||||
raise ValueError(f"Malformed triangle line {i}: {line}")
|
|
||||||
triangles.append(tuple(ints))
|
|
||||||
|
|
||||||
# --- Write binary ---
|
|
||||||
with open(output_path, 'wb') as out:
|
|
||||||
out.write(b'BSMF')
|
|
||||||
out.write(struct.pack('<I', 1))
|
|
||||||
out.write(struct.pack('<I', num_vertices))
|
|
||||||
out.write(struct.pack('<I', num_triangles))
|
|
||||||
|
|
||||||
for i in range(num_vertices):
|
|
||||||
out.write(struct.pack('<3f', *positions[i]))
|
|
||||||
out.write(struct.pack('<3f', *normals[i]))
|
|
||||||
out.write(struct.pack('<2f', *uvs[i]))
|
|
||||||
|
|
||||||
for tri in triangles:
|
|
||||||
out.write(struct.pack('<3I', *tri))
|
|
||||||
|
|
||||||
in_size = os.path.getsize(input_path)
|
|
||||||
out_size = os.path.getsize(output_path)
|
|
||||||
print(f"Converted: {input_path} ({in_size:,} bytes text) -> {output_path} ({out_size:,} bytes binary)")
|
|
||||||
print(f" Vertices: {num_vertices}, Triangles: {num_triangles}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) < 2 or len(sys.argv) > 3:
|
|
||||||
print(f"Usage: {sys.argv[0]} <input.txt> [<output.bin>]")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
input_path = sys.argv[1]
|
|
||||||
output_path = sys.argv[2] if len(sys.argv) == 3 else input_path + '.bin'
|
|
||||||
convert(input_path, output_path)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Convert an old single-mesh text animation file to the new multi-mesh text format.
|
|
||||||
|
|
||||||
The only structural difference is that the new format wraps the mesh block
|
|
||||||
between these two extra headers before the "===Vertices:" line:
|
|
||||||
|
|
||||||
=== TOTAL MESHES TO EXPORT: 1 ===
|
|
||||||
=== Mesh Object: Body ===
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python convert_old_anim_to_new.py <input.txt> <output.txt> [mesh_name]
|
|
||||||
|
|
||||||
If mesh_name is omitted, "Body" is used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def convert(input_path, output_path, mesh_name="Body"):
|
|
||||||
with open(input_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
# Find the first "===Vertices:" line -- that's where the bone block ends
|
|
||||||
# and the mesh block begins in the old format.
|
|
||||||
insert_at = None
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.lstrip().startswith("===Vertices:"):
|
|
||||||
insert_at = i
|
|
||||||
break
|
|
||||||
|
|
||||||
if insert_at is None:
|
|
||||||
raise RuntimeError("Could not find '===Vertices:' line in input file")
|
|
||||||
|
|
||||||
header_lines = [
|
|
||||||
"=== TOTAL MESHES TO EXPORT: 1 ===\n",
|
|
||||||
f"=== Mesh Object: {mesh_name} ===\n",
|
|
||||||
]
|
|
||||||
|
|
||||||
out_lines = lines[:insert_at] + header_lines + lines[insert_at:]
|
|
||||||
|
|
||||||
with open(output_path, 'w', encoding='utf-8') as out:
|
|
||||||
out.writelines(out_lines)
|
|
||||||
|
|
||||||
print(f"Converted: {input_path} -> {output_path} (mesh name: {mesh_name})")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) not in (3, 4):
|
|
||||||
print(f"Usage: {sys.argv[0]} <input.txt> <output.txt> [mesh_name]")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
mesh_name = sys.argv[3] if len(sys.argv) == 4 else "Body"
|
|
||||||
convert(sys.argv[1], sys.argv[2], mesh_name)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
.DS_STORE
|
|
||||||
node_modules
|
|
||||||
scripts/flow/*/.flowconfig
|
|
||||||
.flowconfig
|
|
||||||
*~
|
|
||||||
*.pyc
|
|
||||||
.grunt
|
|
||||||
_SpecRunner.html
|
|
||||||
__benchmarks__
|
|
||||||
build/
|
|
||||||
remote-repo/
|
|
||||||
coverage/
|
|
||||||
.module-cache
|
|
||||||
fixtures/dom/public/react-dom.js
|
|
||||||
fixtures/dom/public/react.js
|
|
||||||
test/the-files-to-test.generated.js
|
|
||||||
*.log*
|
|
||||||
chrome-user-data
|
|
||||||
*.sublime-project
|
|
||||||
*.sublime-workspace
|
|
||||||
.idea
|
|
||||||
*.iml
|
|
||||||
.vscode
|
|
||||||
.zed
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
/tmp
|
|
||||||
/.worktrees
|
|
||||||
.claude/*.local.*
|
|
||||||
|
|
||||||
packages/react-devtools-core/dist
|
|
||||||
packages/react-devtools-extensions/chrome/build
|
|
||||||
packages/react-devtools-extensions/chrome/*.crx
|
|
||||||
packages/react-devtools-extensions/chrome/*.pem
|
|
||||||
packages/react-devtools-extensions/firefox/build
|
|
||||||
packages/react-devtools-extensions/firefox/*.xpi
|
|
||||||
packages/react-devtools-extensions/firefox/*.pem
|
|
||||||
packages/react-devtools-extensions/shared/build
|
|
||||||
packages/react-devtools-extensions/.tempUserDataDir
|
|
||||||
packages/react-devtools-fusebox/dist
|
|
||||||
packages/react-devtools-inline/dist
|
|
||||||
packages/react-devtools-shell/dist
|
|
||||||
packages/react-devtools-timeline/dist
|
|
||||||
|
|
||||||
resources
|
|
||||||
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
# Cutscene System
|
|
||||||
|
|
||||||
Cutscenes are defined in JSON and loaded by `CutsceneDatabase`. Each cutscene is a self-contained object with an array of animated image layers and optional subtitle lines.
|
|
||||||
|
|
||||||
The file can contain multiple cutscenes:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"cutscenes": [
|
|
||||||
{ "id": "intro", ... },
|
|
||||||
{ "id": "ending", ... }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Cutscenes and dialogues are loaded from **separate files**:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
dialogueSystem.loadDatabase("resources/dialogue/uni_interior.json"); // dialogues
|
|
||||||
dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes.json"); // cutscenes
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cutscene object
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "intro_cutscene",
|
|
||||||
"skippable": true,
|
|
||||||
"durationMs": 8000,
|
|
||||||
"fadeOutMs": 500,
|
|
||||||
"fadeInMs": 500,
|
|
||||||
"endFadeOutMs": 500,
|
|
||||||
"endFadeInMs": 500,
|
|
||||||
"onFadeInCallback": "",
|
|
||||||
"imageSegments": [ ... ],
|
|
||||||
"lines": [ ... ]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `id` | string | — | Unique identifier used to start the cutscene from C++ or dialogue (**required**) |
|
|
||||||
| `skippable` | bool | `true` | Whether the player can skip by holding LMB / touch |
|
|
||||||
| `durationMs` | int | `0` | Minimum content duration in ms. The cutscene will not end before this time even if all subtitle lines have finished. `0` means duration is determined solely by subtitle lines or `imageSegments.endMs` |
|
|
||||||
| `fadeOutMs` | int | `0` | Duration of the **opening fade** — game world fades to black before the cutscene images appear |
|
|
||||||
| `fadeInMs` | int | `0` | Duration of the **opening reveal** — cutscene images fade in from black after `fadeOutMs` |
|
|
||||||
| `endFadeOutMs` | int | `0` | Duration of the **closing fade** — cutscene fades to black at the end of content |
|
|
||||||
| `endFadeInMs` | int | `0` | Duration of the **closing reveal** — game world fades back in from black |
|
|
||||||
| `onFadeInCallback` | string | `""` | Lua function name called once the opening fade-in completes (fired after `fadeOutMs + fadeInMs` ms) |
|
|
||||||
| `imageSegments` | array | `[]` | Image layers with motion — see [Image segments](#image-segments) |
|
|
||||||
| `lines` | array | `[]` | Subtitle lines shown sequentially — see [Subtitle lines](#subtitle-lines) |
|
|
||||||
|
|
||||||
### Timing model
|
|
||||||
|
|
||||||
The total cutscene duration is:
|
|
||||||
|
|
||||||
```
|
|
||||||
contentDuration = max(durationMs, max(segment.endMs for all segments))
|
|
||||||
totalDuration = contentDuration + endFadeOutMs + endFadeInMs
|
|
||||||
```
|
|
||||||
|
|
||||||
The full timeline looks like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
|-- fadeOutMs --|-- fadeInMs --|--- content plays (images + subtitles) ---|-- endFadeOutMs --|-- endFadeInMs --|
|
|
||||||
world→black black→images images→black black→world
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Image segments
|
|
||||||
|
|
||||||
Each entry in `imageSegments` describes one image layer: when it is visible, how it fades in/out, and how it animates from a start pose to an end pose.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/bg_layer.png",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 8000,
|
|
||||||
"fadeInMs": 300,
|
|
||||||
"fadeOutMs": 300,
|
|
||||||
"easing": "EaseInOutSine",
|
|
||||||
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
|
|
||||||
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `path` | string | — | Path to the PNG image (**required**) |
|
|
||||||
| `width` | int | `0` | Logical width used for all UV and aspect-ratio math. `0` uses the actual texture pixel width |
|
|
||||||
| `height` | int | `0` | Logical height. `0` uses the actual texture pixel height |
|
|
||||||
| `startMs` | int | `0` | Time (ms from cutscene start) when this layer becomes active |
|
|
||||||
| `endMs` | int | `0` | Time (ms) when this layer stops being active. Must be > `startMs` |
|
|
||||||
| `fadeInMs` | int | `0` | Alpha fades from 0 → 1 over this many ms after `startMs`. `0` = instant |
|
|
||||||
| `fadeOutMs` | int | `0` | Alpha fades from 1 → 0 over this many ms before `endMs`. `0` = instant |
|
|
||||||
| `easing` | string | `"Linear"` | Easing applied to the pose interpolation — see [Easing types](#easing-types) |
|
|
||||||
| `from` | pose object | center/1.0 | Pose at `startMs` — see [Image pose](#image-pose) |
|
|
||||||
| `to` | pose object | same as `from` | Pose at `endMs`. If omitted, the layer stays at `from` the whole time |
|
|
||||||
|
|
||||||
Multiple segments can be active at the same time. They are rendered **in declaration order** (first = bottom layer, last = top layer), which enables parallax layering.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Image pose
|
|
||||||
|
|
||||||
A pose defines how an image is framed on screen at a given moment.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `centerX` | float | `0.5` | Normalized X position (0 = left edge of image, 1 = right edge) of the point that is placed at the horizontal center of the screen |
|
|
||||||
| `centerY` | float | `0.5` | Normalized Y position (0 = top edge, 1 = bottom edge) placed at the screen center |
|
|
||||||
| `scale` | float | `1.0` | Zoom level. `1.0` = the image fills the screen exactly (aspect-ratio corrected). `2.0` = zoomed in 2×, showing half the image area |
|
|
||||||
|
|
||||||
The runtime interpolates all three values independently from `from` to `to` using the chosen easing.
|
|
||||||
|
|
||||||
**Coordinate clamping:** `centerX`/`centerY` are automatically clamped so the viewport never shows area outside the image. For a zoomed-in segment (`scale > 1`) you therefore have more freedom to pan; for `scale = 1.0` the center is locked to `0.5/0.5`.
|
|
||||||
|
|
||||||
### Pose intuition
|
|
||||||
|
|
||||||
| Goal | Config |
|
|
||||||
|---|---|
|
|
||||||
| Centered, no zoom | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }` |
|
|
||||||
| Slightly zoomed in on center | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }` |
|
|
||||||
| Pan left to right | `from: { "centerX": 0.3, "scale": 1.2 }` → `to: { "centerX": 0.7, "scale": 1.2 }` |
|
|
||||||
| Zoom out from close-up | `from: { "scale": 1.8 }` → `to: { "scale": 1.0 }` |
|
|
||||||
| Look at top portion | `{ "centerY": 0.2, "scale": 1.3 }` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Easing types
|
|
||||||
|
|
||||||
Controls the interpolation curve applied to pose animation between `from` and `to`.
|
|
||||||
|
|
||||||
| Value | Description |
|
|
||||||
|---|---|
|
|
||||||
| `"Linear"` | Constant speed (default) |
|
|
||||||
| `"EaseInSine"` | Slow start, fast end |
|
|
||||||
| `"EaseOutSine"` | Fast start, slow end |
|
|
||||||
| `"EaseInOutSine"` | Slow start and end, fast middle |
|
|
||||||
| `"EaseInQuad"` | Quadratic slow start |
|
|
||||||
| `"EaseOutQuad"` | Quadratic slow end |
|
|
||||||
| `"EaseInOutQuad"` | Quadratic slow start and end |
|
|
||||||
| `"EaseInCubic"` | Cubic slow start |
|
|
||||||
| `"EaseOutCubic"` | Cubic slow end |
|
|
||||||
| `"EaseInOutCubic"` | Cubic slow start and end |
|
|
||||||
|
|
||||||
For cinematic camera motion `"EaseInOutSine"` or `"EaseInOutCubic"` give the most natural feel.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Subtitle lines
|
|
||||||
|
|
||||||
Lines are displayed sequentially on top of the cutscene images. Each line shows until its duration expires (or until the player advances, if `waitForConfirm` is set).
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"speaker": "Аида Дженибековна",
|
|
||||||
"text": "Здравствуйте, студенты.",
|
|
||||||
"durationMs": 3000,
|
|
||||||
"waitForConfirm": false,
|
|
||||||
"luaCallback": ""
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `speaker` | string | `""` | Speaker name shown above the subtitle text. Empty = no name bar |
|
|
||||||
| `text` | string | `""` | Subtitle text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
|
|
||||||
| `durationMs` | int | `0` | How long this line is displayed in ms. `0` = auto-computed from text length (~17 chars/sec, minimum 1500 ms) |
|
|
||||||
| `waitForConfirm` | bool | `false` | When `true`, the line waits for player input (tap/click/Enter) before advancing. No timer runs |
|
|
||||||
| `luaCallback` | string | `""` | Lua function name called when this line begins. Useful for triggering SFX, spawning effects, etc. |
|
|
||||||
|
|
||||||
Subtitle lines run on their own timer that is **independent** of the image segments. The cutscene ends when **both** subtitle lines are exhausted **and** `contentDuration` has elapsed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## C++ API
|
|
||||||
|
|
||||||
### Starting a cutscene
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Standalone cutscene (not part of a dialogue):
|
|
||||||
dialogueSystem.startCutscene("intro_cutscene");
|
|
||||||
|
|
||||||
// Skip the currently playing cutscene:
|
|
||||||
dialogueSystem.skipCutscene();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Callbacks
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Called when a cutscene begins:
|
|
||||||
dialogueSystem.setOnCutsceneStarted([]() { /* hide HUD, etc. */ });
|
|
||||||
|
|
||||||
// Called when a cutscene ends (receives the cutscene id):
|
|
||||||
dialogueSystem.setOnCutsceneFinished([](const std::string& id) {
|
|
||||||
// id == "intro_cutscene"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Called when a subtitle line begins (receives luaCallback value):
|
|
||||||
dialogueSystem.setOnCutsceneLineStarted([](const std::string& fn) {
|
|
||||||
scriptEngine.callActivateFunction(fn);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Called when the opening fade-in completes (receives onFadeInCallback value):
|
|
||||||
dialogueSystem.setOnCutsceneFadeInComplete([](const std::string& fn) {
|
|
||||||
scriptEngine.callActivateFunction(fn);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Triggering from dialogue
|
|
||||||
|
|
||||||
A dialogue node of type `CutsceneStart` embeds a cutscene mid-conversation. Dialogue resumes at `next` when the cutscene ends.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "node_cutscene",
|
|
||||||
"type": "CutsceneStart",
|
|
||||||
"cutsceneId": "intro_cutscene",
|
|
||||||
"next": "node_after_cutscene"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Full examples
|
|
||||||
|
|
||||||
### Minimal — static image, timed
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "simple",
|
|
||||||
"durationMs": 4000,
|
|
||||||
"fadeOutMs": 300,
|
|
||||||
"fadeInMs": 300,
|
|
||||||
"endFadeOutMs": 300,
|
|
||||||
"endFadeInMs": 300,
|
|
||||||
"imageSegments": [
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/city.png",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 4000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Two-layer parallax pan
|
|
||||||
|
|
||||||
Background moves slowly left-to-right; foreground character moves faster, creating depth.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "classroom_intro",
|
|
||||||
"durationMs": 8000,
|
|
||||||
"fadeOutMs": 500,
|
|
||||||
"fadeInMs": 500,
|
|
||||||
"endFadeOutMs": 500,
|
|
||||||
"endFadeInMs": 500,
|
|
||||||
"imageSegments": [
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/classroom_bg.png",
|
|
||||||
"width": 1920,
|
|
||||||
"height": 1080,
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 8000,
|
|
||||||
"fadeInMs": 400,
|
|
||||||
"easing": "EaseInOutSine",
|
|
||||||
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
|
|
||||||
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/classroom_teacher.png",
|
|
||||||
"width": 1920,
|
|
||||||
"height": 1080,
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 8000,
|
|
||||||
"easing": "EaseInOutSine",
|
|
||||||
"from": { "centerX": 0.35, "centerY": 0.5, "scale": 1.0 },
|
|
||||||
"to": { "centerX": 0.65, "centerY": 0.5, "scale": 1.0 }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lines": [
|
|
||||||
{
|
|
||||||
"speaker": "Аида Дженибековна",
|
|
||||||
"text": "Здравствуйте, студенты.",
|
|
||||||
"durationMs": 3000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker": "Аида Дженибековна",
|
|
||||||
"text": "Рассаживайтесь.",
|
|
||||||
"durationMs": 2500
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Zoom-in reveal with a second image appearing mid-way
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "letter_reveal",
|
|
||||||
"durationMs": 7000,
|
|
||||||
"fadeOutMs": 400,
|
|
||||||
"fadeInMs": 600,
|
|
||||||
"endFadeOutMs": 600,
|
|
||||||
"endFadeInMs": 400,
|
|
||||||
"imageSegments": [
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/desk_bg.png",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 7000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "resources/cutscenes/letter_closeup.png",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"startMs": 2000,
|
|
||||||
"endMs": 7000,
|
|
||||||
"fadeInMs": 800,
|
|
||||||
"easing": "EaseOutCubic",
|
|
||||||
"from": { "centerX": 0.5, "centerY": 0.5, "scale": 2.5 },
|
|
||||||
"to": { "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lines": [
|
|
||||||
{
|
|
||||||
"text": "Среди бумаг на столе лежит конверт.",
|
|
||||||
"durationMs": 2500
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker": "Главный герой",
|
|
||||||
"text": "«Явитесь в деканат немедленно».",
|
|
||||||
"durationMs": 3000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
{
|
|
||||||
"cutscenes": [
|
|
||||||
{
|
|
||||||
"id": "test_cutscene_01",
|
|
||||||
"background": "resources/black.png",
|
|
||||||
"durationMs": 5000,
|
|
||||||
"fadeOutMs": 500,
|
|
||||||
"fadeInMs": 500,
|
|
||||||
"endFadeOutMs": 500,
|
|
||||||
"endFadeInMs": 500,
|
|
||||||
"imageSegments": [
|
|
||||||
{
|
|
||||||
"path": "resources/w/cutscenes/cutscene1/cutscene1_wall_x.png",
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 8000,
|
|
||||||
"fadeInMs": 0,
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"from": {
|
|
||||||
"centerX": 0.3, "scale": 1.2
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"centerX": 0.7, "scale": 1.2
|
|
||||||
},
|
|
||||||
"easing": "Linear"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "resources/w/cutscenes/cutscene1/cutscene1_aida1_x.png",
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 8000,
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"from": {
|
|
||||||
|
|
||||||
"centerX": 0.3,
|
|
||||||
"centerY": 0.5,
|
|
||||||
"scale": 1.0
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"centerX": 0.7,
|
|
||||||
"centerY": 0.5,
|
|
||||||
"scale": 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lines": [
|
|
||||||
{
|
|
||||||
"speaker": "Аида Дженибековна",
|
|
||||||
"text": "Здравствуйте, студенты. Кого я вижу, где вы были весь семестр?",
|
|
||||||
"durationMs": 3000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker": "Аида Дженибековна",
|
|
||||||
"text": "В эпизоде \"Семетей\" трилогии \"Манас\", изменники Канчоро и Кыяз захватывают власть над кыргызами.",
|
|
||||||
"durationMs": 3000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker": "Аида Дженибековна",
|
|
||||||
"text": "На сегодня лекция завершена. Домашнее задание - к практическому занятию вы должны подготовить презентации, каждый по своей теме.",
|
|
||||||
"durationMs": 2000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;width:100%;overflow:hidden}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;background:#1a1a1a;color:#e0e0e0}button{cursor:pointer;font-family:inherit;font-size:12px}input,select,textarea{font-family:inherit;font-size:12px;background:#2a2a2a;color:#e0e0e0;border:1px solid #444;border-radius:3px;padding:3px 6px}input:focus,select:focus,textarea:focus{outline:none;border-color:#5b9bd5}label{color:#aaa;font-size:11px}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:#1a1a1a}::-webkit-scrollbar-thumb{background:#444;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#666}._app_1ibpl_1{display:flex;width:100%;height:100%;overflow:hidden}._panel_e520h_1{width:200px;min-width:180px;background:#252525;border-right:1px solid #333;display:flex;flex-direction:column;overflow:hidden}._header_e520h_11{padding:10px 12px;font-size:12px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:#888;border-bottom:1px solid #333}._actions_e520h_21{display:flex;flex-direction:column;gap:6px;padding:10px;border-bottom:1px solid #333}._btn_e520h_29{background:#333;color:#ddd;border:1px solid #444;border-radius:4px;padding:5px 8px;text-align:center;transition:background .15s}._btn_e520h_29:hover:not(:disabled){background:#3d3d3d}._btn_e520h_29:disabled{opacity:.4;cursor:default}._btnPrimary_e520h_42{background:#2a4a6e;border-color:#3a6090;color:#a8d0f0}._btnPrimary_e520h_42:hover{background:#2e5480}._list_e520h_50{flex:1;overflow-y:auto;padding:4px 0}._empty_e520h_56{padding:16px 12px;color:#555;font-size:11px;line-height:1.5}._item_e520h_63{display:flex;align-items:center;justify-content:space-between;padding:7px 12px;cursor:pointer;-webkit-user-select:none;user-select:none;border-left:3px solid transparent;transition:background .1s}._item_e520h_63:hover{background:#2e2e2e}._selected_e520h_76{background:#1e3a55;border-left-color:#5b9bd5}._itemId_e520h_81{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}._deleteBtn_e520h_89{background:none;border:none;color:#666;padding:2px 4px;border-radius:3px;font-size:10px;opacity:0;transition:opacity .1s,color .1s}._item_e520h_63:hover ._deleteBtn_e520h_89{opacity:1}._deleteBtn_e520h_89:hover{color:#e06c6c}._panel_1kkcd_1{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}._previewArea_1kkcd_9{flex:0 0 auto;display:flex;justify-content:center;align-items:center;background:#111;padding:8px;max-height:60%}._wrapper_1nucj_1{width:100%;display:flex;justify-content:center;align-items:center;background:#111;padding:8px;flex-shrink:0}._viewport_1nucj_11{position:relative;aspect-ratio:16 / 9;width:100%;max-height:100%;background:#000;overflow:hidden}._empty_1nucj_20{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;color:#444;font-size:13px}._subtitleBar_1nucj_30{position:absolute;bottom:0;left:0;right:0;padding:10px 20px 14px;background:linear-gradient(transparent,#000000bf);text-align:center}._speaker_1nucj_40{font-size:11px;font-weight:600;color:#f0c060;margin-bottom:4px;text-shadow:0 1px 3px rgba(0,0,0,.8)}._text_1nucj_48{font-size:14px;color:#fff;line-height:1.5;text-shadow:0 1px 4px rgba(0,0,0,.9)}._controls_1qoyb_1{display:flex;align-items:center;gap:4px;padding:6px 10px;background:#1e1e1e;border-top:1px solid #333;border-bottom:1px solid #333;flex-shrink:0}._btn_1qoyb_12{background:#2d2d2d;border:1px solid #404040;color:#ccc;border-radius:4px;padding:4px 10px;font-size:13px;transition:background .1s;min-width:32px}._btn_1qoyb_12:hover:not(:disabled){background:#3a3a3a;color:#fff}._btn_1qoyb_12:disabled{opacity:.35;cursor:default}._active_1qoyb_26{color:#5ba3e0;border-color:#4a7aaa}._time_1qoyb_31{margin-left:10px;font-size:11px;color:#888;font-variant-numeric:tabular-nums}._container_uetgi_1{display:flex;flex-direction:column;flex:1;min-height:0;background:#1a1a1a;border-top:1px solid #333;overflow:hidden}._toolbar_uetgi_11{display:flex;align-items:center;gap:4px;padding:4px 8px;background:#222;border-bottom:1px solid #333;flex-shrink:0}._tbBtn_uetgi_21{background:#2d2d2d;border:1px solid #404040;color:#bbb;border-radius:3px;padding:3px 8px;font-size:11px;transition:background .1s}._tbBtn_uetgi_21:hover:not(:disabled){background:#3a3a3a;color:#fff}._tbBtn_uetgi_21:disabled{opacity:.3;cursor:default}._spacer_uetgi_33{flex:1}._zoomLabel_uetgi_35{font-size:11px;color:#666;margin-right:2px}._scroll_uetgi_37{flex:1;overflow:auto;position:relative;cursor:default}._ruler_uetgi_45{display:flex;height:22px;background:#212121;border-bottom:1px solid #333;position:sticky;top:0;z-index:10;-webkit-user-select:none;user-select:none;cursor:crosshair}._tick_uetgi_57{position:absolute;top:0;bottom:0;width:1px;background:#444}._tickLabel_uetgi_65{position:absolute;top:3px;left:3px;font-size:9px;color:#666;white-space:nowrap}._layersArea_Line truncated
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const p of document.querySelectorAll('link[rel="modulepreload"]'))f(p);new MutationObserver(p=>{for(const g of p)if(g.type==="childList")for(const S of g.addedNodes)S.tagName==="LINK"&&S.rel==="modulepreload"&&f(S)}).observe(document,{childList:!0,subtree:!0});function s(p){const g={};return p.integrity&&(g.integrity=p.integrity),p.referrerPolicy&&(g.referrerPolicy=p.referrerPolicy),p.crossOrigin==="use-credentials"?g.credentials="include":p.crossOrigin==="anonymous"?g.credentials="omit":g.credentials="same-origin",g}function f(p){if(p.ep)return;p.ep=!0;const g=s(p);fetch(p.href,g)}})();function Bd(u){return u&&u.__esModule&&Object.prototype.hasOwnProperty.call(u,"default")?u.default:u}var bu={exports:{}},Or={},eo={exports:{}},G={};/**
|
|
||||||
* @license React
|
|
||||||
* react.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/var pc;function $d(){if(pc)return G;pc=1;var u=Symbol.for("react.element"),a=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),p=Symbol.for("react.profiler"),g=Symbol.for("react.provider"),S=Symbol.for("react.context"),L=Symbol.for("react.forward_ref"),z=Symbol.for("react.suspense"),U=Symbol.for("react.memo"),ee=Symbol.for("react.lazy"),D=Symbol.iterator;function H(h){return h===null||typeof h!="object"?null:(h=D&&h[D]||h["@@iterator"],typeof h=="function"?h:null)}var fe={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},me=Object.assign,re={};function q(h,k,X){this.props=h,this.context=k,this.refs=re,this.updater=X||fe}q.prototype.isReactComponent={},q.prototype.setState=function(h,k){if(typeof h!="object"&&typeof h!="function"&&h!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,h,k,"setState")},q.prototype.forceUpdate=function(h){this.updater.enqueueForceUpdate(this,h,"forceUpdate")};function ge(){}ge.prototype=q.prototype;function Ge(h,k,X){this.props=h,this.context=k,this.refs=re,this.updater=X||fe}var Be=Ge.prototype=new ge;Be.constructor=Ge,me(Be,q.prototype),Be.isPureReactComponent=!0;var ke=Array.isArray,Ze=Object.prototype.hasOwnProperty,Ee={current:null},Ne={key:!0,ref:!0,__self:!0,__source:!0};function $e(h,k,X){var Y,Z={},b=null,oe=null;if(k!=null)for(Y in k.ref!==void 0&&(oe=k.ref),k.key!==void 0&&(b=""+k.key),k)Ze.call(k,Y)&&!Ne.hasOwnProperty(Y)&&(Z[Y]=k[Y]);var J=arguments.length-2;if(J===1)Z.children=X;else if(1<J){for(var de=Array(J),Je=0;Je<J;Je++)de[Je]=arguments[Je+2];Z.children=de}if(h&&h.defaultProps)for(Y in J=h.defaultProps,J)Z[Y]===void 0&&(Z[Y]=J[Y]);return{$$typeof:u,type:h,key:b,ref:oe,props:Z,_owner:Ee.current}}function rt(h,k){return{$$typeof:u,type:h.type,key:k,ref:h.ref,props:h.props,_owner:h._owner}}function pt(h){return typeof h=="object"&&h!==null&&h.$$typeof===u}function kt(h){var k={"=":"=0",":":"=2"};return"$"+h.replace(/[=:]/g,function(X){return k[X]})}var He=/\/+/g;function De(h,k){return typeof h=="object"&&h!==null&&h.key!=null?kt(""+h.key):k.toString(36)}function I(h,k,X,Y,Z){var b=typeof h;(b==="undefined"||b==="boolean")&&(h=null);var oe=!1;if(h===null)oe=!0;else switch(b){case"string":case"number":oe=!0;break;case"object":switch(h.$$typeof){case u:case a:oe=!0}}if(oe)return oe=h,Z=Z(oe),h=Y===""?"."+De(oe,0):Y,ke(Z)?(X="",h!=null&&(X=h.replace(He,"$&/")+"/"),I(Z,k,X,"",function(Je){return Je})):Z!=null&&(pt(Z)&&(Z=rt(Z,X+(!Z.key||oe&&oe.key===Z.key?"":(""+Z.key).replace(He,"$&/")+"/")+h)),k.push(Z)),1;if(oe=0,Y=Y===""?".":Y+":",ke(h))for(var J=0;J<h.length;J++){b=h[J];var de=Y+De(b,J);oe+=I(b,k,X,de,Z)}else if(de=H(h),typeof de=="function")for(h=de.call(h),J=0;!(b=h.next()).done;)b=b.value,de=Y+De(b,J++),oe+=I(b,k,X,de,Z);else if(b==="object")throw k=String(h),Error("Objects are not valid as a React child (found: "+(k==="[object Object]"?"object with keys {"+Object.keys(h).join(", ")+"}":k)+"). If you meant to render a collection of children, use an array instead.");return oe}function Q(h,k,X){if(h==null)return h;var Y=[],Z=0;return I(h,Y,"","",function(b){return k.call(X,b,Z++)}),Y}function le(h){if(h._status===-1){var k=h._result;k=k(),k.then(function(X){(h._status===0||h._status===-1)&&(h._status=1,h._result=X)},function(X){(h._status===0||h._status===-1)&&(h._status=2,h._result=X)}),h._status===-1&&(h._status=0,h._result=k)}if(h._status===1)return h._result.default;throw h._result}var K={current:null},M={transition:null},A={ReactCurrentDispatcher:K,ReactCurrentBatchConfig:M,ReactCurrentOwner:Ee};function P(){throw Error("act(...) is not supported in production builds of React.")}return G.Children={map:Q,forEach:function(h,k,X){Q(h,function(){k.apply(this,arguments)},X)},count:function(h){var k=0;return Q(h,function(){k++}),k},toArray:function(h){return Q(h,function(k){return k})||[]},only:function(h){if(!pt(h))throw Error("React.Children.only expected to receive a single React element child.");return h}},G.Component=q,G.Fragment=s,G.Profiler=p,G.PureComponent=Ge,G.StrictMode=f,G.Suspense=z,G.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=A,G.act=P,G.cloneElement=function(h,k,X){if(h==null)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+h+".");var Y=me({},h.props),Z=h.key,b=h.ref,oe=h._owner;if(k!=null){if(k.ref!==void 0&&(b=k.ref,oe=Ee.current),k.key!==void 0&&(Z=""+k.key),h.type&&h.type.defaultProps)var J=h.type.defaultProps;for(de in k)Ze.call(k,de)&&!Ne.hasOwnProperty(de)&&(Y[de]=k[de]===void 0&&J!==void 0?J[de]:k[de])}var de=arguments.length-2;if(de===1)Y.children=X;else if(1<de){J=Array(de);for(var Je=0;Je<de;Je++)J[Je]=arguments[Je+2];Y.children=J}return{$$typeof:u,type:h.type,key:Z,ref:b,props:Y,_owner:oe}},G.createContext=function(h){return h={$$typeof:S,_currLine truncated
|
|
||||||
* @license React
|
|
||||||
* react-jsx-runtime.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/var hc;function Hd(){if(hc)return Or;hc=1;var u=vo(),a=Symbol.for("react.element"),s=Symbol.for("react.fragment"),f=Object.prototype.hasOwnProperty,p=u.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,g={key:!0,ref:!0,__self:!0,__source:!0};function S(L,z,U){var ee,D={},H=null,fe=null;U!==void 0&&(H=""+U),z.key!==void 0&&(H=""+z.key),z.ref!==void 0&&(fe=z.ref);for(ee in z)f.call(z,ee)&&!g.hasOwnProperty(ee)&&(D[ee]=z[ee]);if(L&&L.defaultProps)for(ee in z=L.defaultProps,z)D[ee]===void 0&&(D[ee]=z[ee]);return{$$typeof:a,type:L,key:H,ref:fe,props:D,_owner:p.current}}return Or.Fragment=s,Or.jsx=S,Or.jsxs=S,Or}var vc;function Wd(){return vc||(vc=1,bu.exports=Hd()),bu.exports}var y=Wd(),ze=vo();const Zl=Bd(ze);var Jl={},to={exports:{}},Ke={},no={exports:{}},ro={};/**
|
|
||||||
* @license React
|
|
||||||
* scheduler.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/var yc;function Vd(){return yc||(yc=1,(function(u){function a(M,A){var P=M.length;M.push(A);e:for(;0<P;){var h=P-1>>>1,k=M[h];if(0<p(k,A))M[h]=A,M[P]=k,P=h;else break e}}function s(M){return M.length===0?null:M[0]}function f(M){if(M.length===0)return null;var A=M[0],P=M.pop();if(P!==A){M[0]=P;e:for(var h=0,k=M.length,X=k>>>1;h<X;){var Y=2*(h+1)-1,Z=M[Y],b=Y+1,oe=M[b];if(0>p(Z,P))b<k&&0>p(oe,Z)?(M[h]=oe,M[b]=P,h=b):(M[h]=Z,M[Y]=P,h=Y);else if(b<k&&0>p(oe,P))M[h]=oe,M[b]=P,h=b;else break e}}return A}function p(M,A){var P=M.sortIndex-A.sortIndex;return P!==0?P:M.id-A.id}if(typeof performance=="object"&&typeof performance.now=="function"){var g=performance;u.unstable_now=function(){return g.now()}}else{var S=Date,L=S.now();u.unstable_now=function(){return S.now()-L}}var z=[],U=[],ee=1,D=null,H=3,fe=!1,me=!1,re=!1,q=typeof setTimeout=="function"?setTimeout:null,ge=typeof clearTimeout=="function"?clearTimeout:null,Ge=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function Be(M){for(var A=s(U);A!==null;){if(A.callback===null)f(U);else if(A.startTime<=M)f(U),A.sortIndex=A.expirationTime,a(z,A);else break;A=s(U)}}function ke(M){if(re=!1,Be(M),!me)if(s(z)!==null)me=!0,le(Ze);else{var A=s(U);A!==null&&K(ke,A.startTime-M)}}function Ze(M,A){me=!1,re&&(re=!1,ge($e),$e=-1),fe=!0;var P=H;try{for(Be(A),D=s(z);D!==null&&(!(D.expirationTime>A)||M&&!kt());){var h=D.callback;if(typeof h=="function"){D.callback=null,H=D.priorityLevel;var k=h(D.expirationTime<=A);A=u.unstable_now(),typeof k=="function"?D.callback=k:D===s(z)&&f(z),Be(A)}else f(z);D=s(z)}if(D!==null)var X=!0;else{var Y=s(U);Y!==null&&K(ke,Y.startTime-A),X=!1}return X}finally{D=null,H=P,fe=!1}}var Ee=!1,Ne=null,$e=-1,rt=5,pt=-1;function kt(){return!(u.unstable_now()-pt<rt)}function He(){if(Ne!==null){var M=u.unstable_now();pt=M;var A=!0;try{A=Ne(!0,M)}finally{A?De():(Ee=!1,Ne=null)}}else Ee=!1}var De;if(typeof Ge=="function")De=function(){Ge(He)};else if(typeof MessageChannel<"u"){var I=new MessageChannel,Q=I.port2;I.port1.onmessage=He,De=function(){Q.postMessage(null)}}else De=function(){q(He,0)};function le(M){Ne=M,Ee||(Ee=!0,De())}function K(M,A){$e=q(function(){M(u.unstable_now())},A)}u.unstable_IdlePriority=5,u.unstable_ImmediatePriority=1,u.unstable_LowPriority=4,u.unstable_NormalPriority=3,u.unstable_Profiling=null,u.unstable_UserBlockingPriority=2,u.unstable_cancelCallback=function(M){M.callback=null},u.unstable_continueExecution=function(){me||fe||(me=!0,le(Ze))},u.unstable_forceFrameRate=function(M){0>M||125<M?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):rt=0<M?Math.floor(1e3/M):5},u.unstable_getCurrentPriorityLevel=function(){return H},u.unstable_getFirstCallbackNode=function(){return s(z)},u.unstable_next=function(M){switch(H){case 1:case 2:case 3:var A=3;break;default:A=H}var P=H;H=A;try{return M()}finally{H=P}},u.unstable_pauseExecution=function(){},u.unstable_requestPaint=function(){},u.unstable_runWithPriority=function(M,A){switch(M){case 1:case 2:case 3:case 4:case 5:break;default:M=3}var P=H;H=M;try{return A()}finally{H=P}},u.unstable_scheduleCallback=function(M,A,P){var h=u.unstable_now();switch(typeof P=="object"&&P!==null?(P=P.delay,P=typeof P=="number"&&0<P?h+P:h):P=h,M){case 1:var k=-1;break;case 2:k=250;break;case 5:k=1073741823;break;case 4:k=1e4;break;default:k=5e3}return k=P+k,M={id:ee++,callback:A,priorityLevel:M,startTime:P,expirationTime:k,sortIndex:-1},P>h?(M.sortIndex=P,a(U,M),s(z)===null&&M===s(U)&&(re?(ge($e),$e=-1):re=!0,K(ke,P-h))):(M.sortIndex=k,a(z,M),me||fe||(me=!0,le(Ze))),M},u.unstable_shouldYield=kt,u.unstable_wrapCallback=function(M){var A=H;return function(){var P=H;H=A;try{return M.apply(this,arguments)}finally{H=P}}}})(ro)),ro}var gc;function Qd(){return gc||(gc=1,no.exports=Vd()),no.exports}/**
|
|
||||||
* @license React
|
|
||||||
* react-dom.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/var _c;function Xd(){if(_c)return Ke;_c=1;var u=vo(),a=Qd();function s(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n<arguments.length;n++)t+="&args[]="+encodeURIComponent(arguments[n]);return"Minified React error #"+e+"; visit "+t+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}var f=new Set,p={};function g(e,t){S(e,t),S(e+"Capture",t)}function S(e,t){for(p[e]=t,e=0;e<t.length;e++)f.add(t[e])}var L=!(typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),z=Object.prototype.hasOwnProperty,U=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,ee={},D={};function H(e){return z.call(D,e)?!0:z.call(ee,e)?!1:U.test(e)?D[e]=!0:(ee[e]=!0,!1)}function fe(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function me(e,t,n,r){if(t===null||typeof t>"u"||fe(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function re(e,t,n,r,l,i,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=o}var q={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){q[e]=new re(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];q[t]=new re(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){q[e]=new re(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){q[e]=new re(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){q[e]=new re(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){q[e]=new re(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){q[e]=new re(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){q[e]=new re(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){q[e]=new re(e,5,!1,e.toLowerCase(),null,!1,!1)});var ge=/[\-:]([a-z])/g;function Ge(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ge,Ge);q[t]=new re(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ge,Ge);q[t]=new re(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ge,Ge);q[t]=new re(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){q[e]=new re(e,1,!1,e.toLowerCase(),null,!1,!1)}),q.xlinkHref=new re("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){q[e]=new re(e,1,!1,e.toLowerCase(),null,!0,!0)});function Be(e,t,n,r){var l=q.hasOwnProperty(t)?q[t]:null;(l!==null?l.type!==0:r||!(2<t.length)||t[0]!=="o"&
Line truncated
|
|
||||||
`+h+e}var X=!1;function Y(e,t){if(!e||X)return"";X=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(t)if(t=function(){throw Error()},Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(t,[])}catch(w){var r=w}Reflect.construct(e,[],t)}else{try{t.call()}catch(w){r=w}e.call(t.prototype)}else{try{throw Error()}catch(w){r=w}e()}}catch(w){if(w&&r&&typeof w.stack=="string"){for(var l=w.stack.split(`
|
|
||||||
`),i=r.stack.split(`
|
|
||||||
`),o=l.length-1,c=i.length-1;1<=o&&0<=c&&l[o]!==i[c];)c--;for(;1<=o&&0<=c;o--,c--)if(l[o]!==i[c]){if(o!==1||c!==1)do if(o--,c--,0>c||l[o]!==i[c]){var d=`
|
|
||||||
`+l[o].replace(" at new "," at ");return e.displayName&&d.includes("<anonymous>")&&(d=d.replace("<anonymous>",e.displayName)),d}while(1<=o&&0<=c);break}}}finally{X=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?k(e):""}function Z(e){switch(e.tag){case 5:return k(e.type);case 16:return k("Lazy");case 13:return k("Suspense");case 19:return k("SuspenseList");case 0:case 2:case 15:return e=Y(e.type,!1),e;case 11:return e=Y(e.type.render,!1),e;case 1:return e=Y(e.type,!0),e;default:return""}}function b(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ne:return"Fragment";case Ee:return"Portal";case rt:return"Profiler";case $e:return"StrictMode";case De:return"Suspense";case I:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case kt:return(e.displayName||"Context")+".Consumer";case pt:return(e._context.displayName||"Context")+".Provider";case He:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Q:return t=e.displayName||null,t!==null?t:b(e.type)||"Memo";case le:t=e._payload,e=e._init;try{return b(e(t))}catch{}}return null}function oe(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return b(t);case 8:return t===$e?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function J(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function de(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Je(e){var t=de(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(o){r=""+o,i.call(this,o)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(o){r=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Br(e){e._valueTracker||(e._valueTracker=Je(e))}function _o(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=de(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function $r(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ui(e,t){var n=t.checked;return P({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function wo(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=J(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function So(e,t){t=t.checked,t!=null&&Be(e,"checked",t,!1)}function oi(e,t){So(e,t);var n=J(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?si(e,t.type,n):t.hasOwnProperty("defaultValue")&&si(e,t.type,J(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ko(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function si(e,t,n){(t!=="number"||$r(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Gn=Array.isArray;function Sn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l<n.length;l++)t["$"+n[l]]=!0;for(n=0;n<e.length;n++)l=t.hasOwnProperty("$"+e[n].value),e[n].selected!==l&&(e[n].selected=l),l&&r&&(e[n].defaultSelected=!0)}else{for(n=""+J(n),t=null,l=0;l<e.length;l++){if(e[l].value===n){e[l].selected=!0,r&&(e[l].defaultSelected=!0);return}t!==null||e[l].disabled||(t=e[l])}t!==null&&(t.selected=!0)}}function ai(e,t){if(t.dangerouslySetInnerHTML!=
Line truncated
|
|
||||||
`).replace(bf,"")}function al(e,t,n){if(t=zs(t),zs(e)!==t&&n)throw Error(s(425))}function cl(){}var Vi=null,Qi=null;function Xi(e,t){return e==="textarea"||e==="noscript"||typeof t.children=="string"||typeof t.children=="number"||typeof t.dangerouslySetInnerHTML=="object"&&t.dangerouslySetInnerHTML!==null&&t.dangerouslySetInnerHTML.__html!=null}var Yi=typeof setTimeout=="function"?setTimeout:void 0,ed=typeof clearTimeout=="function"?clearTimeout:void 0,Ts=typeof Promise=="function"?Promise:void 0,td=typeof queueMicrotask=="function"?queueMicrotask:typeof Ts<"u"?function(e){return Ts.resolve(null).then(e).catch(nd)}:Yi;function nd(e){setTimeout(function(){throw e})}function Ki(e,t){var n=t,r=0;do{var l=n.nextSibling;if(e.removeChild(n),l&&l.nodeType===8)if(n=l.data,n==="/$"){if(r===0){e.removeChild(l),or(t);return}r--}else n!=="$"&&n!=="$?"&&n!=="$!"||r++;n=l}while(n);or(t)}function Wt(e){for(;e!=null;e=e.nextSibling){var t=e.nodeType;if(t===1||t===3)break;if(t===8){if(t=e.data,t==="$"||t==="$!"||t==="$?")break;if(t==="/$")return null}}return e}function Os(e){e=e.previousSibling;for(var t=0;e;){if(e.nodeType===8){var n=e.data;if(n==="$"||n==="$!"||n==="$?"){if(t===0)return e;t--}else n==="/$"&&t++}e=e.previousSibling}return null}var jn=Math.random().toString(36).slice(2),Ct="__reactFiber$"+jn,gr="__reactProps$"+jn,Pt="__reactContainer$"+jn,Gi="__reactEvents$"+jn,rd="__reactListeners$"+jn,ld="__reactHandles$"+jn;function ln(e){var t=e[Ct];if(t)return t;for(var n=e.parentNode;n;){if(t=n[Pt]||n[Ct]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=Os(e);e!==null;){if(n=e[Ct])return n;e=Os(e)}return t}e=n,n=e.parentNode}return null}function _r(e){return e=e[Ct]||e[Pt],!e||e.tag!==5&&e.tag!==6&&e.tag!==13&&e.tag!==3?null:e}function In(e){if(e.tag===5||e.tag===6)return e.stateNode;throw Error(s(33))}function fl(e){return e[gr]||null}var Zi=[],zn=-1;function Vt(e){return{current:e}}function ce(e){0>zn||(e.current=Zi[zn],Zi[zn]=null,zn--)}function se(e,t){zn++,Zi[zn]=e.current,e.current=t}var Qt={},Te=Vt(Qt),We=Vt(!1),un=Qt;function Tn(e,t){var n=e.type.contextTypes;if(!n)return Qt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function Ve(e){return e=e.childContextTypes,e!=null}function dl(){ce(We),ce(Te)}function Rs(e,t,n){if(Te.current!==Qt)throw Error(s(168));se(Te,t),se(We,n)}function Fs(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(s(108,oe(e)||"Unknown",l));return P({},n,r)}function pl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Qt,un=Te.current,se(Te,e),se(We,We.current),!0}function Ds(e,t,n){var r=e.stateNode;if(!r)throw Error(s(169));n?(e=Fs(e,t,un),r.__reactInternalMemoizedMergedChildContext=e,ce(We),ce(Te),se(Te,e)):ce(We),se(We,n)}var jt=null,ml=!1,Ji=!1;function As(e){jt===null?jt=[e]:jt.push(e)}function id(e){ml=!0,As(e)}function Xt(){if(!Ji&&jt!==null){Ji=!0;var e=0,t=ue;try{var n=jt;for(ue=1;e<n.length;e++){var r=n[e];do r=r(!0);while(r!==null)}jt=null,ml=!1}catch(l){throw jt!==null&&(jt=jt.slice(e+1)),Bo(_i,Xt),l}finally{ue=t,Ji=!1}}return null}var On=[],Rn=0,hl=null,vl=0,lt=[],it=0,on=null,It=1,zt="";function sn(e,t){On[Rn++]=vl,On[Rn++]=hl,hl=e,vl=t}function Us(e,t,n){lt[it++]=It,lt[it++]=zt,lt[it++]=on,on=e;var r=It;e=zt;var l=32-mt(r)-1;r&=~(1<<l),n+=1;var i=32-mt(t)+l;if(30<i){var o=l-l%5;i=(r&(1<<o)-1).toString(32),r>>=o,l-=o,It=1<<32-mt(t)+l|n<<l|r,zt=i+e}else It=1<<i|n<<l|r,zt=e}function qi(e){e.return!==null&&(sn(e,1),Us(e,1,0))}function bi(e){for(;e===hl;)hl=On[--Rn],On[Rn]=null,vl=On[--Rn],On[Rn]=null;for(;e===on;)on=lt[--it],lt[it]=null,zt=lt[--it],lt[it]=null,It=lt[--it],lt[it]=null}var be=null,et=null,pe=!1,vt=null;function Bs(e,t){var n=at(5,null,null,0);n.elementType="DELETED",n.stateNode=t,n.return=e,t=e.deletions,t===null?(e.deletions=[n],e.flags|=16):t.push(n)}function $s(e,t){switch(e.tag){case 5:var n=e.type;return t=t.nodeType!==1||n.toLowerCase()!==t.nodeName.toLowerCase()?null:t,t!==null?(e.stateNode=t,be=e,et=Wt(t.firstChild),!0):!1;case 6:return t=e.pendingProps===""||t.nodeType!==3?null:t,t!==null?(e.stateNode=t,be=e,et=null,!0):!1;case 13:return t=t.nodeType!==8?null:t,t!==null?(n=on!==null?{id:It,overflow:zt}:null,e.memoizedState={dehydrated:t,treeContext:n,retryLane:1073741824},n=at(18,null,null,0),n.stateNode=t,n.return=e,e.child=n,be=e,et=null,!0):!1;default:return!1}}function eu(e){return(e.mode&1)!==0&&(e.flags&128)===0}function tu(e){if(pe){var t=et;if(t){var n=t;if(!$s(e,t)){if(eu(e))throw Error(s(418));t=Wt(n.nextSibling);var r=be;t&&$s(e,t)?Bs(r,n):(e.flags=e.flags&-4097|2,pe=!1,be=e)}}else{if(eu(e))throw Error(s(418));e.flags=e.flags&-4097|2,pe=!1,be=e}}}function Hs(e){for(e=e.
Line truncated
|
|
||||||
Error generating stack: `+i.message+`
|
|
||||||
`+i.stack}return{value:e,source:t,stack:l,digest:null}}function xu(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function Cu(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var md=typeof WeakMap=="function"?WeakMap:Map;function ka(e,t,n){n=Ot(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){Fl||(Fl=!0,Uu=r),Cu(e,t)},n}function xa(e,t,n){n=Ot(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){Cu(e,t)}}var i=e.stateNode;return i!==null&&typeof i.componentDidCatch=="function"&&(n.callback=function(){Cu(e,t),typeof r!="function"&&(Zt===null?Zt=new Set([this]):Zt.add(this));var o=t.stack;this.componentDidCatch(t.value,{componentStack:o!==null?o:""})}),n}function Ca(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new md;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=Md.bind(null,e,t,n),t.then(e,e))}function Ea(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function Na(e,t,n,r,l){return(e.mode&1)===0?(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=Ot(-1,1),t.tag=2,Kt(n,t,1))),n.lanes|=1),e):(e.flags|=65536,e.lanes=l,e)}var hd=ke.ReactCurrentOwner,Qe=!1;function Ae(e,t,n,r){t.child=e===null?Xs(t,null,n,r):Dn(t,e.child,n,r)}function La(e,t,n,r,l){n=n.render;var i=t.ref;return Un(t,l),r=hu(e,t,n,r,i,l),n=vu(),e!==null&&!Qe?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Rt(e,t,l)):(pe&&n&&qi(t),t.flags|=1,Ae(e,t,r,l),t.child)}function Ma(e,t,n,r,l){if(e===null){var i=n.type;return typeof i=="function"&&!Xu(i)&&i.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=i,Pa(e,t,i,r,l)):(e=Hl(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(i=e.child,(e.lanes&l)===0){var o=i.memoizedProps;if(n=n.compare,n=n!==null?n:pr,n(o,r)&&e.ref===t.ref)return Rt(e,t,l)}return t.flags|=1,e=en(i,r),e.ref=t.ref,e.return=t,t.child=e}function Pa(e,t,n,r,l){if(e!==null){var i=e.memoizedProps;if(pr(i,r)&&e.ref===t.ref)if(Qe=!1,t.pendingProps=r=i,(e.lanes&l)!==0)(e.flags&131072)!==0&&(Qe=!0);else return t.lanes=e.lanes,Rt(e,t,l)}return Eu(e,t,n,r,l)}function ja(e,t,n){var r=t.pendingProps,l=r.children,i=e!==null?e.memoizedState:null;if(r.mode==="hidden")if((t.mode&1)===0)t.memoizedState={baseLanes:0,cachePool:null,transitions:null},se(Wn,tt),tt|=n;else{if((n&1073741824)===0)return e=i!==null?i.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,se(Wn,tt),tt|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=i!==null?i.baseLanes:n,se(Wn,tt),tt|=r}else i!==null?(r=i.baseLanes|n,t.memoizedState=null):r=n,se(Wn,tt),tt|=r;return Ae(e,t,l,n),t.child}function Ia(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function Eu(e,t,n,r,l){var i=Ve(n)?un:Te.current;return i=Tn(t,i),Un(t,l),n=hu(e,t,n,r,i,l),r=vu(),e!==null&&!Qe?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Rt(e,t,l)):(pe&&r&&qi(t),t.flags|=1,Ae(e,t,n,l),t.child)}function za(e,t,n,r,l){if(Ve(n)){var i=!0;pl(t)}else i=!1;if(Un(t,l),t.stateNode===null)Il(e,t),wa(t,n,r),ku(t,n,r,l),r=!0;else if(e===null){var o=t.stateNode,c=t.memoizedProps;o.props=c;var d=o.context,w=n.contextType;typeof w=="object"&&w!==null?w=ut(w):(w=Ve(n)?un:Te.current,w=Tn(t,w));var C=n.getDerivedStateFromProps,E=typeof C=="function"||typeof o.getSnapshotBeforeUpdate=="function";E||typeof o.UNSAFE_componentWillReceiveProps!="function"&&typeof o.componentWillReceiveProps!="function"||(c!==r||d!==w)&&Sa(t,o,r,w),Yt=!1;var x=t.memoizedState;o.state=x,kl(t,r,o,l),d=t.memoizedState,c!==r||x!==d||We.current||Yt?(typeof C=="function"&&(Su(t,n,C,r),d=t.memoizedState),(c=Yt||_a(t,n,c,r,x,d,w))?(E||typeof o.UNSAFE_componentWillMount!="function"&&typeof o.componentWillMount!="function"||(typeof o.componentWillMount=="function"&&o.componentWillMount(),typeof o.UNSAFE_componentWillMount=="function"&&o.UNSAFE_componentWillMount()),typeof o.componentDidMount=="function"&&(t.flags|=4194308)):(typeof o.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=d),o.props=r,o.state=d,o.context=w,r=c):(typeof o.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{o=t.stateNode,Ks(e,t),c=t.memoizedProps,w=t.type===t.elementType?c:yt(t.type,c),o.props=w,E=t.pendingProps,x=o.context,d=n.contextType,typeof d=="object"&&d!==null?d=ut(d):(d=Ve(n)?un:Te.current,d=Tn(t,d));var j=n.getDerivedStateFromProps;(C=typeof j=="function"||typeof o.getSnapshotBeforeUpdate=="function")||typeof o.UNSAFE_componentWillReceiveProps!="function"&&typeof o.componentWillReceiveProps!="function"||(c!==E||x!==d)&&Sa(t,o,r,d),Yt=!1,x=t.memoizedState,o.state=x,kl(t,r,o,
Line truncated
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Cutscene Editor</title>
|
|
||||||
<script type="module" crossorigin src="/assets/index-D_5Tak8P.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B96C0g1n.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Cutscene Editor</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cutscene-editor",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.1.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc && vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"immer": "^10.1.1",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"zustand": "^5.0.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^18.3.1",
|
|
||||||
"@types/react-dom": "^18.3.1",
|
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
|
||||||
"typescript": "^5.7.2",
|
|
||||||
"vite": "^6.0.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
.app {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import styles from './App.module.css';
|
|
||||||
import LeftPanel from './components/LeftPanel/LeftPanel';
|
|
||||||
import CenterPanel from './components/CenterPanel/CenterPanel';
|
|
||||||
import RightPanel from './components/RightPanel/RightPanel';
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<div className={styles.app}>
|
|
||||||
<LeftPanel />
|
|
||||||
<CenterPanel />
|
|
||||||
<RightPanel />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
.panel {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewArea {
|
|
||||||
flex: 0 1 420px;
|
|
||||||
min-height: 120px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: #111;
|
|
||||||
padding: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import styles from './CenterPanel.module.css';
|
|
||||||
import Preview from '../Preview/Preview';
|
|
||||||
import Controls from '../Controls/Controls';
|
|
||||||
import Timeline from '../Timeline/Timeline';
|
|
||||||
|
|
||||||
export default function CenterPanel() {
|
|
||||||
return (
|
|
||||||
<div className={styles.panel}>
|
|
||||||
<div className={styles.previewArea}>
|
|
||||||
<Preview />
|
|
||||||
</div>
|
|
||||||
<Controls />
|
|
||||||
<Timeline />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: #1e1e1e;
|
|
||||||
border-top: 1px solid #333;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
background: #2d2d2d;
|
|
||||||
border: 1px solid #404040;
|
|
||||||
color: #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
transition: background 0.1s;
|
|
||||||
min-width: 32px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover:not(:disabled) { background: #3a3a3a; color: #fff; }
|
|
||||||
.btn:disabled { opacity: 0.35; cursor: default; }
|
|
||||||
|
|
||||||
.active {
|
|
||||||
color: #5ba3e0;
|
|
||||||
border-color: #4a7aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Scrubber ───────────────────────────────────────────────── */
|
|
||||||
.scrubber {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
height: 4px;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
border-radius: 2px;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
/* filled portion via CSS variable set inline */
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
#5b9bd5 0%,
|
|
||||||
#5b9bd5 var(--progress, 0%),
|
|
||||||
#3a3a3a var(--progress, 0%),
|
|
||||||
#3a3a3a 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrubber:disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrubber::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #5b9bd5;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid #1e1e1e;
|
|
||||||
transition: transform 0.1s;
|
|
||||||
}
|
|
||||||
.scrubber:not(:disabled)::-webkit-slider-thumb:hover {
|
|
||||||
transform: scale(1.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrubber::-moz-range-thumb {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #5b9bd5;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid #1e1e1e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrubber::-moz-range-track {
|
|
||||||
height: 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
background: #3a3a3a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #888;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
|
|
||||||
import { usePlayback } from '../../hooks/usePlayback';
|
|
||||||
import styles from './Controls.module.css';
|
|
||||||
|
|
||||||
function formatMs(ms: number) {
|
|
||||||
const s = Math.floor(ms / 1000);
|
|
||||||
const frac = Math.floor((ms % 1000) / 10).toString().padStart(2, '0');
|
|
||||||
return `${s}.${frac}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Controls() {
|
|
||||||
usePlayback();
|
|
||||||
|
|
||||||
const { playState, currentTimeMs, setPlayState, setCurrentTime } = useCutsceneStore();
|
|
||||||
const cutscene = useSelectedCutscene();
|
|
||||||
|
|
||||||
const totalMs = cutscene
|
|
||||||
? Math.max(
|
|
||||||
cutscene.durationMs,
|
|
||||||
cutscene.imageSegments.reduce((m, s) => Math.max(m, s.endMs), 0)
|
|
||||||
) + cutscene.endFadeOutMs + cutscene.endFadeInMs
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
function play() {
|
|
||||||
if (playState === 'stopped' || currentTimeMs >= totalMs) setCurrentTime(0);
|
|
||||||
setPlayState('playing');
|
|
||||||
}
|
|
||||||
|
|
||||||
function pause() { setPlayState('paused'); }
|
|
||||||
function stop() { setPlayState('stopped'); setCurrentTime(0); }
|
|
||||||
|
|
||||||
function handleScrub(e: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
const ms = Number(e.target.value);
|
|
||||||
setCurrentTime(ms);
|
|
||||||
if (playState === 'playing') setPlayState('paused');
|
|
||||||
}
|
|
||||||
|
|
||||||
const progress = totalMs > 0 ? currentTimeMs / totalMs : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.controls}>
|
|
||||||
<button className={styles.btn} onClick={stop} title="Stop" disabled={!cutscene}>⏹</button>
|
|
||||||
<button className={styles.btn} onClick={() => setCurrentTime(0)} title="Rewind" disabled={!cutscene}>⏮</button>
|
|
||||||
{playState === 'playing' ? (
|
|
||||||
<button className={`${styles.btn} ${styles.active}`} onClick={pause} title="Pause" disabled={!cutscene}>⏸</button>
|
|
||||||
) : (
|
|
||||||
<button className={`${styles.btn} ${styles.active}`} onClick={play} title="Play" disabled={!cutscene}>▶</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<input
|
|
||||||
className={styles.scrubber}
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={totalMs || 1}
|
|
||||||
step={16}
|
|
||||||
value={currentTimeMs}
|
|
||||||
onChange={handleScrub}
|
|
||||||
disabled={!cutscene}
|
|
||||||
style={{ '--progress': `${progress * 100}%` } as React.CSSProperties}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.time}>
|
|
||||||
{formatMs(currentTimeMs)} / {formatMs(totalMs)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
.panel {
|
|
||||||
width: 200px;
|
|
||||||
min-width: 180px;
|
|
||||||
background: #252525;
|
|
||||||
border-right: 1px solid #333;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #888;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
background: #333;
|
|
||||||
color: #ddd;
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 5px 8px;
|
|
||||||
text-align: center;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover:not(:disabled) { background: #3d3d3d; }
|
|
||||||
.btn:disabled { opacity: 0.4; cursor: default; }
|
|
||||||
|
|
||||||
.btnPrimary {
|
|
||||||
composes: btn;
|
|
||||||
background: #2a4a6e;
|
|
||||||
border-color: #3a6090;
|
|
||||||
color: #a8d0f0;
|
|
||||||
}
|
|
||||||
.btnPrimary:hover { background: #2e5480; }
|
|
||||||
|
|
||||||
.list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
padding: 16px 12px;
|
|
||||||
color: #555;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 7px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
border-left: 3px solid transparent;
|
|
||||||
transition: background 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:hover { background: #2e2e2e; }
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
background: #1e3a55;
|
|
||||||
border-left-color: #5b9bd5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemId {
|
|
||||||
font-size: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deleteBtn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #666;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 10px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.1s, color 0.1s;
|
|
||||||
}
|
|
||||||
.item:hover .deleteBtn { opacity: 1; }
|
|
||||||
.deleteBtn:hover { color: #e06c6c; }
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { useRef } from 'react';
|
|
||||||
import { useCutsceneStore } from '../../store/cutsceneStore';
|
|
||||||
import { parseFile, triggerDownload } from '../../utils/fileIO';
|
|
||||||
import styles from './LeftPanel.module.css';
|
|
||||||
|
|
||||||
export default function LeftPanel() {
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const { file, selectedCutsceneId, loadFile, addCutscene, deleteCutscene, selectCutscene, getExportData } = useCutsceneStore();
|
|
||||||
|
|
||||||
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
const f = e.target.files?.[0];
|
|
||||||
if (!f) return;
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (ev) => {
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(ev.target!.result as string);
|
|
||||||
loadFile(parseFile(json));
|
|
||||||
} catch {
|
|
||||||
alert('Invalid JSON file');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(f);
|
|
||||||
e.target.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSave() {
|
|
||||||
const data = getExportData();
|
|
||||||
if (data) triggerDownload(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDelete(id: string) {
|
|
||||||
if (confirm(`Delete cutscene "${id}"?`)) deleteCutscene(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.panel}>
|
|
||||||
<div className={styles.header}>Cutscenes</div>
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<button className={styles.btnPrimary} onClick={() => fileInputRef.current?.click()}>
|
|
||||||
Load JSON
|
|
||||||
</button>
|
|
||||||
<input ref={fileInputRef} type="file" accept=".json" style={{ display: 'none' }} onChange={handleFileChange} />
|
|
||||||
<button className={styles.btn} onClick={handleSave} disabled={!file}>
|
|
||||||
Save JSON
|
|
||||||
</button>
|
|
||||||
<button className={styles.btn} onClick={addCutscene}>
|
|
||||||
+ New
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.list}>
|
|
||||||
{!file || file.cutscenes.length === 0 ? (
|
|
||||||
<div className={styles.empty}>No cutscenes. Load a JSON or create new.</div>
|
|
||||||
) : (
|
|
||||||
file.cutscenes.map(c => (
|
|
||||||
<div
|
|
||||||
key={c.id}
|
|
||||||
className={`${styles.item} ${c.id === selectedCutsceneId ? styles.selected : ''}`}
|
|
||||||
onClick={() => selectCutscene(c.id)}
|
|
||||||
>
|
|
||||||
<span className={styles.itemId}>{c.id}</span>
|
|
||||||
<button
|
|
||||||
className={styles.deleteBtn}
|
|
||||||
onClick={(e) => { e.stopPropagation(); handleDelete(c.id); }}
|
|
||||||
title="Delete"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
.wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewport {
|
|
||||||
position: relative;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
background: #000;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #444;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitleBar {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 10px 20px 14px;
|
|
||||||
background: linear-gradient(transparent, rgba(0,0,0,0.75));
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #f0c060;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #fff;
|
|
||||||
line-height: 1.5;
|
|
||||||
text-shadow: 0 1px 4px rgba(0,0,0,0.9);
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { useRef, useEffect, useState } from 'react';
|
|
||||||
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
|
|
||||||
import { computeSegmentState, poseToStyle } from '../../utils/rendering';
|
|
||||||
import styles from './Preview.module.css';
|
|
||||||
|
|
||||||
const LOGICAL_W = 1280;
|
|
||||||
const LOGICAL_H = 720;
|
|
||||||
|
|
||||||
function computeSubtitle(cutscene: ReturnType<typeof useSelectedCutscene>, currentMs: number) {
|
|
||||||
if (!cutscene) return null;
|
|
||||||
let elapsed = 0;
|
|
||||||
for (const line of cutscene.lines) {
|
|
||||||
const dur = line.durationMs > 0
|
|
||||||
? line.durationMs
|
|
||||||
: Math.max(1500, Math.round((line.text.length / 17) * 1000));
|
|
||||||
if (currentMs >= elapsed && currentMs < elapsed + dur) return line;
|
|
||||||
elapsed += dur;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Preview() {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [containerSize, setContainerSize] = useState({ w: LOGICAL_W, h: LOGICAL_H });
|
|
||||||
|
|
||||||
const currentTimeMs = useCutsceneStore(s => s.currentTimeMs);
|
|
||||||
const cutscene = useSelectedCutscene();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = containerRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
const ro = new ResizeObserver(entries => {
|
|
||||||
const e = entries[0];
|
|
||||||
if (e) setContainerSize({ w: e.contentRect.width, h: e.contentRect.height });
|
|
||||||
});
|
|
||||||
ro.observe(el);
|
|
||||||
return () => ro.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const subtitle = computeSubtitle(cutscene, currentTimeMs);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.wrapper}>
|
|
||||||
<div className={styles.viewport} ref={containerRef}>
|
|
||||||
{!cutscene ? (
|
|
||||||
<div className={styles.empty}>Select or create a cutscene</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{cutscene.imageSegments.map((seg, i) => {
|
|
||||||
const state = computeSegmentState(seg, currentTimeMs);
|
|
||||||
if (!state) return null;
|
|
||||||
|
|
||||||
const imgStyle = poseToStyle(
|
|
||||||
state.pose,
|
|
||||||
seg.width || LOGICAL_W,
|
|
||||||
seg.height || LOGICAL_H,
|
|
||||||
containerSize.w,
|
|
||||||
containerSize.h,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
key={i}
|
|
||||||
src={`/${seg.path}`}
|
|
||||||
alt=""
|
|
||||||
style={{ ...imgStyle, opacity: state.alpha }}
|
|
||||||
draggable={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{subtitle && (
|
|
||||||
<div className={styles.subtitleBar}>
|
|
||||||
{subtitle.speaker && (
|
|
||||||
<div className={styles.speaker}>{subtitle.speaker}</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.text}>{subtitle.text}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
|
|
||||||
import type { Cutscene } from '../../types/cutscene';
|
|
||||||
import styles from './RightPanel.module.css';
|
|
||||||
|
|
||||||
type CutscenePatch = Partial<Omit<Cutscene, 'imageSegments' | 'lines'>>;
|
|
||||||
|
|
||||||
function NumField({ label, value, onChange, min }: {
|
|
||||||
label: string; value: number; onChange: (v: number) => void; min?: number;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className={styles.field}>
|
|
||||||
<label>{label}</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={value}
|
|
||||||
min={min}
|
|
||||||
onChange={e => onChange(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CutsceneProperties() {
|
|
||||||
const cutscene = useSelectedCutscene();
|
|
||||||
const updateCutscene = useCutsceneStore(s => s.updateCutscene);
|
|
||||||
|
|
||||||
if (!cutscene) return <div className={styles.empty}>No cutscene selected</div>;
|
|
||||||
|
|
||||||
function upd(patch: CutscenePatch) {
|
|
||||||
updateCutscene(cutscene!.id, patch);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.section}>
|
|
||||||
<div className={styles.sectionTitle}>Cutscene</div>
|
|
||||||
|
|
||||||
<div className={styles.field}>
|
|
||||||
<label>ID</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={cutscene.id}
|
|
||||||
onChange={e => upd({ id: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.field}>
|
|
||||||
<label>Skippable</label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={cutscene.skippable}
|
|
||||||
onChange={e => upd({ skippable: e.target.checked })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NumField label="Duration (ms)" value={cutscene.durationMs} onChange={v => upd({ durationMs: v })} min={0} />
|
|
||||||
<NumField label="Fade out (ms)" value={cutscene.fadeOutMs} onChange={v => upd({ fadeOutMs: v })} min={0} />
|
|
||||||
<NumField label="Fade in (ms)" value={cutscene.fadeInMs} onChange={v => upd({ fadeInMs: v })} min={0} />
|
|
||||||
<NumField label="End fade out (ms)" value={cutscene.endFadeOutMs} onChange={v => upd({ endFadeOutMs: v })} min={0} />
|
|
||||||
<NumField label="End fade in (ms)" value={cutscene.endFadeInMs} onChange={v => upd({ endFadeInMs: v })} min={0} />
|
|
||||||
|
|
||||||
<div className={styles.field}>
|
|
||||||
<label>onFadeIn callback</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={cutscene.onFadeInCallback}
|
|
||||||
onChange={e => upd({ onFadeInCallback: e.target.value })}
|
|
||||||
placeholder="lua function name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||