diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8e930f5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,321 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# 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
+
+# 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/
+**/Properties/launchSettings.json
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.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
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# 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
+# 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
+
+# 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
+
+# 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/
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# 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/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# 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
+
diff --git a/RendererDirect3D12/RendererDirect3D12.cpp b/RendererDirect3D12/RendererDirect3D12.cpp
new file mode 100644
index 0000000..e95862c
--- /dev/null
+++ b/RendererDirect3D12/RendererDirect3D12.cpp
@@ -0,0 +1,6 @@
+// RendererDirect3D12.cpp : Defines the exported functions for the DLL application.
+//
+
+#include "stdafx.h"
+
+
diff --git a/RendererDirect3D12/RendererDirect3D12.vcxproj b/RendererDirect3D12/RendererDirect3D12.vcxproj
new file mode 100644
index 0000000..0d3c9a2
--- /dev/null
+++ b/RendererDirect3D12/RendererDirect3D12.vcxproj
@@ -0,0 +1,99 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 15.0
+ {53923514-84B2-4B78-889A-8709C6BFA3A5}
+ Win32Proj
+ RendererDirect3D12
+ 10.0.17763.0
+
+
+
+ DynamicLibrary
+ true
+ v141
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v141
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ false
+
+
+
+ Use
+ Level3
+ Disabled
+ true
+ _DEBUG;RENDERERDIRECT3D12_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+
+
+ Windows
+ true
+
+
+
+
+ Use
+ Level3
+ MaxSpeed
+ true
+ true
+ true
+ NDEBUG;RENDERERDIRECT3D12_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+
+
+ Windows
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+ Create
+ Create
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RendererDirect3D12/RendererDirect3D12.vcxproj.filters b/RendererDirect3D12/RendererDirect3D12.vcxproj.filters
new file mode 100644
index 0000000..2c8dcf7
--- /dev/null
+++ b/RendererDirect3D12/RendererDirect3D12.vcxproj.filters
@@ -0,0 +1,36 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
\ No newline at end of file
diff --git a/RendererDirect3D12/dllmain.cpp b/RendererDirect3D12/dllmain.cpp
new file mode 100644
index 0000000..465ae72
--- /dev/null
+++ b/RendererDirect3D12/dllmain.cpp
@@ -0,0 +1,19 @@
+// dllmain.cpp : Defines the entry point for the DLL application.
+#include "stdafx.h"
+
+BOOL APIENTRY DllMain( HMODULE hModule,
+ DWORD ul_reason_for_call,
+ LPVOID lpReserved
+ )
+{
+ switch (ul_reason_for_call)
+ {
+ case DLL_PROCESS_ATTACH:
+ case DLL_THREAD_ATTACH:
+ case DLL_THREAD_DETACH:
+ case DLL_PROCESS_DETACH:
+ break;
+ }
+ return TRUE;
+}
+
diff --git a/RendererDirect3D12/stdafx.cpp b/RendererDirect3D12/stdafx.cpp
new file mode 100644
index 0000000..fd4f341
--- /dev/null
+++ b/RendererDirect3D12/stdafx.cpp
@@ -0,0 +1 @@
+#include "stdafx.h"
diff --git a/RendererDirect3D12/stdafx.h b/RendererDirect3D12/stdafx.h
new file mode 100644
index 0000000..f380517
--- /dev/null
+++ b/RendererDirect3D12/stdafx.h
@@ -0,0 +1,16 @@
+// stdafx.h : include file for standard system include files,
+// or project specific include files that are used frequently, but
+// are changed infrequently
+//
+
+#pragma once
+
+#include "targetver.h"
+
+#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
+// Windows Header Files
+#include
+
+
+
+// reference additional headers your program requires here
diff --git a/RendererDirect3D12/targetver.h b/RendererDirect3D12/targetver.h
new file mode 100644
index 0000000..87c0086
--- /dev/null
+++ b/RendererDirect3D12/targetver.h
@@ -0,0 +1,8 @@
+#pragma once
+
+// Including SDKDDKVer.h defines the highest available Windows platform.
+
+// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
+// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.
+
+#include
diff --git a/RendererVulkan/RendererVulkan.cpp b/RendererVulkan/RendererVulkan.cpp
new file mode 100644
index 0000000..ca9230f
--- /dev/null
+++ b/RendererVulkan/RendererVulkan.cpp
@@ -0,0 +1,6 @@
+// RendererVulkan.cpp : Defines the exported functions for the DLL application.
+//
+
+#include "stdafx.h"
+
+
diff --git a/RendererVulkan/RendererVulkan.vcxproj b/RendererVulkan/RendererVulkan.vcxproj
new file mode 100644
index 0000000..1eb722a
--- /dev/null
+++ b/RendererVulkan/RendererVulkan.vcxproj
@@ -0,0 +1,99 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 15.0
+ {C9195A1C-9224-4B40-BBBC-AA90EF3BE3E0}
+ Win32Proj
+ RendererVulkan
+ 10.0.17763.0
+
+
+
+ DynamicLibrary
+ true
+ v141
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v141
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ false
+
+
+
+ Use
+ Level3
+ Disabled
+ true
+ _DEBUG;RENDERERVULKAN_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+
+
+ Windows
+ true
+
+
+
+
+ Use
+ Level3
+ MaxSpeed
+ true
+ true
+ true
+ NDEBUG;RENDERERVULKAN_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+
+
+ Windows
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+ Create
+ Create
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RendererVulkan/RendererVulkan.vcxproj.filters b/RendererVulkan/RendererVulkan.vcxproj.filters
new file mode 100644
index 0000000..2356819
--- /dev/null
+++ b/RendererVulkan/RendererVulkan.vcxproj.filters
@@ -0,0 +1,36 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
\ No newline at end of file
diff --git a/RendererVulkan/dllmain.cpp b/RendererVulkan/dllmain.cpp
new file mode 100644
index 0000000..465ae72
--- /dev/null
+++ b/RendererVulkan/dllmain.cpp
@@ -0,0 +1,19 @@
+// dllmain.cpp : Defines the entry point for the DLL application.
+#include "stdafx.h"
+
+BOOL APIENTRY DllMain( HMODULE hModule,
+ DWORD ul_reason_for_call,
+ LPVOID lpReserved
+ )
+{
+ switch (ul_reason_for_call)
+ {
+ case DLL_PROCESS_ATTACH:
+ case DLL_THREAD_ATTACH:
+ case DLL_THREAD_DETACH:
+ case DLL_PROCESS_DETACH:
+ break;
+ }
+ return TRUE;
+}
+
diff --git a/RendererVulkan/stdafx.cpp b/RendererVulkan/stdafx.cpp
new file mode 100644
index 0000000..fd4f341
--- /dev/null
+++ b/RendererVulkan/stdafx.cpp
@@ -0,0 +1 @@
+#include "stdafx.h"
diff --git a/RendererVulkan/stdafx.h b/RendererVulkan/stdafx.h
new file mode 100644
index 0000000..f380517
--- /dev/null
+++ b/RendererVulkan/stdafx.h
@@ -0,0 +1,16 @@
+// stdafx.h : include file for standard system include files,
+// or project specific include files that are used frequently, but
+// are changed infrequently
+//
+
+#pragma once
+
+#include "targetver.h"
+
+#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
+// Windows Header Files
+#include
+
+
+
+// reference additional headers your program requires here
diff --git a/RendererVulkan/targetver.h b/RendererVulkan/targetver.h
new file mode 100644
index 0000000..87c0086
--- /dev/null
+++ b/RendererVulkan/targetver.h
@@ -0,0 +1,8 @@
+#pragma once
+
+// Including SDKDDKVer.h defines the highest available Windows platform.
+
+// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
+// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.
+
+#include
diff --git a/Verus.sln b/Verus.sln
new file mode 100644
index 0000000..7a03404
--- /dev/null
+++ b/Verus.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27130.2036
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Verus", "Verus\Verus.vcxproj", "{B154D670-E4B1-4D8A-885C-69546A5BD833}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VerusTest", "VerusTest\VerusTest.vcxproj", "{D7085182-35B9-4689-ADBA-B77087AD16BF}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererDirect3D12", "RendererDirect3D12\RendererDirect3D12.vcxproj", "{53923514-84B2-4B78-889A-8709C6BFA3A5}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererVulkan", "RendererVulkan\RendererVulkan.vcxproj", "{C9195A1C-9224-4B40-BBBC-AA90EF3BE3E0}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x64 = Debug|x64
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {B154D670-E4B1-4D8A-885C-69546A5BD833}.Debug|x64.ActiveCfg = Debug|x64
+ {B154D670-E4B1-4D8A-885C-69546A5BD833}.Debug|x64.Build.0 = Debug|x64
+ {B154D670-E4B1-4D8A-885C-69546A5BD833}.Release|x64.ActiveCfg = Release|x64
+ {B154D670-E4B1-4D8A-885C-69546A5BD833}.Release|x64.Build.0 = Release|x64
+ {D7085182-35B9-4689-ADBA-B77087AD16BF}.Debug|x64.ActiveCfg = Debug|x64
+ {D7085182-35B9-4689-ADBA-B77087AD16BF}.Debug|x64.Build.0 = Debug|x64
+ {D7085182-35B9-4689-ADBA-B77087AD16BF}.Release|x64.ActiveCfg = Release|x64
+ {D7085182-35B9-4689-ADBA-B77087AD16BF}.Release|x64.Build.0 = Release|x64
+ {53923514-84B2-4B78-889A-8709C6BFA3A5}.Debug|x64.ActiveCfg = Debug|x64
+ {53923514-84B2-4B78-889A-8709C6BFA3A5}.Debug|x64.Build.0 = Debug|x64
+ {53923514-84B2-4B78-889A-8709C6BFA3A5}.Release|x64.ActiveCfg = Release|x64
+ {53923514-84B2-4B78-889A-8709C6BFA3A5}.Release|x64.Build.0 = Release|x64
+ {C9195A1C-9224-4B40-BBBC-AA90EF3BE3E0}.Debug|x64.ActiveCfg = Debug|x64
+ {C9195A1C-9224-4B40-BBBC-AA90EF3BE3E0}.Debug|x64.Build.0 = Debug|x64
+ {C9195A1C-9224-4B40-BBBC-AA90EF3BE3E0}.Release|x64.ActiveCfg = Release|x64
+ {C9195A1C-9224-4B40-BBBC-AA90EF3BE3E0}.Release|x64.Build.0 = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {0D2FFAEF-1E59-46F0-BDDF-1634F6790F83}
+ EndGlobalSection
+EndGlobal
diff --git a/Verus/Verus.props b/Verus/Verus.props
new file mode 100644
index 0000000..e3afd6f
--- /dev/null
+++ b/Verus/Verus.props
@@ -0,0 +1,11 @@
+
+
+
+
+
+ C:\Home\Projects\Verus\verus\Verus\src;C:\Home\Middleware\bullet3-2.88\src;C:\Home\Middleware\bullet3-2.88\Extras;C:\Home\Middleware\glm-0.9.9.3\glm;C:\Home\Middleware\json-3.5.0;C:\Home\Middleware\libogg-1.3.3\include;C:\Home\Middleware\libvorbis-1.3.6\include;C:\Home\Middleware\SDL2-2.0.9\include;C:\Home\Middleware\tinyxml2-7.0.1;C:\Home\Middleware\vectormath;C:\Home\Middleware\zlib-1.2.11;C:\Program Files %28x86%29\OpenAL 1.1 SDK\include;$(IncludePath)
+ C:\Home\Middleware\bullet3-2.88\lib\Debug;C:\Home\Middleware\libogg-1.3.3\lib;C:\Home\Middleware\libvorbis-1.3.6\lib2;C:\Home\Middleware\SDL2-2.0.9\lib\x64;C:\Home\Middleware\tinyxml2-7.0.1;C:\Home\Middleware\zlib-1.2.11;C:\Program Files %28x86%29\OpenAL 1.1 SDK\libs\Win64;$(LibraryPath)
+
+
+
+
\ No newline at end of file
diff --git a/Verus/Verus.vcxproj b/Verus/Verus.vcxproj
new file mode 100644
index 0000000..e63f8f2
--- /dev/null
+++ b/Verus/Verus.vcxproj
@@ -0,0 +1,275 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 15.0
+ {B154D670-E4B1-4D8A-885C-69546A5BD833}
+ Win32Proj
+ Verus
+ 10.0.16299.0
+
+
+
+ StaticLibrary
+ true
+ v141
+ Unicode
+
+
+ StaticLibrary
+ false
+ v141
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ false
+
+
+
+ Use
+ Level3
+ Disabled
+ true
+ _DEBUG;_LIB;%(PreprocessorDefinitions)
+ true
+ verus.h
+ $(ProjectDir)src;%(AdditionalIncludeDirectories)
+
+
+ Windows
+ true
+
+
+
+
+ Use
+ Level3
+ MaxSpeed
+ true
+ true
+ true
+ NDEBUG;_LIB;%(PreprocessorDefinitions)
+ true
+ verus.h
+ $(ProjectDir)src;%(AdditionalIncludeDirectories)
+
+
+ Windows
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NotUsing
+ NotUsing
+ 4244
+ 4244
+
+
+ NotUsing
+ NotUsing
+ 4244
+ 4244
+
+
+ NotUsing
+ NotUsing
+ 4267
+ 4267
+ _CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)
+ _CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)
+
+
+ Create
+ Create
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Verus/Verus.vcxproj.filters b/Verus/Verus.vcxproj.filters
new file mode 100644
index 0000000..ffd24eb
--- /dev/null
+++ b/Verus/Verus.vcxproj.filters
@@ -0,0 +1,549 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;hm;inl;inc;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+ {6dd7f6ca-0929-4d46-9f6d-71ecf8c91abd}
+
+
+ {f756df7f-0ce7-4098-895a-2c1d03fe386b}
+
+
+ {afbabad8-439c-4609-b622-a881c8d6d034}
+
+
+ {9c9fdfe5-4d92-491c-bff1-7442a538ac24}
+
+
+ {d5e876a9-20a7-460a-89e4-c6b1cd902307}
+
+
+ {d44bf87b-c74b-42a1-9b54-dfe744447364}
+
+
+ {ebb05836-e3a7-452d-8e05-a9007ffe1320}
+
+
+ {5ece1220-9707-4688-9429-dcde547cb05f}
+
+
+ {bcb0dace-45fa-45b5-85d6-c80026a9577b}
+
+
+ {b02345a2-66ea-4995-8a5b-b7b6df5e4470}
+
+
+ {779ebfcc-422d-4f9a-a3de-3d5d416612a0}
+
+
+ {9026283b-bf5f-489c-9008-4093adeb4ce6}
+
+
+ {2c793d5f-0a67-4166-8490-3ef154bdb4fc}
+
+
+ {28848bdf-d48d-40a2-92a2-6946c609e14e}
+
+
+ {6c75fda0-ca2a-4925-9260-1d069a48b1b7}
+
+
+ {35c65d58-6412-4aa5-bb70-9f3ab9ee37a2}
+
+
+ {b10e474d-ad34-4fb3-9a41-875a17a54d36}
+
+
+ {8487cc8d-90f6-4f48-83f8-433947c79c5c}
+
+
+
+
+ src\CGI
+
+
+ src\CGI
+
+
+ src\CGI
+
+
+ src\CGI
+
+
+ src\CGI
+
+
+ src\IO
+
+
+ src\IO
+
+
+ src\IO
+
+
+ src
+
+
+ src\D
+
+
+ src\D
+
+
+ src\D
+
+
+ src\D
+
+
+ src\D
+
+
+ src\D
+
+
+ src\Security
+
+
+ src\Security
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Math
+
+
+ src\Game
+
+
+ src\Game
+
+
+ src\Game
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Net
+
+
+ src\Net
+
+
+ src\Net
+
+
+ src\Global
+
+
+ src\Audio
+
+
+ src\Audio
+
+
+ src\Audio
+
+
+ src\Math
+
+
+ src\Math
+
+
+ src\Math
+
+
+ src\Global
+
+
+ src\IO
+
+
+ src\Global
+
+
+ src\Audio
+
+
+ src\Global
+
+
+ src\Audio
+
+
+ src\Global
+
+
+ src\Audio
+
+
+ src\ThirdParty
+
+
+ src\ThirdParty
+
+
+ src\ThirdParty\utf8
+
+
+ src\ThirdParty\utf8
+
+
+ src\ThirdParty\utf8
+
+
+ src\Global
+
+
+ src\ThirdParty\libb64
+
+
+ src\ThirdParty\libb64
+
+
+ src\ThirdParty
+
+
+ src\IO
+
+
+ src\IO
+
+
+ src\Global
+
+
+ src\Scene
+
+
+ src\Scene
+
+
+ src\Math
+
+
+ src\Math
+
+
+ src\Math
+
+
+ src\Math
+
+
+ src\AI
+
+
+ src\AI
+
+
+ src\AI
+
+
+ src\Scene
+
+
+ src\Scene
+
+
+ src\IO
+
+
+ src\Math
+
+
+ src\Anim
+
+
+ src\Anim
+
+
+ src\Anim
+
+
+ src\Anim
+
+
+ src\IO
+
+
+ src\Net
+
+
+ src\Game
+
+
+ src\Global
+
+
+ src\Input
+
+
+ src\Input
+
+
+ src\App
+
+
+ src\App
+
+
+ src\App
+
+
+ src\IO
+
+
+
+
+ src\CGI
+
+
+ src\CGI
+
+
+ src\CGI
+
+
+ src\CGI
+
+
+ src\IO
+
+
+ src\IO
+
+
+ src
+
+
+ src\D
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Math
+
+
+ src\Game
+
+
+ src\Game
+
+
+ src\Global
+
+
+ src\Global
+
+
+ src\Net
+
+
+ src\Net
+
+
+ src\Net
+
+
+ src\Audio
+
+
+ src\Audio
+
+
+ src\Audio
+
+
+ src\Math
+
+
+ src\Math
+
+
+ src\Math
+
+
+ src\IO
+
+
+ src\Audio
+
+
+ src\Audio
+
+
+ src\Global
+
+
+ src\Audio
+
+
+ src\Global
+
+
+ src\ThirdParty\libb64
+
+
+ src\ThirdParty\libb64
+
+
+ src\ThirdParty
+
+
+ src\IO
+
+
+ src\IO
+
+
+ src\Global
+
+
+ src\Scene
+
+
+ src\Scene
+
+
+ src\Math
+
+
+ src\Math
+
+
+ src\Math
+
+
+ src\Math
+
+
+ src\AI
+
+
+ src\AI
+
+
+ src\AI
+
+
+ src\Scene
+
+
+ src\Scene
+
+
+ src\Math
+
+
+ src\Anim
+
+
+ src\Anim
+
+
+ src\Anim
+
+
+ src\Anim
+
+
+ src\IO
+
+
+ src\Net
+
+
+ src\Game
+
+
+ src\Global
+
+
+ src\Input
+
+
+ src\Input
+
+
+ src\App
+
+
+ src\App
+
+
+ src\App
+
+
+ src\IO
+
+
+
\ No newline at end of file
diff --git a/Verus/src/AI/AI.cpp b/Verus/src/AI/AI.cpp
new file mode 100644
index 0000000..cc44d60
--- /dev/null
+++ b/Verus/src/AI/AI.cpp
@@ -0,0 +1 @@
+#include "verus.h"
diff --git a/Verus/src/AI/AI.h b/Verus/src/AI/AI.h
new file mode 100644
index 0000000..29f78a9
--- /dev/null
+++ b/Verus/src/AI/AI.h
@@ -0,0 +1,4 @@
+#pragma once
+
+#include "TaskDriver.h"
+#include "Turret.h"
diff --git a/Verus/src/AI/TaskDriver.cpp b/Verus/src/AI/TaskDriver.cpp
new file mode 100644
index 0000000..ca86f09
--- /dev/null
+++ b/Verus/src/AI/TaskDriver.cpp
@@ -0,0 +1,72 @@
+#include "verus.h"
+
+using namespace verus;
+using namespace verus::AI;
+
+TaskDriver::TaskDriver()
+{
+}
+
+TaskDriver::~TaskDriver()
+{
+}
+
+void TaskDriver::Update()
+{
+ VERUS_QREF_TIMER;
+ VERUS_QREF_UTILS;
+
+ if (_cooldownTimer > 0)
+ {
+ _cooldownTimer -= dt;
+ if (_cooldownTimer < 0)
+ _cooldownTimer = 0;
+ else
+ return;
+ }
+
+ RMode mode = _mapModes.find(_currentMode)->second;
+
+ VERUS_FOREACH(TMapTasks, mode._mapTasks, it)
+ {
+ RTask task = it->second;
+ task._time += dt;
+ if (task._time >= task._deadline)
+ {
+ if (utils.GetRandom().NextFloat() * 100 < task._chance)
+ {
+ if (_pDelegate)
+ _pDelegate->TaskDriver_OnTask(_C(task._name), _C(_currentMode));
+ _cooldownTimer = _cooldown;
+ }
+ task._deadline = task._intervalMin + task._intervalSize*utils.GetRandom().NextFloat();
+ task._time = 0;
+ }
+ }
+}
+
+void TaskDriver::AddMode(CSZ name)
+{
+ if (_mapModes.find(name) != _mapModes.end())
+ return;
+ Mode mode;
+ mode._name = name;
+ _mapModes[name] = mode;
+ _currentMode = name;
+}
+
+void TaskDriver::AddTask(CSZ name, float chance, float intervalMin, float intervalMax, CSZ mode)
+{
+ VERUS_QREF_UTILS;
+
+ RMode thisMode = _mapModes.find(mode ? mode : _currentMode)->second;
+
+ Task task;
+ task._name = name;
+ task._chance = chance;
+ task._intervalMin = intervalMin;
+ task._intervalSize = intervalMax - intervalMin;
+ task._deadline = task._intervalMin + task._intervalSize*utils.GetRandom().NextFloat();
+
+ thisMode._mapTasks[name] = task;
+}
diff --git a/Verus/src/AI/TaskDriver.h b/Verus/src/AI/TaskDriver.h
new file mode 100644
index 0000000..dde5a40
--- /dev/null
+++ b/Verus/src/AI/TaskDriver.h
@@ -0,0 +1,58 @@
+#pragma once
+
+namespace verus
+{
+ namespace AI
+ {
+ struct TaskDriverDelegate
+ {
+ virtual void TaskDriver_OnTask(CSZ task, CSZ mode) = 0;
+ };
+ VERUS_TYPEDEFS(TaskDriverDelegate);
+
+ class TaskDriver
+ {
+ struct Task
+ {
+ String _name;
+ float _chance = 100;
+ float _time = 0;
+ float _deadline = 0;
+ float _intervalMin = 0;
+ float _intervalSize = 0;
+ };
+ VERUS_TYPEDEFS(Task);
+
+ typedef Map TMapTasks;
+
+ struct Mode
+ {
+ String _name;
+ TMapTasks _mapTasks;
+ };
+ VERUS_TYPEDEFS(Mode);
+
+ typedef Map TMapModes;
+
+ TMapModes _mapModes;
+ String _currentMode;
+ PTaskDriverDelegate _pDelegate = nullptr;
+ float _cooldown = 1;
+ float _cooldownTimer = 0;
+
+ public:
+ TaskDriver();
+ ~TaskDriver();
+
+ void Update();
+
+ Str GetCurrentMode() const { return _C(_currentMode); }
+ void SetCurrentMode(CSZ name) { _currentMode = name; }
+ void SetDelegate(PTaskDriverDelegate p) { _pDelegate = p; }
+
+ void AddMode(CSZ name);
+ void AddTask(CSZ name, float chance, float intervalMin, float intervalMax, CSZ mode = nullptr);
+ };
+ VERUS_TYPEDEFS(TaskDriver);
+ }
+}
diff --git a/Verus/src/AI/Turret.cpp b/Verus/src/AI/Turret.cpp
new file mode 100644
index 0000000..eb1c1ea
--- /dev/null
+++ b/Verus/src/AI/Turret.cpp
@@ -0,0 +1,27 @@
+#include "verus.h"
+
+using namespace verus;
+using namespace verus::AI;
+
+void Turret::Update()
+{
+ VERUS_QREF_TIMER;
+
+ float delta = _targetYaw - _actualYaw;
+ if (delta <= -VERUS_PI) delta += VERUS_2PI;
+ if (delta >= +VERUS_PI) delta -= VERUS_2PI;
+ if (delta < 0)
+ _actualYaw -= Math::Max(_yawSpeed*dt, -delta);
+ else
+ _actualYaw += Math::Min(_yawSpeed*dt, delta);
+ _actualYaw = Math::WrapAngle(_actualYaw);
+}
+
+void Turret::LookAt(RcVector3 rayFromTurret, bool instantly)
+{
+ _targetYaw = Math::WrapAngle(atan2(
+ static_cast(rayFromTurret.getX()),
+ static_cast(rayFromTurret.getZ())));
+ if (instantly)
+ _actualYaw = _targetYaw;
+}
diff --git a/Verus/src/AI/Turret.h b/Verus/src/AI/Turret.h
new file mode 100644
index 0000000..6c1894f
--- /dev/null
+++ b/Verus/src/AI/Turret.h
@@ -0,0 +1,34 @@
+#pragma once
+
+namespace verus
+{
+ namespace AI
+ {
+ class Turret
+ {
+ float _targetPitch = 0;
+ float _targetYaw = 0;
+ float _actualPitch = 0;
+ float _actualYaw = 0;
+ float _pitchSpeed = 1;
+ float _yawSpeed = 1;
+
+ public:
+ void Update();
+
+ float GetTargetPitch() const { return _targetPitch; }
+ float GetTargetYaw() const { return _targetYaw; }
+ float GetActualPitch() const { return _actualPitch; }
+ float GetActualYaw() const { return _actualYaw; }
+
+ // Speed:
+ float GetPitchSpeed() const { return _pitchSpeed; }
+ float GetYawSpeed() const { return _yawSpeed; }
+ void SetPitchSpeed(float x) { _pitchSpeed = x; }
+ void SetYawSpeed(float x) { _yawSpeed = x; }
+
+ void LookAt(RcVector3 rayFromTurret, bool instantly = false);
+ };
+ VERUS_TYPEDEFS(Turret);
+ }
+}
diff --git a/Verus/src/Anim/Anim.cpp b/Verus/src/Anim/Anim.cpp
new file mode 100644
index 0000000..67333b2
--- /dev/null
+++ b/Verus/src/Anim/Anim.cpp
@@ -0,0 +1,11 @@
+#include "verus.h"
+
+namespace verus
+{
+ void Make_Anim()
+ {
+ }
+ void Free_Anim()
+ {
+ }
+}
diff --git a/Verus/src/Anim/Anim.h b/Verus/src/Anim/Anim.h
new file mode 100644
index 0000000..1c115b9
--- /dev/null
+++ b/Verus/src/Anim/Anim.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "Motion.h"
+#include "Skeleton.h"
+#include "Animation.h"
+
+namespace verus
+{
+ void Make_Anim();
+ void Free_Anim();
+}
diff --git a/Verus/src/Anim/Animation.cpp b/Verus/src/Anim/Animation.cpp
new file mode 100644
index 0000000..30edcaa
--- /dev/null
+++ b/Verus/src/Anim/Animation.cpp
@@ -0,0 +1,309 @@
+#include "verus.h"
+
+using namespace verus;
+using namespace verus::Anim;
+
+// Collection:
+
+Collection::Collection()
+{
+}
+
+Collection::~Collection()
+{
+ DeleteAll();
+}
+
+void Collection::Async_Run(CSZ url, RcBlob blob)
+{
+ IO::StreamPtr sp(blob);
+ CSZ niceName = strrchr(url, '/');
+ niceName = niceName ? niceName + 1 : url;
+ RMotionData md = _map[niceName];
+ md._motion.Init();
+ md._motion.Deserialize(sp);
+ if (md._duration)
+ md._motion.ComputePlaybackSpeed(md._duration);
+}
+
+void Collection::AddMotion(CSZ name, bool loop, float duration)
+{
+ CSZ niceName = strrchr(name, '/');
+ niceName = niceName ? niceName + 1 : name;
+ RMotionData md = _map[niceName];
+ md._duration = duration;
+ md._loop = loop;
+ IO::Async::I().Load(name, this);
+}
+
+void Collection::DeleteAll()
+{
+ IO::Async::Cancel(this);
+ _map.clear();
+}
+
+PMotionData Collection::Find(CSZ name)
+{
+ return TStoreMotions::Find(name);
+}
+
+int Collection::GetMaxBones()
+{
+ int num = 0;
+ VERUS_FOREACH(TStoreMotions::TMap, _map, it)
+ num = Math::Max(num, it->second._motion.GetNumBones());
+ return num;
+}
+
+// Animation:
+
+Animation::Animation()
+{
+}
+
+Animation::~Animation()
+{
+}
+
+void Animation::Update(int numAlphaTracks, PAlphaTrack pAlphaTracks)
+{
+ VERUS_QREF_TIMER;
+
+ // Async BindCollection():
+ if (_vTriggerStates.size() != _pCollection->GetMaxBones())
+ _vTriggerStates.resize(_pCollection->GetMaxBones());
+
+ // Async BindSkeleton():
+ if (!_blendMotion.GetNumBones() && _pSkeleton->GetNumBones())
+ _pSkeleton->InsertBonesIntoMotion(_blendMotion);
+
+ if (_playing)
+ {
+ if (_blending)
+ {
+ _blendTimer += dt;
+ if (_blendTimer >= _blendDuration)
+ _blending = false;
+ }
+ else if (!_currentMotion.empty())
+ {
+ RMotionData md = *_pCollection->Find(_C(_currentMotion));
+ const float len = md._motion.GetDuration();
+ if (_time < len + 0.5f)
+ {
+ _time += dt;
+ md._motion.ProcessTriggers(_time, this, GetTriggerStatesArray());
+ if (_time >= len)
+ {
+ if (_pDelegate)
+ _pDelegate->Animation_OnEnd(_C(_currentMotion));
+ _time = md._loop ? fmod(_time, len) : len + 1;
+ md._motion.ResetTriggers(GetTriggerStatesArray()); // New loop, reset triggers.
+ }
+ }
+ }
+ }
+
+ if (!_currentMotion.empty() && numAlphaTracks >= 0) // Alpha track (-1) should not modify the skeleton.
+ {
+ RMotionData md = *_pCollection->Find(_C(_currentMotion));
+
+ int numAlphaMotions = 0;
+ Skeleton::AlphaMotion alphaMotions[4];
+
+ for (int i = 0; i < numAlphaTracks && i < 4; ++i)
+ {
+ alphaMotions[i]._pMotion = pAlphaTracks[i]._pAnimation->GetMotion(); // Can be in blend state.
+ alphaMotions[i]._rootBone = pAlphaTracks[i]._rootBone;
+ alphaMotions[i]._alpha = pAlphaTracks[i]._pAnimation->GetAlpha();
+ alphaMotions[i]._time = pAlphaTracks[i]._pAnimation->GetTime();
+ numAlphaMotions++;
+ }
+
+ if (_blending)
+ {
+ md._motion.BindBlendMotion(&_blendMotion, _blendTimer / _blendDuration);
+ _pSkeleton->ApplyMotion(md._motion, _time, numAlphaMotions, alphaMotions);
+ }
+ else
+ {
+#ifdef VERUS_COMPARE_MODE
+ _pSkeleton->ApplyMotion(md._motion, 0, numAlphaMotions, alphaMotions);
+#else
+ _pSkeleton->ApplyMotion(md._motion, _time, numAlphaMotions, alphaMotions);
+#endif
+ }
+ }
+}
+
+void Animation::BindCollection(PCollection p)
+{
+ _pCollection = p;
+}
+
+void Animation::BindSkeleton(PSkeleton p)
+{
+ _pSkeleton = p;
+}
+
+void Animation::SetCurrentMotion(CSZ name)
+{
+ // Reset triggers:
+ if (!_currentMotion.empty())
+ _pCollection->Find(_C(_currentMotion))->_motion.ResetTriggers(GetTriggerStatesArray());
+ if (name)
+ {
+ _pCollection->Find(name)->_motion.ResetTriggers(GetTriggerStatesArray());
+ _currentMotion = name;
+ }
+ else
+ _currentMotion.clear();
+}
+
+void Animation::Play()
+{
+ _playing = true;
+}
+
+void Animation::Stop()
+{
+ _playing = false;
+ _time = 0;
+}
+
+void Animation::Pause()
+{
+ _playing = false;
+}
+
+void Animation::BlendTo(CSZ name, Range duration, int randTime, PMotion pMotionFrom)
+{
+ VERUS_QREF_UTILS;
+
+ PMotion pMotion = pMotionFrom;
+ if (!_currentMotion.empty() || pMotionFrom)
+ {
+ if (!pMotionFrom)
+ pMotion = &_pCollection->Find(_C(_currentMotion))->_motion;
+ if (_blending) // Already blending?
+ pMotion->BindBlendMotion(&_blendMotion, _blendTimer / _blendDuration);
+ pMotion->BakeMotionAt(_time, _blendMotion); // Capture current pose.
+ pMotion->BindBlendMotion(nullptr, 0);
+ }
+
+ if ((0 == duration._max) || (_currentMotion.empty() && name && _prevMotion == name && _blendTimer / _blendDuration < 0.5f))
+ {
+ _blending = false; // Special case for alpha tracks.
+ }
+ else
+ {
+ _blending = true;
+ _blendDuration = (_currentMotion == (name ? name : "")) ? duration._min : duration._max;
+ _blendTimer = 0;
+ }
+
+ _prevMotion = _currentMotion;
+ _currentMotion = name ? name : "";
+
+ // Reset triggers:
+ if (pMotion)
+ pMotion->ResetTriggers(GetTriggerStatesArray());
+ if (name)
+ {
+ pMotion = &_pCollection->Find(name)->_motion;
+ pMotion->ResetTriggers(GetTriggerStatesArray());
+ }
+
+ // Reset time:
+ _time = (randTime >= 0 && pMotion) ? (randTime / 255.f)*pMotion->GetDuration() : 0;
+ if (_time)
+ pMotion->SkipTriggers(_time, GetTriggerStatesArray());
+}
+
+bool Animation::BlendToNew(std::initializer_list names, Range duration, int randTime, PMotion pMotionFrom)
+{
+ for (auto name : names)
+ {
+ if (_currentMotion == name)
+ return false;
+ }
+ BlendTo(*names.begin(), duration, randTime, pMotionFrom);
+ return true;
+}
+
+void Animation::Motion_OnTrigger(CSZ name, int state)
+{
+ if (_pDelegate)
+ _pDelegate->Animation_OnTrigger(name, state);
+}
+
+PMotion Animation::GetMotion()
+{
+ PMotion p = nullptr;
+ if (_currentMotion.empty())
+ {
+ if (!_prevMotion.empty())
+ p = &_pCollection->Find(_C(_prevMotion))->_motion;
+ }
+ else
+ {
+ p = &_pCollection->Find(_C(_currentMotion))->_motion;
+ }
+ if (p && _blending)
+ p->BindBlendMotion(&_blendMotion, _blendTimer / _blendDuration);
+ return p;
+}
+
+float Animation::GetAlpha(CSZ name) const
+{
+ bool matchCurrentMotion = false;
+ bool matchPrevMotion = false;
+ if (name)
+ {
+ matchCurrentMotion = Str::StartsWith(_C(_currentMotion), name);
+ matchPrevMotion = Str::StartsWith(_C(_prevMotion), name);
+ }
+ const bool doBlend = name ? (matchCurrentMotion || matchPrevMotion) : true;
+ if (_blending && doBlend)
+ {
+ bool fadeIn = _prevMotion.empty() && !_currentMotion.empty();
+ bool fadeOut = !_prevMotion.empty() && _currentMotion.empty();
+ if (name)
+ {
+ fadeIn = !matchPrevMotion && matchCurrentMotion;
+ fadeOut = matchPrevMotion && !matchCurrentMotion;
+ }
+ if (fadeIn)
+ return Math::SmoothStep(0, 1, _blendTimer / _blendDuration);
+ if (fadeOut)
+ return Math::SmoothStep(0, 1, 1 - _blendTimer / _blendDuration);
+ }
+ if (name)
+ return matchCurrentMotion ? 1.f : 0.f;
+ else
+ return _currentMotion.empty() ? 0.f : 1.f;
+}
+
+float Animation::GetTime()
+{
+ if (_currentMotion.empty())
+ {
+ if (_prevMotion.empty())
+ return _time;
+ else // When alpha goes to zero, use last motion frame:
+ return _pCollection->Find(_C(_prevMotion))->_motion.GetDuration();
+ }
+ else
+ {
+ return _time;
+ }
+}
+
+bool Animation::IsNearEdge(float t, Edge edge)
+{
+ PMotion pMotion = GetMotion();
+ if (!pMotion)
+ return false;
+ const float at = _time / pMotion->GetDuration();
+ return ((edge&Edge::begin) && at < t) || ((edge&Edge::end) && at >= 1 - t);
+}
diff --git a/Verus/src/Anim/Animation.h b/Verus/src/Anim/Animation.h
new file mode 100644
index 0000000..7286041
--- /dev/null
+++ b/Verus/src/Anim/Animation.h
@@ -0,0 +1,109 @@
+#pragma once
+
+namespace verus
+{
+ namespace Anim
+ {
+ struct AnimationDelegate
+ {
+ virtual void Animation_OnEnd(CSZ name) {}
+ virtual void Animation_OnTrigger(CSZ name, int state) {}
+ };
+ VERUS_TYPEDEFS(AnimationDelegate);
+
+ //! Objects of this type are stored in Collection.
+ struct MotionData
+ {
+ Motion _motion;
+ float _duration = 0;
+ bool _loop = true;
+ };
+ VERUS_TYPEDEFS(MotionData);
+
+ typedef StoreUnique TStoreMotions;
+ //! The collection of motion objects (Motion) that can be reused by multiple animation objects (Animation).
+ class Collection : private TStoreMotions, public IO::AsyncCallback
+ {
+ public:
+ Collection();
+ ~Collection();
+
+ virtual void Async_Run(CSZ url, RcBlob blob) override;
+
+ void AddMotion(CSZ name, bool loop = true, float duration = 0);
+ void DeleteAll();
+ PMotionData Find(CSZ name);
+ int GetMaxBones();
+ };
+ VERUS_TYPEDEFS(Collection);
+
+ //! Holds the collection (Collection) of motion objects (Motion) and provides the ability to interpolate between them.
+
+ //! Note: triggers will work properly with 'multiple animations' + 'single
+ //! collection' only if you add motions before binding collection.
+ //!
+ class Animation : public MotionDelegate
+ {
+ public:
+ enum class Edge : int
+ {
+ begin = (1 << 0),
+ end = (1 << 1)
+ };
+
+ struct AlphaTrack
+ {
+ Animation* _pAnimation = nullptr;
+ CSZ _rootBone = nullptr;
+ };
+ VERUS_TYPEDEFS(AlphaTrack);
+
+ private:
+ PCollection _pCollection = nullptr;
+ PAnimationDelegate _pDelegate = nullptr;
+ PSkeleton _pSkeleton = nullptr;
+ Motion _blendMotion;
+ String _currentMotion;
+ String _prevMotion;
+ Vector _vTriggerStates;
+ float _time = 0;
+ float _blendDuration = 0;
+ float _blendTimer = 0;
+ bool _blending = false;
+ bool _playing = false;
+
+ public:
+ Animation();
+ ~Animation();
+
+ void Update(int numAlphaTracks = 0, PAlphaTrack pAlphaTracks = nullptr);
+
+ void BindCollection(PCollection p);
+ void BindSkeleton(PSkeleton p);
+ void SetCurrentMotion(CSZ name);
+ PAnimationDelegate SetDelegate(PAnimationDelegate p) { return Utils::Swap(_pDelegate, p); }
+
+ void Play();
+ void Stop();
+ void Pause();
+
+ Str GetCurrentMotionName() const { return _C(_currentMotion); }
+
+ void BlendTo(CSZ name,
+ Range duration = 0.5f, int randTime = -1, PMotion pMotionFrom = nullptr);
+ bool BlendToNew(std::initializer_list names,
+ Range duration = 0.5f, int randTime = -1, PMotion pMotionFrom = nullptr);
+ bool IsBlending() const { return _blending; }
+
+ virtual void Motion_OnTrigger(CSZ name, int state) override;
+
+ PMotion GetMotion();
+ float GetAlpha(CSZ name = nullptr) const;
+ float GetTime();
+ bool IsNearEdge(float t = 0.1f, Edge edge = Edge::begin | Edge::end);
+
+ int* GetTriggerStatesArray() { return _vTriggerStates.empty() ? nullptr : _vTriggerStates.data(); }
+ };
+ VERUS_TYPEDEFS(Animation);
+ }
+}
diff --git a/Verus/src/Anim/Motion.cpp b/Verus/src/Anim/Motion.cpp
new file mode 100644
index 0000000..d89c081
--- /dev/null
+++ b/Verus/src/Anim/Motion.cpp
@@ -0,0 +1,972 @@
+#include "verus.h"
+
+using namespace verus;
+using namespace verus::Anim;
+
+// Motion::Bone::Rotation:
+
+Motion::Bone::Rotation::Rotation()
+{
+}
+
+Motion::Bone::Rotation::Rotation(RcQuat q)
+{
+ _q = q;
+}
+
+Motion::Bone::Rotation::Rotation(RcVector3 euler)
+{
+ euler.EulerToQuaternion(_q);
+}
+
+// Motion::Bone:
+
+Motion::Bone::Bone(PMotion pMotion) :
+ _pMotion(pMotion)
+{
+}
+
+Motion::Bone::~Bone()
+{
+ DeleteAll();
+}
+
+void Motion::Bone::DeleteAll()
+{
+ _mapRot.clear();
+ _mapPos.clear();
+ _mapScale.clear();
+ _mapTrigger.clear();
+}
+
+// Insert:
+void Motion::Bone::InsertKeyframeRotation(int frame, RcQuat q)
+{
+ _mapRot[frame] = Rotation(q);
+}
+void Motion::Bone::InsertKeyframeRotation(int frame, RcVector3 euler)
+{
+ _mapRot[frame] = Rotation(euler);
+}
+void Motion::Bone::InsertKeyframePosition(int frame, RcVector3 pos)
+{
+ _mapPos[frame] = pos;
+}
+void Motion::Bone::InsertKeyframeScale(int frame, RcVector3 scale)
+{
+ _mapScale[frame] = scale;
+}
+void Motion::Bone::InsertKeyframeTrigger(int frame, int state)
+{
+ _mapTrigger[frame] = state;
+}
+
+// Delete:
+void Motion::Bone::DeleteKeyframeRotation(int frame)
+{
+ VERUS_IF_FOUND_IN(TMapRot, _mapRot, frame, it)
+ _mapRot.erase(it);
+}
+void Motion::Bone::DeleteKeyframePosition(int frame)
+{
+ VERUS_IF_FOUND_IN(TMapPos, _mapPos, frame, it)
+ _mapPos.erase(it);
+}
+void Motion::Bone::DeleteKeyframeScale(int frame)
+{
+ VERUS_IF_FOUND_IN(TMapScale, _mapScale, frame, it)
+ _mapScale.erase(it);
+}
+void Motion::Bone::DeleteKeyframeTrigger(int frame)
+{
+ VERUS_IF_FOUND_IN(TMapTrigger, _mapTrigger, frame, it)
+ _mapTrigger.erase(it);
+}
+
+// Find:
+bool Motion::Bone::FindKeyframeRotation(int frame, RVector3 euler, RQuat q) const
+{
+ euler = Vector3(0);
+ q = Quat::identity();
+ VERUS_IF_FOUND_IN(TMapRot, _mapRot, frame, it)
+ {
+ euler.EulerFromQuaternion(it->second._q);
+ q = it->second._q;
+ return true;
+ }
+ return false;
+}
+bool Motion::Bone::FindKeyframePosition(int frame, RVector3 pos) const
+{
+ pos = Vector3(0);
+ VERUS_IF_FOUND_IN(TMapPos, _mapPos, frame, it)
+ {
+ pos = it->second;
+ return true;
+ }
+ return false;
+}
+bool Motion::Bone::FindKeyframeScale(int frame, RVector3 scale) const
+{
+ scale = Vector3(1, 1, 1);
+ VERUS_IF_FOUND_IN(TMapScale, _mapScale, frame, it)
+ {
+ scale = it->second;
+ return true;
+ }
+ return false;
+}
+bool Motion::Bone::FindKeyframeTrigger(int frame, int& state) const
+{
+ state = 0;
+ VERUS_IF_FOUND_IN(TMapTrigger, _mapTrigger, frame, it)
+ {
+ state = it->second;
+ return true;
+ }
+ return false;
+}
+
+void Motion::Bone::ComputeRotationAt(float time, RVector3 euler, RQuat q) const
+{
+ Rotation prev, next, null(Quat(0));
+ const float alpha = GetAlpha(_mapRot, prev, next, null, time);
+ q = VMath::slerp(alpha, prev._q, next._q);
+
+ PMotion pBlendMotion = _pMotion->GetBlendMotion();
+ if (pBlendMotion)
+ {
+ PBone pBone = pBlendMotion->FindBone(_C(_name));
+ if (pBone)
+ {
+ Vector3 eulerBlend;
+ Quat qBlend;
+ if (pBone->FindKeyframeRotation(0, eulerBlend, qBlend))
+ q = VMath::slerp(_pMotion->GetBlendAlpha(), qBlend, q);
+ }
+ }
+
+ euler.EulerFromQuaternion(q);
+}
+
+void Motion::Bone::ComputePositionAt(float time, RVector3 pos) const
+{
+ Vector3 prev, next, null(0);
+ const float alpha = GetAlpha(_mapPos, prev, next, null, time);
+ pos = VMath::lerp(alpha, prev, next);
+
+ PMotion pBlendMotion = _pMotion->GetBlendMotion();
+ if (pBlendMotion)
+ {
+ PBone pBone = pBlendMotion->FindBone(_C(_name));
+ if (pBone)
+ {
+ Vector3 posBlend;
+ if (pBone->FindKeyframePosition(0, posBlend))
+ pos = VMath::lerp(_pMotion->GetBlendAlpha(), posBlend, pos);
+ }
+ }
+}
+
+void Motion::Bone::ComputeScaleAt(float time, RVector3 scale) const
+{
+ Vector3 prev, next, null(1, 1, 1);
+ const float alpha = GetAlpha(_mapScale, prev, next, null, time);
+ scale = VMath::lerp(alpha, prev, next);
+
+ PMotion pBlendMotion = _pMotion->GetBlendMotion();
+ if (pBlendMotion)
+ {
+ PBone pBone = pBlendMotion->FindBone(_C(_name));
+ if (pBone)
+ {
+ Vector3 scaleBlend;
+ if (pBone->FindKeyframeScale(0, scaleBlend))
+ scale = VMath::lerp(_pMotion->GetBlendAlpha(), scaleBlend, scale);
+ }
+ }
+}
+
+void Motion::Bone::ComputeTriggerAt(float time, int& state) const
+{
+ if (_mapTrigger.empty())
+ {
+ state = 0;
+ return;
+ }
+ const int frame = int(_pMotion->GetFps()*time);
+ TMapTrigger::const_iterator it = _mapTrigger.upper_bound(frame); // Find frame after 'time'.
+ if (it != _mapTrigger.begin())
+ {
+ it--;
+ state = it->second;
+ }
+ else // All keyframes are after 'time':
+ {
+ state = 0;
+ }
+}
+
+void Motion::Bone::ComputeMatrixAt(float time, RTransform3 mat)
+{
+ Quat q;
+ Vector3 scale, euler, pos;
+ ComputeRotationAt(time, euler, q);
+ ComputePositionAt(time, pos);
+ ComputeScaleAt(time, scale);
+ mat = VMath::appendScale(Transform3(q, pos), scale);
+}
+
+void Motion::Bone::MoveKeyframe(int direction, Type type, int frame)
+{
+ const int frameDest = (direction >= 0) ? frame + 1 : frame - 1;
+ if (frameDest < 0 || frameDest >= _pMotion->GetNumFrames())
+ return;
+
+ switch (type)
+ {
+ case Type::rotation:
+ {
+ VERUS_IF_FOUND_IN(TMapRot, _mapRot, frame, it)
+ {
+ if (_mapRot.find(frameDest) == _mapRot.end())
+ {
+ _mapRot[frameDest] = it->second;
+ _mapRot.erase(it);
+ }
+ }
+ }
+ break;
+ case Type::position:
+ {
+ VERUS_IF_FOUND_IN(TMapPos, _mapPos, frame, it)
+ {
+ if (_mapPos.find(frameDest) == _mapPos.end())
+ {
+ _mapPos[frameDest] = it->second;
+ _mapPos.erase(it);
+ }
+ }
+ }
+ break;
+ case Type::scale:
+ {
+ VERUS_IF_FOUND_IN(TMapScale, _mapScale, frame, it)
+ {
+ if (_mapScale.find(frameDest) == _mapScale.end())
+ {
+ _mapScale[frameDest] = it->second;
+ _mapScale.erase(it);
+ }
+ }
+ }
+ break;
+ case Type::trigger:
+ {
+ VERUS_IF_FOUND_IN(TMapTrigger, _mapTrigger, frame, it)
+ {
+ if (_mapTrigger.find(frameDest) == _mapTrigger.end())
+ {
+ _mapTrigger[frameDest] = it->second;
+ _mapTrigger.erase(it);
+ }
+ }
+ }
+ break;
+ }
+}
+
+void Motion::Bone::Serialize(IO::RStream stream)
+{
+ const int numKeyframesRot = Utils::Cast32(_mapRot.size());
+ const int numKeyframesPos = Utils::Cast32(_mapPos.size());
+ const int numKeyframesScale = Utils::Cast32(_mapScale.size());
+ const int numKeyframesTrigger = Utils::Cast32(_mapTrigger.size());
+
+ stream << numKeyframesRot;
+ VERUS_FOREACH_CONST(TMapRot, _mapRot, it)
+ {
+ stream << it->first;
+ stream.Write(&it->second._q, 16);
+ }
+
+ stream << numKeyframesPos;
+ VERUS_FOREACH_CONST(TMapPos, _mapPos, it)
+ {
+ stream << it->first;
+ stream.Write(&it->second, 12);
+ }
+
+ stream << numKeyframesScale;
+ VERUS_FOREACH_CONST(TMapScale, _mapScale, it)
+ {
+ stream << it->first;
+ stream.Write(&it->second, 12);
+ }
+
+ stream << numKeyframesTrigger;
+ VERUS_FOREACH_CONST(TMapTrigger, _mapTrigger, it)
+ {
+ stream << it->first;
+ stream << it->second;
+ }
+}
+
+void Motion::Bone::Deserialize(IO::RStream stream)
+{
+ int numKeyframesRot, numKeyframesPos, numKeyframesScale, numKeyframesTrigger, frame, state;
+ Vector3 temp;
+ Quat q;
+
+ stream >> numKeyframesRot;
+ VERUS_FOR(i, numKeyframesRot)
+ {
+ stream >> frame;
+ stream.Read(&q, 16);
+ InsertKeyframeRotation(frame, q);
+ }
+
+ stream >> numKeyframesPos;
+ VERUS_FOR(i, numKeyframesPos)
+ {
+ stream >> frame;
+ stream.Read(&temp, 12);
+ InsertKeyframePosition(frame, temp);
+ }
+
+ stream >> numKeyframesScale;
+ VERUS_FOR(i, numKeyframesScale)
+ {
+ stream >> frame;
+ stream.Read(&temp, 12);
+ InsertKeyframeScale(frame, temp);
+ }
+
+ stream >> numKeyframesTrigger;
+ VERUS_FOR(i, numKeyframesTrigger)
+ {
+ stream >> frame;
+ stream >> state;
+ InsertKeyframeTrigger(frame, state);
+ }
+}
+
+void Motion::Bone::DeleteRedundantKeyframes()
+{
+ if (2 == _mapRot.size())
+ {
+ if (!memcmp(
+ &_mapRot.begin()->second,
+ &_mapRot.rbegin()->second,
+ 16))
+ _mapRot.erase(--_mapRot.end());
+ }
+ if (1 == _mapRot.size())
+ {
+ RQuat q = _mapRot.begin()->second._q;
+ if (q.IsIdentity())
+ _mapRot.clear();
+ }
+
+ if (2 == _mapPos.size())
+ {
+ if (!memcmp(
+ &_mapPos.begin()->second,
+ &_mapPos.rbegin()->second,
+ 12))
+ _mapPos.erase(--_mapPos.end());
+ }
+
+ if (2 == _mapScale.size())
+ {
+ if (!memcmp(
+ &_mapScale.begin()->second,
+ &_mapScale.rbegin()->second,
+ 12))
+ _mapScale.erase(--_mapScale.end());
+ }
+}
+
+void Motion::Bone::DeleteOddKeyframes()
+{
+ {
+ TMapRot::iterator it = _mapRot.begin();
+ while (it != _mapRot.end())
+ {
+ if (it->first & 0x1)
+ _mapRot.erase(it++);
+ else
+ ++it;
+ }
+ }
+ {
+ TMapPos::iterator it = _mapPos.begin();
+ while (it != _mapPos.end())
+ {
+ if (it->first & 0x1)
+ _mapPos.erase(it++);
+ else
+ ++it;
+ }
+ }
+ {
+ TMapScale::iterator it = _mapScale.begin();
+ while (it != _mapScale.end())
+ {
+ if (it->first & 0x1)
+ _mapScale.erase(it++);
+ else
+ ++it;
+ }
+ }
+}
+
+void Motion::Bone::InsertLoopKeyframes()
+{
+ if (!_mapRot.empty())
+ _mapRot[_pMotion->GetNumFrames()] = _mapRot.begin()->second;
+ if (!_mapPos.empty())
+ _mapPos[_pMotion->GetNumFrames()] = _mapPos.begin()->second;
+ if (!_mapScale.empty())
+ _mapScale[_pMotion->GetNumFrames()] = _mapScale.begin()->second;
+}
+
+void Motion::Bone::Cut(int frame, bool before)
+{
+ if (before && !frame)
+ return;
+
+ {
+ TMapRot::iterator it = _mapRot.begin();
+ while (it != _mapRot.end())
+ {
+ if (before)
+ {
+ if (it->first >= frame)
+ _mapRot[it->first - frame] = it->second;
+ _mapRot.erase(it++);
+ }
+ else
+ {
+ if (it->first > frame)
+ _mapRot.erase(it++);
+ else
+ ++it;
+ }
+ }
+ }
+ {
+ TMapPos::iterator it = _mapPos.begin();
+ while (it != _mapPos.end())
+ {
+ if (before)
+ {
+ if (it->first >= frame)
+ _mapPos[it->first - frame] = it->second;
+ _mapPos.erase(it++);
+ }
+ else
+ {
+ if (it->first > frame)
+ _mapPos.erase(it++);
+ else
+ ++it;
+ }
+ }
+ }
+ {
+ TMapScale::iterator it = _mapScale.begin();
+ while (it != _mapScale.end())
+ {
+ if (before)
+ {
+ if (it->first >= frame)
+ _mapScale[it->first - frame] = it->second;
+ _mapScale.erase(it++);
+ }
+ else
+ {
+ if (it->first > frame)
+ _mapScale.erase(it++);
+ else
+ ++it;
+ }
+ }
+ }
+}
+
+void Motion::Bone::Fix(bool speedLimit)
+{
+ const float e = 0.02f;
+
+ // Remove rotation keyframes:
+ if (true)
+ {
+ Quat qBase(0);
+ TMapRot::iterator it = _mapRot.begin();
+ bool hasBroken = false;
+ while (it != _mapRot.end())
+ {
+ RcQuat q = it->second._q;
+ const bool broken =
+ Math::IsNaN(q.getX()) ||
+ Math::IsNaN(q.getY()) ||
+ Math::IsNaN(q.getZ()) ||
+ Math::IsNaN(q.getW());
+ if (broken)
+ hasBroken = true;
+ const bool same = qBase.IsEqual(q, e);
+ if (same || broken)
+ _mapRot.erase(it++);
+ else
+ {
+ qBase = q;
+ ++it;
+ }
+ }
+ }
+
+ // Remove position keyframes:
+ if (true)
+ {
+ Vector3 base(0);
+ TMapPos::iterator it = _mapPos.begin();
+ while (it != _mapPos.end())
+ {
+ RcVector3 pos = it->second;
+ const bool broken =
+ Math::IsNaN(pos.getX()) ||
+ Math::IsNaN(pos.getY()) ||
+ Math::IsNaN(pos.getZ());
+ const bool same = base.IsEqual(pos, e);
+ if (same || broken)
+ _mapPos.erase(it++);
+ else
+ {
+ base = pos;
+ ++it;
+ }
+ }
+ }
+
+ // Limit rotation speed:
+ if (speedLimit && Skeleton::IsKinectBone(_C(_name)))
+ {
+ VERUS_FOREACH(TMapRot, _mapRot, it)
+ {
+ RQuat q = it->second._q;
+ if (it->first >= 1)
+ {
+ Quat qPrev;
+ Vector3 euler;
+ ComputeRotationAt((it->first - 1)*_pMotion->GetFpsInv(), euler, qPrev);
+ const float threshold = 0.5f;
+ const Quat qPrevInv = VMath::inverse(Matrix3(qPrev));
+ const Quat qD = q * qPrevInv;
+ const float a = abs(glm::angle(qD.GLM()));
+ if (a > threshold)
+ {
+ const float back = Math::Clamp((a - threshold) / threshold, 0.f, 0.75f);
+ q = VMath::slerp(back, q, qPrev);
+ }
+ }
+ }
+ }
+}
+
+void Motion::Bone::ApplyScaleBias(RcVector3 scale, RcVector3 bias)
+{
+ VERUS_FOREACH(TMapPos, _mapPos, it)
+ it->second = VMath::mulPerElem(it->second, scale) + bias;
+}
+
+void Motion::Bone::Scatter(int srcFrom, int srcTo, int dMin, int dMax)
+{
+ VERUS_QREF_UTILS;
+ int start = srcFrom;
+ const int range = dMax - dMin;
+ while (true)
+ {
+ start += dMin + utils.GetRandom().Next() % range;
+ if (start >= _pMotion->GetNumFrames())
+ break;
+ for (int i = srcFrom; i < srcTo; ++i)
+ {
+ Quat q;
+ Vector3 euler, pos;
+ if (FindKeyframeRotation(i, euler, q))
+ InsertKeyframeRotation(i + start, q);
+ if (FindKeyframePosition(i, pos))
+ InsertKeyframePosition(i + start, pos);
+ }
+ }
+}
+
+int Motion::Bone::GetLastKeyframe() const
+{
+ int frame = 0;
+ if (!_mapRot.empty())
+ frame = Math::Max(frame, _mapRot.rbegin()->first);
+ if (!_mapPos.empty())
+ frame = Math::Max(frame, _mapPos.rbegin()->first);
+ if (!_mapScale.empty())
+ frame = Math::Max(frame, _mapScale.rbegin()->first);
+ if (!_mapTrigger.empty())
+ frame = Math::Max(frame, _mapTrigger.rbegin()->first);
+ return frame;
+}
+
+// Motion:
+
+Motion::Motion()
+{
+}
+
+Motion::~Motion()
+{
+ Done();
+}
+
+void Motion::Init()
+{
+ VERUS_INIT();
+}
+
+void Motion::Done()
+{
+ DeleteAllBones();
+ VERUS_DONE(Motion);
+}
+
+Motion::PBone Motion::GetBoneByIndex(int index)
+{
+ int i = 0;
+ for (auto& kv : _mapBones)
+ {
+ if (i == index)
+ return &kv.second;
+ i++;
+ }
+ return nullptr;
+}
+
+int Motion::GetBoneIndex(CSZ name) const
+{
+ int i = 0;
+ VERUS_FOREACH_CONST(TMapBones, _mapBones, it)
+ {
+ if (it->first == name)
+ return i;
+ i++;
+ }
+ return -1;
+}
+
+Motion::PBone Motion::InsertBone(CSZ name)
+{
+ PBone pBone = FindBone(name);
+ if (pBone)
+ return pBone;
+ Bone bone(this);
+ bone.Rename(name);
+ _mapBones[name] = bone;
+ return &_mapBones[name];
+}
+
+void Motion::DeleteBone(CSZ name)
+{
+ VERUS_IF_FOUND_IN(TMapBones, _mapBones, name, it)
+ _mapBones.erase(it);
+}
+
+void Motion::DeleteAllBones()
+{
+ _mapBones.clear();
+}
+
+Motion::PBone Motion::FindBone(CSZ name)
+{
+ VERUS_IF_FOUND_IN(TMapBones, _mapBones, name, it)
+ return &it->second;
+ return nullptr;
+}
+
+void Motion::Serialize(IO::RStream stream)
+{
+ const UINT32 magic = '2NAX';
+ stream << magic;
+
+ const UINT16 version = xanVersion;
+ stream << version;
+
+ stream << _numFrames;
+ stream << _fps;
+ stream << GetNumBones();
+
+ for (auto& kv : _mapBones)
+ {
+ RBone bone = kv.second;
+ stream.WriteString(_C(bone.GetName()));
+ bone.Serialize(stream);
+ }
+}
+
+void Motion::Deserialize(IO::RStream stream)
+{
+ UINT32 magic = 0;
+ stream >> magic;
+ if ('2NAX' != magic)
+ throw VERUS_RECOVERABLE << "Deserialize(), Invalid XAN format";
+
+ UINT16 version = 0;
+ stream >> version;
+ if (xanVersion != version)
+ throw VERUS_RECOVERABLE << "Deserialize(), Invalid XAN version";
+
+ stream >> _numFrames;
+ stream >> _fps;
+ if (_numFrames < 0 || _numFrames > maxNumFrames)
+ throw VERUS_RECOVERABLE << "Deserialize(), Invalid number of frames in XAN";
+ if (_fps <= 0 || _fps > maxFps)
+ throw VERUS_RECOVERABLE << "Deserialize(), Invalid FPS in XAN";
+
+ SetFps(_fps);
+
+ int numBones = 0;
+ stream >> numBones;
+ if (numBones < 0 || numBones > maxNumBones)
+ throw VERUS_RECOVERABLE << "Deserialize(), Invalid number of bones in XAN";
+
+ char buffer[IO::Stream::bufferSize] = {};
+ VERUS_FOR(i, numBones)
+ {
+ stream.ReadString(buffer);
+ PBone pBone = InsertBone(buffer);
+ pBone->Deserialize(stream);
+ }
+}
+
+void Motion::BakeMotionAt(float time, Motion& dest) const
+{
+ float nativeTime = time * _playbackSpeed;
+ if (_reversed)
+ nativeTime = GetNativeDuration() - nativeTime;
+
+ VERUS_FOREACH_CONST(TMapBones, _mapBones, it)
+ {
+ PcBone pBone = &it->second;
+ PBone pBoneDest = dest.FindBone(_C(it->first));
+ if (pBoneDest)
+ {
+ Vector3 rot, pos, scale;
+ Quat q;
+
+ pBone->ComputeRotationAt(nativeTime, rot, q);
+ pBone->ComputePositionAt(nativeTime, pos);
+ pBone->ComputeScaleAt(nativeTime, scale);
+
+ pBoneDest->InsertKeyframeRotation(0, q);
+ pBoneDest->InsertKeyframePosition(0, pos);
+ pBoneDest->InsertKeyframeScale(0, scale);
+ }
+ }
+}
+
+void Motion::BindBlendMotion(PMotion p, float alpha)
+{
+ _pBlendMotion = p;
+ _blendAlpha = alpha;
+}
+
+void Motion::DeleteRedundantKeyframes()
+{
+ for (auto& kv : _mapBones)
+ kv.second.DeleteRedundantKeyframes();
+}
+
+void Motion::DeleteOddKeyframes()
+{
+ for (auto& kv : _mapBones)
+ kv.second.DeleteOddKeyframes();
+}
+
+void Motion::InsertLoopKeyframes()
+{
+ for (auto& kv : _mapBones)
+ kv.second.InsertLoopKeyframes();
+}
+
+void Motion::Cut(int frame, bool before)
+{
+ for (auto& kv : _mapBones)
+ kv.second.Cut(frame, before);
+ _numFrames = before ? _numFrames - frame : frame + 1;
+}
+
+void Motion::Fix(bool speedLimit)
+{
+ for (auto& kv : _mapBones)
+ kv.second.Fix(speedLimit);
+}
+
+void Motion::ProcessTriggers(float time, PMotionDelegate p, int* pUserTriggerStates)
+{
+ VERUS_RT_ASSERT(p);
+
+ float nativeTime = time * _playbackSpeed;
+ if (_reversed)
+ nativeTime = GetNativeDuration() - nativeTime;
+
+ int i = 0;
+ for (auto& kv : _mapBones)
+ {
+ PBone pBone = &kv.second;
+ int state = 0;
+ if (!(_reversed && nativeTime < _fpsInv*0.5f)) // Avoid edge case for the first frame in reverse.
+ pBone->ComputeTriggerAt(nativeTime, state);
+ const int last = pUserTriggerStates ? pUserTriggerStates[i] : pBone->GetLastTriggerState();
+ if (state != last)
+ {
+ p->Motion_OnTrigger(_C(pBone->GetName()), state);
+ if (pUserTriggerStates)
+ pUserTriggerStates[i] = state;
+ else
+ pBone->SetLastTriggerState(state);
+ }
+ i++;
+ }
+}
+
+void Motion::ResetTriggers(int* pUserTriggerStates)
+{
+ int i = 0;
+ for (auto& kv : _mapBones)
+ {
+ PBone pBone = &kv.second;
+ int state = 0;
+ if (_reversed)
+ pBone->ComputeTriggerAt(GetNativeDuration(), state);
+ if (pUserTriggerStates)
+ pUserTriggerStates[i] = state;
+ else
+ pBone->SetLastTriggerState(state);
+ i++;
+ }
+}
+
+void Motion::SkipTriggers(float time, int* pUserTriggerStates)
+{
+ float nativeTime = time * _playbackSpeed;
+ if (_reversed)
+ nativeTime = GetNativeDuration() - nativeTime;
+
+ int i = 0;
+ for (auto& kv : _mapBones)
+ {
+ PBone pBone = &kv.second;
+ int state;
+ pBone->ComputeTriggerAt(nativeTime, state);
+ if (pUserTriggerStates)
+ pUserTriggerStates[i] = state;
+ else
+ pBone->SetLastTriggerState(state);
+ i++;
+ }
+}
+
+void Motion::ApplyScaleBias(CSZ name, RcVector3 scale, RcVector3 bias)
+{
+ PBone pBone = FindBone(name);
+ if (pBone)
+ pBone->ApplyScaleBias(scale, bias);
+}
+
+void Motion::SetPlaybackSpeed(float x)
+{
+ VERUS_RT_ASSERT(x != 0);
+ _playbackSpeed = x;
+ _reversed = x < 0;
+ if (_reversed)
+ _playbackSpeed = -_playbackSpeed;
+ _playbackSpeedInv = 1 / _playbackSpeed;
+}
+
+void Motion::ComputePlaybackSpeed(float duration)
+{
+ VERUS_RT_ASSERT(duration != 0);
+ _playbackSpeed = GetNativeDuration() / duration;
+ _reversed = duration < 0;
+ if (_reversed)
+ _playbackSpeed = -_playbackSpeed;
+ _playbackSpeedInv = 1 / _playbackSpeed;
+}
+
+void Motion::Exec(CSZ code)
+{
+ if (Str::StartsWith(code, "copy "))
+ {
+ char boneSrc[80] = {};
+ char boneDst[80] = {};
+ sscanf(code, "%*s %s %s", boneSrc, boneDst);
+ PBone pSrc = FindBone(boneSrc);
+ PBone pDst = FindBone(boneDst);
+ pDst->_mapRot = pSrc->_mapRot;
+ pDst->_mapPos = pSrc->_mapPos;
+ }
+ if (Str::StartsWith(code, "scatter "))
+ {
+ int srcFrom = 0, srcTo = 0;
+ int dMin = 0, dMax = 0;
+ char bone[80] = {};
+ sscanf(code, "%*s %d %d %d %d %s", &srcFrom, &srcTo, &dMin, &dMax, bone);
+ const int srcSize = srcTo - srcFrom;
+ if (dMin < srcSize)
+ {
+ const int d = srcSize - dMin;
+ dMin += d;
+ dMax += d;
+ }
+ PBone p = FindBone(bone + 5);
+ if (p)
+ p->Scatter(srcFrom, srcTo, dMin, dMax);
+ }
+ if (Str::StartsWith(code, "delete "))
+ {
+ char what[80] = {};
+ char name[80] = {};
+ sscanf(code, "%*s %s %s", what, name);
+ if (strlen(name) > 5)
+ {
+ PBone p = FindBone(name + 5);
+ if (p)
+ {
+ if (!strcmp(what, "rot"))
+ p->_mapRot.clear();
+ if (!strcmp(what, "pos"))
+ p->_mapPos.clear();
+ if (!strcmp(what, "scale"))
+ p->_mapScale.clear();
+ }
+ }
+ else
+ {
+ for (auto& bone : _mapBones)
+ {
+ if (!strcmp(what, "rot"))
+ bone.second._mapRot.clear();
+ if (!strcmp(what, "pos"))
+ bone.second._mapPos.clear();
+ if (!strcmp(what, "scale"))
+ bone.second._mapScale.clear();
+ }
+ }
+ }
+}
+
+int Motion::GetLastKeyframe() const
+{
+ int frame = 0;
+ VERUS_FOREACH_CONST(TMapBones, _mapBones, it)
+ frame = Math::Max(frame, it->second.GetLastKeyframe());
+ return frame;
+}
diff --git a/Verus/src/Anim/Motion.h b/Verus/src/Anim/Motion.h
new file mode 100644
index 0000000..775430c
--- /dev/null
+++ b/Verus/src/Anim/Motion.h
@@ -0,0 +1,237 @@
+#pragma once
+
+namespace verus
+{
+ namespace Anim
+ {
+ struct MotionDelegate
+ {
+ virtual void Motion_OnTrigger(CSZ name, int state) = 0;
+ };
+ VERUS_TYPEDEFS(MotionDelegate);
+
+ //! Motion holds a series of keyframes and provides the ability to interpolate between them.
+
+ //! Motion can be stored in XAN format. Motion's rate can be
+ //! scaled and even reversed. Animation object (Animation) can be used to
+ //! handle multiple motion objects.
+ //!
+ class Motion : public Object
+ {
+ public:
+ class Bone : public AllocatorAware
+ {
+ friend class Motion;
+
+ class Rotation
+ {
+ public:
+ Quat _q;
+
+ Rotation();
+ Rotation(RcQuat q);
+ Rotation(RcVector3 euler);
+ };
+ VERUS_TYPEDEFS(Rotation);
+
+ typedef Map TMapRot;
+ typedef Map TMapPos;
+ typedef Map TMapScale;
+ typedef Map TMapTrigger;
+
+ String _name;
+ Motion* _pMotion = nullptr;
+ TMapRot _mapRot; //!< Rotation keyframes.
+ TMapPos _mapPos; //!< Position keyframes.
+ TMapScale _mapScale; //!< Scaling keyframes.
+ TMapTrigger _mapTrigger; //!< Trigger keyframes.
+ int _lastTriggerState = 0;
+
+ template
+ float GetAlpha(const TMap& m, T& prev, T& next, const T& null, float time) const
+ {
+ if (m.empty()) // No frames at all, so return null.
+ {
+ prev = null;
+ next = null;
+ return 0;
+ }
+ float alpha;
+ const int frame = static_cast(_pMotion->GetFps()*time); // Frame is before or at 'time'.
+ typename TMap::const_iterator it = m.upper_bound(frame); // Find frame after 'time'.
+ if (it != m.end()) // There are frames greater (after 'time'):
+ {
+ if (it != m.begin()) // And there are less than (before 'time'), full interpolation:
+ {
+ typename TMap::const_iterator itPrev = it;
+ itPrev--;
+ const float prevTime = itPrev->first*_pMotion->GetFpsInv();
+ const float nextTime = it->first*_pMotion->GetFpsInv();
+ const float delta = nextTime - prevTime;
+ const float offset = nextTime - time;
+ alpha = 1 - offset / delta;
+ prev = itPrev->second;
+ next = it->second;
+ }
+ else // But there are no less than:
+ {
+ const float nextTime = it->first*_pMotion->GetFpsInv();
+ const float delta = nextTime;
+ const float offset = time;
+ alpha = offset / delta;
+ prev = null;
+ next = it->second;
+ }
+ }
+ else // There are no frames greater, but there are less than:
+ {
+ it--;
+ alpha = 0;
+ prev = it->second;
+ next = null;
+ }
+ return alpha;
+ }
+
+ public:
+ enum class Type : int
+ {
+ rotation,
+ position,
+ scale,
+ trigger
+ };
+
+ Bone(Motion* pMotion = nullptr);
+ ~Bone();
+
+ Str GetName() const { return _C(_name); }
+ void Rename(CSZ name) { _name = name; }
+
+ int GetLastTriggerState() const { return _lastTriggerState; }
+ void SetLastTriggerState(int state) { _lastTriggerState = state; }
+
+ void DeleteAll();
+
+ void InsertKeyframeRotation(int frame, RcQuat q);
+ void InsertKeyframeRotation(int frame, RcVector3 euler);
+ void InsertKeyframePosition(int frame, RcVector3 pos);
+ void InsertKeyframeScale(int frame, RcVector3 scale);
+ void InsertKeyframeTrigger(int frame, int state);
+
+ void DeleteKeyframeRotation(int frame);
+ void DeleteKeyframePosition(int frame);
+ void DeleteKeyframeScale(int frame);
+ void DeleteKeyframeTrigger(int frame);
+
+ bool FindKeyframeRotation(int frame, RVector3 euler, RQuat q) const;
+ bool FindKeyframePosition(int frame, RVector3 pos) const;
+ bool FindKeyframeScale(int frame, RVector3 scale) const;
+ bool FindKeyframeTrigger(int frame, int& state) const;
+
+ void ComputeRotationAt(float time, RVector3 euler, RQuat q) const;
+ void ComputePositionAt(float time, RVector3 pos) const;
+ void ComputeScaleAt(float time, RVector3 scale) const;
+ void ComputeTriggerAt(float time, int& state) const;
+ void ComputeMatrixAt(float time, RTransform3 mat);
+
+ void MoveKeyframe(int direction, Type type, int frame);
+
+ int GetNumKeysRotation() const { return Utils::Cast32(_mapRot.size()); }
+ int GetNumKeysPosition() const { return Utils::Cast32(_mapPos.size()); }
+ int GetNumKeysScale() const { return Utils::Cast32(_mapScale.size()); }
+ int GetNumKeysTrigger() const { return Utils::Cast32(_mapTrigger.size()); }
+
+ VERUS_P(void Serialize(IO::RStream stream));
+ VERUS_P(void Deserialize(IO::RStream stream));
+
+ void DeleteRedundantKeyframes();
+ void DeleteOddKeyframes();
+ void InsertLoopKeyframes();
+ void Cut(int frame, bool before);
+ void Fix(bool speedLimit);
+
+ void ApplyScaleBias(RcVector3 scale, RcVector3 bias);
+
+ void Scatter(int srcFrom, int srcTo, int dMin, int dMax);
+
+ int GetLastKeyframe() const;
+ };
+ VERUS_TYPEDEFS(Bone);
+
+ private:
+ enum { xanVersion = 0x0101, maxFps = 10000, maxNumBones = 10000, maxNumFrames = 32 * 1024 * 1024 };
+
+ typedef Map TMapBones;
+
+ TMapBones _mapBones;
+ Motion* _pBlendMotion = nullptr;
+ int _numFrames = 50;
+ int _fps = 10;
+ float _fpsInv = 0.1f;
+ float _blendAlpha = 0;
+ float _playbackSpeed = 1;
+ float _playbackSpeedInv = 1;
+ bool _reversed = false;
+
+ public:
+ Motion();
+ ~Motion();
+
+ void Init();
+ void Done();
+
+ int GetFps() const { return _fps; }
+ float GetFpsInv() const { return _fpsInv; }
+ void SetFps(int fps) { _fps = fps; _fpsInv = 1.f / _fps; }
+
+ int GetNumFrames() const { return _numFrames; }
+ void SetNumFrames(int num) { _numFrames = num; }
+
+ int GetNumBones() const { return Utils::Cast32(_mapBones.size()); }
+
+ float GetDuration() const { return GetNativeDuration()*_playbackSpeedInv; }
+ float GetNativeDuration() const { return _numFrames * _fpsInv; }
+
+ PBone GetBoneByIndex(int index);
+ int GetBoneIndex(CSZ name) const;
+
+ PBone InsertBone(CSZ name);
+ void DeleteBone(CSZ name);
+ void DeleteAllBones();
+ PBone FindBone(CSZ name);
+
+ void Serialize(IO::RStream stream);
+ void Deserialize(IO::RStream stream);
+
+ void BakeMotionAt(float time, Motion& dest) const;
+ void BindBlendMotion(Motion* p, float alpha);
+ Motion* GetBlendMotion() const { return _pBlendMotion; }
+ float GetBlendAlpha() const { return _blendAlpha; }
+
+ void DeleteRedundantKeyframes();
+ void DeleteOddKeyframes();
+ void InsertLoopKeyframes();
+ void Cut(int frame, bool before = true);
+ void Fix(bool speedLimit);
+
+ // Triggers:
+ void ProcessTriggers(float time, PMotionDelegate p, int* pUserTriggerStates = nullptr);
+ void ResetTriggers(int* pUserTriggerStates = nullptr);
+ void SkipTriggers(float time, int* pUserTriggerStates = nullptr);
+
+ void ApplyScaleBias(CSZ name, RcVector3 scale, RcVector3 bias);
+
+ float GetPlaybackSpeed() const { return _playbackSpeed; }
+ float GetPlaybackSpeedInv() const { return _playbackSpeedInv; }
+ void SetPlaybackSpeed(float x);
+ void ComputePlaybackSpeed(float duration);
+ bool IsReversed() const { return _reversed; }
+
+ void Exec(CSZ code);
+
+ int GetLastKeyframe() const;
+ };
+ VERUS_TYPEDEFS(Motion);
+ }
+}
diff --git a/Verus/src/Anim/Skeleton.cpp b/Verus/src/Anim/Skeleton.cpp
new file mode 100644
index 0000000..7007f59
--- /dev/null
+++ b/Verus/src/Anim/Skeleton.cpp
@@ -0,0 +1,1247 @@
+#include "verus.h"
+
+using namespace verus;
+using namespace verus::Anim;
+
+Skeleton::Skeleton()
+{
+}
+
+Skeleton::~Skeleton()
+{
+ Done();
+}
+
+void Skeleton::operator=(RcSkeleton that)
+{
+ Init();
+ for (const auto& kv : that._mapBones)
+ _mapBones[kv.first] = kv.second;
+ _numPrimaryBones = that._numPrimaryBones;
+}
+
+void Skeleton::Init()
+{
+ VERUS_INIT();
+}
+
+void Skeleton::Done()
+{
+ EndRagdoll();
+ VERUS_DONE(Skeleton);
+}
+
+void Skeleton::Draw(bool bindPose, int selected)
+{
+#if 0
+ VERUS_QREF_DR;
+ dr.Begin(CGL::CDebugRender::T_LINES, nullptr, false);
+ for (const auto& kv : _mapBones)
+ {
+ PcBone pBone = &kv.second;
+ PcBone pParent = FindBone(_C(pBone->_parentName));
+ if (pParent)
+ {
+ if (bindPose)
+ {
+ dr.AddLine(
+ pParent->_matFromBoneSpace.getTranslation(),
+ pBone->_matFromBoneSpace.getTranslation(),
+ VERUS_COLOR_WHITE);
+ }
+ else
+ {
+ dr.AddLine(
+ (pParent->_matFinal*pParent->_matFromBoneSpace).getTranslation(),
+ (pBone->_matFinal*pBone->_matFromBoneSpace).getTranslation(),
+ VERUS_COLOR_WHITE);
+ }
+ }
+ }
+ for (const auto& kv : _mapBones)
+ {
+ PcBone pBone = &kv.second;
+ const Transform3 mat = bindPose ? pBone->_matFromBoneSpace : pBone->_matFinal*pBone->_matFromBoneSpace;
+
+ const float scale = 0.04f;
+ Point3 a(0);
+ Point3 x(scale, 0, 0);
+ Point3 y(0, scale, 0);
+ Point3 z(0, 0, scale);
+ a = mat * a;
+ x = mat * x;
+ y = mat * y;
+ z = mat * z;
+
+ dr.AddLine(a, x, VERUS_COLOR_RGBA(255, 0, 0, 255));
+ dr.AddLine(a, y, VERUS_COLOR_RGBA(0, 255, 0, 255));
+ dr.AddLine(a, z, VERUS_COLOR_RGBA(0, 0, 255, 255));
+
+ if (pBone->_shaderIndex == selected)
+ {
+ const float scale2 = scale * 0.5f;
+ Point3 x(scale2, 0, 0);
+ Point3 y(0, scale2, 0);
+ Point3 z(0, 0, scale2);
+ x = mat * x;
+ y = mat * y;
+ z = mat * z;
+
+ dr.AddLine(x, y, VERUS_COLOR_RGBA(255, 255, 0, 255));
+ dr.AddLine(y, z, VERUS_COLOR_RGBA(255, 255, 0, 255));
+ dr.AddLine(z, x, VERUS_COLOR_RGBA(255, 255, 0, 255));
+ }
+ }
+ dr.End();
+#endif
+}
+
+Skeleton::PBone Skeleton::InsertBone(RBone bone)
+{
+ PBone p = FindBone(_C(bone._name));
+ if (p)
+ return p;
+
+ bone._shaderIndex = Utils::Cast32(_mapBones.size()); // Assume bones are added in same order as matrices in shader.
+ _mapBones[bone._name] = bone;
+ return FindBone(_C(bone._name));
+}
+
+Skeleton::PBone Skeleton::FindBone(CSZ name)
+{
+ VERUS_IF_FOUND_IN(TMapBones, _mapBones, name, it)
+ return &it->second;
+ return nullptr;
+}
+
+Skeleton::PcBone Skeleton::FindBone(CSZ name) const
+{
+ VERUS_IF_FOUND_IN(TMapBones, _mapBones, name, it)
+ return &it->second;
+ return nullptr;
+}
+
+Skeleton::PBone Skeleton::FindBoneByIndex(int index)
+{
+ for (auto& kv : _mapBones)
+ {
+ if (kv.second._shaderIndex == index)
+ return &kv.second;
+ }
+ return nullptr;
+}
+
+void Skeleton::ApplyMotion(RMotion motion, float time, int numAlphaMotions, PAlphaMotion pAlphaMotions)
+{
+ if (_ragdollMode)
+ {
+ for (auto& kv : _mapBones)
+ {
+ RBone bone = kv.second;
+ if (bone._pBody)
+ {
+ bone._matFinal = _matRagdollToWorldInv * Transform3(bone._pBody->getWorldTransform())*bone._matToActorSpace;
+ }
+ else
+ {
+ bone._matFinal = Transform3::identity();
+ if (bone._shaderIndex >= 0)
+ {
+ PBone pParent = FindBone(_C(bone._parentName));
+ while (pParent)
+ {
+ if (pParent->_pBody)
+ {
+ bone._matFinal = _matRagdollToWorldInv * Transform3(pParent->_pBody->getWorldTransform())*pParent->_matToActorSpace;
+ break;
+ }
+ pParent = FindBone(_C(pParent->_parentName));
+ }
+ }
+ }
+ bone._matFinalInv = VMath::inverse(bone._matFinal);
+ }
+ return;
+ }
+
+ _pCurrentMotion = &motion;
+ _currentTime = time * _pCurrentMotion->GetPlaybackSpeed(); // To native time.
+ if (_pCurrentMotion->IsReversed())
+ _currentTime = _pCurrentMotion->GetNativeDuration() - _currentTime;
+
+ _numAlphaMotions = numAlphaMotions;
+ _pAlphaMotions = pAlphaMotions;
+
+ ResetBones();
+ for (auto& kv : _mapBones)
+ {
+ RBone bone = kv.second;
+ if (!bone._ready)
+ {
+ _pCurrentBone = &bone;
+ _matParents = Transform3::identity();
+ RecursiveBoneUpdate();
+ }
+ }
+
+ // Reset blend motion!
+ motion.BindBlendMotion(nullptr, 0);
+ VERUS_FOR(i, _numAlphaMotions)
+ {
+ if (_pAlphaMotions[i]._pMotion)
+ _pAlphaMotions[i]._pMotion->BindBlendMotion(nullptr, 0);
+ }
+}
+
+void Skeleton::FillMatrixArray(mataff* p) const
+{
+#if 0
+ VERUS_QREF_RENDER;
+ for (const auto& kv : _mapBones)
+ {
+ RcBone bone = kv.second;
+ if (bone._shaderIndex >= 0 && bone._shaderIndex < render.GetMaxNumBones())
+ p[bone._shaderIndex] = bone._matFinal.ConstBufferFormat();
+ }
+#endif
+}
+
+void Skeleton::ResetFinalPose()
+{
+ for (auto& kv : _mapBones)
+ {
+ kv.second._matFinal = Transform3::identity();
+ kv.second._matFinalInv = Transform3::identity();
+ }
+}
+
+void Skeleton::ResetBones()
+{
+ for (auto& kv : _mapBones)
+ kv.second._ready = false;
+}
+
+void Skeleton::RecursiveBoneUpdate()
+{
+ Transform3 mat;
+ PBone pCurrentBone = _pCurrentBone;
+
+ Motion::PBone pMotionBone = _pCurrentMotion->FindBone(_C(pCurrentBone->_name));
+
+ if (pMotionBone)
+ {
+ Quat q;
+ Vector3 scale, euler, pos;
+ pMotionBone->ComputeRotationAt(_currentTime, euler, q);
+ pMotionBone->ComputePositionAt(_currentTime, pos);
+ pMotionBone->ComputeScaleAt(_currentTime, scale);
+
+ // Blend with other motions:
+ VERUS_FOR(i, _numAlphaMotions)
+ {
+ if (!_pAlphaMotions[i]._pMotion)
+ continue;
+
+ PMotion pAlphaMotion = _pAlphaMotions[i]._pMotion;
+ Motion::PBone pAlphaBone = pAlphaMotion->FindBone(_C(pCurrentBone->_name));
+ const float alpha = _pAlphaMotions[i]._alpha;
+ float time = _pAlphaMotions[i]._time*pAlphaMotion->GetPlaybackSpeed(); // To native time.
+ if (pAlphaMotion->IsReversed())
+ time = pAlphaMotion->GetNativeDuration() - time;
+ if (pAlphaBone && alpha > 0 &&
+ (pCurrentBone->_name == _pAlphaMotions[i]._rootBone ||
+ IsParentOf(_C(pCurrentBone->_name), _pAlphaMotions[i]._rootBone)))
+ {
+ // Alpha motion can also be in blend state.
+ Quat qA;
+ Vector3 scaleA, eulerA, posA;
+ pAlphaBone->ComputeRotationAt(time, eulerA, qA);
+ pAlphaBone->ComputePositionAt(time, posA);
+ pAlphaBone->ComputeScaleAt(time, scaleA);
+
+ // Mix with alpha motion:
+ q = VMath::slerp(alpha, q, qA);
+ pos = VMath::lerp(alpha, pos, posA);
+ scale = VMath::lerp(alpha, scale, scaleA);
+ }
+ }
+
+ const Transform3 matSRT = VMath::appendScale(Transform3(q, pos), scale);
+ const Transform3 matBone = pCurrentBone->_matExternal*matSRT;
+ mat = pCurrentBone->_matFromBoneSpace*matBone*pCurrentBone->_matToBoneSpace*pCurrentBone->_matAdapt;
+ }
+ else
+ mat = Transform3::identity();
+
+ _pCurrentBone = FindBone(_C(pCurrentBone->_parentName));
+ if (_pCurrentBone)
+ {
+ if (_pCurrentBone->_ready)
+ _matParents = _pCurrentBone->_matFinal;
+ else
+ RecursiveBoneUpdate();
+ }
+ else if (pMotionBone = _pCurrentMotion->FindBone(RootName()))
+ {
+ Quat q;
+ Vector3 scale, euler, pos;
+ pMotionBone->ComputeRotationAt(_currentTime, euler, q);
+ pMotionBone->ComputePositionAt(_currentTime, pos);
+ pMotionBone->ComputeScaleAt(_currentTime, scale);
+ const Transform3 matSRT = VMath::appendScale(Transform3(q, pos), scale);
+ mat = matSRT * mat;
+ }
+
+ _matParents = _matParents * mat;
+ pCurrentBone->_matFinal = _matParents;
+ pCurrentBone->_matFinalInv = VMath::inverse(_matParents);
+ pCurrentBone->_ready = true;
+}
+
+void Skeleton::VisitBones(std::function fn)
+{
+ for (auto& kv : _mapBones)
+ if (Continue::yes != fn(kv.second))
+ return;
+}
+
+void Skeleton::VisitBones(std::function fn) const
+{
+ for (const auto& kv : _mapBones)
+ if (Continue::yes != fn(kv.second))
+ return;
+}
+
+void Skeleton::InsertBonesIntoMotion(RMotion motion) const
+{
+ for (const auto& kv : _mapBones)
+ motion.InsertBone(_C(kv.first));
+}
+
+void Skeleton::DeleteOutsiders(RMotion motion) const
+{
+ const int num = motion.GetNumBones();
+ Vector vNames;
+ VERUS_FOR(i, num)
+ if (_mapBones.find(_C(motion.GetBoneByIndex(i)->GetName())) == _mapBones.end() &&
+ motion.GetBoneByIndex(i)->GetName() != RootName())
+ vNames.push_back(_C(motion.GetBoneByIndex(i)->GetName()));
+ VERUS_FOREACH_CONST(Vector, vNames, it)
+ motion.DeleteBone(_C(*it));
+}
+
+void Skeleton::AdjustPrimaryBones(const Vector& vPrimaryBones)
+{
+ typedef Map TMapSort;
+ TMapSort mapSort;
+
+ enum { secondaryOffset = 1000000 };
+
+ const bool inverse = (vPrimaryBones.end() != std::find(vPrimaryBones.begin(), vPrimaryBones.end(), "-"));
+ int addPri = 0;
+ int addSec = secondaryOffset;
+ if (inverse)
+ std::swap(addPri, addSec);
+
+ for (const auto& kv : _mapBones)
+ {
+ RcBone bone = kv.second;
+ if (vPrimaryBones.end() != std::find(vPrimaryBones.begin(), vPrimaryBones.end(), bone._name))
+ mapSort[bone._shaderIndex + addPri] = bone._name;
+ else
+ mapSort[bone._shaderIndex + addSec] = bone._name;
+ }
+
+ _numPrimaryBones = 0;
+
+ for (const auto& kv : mapSort)
+ {
+ if (kv.first < secondaryOffset) // Primary bone:
+ {
+ PBone pBone = FindBone(_C(kv.second));
+ const int newIndex = Utils::Cast32(_mapPrimary.size());
+ _mapPrimary[pBone->_shaderIndex] = newIndex;
+ pBone->_shaderIndex = newIndex;
+ _numPrimaryBones++;
+ }
+ else // Secondary bone:
+ {
+ PBone pBone = FindBone(_C(kv.second));
+ PcBone pParent = FindBone(_C(pBone->_parentName));
+ bool isPrimary = false;
+ while (!isPrimary)
+ {
+ isPrimary = (vPrimaryBones.end() != std::find(vPrimaryBones.begin(), vPrimaryBones.end(), pParent->_name));
+ if (inverse)
+ isPrimary = !isPrimary;
+ if (!isPrimary)
+ pParent = FindBone(_C(pParent->_parentName));
+ }
+ _mapPrimary[pBone->_shaderIndex] = pParent->_shaderIndex;
+ pBone->_shaderIndex = -1; // Do not set any vertex shader's register.
+ }
+ }
+}
+
+int Skeleton::RemapBoneIndex(int index) const
+{
+ VERUS_IF_FOUND_IN(TMapPrimary, _mapPrimary, index, it)
+ return it->second;
+ return 0;
+}
+
+bool Skeleton::IsParentOf(CSZ bone, CSZ parent) const
+{
+ PcBone pBone = FindBone(bone);
+ while (pBone)
+ {
+ if (pBone->_parentName == parent)
+ return true;
+ pBone = FindBone(_C(pBone->_parentName));
+ }
+ return false;
+}
+
+void Skeleton::LoadRigInfo(CSZ url)
+{
+ Vector v;
+ IO::FileSystem::LoadResource(url, v, IO::FileSystem::LoadDesc(true));
+ LoadRigInfoFromPtr(v.data());
+}
+
+void Skeleton::LoadRigInfoFromPtr(const BYTE* p)
+{
+ for (auto& kv : _mapBones)
+ {
+ RBone bone = kv.second;
+ bone._rigRot = Vector3(0);
+ bone._cRot = Vector3(0);
+ bone._cLimits = Vector3(0);
+ bone._boxSize = Vector3(0);
+ bone._width = 0;
+ bone._length = 0;
+ bone._mass = 0.01f;
+ bone._rigBone = true;
+ bone._hinge = false;
+ bone._noCollision = false;
+ }
+
+ float massCheck = 0;
+ if (p)
+ {
+ for (auto& kv : _mapBones)
+ {
+ RBone bone = kv.second;
+ bone._rigBone = false;
+ }
+
+ tinyxml2::XMLDocument doc;
+ doc.Parse(reinterpret_cast(p));
+ if (doc.Error())
+ return;
+
+ tinyxml2::XMLElement* pElem = nullptr;
+ tinyxml2::XMLElement* pRoot = doc.FirstChildElement();
+ if (!pRoot)
+ return;
+
+ pElem = pRoot->FirstChildElement("mass");
+ if (pElem)
+ _mass = static_cast(atof(pElem->GetText()));
+
+ for (pElem = pRoot->FirstChildElement("bone"); pElem; pElem = pElem->NextSiblingElement("bone"))
+ {
+ PBone pBone = FindBone(pElem->Attribute("name"));
+ if (pBone)
+ {
+ pBone->_rigBone = true;
+ pElem->QueryFloatAttribute("w", &pBone->_width);
+ pElem->QueryFloatAttribute("l", &pBone->_length);
+ CSZ rot = pElem->Attribute("rot");
+ if (rot)
+ pBone->_rigRot.FromString(rot);
+
+ CSZ climit = pElem->Attribute("climit");
+ if (climit)
+ pBone->_cLimits.FromString(climit);
+
+ CSZ ctype = pElem->Attribute("ctype");
+ if (ctype)
+ {
+ if (!strcmp(ctype, "hinge"))
+ pBone->_hinge = true;
+ if (!strcmp(ctype, "free"))
+ pBone->_cLimits.setZ(-100);
+ }
+
+ CSZ crot = pElem->Attribute("crot");
+ if (crot)
+ pBone->_cRot.FromString(crot);
+
+ CSZ boxSize = pElem->Attribute("boxSize");
+ if (boxSize)
+ pBone->_boxSize.FromString(boxSize);
+
+ float pmass = 0;
+ pElem->QueryFloatAttribute("m", &pmass);
+ if (pmass != 0)
+ pBone->_mass = pmass * _mass;
+
+ pElem->QueryBoolAttribute("noCollision", &pBone->_noCollision);
+
+ massCheck += pBone->_mass;
+ }
+ }
+ }
+ if (massCheck > 1000)
+ {
+ VERUS_LOG_DEBUG("Ton");
+ }
+}
+
+void Skeleton::BeginRagdoll(RcTransform3 matW, RcVector3 impulse, CSZ bone)
+{
+#if 0
+ VERUS_QREF_BULLET;
+
+ EndRagdoll();
+
+ _matRagdollToWorld = matW;
+ _matRagdollToWorldInv = VMath::inverse(matW);
+
+ // RagdollDemo values:
+ const float dampL = 0.05f;
+ const float dampA = 0.85f*1.15f;
+ const float daTime = 0.8f;
+ const float sleepL = 1.6f;
+ const float sleepA = 2.5f;
+
+ for (auto& kv : _mapBones)
+ kv.second._ready = true;
+
+ for (auto& kv : _mapBones)
+ {
+ PBone pBone = &kv.second;
+ PBone pParent = FindBone(_C(pBone->_parentName));
+
+ if (pParent && pParent->_rigBone && !pParent->_pShape) // Create a shape for parent bone:
+ {
+ const Point3 a = pParent->_matFromBoneSpace.getTranslation();
+ const Point3 b = pBone->_matFromBoneSpace.getTranslation();
+
+ if (VMath::distSqr(a, b) < 0.01f*0.01f) // Too short, unsuitable?
+ {
+ pParent->_ready = false; // Mark it.
+ continue;
+ }
+
+ // Pick the correct child bone:
+ if (0 == pParent->_length)
+ {
+ const Vector3 test = VMath::normalizeApprox(Vector3(pParent->_matToBoneSpace*b));
+ if (test.getX() < 0.9f) // Bone should point to child bone.
+ continue;
+ }
+
+ const float len = (pParent->_length != 0) ?
+ pParent->_length : Math::Max(static_cast(VMath::dist(a, b)), 0.01f);
+ const float w = (pParent->_width != 0) ? pParent->_width : len * 0.3f;
+ pParent->_length = len;
+ pParent->_width = w;
+
+ if (pParent->_boxSize.IsZero())
+ {
+ pParent->_pShape = new btCapsuleShape(w, len);
+ }
+ else
+ {
+ if (0 == pParent->_boxSize.getY())
+ pParent->_boxSize.setY(len*0.5f);
+ pParent->_pShape = new btBoxShape(pParent->_boxSize.Bullet());
+ }
+
+ Transform3 matBody;
+ if (pParent->_rigRot.IsZero())
+ {
+ matBody = pParent->_matFromBoneSpace*
+ Transform3(Matrix3::rotationZ(Math::ToRadians(90)), Vector3(len*0.5f, 0, 0));
+ }
+ else
+ {
+ const Transform3 matR = Transform3::rotationZYX(pParent->_rigRot);
+ const Transform3 matT = Transform3(Matrix3::rotationZ(Math::ToRadians(90)), Vector3(len*0.5f, 0, 0));
+ matBody = pParent->_matFromBoneSpace*matR*matT;
+ }
+ pParent->_matToActorSpace = VMath::inverse(matBody);
+ matBody = _matRagdollToWorld * pParent->_matFinal*matBody;
+
+ short group = 1, mask = -1;
+ if (pParent->_noCollision)
+ group = mask = 0;
+ const btTransform t = matBody.Bullet();
+ pParent->_pBody = bullet.AddNewRigidBody(pParent->_mass, t, pParent->_pShape, group, mask);
+ pParent->_pBody->setFriction(Physics::CBullet::GetFriction(Physics::Material::leather));
+ pParent->_pBody->setRestitution(Physics::CBullet::GetRestitution(Physics::Material::leather)*0.5f);
+ pParent->_pBody->setDamping(dampL, dampA);
+ pParent->_pBody->setDeactivationTime(daTime);
+ pParent->_pBody->setSleepingThresholds(sleepL, sleepA);
+ }
+ }
+
+ // Aligns constraint to good starting point:
+ const Transform3 matInitC = Transform3::rotationZYX(Vector3(-VERUS_PI / 2, 0, -VERUS_PI / 2));
+
+ // Create leaf actors and joints:
+ for (auto& kv : _mapBones)
+ {
+ PBone pBone = &kv.second;
+ PBone pParent = pBone;
+ do
+ {
+ pParent = FindBone(_C(pParent->_parentName));
+ } while (pParent && (!pParent->_rigBone || !pParent->_ready)); // Skip unsuitable bones.
+
+ bool isLeaf = false;
+ if (pBone->_rigBone && !pBone->_pShape && pBone->_ready)
+ {
+ const float len = (pBone->_length != 0) ? pBone->_length : 0.1f;
+ const float w = (pBone->_width != 0) ? pBone->_width : len * 0.3f;
+ pBone->_length = len;
+ pBone->_width = w;
+
+ pBone->_pShape = new btCapsuleShape(w, len); // Leaf is always a capsule.
+
+ Transform3 matBody;
+ if (pBone->_rigRot.IsZero())
+ {
+ matBody = pBone->_matFromBoneSpace*
+ Transform3(Matrix3::rotationZ(Math::ToRadians(90)), Vector3(len*0.5f, 0, 0));
+ }
+ else
+ {
+ const Transform3 matR = Transform3::rotationZYX(pBone->_rigRot);
+ const Transform3 matT = Transform3(Matrix3::rotationZ(Math::ToRadians(90)), Vector3(len*0.5f, 0, 0));
+ matBody = pBone->_matFromBoneSpace*matR*matT;
+ }
+ pBone->_matToActorSpace = VMath::inverse(matBody);
+ matBody = _matRagdollToWorld * pBone->_matFinal*matBody;
+
+ short group = 1, mask = -1;
+ if (pBone->_noCollision)
+ group = mask = 0;
+ const btTransform t = matBody.Bullet();
+ pBone->_pBody = bullet.AddNewRigidBody(pBone->_mass, t, pBone->_pShape, group, mask);
+ pBone->_pBody->setFriction(Physics::CBullet::GetFriction(Physics::Material::leather));
+ pBone->_pBody->setRestitution(Physics::CBullet::GetRestitution(Physics::Material::leather)*0.5f);
+ pBone->_pBody->setDamping(dampL, dampA);
+ pBone->_pBody->setDeactivationTime(daTime);
+ pBone->_pBody->setSleepingThresholds(sleepL, sleepA);
+
+ isLeaf = true;
+ }
+
+ if (pBone->_pShape && pParent && pParent->_pShape && pBone->_cLimits.getZ() > -99) // Connect bones?
+ {
+ btTransform localA, localB;
+ const Transform3 matToParentSpace = pParent->_matToActorSpace*VMath::inverse(pBone->_matToActorSpace);
+
+ const Transform3 matR = Transform3::rotationZYX(pBone->_cRot);
+ const Transform3 matT = Transform3::translation(Vector3(0, pBone->_length*0.5f, 0));
+ localA = Transform3(matT*matInitC*matR).Bullet();
+ localB = Transform3(matToParentSpace*matT*matInitC*matR).Bullet();
+
+ if (pBone->_hinge)
+ {
+ btHingeConstraint* pHingeC = new btHingeConstraint(*pBone->_pBody, *pParent->_pBody, localA, localB);
+ if (!pBone->_cLimits.IsZero())
+ pHingeC->setLimit(pBone->_cLimits.getX(), pBone->_cLimits.getY());
+ else
+ pHingeC->setLimit(-VERUS_PI / 2, VERUS_PI / 2);
+ pBone->_pConstraint = pHingeC;
+ bullet.GetWorld()->addConstraint(pBone->_pConstraint, true);
+ }
+ else
+ {
+ btConeTwistConstraint* pConeC = new btConeTwistConstraint(*pBone->_pBody, *pParent->_pBody, localA, localB);
+ if (!pBone->_cLimits.IsZero())
+ pConeC->setLimit(pBone->_cLimits.getX(), pBone->_cLimits.getY(), pBone->_cLimits.getZ(), 0.9f);
+ else
+ pConeC->setLimit(VERUS_PI / 4, VERUS_PI / 4, VERUS_PI / 4, 0.9f);
+ if (!isLeaf)
+ pConeC->setFixThresh(1);
+ pBone->_pConstraint = pConeC;
+ bullet.GetWorld()->addConstraint(pBone->_pConstraint, true);
+ }
+ }
+
+ if (pBone->_name == bone && pBone->_pBody)
+ pBone->_pBody->applyCentralImpulse(impulse.Bullet());
+ }
+
+ _ragdollMode = true;
+#endif
+}
+
+void Skeleton::EndRagdoll()
+{
+#if 0
+ VERUS_QREF_BULLET;
+
+ for (auto& kv : _mapBones)
+ {
+ RBone bone = kv.second;
+ if (bone._pConstraint)
+ {
+ bullet.GetWorld()->removeConstraint(bone._pConstraint);
+ delete bone._pConstraint;
+ bone._pConstraint = nullptr;
+ }
+ }
+
+ for (auto& kv : _mapBones)
+ {
+ RBone bone = kv.second;
+ if (bone._pBody)
+ {
+ bullet.GetWorld()->removeRigidBody(bone._pBody);
+ delete bone._pBody->getMotionState();
+ delete bone._pBody;
+ bone._pBody = nullptr;
+ }
+ if (bone._pShape)
+ {
+ delete bone._pShape;
+ bone._pShape = nullptr;
+ }
+ }
+
+ _ragdollMode = false;
+#endif
+}
+
+void Skeleton::BakeMotion(RMotion motion, int frame, bool kinect)
+{
+ for (const auto& kv : _mapBones)
+ {
+ RcBone bone = kv.second;
+ if (kinect && !IsKinectBone(_C(bone._name)))
+ continue;
+ PcBone pParent = FindBone(_C(bone._parentName));
+
+ Transform3 matParent, matFromParentSpace;
+ if (pParent)
+ {
+ matParent = pParent->_matFinal*pParent->_matFromBoneSpace;
+ matFromParentSpace = pParent->_matFromBoneSpace;
+ }
+ else
+ {
+ matParent = Transform3::identity();
+ matFromParentSpace = Transform3::identity();
+ }
+
+ Motion::PBone pMotionBone = motion.FindBone(_C(bone._name));
+ if (!pMotionBone)
+ pMotionBone = motion.InsertBone(_C(bone._name));
+
+ // Actor in parent actor's space is transformed to bind pose space
+ // and then to bone space:
+ const Transform3 mat =
+ bone._matToBoneSpace*matFromParentSpace*VMath::inverse(matParent)*
+ bone._matFinal*bone._matFromBoneSpace;
+
+ Quat q(mat.getUpper3x3());
+ Vector3 pos = mat.getTranslation();
+
+ if (glm::epsilonEqual(q.getX(), 0, 1e-4f)) q.setX(0);
+ if (glm::epsilonEqual(q.getY(), 0, 1e-4f)) q.setY(0);
+ if (glm::epsilonEqual(q.getZ(), 0, 1e-4f)) q.setZ(0);
+ if (glm::epsilonEqual(q.getW(), 1, 1e-4f)) q.setW(1);
+ if (glm::epsilonEqual(pos.getX(), 0, 1e-4f)) pos.setX(0);
+ if (glm::epsilonEqual(pos.getY(), 0, 1e-4f)) pos.setY(0);
+ if (glm::epsilonEqual(pos.getZ(), 0, 1e-4f)) pos.setZ(0);
+
+ if (!Math::IsNaN(q.getX()) &&
+ !Math::IsNaN(q.getY()) &&
+ !Math::IsNaN(q.getZ()) &&
+ !Math::IsNaN(q.getW()))
+ pMotionBone->InsertKeyframeRotation(frame, q);
+ if (!kinect)
+ pMotionBone->InsertKeyframePosition(frame, pos);
+ }
+
+ // Set values for non-kinect bones:
+ if (kinect)
+ {
+ Vector3 euler;
+ Quat q, qI(0);
+ Motion::PBone pSrcBone, pDstBone, pExtBone;
+
+ pSrcBone = motion.FindBone("ShoulderLeft");
+ pDstBone = motion.FindBone("ShoulderLeft0");
+ if (pSrcBone && pDstBone)
+ {
+ pSrcBone->FindKeyframeRotation(frame, euler, q);
+ pSrcBone->InsertKeyframeRotation(frame, VMath::slerp(0.8f, qI, q));
+ pDstBone->InsertKeyframeRotation(frame, VMath::slerp(0.2f, qI, q));
+ }
+
+ pSrcBone = motion.FindBone("ShoulderRight");
+ pDstBone = motion.FindBone("ShoulderRight0");
+ if (pSrcBone && pDstBone)
+ {
+ pSrcBone->FindKeyframeRotation(frame, euler, q);
+ pSrcBone->InsertKeyframeRotation(frame, VMath::slerp(0.8f, qI, q));
+ pDstBone->InsertKeyframeRotation(frame, VMath::slerp(0.2f, qI, q));
+ }
+
+ pSrcBone = motion.FindBone("Spine");
+ pDstBone = motion.FindBone("Spine1");
+ pExtBone = motion.FindBone("Spine2");
+ if (pSrcBone && pDstBone && pExtBone)
+ {
+ pSrcBone->FindKeyframeRotation(frame, euler, q);
+ pSrcBone->InsertKeyframeRotation(frame, VMath::slerp(0.6f, qI, q));
+ pDstBone->InsertKeyframeRotation(frame, VMath::slerp(0.2f, qI, q));
+ pExtBone->InsertKeyframeRotation(frame, VMath::slerp(0.2f, qI, q));
+ }
+ }
+}
+
+void Skeleton::AdaptBindPoseOf(RcSkeleton that)
+{
+ // TODO: improve.
+
+ const Point3 origin(0);
+ for (auto& kv : _mapBones)
+ {
+ RBone boneDst = kv.second;
+ PBone pParentDst = FindBone(_C(boneDst._parentName));
+ PcBone pBoneSrc = that.FindBone(_C(boneDst._name));
+ PcBone pParentSrc = pBoneSrc ? that.FindBone(_C(pBoneSrc->_parentName)) : nullptr;
+
+ if (boneDst._parentName != "Shoulder.L" &&
+ boneDst._parentName != "Shoulder.R")
+ continue;
+
+ if (pParentDst && pParentSrc)
+ {
+ const Point3 d0 = pParentDst->_matFromBoneSpace*origin;
+ const Point3 d1 = boneDst._matFromBoneSpace*origin;
+ const Point3 s0 = pParentSrc->_matFromBoneSpace*origin;
+ const Point3 s1 = pBoneSrc->_matFromBoneSpace*origin;
+
+ Vector3 vd = d1 - d0;
+ Vector3 vs = s1 - s0;
+ const float ld = VMath::length(vd);
+ const float ls = VMath::length(vs);
+ if (ld < 1e-6f || ls < 1e-6f)
+ continue;
+ vd /= ld;
+ vs /= ls;
+ const Matrix3 matWorldR = Matrix3::MakeRotateTo(vd, vs);
+
+ const Point3 offset = pParentDst->_matFromBoneSpace*origin;
+ const Transform3 matToBoneOrigin = Transform3::translation(-Vector3(offset));
+ const Transform3 matFromBoneOrigin = Transform3::translation(Vector3(offset));
+
+ pParentDst->_matAdapt = matFromBoneOrigin * Transform3(matWorldR, Vector3(0))*matToBoneOrigin;
+ }
+ }
+}
+
+void Skeleton::SimpleIK(CSZ boneDriven, CSZ boneDriver, RcVector3 dirDriverSpace, RcVector3 dirDesiredMeshSpace, float limitDot, float alpha)
+{
+ PBone pBoneDriven = FindBone(boneDriven);
+ Vector3 dirActual = dirDriverSpace;
+ if (boneDriver)
+ {
+ PBone pBoneDriver = FindBone(boneDriver);
+ dirActual = pBoneDriver->_matFinal.getUpper3x3()*pBoneDriver->_matFromBoneSpace.getUpper3x3()*dirDriverSpace;
+ }
+ Vector3 dirDesired = dirDesiredMeshSpace;
+ dirDesired.LimitDot(dirActual, limitDot);
+ const Matrix3 matR = Matrix3::MakeRotateTo(
+ dirActual,
+ VMath::normalizeApprox(VMath::lerp(alpha, dirActual, dirDesired)));
+ const Matrix3 matDrivenFrom = pBoneDriven->_matFinal.getUpper3x3()*pBoneDriven->_matFromBoneSpace.getUpper3x3();
+ const Matrix3 matDrivenTo = VMath::inverse(matDrivenFrom);
+ pBoneDriven->_matExternal.setUpper3x3(matDrivenTo*matR*matDrivenFrom);
+}
+
+void Skeleton::ProcessKinectData(const BYTE* p, RMotion motion, int frame)
+{
+ for (auto& kv : _mapBones)
+ {
+ kv.second._matFinal = Transform3::identity();
+ kv.second._matExternal = Transform3::identity();
+ }
+
+ UINT64 timestamp;
+ memcpy(×tamp, p, 8);
+
+ Vector3 skeletonPos;
+ float temp;
+ memcpy(&temp, p + 8, 4); skeletonPos.setX(temp);
+ memcpy(&temp, p + 12, 4); skeletonPos.setY(temp);
+ memcpy(&temp, p + 16, 4); skeletonPos.setZ(temp);
+
+ ProcessKinectJoint(p + 20, "AnkleLeft", skeletonPos);
+ ProcessKinectJoint(p + 36, "AnkleRight", skeletonPos);
+
+ ProcessKinectJoint(p + 52, "ElbowLeft", skeletonPos);
+ ProcessKinectJoint(p + 68, "ElbowRight", skeletonPos);
+
+ ProcessKinectJoint(p + 84, "FootLeft", skeletonPos);
+ ProcessKinectJoint(p + 100, "FootRight", skeletonPos);
+
+ ProcessKinectJoint(p + 116, "HandLeft", skeletonPos);
+ ProcessKinectJoint(p + 132, "HandRight", skeletonPos);
+
+ ProcessKinectJoint(p + 148, "Head", skeletonPos);
+
+ ProcessKinectJoint(p + 164, "HipCenter", skeletonPos);
+ ProcessKinectJoint(p + 180, "HipLeft", skeletonPos);
+ ProcessKinectJoint(p + 196, "HipRight", skeletonPos);
+
+ ProcessKinectJoint(p + 212, "KneeLeft", skeletonPos);
+ ProcessKinectJoint(p + 228, "KneeRight", skeletonPos);
+
+ ProcessKinectJoint(p + 244, "ShoulderCenter", skeletonPos);
+ ProcessKinectJoint(p + 260, "ShoulderLeft", skeletonPos);
+ ProcessKinectJoint(p + 276, "ShoulderRight", skeletonPos);
+
+ ProcessKinectJoint(p + 292, "Spine", skeletonPos);
+
+ ProcessKinectJoint(p + 308, "WristLeft", skeletonPos);
+ ProcessKinectJoint(p + 324, "WristRight", skeletonPos);
+
+ // Twist:
+ float hipTwist = 0;
+ PBone pBoneL = FindBone("HipLeft");
+ PBone pBoneR = FindBone("HipRight");
+ if (pBoneL && pBoneR)
+ {
+ const Vector3 hipAxis = VMath::normalize(
+ pBoneL->_matExternal.getTranslation() -
+ pBoneR->_matExternal.getTranslation());
+ const glm::vec3 a(1, 0, 0);
+ const glm::vec3 b = hipAxis.GLM();
+ const glm::vec3 r(0, 1, 0);
+ //hipTwist = glm::orientedAngle(a, b, r);
+ hipTwist *= 1 - abs(hipAxis.getY());
+ }
+ float shTwist = 0;
+ pBoneL = FindBone("ShoulderLeft");
+ pBoneR = FindBone("ShoulderRight");
+ if (pBoneL && pBoneR)
+ {
+ const Vector3 shAxis = VMath::normalize(
+ pBoneL->_matExternal.getTranslation() -
+ pBoneR->_matExternal.getTranslation());
+ const glm::vec3 a(1, 0, 0);
+ const glm::vec3 b = shAxis.GLM();
+ const glm::vec3 r(0, 1, 0);
+ //shTwist = glm::orientedAngle(a, b, r);
+ shTwist *= 1 - abs(shAxis.getY());
+ }
+
+ for (auto& kv : _mapBones)
+ {
+ RBone bone = kv.second;
+ if (!IsKinectBone(_C(bone._name)))
+ continue;
+#if 0
+ char xml[800];
+ sprintf_s(xml, "", _C(bone._name),
+ _C(bone._matExternal.GetTranslationPart().ToString3()));
+ VERUS_OUTPUT_DEBUG_STRING(xml);
+#endif
+ PBone pParent = FindBone(_C(bone._parentName));
+ while (pParent && !IsKinectBone(_C(pParent->_name)))
+ pParent = FindBone(_C(pParent->_parentName));
+ if (pParent)
+ {
+ const Vector3 bindPosePos = pParent->_matFromBoneSpace.getTranslation();
+ const Vector3 kinePosePos = pParent->_matExternal.getTranslation();
+ const Vector3 bindPoseDir = VMath::normalize(bone._matFromBoneSpace.getTranslation() - bindPosePos);
+ const Vector3 kinePoseDir = VMath::normalize(bone._matExternal.getTranslation() - kinePosePos);
+
+ Matrix3 matR3;
+ matR3.RotateTo(bindPoseDir, kinePoseDir);
+ const Transform3 matR(matR3, Vector3(0));
+ const Transform3 matT = Transform3::translation(kinePosePos - bindPosePos);
+ const Transform3 matOffset = Transform3::translation(kinePosePos);
+
+ // Twist:
+ const float twist = IsParentOf(_C(bone._parentName), "Spine") ? shTwist : hipTwist;
+ const float angle = kinePoseDir.getY()*twist;
+ const Transform3 matTwist = Transform3(Quat::rotation(angle, kinePoseDir), Vector3(0));
+
+ // Kinect's bind pose correction:
+ const Transform3 m = matTwist * matR*pParent->_matToActorSpace;
+
+ const bool secondary =
+ strstr(_C(bone._name), "Left") ||
+ strstr(_C(bone._name), "Right");
+ if (!secondary || pParent->_matFinal.IsIdentity())
+ pParent->_matFinal = matOffset * m*VMath::inverse(matOffset)*matT;
+ }
+ }
+
+ VERUS_FOR(i, 4)
+ {
+ for (auto& kv : _mapBones)
+ {
+ RBone bone = kv.second;
+ if (IsKinectBone(_C(bone._name)) && !IsKinectLeafBone(_C(bone._name)))
+ continue;
+ PBone pParent = FindBone(_C(bone._parentName));
+ if (pParent)
+ bone._matFinal = pParent->_matFinal;
+ else
+ bone._matFinal = Transform3::identity();
+ }
+ }
+
+ BakeMotion(motion, frame, true);
+
+ for (auto& kv : _mapBones)
+ {
+ kv.second._matFinal = Transform3::identity();
+ kv.second._matExternal = Transform3::identity();
+ }
+}
+
+void Skeleton::ProcessKinectJoint(const BYTE* p, CSZ name, RcVector3 skeletonPos)
+{
+ int id;
+ memcpy(&id, p, 4);
+
+ Vector3 pos;
+ float temp;
+ memcpy(&temp, p + 4, 4); pos.setX(temp);
+ memcpy(&temp, p + 8, 4); pos.setY(temp);
+ memcpy(&temp, p + 12, 4); pos.setZ(temp);
+ pos -= skeletonPos;
+ pos += Vector3(0, 1, 0);
+
+ pos = Transform3::rotationY(VERUS_PI)*pos;
+
+ PBone pBone = FindBone(name);
+ if (pBone)
+ pBone->_matExternal = Transform3::translation(pos);
+}
+
+void Skeleton::LoadKinectBindPose(CSZ xml)
+{
+#if 0
+ tinyxml2::XMLDocument doc;
+ doc.Parse(xml);
+ if (doc.Error())
+ return;
+
+ tinyxml2::XMLElement* pElem = nullptr;
+ tinyxml2::XMLElement* pRoot = doc.FirstChildElement();
+ if (!pRoot)
+ return;
+
+ Map mapData;
+ for (pElem = pRoot->FirstChildElement("p"); pElem; pElem = pElem->NextSiblingElement("p"))
+ {
+ Vector3 pos;
+ pos.FromString(pElem->Attribute("v"));
+ mapData[pElem->Attribute("n")] = pos;
+ }
+
+ for (auto& kv : _mapBones)
+ {
+ RBone bone = kv.second;
+ if (!IsKinectBone(_C(bone._name)))
+ continue;
+ PBone pParent = FindBone(_C(bone._parentName));
+ while (pParent && !IsKinectBone(_C(pParent->_name)))
+ pParent = FindBone(_C(pParent->_parentName));
+ if (pParent)
+ {
+ const Vector3 bindPosePos = pParent->_matFromBoneSpace.getTranslation();
+ const Vector3 kinePosePos = mapData[pParent->_name];
+ const Vector3 bindPoseDir = VMath::normalize(bone._matFromBoneSpace.getTranslation() - bindPosePos);
+ const Vector3 kinePoseDir = VMath::normalize(mapData[bone._name] - kinePosePos);
+
+ Matrix3 matR3;
+ matR3.RotateTo(bindPoseDir, kinePoseDir);
+ matR3 = VMath::inverse(matR3);
+
+ const bool secondary =
+ strstr(_C(bone._name), "Left") ||
+ strstr(_C(bone._name), "Right");
+ if (!secondary || pParent->_matToActorSpace.IsIdentity())
+ pParent->_matToActorSpace = Transform3(matR3, Vector3(0));
+ }
+ }
+#endif
+}
+
+bool Skeleton::IsKinectBone(CSZ name)
+{
+ const CSZ names[] =
+ {
+ "AnkleLeft",
+ "AnkleRight",
+ "ElbowLeft",
+ "ElbowRight",
+ "FootLeft",
+ "FootRight",
+ "HandLeft",
+ "HandRight",
+ "Head",
+ "HipCenter",
+ "HipLeft",
+ "HipRight",
+ "KneeLeft",
+ "KneeRight",
+ "ShoulderCenter",
+ "ShoulderLeft",
+ "ShoulderRight",
+ "Spine",
+ "WristLeft",
+ "WristRight"
+ };
+ return std::binary_search(names, names + VERUS_ARRAY_LENGTH(names),
+ name, [](CSZ a, CSZ b) {return strcmp(a, b) < 0; });
+}
+
+bool Skeleton::IsKinectLeafBone(CSZ name)
+{
+ const CSZ names[] =
+ {
+ "FootLeft",
+ "FootRight",
+ "Head",
+ "WristLeft",
+ "WristRight"
+ };
+ return std::binary_search(names, names + VERUS_ARRAY_LENGTH(names),
+ name, [](CSZ a, CSZ b) {return strcmp(a, b) < 0; });
+}
+
+void Skeleton::FixateFeet(RMotion motion)
+{
+ enum class Foot : int
+ {
+ none,
+ left,
+ right
+ } foot = Foot::none;
+
+ Point3 posFixed, posBase, posHeadPrev;
+ int frameEvent = 0;
+
+ PBone pAnkleL = FindBone("AnkleLeft");
+ PBone pAnkleR = FindBone("AnkleRight");
+ PBone pHead = FindBone("Head");
+
+ if (!pAnkleL || !pAnkleR)
+ return;
+
+ Vector vPos;
+ vPos.resize(motion.GetNumFrames());
+ VERUS_FOR(i, motion.GetNumFrames())
+ {
+ ApplyMotion(motion, i*motion.GetFpsInv());
+
+ const Point3 posAnkleL = (pAnkleL->_matFinal*pAnkleL->_matFromBoneSpace).getTranslation();
+ const Point3 posAnkleR = (pAnkleR->_matFinal*pAnkleR->_matFromBoneSpace).getTranslation();
+
+ const Foot target = (posAnkleL.getY() < posAnkleR.getY()) ? Foot::left : Foot::right;
+ Point3 pos;
+ if (foot != target && (i - frameEvent > 5 || !i))
+ {
+ frameEvent = i;
+ foot = target;
+ posFixed = (foot == Foot::left) ? posAnkleL : posAnkleR;
+ if (i)
+ posFixed += Vector3(vPos[i - 1]);
+ posFixed.setY(0.05f);
+ }
+ pos = (foot == Foot::left) ? posAnkleL : posAnkleR;
+
+ vPos[i] = posFixed - pos;
+
+ // Stabilize head position:
+ const Point3 posHead = (pHead->_matFinal*pHead->_matFromBoneSpace).getTranslation() + vPos[i];
+ if (i)
+ {
+ Vector3 d = posHeadPrev - posHead;
+ d = VMath::mulPerElem(d, Vector3(0.5f, 0, 0.5f));
+ vPos[i] += d;
+ }
+ posHeadPrev = posHead;
+ }
+
+ // Add significant keyframes:
+ const float e = 0.01f;
+ Motion::PBone pRoot = motion.InsertBone(RootName());
+ VERUS_FOR(i, motion.GetNumFrames())
+ {
+ const Point3 p = vPos[i];
+ const bool same = p.IsEqual(posBase, e);
+ if (!same)
+ {
+ posBase = p;
+ pRoot->InsertKeyframePosition(i, p);
+ }
+ }
+}
+
+Vector3 Skeleton::GetHighestSpeed(RMotion motion, CSZ name, RcVector3 scale, bool positive)
+{
+ const int numFrames = motion.GetNumFrames();
+ const float dt = motion.GetFpsInv();
+ const float dti = float(motion.GetFps());
+
+ Vector vDX;
+ Vector vDY;
+ Vector vDZ;
+ vDX.reserve(numFrames);
+ vDY.reserve(numFrames);
+ vDZ.reserve(numFrames);
+
+ Point3 prevPos(0);
+ VERUS_FOR(i, numFrames)
+ {
+ ApplyMotion(motion, i*dt);
+ PBone pBone = FindBone(name);
+ Point3 pos = VMath::mulPerElem(pBone->_matFromBoneSpace.getTranslation(), scale);
+ pos = pBone->_matFinal*pos;
+ if (i)
+ {
+ vDX.push_back((pos.getX() - prevPos.getX())*dti);
+ vDY.push_back((pos.getY() - prevPos.getY())*dti);
+ vDZ.push_back((pos.getZ() - prevPos.getZ())*dti);
+ }
+ prevPos = pos;
+ }
+ if (positive)
+ {
+ std::sort(vDX.begin(), vDX.end(), std::greater());
+ std::sort(vDY.begin(), vDY.end(), std::greater());
+ std::sort(vDZ.begin(), vDZ.end(), std::greater());
+ }
+ else
+ {
+ std::sort(vDX.begin(), vDX.end());
+ std::sort(vDY.begin(), vDY.end());
+ std::sort(vDZ.begin(), vDZ.end());
+ }
+
+ const int offA = motion.GetNumFrames() / 32;
+ const int offB = motion.GetNumFrames() / 24;
+ const int offC = motion.GetNumFrames() / 16;
+ const int offD = motion.GetNumFrames() / 8;
+ return Vector3(
+ 0.25f*(vDX[offA] + vDX[offB] + vDX[offC] + vDX[offD]),
+ 0.25f*(vDY[offA] + vDY[offB] + vDY[offC] + vDY[offD]),
+ 0.25f*(vDZ[offA] + vDZ[offB] + vDZ[offC] + vDZ[offD]));
+}
diff --git a/Verus/src/Anim/Skeleton.h b/Verus/src/Anim/Skeleton.h
new file mode 100644
index 0000000..8db7390
--- /dev/null
+++ b/Verus/src/Anim/Skeleton.h
@@ -0,0 +1,153 @@
+#pragma once
+
+namespace verus
+{
+ namespace Anim
+ {
+ //! Standard skeleton, which can be animated using motion object (Motion).
+
+ //! X3D format can support up to 256 bones, but the hardware has certain
+ //! limitations. With Shader Model 2.0 the shader can hold about 32 bone
+ //! matrices. Other bones must be remapped using Primary Bones concept.
+ //!
+ //! Alpha Motions (AlphaMotion) add the ability to mix multiple motions,
+ //! just like images are mixed with alpha channel. Alpha Motion affects
+ //! it's root bone and all descendants of that bone.
+ //!
+ //! A ragdoll can be created automatically or configured using XML .rig file.
+ //! Ragdoll's simulation can start from any motion frame and any simulated pose
+ //! can be baked back into motion object, allowing smooth transition to ragdoll
+ //! simulation and back to motion.
+ //!
+ class Skeleton : public Object
+ {
+ public:
+ struct AlphaMotion
+ {
+ PMotion _pMotion = nullptr;
+ CSZ _rootBone = nullptr;
+ float _alpha = 0;
+ float _time = 0;
+ };
+ VERUS_TYPEDEFS(AlphaMotion);
+
+ struct Bone : public AllocatorAware
+ {
+ Transform3 _matToBoneSpace = Transform3::identity();
+ Transform3 _matFromBoneSpace = Transform3::identity();
+ Transform3 _matFinal = Transform3::identity();
+ Transform3 _matFinalInv = Transform3::identity();
+ Transform3 _matExternal = Transform3::identity();
+ Transform3 _matAdapt = Transform3::identity();
+ Transform3 _matToActorSpace = Transform3::identity();
+ Vector3 _rigRot = Vector3(0); //!< Rotation angles of ragdoll's rigid component.
+ Vector3 _cRot = Vector3(0); //!< Constraint's rotation angles.
+ Vector3 _cLimits = Vector3(0); //!< Constraint's limits.
+ Vector3 _boxSize = Vector3(0); //!< For a box shape.
+ String _name;
+ String _parentName;
+ btCollisionShape* _pShape = nullptr;
+ btRigidBody* _pBody = nullptr;
+ btTypedConstraint* _pConstraint = nullptr;
+ float _width = 0;
+ float _length = 0;
+ float _mass = 0;
+ int _shaderIndex = 0; //!< Index of a matrix in the vertex shader.
+ bool _ready = false;
+ bool _rigBone = true;
+ bool _hinge = false; //!< btHingeConstraint vs btConeTwistConstraint.
+ bool _noCollision = false;
+ };
+ VERUS_TYPEDEFS(Bone);
+
+ private:
+ typedef Map TMapBones;
+ typedef Map TMapPrimary;
+
+ Transform3 _matParents = Transform3::identity();
+ Transform3 _matRagdollToWorld = Transform3::identity();
+ Transform3 _matRagdollToWorldInv = Transform3::identity();
+ TMapBones _mapBones;
+ TMapPrimary _mapPrimary;
+ PBone _pCurrentBone = nullptr;
+ PMotion _pCurrentMotion = nullptr;
+ PAlphaMotion _pAlphaMotions = nullptr;
+ float _currentTime = 0;
+ float _mass = 0;
+ int _numPrimaryBones = 0;
+ int _numAlphaMotions = 0;
+ bool _ragdollMode = false;
+
+ public:
+ Skeleton();
+ ~Skeleton();
+
+ void operator=(const Skeleton& that);
+
+ void Init();
+ void Done();
+
+ void Draw(bool bindPose = true, int selected = -1);
+
+ static CSZ RootName() { return "$ROOT"; }
+
+ PBone InsertBone(RBone bone);
+ PBone FindBone(CSZ name);
+ PcBone FindBone(CSZ name) const;
+ //! Uses shader's array index to find a bone.
+ PBone FindBoneByIndex(int index);
+
+ //! Sets the current pose using motion object (Motion).
+ void ApplyMotion(RMotion motion, float time,
+ int numAlphaMotions = 0, PAlphaMotion pAlphaMotions = nullptr);
+
+ //! Fills the array of matrices that will be used by a shader.
+ void FillMatrixArray(mataff* p) const;
+
+ void ResetFinalPose();
+
+ VERUS_P(void ResetBones());
+ VERUS_P(void RecursiveBoneUpdate());
+
+ int GetNumBones() const { return _numPrimaryBones ? _numPrimaryBones : Utils::Cast32(_mapBones.size()); }
+
+ void VisitBones(std::function fn);
+ void VisitBones(std::function fn) const;
+
+ //! Adds skeleton's bones to motion object (Motion).
+ void InsertBonesIntoMotion(RMotion motion) const;
+ //! Removes motion's bones, which are not skeleton's bones.
+ void DeleteOutsiders(RMotion motion) const;
+
+ void AdjustPrimaryBones(const Vector& vPrimaryBones);
+ int RemapBoneIndex(int index) const;
+
+ bool IsParentOf(CSZ bone, CSZ parent) const;
+
+ void LoadRigInfo(CSZ url);
+ void LoadRigInfoFromPtr(const BYTE* p);
+ void BeginRagdoll(RcTransform3 matW, RcVector3 impulse = Vector3(0), CSZ bone = "Spine2");
+ void EndRagdoll();
+ bool IsRagdollMode() const { return _ragdollMode; }
+ RcTransform3 GetRagdollToWorldMatrix() { return _matRagdollToWorld; }
+
+ //! Saves the current pose into motion object (Motion) at some frame.
+ void BakeMotion(RMotion motion, int frame = 0, bool kinect = false);
+ void AdaptBindPoseOf(const Skeleton& that);
+ void SimpleIK(CSZ boneDriven, CSZ boneDriver, RcVector3 dirDriverSpace, RcVector3 dirDesiredMeshSpace, float limitDot, float alpha);
+
+ void ProcessKinectData(const BYTE* p, RMotion motion, int frame = 0);
+ void ProcessKinectJoint(const BYTE* p, CSZ name, RcVector3 skeletonPos);
+ void LoadKinectBindPose(CSZ xml);
+ static bool IsKinectBone(CSZ name);
+ static bool IsKinectLeafBone(CSZ name);
+
+ void FixateFeet(RMotion motion);
+
+ //! Tries to compute the highest speed at which a bone would move with this motion applied.
+ //! Can be used to sync animation and movement to fix the sliding feet problem.
+ Vector3 GetHighestSpeed(RMotion motion, CSZ name, RcVector3 scale = Vector3(1, 0, 1), bool positive = false);
+ };
+ VERUS_TYPEDEFS(Skeleton);
+ }
+}
diff --git a/Verus/src/App/App.cpp b/Verus/src/App/App.cpp
new file mode 100644
index 0000000..05879c9
--- /dev/null
+++ b/Verus/src/App/App.cpp
@@ -0,0 +1,52 @@
+#include "verus.h"
+
+namespace verus
+{
+ void Make_App()
+ {
+ }
+ void Free_App()
+ {
+ }
+}
+
+using namespace verus;
+
+void App::RunEventLoop()
+{
+ SDL_Event event = {};
+ bool done = false;
+
+ do
+ {
+ while (SDL_PollEvent(&event))
+ {
+ switch (event.type)
+ {
+ case SDL_QUIT:
+ {
+ done = true;
+ }
+ break;
+ case SDL_WINDOWEVENT:
+ {
+ switch (event.window.event)
+ {
+ case SDL_WINDOWEVENT_MINIMIZED:
+ break;
+ case SDL_WINDOWEVENT_RESTORED:
+ break;
+ }
+ }
+ break;
+ case SDL_USEREVENT:
+ {
+ }
+ break;
+ }
+ }
+
+ if (done)
+ break;
+ } while (!done);
+}
diff --git a/Verus/src/App/App.h b/Verus/src/App/App.h
new file mode 100644
index 0000000..88fc6a2
--- /dev/null
+++ b/Verus/src/App/App.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "Settings.h"
+#include "Window.h"
+
+namespace verus
+{
+ void Make_App();
+ void Free_App();
+}
+
+namespace verus
+{
+ namespace App
+ {
+ void RunEventLoop();
+ }
+}
diff --git a/Verus/src/App/Settings.cpp b/Verus/src/App/Settings.cpp
new file mode 100644
index 0000000..10749d3
--- /dev/null
+++ b/Verus/src/App/Settings.cpp
@@ -0,0 +1,272 @@
+#include "verus.h"
+
+using namespace verus;
+using namespace verus::App;
+
+Settings::Settings()
+{
+ VERUS_ZERO_MEM(_commandLine);
+#ifdef _WIN32
+ if (0x419 == GetUserDefaultUILanguage())
+ _uiLang = "RU";
+#endif
+ SetQuality(Quality::medium);
+}
+
+Settings::~Settings()
+{
+}
+
+void Settings::LoadValidateSave()
+{
+ Load();
+ Validate();
+ Save();
+}
+
+void Settings::Load()
+{
+ Json::Load();
+
+ _quality = static_cast(GetI("quality", +_quality));
+ _direct3D = GetI("direct3D", _direct3D);
+ _gpuAnisotropyLevel = GetI("gpuAnisotropyLevel", _gpuAnisotropyLevel);
+ _gpuAntialiasingLevel = GetI("gpuAntialiasingLevel", _gpuAntialiasingLevel);
+ _gpuDepthTexture = GetI("gpuDepthTexture", _gpuDepthTexture);
+ _gpuForcedProfile = GetS("gpuForcedProfile", _C(_gpuForcedProfile));
+ _gpuOffscreenRender = GetB("gpuOffscreenRender", _gpuOffscreenRender);
+ _gpuParallaxMapping = GetB("gpuParallaxMapping", _gpuParallaxMapping);
+ _gpuPerPixelLighting = GetB("gpuPerPixelLighting", _gpuPerPixelLighting);
+ _gpuTextureLodLevel = GetI("gpuTextureLodLevel", _gpuTextureLodLevel);
+ _gpuTrilinearFilter = GetB("gpuTrilinearFilter", _gpuTrilinearFilter);
+ _inputMouseSensitivity = GetF("inputMouseSensitivity", _inputMouseSensitivity);
+ _postProcessBloom = GetB("postProcessBloom", _postProcessBloom);
+ _postProcessCinema = GetB("postProcessCinema", _postProcessCinema);
+ _postProcessMotionBlur = GetB("postProcessMotionBlur", _postProcessMotionBlur);
+ _postProcessSSAO = GetB("postProcessSSAO", _postProcessSSAO);
+ _sceneGrassDensity = GetI("sceneGrassDensity", _sceneGrassDensity);
+ _sceneShadowQuality = static_cast(GetI("sceneShadowQuality", +_sceneShadowQuality));
+ _sceneWaterQuality = static_cast(GetI("sceneWaterQuality", +_sceneWaterQuality));
+ _screenFOV = GetF("screenFOV", _screenFOV);
+ _screenSizeHeight = GetI("screenSizeHeight", _screenSizeHeight);
+ _screenSizeWidth = GetI("screenSizeWidth", _screenSizeWidth);
+ _screenVSync = GetB("screenVSync", _screenVSync);
+ _screenWindowed = GetB("screenWindowed", _screenWindowed);
+ _uiLang = GetS("uiLang", _C(_uiLang));
+}
+
+void Settings::Save()
+{
+ Set("quality", +_quality);
+ Set("direct3D", _direct3D);
+ Set("gpuAnisotropyLevel", _gpuAnisotropyLevel);
+ Set("gpuAntialiasingLevel", _gpuAntialiasingLevel);
+ Set("gpuDepthTexture", _gpuDepthTexture);
+ Set("gpuForcedProfile", _C(_gpuForcedProfile));
+ Set("gpuOffscreenRender", _gpuOffscreenRender);
+ Set("gpuParallaxMapping", _gpuParallaxMapping);
+ Set("gpuPerPixelLighting", _gpuPerPixelLighting);
+ Set("gpuTextureLodLevel", _gpuTextureLodLevel);
+ Set("gpuTrilinearFilter", _gpuTrilinearFilter);
+ Set("inputMouseSensitivity", _inputMouseSensitivity);
+ Set("postProcessBloom", _postProcessBloom);
+ Set("postProcessCinema", _postProcessCinema);
+ Set("postProcessMotionBlur", _postProcessMotionBlur);
+ Set("postProcessSSAO", _postProcessSSAO);
+ Set("sceneGrassDensity", _sceneGrassDensity);
+ Set("sceneShadowQuality", +_sceneShadowQuality);
+ Set("sceneWaterQuality", +_sceneWaterQuality);
+ Set("screenFOV", _screenFOV);
+ Set("screenSizeHeight", _screenSizeHeight);
+ Set("screenSizeWidth", _screenSizeWidth);
+ Set("screenVSync", _screenVSync);
+ Set("screenWindowed", _screenWindowed);
+ Set("uiLang", _C(_uiLang));
+
+ Json::Save();
+}
+
+void Settings::Validate()
+{
+ _quality = Math::Clamp(_quality, Quality::low, Quality::ultra);
+ _gpuAnisotropyLevel = Math::Clamp(_gpuAnisotropyLevel, 0, 16);
+ _gpuAntialiasingLevel = Math::Clamp(_gpuAntialiasingLevel, 0, 16);
+ _gpuTextureLodLevel = Math::Clamp(_gpuTextureLodLevel, 0, 4);
+ _screenFOV = Math::Clamp(_screenFOV, 60, 90);
+ _screenSizeHeight = Math::Clamp(_screenSizeHeight, 270, 0x2000);
+ _screenSizeWidth = Math::Clamp(_screenSizeWidth, 480, 0x2000);
+
+ if (_gpuForcedProfile == "sm_2_0")
+ {
+ _postProcessMotionBlur = false;
+ _postProcessSSAO = false;
+ _sceneShadowQuality = Math::Min(_sceneShadowQuality, ShadowQuality::filtered);
+ }
+
+ if (!_gpuPerPixelLighting)
+ {
+ _gpuParallaxMapping = false;
+ _sceneWaterQuality = Math::Clamp(_sceneWaterQuality, WaterQuality::solidColor, WaterQuality::distortedReflection);
+ }
+ _postProcessBloom = _postProcessBloom && _gpuOffscreenRender;
+ _sceneGrassDensity = Math::Clamp(_sceneGrassDensity, 15, 1500);
+ _sceneShadowQuality = Math::Clamp(_sceneShadowQuality, ShadowQuality::none, ShadowQuality::cascaded);
+ _sceneWaterQuality = Math::Clamp(_sceneWaterQuality, WaterQuality::solidColor, WaterQuality::trueWavesRefraction);
+
+ if (_sceneShadowQuality >= ShadowQuality::cascaded)
+ _gpuDepthTexture = 1;
+
+ if (_uiLang != "RU" && _uiLang != "EN")
+ _uiLang = "EN";
+}
+
+void Settings::ParseCommandLineArgs(int argc, wchar_t* argv[])
+{
+ Vector v;
+ VERUS_FOR(i, argc)
+ v.push_back(Str::WideToUtf8(argv[i]));
+
+ Vector vArgv;
+ VERUS_FOR(i, argc)
+ vArgv.push_back(const_cast(_C(v[i])));
+
+ ParseCommandLineArgs(argc, vArgv.data());
+}
+
+void Settings::ParseCommandLineArgs(int argc, char* argv[])
+{
+ auto IsArg = [&argv](int i, CSZ arg)
+ {
+ return !strcmp(argv[i], arg);
+ };
+
+ VERUS_FOR(i, argc)
+ {
+ _commandLine.normalMapOnlyLS = _commandLine.normalMapOnlyLS || IsArg(i, "--normal-map-only-ls");
+ _commandLine.physicsDebug = _commandLine.physicsDebug || IsArg(i, "--physics-debug");
+ _commandLine.physicsDebugFull = _commandLine.physicsDebugFull || IsArg(i, "--physics-debug-full");
+ _commandLine.profiler = _commandLine.profiler || IsArg(i, "--profiler");
+ _commandLine.testTexToPix = _commandLine.testTexToPix || IsArg(i, "--test-tex-to-pix");
+ }
+
+ VERUS_QREF_UTILS;
+ //utils.SetupPathsEx(); // Only now we can finally do this.
+
+ SetFilename("Settings.json");
+ const String pathName = _C(GetFilename());
+
+ VERUS_FOR(i, argc)
+ {
+ if (IsArg(i, "--q-low"))
+ {
+ const bool ret = IO::FileSystem::Delete(_C(pathName));
+ SetQuality(Quality::low);
+ }
+ if (IsArg(i, "--q-medium"))
+ {
+ const bool ret = IO::FileSystem::Delete(_C(pathName));
+ SetQuality(Quality::medium);
+ }
+ if (IsArg(i, "--q-high"))
+ {
+ const bool ret = IO::FileSystem::Delete(_C(pathName));
+ SetQuality(Quality::high);
+ }
+ if (IsArg(i, "--q-ultra"))
+ {
+ const bool ret = IO::FileSystem::Delete(_C(pathName));
+ SetQuality(Quality::ultra);
+ }
+ }
+}
+
+void Settings::SetQuality(Quality q)
+{
+ _quality = q;
+ _direct3D = 9;
+ _gpuAnisotropyLevel = 0;
+ _gpuAntialiasingLevel = 0;
+ _gpuDepthTexture = 0;
+ _gpuForcedProfile = "";
+ _gpuOffscreenRender = true;
+ _gpuParallaxMapping = false;
+ _gpuPerPixelLighting = true;
+ _gpuTextureLodLevel = 0;
+ _gpuTrilinearFilter = false;
+ _postProcessBloom = false;
+ _postProcessCinema = false;
+ _postProcessMotionBlur = false;
+ _postProcessSSAO = false;
+ _sceneGrassDensity = 1000;
+ _sceneShadowQuality = ShadowQuality::sharp;
+ _sceneWaterQuality = WaterQuality::distortedReflection;
+ _screenVSync = false;
+ _screenWindowed = true;
+
+ switch (q)
+ {
+ case Quality::low:
+ _gpuDepthTexture = -1;
+ _gpuForcedProfile = "sm_3_0";
+ _sceneGrassDensity = 500;
+ _sceneShadowQuality = ShadowQuality::none;
+ _sceneWaterQuality = WaterQuality::solidColor;
+ _screenSizeHeight = 600;
+ _screenSizeWidth = 800;
+ break;
+ case Quality::medium:
+ _gpuAnisotropyLevel = 4;
+ _gpuForcedProfile = "sm_3_0";
+ _screenSizeHeight = 768;
+ _screenSizeWidth = 1024;
+ break;
+ case Quality::high:
+ _direct3D = 11;
+ _gpuAnisotropyLevel = 8;
+ _gpuTrilinearFilter = true;
+ _postProcessBloom = true;
+ _postProcessCinema = true;
+ _postProcessSSAO = true;
+ _sceneGrassDensity = 1500;
+ _sceneShadowQuality = ShadowQuality::filtered;
+ _sceneWaterQuality = WaterQuality::trueWavesReflection;
+ _screenSizeHeight = 720;
+ _screenSizeWidth = 1280;
+ break;
+ case Quality::ultra:
+ _direct3D = 11;
+ _gpuAnisotropyLevel = 16;
+ _gpuDepthTexture = 1;
+ _gpuTrilinearFilter = true;
+ _postProcessBloom = true;
+ _postProcessCinema = true;
+ _postProcessMotionBlur = true;
+ _postProcessSSAO = true;
+ _sceneGrassDensity = 1500;
+ _sceneShadowQuality = ShadowQuality::cascaded;
+ _sceneWaterQuality = WaterQuality::trueWavesRefraction;
+ _screenSizeHeight = 1080;
+ _screenSizeWidth = 1980;
+ break;
+ }
+
+#ifdef _WIN32
+# ifndef _DEBUG
+ _screenSizeHeight = GetSystemMetrics(SM_CYSCREEN);
+ _screenSizeWidth = GetSystemMetrics(SM_CXSCREEN);
+ _screenWindowed = false;
+# endif
+#else // Linux?
+ _direct3D = 0;
+ _gpuForcedProfile = "arb1";
+#endif
+}
+
+void Settings::MatchScreen()
+{
+ SDL_DisplayMode dm;
+ if (SDL_GetCurrentDisplayMode(0, &dm) < 0)
+ throw VERUS_RUNTIME_ERROR << "SDL_GetCurrentDisplayMode(), " << SDL_GetError();
+ _screenSizeWidth = dm.w;
+ _screenSizeHeight = dm.h;
+}
diff --git a/Verus/src/App/Settings.h b/Verus/src/App/Settings.h
new file mode 100644
index 0000000..0c2638b
--- /dev/null
+++ b/Verus/src/App/Settings.h
@@ -0,0 +1,86 @@
+#pragma once
+
+namespace verus
+{
+ namespace App
+ {
+ class Settings : public Singleton, public IO::Json
+ {
+ public:
+ enum class Quality : int
+ {
+ low,
+ medium,
+ high,
+ ultra
+ };
+
+ enum class ShadowQuality : int
+ {
+ none,
+ sharp,
+ filtered,
+ cascaded
+ };
+
+ enum class WaterQuality : int
+ {
+ solidColor,
+ simpleReflection,
+ distortedReflection,
+ trueWavesReflection,
+ trueWavesRefraction
+ };
+
+ Quality _quality = Quality::medium;
+ int _direct3D = 9;
+ int _gpuAnisotropyLevel = 4;
+ int _gpuAntialiasingLevel = 0;
+ int _gpuDepthTexture = 0;
+ String _gpuForcedProfile = "sm_3_0";
+ bool _gpuOffscreenRender = false;
+ bool _gpuParallaxMapping = false;
+ bool _gpuPerPixelLighting = true;
+ int _gpuTextureLodLevel = 0;
+ bool _gpuTrilinearFilter = false;
+ float _inputMouseSensitivity = 1;
+ bool _postProcessBloom = false;
+ bool _postProcessCinema = false;
+ bool _postProcessMotionBlur = false;
+ bool _postProcessSSAO = false;
+ int _sceneGrassDensity = 1000;
+ ShadowQuality _sceneShadowQuality = ShadowQuality::sharp;
+ WaterQuality _sceneWaterQuality = WaterQuality::solidColor;
+ float _screenFOV = 70;
+ int _screenSizeHeight = 720;
+ int _screenSizeWidth = 1280;
+ bool _screenVSync = false;
+ bool _screenWindowed = true;
+ String _uiLang = "EN";
+ struct
+ {
+ bool normalMapOnlyLS;
+ bool physicsDebug;
+ bool physicsDebugFull;
+ bool profiler;
+ bool testTexToPix;
+ } _commandLine;
+
+ Settings();
+ ~Settings();
+
+ void LoadValidateSave();
+ void Load();
+ void Save();
+ void Validate();
+
+ void ParseCommandLineArgs(int argc, wchar_t* argv[]);
+ void ParseCommandLineArgs(int argc, char* argv[]);
+
+ void SetQuality(Quality q);
+
+ void MatchScreen();
+ };
+ VERUS_TYPEDEFS(Settings);
+ }
+}
diff --git a/Verus/src/App/Window.cpp b/Verus/src/App/Window.cpp
new file mode 100644
index 0000000..f1f06e5
--- /dev/null
+++ b/Verus/src/App/Window.cpp
@@ -0,0 +1,58 @@
+#include "verus.h"
+
+using namespace verus;
+using namespace verus::App;
+
+// Window::Desc:
+
+void Window::Desc::ApplySettings()
+{
+ VERUS_QREF_SETTINGS;
+ _width = settings._screenSizeWidth;
+ _height = settings._screenSizeHeight;
+ _fullscreen = !settings._screenWindowed;
+}
+
+// Window:
+
+Window::Window()
+{
+}
+
+Window::~Window()
+{
+ Done();
+}
+
+void Window::Init(RcDesc descConst)
+{
+ VERUS_INIT();
+
+ Desc desc = descConst;
+ if (desc._useSettings)
+ desc.ApplySettings();
+
+ Uint32 flags = 0;
+ if (desc._fullscreen)
+ flags |= SDL_WINDOW_FULLSCREEN;
+
+ _pWnd = SDL_CreateWindow(
+ desc._title ? desc._title : "",
+ SDL_WINDOWPOS_CENTERED,
+ SDL_WINDOWPOS_CENTERED,
+ desc._width,
+ desc._height,
+ flags);
+ if (!_pWnd)
+ throw VERUS_RECOVERABLE << "SDL_CreateWindow(), " << SDL_GetError();
+}
+
+void Window::Done()
+{
+ if (_pWnd)
+ {
+ SDL_DestroyWindow(_pWnd);
+ _pWnd = nullptr;
+ }
+ VERUS_DONE(Window);
+}
diff --git a/Verus/src/App/Window.h b/Verus/src/App/Window.h
new file mode 100644
index 0000000..c04c786
--- /dev/null
+++ b/Verus/src/App/Window.h
@@ -0,0 +1,31 @@
+#pragma once
+
+namespace verus
+{
+ namespace App
+ {
+ class Window : public Object
+ {
+ SDL_Window* _pWnd = nullptr;
+
+ public:
+ struct Desc
+ {
+ CSZ _title = nullptr;
+ int _width = 0;
+ int _height = 0;
+ bool _fullscreen = false;
+ bool _useSettings = true;
+
+ void ApplySettings();
+ };
+ VERUS_TYPEDEFS(Desc);
+
+ Window();
+ ~Window();
+
+ void Init(RcDesc desc = Desc());
+ void Done();
+ };
+ }
+}
diff --git a/Verus/src/Audio/Audio.cpp b/Verus/src/Audio/Audio.cpp
new file mode 100644
index 0000000..3da5143
--- /dev/null
+++ b/Verus/src/Audio/Audio.cpp
@@ -0,0 +1,13 @@
+#include "verus.h"
+
+namespace verus
+{
+ void Make_Audio()
+ {
+ Audio::AudioSystem::Make();
+ }
+ void Free_Audio()
+ {
+ Audio::AudioSystem::Free();
+ }
+}
diff --git a/Verus/src/Audio/Audio.h b/Verus/src/Audio/Audio.h
new file mode 100644
index 0000000..43b3f80
--- /dev/null
+++ b/Verus/src/Audio/Audio.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "OggCallbacks.h"
+#include "Source.h"
+#include "Sound.h"
+#include "StreamPlayer.h"
+#include "AudioSystem.h"
+
+namespace verus
+{
+ void Make_Audio();
+ void Free_Audio();
+}
diff --git a/Verus/src/Audio/AudioSystem.cpp b/Verus/src/Audio/AudioSystem.cpp
new file mode 100644
index 0000000..382e0b7
--- /dev/null
+++ b/Verus/src/Audio/AudioSystem.cpp
@@ -0,0 +1,142 @@
+#include "verus.h"
+
+using namespace verus;
+using namespace verus::Audio;
+
+AudioSystem::AudioSystem()
+{
+}
+
+AudioSystem::~AudioSystem()
+{
+ Done();
+}
+
+void AudioSystem::Init()
+{
+ VERUS_INIT();
+
+ const ALCchar* deviceName = nullptr;
+#ifdef _WIN32
+ deviceName = "DirectSound3D";
+#endif
+
+ if (!(_pDevice = alcOpenDevice(deviceName)))
+ throw VERUS_RUNTIME_ERROR << "alcOpenDevice()";
+
+ if (!(_pContext = alcCreateContext(_pDevice, nullptr)))
+ throw VERUS_RUNTIME_ERROR << "alcCreateContext(), " << alcGetError(_pDevice);
+
+ if (!alcMakeContextCurrent(_pContext))
+ throw VERUS_RUNTIME_ERROR << "alcMakeContextCurrent(), " << alcGetError(_pDevice);
+
+ int vMajor = 0, vMinor = 0;
+ alcGetIntegerv(_pDevice, ALC_MAJOR_VERSION, sizeof(vMajor), &vMajor);
+ alcGetIntegerv(_pDevice, ALC_MINOR_VERSION, sizeof(vMinor), &vMinor);
+ StringStream ss;
+ ss << vMajor << "." << vMinor;
+ _version = ss.str();
+
+ _deviceSpecifier = alcGetString(_pDevice, ALC_DEVICE_SPECIFIER);
+}
+
+void AudioSystem::Done()
+{
+ DeleteAllStreams();
+ DeleteAllSounds();
+
+ alcMakeContextCurrent(0);
+ if (_pContext)
+ {
+ alcDestroyContext(_pContext);
+ _pContext = nullptr;
+ }
+ if (_pDevice)
+ {
+ alcCloseDevice(_pDevice);
+ _pDevice = nullptr;
+ }
+
+ VERUS_DONE(AudioSystem);
+}
+
+void AudioSystem::Update()
+{
+ if (!IsInitialized())
+ return;
+ VERUS_UPDATE_ONCE_CHECK;
+
+ VERUS_QREF_TIMER;
+
+ VERUS_FOR(i, VERUS_ARRAY_LENGTH(_streamPlayers))
+ _streamPlayers[i].Update();
+
+ // Update every frame:
+ for (auto& x : TStoreSounds::_map)
+ x.second.Update();
+
+ // Update ~15 times per second:
+ if (timer.IsEventEvery(67))
+ {
+ for (auto& x : TStoreSounds::_map)
+ x.second.UpdateHRTF();
+
+ if (VMath::lengthSqr(_listenerDirection) > 0.1f &&
+ VMath::lengthSqr(_listenerUp) > 0.1f)
+ {
+ const float orien[6] =
+ {
+ _listenerDirection.getX(),
+ _listenerDirection.getY(),
+ _listenerDirection.getZ(),
+ _listenerUp.getX(),
+ _listenerUp.getY(),
+ _listenerUp.getZ()
+ };
+ alListenerfv(AL_POSITION, _listenerPosition.ToPointer());
+ alListenerfv(AL_VELOCITY, _listenerVelocity.ToPointer());
+ alListenerfv(AL_ORIENTATION, orien); // Zero-length vectors can crash OpenAL!
+ }
+ }
+}
+
+void AudioSystem::DeleteAllStreams()
+{
+ VERUS_FOR(i, VERUS_ARRAY_LENGTH(_streamPlayers))
+ _streamPlayers[i].Done();
+}
+
+PSound AudioSystem::InsertSound(CSZ url)
+{
+ return TStoreSounds::Insert(url);
+}
+
+PSound AudioSystem::FindSound(CSZ url)
+{
+ return TStoreSounds::Find(url);
+}
+
+void AudioSystem::DeleteSound(CSZ url)
+{
+ TStoreSounds::Delete(url);
+}
+
+void AudioSystem::DeleteAllSounds()
+{
+ TStoreSounds::DeleteAll();
+}
+
+void AudioSystem::UpdateListener(RcPoint3 pos, RcVector3 dir, RcVector3 vel, RcVector3 up)
+{
+ _listenerPosition = pos;
+ _listenerDirection = dir;
+ _listenerVelocity = vel;
+ _listenerUp = up;
+}
+
+float AudioSystem::ComputeTravelDelay(RcPoint3 pos) const
+{
+ const float speed = alGetFloat(AL_SPEED_OF_SOUND);
+ const float dist = VMath::dist(pos, _listenerPosition);
+ return Math::Clamp(dist / speed, 0, 3);
+}
diff --git a/Verus/src/Audio/AudioSystem.h b/Verus/src/Audio/AudioSystem.h
new file mode 100644
index 0000000..5205ef9
--- /dev/null
+++ b/Verus/src/Audio/AudioSystem.h
@@ -0,0 +1,44 @@
+#pragma once
+
+namespace verus
+{
+ namespace Audio
+ {
+ typedef StoreUnique TStoreSounds;
+ class AudioSystem : public Singleton, public Object, private TStoreSounds
+ {
+ Point3 _listenerPosition = Point3(0);
+ Vector3 _listenerDirection = Vector3(0, 0, 1);
+ Vector3 _listenerVelocity = Vector3(0);
+ Vector3 _listenerUp = Vector3(0, 1, 0);
+ ALCdevice* _pDevice = nullptr;
+ ALCcontext* _pContext = nullptr;
+ String _version;
+ String _deviceSpecifier;
+ StreamPlayer _streamPlayers[4];
+
+ public:
+ AudioSystem();
+ ~AudioSystem();
+
+ void Init();
+ void Done();
+
+ void Update();
+
+ RStreamPlayer GetStreamPlayer(int index) { return _streamPlayers[index]; }
+ void DeleteAllStreams();
+
+ PSound InsertSound(CSZ url);
+ PSound FindSound(CSZ url);
+ void DeleteSound(CSZ url);
+ void DeleteAllSounds();
+
+ void UpdateListener(RcPoint3 pos, RcVector3 dir, RcVector3 vel, RcVector3 up);
+ float ComputeTravelDelay(RcPoint3 pos) const;
+
+ static CSZ GetSingletonFailMessage() { return "Make_Audio(); // FAIL.\r\n"; }
+ };
+ VERUS_TYPEDEFS(AudioSystem);
+ }
+}
diff --git a/Verus/src/Audio/OggCallbacks.cpp b/Verus/src/Audio/OggCallbacks.cpp
new file mode 100644
index 0000000..496930b
--- /dev/null
+++ b/Verus/src/Audio/OggCallbacks.cpp
@@ -0,0 +1,46 @@
+#include "verus.h"
+
+namespace verus
+{
+ namespace Audio
+ {
+ size_t read_func(void* ptr, size_t size, size_t nmemb, void* datasource)
+ {
+ POggDataSource pDS = static_cast(datasource);
+ INT64 readLen = size * nmemb;
+ const INT64 maxLen = pDS->_size - pDS->_cursor;
+ if (readLen > maxLen)
+ readLen = maxLen;
+ memcpy(ptr, pDS->_p + pDS->_cursor, size_t(readLen));
+ pDS->_cursor += readLen;
+ return size_t(readLen);
+ }
+
+ int seek_func(void* datasource, ogg_int64_t offset, int whence)
+ {
+ POggDataSource pDS = static_cast(datasource);
+ switch (whence)
+ {
+ case SEEK_SET: pDS->_cursor = offset; return 0;
+ case SEEK_CUR: pDS->_cursor += offset; return 0;
+ case SEEK_END: pDS->_cursor = pDS->_size - offset; return 0;
+ }
+ return -1;
+ }
+
+ int close_func(void* datasource)
+ {
+ POggDataSource pDS = static_cast(datasource);
+ pDS->_cursor = 0;
+ return 0;
+ }
+
+ long tell_func(void* datasource)
+ {
+ POggDataSource pDS = static_cast(datasource);
+ return long(pDS->_cursor);
+ }
+
+ const ov_callbacks g_oggCallbacks = { read_func, seek_func, close_func, tell_func };
+ }
+}
diff --git a/Verus/src/Audio/OggCallbacks.h b/Verus/src/Audio/OggCallbacks.h
new file mode 100644
index 0000000..e197c32
--- /dev/null
+++ b/Verus/src/Audio/OggCallbacks.h
@@ -0,0 +1,22 @@
+#pragma once
+
+namespace verus
+{
+ namespace Audio
+ {
+ struct OggDataSource
+ {
+ const BYTE* _p = nullptr;
+ INT64 _size = 0;
+ INT64 _cursor = 0;
+ };
+ VERUS_TYPEDEFS(OggDataSource);
+
+ size_t read_func(void*, size_t, size_t, void*);
+ int seek_func(void*, ogg_int64_t, int);
+ int close_func(void*);
+ long tell_func(void*);
+
+ extern const ov_callbacks g_oggCallbacks;
+ }
+}
diff --git a/Verus/src/Audio/Sound.cpp b/Verus/src/Audio/Sound.cpp
new file mode 100644
index 0000000..875d65b
--- /dev/null
+++ b/Verus/src/Audio/Sound.cpp
@@ -0,0 +1,183 @@
+#include "verus.h"
+
+using namespace verus;
+using namespace verus::Audio;
+
+// Sound:
+
+Sound::Sound()
+{
+}
+
+Sound::~Sound()
+{
+ Done();
+}
+
+void Sound::Init(RcDesc desc)
+{
+ if (_refCount)
+ return;
+
+ VERUS_INIT();
+
+ _url = desc._url;
+ _refCount = 1;
+ if (desc._is3D) SetFlag(SoundFlags::is3D);
+ if (desc._loop) SetFlag(SoundFlags::loop);
+ _gain = desc._gain;
+ _pitch = desc._pitch;
+ _referenceDistance = desc._referenceDistance;
+ if (desc._randomOffset) SetFlag(SoundFlags::randOff);
+
+ IO::Async::I().Load(desc._url, this);
+}
+
+bool Sound::Done()
+{
+ _refCount--;
+ if (_refCount <= 0)
+ {
+ IO::Async::Cancel(this);
+ VERUS_FOR(i, VERUS_ARRAY_LENGTH(_sources))
+ _sources[i].Done();
+ if (_buffer)
+ alDeleteBuffers(1, &_buffer);
+ VERUS_DONE(Sound);
+ return true;
+ }
+ return false;
+}
+
+void Sound::Async_Run(CSZ url, RcBlob blob)
+{
+ VERUS_RT_ASSERT(_url == url);
+ VERUS_RT_ASSERT(!_buffer);
+ VERUS_RT_ASSERT(blob._size);
+
+ OggDataSource oggds;
+ oggds._p = blob._p;
+ oggds._size = blob._size;
+
+ OggVorbis_File ovf;
+ vorbis_info* povi;
+ const int ret = ov_open_callbacks(&oggds, &ovf, 0, 0, g_oggCallbacks);
+ if (ret < 0)
+ throw VERUS_RUNTIME_ERROR << "ov_open_callbacks(), " << ret;
+
+ povi = ov_info(&ovf, -1);
+ const ALenum format = (povi->channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16;
+
+ alGenBuffers(1, &_buffer);
+
+ _length = static_cast(ov_time_total(&ovf, -1));
+ const INT64 pcmSize = ov_pcm_total(&ovf, -1) * 2 * povi->channels + 1;
+ Vector vRaw;
+ vRaw.resize(pcmSize);
+ int bitstream, offset = 0;
+ long num = ov_read(&ovf, reinterpret_cast(vRaw.data()), Utils::Cast32(vRaw.size()), 0, 2, 1, &bitstream);
+ do
+ {
+ offset += num;
+ num = ov_read(&ovf, reinterpret_cast(&vRaw[offset]), Utils::Cast32(vRaw.size()) - offset, 0, 2, 1, &bitstream);
+ } while (num > 0);
+ alBufferData(_buffer, format, vRaw.data(), offset, povi->rate);
+ ov_clear(&ovf);
+
+ SetFlag(SoundFlags::loaded);
+}
+
+void Sound::Update()
+{
+ if (!IsLoaded())
+ return;
+ VERUS_UPDATE_ONCE_CHECK;
+
+ VERUS_FOR(i, VERUS_ARRAY_LENGTH(_sources))
+ _sources[i].Update();
+}
+
+void Sound::UpdateHRTF()
+{
+ if (!IsLoaded())
+ return;
+ const bool is3D = IsFlagSet(SoundFlags::is3D);
+ VERUS_FOR(i, VERUS_ARRAY_LENGTH(_sources))
+ _sources[i].UpdateHRTF(is3D);
+}
+
+SourcePtr Sound::NewSource(PSourcePtr pID, Source::RcDesc desc)
+{
+ SourcePtr source;
+ if (IsLoaded())
+ {
+ source.Attach(_sources + _next, this);
+ VERUS_CIRCULAR_ADD(_next, VERUS_ARRAY_LENGTH(_sources));
+
+ if (source->_sid) // Delete existing?
+ alDeleteSources(1, &source->_sid);
+
+ alGenSources(1, &source->_sid);
+ const ALuint sid = source->_sid;
+ if (IsFlagSet(SoundFlags::is3D))
+ {
+ alSourcef(sid, AL_REFERENCE_DISTANCE, _referenceDistance);
+ alSourcef(sid, AL_ROLLOFF_FACTOR, 4);
+ }
+ else // No 3D effect?
+ {
+ alSourcei(sid, AL_SOURCE_RELATIVE, AL_TRUE);
+ alSource3f(sid, AL_POSITION, 0, 0, 0);
+ alSource3f(sid, AL_VELOCITY, 0, 0, 0);
+ alSourcef(sid, AL_ROLLOFF_FACTOR, 0);
+ }
+ alSourcef(sid, AL_PITCH, _pitch.GetRandomValue()*desc._pitch);
+ alSourcei(sid, AL_LOOPING, IsFlagSet(SoundFlags::loop) ? 1 : 0);
+ alSourcei(sid, AL_BUFFER, _buffer);
+ alSourcef(sid, AL_GAIN, _gain.GetRandomValue()*desc._gain);
+
+ if (desc._secOffset < 0)
+ {
+ VERUS_QREF_UTILS;
+ ALint size = 0;
+ alGetBufferi(_buffer, AL_SIZE, &size);
+ const ALint offset = utils.GetRandom().Next() % size;
+ alSourcei(sid, AL_BYTE_OFFSET, offset);
+ }
+ else if (desc._secOffset > 0)
+ {
+ alSourcef(sid, AL_SEC_OFFSET, desc._secOffset);
+ }
+ }
+ if (pID) // Adjust this source later?
+ {
+ if (desc._stopSource && *pID)
+ (*pID)->Stop();
+ *pID = source;
+ }
+ return source;
+}
+
+float Sound::GetRandomOffset() const
+{
+ return _length * Utils::I().GetRandom().NextFloat();
+}
+
+// SoundPtr:
+
+void SoundPtr::Init(Sound::RcDesc desc)
+{
+ VERUS_QREF_ASYS;
+ VERUS_RT_ASSERT(!_p);
+ _p = asys.InsertSound(desc._url);
+ _p->Init(desc);
+}
+
+void SoundPwn::Done()
+{
+ if (_p)
+ {
+ AudioSystem::I().DeleteSound(_C(_p->GetUrl()));
+ _p = nullptr;
+ }
+}
diff --git a/Verus/src/Audio/Sound.h b/Verus/src/Audio/Sound.h
new file mode 100644
index 0000000..e6752f7
--- /dev/null
+++ b/Verus/src/Audio/Sound.h
@@ -0,0 +1,141 @@
+#pragma once
+
+namespace verus
+{
+ namespace Audio
+ {
+ struct SoundFlags
+ {
+ enum
+ {
+ loaded = (ObjectFlags::user << 0),
+ is3D = (ObjectFlags::user << 1),
+ loop = (ObjectFlags::user << 2),
+ randOff = (ObjectFlags::user << 3),
+ user = (ObjectFlags::user << 4)
+ };
+ };
+
+ class Sound : public Object, public IO::AsyncCallback
+ {
+ Source _sources[8];
+ String _url;
+ ALuint _buffer = 0;
+ int _next = 0;
+ int _refCount = 0;
+ Range _gain = 1;
+ Range _pitch = 1;
+ float _length = 0;
+ float _referenceDistance = 4;
+
+ public:
+ struct Desc
+ {
+ CSZ _url = nullptr;
+ Range _gain = 0.8f;
+ Range _pitch = 1;
+ float _referenceDistance = 4;
+ bool _is3D = false;
+ bool _loop = false;
+ bool _randomOffset = false;
+
+ Desc(CSZ url) : _url(url) {}
+ Desc& Set3D(bool b = true) { _is3D = b; return *this; }
+ Desc& SetLoop(bool b = true) { _loop = b; return *this; }
+ Desc& SetGain(const Range& gain) { _gain = gain; return *this; }
+ Desc& SetPitch(const Range& pitch) { _pitch = pitch; return *this; }
+ Desc& SetRandomOffset(bool b = true) { _randomOffset = b; return *this; }
+ Desc& SetReferenceDistance(float rd) { _referenceDistance = rd; return *this; }
+ };
+ VERUS_TYPEDEFS(Desc);
+
+ Sound();
+ ~Sound();
+
+ void Init(RcDesc desc);
+ bool Done();
+
+ virtual void Async_Run(CSZ url, RcBlob blob) override;
+ bool IsLoaded() const { return IsFlagSet(SoundFlags::loaded); }
+ void AddRef() { _refCount++; }
+
+ Str GetUrl() const { return _C(_url); }
+
+ void Update();
+ void UpdateHRTF();
+
+ SourcePtr NewSource(PSourcePtr pID = nullptr, Source::RcDesc desc = Source::Desc());
+
+ const Range& GetGain() const { return _gain; }
+ const Range& GetPitch() const { return _pitch; }
+
+ float GetLength() const { return _length; }
+
+ float GetRandomOffset() const;
+ void SetRandomOffset(bool b) { b ? SetFlag(SoundFlags::randOff) : ResetFlag(SoundFlags::randOff); }
+ bool HasRandomOffset() const { return IsFlagSet(SoundFlags::randOff); }
+ };
+ VERUS_TYPEDEFS(Sound);
+
+ class SoundPtr : public Ptr
+ {
+ public:
+ void Init(Sound::RcDesc desc);
+ };
+ VERUS_TYPEDEFS(SoundPtr);
+
+ class SoundPwn : public SoundPtr
+ {
+ public:
+ ~SoundPwn() { Done(); }
+ void Done();
+ };
+ VERUS_TYPEDEFS(SoundPwn);
+
+ template
+ class SoundPwns
+ {
+ SoundPwn _sounds[NUM];
+ int _prev = 0;
+
+ public:
+ SoundPwns()
+ {
+ }
+
+ ~SoundPwns()
+ {
+ Done();
+ }
+
+ void Init(Sound::RcDesc desc)
+ {
+ char buffer[200];
+ VERUS_FOR(i, NUM)
+ {
+ sprintf_s(buffer, desc._url, i);
+ Sound::Desc descs = desc;
+ descs._url = buffer;
+ _sounds[i].Init(descs);
+ }
+ }
+
+ void Done()
+ {
+ VERUS_FOR(i, NUM)
+ _sounds[i].Done();
+ }
+
+ RSoundPtr operator[](int i)
+ {
+ if (i < 0)
+ i = Utils::I().GetRandom().Next() & 0xFF; // Only positive.
+ i %= NUM;
+ if (i == _prev)
+ i = (i + 1) % NUM;
+ _prev = i;
+ return _sounds[i];
+ }
+ };
+ }
+}
diff --git a/Verus/src/Audio/Source.cpp b/Verus/src/Audio/Source.cpp
new file mode 100644
index 0000000..f955d26
--- /dev/null
+++ b/Verus/src/Audio/Source.cpp
@@ -0,0 +1,136 @@
+#include "verus.h"
+
+using namespace verus;
+using namespace verus::Audio;
+
+// Source:
+
+Source::Source()
+{
+}
+
+Source::~Source()
+{
+ Done();
+}
+
+void Source::Done()
+{
+ if (_sid)
+ {
+ alDeleteSources(1, &_sid);
+ _sid = 0;
+ _pSound = nullptr;
+ }
+}
+
+void Source::Update()
+{
+ if (!_sid)
+ return;
+
+ VERUS_QREF_TIMER;
+
+ int state;
+ alGetSourcei(_sid, AL_SOURCE_STATE, &state);
+ if (AL_INITIAL == state)
+ {
+ _travelDelay -= dt;
+ if (_travelDelay <= 0)
+ {
+ if (_pSound->HasRandomOffset())
+ alSourcef(_sid, AL_SEC_OFFSET, _pSound->GetRandomOffset());
+ alSourcePlay(_sid);
+ }
+ }
+}
+
+void Source::UpdateHRTF(bool is3D)
+{
+ if (!_sid)
+ return;
+
+ int state;
+ alGetSourcei(_sid, AL_SOURCE_STATE, &state);
+ if (AL_STOPPED == state)
+ {
+ alDeleteSources(1, &_sid);
+ _sid = 0;
+ }
+ else if (is3D)
+ {
+ alSourcefv(_sid, AL_POSITION, _position.ToPointer());
+ alSourcefv(_sid, AL_DIRECTION, _direction.ToPointer());
+ alSourcefv(_sid, AL_VELOCITY, _velocity.ToPointer());
+ }
+}
+
+void Source::Play()
+{
+ if (!this)
+ return; // For NewSource()-> pattern.
+ if (_pSound && _pSound->IsLoaded())
+ {
+ if (_pSound->HasRandomOffset())
+ alSourcef(_sid, AL_SEC_OFFSET, _pSound->GetRandomOffset());
+ alSourcePlay(_sid);
+ }
+}
+
+void Source::PlayAt(RcPoint3 pos, RcVector3 dir, RcVector3 vel, float delay)
+{
+ if (!this)
+ return; // For NewSource()-> pattern.
+ VERUS_QREF_ASYS;
+ if (_pSound && _pSound->IsLoaded())
+ {
+ _travelDelay = asys.ComputeTravelDelay(pos) + delay;
+ MoveTo(pos, dir, vel);
+ UpdateHRTF(_pSound->IsFlagSet(SoundFlags::is3D));
+ }
+}
+
+void Source::Stop()
+{
+ if (_pSound && _pSound->IsLoaded())
+ alSourceStop(_sid);
+}
+
+void Source::MoveTo(RcPoint3 pos, RcVector3 dir, RcVector3 vel)
+{
+ if (_pSound && _pSound->IsLoaded())
+ {
+ _position = pos;
+ _direction = dir;
+ _velocity = vel;
+ }
+}
+
+void Source::SetGain(float gain)
+{
+ if (_pSound && _pSound->IsLoaded())
+ alSourcef(_sid, AL_GAIN, gain*_pSound->GetGain().GetRandomValue());
+}
+
+void Source::SetPitch(float pitch)
+{
+ if (_pSound && _pSound->IsLoaded())
+ alSourcef(_sid, AL_PITCH, pitch*_pSound->GetPitch().GetRandomValue());
+}
+
+// SourcePtr:
+
+void SourcePtr::Attach(Source* pSource, Sound* pSound)
+{
+ _p = pSource;
+ _p->_pSound = pSound;
+}
+
+void SourcePtr::Done()
+{
+ if (_p)
+ {
+ _p->Done();
+ _p = nullptr;
+ }
+}
diff --git a/Verus/src/Audio/Source.h b/Verus/src/Audio/Source.h
new file mode 100644
index 0000000..5ae6b20
--- /dev/null
+++ b/Verus/src/Audio/Source.h
@@ -0,0 +1,55 @@
+#pragma once
+
+namespace verus
+{
+ namespace Audio
+ {
+ class Source
+ {
+ friend class SourcePtr; // _pSound @ Attach().
+ friend class Sound; // _sid @ NewSource().
+
+ Point3 _position = Point3(0);
+ Vector3 _direction = Vector3(0);
+ Vector3 _velocity = Vector3(0);
+ Sound* _pSound = nullptr;
+ ALuint _sid = 0;
+ float _travelDelay = 0;
+
+ public:
+ struct Desc
+ {
+ float _secOffset = 0;
+ float _gain = 1;
+ float _pitch = 1;
+ bool _stopSource = true;
+ };
+ VERUS_TYPEDEFS(Desc);
+
+ Source();
+ ~Source();
+
+ void Done();
+
+ void Update();
+ void UpdateHRTF(bool is3D);
+
+ void Play();
+ void PlayAt(RcPoint3 pos, RcVector3 dir = Vector3(0), RcVector3 vel = Vector3(0), float delay = 0);
+ void Stop();
+ void MoveTo(RcPoint3 pos, RcVector3 dir = Vector3(0), RcVector3 vel = Vector3(0));
+
+ void SetGain(float gain);
+ void SetPitch(float pitch);
+ };
+ VERUS_TYPEDEFS(Source);
+
+ class SourcePtr : public Ptr