mirror of
https://github.com/orange-cpp/omath.git
synced 2026-02-13 23:13:26 +00:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a7b9d2338 | |||
| 3d67827704 | |||
| 45a37eb413 | |||
| 90c4ea2036 | |||
| e10cbf9356 | |||
| 4ad44badb9 | |||
| adce4a808a | |||
| 257b06c552 | |||
| a94c78f834 | |||
| b6ac0a1d61 | |||
| f3b74fe433 | |||
| 2ddf29b158 | |||
| bf30957acf | |||
| c9ac61935e | |||
| 60a3a42140 | |||
| 17e21cde4b | |||
| 7fb5ea47dd | |||
| d7189eb7d4 | |||
| ff35571231 | |||
| 3744a6cdec | |||
| 0fd9a5aed8 | |||
| 3dd792c2d5 | |||
| 584969da44 | |||
| acf36c3e04 | |||
| 27c1d147c5 | |||
| 3831bc0999 | |||
| d23bc3204d | |||
| c158f08430 | |||
| e05eba42c3 | |||
| e97be8c142 | |||
| e97d097b2b | |||
| 58aa03c4a9 | |||
| e1399d1814 | |||
| 1964d3d36f | |||
| d7a009eb67 | |||
| 0e03805439 | |||
| eafefb40ec | |||
| 9e4c778e8f | |||
| 0788fd6122 | |||
| 3685f13344 | |||
| d4d8f70fff | |||
| 918858e255 | |||
| 1aff083ef3 | |||
| 6414922884 | |||
| 57ba809076 | |||
| 6fd3a695cf | |||
| f6857cac90 | |||
| b994e47357 | |||
| 82b21d0458 | |||
| daa1abc047 | |||
| 3a66b66c6a | |||
| 6c89c72041 | |||
| e54d5e7388 | |||
| 9a89e2467e | |||
| 48bf06f69c | |||
| 8feddf872a | |||
| 99ebdeb188 | |||
| ba267cbcb8 | |||
| b98093b244 | |||
| 58392144ca | |||
| f1394a24e5 | |||
| e396e00016 | |||
| 0dc4890107 | |||
| dd8c41b19f | |||
| 0283935918 | |||
| 12f888b8d4 | |||
| a5b24f90dc | |||
|
|
df4947ceb3 | ||
|
|
190a8bf91e | ||
|
|
d118e88f6b | ||
| 1553139a80 | |||
| 798caa2b0d | |||
| 88d4447b20 | |||
| ee458a24f7 | |||
| fa91f21e39 | |||
| 873bdd2036 | |||
| 20aecac2ae | |||
| 09fd92ccad | |||
| 2b21caf58f | |||
| 40e26be72e | |||
| 2699053102 | |||
| 06b597f37c | |||
| 6d3b543648 | |||
| c515dc89a9 | |||
| 66919af46a | |||
| 28ef194586 | |||
| 05bc7577b5 | |||
| 9efabd8fb2 | |||
| 8f225c5f8e | |||
| 44682b6f2c | |||
| a6cf043d79 | |||
| 3968d13a19 | |||
| a3f45628aa | |||
| 6da44a5a51 | |||
| e5d8e1c953 | |||
| e2378bfa8b | |||
| ca3dab855b | |||
| 79482d56d1 | |||
| c4024663bb | |||
| b2a512eafe | |||
| 7b0e2127dc | |||
| 0b663b73d5 | |||
| afc0720f08 | |||
| 015fc9b1e7 | |||
| 62d1a615ae | |||
| 043b5c588d | |||
| cd18b088cb | |||
| ebfdd0b70e | |||
| e7b82f441c | |||
| 6e59957247 | |||
| 1e540862a0 | |||
| 31cc1116ae | |||
| 338246a618 | |||
| 10ebf6ed04 | |||
| ec9a15927a | |||
| 1a0e55b4cf | |||
| b48160e1b7 | |||
| d4a16a94e6 | |||
| 07c0eebf21 | |||
| 9ed18f27c3 | |||
|
|
86b1e8a00d |
36
.github/workflows/cmake-multi-platform.yml
vendored
36
.github/workflows/cmake-multi-platform.yml
vendored
@@ -84,3 +84,39 @@ jobs:
|
||||
- name: Run unit_tests.exe
|
||||
shell: bash
|
||||
run: ./out/Release/unit_tests.exe
|
||||
|
||||
##############################################################################
|
||||
# 3) macOS – AppleClang / Ninja
|
||||
##############################################################################
|
||||
macosx-build-and-test:
|
||||
name: macOS (AppleClang)
|
||||
runs-on: macOS-latest
|
||||
env:
|
||||
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
|
||||
steps:
|
||||
- name: Install basic tool-chain with Homebrew
|
||||
shell: bash
|
||||
run: |
|
||||
brew install cmake ninja
|
||||
|
||||
- name: Checkout repository (with sub-modules)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up vcpkg
|
||||
shell: bash
|
||||
run: |
|
||||
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
|
||||
|
||||
- name: Configure (cmake --preset)
|
||||
shell: bash
|
||||
run: cmake --preset darwin-release-vcpkg -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: cmake --build cmake-build/build/darwin-release-vcpkg --target unit_tests omath
|
||||
|
||||
- name: Run unit_tests
|
||||
shell: bash
|
||||
run: ./out/Release/unit_tests
|
||||
|
||||
4
.idea/editor.xml
generated
4
.idea/editor.xml
generated
@@ -201,7 +201,7 @@
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppStaticDataMemberInUnnamedStruct/@EntryIndexedValue" value="WARNING" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppStaticSpecifierOnAnonymousNamespaceMember/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppStringLiteralToCharPointerConversion/@EntryIndexedValue" value="WARNING" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTabsAreDisallowed/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTabsAreDisallowed/@EntryIndexedValue" value="WARNING" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTemplateArgumentsCanBeDeduced/@EntryIndexedValue" value="HINT" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTemplateParameterNeverUsed/@EntryIndexedValue" value="HINT" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTemplateParameterShadowing/@EntryIndexedValue" value="WARNING" type="string" />
|
||||
@@ -215,7 +215,7 @@
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnmatchedPragmaEndRegionDirective/@EntryIndexedValue" value="WARNING" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnmatchedPragmaRegionDirective/@EntryIndexedValue" value="WARNING" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnnamedNamespaceInHeaderFile/@EntryIndexedValue" value="WARNING" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnnecessaryWhitespace/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnnecessaryWhitespace/@EntryIndexedValue" value="WARNING" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnsignedZeroComparison/@EntryIndexedValue" value="WARNING" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnusedIncludeDirective/@EntryIndexedValue" value="WARNING" type="string" />
|
||||
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseAlgorithmWithCount/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||
|
||||
@@ -35,6 +35,8 @@ if (VCPKG_MANIFEST_FEATURES)
|
||||
set(OMATH_BUILD_TESTS ON)
|
||||
elseif (omath_feature STREQUAL "benchmark")
|
||||
set(OMATH_BUILD_BENCHMARK ON)
|
||||
elseif (omath_feature STREQUAL "examples")
|
||||
set(OMATH_BUILD_EXAMPLES ON)
|
||||
endif ()
|
||||
|
||||
endforeach ()
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"OMATH_BUILD_VIA_VCPKG": "ON",
|
||||
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||
"VCPKG_INSTALLED_DIR": "${sourceDir}/cmake-build/vcpkg_installed",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2"
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
{
|
||||
"name": "windows-debug-vcpkg",
|
||||
"displayName": "Debug",
|
||||
"displayName": "Windows Debug Vcpkg",
|
||||
"inherits": "windows-base-vcpkg",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
{
|
||||
"name": "windows-release-vcpkg",
|
||||
"displayName": "Release",
|
||||
"displayName": "Windows Release Vcpkg",
|
||||
"inherits": "windows-base-vcpkg",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
@@ -127,7 +127,8 @@
|
||||
"binaryDir": "${sourceDir}/cmake-build/build/${presetName}",
|
||||
"installDir": "${sourceDir}/cmake-build/install/${presetName}",
|
||||
"cacheVariables": {
|
||||
"CMAKE_CXX_COMPILER": "clang++"
|
||||
"CMAKE_CXX_COMPILER": "clang++",
|
||||
"CMAKE_MAKE_PROGRAM": "ninja"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
@@ -135,6 +136,17 @@
|
||||
"rhs": "Darwin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "darwin-base-vcpkg",
|
||||
"hidden": true,
|
||||
"inherits": "darwin-base",
|
||||
"cacheVariables": {
|
||||
"OMATH_BUILD_VIA_VCPKG": "ON",
|
||||
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||
"VCPKG_INSTALLED_DIR": "${sourceDir}/cmake-build/vcpkg_installed",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "darwin-debug",
|
||||
"displayName": "Darwin Debug",
|
||||
@@ -145,8 +157,8 @@
|
||||
},
|
||||
{
|
||||
"name": "darwin-debug-vcpkg",
|
||||
"displayName": "Darwin Debug",
|
||||
"inherits": "darwin-base",
|
||||
"displayName": "Darwin Debug Vcpkg",
|
||||
"inherits": "darwin-base-vcpkg",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
@@ -154,15 +166,15 @@
|
||||
{
|
||||
"name": "darwin-release",
|
||||
"displayName": "Darwin Release",
|
||||
"inherits": "darwin-debug",
|
||||
"inherits": "darwin-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "darwin-release-vcpkg",
|
||||
"displayName": "Darwin Release",
|
||||
"inherits": "darwin-debug",
|
||||
"displayName": "Darwin Release Vcpkg",
|
||||
"inherits": "darwin-base-vcpkg",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
}
|
||||
|
||||
@@ -1,93 +1,201 @@
|
||||
## 🎯 Goal
|
||||
# UNIVERSAL DECLARATION OF CODE OF CONDUCT
|
||||
_Declaration of Community Rights and Responsibilities_
|
||||
|
||||
My goal is to provide a space where it is safe for everyone to contribute to,
|
||||
and get support for, open-source software in a respectful and cooperative
|
||||
manner.
|
||||
## Preamble
|
||||
|
||||
I value all contributions and want to make this project and its
|
||||
surrounding community a place for everyone.
|
||||
Whereas the Orange++ community is founded on cooperation, mutual respect and support for the development of open-source software;
|
||||
|
||||
As members, contributors, and everyone else who may participate in the
|
||||
development, I strive to keep the entire experience civil.
|
||||
Whereas it is essential that all participants can contribute and seek assistance in an environment that is safe, inclusive and free from discrimination and harassment;
|
||||
|
||||
## 📜 Standards
|
||||
Whereas the dignity and equality of all participants, regardless of their traits or background, must be respected and protected;
|
||||
|
||||
Our community standards exist in order to make sure everyone feels comfortable
|
||||
contributing to the project(s) together.
|
||||
Now, therefore, this Community Code of Conduct is proclaimed as a common standard of behaviour for all members, contributors and participants in projects led by Orange++ and its official communities.
|
||||
|
||||
Our standards are:
|
||||
- Do not harass, attack, or in any other way discriminate against anyone, including
|
||||
for their protected traits, including, but not limited to, sex, religion, race,
|
||||
appearance, gender, identity, nationality, sexuality, etc.
|
||||
- Do not go off-topic, do not post spam.
|
||||
- Treat everyone with respect.
|
||||
---
|
||||
|
||||
Examples of breaking each rule respectively include:
|
||||
- Harassment, bullying or inappropriate jokes about another person.
|
||||
- Posting distasteful imagery, trolling, or posting things unrelated to the topic at hand.
|
||||
- Treating someone as worse because of their lack of understanding of an issue.
|
||||
## Article 1
|
||||
|
||||
## ⚡ Enforcement
|
||||
This Code of Conduct establishes standards of behaviour intended to:
|
||||
|
||||
Enforcement of this CoC is done by Orange++ and/or other core contributors.
|
||||
1. Provide a safe and welcoming environment for all participants.
|
||||
2. Encourage respectful and constructive collaboration.
|
||||
3. Prevent harassment, discrimination, and other harmful conduct.
|
||||
|
||||
I, as the core developer, will strive my best to keep this community civil and
|
||||
following the standards outlined above.
|
||||
All individuals who participate in Orange++ projects or official communities, whether online or offline, are expected to adhere to this Code of Conduct.
|
||||
|
||||
### 🚩 Reporting incidents
|
||||
---
|
||||
|
||||
If you believe an incident of breaking these standards has occurred, but nobody has
|
||||
taken appropriate action, you can privately contact the people responsible for dealing
|
||||
with such incidents in multiple ways:
|
||||
## Article 2
|
||||
|
||||
All participants are equal in dignity and rights within the community.
|
||||
|
||||
No person shall be harassed, attacked, or discriminated against on the basis of protected or personal traits, including but not limited to:
|
||||
|
||||
- sex;
|
||||
- religion or belief;
|
||||
- race or ethnicity;
|
||||
- appearance;
|
||||
- gender or gender identity;
|
||||
- nationality;
|
||||
- sexual orientation;
|
||||
- or any other similar characteristic.
|
||||
|
||||
Treating someone as lesser or unworthy because of their knowledge, experience, or level of understanding of an issue is incompatible with this Code.
|
||||
|
||||
---
|
||||
|
||||
## Article 3
|
||||
|
||||
Participants shall treat one another with respect at all times.
|
||||
|
||||
Participants shall:
|
||||
|
||||
1. Engage in discussion in good faith and assume good intent where reasonable.
|
||||
2. Provide feedback and criticism in a constructive and considerate manner.
|
||||
3. Recognize that people have different backgrounds, perspectives, and levels of expertise.
|
||||
|
||||
Examples of conduct contrary to this Article include, but are not limited to:
|
||||
|
||||
- harassment, bullying, personal attacks or degrading comments;
|
||||
- inappropriate or offensive jokes or remarks about another person;
|
||||
- persistent disruption of discussions or activities.
|
||||
|
||||
---
|
||||
|
||||
## Article 4
|
||||
|
||||
Participants shall remain on topic and avoid posting spam or irrelevant material.
|
||||
|
||||
Content that is distasteful, deliberately inflammatory, or unrelated to the project or discussion at hand is prohibited.
|
||||
|
||||
Examples of prohibited conduct under this Article include:
|
||||
|
||||
- posting trolling or inflammatory messages;
|
||||
- sharing disturbing or inappropriate imagery unrelated to the topic;
|
||||
- repeatedly derailing conversations away from their intended purpose.
|
||||
|
||||
---
|
||||
|
||||
## Article 5
|
||||
|
||||
The following standards shall guide all participation in Orange++ projects and official communities:
|
||||
|
||||
1. Do not harass, attack, or discriminate against any person.
|
||||
2. Do not go off-topic and do not post spam.
|
||||
3. Treat all participants with respect.
|
||||
|
||||
These standards apply equally to maintainers, contributors, and all other participants, regardless of status or seniority.
|
||||
|
||||
---
|
||||
|
||||
## Article 6
|
||||
|
||||
Enforcement of this Code of Conduct is carried out by Orange++ and/or other core contributors (hereinafter “members”).
|
||||
|
||||
Members shall strive to:
|
||||
|
||||
1. Act fairly, consistently, and transparently.
|
||||
2. Consider the context and severity of each incident.
|
||||
3. Maintain a civil and welcoming environment for the community as a whole.
|
||||
|
||||
Where appropriate, members may consult individuals with relevant lived experience, particularly when an incident concerns a marginalized group, while preserving confidentiality as required by this Code.
|
||||
|
||||
---
|
||||
|
||||
## Article 7
|
||||
|
||||
Any participant who believes that a breach of this Code of Conduct has occurred and has not been appropriately addressed may report the incident privately.
|
||||
|
||||
Reports may be submitted through any of the following channels:
|
||||
|
||||
**E-mail**
|
||||
|
||||
***E-Mail***
|
||||
- `orange-cpp@yandex.ru`
|
||||
|
||||
***Discord***
|
||||
**Discord**
|
||||
|
||||
- `@orange_cpp`
|
||||
|
||||
***Telegram***
|
||||
**Telegram**
|
||||
|
||||
- `@orange_cpp`
|
||||
|
||||
I guarantee your privacy and will not share those reports with anyone.
|
||||
The reporting party’s privacy shall be respected, and reports shall not be shared beyond those responsible for handling them, except where required by law or with the explicit consent of the reporting party.
|
||||
|
||||
## ⚖️ Enforcement Strategy
|
||||
---
|
||||
|
||||
Depending on the severity of the infraction, any action from the list below may be applied.
|
||||
Please keep in mind cases are reviewed on a per-case basis and members are the ultimate
|
||||
deciding factor in the type of punishment.
|
||||
## Article 8
|
||||
|
||||
If the matter benefited from an outside opinion, a member might reach for more opinions
|
||||
from people unrelated, however, the final decision regarding the action
|
||||
to be taken is still up to the member.
|
||||
Depending on the nature and severity of the infraction, and taking into account past behaviour, members may apply one or more of the following measures.
|
||||
|
||||
For example, if the matter at hand regards a representative of a marginalized group or minority,
|
||||
the member might ask for a first-hand opinion from another representative of such group.
|
||||
**1. Correction / Edit**
|
||||
|
||||
### ✏️ Correction/Edit
|
||||
Where a message is misleading, poorly worded, or likely to cause misunderstanding, members may:
|
||||
|
||||
If your message is found to be misleading or poorly worded, a member might
|
||||
edit your message.
|
||||
1. Request that the author clarify or correct the message; or
|
||||
2. Edit the message where the platform permits and such action is appropriate and transparent.
|
||||
|
||||
### ⚠️ Warning/Deletion
|
||||
**2. Warning / Deletion**
|
||||
|
||||
If your message is found inappropriate, a member might give you a public or private warning,
|
||||
and/or delete your message.
|
||||
Where a message is inappropriate or in breach of the standards, members may:
|
||||
|
||||
### 🔇 Mute
|
||||
1. Issue a public or private warning; and/or
|
||||
2. Delete the message.
|
||||
|
||||
If your message is disruptive, or you have been repeatedly violating the standards,
|
||||
a member might mute (or temporarily ban) you.
|
||||
**3. Mute / Temporary Ban**
|
||||
|
||||
### ⛔ Ban
|
||||
Where a participant is repeatedly violating the standards, or where their behaviour is significantly disruptive, members may:
|
||||
|
||||
If your message is hateful, very disruptive, or other, less serious infractions are repeated
|
||||
ignoring previous punishments, a member might ban you permanently.
|
||||
1. Temporarily mute the participant; or
|
||||
2. Temporarily suspend or ban the participant from the community.
|
||||
|
||||
## 🔎 Scope
|
||||
**4. Permanent Ban**
|
||||
|
||||
This CoC shall apply to all projects ran under the Orange++ lead and all _official_ communities
|
||||
outside of GitHub.
|
||||
Where a message is hateful or severely disruptive, or where less serious infractions are repeated despite prior measures, members may permanently ban the participant.
|
||||
|
||||
However, it is worth noting that official communities outside of GitHub might have their own,
|
||||
additional sets of rules.
|
||||
Each case shall be considered individually. The final decision regarding the appropriate measure lies with the members responsible for enforcement.
|
||||
|
||||
---
|
||||
|
||||
## Article 9
|
||||
|
||||
Reports of misconduct and information regarding enforcement actions shall be handled with care and confidentiality.
|
||||
|
||||
The personal data of reporters, witnesses, and involved parties shall not be disclosed to third parties, except:
|
||||
|
||||
1. Where such disclosure is required by law; or
|
||||
2. Where explicit consent has been given by the person concerned.
|
||||
|
||||
The maintainer guarantees that every report will be treated with discretion and respect.
|
||||
|
||||
---
|
||||
|
||||
## Article 10
|
||||
|
||||
This Code of Conduct applies to:
|
||||
|
||||
1. All projects led under the Orange++ name or leadership; and
|
||||
2. All official communities associated with Orange++ outside of GitHub.
|
||||
|
||||
Official communities outside of GitHub may maintain additional rules specific to their platform. In case of overlap, participants are expected to follow:
|
||||
|
||||
1. The rules of the platform or community; and
|
||||
2. This Code of Conduct, insofar as it is applicable.
|
||||
|
||||
---
|
||||
|
||||
## Article 11
|
||||
|
||||
This Code of Conduct shall be interpreted in a manner consistent with its purpose: to promote a safe, respectful and inclusive community.
|
||||
|
||||
The maintainer and core contributors may review and revise this Code of Conduct periodically in light of community needs and experience.
|
||||
|
||||
Significant changes should be communicated to the community in a timely and clear manner.
|
||||
|
||||
---
|
||||
|
||||
## Article 12
|
||||
|
||||
Nothing in this Community Code of Conduct may be interpreted as granting to any maintainer, member, contributor, or participant any right to engage in any activity or to perform any act that aims at undermining, limiting, or destroying the rights, protections, and standards set forth herein.
|
||||
|
||||
No rule, policy, custom, or decision within the Orange++ projects or their official communities may be invoked to justify harassment, discrimination, retaliation, or any other conduct contrary to this Code of Conduct.
|
||||
|
||||
17
README.md
17
README.md
@@ -106,6 +106,10 @@ if (auto screen = camera.world_to_screen(world_position)) {
|
||||
|
||||
![TF2 Preview]
|
||||
|
||||
<br>
|
||||
|
||||
![OpenGL Preview]
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
@@ -113,12 +117,12 @@ if (auto screen = camera.world_to_screen(world_position)) {
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **[Getting Started Guide](https://libomath.org/getting_started/)** - Installation and first steps
|
||||
- **[API Overview](https://libomath.org/api_overview/)** - Complete API reference
|
||||
- **[Tutorials](https://libomath.org/tutorials/)** - Step-by-step guides
|
||||
- **[FAQ](https://libomath.org/faq/)** - Common questions and answers
|
||||
- **[Troubleshooting](https://libomath.org/troubleshooting/)** - Solutions to common issues
|
||||
- **[Best Practices](https://libomath.org/best_practices/)** - Guidelines for effective usage
|
||||
- **[Getting Started Guide](http://libomath.org/getting_started/)** - Installation and first steps
|
||||
- **[API Overview](http://libomath.org/api_overview/)** - Complete API reference
|
||||
- **[Tutorials](http://libomath.org/tutorials/)** - Step-by-step guides
|
||||
- **[FAQ](http://libomath.org/faq/)** - Common questions and answers
|
||||
- **[Troubleshooting](http://libomath.org/troubleshooting/)** - Solutions to common issues
|
||||
- **[Best Practices](http://libomath.org/best_practices/)** - Guidelines for effective usage
|
||||
|
||||
## 🤝 Community & Support
|
||||
|
||||
@@ -135,6 +139,7 @@ if (auto screen = camera.world_to_screen(world_position)) {
|
||||
[BO2 Preview]: docs/images/showcase/cod_bo2.png
|
||||
[CS2 Preview]: docs/images/showcase/cs2.jpeg
|
||||
[TF2 Preview]: docs/images/showcase/tf2.jpg
|
||||
[OpenGL Preview]: docs/images/showcase/opengl.png
|
||||
<!----------------------------------{ Buttons }--------------------------------->
|
||||
[QUICKSTART]: docs/getting_started.md
|
||||
[INSTALL]: INSTALL.md
|
||||
|
||||
465
docs/3d_primitives/mesh.md
Normal file
465
docs/3d_primitives/mesh.md
Normal file
@@ -0,0 +1,465 @@
|
||||
# `omath::primitives::Mesh` — 3D mesh with transformation support
|
||||
|
||||
> Header: `omath/3d_primitives/mesh.hpp`
|
||||
> Namespace: `omath::primitives`
|
||||
> Depends on: `omath::Vector3<T>`, `omath::Mat4X4`, `omath::Triangle<Vector3<T>>`
|
||||
> Purpose: represent and transform 3D meshes in different engine coordinate systems
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`Mesh` represents a 3D polygonal mesh with vertex data and transformation capabilities. It stores:
|
||||
* **Vertex buffer (VBO)** — array of 3D vertex positions
|
||||
* **Index buffer (VAO)** — array of triangular faces (indices into VBO)
|
||||
* **Transformation** — position, rotation, and scale with caching
|
||||
|
||||
The mesh supports transformation from local space to world space using engine-specific coordinate systems through the `MeshTrait` template parameter.
|
||||
|
||||
---
|
||||
|
||||
## Template Declaration
|
||||
|
||||
```cpp
|
||||
template<class Mat4X4, class RotationAngles, class MeshTypeTrait, class Type = float>
|
||||
class Mesh final;
|
||||
```
|
||||
|
||||
### Template Parameters
|
||||
|
||||
* `Mat4X4` — Matrix type for transformations (typically `omath::Mat4X4`)
|
||||
* `RotationAngles` — Rotation representation (e.g., `ViewAngles` with pitch/yaw/roll)
|
||||
* `MeshTypeTrait` — Engine-specific transformation trait (see [Engine Traits](#engine-traits))
|
||||
* `Type` — Scalar type for vertex coordinates (default `float`)
|
||||
|
||||
---
|
||||
|
||||
## Type Aliases
|
||||
|
||||
```cpp
|
||||
using NumericType = Type;
|
||||
```
|
||||
|
||||
Common engine-specific aliases:
|
||||
|
||||
```cpp
|
||||
// Source Engine
|
||||
using Mesh = omath::primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
|
||||
// Unity Engine
|
||||
using Mesh = omath::primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
|
||||
// Unreal Engine
|
||||
using Mesh = omath::primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
|
||||
// Frostbite, IW Engine, OpenGL similar...
|
||||
```
|
||||
|
||||
Use the pre-defined type aliases in engine namespaces:
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
Mesh my_mesh = /* ... */; // Uses SourceEngine::Mesh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Members
|
||||
|
||||
### Vertex Data
|
||||
|
||||
```cpp
|
||||
std::vector<Vector3<NumericType>> m_vertex_buffer; // VBO: vertex positions
|
||||
std::vector<Vector3<std::size_t>> m_vertex_array_object; // VAO: face indices
|
||||
```
|
||||
|
||||
* `m_vertex_buffer` — array of vertex positions in **local space**
|
||||
* `m_vertex_array_object` — array of triangular faces, each containing 3 indices into `m_vertex_buffer`
|
||||
|
||||
**Public access**: These members are public for direct manipulation when needed.
|
||||
|
||||
---
|
||||
|
||||
## Constructor
|
||||
|
||||
```cpp
|
||||
Mesh(std::vector<Vector3<NumericType>> vbo,
|
||||
std::vector<Vector3<std::size_t>> vao,
|
||||
Vector3<NumericType> scale = {1, 1, 1});
|
||||
```
|
||||
|
||||
Creates a mesh from vertex and index data.
|
||||
|
||||
**Parameters**:
|
||||
* `vbo` — vertex buffer (moved into mesh)
|
||||
* `vao` — index buffer / vertex array object (moved into mesh)
|
||||
* `scale` — initial scale (default `{1, 1, 1}`)
|
||||
|
||||
**Example**:
|
||||
```cpp
|
||||
std::vector<Vector3<float>> vertices = {
|
||||
{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}
|
||||
};
|
||||
|
||||
std::vector<Vector3<std::size_t>> faces = {
|
||||
{0, 1, 2}, // Triangle 1
|
||||
{0, 1, 3}, // Triangle 2
|
||||
{0, 2, 3}, // Triangle 3
|
||||
{1, 2, 3} // Triangle 4
|
||||
};
|
||||
|
||||
using namespace omath::source_engine;
|
||||
Mesh tetrahedron(std::move(vertices), std::move(faces));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transformation Methods
|
||||
|
||||
### Setting Transform Components
|
||||
|
||||
```cpp
|
||||
void set_origin(const Vector3<NumericType>& new_origin);
|
||||
void set_scale(const Vector3<NumericType>& new_scale);
|
||||
void set_rotation(const RotationAngles& new_rotation_angles);
|
||||
```
|
||||
|
||||
Update the mesh's transformation. **Side effect**: invalidates the cached transformation matrix, which will be recomputed on the next `get_to_world_matrix()` call.
|
||||
|
||||
**Example**:
|
||||
```cpp
|
||||
mesh.set_origin({10, 0, 5});
|
||||
mesh.set_scale({2, 2, 2});
|
||||
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(45.0f);
|
||||
angles.yaw = YawAngle::from_degrees(30.0f);
|
||||
mesh.set_rotation(angles);
|
||||
```
|
||||
|
||||
### Getting Transform Components
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] const Vector3<NumericType>& get_origin() const;
|
||||
[[nodiscard]] const Vector3<NumericType>& get_scale() const;
|
||||
[[nodiscard]] const RotationAngles& get_rotation_angles() const;
|
||||
```
|
||||
|
||||
Retrieve current transformation components.
|
||||
|
||||
### Transformation Matrix
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] const Mat4X4& get_to_world_matrix() const;
|
||||
```
|
||||
|
||||
Returns the cached local-to-world transformation matrix. The matrix is computed lazily on first access after any transformation change:
|
||||
|
||||
```
|
||||
M = Translation(origin) × Scale(scale) × Rotation(angles)
|
||||
```
|
||||
|
||||
The rotation matrix is computed using the engine-specific `MeshTrait::rotation_matrix()` method.
|
||||
|
||||
**Caching**: The matrix is stored in a `mutable std::optional` and recomputed only when invalidated by `set_*` methods.
|
||||
|
||||
---
|
||||
|
||||
## Vertex Transformation
|
||||
|
||||
### `vertex_to_world_space`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]]
|
||||
Vector3<float> vertex_to_world_space(const Vector3<float>& vertex) const;
|
||||
```
|
||||
|
||||
Transforms a vertex from local space to world space by multiplying with the transformation matrix.
|
||||
|
||||
**Algorithm**:
|
||||
1. Convert vertex to column matrix: `[x, y, z, 1]ᵀ`
|
||||
2. Multiply by transformation matrix: `M × vertex`
|
||||
3. Extract the resulting 3D position
|
||||
|
||||
**Usage**:
|
||||
```cpp
|
||||
Vector3<float> local_vertex{1, 0, 0};
|
||||
Vector3<float> world_vertex = mesh.vertex_to_world_space(local_vertex);
|
||||
```
|
||||
|
||||
**Note**: This is used internally by `MeshCollider` to provide world-space support functions for GJK/EPA.
|
||||
|
||||
---
|
||||
|
||||
## Face Transformation
|
||||
|
||||
### `make_face_in_world_space`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]]
|
||||
Triangle<Vector3<float>> make_face_in_world_space(
|
||||
const std::vector<Vector3<std::size_t>>::const_iterator vao_iterator
|
||||
) const;
|
||||
```
|
||||
|
||||
Creates a triangle in world space from a face index iterator.
|
||||
|
||||
**Parameters**:
|
||||
* `vao_iterator` — iterator to an element in `m_vertex_array_object`
|
||||
|
||||
**Returns**: `Triangle` with all three vertices transformed to world space.
|
||||
|
||||
**Example**:
|
||||
```cpp
|
||||
for (auto it = mesh.m_vertex_array_object.begin();
|
||||
it != mesh.m_vertex_array_object.end();
|
||||
++it) {
|
||||
Triangle<Vector3<float>> world_triangle = mesh.make_face_in_world_space(it);
|
||||
// Render or process the triangle
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating a Box Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
|
||||
std::vector<Vector3<float>> box_vbo = {
|
||||
// Bottom face
|
||||
{-0.5f, -0.5f, 0.0f}, { 0.5f, -0.5f, 0.0f},
|
||||
{ 0.5f, 0.5f, 0.0f}, {-0.5f, 0.5f, 0.0f},
|
||||
// Top face
|
||||
{-0.5f, -0.5f, 1.0f}, { 0.5f, -0.5f, 1.0f},
|
||||
{ 0.5f, 0.5f, 1.0f}, {-0.5f, 0.5f, 1.0f}
|
||||
};
|
||||
|
||||
std::vector<Vector3<std::size_t>> box_vao = {
|
||||
// Bottom
|
||||
{0, 1, 2}, {0, 2, 3},
|
||||
// Top
|
||||
{4, 6, 5}, {4, 7, 6},
|
||||
// Sides
|
||||
{0, 4, 5}, {0, 5, 1},
|
||||
{1, 5, 6}, {1, 6, 2},
|
||||
{2, 6, 7}, {2, 7, 3},
|
||||
{3, 7, 4}, {3, 4, 0}
|
||||
};
|
||||
|
||||
Mesh box(std::move(box_vbo), std::move(box_vao));
|
||||
box.set_origin({0, 0, 50});
|
||||
box.set_scale({10, 10, 10});
|
||||
```
|
||||
|
||||
### Transforming Mesh Over Time
|
||||
|
||||
```cpp
|
||||
void update_mesh(Mesh& mesh, float delta_time) {
|
||||
// Rotate mesh
|
||||
auto rotation = mesh.get_rotation_angles();
|
||||
rotation.yaw = YawAngle::from_degrees(
|
||||
rotation.yaw.as_degrees() + 45.0f * delta_time
|
||||
);
|
||||
mesh.set_rotation(rotation);
|
||||
|
||||
// Oscillate position
|
||||
auto origin = mesh.get_origin();
|
||||
origin.z = 50.0f + 10.0f * std::sin(current_time * 2.0f);
|
||||
mesh.set_origin(origin);
|
||||
}
|
||||
```
|
||||
|
||||
### Collision Detection
|
||||
|
||||
```cpp
|
||||
using namespace omath::collision;
|
||||
using namespace omath::source_engine;
|
||||
|
||||
Mesh mesh_a(vbo_a, vao_a);
|
||||
mesh_a.set_origin({0, 0, 0});
|
||||
|
||||
Mesh mesh_b(vbo_b, vao_b);
|
||||
mesh_b.set_origin({5, 0, 0});
|
||||
|
||||
MeshCollider collider_a(std::move(mesh_a));
|
||||
MeshCollider collider_b(std::move(mesh_b));
|
||||
|
||||
auto result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||
collider_a, collider_b
|
||||
);
|
||||
```
|
||||
|
||||
### Rendering Transformed Triangles
|
||||
|
||||
```cpp
|
||||
void render_mesh(const Mesh& mesh) {
|
||||
for (auto it = mesh.m_vertex_array_object.begin();
|
||||
it != mesh.m_vertex_array_object.end();
|
||||
++it) {
|
||||
|
||||
Triangle<Vector3<float>> tri = mesh.make_face_in_world_space(it);
|
||||
|
||||
// Draw triangle with your renderer
|
||||
draw_triangle(tri.m_vertex1, tri.m_vertex2, tri.m_vertex3);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Engine Traits
|
||||
|
||||
Each game engine has a corresponding `MeshTrait` that provides the `rotation_matrix` function:
|
||||
|
||||
```cpp
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
```
|
||||
|
||||
### Available Engines
|
||||
|
||||
| Engine | Namespace | Header |
|
||||
|--------|-----------|--------|
|
||||
| Source Engine | `omath::source_engine` | `engines/source_engine/mesh.hpp` |
|
||||
| Unity | `omath::unity_engine` | `engines/unity_engine/mesh.hpp` |
|
||||
| Unreal | `omath::unreal_engine` | `engines/unreal_engine/mesh.hpp` |
|
||||
| Frostbite | `omath::frostbite_engine` | `engines/frostbite_engine/mesh.hpp` |
|
||||
| IW Engine | `omath::iw_engine` | `engines/iw_engine/mesh.hpp` |
|
||||
| OpenGL | `omath::opengl_engine` | `engines/opengl_engine/mesh.hpp` |
|
||||
|
||||
**Example** (Source Engine):
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// Uses source_engine::MeshTrait automatically
|
||||
Mesh my_mesh(vertices, indices);
|
||||
```
|
||||
|
||||
See [MeshTrait Documentation](#mesh-trait-documentation) for engine-specific details.
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Matrix Caching
|
||||
|
||||
The transformation matrix is computed lazily and cached:
|
||||
* **First access**: O(matrix multiply) ≈ 64 float operations
|
||||
* **Subsequent access**: O(1) — returns cached matrix
|
||||
* **Cache invalidation**: Any `set_*` call invalidates the cache
|
||||
|
||||
**Best practice**: Batch transformation updates before accessing the matrix:
|
||||
```cpp
|
||||
// Good: single matrix recomputation
|
||||
mesh.set_origin(new_origin);
|
||||
mesh.set_rotation(new_rotation);
|
||||
mesh.set_scale(new_scale);
|
||||
auto matrix = mesh.get_to_world_matrix(); // Computes once
|
||||
|
||||
// Bad: three matrix recomputations
|
||||
mesh.set_origin(new_origin);
|
||||
auto m1 = mesh.get_to_world_matrix(); // Compute
|
||||
mesh.set_rotation(new_rotation);
|
||||
auto m2 = mesh.get_to_world_matrix(); // Compute again
|
||||
mesh.set_scale(new_scale);
|
||||
auto m3 = mesh.get_to_world_matrix(); // Compute again
|
||||
```
|
||||
|
||||
### Memory Layout
|
||||
|
||||
* **VBO**: Contiguous `std::vector` for cache-friendly access
|
||||
* **VAO**: Contiguous indices for cache-friendly face iteration
|
||||
* **Matrix**: Cached in `std::optional` (no allocation)
|
||||
|
||||
### Transformation Cost
|
||||
|
||||
* `vertex_to_world_space`: ~15-20 FLOPs per vertex (4×4 matrix multiply)
|
||||
* `make_face_in_world_space`: ~60 FLOPs (3 vertices)
|
||||
|
||||
For high-frequency transformations, consider:
|
||||
* Caching transformed vertices if the mesh doesn't change
|
||||
* Using simpler proxy geometry for collision
|
||||
* Batching transformations
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System Details
|
||||
|
||||
Different engines use different coordinate systems:
|
||||
|
||||
| Engine | Up Axis | Forward Axis | Handedness |
|
||||
|--------|---------|--------------|------------|
|
||||
| Source | +Z | +Y | Right |
|
||||
| Unity | +Y | +Z | Left |
|
||||
| Unreal | +Z | +X | Left |
|
||||
| Frostbite | +Y | +Z | Right |
|
||||
| IW Engine | +Z | +Y | Right |
|
||||
| OpenGL | +Y | +Z | Right |
|
||||
|
||||
The `MeshTrait::rotation_matrix` function accounts for these differences, ensuring correct transformations in each engine's space.
|
||||
|
||||
---
|
||||
|
||||
## Limitations & Edge Cases
|
||||
|
||||
### Empty Mesh
|
||||
|
||||
A mesh with no vertices or faces is valid but not useful:
|
||||
```cpp
|
||||
Mesh empty_mesh({}, {}); // Valid but meaningless
|
||||
```
|
||||
|
||||
For collision detection, ensure `m_vertex_buffer` is non-empty.
|
||||
|
||||
### Index Validity
|
||||
|
||||
No bounds checking is performed on indices in `m_vertex_array_object`. Ensure all indices are valid:
|
||||
```cpp
|
||||
assert(face.x < mesh.m_vertex_buffer.size());
|
||||
assert(face.y < mesh.m_vertex_buffer.size());
|
||||
assert(face.z < mesh.m_vertex_buffer.size());
|
||||
```
|
||||
|
||||
### Degenerate Triangles
|
||||
|
||||
Faces with duplicate indices or collinear vertices will produce degenerate triangles. The mesh doesn't validate this; users must ensure clean geometry.
|
||||
|
||||
### Thread Safety
|
||||
|
||||
* **Read-only**: Safe to read from multiple threads (including const methods)
|
||||
* **Modification**: Not thread-safe; synchronize `set_*` calls externally
|
||||
* **Matrix cache**: Uses `mutable` member; not thread-safe even for const methods
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [MeshCollider Documentation](../collision/mesh_collider.md) - Collision wrapper for meshes
|
||||
- [GJK Algorithm Documentation](../collision/gjk_algorithm.md) - Uses mesh for collision detection
|
||||
- [EPA Algorithm Documentation](../collision/epa_algorithm.md) - Penetration depth with meshes
|
||||
- [Triangle Documentation](../linear_algebra/triangle.md) - Triangle primitive
|
||||
- [Mat4X4 Documentation](../linear_algebra/mat.md) - Transformation matrices
|
||||
- [Box Documentation](box.md) - Box primitive
|
||||
- [Plane Documentation](plane.md) - Plane primitive
|
||||
|
||||
---
|
||||
|
||||
## Mesh Trait Documentation
|
||||
|
||||
For engine-specific `MeshTrait` details, see:
|
||||
|
||||
- [Source Engine MeshTrait](../engines/source_engine/mesh_trait.md)
|
||||
- [Unity Engine MeshTrait](../engines/unity_engine/mesh_trait.md)
|
||||
- [Unreal Engine MeshTrait](../engines/unreal_engine/mesh_trait.md)
|
||||
- [Frostbite Engine MeshTrait](../engines/frostbite/mesh_trait.md)
|
||||
- [IW Engine MeshTrait](../engines/iw_engine/mesh_trait.md)
|
||||
- [OpenGL Engine MeshTrait](../engines/opengl_engine/mesh_trait.md)
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
@@ -11,10 +11,10 @@ OMath is organized into several logical modules:
|
||||
### Core Mathematics
|
||||
- **Linear Algebra** - Vectors, matrices, triangles
|
||||
- **Trigonometry** - Angles, view angles, trigonometric functions
|
||||
- **3D Primitives** - Boxes, planes, geometric shapes
|
||||
- **3D Primitives** - Boxes, planes, meshes, geometric shapes
|
||||
|
||||
### Game Development
|
||||
- **Collision Detection** - Ray tracing, intersection tests
|
||||
- **Collision Detection** - Ray tracing, GJK/EPA algorithms, mesh collision, intersection tests
|
||||
- **Projectile Prediction** - Ballistics and aim-assist calculations
|
||||
- **Projection** - Camera systems and world-to-screen transformations
|
||||
- **Pathfinding** - A* algorithm, navigation meshes
|
||||
@@ -131,6 +131,41 @@ omath::opengl_engine::Camera // OpenGL
|
||||
|
||||
## Collision Detection
|
||||
|
||||
### GJK/EPA Algorithms
|
||||
|
||||
Advanced convex shape collision detection using the Gilbert-Johnson-Keerthi and Expanding Polytope algorithms:
|
||||
|
||||
```cpp
|
||||
namespace omath::collision {
|
||||
template<class ColliderType>
|
||||
class GjkAlgorithm;
|
||||
|
||||
template<class ColliderType>
|
||||
class Epa;
|
||||
}
|
||||
```
|
||||
|
||||
**GJK (Gilbert-Johnson-Keerthi):**
|
||||
* Detects collision between two convex shapes
|
||||
* Returns a 4-point simplex when collision is detected
|
||||
* O(k) complexity where k is typically < 20 iterations
|
||||
* Works with any collider implementing `find_abs_furthest_vertex()`
|
||||
|
||||
**EPA (Expanding Polytope Algorithm):**
|
||||
* Computes penetration depth and separation normal
|
||||
* Takes GJK's output simplex as input
|
||||
* Provides contact information for physics simulation
|
||||
* Configurable iteration limit and convergence tolerance
|
||||
|
||||
**Supporting Types:**
|
||||
|
||||
| Type | Description | Key Features |
|
||||
|------|-------------|--------------|
|
||||
| `Simplex<VectorType>` | 1-4 point geometric simplex | Fixed capacity, GJK iteration support |
|
||||
| `MeshCollider<MeshType>` | Convex mesh collider | Support function for GJK/EPA |
|
||||
| `GjkHitInfo<VertexType>` | Collision result | Hit flag and simplex |
|
||||
| `Epa::Result` | Penetration info | Depth, normal, iteration count |
|
||||
|
||||
### LineTracer
|
||||
|
||||
Ray-casting and line tracing utilities:
|
||||
@@ -142,7 +177,7 @@ namespace omath::collision {
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Ray-triangle intersection
|
||||
- Ray-triangle intersection (Möller-Trumbore algorithm)
|
||||
- Ray-plane intersection
|
||||
- Ray-box intersection
|
||||
- Distance calculations
|
||||
@@ -154,6 +189,14 @@ namespace omath::collision {
|
||||
|------|-------------|-------------|
|
||||
| `Plane` | Infinite plane | `intersects_ray()`, `distance_to_point()` |
|
||||
| `Box` | Axis-aligned bounding box | `contains()`, `intersects()` |
|
||||
| `Mesh` | Polygonal mesh with transforms | `vertex_to_world_space()`, `make_face_in_world_space()` |
|
||||
|
||||
**Mesh Features:**
|
||||
* Vertex buffer (VBO) and index buffer (VAO/EBO) storage
|
||||
* Position, rotation, and scale transformations
|
||||
* Cached transformation matrix
|
||||
* Engine-specific coordinate system support
|
||||
* Compatible with `MeshCollider` for collision detection
|
||||
|
||||
---
|
||||
|
||||
@@ -241,6 +284,13 @@ Implements camera math for an engine:
|
||||
- `calc_view_matrix()` - Build view matrix from angles and position
|
||||
- `calc_projection_matrix()` - Build projection matrix from FOV and viewport
|
||||
|
||||
### MeshTrait
|
||||
|
||||
Provides mesh transformation for an engine:
|
||||
- `rotation_matrix()` - Build rotation matrix from engine-specific angles
|
||||
- Handles coordinate system differences (Y-up vs Z-up, left/right-handed)
|
||||
- Used by `Mesh` class for local-to-world transformations
|
||||
|
||||
### PredEngineTrait
|
||||
|
||||
Provides physics/ballistics specific to an engine:
|
||||
@@ -251,18 +301,18 @@ Provides physics/ballistics specific to an engine:
|
||||
|
||||
### Available Traits
|
||||
|
||||
| Engine | Camera Trait | Pred Engine Trait | Constants | Formulas |
|
||||
|--------|--------------|-------------------|-----------|----------|
|
||||
| Source Engine | ✓ | ✓ | ✓ | ✓ |
|
||||
| Unity Engine | ✓ | ✓ | ✓ | ✓ |
|
||||
| Unreal Engine | ✓ | ✓ | ✓ | ✓ |
|
||||
| Frostbite | ✓ | ✓ | ✓ | ✓ |
|
||||
| IW Engine | ✓ | ✓ | ✓ | ✓ |
|
||||
| OpenGL | ✓ | ✓ | ✓ | ✓ |
|
||||
| Engine | Camera Trait | Mesh Trait | Pred Engine Trait | Constants | Formulas |
|
||||
|--------|--------------|------------|-------------------|-----------|----------|
|
||||
| Source Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Unity Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Unreal Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Frostbite | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| IW Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| OpenGL | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
**Documentation:**
|
||||
- See `docs/engines/<engine_name>/` for detailed per-engine docs
|
||||
- Each engine has separate docs for camera_trait, pred_engine_trait, constants, and formulas
|
||||
- Each engine has separate docs for camera_trait, mesh_trait, pred_engine_trait, constants, and formulas
|
||||
|
||||
---
|
||||
|
||||
@@ -524,4 +574,4 @@ UnityCamera camera{pos, SourceAngles{}}; // Wrong!
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
*Last updated: 13 Nov 2025*
|
||||
|
||||
322
docs/collision/epa_algorithm.md
Normal file
322
docs/collision/epa_algorithm.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# `omath::collision::Epa` — Expanding Polytope Algorithm for penetration depth
|
||||
|
||||
> Header: `omath/collision/epa_algorithm.hpp`
|
||||
> Namespace: `omath::collision`
|
||||
> Depends on: `Simplex<VertexType>`, collider types with `find_abs_furthest_vertex` method
|
||||
> Algorithm: **EPA** (Expanding Polytope Algorithm) for penetration depth and contact normal
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The **EPA (Expanding Polytope Algorithm)** calculates the **penetration depth** and **separation normal** between two intersecting convex shapes. It is typically used as a follow-up to the GJK algorithm after a collision has been detected.
|
||||
|
||||
EPA takes a 4-point simplex containing the origin (from GJK) and iteratively expands it to find the point on the Minkowski difference closest to the origin. This point gives both:
|
||||
* **Depth**: minimum translation distance to separate the shapes
|
||||
* **Normal**: direction of separation (pointing from shape B to shape A)
|
||||
|
||||
`Epa` is a template class working with any collider type that implements the support function interface.
|
||||
|
||||
---
|
||||
|
||||
## `Epa::Result`
|
||||
|
||||
```cpp
|
||||
struct Result final {
|
||||
bool success{false}; // true if EPA converged
|
||||
Vertex normal{}; // outward normal (from B to A)
|
||||
float depth{0.0f}; // penetration depth
|
||||
int iterations{0}; // number of iterations performed
|
||||
int num_vertices{0}; // final polytope vertex count
|
||||
int num_faces{0}; // final polytope face count
|
||||
};
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
* `success` — `true` if EPA successfully computed depth and normal; `false` if it failed to converge
|
||||
* `normal` — unit vector pointing from shape B toward shape A (separation direction)
|
||||
* `depth` — minimum distance to move shape A along `normal` to separate the shapes
|
||||
* `iterations` — actual iteration count (useful for performance tuning)
|
||||
* `num_vertices`, `num_faces` — final polytope size (for diagnostics)
|
||||
|
||||
---
|
||||
|
||||
## `Epa::Params`
|
||||
|
||||
```cpp
|
||||
struct Params final {
|
||||
int max_iterations{64}; // maximum iterations before giving up
|
||||
float tolerance{1e-4f}; // absolute tolerance on distance growth
|
||||
};
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
* `max_iterations` — safety limit to prevent infinite loops (default 64)
|
||||
* `tolerance` — convergence threshold: stop when distance grows less than this (default 1e-4)
|
||||
|
||||
---
|
||||
|
||||
## `Epa` Template Class
|
||||
|
||||
```cpp
|
||||
template<class ColliderType>
|
||||
class Epa final {
|
||||
public:
|
||||
using Vertex = typename ColliderType::VertexType;
|
||||
static_assert(EpaVector<Vertex>, "VertexType must satisfy EpaVector concept");
|
||||
|
||||
// Solve for penetration depth and normal
|
||||
[[nodiscard]]
|
||||
static Result solve(
|
||||
const ColliderType& a,
|
||||
const ColliderType& b,
|
||||
const Simplex<Vertex>& simplex,
|
||||
const Params params = {}
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Precondition
|
||||
|
||||
The `simplex` parameter must:
|
||||
* Have exactly 4 points (`simplex.size() == 4`)
|
||||
* Contain the origin (i.e., be a valid GJK result with `hit == true`)
|
||||
|
||||
Violating this precondition leads to undefined behavior.
|
||||
|
||||
---
|
||||
|
||||
## Collider Requirements
|
||||
|
||||
Any type used as `ColliderType` must provide:
|
||||
|
||||
```cpp
|
||||
// Type alias for vertex type (typically Vector3<float>)
|
||||
using VertexType = /* ... */;
|
||||
|
||||
// Find the farthest point in world space along the given direction
|
||||
[[nodiscard]]
|
||||
VertexType find_abs_furthest_vertex(const VertexType& direction) const;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Algorithm Details
|
||||
|
||||
### Expanding Polytope
|
||||
|
||||
EPA maintains a convex polytope (polyhedron) in Minkowski difference space `A - B`. Starting from the 4-point tetrahedron (simplex from GJK), it repeatedly:
|
||||
|
||||
1. **Find closest face** to the origin
|
||||
2. **Support query** in the direction of the face normal
|
||||
3. **Expand polytope** by adding the new support point
|
||||
4. **Update faces** to maintain convexity
|
||||
|
||||
The algorithm terminates when:
|
||||
* **Convergence**: the distance from origin to polytope stops growing (within tolerance)
|
||||
* **Max iterations**: safety limit reached
|
||||
* **Failure cases**: degenerate polytope or numerical issues
|
||||
|
||||
### Minkowski Difference
|
||||
|
||||
Like GJK, EPA operates in Minkowski difference space where `point = a - b` for points in shapes A and B. The closest point on this polytope to the origin gives the minimum separation.
|
||||
|
||||
### Face Winding
|
||||
|
||||
Faces are stored with outward-pointing normals. The algorithm uses a priority queue to efficiently find the face closest to the origin.
|
||||
|
||||
---
|
||||
|
||||
## Vertex Type Requirements
|
||||
|
||||
The `VertexType` must satisfy the `EpaVector` concept:
|
||||
|
||||
```cpp
|
||||
template<class V>
|
||||
concept EpaVector = requires(const V& a, const V& b, float s) {
|
||||
{ a - b } -> std::same_as<V>;
|
||||
{ a.cross(b) } -> std::same_as<V>;
|
||||
{ a.dot(b) } -> std::same_as<float>;
|
||||
{ -a } -> std::same_as<V>;
|
||||
{ a * s } -> std::same_as<V>;
|
||||
{ a / s } -> std::same_as<V>;
|
||||
};
|
||||
```
|
||||
|
||||
`omath::Vector3<float>` satisfies this concept.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic EPA Usage
|
||||
|
||||
```cpp
|
||||
using namespace omath::collision;
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// First, run GJK to detect collision
|
||||
MeshCollider<Mesh> collider_a(mesh_a);
|
||||
MeshCollider<Mesh> collider_b(mesh_b);
|
||||
|
||||
auto gjk_result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||
collider_a,
|
||||
collider_b
|
||||
);
|
||||
|
||||
if (gjk_result.hit) {
|
||||
// Collision detected, use EPA to get penetration info
|
||||
auto epa_result = Epa<MeshCollider<Mesh>>::solve(
|
||||
collider_a,
|
||||
collider_b,
|
||||
gjk_result.simplex
|
||||
);
|
||||
|
||||
if (epa_result.success) {
|
||||
std::cout << "Penetration depth: " << epa_result.depth << "\n";
|
||||
std::cout << "Separation normal: "
|
||||
<< "(" << epa_result.normal.x << ", "
|
||||
<< epa_result.normal.y << ", "
|
||||
<< epa_result.normal.z << ")\n";
|
||||
|
||||
// Apply separation: move A away from B
|
||||
Vector3<float> correction = epa_result.normal * epa_result.depth;
|
||||
mesh_a.set_origin(mesh_a.get_origin() + correction);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Parameters
|
||||
|
||||
```cpp
|
||||
// Use custom convergence settings
|
||||
Epa<Collider>::Params params;
|
||||
params.max_iterations = 128; // Allow more iterations for complex shapes
|
||||
params.tolerance = 1e-5f; // Tighter tolerance for more accuracy
|
||||
|
||||
auto result = Epa<Collider>::solve(a, b, simplex, params);
|
||||
```
|
||||
|
||||
### Physics Integration
|
||||
|
||||
```cpp
|
||||
void resolve_collision(PhysicsBody& body_a, PhysicsBody& body_b) {
|
||||
auto gjk_result = GjkAlgorithm<Collider>::check_collision(
|
||||
body_a.collider, body_b.collider
|
||||
);
|
||||
|
||||
if (!gjk_result.hit)
|
||||
return; // No collision
|
||||
|
||||
auto epa_result = Epa<Collider>::solve(
|
||||
body_a.collider,
|
||||
body_b.collider,
|
||||
gjk_result.simplex
|
||||
);
|
||||
|
||||
if (epa_result.success) {
|
||||
// Separate bodies
|
||||
float mass_sum = body_a.mass + body_b.mass;
|
||||
float ratio_a = body_b.mass / mass_sum;
|
||||
float ratio_b = body_a.mass / mass_sum;
|
||||
|
||||
body_a.position += epa_result.normal * (epa_result.depth * ratio_a);
|
||||
body_b.position -= epa_result.normal * (epa_result.depth * ratio_b);
|
||||
|
||||
// Apply collision response
|
||||
apply_impulse(body_a, body_b, epa_result.normal);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
* **Time complexity**: O(k × f) where k is iterations and f is faces per iteration (typically f grows slowly)
|
||||
* **Space complexity**: O(n) where n is the number of polytope vertices (typically < 100)
|
||||
* **Typical iterations**: 4-20 for most collisions
|
||||
* **Worst case**: 64 iterations (configurable limit)
|
||||
|
||||
### Performance Tips
|
||||
|
||||
1. **Adjust max_iterations**: Balance accuracy vs. performance for your use case
|
||||
2. **Tolerance tuning**: Larger tolerance = faster convergence but less accurate
|
||||
3. **Shape complexity**: Simpler shapes (fewer faces) converge faster
|
||||
4. **Deep penetrations**: Require more iterations; consider broad-phase separation
|
||||
|
||||
---
|
||||
|
||||
## Limitations & Edge Cases
|
||||
|
||||
* **Requires valid simplex**: Must be called with a 4-point simplex containing the origin (from successful GJK)
|
||||
* **Convex shapes only**: Like GJK, EPA only works with convex colliders
|
||||
* **Convergence failure**: Can fail to converge for degenerate or very thin shapes (check `result.success`)
|
||||
* **Numerical precision**: Extreme scale differences or very small shapes may cause issues
|
||||
* **Deep penetration**: Very deep intersections may require many iterations or fail to converge
|
||||
|
||||
### Error Handling
|
||||
|
||||
```cpp
|
||||
auto result = Epa<Collider>::solve(a, b, simplex);
|
||||
|
||||
if (!result.success) {
|
||||
// EPA failed to converge
|
||||
// Fallback options:
|
||||
// 1. Use a default separation (e.g., axis between centers)
|
||||
// 2. Increase max_iterations and retry
|
||||
// 3. Log a warning and skip this collision
|
||||
std::cerr << "EPA failed after " << result.iterations << " iterations\n";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theory & Background
|
||||
|
||||
### Why EPA after GJK?
|
||||
|
||||
GJK determines **if** shapes intersect but doesn't compute penetration depth. EPA extends GJK's final simplex to find the exact depth and normal needed for:
|
||||
* **Collision response** — separating objects realistically
|
||||
* **Contact manifolds** — generating contact points for physics
|
||||
* **Constraint solving** — iterative physics solvers
|
||||
|
||||
### Comparison with SAT
|
||||
|
||||
| Feature | EPA | SAT (Separating Axis Theorem) |
|
||||
|---------|-----|-------------------------------|
|
||||
| Works with | Any convex shape | Polytopes (faces/edges) |
|
||||
| Penetration depth | Yes | Yes |
|
||||
| Complexity | Iterative | Per-axis projection |
|
||||
| Best for | General convex | Boxes, prisms |
|
||||
| Typical speed | Moderate | Fast (few axes) |
|
||||
|
||||
EPA is more general; SAT is faster for axis-aligned shapes.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The EPA implementation in OMath:
|
||||
* Uses a **priority queue** to efficiently find the closest face
|
||||
* Maintains face winding for consistent normals
|
||||
* Handles **edge cases**: degenerate faces, numerical instability
|
||||
* Prevents infinite loops with iteration limits
|
||||
* Returns detailed diagnostics (iteration count, polytope size)
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [GJK Algorithm Documentation](gjk_algorithm.md) - Collision detection (required before EPA)
|
||||
- [Simplex Documentation](simplex.md) - Input simplex structure
|
||||
- [MeshCollider Documentation](mesh_collider.md) - Mesh-based collider
|
||||
- [Mesh Documentation](../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
|
||||
- [API Overview](../api_overview.md) - High-level API reference
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
216
docs/collision/gjk_algorithm.md
Normal file
216
docs/collision/gjk_algorithm.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# `omath::collision::GjkAlgorithm` — Gilbert-Johnson-Keerthi collision detection
|
||||
|
||||
> Header: `omath/collision/gjk_algorithm.hpp`
|
||||
> Namespace: `omath::collision`
|
||||
> Depends on: `Simplex<VertexType>`, collider types with `find_abs_furthest_vertex` method
|
||||
> Algorithm: **GJK** (Gilbert-Johnson-Keerthi) for convex shape collision detection
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The **GJK algorithm** determines whether two convex shapes intersect by iteratively constructing a simplex in Minkowski difference space. The algorithm is widely used in physics engines and collision detection systems due to its efficiency and robustness.
|
||||
|
||||
`GjkAlgorithm` is a template class that works with any collider type implementing the required support function interface:
|
||||
|
||||
* `find_abs_furthest_vertex(direction)` — returns the farthest point in the collider along the given direction.
|
||||
|
||||
The algorithm returns a `GjkHitInfo` containing:
|
||||
* `hit` — boolean indicating whether the shapes intersect
|
||||
* `simplex` — a 4-point simplex containing the origin (valid only when `hit == true`)
|
||||
|
||||
---
|
||||
|
||||
## `GjkHitInfo`
|
||||
|
||||
```cpp
|
||||
template<class VertexType>
|
||||
struct GjkHitInfo final {
|
||||
bool hit{false}; // true if collision detected
|
||||
Simplex<VertexType> simplex; // 4-point simplex (valid only if hit == true)
|
||||
};
|
||||
```
|
||||
|
||||
The `simplex` field is only meaningful when `hit == true` and contains 4 points. This simplex can be passed to the EPA algorithm for penetration depth calculation.
|
||||
|
||||
---
|
||||
|
||||
## `GjkAlgorithm`
|
||||
|
||||
```cpp
|
||||
template<class ColliderType>
|
||||
class GjkAlgorithm final {
|
||||
using VertexType = typename ColliderType::VertexType;
|
||||
|
||||
public:
|
||||
// Find support vertex in Minkowski difference
|
||||
[[nodiscard]]
|
||||
static VertexType find_support_vertex(
|
||||
const ColliderType& collider_a,
|
||||
const ColliderType& collider_b,
|
||||
const VertexType& direction
|
||||
);
|
||||
|
||||
// Check if two convex shapes intersect
|
||||
[[nodiscard]]
|
||||
static GjkHitInfo<VertexType> check_collision(
|
||||
const ColliderType& collider_a,
|
||||
const ColliderType& collider_b
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collider Requirements
|
||||
|
||||
Any type used as `ColliderType` must provide:
|
||||
|
||||
```cpp
|
||||
// Type alias for vertex type (typically Vector3<float>)
|
||||
using VertexType = /* ... */;
|
||||
|
||||
// Find the farthest point in world space along the given direction
|
||||
[[nodiscard]]
|
||||
VertexType find_abs_furthest_vertex(const VertexType& direction) const;
|
||||
```
|
||||
|
||||
Common collider types:
|
||||
* `MeshCollider<MeshType>` — for arbitrary triangle meshes
|
||||
* Custom colliders for spheres, boxes, capsules, etc.
|
||||
|
||||
---
|
||||
|
||||
## Algorithm Details
|
||||
|
||||
### Minkowski Difference
|
||||
|
||||
GJK operates in the **Minkowski difference** space `A - B`, where a point in this space represents the difference between points in shapes A and B. The shapes intersect if and only if the origin lies within this Minkowski difference.
|
||||
|
||||
### Support Function
|
||||
|
||||
The support function finds the point in the Minkowski difference farthest along a given direction:
|
||||
|
||||
```cpp
|
||||
support(A, B, dir) = A.furthest(dir) - B.furthest(-dir)
|
||||
```
|
||||
|
||||
This is computed by `find_support_vertex`.
|
||||
|
||||
### Simplex Iteration
|
||||
|
||||
The algorithm builds a simplex incrementally:
|
||||
1. Start with an initial direction (typically vector between shape centers)
|
||||
2. Add support vertices in directions that move the simplex toward the origin
|
||||
3. Simplify the simplex to keep only points closest to the origin
|
||||
4. Repeat until either:
|
||||
* Origin is contained (collision detected, returns 4-point simplex)
|
||||
* No progress can be made (no collision)
|
||||
|
||||
Maximum 64 iterations are performed to prevent infinite loops in edge cases.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Collision Check
|
||||
|
||||
```cpp
|
||||
using namespace omath::collision;
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// Create mesh colliders
|
||||
Mesh mesh_a = /* ... */;
|
||||
Mesh mesh_b = /* ... */;
|
||||
|
||||
MeshCollider collider_a(mesh_a);
|
||||
MeshCollider collider_b(mesh_b);
|
||||
|
||||
// Check for collision
|
||||
auto result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||
collider_a,
|
||||
collider_b
|
||||
);
|
||||
|
||||
if (result.hit) {
|
||||
std::cout << "Collision detected!\n";
|
||||
// Can pass result.simplex to EPA for penetration depth
|
||||
}
|
||||
```
|
||||
|
||||
### Combined with EPA
|
||||
|
||||
```cpp
|
||||
auto gjk_result = GjkAlgorithm<Collider>::check_collision(a, b);
|
||||
|
||||
if (gjk_result.hit) {
|
||||
// Get penetration depth and normal using EPA
|
||||
auto epa_result = Epa<Collider>::solve(
|
||||
a, b, gjk_result.simplex
|
||||
);
|
||||
|
||||
if (epa_result.success) {
|
||||
std::cout << "Penetration depth: " << epa_result.depth << "\n";
|
||||
std::cout << "Separation normal: " << epa_result.normal << "\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
* **Time complexity**: O(k) where k is the number of iterations (typically < 20 for most cases)
|
||||
* **Space complexity**: O(1) — only stores a 4-point simplex
|
||||
* **Best case**: 4-8 iterations for well-separated objects
|
||||
* **Worst case**: 64 iterations (hard limit)
|
||||
* **Cache efficient**: operates on small fixed-size data structures
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
1. **Initial direction**: Use vector between shape centers for faster convergence
|
||||
2. **Early exit**: GJK quickly rejects non-intersecting shapes
|
||||
3. **Warm starting**: Reuse previous simplex for continuous collision detection
|
||||
4. **Broad phase**: Use spatial partitioning before GJK (AABB trees, grids)
|
||||
|
||||
---
|
||||
|
||||
## Limitations & Edge Cases
|
||||
|
||||
* **Convex shapes only**: GJK only works with convex colliders. For concave shapes, decompose into convex parts or use a mesh collider wrapper.
|
||||
* **Degenerate simplices**: The algorithm handles degenerate cases, but numerical precision can cause issues with very thin or flat shapes.
|
||||
* **Iteration limit**: Hard limit of 64 iterations prevents infinite loops but may miss collisions in extreme cases.
|
||||
* **Zero-length directions**: The simplex update logic guards against zero-length vectors, returning safe fallbacks.
|
||||
|
||||
---
|
||||
|
||||
## Vertex Type Requirements
|
||||
|
||||
The `VertexType` must satisfy the `GjkVector` concept (defined in `simplex.hpp`):
|
||||
|
||||
```cpp
|
||||
template<class V>
|
||||
concept GjkVector = requires(const V& a, const V& b) {
|
||||
{ -a } -> std::same_as<V>;
|
||||
{ a - b } -> std::same_as<V>;
|
||||
{ a.cross(b) } -> std::same_as<V>;
|
||||
{ a.point_to_same_direction(b) } -> std::same_as<bool>;
|
||||
};
|
||||
```
|
||||
|
||||
`omath::Vector3<float>` satisfies this concept.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [EPA Algorithm Documentation](epa_algorithm.md) - Penetration depth calculation
|
||||
- [Simplex Documentation](simplex.md) - Simplex data structure
|
||||
- [MeshCollider Documentation](mesh_collider.md) - Mesh-based collider
|
||||
- [Mesh Documentation](../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [LineTracer Documentation](line_tracer.md) - Ray-triangle intersection
|
||||
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
371
docs/collision/mesh_collider.md
Normal file
371
docs/collision/mesh_collider.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# `omath::collision::MeshCollider` — Convex hull collider for meshes
|
||||
|
||||
> Header: `omath/collision/mesh_collider.hpp`
|
||||
> Namespace: `omath::collision`
|
||||
> Depends on: `omath::primitives::Mesh`, `omath::Vector3<T>`
|
||||
> Purpose: wrap a mesh to provide collision detection support for GJK/EPA
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`MeshCollider` wraps a `Mesh` object to provide the **support function** interface required by the GJK and EPA collision detection algorithms. The support function finds the vertex of the mesh farthest along a given direction, which is essential for constructing Minkowski difference simplices.
|
||||
|
||||
**Important**: `MeshCollider` assumes the mesh represents a **convex hull**. For non-convex shapes, you must either:
|
||||
* Decompose into convex parts
|
||||
* Use the convex hull of the mesh
|
||||
* Use a different collision detection algorithm
|
||||
|
||||
---
|
||||
|
||||
## Template Declaration
|
||||
|
||||
```cpp
|
||||
template<class MeshType>
|
||||
class MeshCollider;
|
||||
```
|
||||
|
||||
### MeshType Requirements
|
||||
|
||||
The `MeshType` must be an instantiation of `omath::primitives::Mesh` or provide:
|
||||
|
||||
```cpp
|
||||
struct MeshType {
|
||||
using NumericType = /* float, double, etc. */;
|
||||
|
||||
std::vector<Vector3<NumericType>> m_vertex_buffer;
|
||||
|
||||
// Transform vertex from local to world space
|
||||
Vector3<NumericType> vertex_to_world_space(const Vector3<NumericType>&) const;
|
||||
};
|
||||
```
|
||||
|
||||
Common types:
|
||||
* `omath::source_engine::Mesh`
|
||||
* `omath::unity_engine::Mesh`
|
||||
* `omath::unreal_engine::Mesh`
|
||||
* `omath::frostbite_engine::Mesh`
|
||||
* `omath::iw_engine::Mesh`
|
||||
* `omath::opengl_engine::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Type Aliases
|
||||
|
||||
```cpp
|
||||
using NumericType = typename MeshType::NumericType;
|
||||
using VertexType = Vector3<NumericType>;
|
||||
```
|
||||
|
||||
* `NumericType` — scalar type (typically `float`)
|
||||
* `VertexType` — 3D vector type for vertices
|
||||
|
||||
---
|
||||
|
||||
## Constructor
|
||||
|
||||
```cpp
|
||||
explicit MeshCollider(MeshType mesh);
|
||||
```
|
||||
|
||||
Creates a collider from a mesh. The mesh is **moved** into the collider, so pass by value:
|
||||
|
||||
```cpp
|
||||
omath::source_engine::Mesh my_mesh = /* ... */;
|
||||
MeshCollider collider(std::move(my_mesh));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Methods
|
||||
|
||||
### `find_furthest_vertex`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]]
|
||||
const VertexType& find_furthest_vertex(const VertexType& direction) const;
|
||||
```
|
||||
|
||||
Finds the vertex in the mesh's **local space** that has the maximum dot product with `direction`.
|
||||
|
||||
**Algorithm**: Linear search through all vertices (O(n) where n is vertex count).
|
||||
|
||||
**Returns**: Const reference to the vertex in `m_vertex_buffer`.
|
||||
|
||||
---
|
||||
|
||||
### `find_abs_furthest_vertex`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]]
|
||||
VertexType find_abs_furthest_vertex(const VertexType& direction) const;
|
||||
```
|
||||
|
||||
Finds the vertex farthest along `direction` and transforms it to **world space**. This is the primary method used by GJK/EPA.
|
||||
|
||||
**Steps**:
|
||||
1. Find furthest vertex in local space using `find_furthest_vertex`
|
||||
2. Transform to world space using `mesh.vertex_to_world_space()`
|
||||
|
||||
**Returns**: Vertex position in world coordinates.
|
||||
|
||||
**Usage in GJK**:
|
||||
```cpp
|
||||
// GJK support function for Minkowski difference
|
||||
VertexType support = collider_a.find_abs_furthest_vertex(direction)
|
||||
- collider_b.find_abs_furthest_vertex(-direction);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Collision Detection
|
||||
|
||||
```cpp
|
||||
using namespace omath::collision;
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// Create meshes with vertex data
|
||||
std::vector<Vector3<float>> vbo_a = {
|
||||
{-1, -1, -1}, {1, -1, -1}, {1, 1, -1}, {-1, 1, -1},
|
||||
{-1, -1, 1}, {1, -1, 1}, {1, 1, 1}, {-1, 1, 1}
|
||||
};
|
||||
std::vector<Vector3<std::size_t>> vao_a = /* face indices */;
|
||||
|
||||
Mesh mesh_a(vbo_a, vao_a);
|
||||
mesh_a.set_origin({0, 0, 0});
|
||||
|
||||
Mesh mesh_b(vbo_b, vao_b);
|
||||
mesh_b.set_origin({5, 0, 0}); // Positioned away from mesh_a
|
||||
|
||||
// Wrap in colliders
|
||||
MeshCollider<Mesh> collider_a(std::move(mesh_a));
|
||||
MeshCollider<Mesh> collider_b(std::move(mesh_b));
|
||||
|
||||
// Run GJK
|
||||
auto result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||
collider_a, collider_b
|
||||
);
|
||||
|
||||
if (result.hit) {
|
||||
std::cout << "Collision detected!\n";
|
||||
}
|
||||
```
|
||||
|
||||
### With EPA for Penetration Depth
|
||||
|
||||
```cpp
|
||||
auto gjk_result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||
collider_a, collider_b
|
||||
);
|
||||
|
||||
if (gjk_result.hit) {
|
||||
auto epa_result = Epa<MeshCollider<Mesh>>::solve(
|
||||
collider_a, collider_b, gjk_result.simplex
|
||||
);
|
||||
|
||||
if (epa_result.success) {
|
||||
std::cout << "Penetration: " << epa_result.depth << " units\n";
|
||||
std::cout << "Normal: " << epa_result.normal << "\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Mesh Creation
|
||||
|
||||
```cpp
|
||||
// Create a simple box mesh
|
||||
std::vector<Vector3<float>> box_vertices = {
|
||||
{-0.5f, -0.5f, -0.5f}, { 0.5f, -0.5f, -0.5f},
|
||||
{ 0.5f, 0.5f, -0.5f}, {-0.5f, 0.5f, -0.5f},
|
||||
{-0.5f, -0.5f, 0.5f}, { 0.5f, -0.5f, 0.5f},
|
||||
{ 0.5f, 0.5f, 0.5f}, {-0.5f, 0.5f, 0.5f}
|
||||
};
|
||||
|
||||
std::vector<Vector3<std::size_t>> box_indices = {
|
||||
{0, 1, 2}, {0, 2, 3}, // Front face
|
||||
{4, 6, 5}, {4, 7, 6}, // Back face
|
||||
{0, 4, 5}, {0, 5, 1}, // Bottom face
|
||||
{2, 6, 7}, {2, 7, 3}, // Top face
|
||||
{0, 3, 7}, {0, 7, 4}, // Left face
|
||||
{1, 5, 6}, {1, 6, 2} // Right face
|
||||
};
|
||||
|
||||
using namespace omath::source_engine;
|
||||
Mesh box_mesh(box_vertices, box_indices);
|
||||
box_mesh.set_origin({10, 0, 0});
|
||||
box_mesh.set_scale({2, 2, 2});
|
||||
|
||||
MeshCollider<Mesh> box_collider(std::move(box_mesh));
|
||||
```
|
||||
|
||||
### Oriented Collision
|
||||
|
||||
```cpp
|
||||
// Create rotated mesh
|
||||
Mesh mesh(vertices, indices);
|
||||
mesh.set_origin({5, 5, 5});
|
||||
mesh.set_scale({1, 1, 1});
|
||||
|
||||
// Set rotation (engine-specific angles)
|
||||
ViewAngles rotation;
|
||||
rotation.pitch = PitchAngle::from_degrees(45.0f);
|
||||
rotation.yaw = YawAngle::from_degrees(30.0f);
|
||||
mesh.set_rotation(rotation);
|
||||
|
||||
// Collider automatically handles transformation
|
||||
MeshCollider<Mesh> collider(std::move(mesh));
|
||||
|
||||
// Support function returns world-space vertices
|
||||
auto support = collider.find_abs_furthest_vertex({0, 1, 0});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Linear Search
|
||||
|
||||
`find_furthest_vertex` performs a **linear search** through all vertices:
|
||||
* **Time complexity**: O(n) per support query
|
||||
* **GJK iterations**: ~10-20 support queries per collision test
|
||||
* **Total cost**: O(k × n) where k is GJK iterations
|
||||
|
||||
For meshes with many vertices (>1000), consider:
|
||||
* Using simpler proxy geometry (bounding box, convex hull with fewer vertices)
|
||||
* Pre-computing hierarchical structures
|
||||
* Using specialized collision shapes when possible
|
||||
|
||||
### Caching Opportunities
|
||||
|
||||
The implementation uses `std::ranges::max_element`, which is cache-friendly for contiguous vertex buffers. For optimal performance:
|
||||
* Store vertices contiguously in memory
|
||||
* Avoid pointer-based or scattered vertex storage
|
||||
* Consider SoA (Structure of Arrays) layout for SIMD optimization
|
||||
|
||||
### World Space Transformation
|
||||
|
||||
The `vertex_to_world_space` call involves matrix multiplication:
|
||||
* **Cost**: ~15-20 floating-point operations per vertex
|
||||
* **Optimization**: The mesh caches its transformation matrix
|
||||
* **Update cost**: Only recomputed when origin/rotation/scale changes
|
||||
|
||||
---
|
||||
|
||||
## Limitations & Edge Cases
|
||||
|
||||
### Convex Hull Requirement
|
||||
|
||||
**Critical**: GJK/EPA only work with **convex shapes**. If your mesh is concave:
|
||||
|
||||
#### Option 1: Convex Decomposition
|
||||
```cpp
|
||||
// Decompose concave mesh into convex parts
|
||||
std::vector<Mesh> convex_parts = decompose_mesh(concave_mesh);
|
||||
|
||||
for (const auto& part : convex_parts) {
|
||||
MeshCollider collider(part);
|
||||
// Test each part separately
|
||||
}
|
||||
```
|
||||
|
||||
#### Option 2: Use Convex Hull
|
||||
```cpp
|
||||
// Compute convex hull of vertices
|
||||
auto hull_vertices = compute_convex_hull(mesh.m_vertex_buffer);
|
||||
Mesh hull_mesh(hull_vertices, hull_indices);
|
||||
MeshCollider collider(std::move(hull_mesh));
|
||||
```
|
||||
|
||||
#### Option 3: Different Algorithm
|
||||
Use triangle-based collision (e.g., LineTracer) for true concave support.
|
||||
|
||||
### Empty Mesh
|
||||
|
||||
Behavior is undefined if `m_vertex_buffer` is empty. Always ensure:
|
||||
```cpp
|
||||
assert(!mesh.m_vertex_buffer.empty());
|
||||
MeshCollider collider(std::move(mesh));
|
||||
```
|
||||
|
||||
### Degenerate Meshes
|
||||
|
||||
* **Single vertex**: Treated as a point (degenerates to sphere collision)
|
||||
* **Two vertices**: Line segment (may cause GJK issues)
|
||||
* **Coplanar vertices**: Flat mesh; EPA may have convergence issues
|
||||
|
||||
**Recommendation**: Use at least 4 non-coplanar vertices for robustness.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate Systems
|
||||
|
||||
`MeshCollider` supports different engine coordinate systems through the `MeshTrait`:
|
||||
|
||||
| Engine | Up Axis | Handedness | Rotation Order |
|
||||
|--------|---------|------------|----------------|
|
||||
| Source Engine | Z | Right-handed | Pitch/Yaw/Roll |
|
||||
| Unity | Y | Left-handed | Pitch/Yaw/Roll |
|
||||
| Unreal | Z | Left-handed | Roll/Pitch/Yaw |
|
||||
| Frostbite | Y | Right-handed | Pitch/Yaw/Roll |
|
||||
| IW Engine | Z | Right-handed | Pitch/Yaw/Roll |
|
||||
| OpenGL | Y | Right-handed | Pitch/Yaw/Roll |
|
||||
|
||||
The `vertex_to_world_space` method handles these differences transparently.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Support Function
|
||||
|
||||
For specialized collision shapes, implement a custom collider:
|
||||
|
||||
```cpp
|
||||
class SphereCollider {
|
||||
public:
|
||||
using VertexType = Vector3<float>;
|
||||
|
||||
Vector3<float> center;
|
||||
float radius;
|
||||
|
||||
VertexType find_abs_furthest_vertex(const VertexType& direction) const {
|
||||
auto normalized = direction.normalized();
|
||||
return center + normalized * radius;
|
||||
}
|
||||
};
|
||||
|
||||
// Use with GJK/EPA
|
||||
auto result = GjkAlgorithm<SphereCollider>::check_collision(sphere_a, sphere_b);
|
||||
```
|
||||
|
||||
### Debugging Support Queries
|
||||
|
||||
```cpp
|
||||
class DebugMeshCollider : public MeshCollider<Mesh> {
|
||||
public:
|
||||
using MeshCollider::MeshCollider;
|
||||
|
||||
VertexType find_abs_furthest_vertex(const VertexType& direction) const {
|
||||
auto result = MeshCollider::find_abs_furthest_vertex(direction);
|
||||
std::cout << "Support query: direction=" << direction
|
||||
<< " -> vertex=" << result << "\n";
|
||||
return result;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [GJK Algorithm Documentation](gjk_algorithm.md) - Uses `MeshCollider` for collision detection
|
||||
- [EPA Algorithm Documentation](epa_algorithm.md) - Uses `MeshCollider` for penetration depth
|
||||
- [Simplex Documentation](simplex.md) - Data structure used by GJK
|
||||
- [Mesh Documentation](../3d_primitives/mesh.md) - Underlying mesh primitive
|
||||
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
327
docs/collision/simplex.md
Normal file
327
docs/collision/simplex.md
Normal file
@@ -0,0 +1,327 @@
|
||||
# `omath::collision::Simplex` — Fixed-capacity simplex for GJK/EPA
|
||||
|
||||
> Header: `omath/collision/simplex.hpp`
|
||||
> Namespace: `omath::collision`
|
||||
> Depends on: `Vector3<float>` (or any type satisfying `GjkVector` concept)
|
||||
> Purpose: store and manipulate simplices in GJK and EPA algorithms
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`Simplex` is a lightweight container for up to 4 points, used internally by the GJK and EPA collision detection algorithms. A simplex in this context is a geometric shape defined by 1 to 4 vertices:
|
||||
|
||||
* **1 point** — a single vertex
|
||||
* **2 points** — a line segment
|
||||
* **3 points** — a triangle
|
||||
* **4 points** — a tetrahedron
|
||||
|
||||
The GJK algorithm builds simplices incrementally to detect collisions, and EPA extends a 4-point simplex to compute penetration depth.
|
||||
|
||||
---
|
||||
|
||||
## Template & Concepts
|
||||
|
||||
```cpp
|
||||
template<GjkVector VectorType = Vector3<float>>
|
||||
class Simplex final;
|
||||
```
|
||||
|
||||
### `GjkVector` Concept
|
||||
|
||||
The vertex type must satisfy:
|
||||
|
||||
```cpp
|
||||
template<class V>
|
||||
concept GjkVector = requires(const V& a, const V& b) {
|
||||
{ -a } -> std::same_as<V>;
|
||||
{ a - b } -> std::same_as<V>;
|
||||
{ a.cross(b) } -> std::same_as<V>;
|
||||
{ a.point_to_same_direction(b) } -> std::same_as<bool>;
|
||||
};
|
||||
```
|
||||
|
||||
`omath::Vector3<float>` satisfies this concept and is the default.
|
||||
|
||||
---
|
||||
|
||||
## Constructors & Assignment
|
||||
|
||||
```cpp
|
||||
constexpr Simplex() = default;
|
||||
|
||||
constexpr Simplex& operator=(std::initializer_list<VectorType> list) noexcept;
|
||||
```
|
||||
|
||||
### Initialization
|
||||
|
||||
```cpp
|
||||
// Empty simplex
|
||||
Simplex<Vector3<float>> s;
|
||||
|
||||
// Initialize with points
|
||||
Simplex<Vector3<float>> s2;
|
||||
s2 = {v1, v2, v3}; // 3-point simplex (triangle)
|
||||
```
|
||||
|
||||
**Constraint**: Maximum 4 points. Passing more triggers an assertion in debug builds.
|
||||
|
||||
---
|
||||
|
||||
## Core Methods
|
||||
|
||||
### Adding Points
|
||||
|
||||
```cpp
|
||||
constexpr void push_front(const VectorType& p) noexcept;
|
||||
```
|
||||
|
||||
Inserts a point at the **front** (index 0), shifting existing points back. If the simplex is already at capacity (4 points), the last point is discarded.
|
||||
|
||||
**Usage pattern in GJK**:
|
||||
```cpp
|
||||
simplex.push_front(new_support_point);
|
||||
// Now simplex[0] is the newest point
|
||||
```
|
||||
|
||||
### Size & Capacity
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] constexpr std::size_t size() const noexcept;
|
||||
[[nodiscard]] static constexpr std::size_t capacity = 4;
|
||||
```
|
||||
|
||||
* `size()` — current number of points (0-4)
|
||||
* `capacity` — maximum points (always 4)
|
||||
|
||||
### Element Access
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] constexpr VectorType& operator[](std::size_t index) noexcept;
|
||||
[[nodiscard]] constexpr const VectorType& operator[](std::size_t index) const noexcept;
|
||||
```
|
||||
|
||||
Access points by index. **No bounds checking** — index must be `< size()`.
|
||||
|
||||
```cpp
|
||||
if (simplex.size() >= 2) {
|
||||
auto edge = simplex[1] - simplex[0];
|
||||
}
|
||||
```
|
||||
|
||||
### Iterators
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] constexpr auto begin() noexcept;
|
||||
[[nodiscard]] constexpr auto end() noexcept;
|
||||
[[nodiscard]] constexpr auto begin() const noexcept;
|
||||
[[nodiscard]] constexpr auto end() const noexcept;
|
||||
```
|
||||
|
||||
Standard iterator support for range-based loops:
|
||||
|
||||
```cpp
|
||||
for (const auto& vertex : simplex) {
|
||||
std::cout << vertex << "\n";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GJK-Specific Methods
|
||||
|
||||
These methods implement the core logic for simplifying simplices in the GJK algorithm.
|
||||
|
||||
### `contains_origin`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] constexpr bool contains_origin() noexcept;
|
||||
```
|
||||
|
||||
Determines if the origin lies within the current simplex. This is the **core GJK test**: if true, the shapes intersect.
|
||||
|
||||
* For a **1-point** simplex, always returns `false` (can't contain origin)
|
||||
* For a **2-point** simplex (line), checks if origin projects onto the segment
|
||||
* For a **3-point** simplex (triangle), checks if origin projects onto the triangle
|
||||
* For a **4-point** simplex (tetrahedron), checks if origin is inside
|
||||
|
||||
**Side effect**: Simplifies the simplex by removing points not needed to maintain proximity to the origin. After calling, `size()` may have decreased.
|
||||
|
||||
**Return value**:
|
||||
* `true` — origin is contained (collision detected)
|
||||
* `false` — origin not contained; simplex has been simplified toward origin
|
||||
|
||||
### `next_direction`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] constexpr VectorType next_direction() const noexcept;
|
||||
```
|
||||
|
||||
Computes the next search direction for GJK. This is the direction from the simplex toward the origin, used to query the next support point.
|
||||
|
||||
* Must be called **after** `contains_origin()` returns `false`
|
||||
* Behavior is **undefined** if called when `size() == 0` or when origin is already contained
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### GJK Iteration (Simplified)
|
||||
|
||||
```cpp
|
||||
Simplex<Vector3<float>> simplex;
|
||||
Vector3<float> direction{1, 0, 0}; // Initial search direction
|
||||
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
// Get support point in current direction
|
||||
auto support = find_support_vertex(collider_a, collider_b, direction);
|
||||
|
||||
// Check if we made progress
|
||||
if (support.dot(direction) <= 0)
|
||||
break; // No collision possible
|
||||
|
||||
simplex.push_front(support);
|
||||
|
||||
// Check if simplex contains origin
|
||||
if (simplex.contains_origin()) {
|
||||
// Collision detected!
|
||||
assert(simplex.size() == 4);
|
||||
return GjkHitInfo{true, simplex};
|
||||
}
|
||||
|
||||
// Get next search direction
|
||||
direction = simplex.next_direction();
|
||||
}
|
||||
|
||||
// No collision
|
||||
return GjkHitInfo{false, {}};
|
||||
```
|
||||
|
||||
### Manual Simplex Construction
|
||||
|
||||
```cpp
|
||||
using Vec3 = Vector3<float>;
|
||||
|
||||
Simplex<Vec3> simplex;
|
||||
simplex = {
|
||||
Vec3{0.0f, 0.0f, 0.0f},
|
||||
Vec3{1.0f, 0.0f, 0.0f},
|
||||
Vec3{0.0f, 1.0f, 0.0f},
|
||||
Vec3{0.0f, 0.0f, 1.0f}
|
||||
};
|
||||
|
||||
assert(simplex.size() == 4);
|
||||
|
||||
// Check if origin is inside this tetrahedron
|
||||
bool has_collision = simplex.contains_origin();
|
||||
```
|
||||
|
||||
### Iterating Over Points
|
||||
|
||||
```cpp
|
||||
void print_simplex(const Simplex<Vector3<float>>& s) {
|
||||
std::cout << "Simplex with " << s.size() << " points:\n";
|
||||
for (std::size_t i = 0; i < s.size(); ++i) {
|
||||
const auto& p = s[i];
|
||||
std::cout << " [" << i << "] = ("
|
||||
<< p.x << ", " << p.y << ", " << p.z << ")\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Simplex Simplification
|
||||
|
||||
The `contains_origin()` method implements different tests based on simplex size:
|
||||
|
||||
#### Line Segment (2 points)
|
||||
|
||||
Checks if origin projects onto segment `[A, B]`:
|
||||
* If yes, keeps both points
|
||||
* If no, keeps only the closer point
|
||||
|
||||
#### Triangle (3 points)
|
||||
|
||||
Tests the origin against the triangle plane and edges using cross products. Simplifies to:
|
||||
* The full triangle if origin projects onto its surface
|
||||
* An edge if origin is closest to that edge
|
||||
* A single vertex otherwise
|
||||
|
||||
#### Tetrahedron (4 points)
|
||||
|
||||
Tests origin against all four faces:
|
||||
* If origin is inside, returns `true` (collision)
|
||||
* If outside, reduces to the face/edge/vertex closest to origin
|
||||
|
||||
### Direction Calculation
|
||||
|
||||
The `next_direction()` method computes:
|
||||
* For **line**: perpendicular from line toward origin
|
||||
* For **triangle**: perpendicular from triangle toward origin
|
||||
* Implementation uses cross products and projections to avoid sqrt when possible
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
* **Storage**: Fixed 4 × `sizeof(VectorType)` + size counter
|
||||
* **Push front**: O(n) where n is current size (max 4, so effectively O(1))
|
||||
* **Contains origin**: O(1) for each case (line, triangle, tetrahedron)
|
||||
* **Next direction**: O(1) — simple cross products and subtractions
|
||||
* **No heap allocations**: All storage is inline
|
||||
|
||||
**constexpr**: All methods are `constexpr`, enabling compile-time usage where feasible.
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases & Constraints
|
||||
|
||||
### Degenerate Simplices
|
||||
|
||||
* **Zero-length edges**: Can occur if support points coincide. The algorithm handles this by checking `point_to_same_direction` before divisions.
|
||||
* **Collinear points**: Triangle simplification detects and handles collinear cases by reducing to a line.
|
||||
* **Flat tetrahedron**: If the 4th point is coplanar with the first 3, the origin containment test may have reduced precision.
|
||||
|
||||
### Assertions
|
||||
|
||||
* **Capacity**: `operator=` asserts `list.size() <= 4` in debug builds
|
||||
* **Index bounds**: No bounds checking in release builds — ensure `index < size()`
|
||||
|
||||
### Thread Safety
|
||||
|
||||
* **Read-only**: Safe to read from multiple threads
|
||||
* **Modification**: Not thread-safe; synchronize writes externally
|
||||
|
||||
---
|
||||
|
||||
## Relationship to GJK & EPA
|
||||
|
||||
### In GJK
|
||||
|
||||
* Starts empty or with an initial point
|
||||
* Grows via `push_front` as support points are added
|
||||
* Shrinks via `contains_origin` as it's simplified
|
||||
* Once it reaches 4 points and contains origin, GJK succeeds
|
||||
|
||||
### In EPA
|
||||
|
||||
* Takes a 4-point simplex from GJK as input
|
||||
* Uses the tetrahedron as the initial polytope
|
||||
* Does not directly use the `Simplex` class for expansion (EPA maintains a more complex polytope structure)
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [GJK Algorithm Documentation](gjk_algorithm.md) - Uses `Simplex` for collision detection
|
||||
- [EPA Algorithm Documentation](epa_algorithm.md) - Takes 4-point `Simplex` as input
|
||||
- [MeshCollider Documentation](mesh_collider.md) - Provides support function for GJK/EPA
|
||||
- [Vector3 Documentation](../linear_algebra/vector3.md) - Default vertex type
|
||||
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Collision tutorial
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
119
docs/engines/frostbite/mesh_trait.md
Normal file
119
docs/engines/frostbite/mesh_trait.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# `omath::frostbite_engine::MeshTrait` — mesh transformation trait for Frostbite Engine
|
||||
|
||||
> Header: `omath/engines/frostbite_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::frostbite_engine`
|
||||
> Purpose: provide Frostbite Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Frostbite's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**Frostbite Engine** uses:
|
||||
* **Up axis**: +Y
|
||||
* **Forward axis**: +Z
|
||||
* **Right axis**: +X
|
||||
* **Handedness**: Right-handed
|
||||
* **Rotation order**: Pitch (X) → Yaw (Y) → Roll (Z)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::frostbite_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::frostbite_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from Frostbite-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `frostbite_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::frostbite_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
Frostbite uses a right-handed Y-up coordinate system:
|
||||
|
||||
1. **Pitch** (rotation around X-axis / right axis)
|
||||
* Positive pitch looks upward (+Y direction)
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
2. **Yaw** (rotation around Y-axis / up axis)
|
||||
* Positive yaw rotates counterclockwise when viewed from above (right-handed)
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
3. **Roll** (rotation around Z-axis / forward axis)
|
||||
* Positive roll tilts right
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
```cpp
|
||||
namespace omath::frostbite_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Formulas Documentation](formulas.md) - Frostbite rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
119
docs/engines/iw_engine/mesh_trait.md
Normal file
119
docs/engines/iw_engine/mesh_trait.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# `omath::iw_engine::MeshTrait` — mesh transformation trait for IW Engine
|
||||
|
||||
> Header: `omath/engines/iw_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::iw_engine`
|
||||
> Purpose: provide IW Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in IW Engine's (Infinity Ward) coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**IW Engine** (Call of Duty) uses:
|
||||
* **Up axis**: +Z
|
||||
* **Forward axis**: +Y
|
||||
* **Right axis**: +X
|
||||
* **Handedness**: Right-handed
|
||||
* **Rotation order**: Pitch (X) → Yaw (Z) → Roll (Y)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::iw_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::iw_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from IW Engine-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `iw_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::iw_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
IW Engine uses a right-handed Z-up coordinate system (similar to Source Engine):
|
||||
|
||||
1. **Pitch** (rotation around X-axis / right axis)
|
||||
* Positive pitch looks upward (+Z direction)
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
2. **Yaw** (rotation around Z-axis / up axis)
|
||||
* Positive yaw rotates counterclockwise when viewed from above
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
3. **Roll** (rotation around Y-axis / forward axis)
|
||||
* Positive roll tilts right
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
```cpp
|
||||
namespace omath::iw_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Formulas Documentation](formulas.md) - IW Engine rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
121
docs/engines/opengl_engine/mesh_trait.md
Normal file
121
docs/engines/opengl_engine/mesh_trait.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# `omath::opengl_engine::MeshTrait` — mesh transformation trait for OpenGL
|
||||
|
||||
> Header: `omath/engines/opengl_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::opengl_engine`
|
||||
> Purpose: provide OpenGL-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in OpenGL's canonical coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**OpenGL** (canonical) uses:
|
||||
* **Up axis**: +Y
|
||||
* **Forward axis**: +Z (toward viewer)
|
||||
* **Right axis**: +X
|
||||
* **Handedness**: Right-handed
|
||||
* **Rotation order**: Pitch (X) → Yaw (Y) → Roll (Z)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::opengl_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::opengl_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from OpenGL-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `opengl_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::opengl_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
OpenGL uses a right-handed Y-up coordinate system:
|
||||
|
||||
1. **Pitch** (rotation around X-axis / right axis)
|
||||
* Positive pitch looks upward (+Y direction)
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
2. **Yaw** (rotation around Y-axis / up axis)
|
||||
* Positive yaw rotates counterclockwise when viewed from above (right-handed)
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
3. **Roll** (rotation around Z-axis / depth axis)
|
||||
* Positive roll tilts right
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
**Note**: In OpenGL, +Z points toward the viewer in view space, but away from the viewer in world space.
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
```cpp
|
||||
namespace omath::opengl_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Formulas Documentation](formulas.md) - OpenGL rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
182
docs/engines/source_engine/mesh_trait.md
Normal file
182
docs/engines/source_engine/mesh_trait.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# `omath::source_engine::MeshTrait` — mesh transformation trait for Source Engine
|
||||
|
||||
> Header: `omath/engines/source_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::source_engine`
|
||||
> Purpose: provide Source Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Source Engine's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**Source Engine** uses:
|
||||
* **Up axis**: +Z
|
||||
* **Forward axis**: +Y
|
||||
* **Right axis**: +X
|
||||
* **Handedness**: Right-handed
|
||||
* **Rotation order**: Pitch (X) → Yaw (Z) → Roll (Y)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::source_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::source_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from Source Engine-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `source_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
### Direct Usage
|
||||
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(45.0f);
|
||||
angles.yaw = YawAngle::from_degrees(90.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
Mat4X4 rot_matrix = MeshTrait::rotation_matrix(angles);
|
||||
|
||||
// Use the matrix directly
|
||||
Vector3<float> local_point{1, 0, 0};
|
||||
auto rotated = rot_matrix * mat_column_from_vector(local_point);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
The rotation matrix is built following Source Engine's conventions:
|
||||
|
||||
1. **Pitch** (rotation around X-axis / right axis)
|
||||
* Positive pitch looks upward (+Z direction)
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
2. **Yaw** (rotation around Z-axis / up axis)
|
||||
* Positive yaw rotates counterclockwise when viewed from above
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
3. **Roll** (rotation around Y-axis / forward axis)
|
||||
* Positive roll tilts right
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
**Composition**: The matrices are combined in the order Pitch × Yaw × Roll, producing a rotation that:
|
||||
* First applies roll around the forward axis
|
||||
* Then applies yaw around the up axis
|
||||
* Finally applies pitch around the right axis
|
||||
|
||||
This matches Source Engine's internal rotation order.
|
||||
|
||||
---
|
||||
|
||||
## Related Functions
|
||||
|
||||
The trait delegates to the formula defined in `formulas.hpp`:
|
||||
|
||||
```cpp
|
||||
[[nodiscard]]
|
||||
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
|
||||
```
|
||||
|
||||
See [Formulas Documentation](formulas.md) for details on the rotation matrix computation.
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
The Source Engine mesh type is pre-defined:
|
||||
|
||||
```cpp
|
||||
namespace omath::source_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
Use this alias to ensure correct trait usage:
|
||||
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// Correct: uses Source Engine trait
|
||||
Mesh my_mesh(vbo, vao);
|
||||
|
||||
// Avoid: manually specifying template parameters
|
||||
primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float> verbose_mesh(vbo, vao);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
* **Angle ranges**: Ensure angles are within valid ranges (pitch: [-89°, 89°], yaw/roll: [-180°, 180°])
|
||||
* **Performance**: Matrix computation is O(1) with ~64 floating-point operations
|
||||
* **Caching**: The mesh caches the transformation matrix; recomputed only when rotation changes
|
||||
* **Compatibility**: Works with all Source Engine games (CS:GO, TF2, Portal, Half-Life 2, etc.)
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive using this trait
|
||||
- [MeshCollider Documentation](../../collision/mesh_collider.md) - Collision wrapper for meshes
|
||||
- [Formulas Documentation](formulas.md) - Source Engine rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera transformation trait
|
||||
- [Constants Documentation](constants.md) - Source Engine constants
|
||||
- [API Overview](../../api_overview.md) - High-level API reference
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
119
docs/engines/unity_engine/mesh_trait.md
Normal file
119
docs/engines/unity_engine/mesh_trait.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# `omath::unity_engine::MeshTrait` — mesh transformation trait for Unity Engine
|
||||
|
||||
> Header: `omath/engines/unity_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::unity_engine`
|
||||
> Purpose: provide Unity Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Unity's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**Unity Engine** uses:
|
||||
* **Up axis**: +Y
|
||||
* **Forward axis**: +Z
|
||||
* **Right axis**: +X
|
||||
* **Handedness**: Left-handed
|
||||
* **Rotation order**: Pitch (X) → Yaw (Y) → Roll (Z)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::unity_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::unity_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from Unity-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `unity_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::unity_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
Unity uses a left-handed coordinate system with Y-up:
|
||||
|
||||
1. **Pitch** (rotation around X-axis / right axis)
|
||||
* Positive pitch looks upward (+Y direction)
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
2. **Yaw** (rotation around Y-axis / up axis)
|
||||
* Positive yaw rotates clockwise when viewed from above (left-handed)
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
3. **Roll** (rotation around Z-axis / forward axis)
|
||||
* Positive roll tilts right
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
```cpp
|
||||
namespace omath::unity_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Formulas Documentation](formulas.md) - Unity rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
121
docs/engines/unreal_engine/mesh_trait.md
Normal file
121
docs/engines/unreal_engine/mesh_trait.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# `omath::unreal_engine::MeshTrait` — mesh transformation trait for Unreal Engine
|
||||
|
||||
> Header: `omath/engines/unreal_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::unreal_engine`
|
||||
> Purpose: provide Unreal Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Unreal Engine's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**Unreal Engine** uses:
|
||||
* **Up axis**: +Z
|
||||
* **Forward axis**: +X
|
||||
* **Right axis**: +Y
|
||||
* **Handedness**: Left-handed
|
||||
* **Rotation order**: Roll (Y) → Pitch (X) → Yaw (Z)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::unreal_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::unreal_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from Unreal-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `unreal_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::unreal_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
Unreal uses a left-handed Z-up coordinate system:
|
||||
|
||||
1. **Roll** (rotation around Y-axis / right axis)
|
||||
* Positive roll rotates forward axis upward
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
2. **Pitch** (rotation around X-axis / forward axis)
|
||||
* Positive pitch looks upward
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
3. **Yaw** (rotation around Z-axis / up axis)
|
||||
* Positive yaw rotates clockwise when viewed from above (left-handed)
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
**Note**: Unreal applies rotations in Roll-Pitch-Yaw order, different from most other engines.
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
```cpp
|
||||
namespace omath::unreal_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Formulas Documentation](formulas.md) - Unreal rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 137 KiB |
BIN
docs/images/showcase/opengl.png
Normal file
BIN
docs/images/showcase/opengl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
65
docs/styles/links.css
Normal file
65
docs/styles/links.css
Normal file
@@ -0,0 +1,65 @@
|
||||
/* Normal links */
|
||||
a {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
|
||||
/* On hover/focus */
|
||||
a:hover,
|
||||
a:focus {
|
||||
color: #ff9900; /* a slightly different orange, optional */
|
||||
}
|
||||
/* Navbar background */
|
||||
.navbar,
|
||||
.navbar-default,
|
||||
.navbar-inverse {
|
||||
background-color: #a26228 !important; /* your orange */
|
||||
border-color: #ff6600 !important;
|
||||
}
|
||||
|
||||
/* Navbar brand + links */
|
||||
.navbar .navbar-brand,
|
||||
.navbar .navbar-nav > li > a {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Active and hover states */
|
||||
.navbar .navbar-nav > .active > a,
|
||||
.navbar .navbar-nav > .active > a:focus,
|
||||
.navbar .navbar-nav > .active > a:hover,
|
||||
.navbar .navbar-nav > li > a:hover,
|
||||
.navbar .navbar-nav > li > a:focus {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
/* === DROPDOWN MENU BACKGROUND === */
|
||||
.navbar .dropdown-menu {
|
||||
border-color: #ff6600 !important;
|
||||
}
|
||||
|
||||
/* Caret icon (the little triangle) */
|
||||
.navbar .dropdown-toggle .caret {
|
||||
border-top-color: #ffffff !important;
|
||||
border-bottom-color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* === BOOTSTRAP 3 STYLE ITEMS (mkdocs + bootswatch darkly often use this) === */
|
||||
.navbar .dropdown-menu > li > a {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.navbar .dropdown-menu > li > a:hover,
|
||||
.navbar .dropdown-menu > li > a:focus {
|
||||
background-color: #e65c00 !important; /* darker orange on hover */
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* === BOOTSTRAP 4+ STYLE ITEMS (if your theme uses .dropdown-item) === */
|
||||
.navbar .dropdown-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.navbar .dropdown-item:hover,
|
||||
.navbar .dropdown-item:focus {
|
||||
background-color: #e65c00 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
@@ -1,4 +1,32 @@
|
||||
project(examples)
|
||||
|
||||
add_executable(ExampleProjectionMatrixBuilder example_proj_mat_builder.cpp)
|
||||
target_link_libraries(ExampleProjectionMatrixBuilder PRIVATE omath::omath)
|
||||
add_executable(example_projection_matrix_builder example_proj_mat_builder.cpp)
|
||||
set_target_properties(example_projection_matrix_builder PROPERTIES
|
||||
CXX_STANDARD 26
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
)
|
||||
target_link_libraries(example_projection_matrix_builder PRIVATE omath::omath)
|
||||
|
||||
add_executable(example_signature_scan example_signature_scan.cpp)
|
||||
set_target_properties(example_signature_scan PROPERTIES
|
||||
CXX_STANDARD 26
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
)
|
||||
target_link_libraries(example_signature_scan PRIVATE omath::omath)
|
||||
|
||||
|
||||
add_executable(example_glfw3 example_glfw3.cpp)
|
||||
set_target_properties(example_glfw3 PROPERTIES CXX_STANDARD 26
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
)
|
||||
|
||||
find_package(GLEW REQUIRED)
|
||||
find_package(glfw3 CONFIG REQUIRED)
|
||||
target_link_libraries(example_glfw3 PRIVATE omath::omath GLEW::GLEW glfw)
|
||||
339
examples/example_glfw3.cpp
Normal file
339
examples/example_glfw3.cpp
Normal file
@@ -0,0 +1,339 @@
|
||||
// main.cpp
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
// --- OpenGL / windowing ---
|
||||
#include <GL/glew.h> // GLEW must come before GLFW
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
// --- your math / engine stuff ---
|
||||
#include "omath/3d_primitives/mesh.hpp"
|
||||
#include "omath/engines/opengl_engine/camera.hpp"
|
||||
#include "omath/engines/opengl_engine/constants.hpp"
|
||||
#include "omath/engines/opengl_engine/mesh.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
|
||||
using omath::Vector3;
|
||||
|
||||
// ---------------- TYPE ALIASES (ADAPT TO YOUR LIB) ----------------
|
||||
|
||||
// Your 4x4 matrix type
|
||||
using Mat4x4 = omath::opengl_engine::Mat4X4;
|
||||
|
||||
// Rotation angles for the Mesh
|
||||
using RotationAngles = omath::opengl_engine::ViewAngles;
|
||||
|
||||
// For brevity, alias the templates instantiated with your types
|
||||
using VertexType = omath::primitives::Vertex<Vector3<float>>;
|
||||
using CubeMesh = omath::opengl_engine::Mesh;
|
||||
using MyCamera = omath::opengl_engine::Camera;
|
||||
|
||||
// ---------------- SHADERS ----------------
|
||||
|
||||
static const char* vertexShaderSource = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in vec3 aNormal;
|
||||
layout (location = 2) in vec3 aUv;
|
||||
|
||||
uniform mat4 uMVP;
|
||||
uniform mat4 uModel;
|
||||
|
||||
out vec3 vNormal;
|
||||
out vec3 vUv;
|
||||
|
||||
void main() {
|
||||
vNormal = aNormal;
|
||||
vUv = aUv;
|
||||
gl_Position = uMVP * uModel * vec4(aPos, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
static const char* fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
in vec3 vNormal;
|
||||
in vec3 vUv;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
vec3 baseColor = normalize(abs(vNormal));
|
||||
FragColor = vec4(baseColor, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
GLuint compileShader(GLenum type, const char* src)
|
||||
{
|
||||
GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &src, nullptr);
|
||||
glCompileShader(shader);
|
||||
|
||||
GLint ok = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok)
|
||||
{
|
||||
char log[1024];
|
||||
glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||
std::cerr << "Shader compile error: " << log << std::endl;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLuint createShaderProgram()
|
||||
{
|
||||
GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
|
||||
GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
|
||||
|
||||
GLuint prog = glCreateProgram();
|
||||
glAttachShader(prog, vs);
|
||||
glAttachShader(prog, fs);
|
||||
glLinkProgram(prog);
|
||||
|
||||
GLint ok = GL_FALSE;
|
||||
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
|
||||
if (!ok)
|
||||
{
|
||||
char log[1024];
|
||||
glGetProgramInfoLog(prog, sizeof(log), nullptr, log);
|
||||
std::cerr << "Program link error: " << log << std::endl;
|
||||
}
|
||||
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
return prog;
|
||||
}
|
||||
|
||||
void framebuffer_size_callback(GLFWwindow* /*window*/, int w, int h)
|
||||
{
|
||||
glViewport(0, 0, w, h);
|
||||
}
|
||||
|
||||
// ---------------- MAIN ----------------
|
||||
|
||||
int main()
|
||||
{
|
||||
// ---------- GLFW init ----------
|
||||
if (!glfwInit())
|
||||
{
|
||||
std::cerr << "Failed to init GLFW\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
#ifdef __APPLE__
|
||||
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
|
||||
#endif
|
||||
|
||||
const int SCR_WIDTH = 800;
|
||||
const int SCR_HEIGHT = 600;
|
||||
|
||||
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "omath cube + camera (GLEW)", nullptr, nullptr);
|
||||
if (!window)
|
||||
{
|
||||
std::cerr << "Failed to create GLFW window\n";
|
||||
glfwTerminate();
|
||||
return -1;
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent(window);
|
||||
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
|
||||
|
||||
// ---------- GLEW init ----------
|
||||
glewExperimental = GL_TRUE;
|
||||
GLenum glewErr = glewInit();
|
||||
if (glewErr != GLEW_OK)
|
||||
{
|
||||
std::cerr << "Failed to initialize GLEW: " << reinterpret_cast<const char*>(glewGetErrorString(glewErr))
|
||||
<< "\n";
|
||||
glfwTerminate();
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------- GL state ----------
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
|
||||
// Face culling
|
||||
glEnable(GL_CULL_FACE);
|
||||
glCullFace(GL_BACK); // cull back faces
|
||||
glFrontFace(GL_CCW); // counter-clockwise is front
|
||||
|
||||
// ---------- Build Cube Mesh (CPU side) ----------
|
||||
std::vector<VertexType> vbo;
|
||||
vbo.reserve(8);
|
||||
|
||||
Vector3<float> p000{-0.5f, -0.5f, -0.5f};
|
||||
Vector3<float> p001{-0.5f, -0.5f, 0.5f};
|
||||
Vector3<float> p010{-0.5f, 0.5f, -0.5f};
|
||||
Vector3<float> p011{-0.5f, 0.5f, 0.5f};
|
||||
Vector3<float> p100{0.5f, -0.5f, -0.5f};
|
||||
Vector3<float> p101{0.5f, -0.5f, 0.5f};
|
||||
Vector3<float> p110{0.5f, 0.5f, -0.5f};
|
||||
Vector3<float> p111{0.5f, 0.5f, 0.5f};
|
||||
|
||||
VertexType v0{p000, Vector3<float>{-1, -1, -1}, omath::Vector2<float>{0, 0}};
|
||||
VertexType v1{p001, Vector3<float>{-1, -1, 1}, omath::Vector2<float>{0, 1}};
|
||||
VertexType v2{p010, Vector3<float>{-1, 1, -1}, omath::Vector2<float>{1, 0}};
|
||||
VertexType v3{p011, Vector3<float>{-1, 1, 1}, omath::Vector2<float>{1, 1}};
|
||||
VertexType v4{p100, Vector3<float>{1, -1, -1}, omath::Vector2<float>{0, 0}};
|
||||
VertexType v5{p101, Vector3<float>{1, -1, 1}, omath::Vector2<float>{0, 1}};
|
||||
VertexType v6{p110, Vector3<float>{1, 1, -1}, omath::Vector2<float>{1, 0}};
|
||||
VertexType v7{p111, Vector3<float>{1, 1, 1}, omath::Vector2<float>{1, 1}};
|
||||
|
||||
vbo.push_back(v0); // 0
|
||||
vbo.push_back(v1); // 1
|
||||
vbo.push_back(v2); // 2
|
||||
vbo.push_back(v3); // 3
|
||||
vbo.push_back(v4); // 4
|
||||
vbo.push_back(v5); // 5
|
||||
vbo.push_back(v6); // 6
|
||||
vbo.push_back(v7); // 7
|
||||
|
||||
using Idx = Vector3<std::uint32_t>;
|
||||
std::vector<Idx> ebo;
|
||||
ebo.reserve(12);
|
||||
|
||||
// front (z+)
|
||||
ebo.emplace_back(1, 5, 7);
|
||||
ebo.emplace_back(1, 7, 3);
|
||||
|
||||
// back (z-)
|
||||
ebo.emplace_back(0, 2, 6);
|
||||
ebo.emplace_back(0, 6, 4);
|
||||
|
||||
// left (x-)
|
||||
ebo.emplace_back(0, 1, 3);
|
||||
ebo.emplace_back(0, 3, 2);
|
||||
|
||||
// right (x+)
|
||||
ebo.emplace_back(4, 6, 7);
|
||||
ebo.emplace_back(4, 7, 5);
|
||||
|
||||
// bottom (y-)
|
||||
ebo.emplace_back(0, 4, 5);
|
||||
ebo.emplace_back(0, 5, 1);
|
||||
|
||||
// top (y+)
|
||||
ebo.emplace_back(2, 3, 7);
|
||||
ebo.emplace_back(2, 7, 6);
|
||||
|
||||
CubeMesh cube{std::move(vbo), std::move(ebo)};
|
||||
cube.set_origin({0.f, 0.f, 0.f});
|
||||
cube.set_scale({2.f, 2.f, 2.f});
|
||||
cube.set_rotation(RotationAngles{});
|
||||
|
||||
// ---------- OpenGL buffers ----------
|
||||
GLuint VAO = 0, VBO = 0, EBO_GL = 0;
|
||||
glGenVertexArrays(1, &VAO);
|
||||
glGenBuffers(1, &VBO);
|
||||
glGenBuffers(1, &EBO_GL);
|
||||
|
||||
glBindVertexArray(VAO);
|
||||
|
||||
// upload vertex buffer
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, cube.m_vertex_buffer.size() * sizeof(VertexType), cube.m_vertex_buffer.data(),
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
// flatten EBO to GL indices
|
||||
std::vector<GLuint> flatIndices;
|
||||
flatIndices.reserve(cube.m_vertex_array_object.size() * 3);
|
||||
for (const auto& tri : cube.m_vertex_array_object)
|
||||
{
|
||||
flatIndices.push_back(tri.x);
|
||||
flatIndices.push_back(tri.y);
|
||||
flatIndices.push_back(tri.z);
|
||||
}
|
||||
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_GL);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, flatIndices.size() * sizeof(GLuint), flatIndices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// vertex layout: position / normal / uv (each Vector3<float>)
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexType), (void*)offsetof(VertexType, position));
|
||||
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexType), (void*)offsetof(VertexType, normal));
|
||||
|
||||
glEnableVertexAttribArray(2);
|
||||
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(VertexType), (void*)offsetof(VertexType, uv));
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
// ---------- Camera setup ----------
|
||||
omath::projection::ViewPort viewPort{static_cast<float>(SCR_WIDTH), static_cast<float>(SCR_HEIGHT)};
|
||||
|
||||
Vector3<float> camPos{0.f, 0.f, 3.f};
|
||||
|
||||
float nearPlane = 0.1f;
|
||||
float farPlane = 100.f;
|
||||
auto fov = omath::projection::FieldOfView::from_degrees(90.f);
|
||||
|
||||
MyCamera camera{camPos, {}, viewPort, fov, nearPlane, farPlane};
|
||||
|
||||
// ---------- Shader ----------
|
||||
GLuint shaderProgram = createShaderProgram();
|
||||
GLint uMvpLoc = glGetUniformLocation(shaderProgram, "uMVP");
|
||||
GLint uModel = glGetUniformLocation(shaderProgram, "uModel");
|
||||
|
||||
static float old_frame_time = glfwGetTime();
|
||||
|
||||
// ---------- Main loop ----------
|
||||
while (!glfwWindowShouldClose(window))
|
||||
{
|
||||
glfwPollEvents();
|
||||
|
||||
float currentTime = glfwGetTime();
|
||||
float deltaTime = currentTime - old_frame_time;
|
||||
old_frame_time = currentTime;
|
||||
|
||||
int fbW = 0, fbH = 0;
|
||||
glfwGetFramebufferSize(window, &fbW, &fbH);
|
||||
glViewport(0, 0, fbW, fbH);
|
||||
|
||||
viewPort.m_width = static_cast<float>(fbW);
|
||||
viewPort.m_height = static_cast<float>(fbH);
|
||||
camera.set_view_port(viewPort);
|
||||
|
||||
glClearColor(0.1f, 0.15f, 0.2f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
RotationAngles rot = cube.get_rotation_angles();
|
||||
rot.yaw += omath::opengl_engine::YawAngle ::from_degrees(40.f * deltaTime);
|
||||
rot.roll += omath::opengl_engine::RollAngle::from_degrees(40.f * deltaTime);
|
||||
|
||||
if (rot.pitch.as_degrees() == 90.f)
|
||||
rot.pitch = omath::opengl_engine::PitchAngle::from_degrees(-90.f);
|
||||
rot.pitch += omath::opengl_engine::PitchAngle::from_degrees(40.f * deltaTime);
|
||||
cube.set_rotation(rot);
|
||||
|
||||
const Mat4x4& viewProj = camera.get_view_projection_matrix();
|
||||
const auto& model = cube.get_to_world_matrix();
|
||||
|
||||
glUseProgram(shaderProgram);
|
||||
|
||||
// Send matrices to GPU
|
||||
const float* mvpPtr = viewProj.raw_array().data();
|
||||
const float* modelPtr = model.raw_array().data();
|
||||
|
||||
glUniformMatrix4fv(uMvpLoc, 1, GL_FALSE, mvpPtr);
|
||||
glUniformMatrix4fv(uModel, 1, GL_FALSE, modelPtr);
|
||||
|
||||
glBindVertexArray(VAO);
|
||||
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(flatIndices.size()), GL_UNSIGNED_INT, nullptr);
|
||||
|
||||
glfwSwapBuffers(window);
|
||||
}
|
||||
|
||||
// ---------- Cleanup ----------
|
||||
glDeleteVertexArrays(1, &VAO);
|
||||
glDeleteBuffers(1, &VBO);
|
||||
glDeleteBuffers(1, &EBO_GL);
|
||||
glDeleteProgram(shaderProgram);
|
||||
|
||||
glfwDestroyWindow(window);
|
||||
glfwTerminate();
|
||||
return 0;
|
||||
}
|
||||
39
examples/example_signature_scan.cpp
Normal file
39
examples/example_signature_scan.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// Created by Vlad on 11/8/2025.
|
||||
//
|
||||
|
||||
#include <iostream>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
#include <print>
|
||||
|
||||
int main()
|
||||
{
|
||||
std::println("OMATH Signature Scanner");
|
||||
|
||||
std::print("Enter path to PE file: ");
|
||||
std::string file_path;
|
||||
std::getline(std::cin, file_path); // allows spaces
|
||||
|
||||
std::print("Enter target section: ");
|
||||
std::string section;
|
||||
std::getline(std::cin, section);
|
||||
|
||||
std::print("Enter signature: ");
|
||||
std::string signature;
|
||||
std::getline(std::cin, signature);
|
||||
|
||||
std::println("[LOG] Performing scan....");
|
||||
|
||||
const auto result = omath::PePatternScanner::scan_for_pattern_in_file(file_path, signature, section);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
std::println("[ERROR] Scan failed or signature was not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::println("Found at virtual 0x{:x} , raw 0x{:x}", result->virtual_base_addr + result->target_offset,
|
||||
result->raw_base_addr + result->target_offset);
|
||||
|
||||
return 0;
|
||||
}
|
||||
127
include/omath/3d_primitives/mesh.hpp
Normal file
127
include/omath/3d_primitives/mesh.hpp
Normal file
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include <omath/linear_algebra/mat.hpp>
|
||||
#include <omath/linear_algebra/vector3.hpp>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace omath::primitives
|
||||
{
|
||||
template<class VecType = Vector3<float>, class UvT = Vector2<float>>
|
||||
struct Vertex final
|
||||
{
|
||||
using VectorType = VecType;
|
||||
using UvType = UvT;
|
||||
VectorType position;
|
||||
VectorType normal;
|
||||
UvType uv;
|
||||
};
|
||||
|
||||
template<typename T> concept HasPosition = requires(T vertex) { vertex.position; };
|
||||
template<typename T> concept HasNormal = requires(T vertex) { vertex.normal; };
|
||||
template<typename T> concept HasUv = requires(T vertex) { vertex.uv; };
|
||||
|
||||
template<class Mat4X4, class RotationAngles, class MeshTypeTrait, class VertType = Vertex<>>
|
||||
class Mesh final
|
||||
{
|
||||
public:
|
||||
using VectorType = VertType::VectorType;
|
||||
using VertexType = VertType;
|
||||
|
||||
private:
|
||||
using Vbo = std::vector<VertexType>;
|
||||
using Ebo = std::vector<Vector3<std::uint32_t>>;
|
||||
|
||||
public:
|
||||
Vbo m_vertex_buffer;
|
||||
Ebo m_element_buffer_object;
|
||||
|
||||
Mesh(Vbo vbo, Ebo vao,
|
||||
const VectorType scale =
|
||||
{
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
})
|
||||
: m_vertex_buffer(std::move(vbo)), m_element_buffer_object(std::move(vao)), m_scale(std::move(scale))
|
||||
{
|
||||
}
|
||||
void set_origin(const VectorType& new_origin)
|
||||
{
|
||||
m_origin = new_origin;
|
||||
m_to_world_matrix = std::nullopt;
|
||||
}
|
||||
|
||||
void set_scale(const VectorType& new_scale)
|
||||
{
|
||||
m_scale = new_scale;
|
||||
m_to_world_matrix = std::nullopt;
|
||||
}
|
||||
|
||||
void set_rotation(const RotationAngles& new_rotation_angles)
|
||||
{
|
||||
m_rotation_angles = new_rotation_angles;
|
||||
m_to_world_matrix = std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
const VectorType& get_origin() const
|
||||
{
|
||||
return m_origin;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
const VectorType& get_scale() const
|
||||
{
|
||||
return m_scale;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
const RotationAngles& get_rotation_angles() const
|
||||
{
|
||||
return m_rotation_angles;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
const Mat4X4& get_to_world_matrix() const
|
||||
{
|
||||
if (m_to_world_matrix)
|
||||
return m_to_world_matrix.value();
|
||||
m_to_world_matrix = mat_translation<float, Mat4X4::get_store_ordering()>(m_origin)
|
||||
* MeshTypeTrait::rotation_matrix(m_rotation_angles)
|
||||
* mat_scale<float, Mat4X4::get_store_ordering()>(m_scale);
|
||||
|
||||
return m_to_world_matrix.value();
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
VectorType vertex_position_to_world_space(const Vector3<float>& vertex_position) const
|
||||
requires HasPosition<VertexType>
|
||||
{
|
||||
auto abs_vec = get_to_world_matrix() * mat_column_from_vector<typename Mat4X4::ContainedType, Mat4X4::get_store_ordering()>(vertex_position);
|
||||
|
||||
return {abs_vec.at(0, 0), abs_vec.at(1, 0), abs_vec.at(2, 0)};
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
Triangle<VectorType> make_face_in_world_space(const Ebo::const_iterator vao_iterator) const
|
||||
requires HasPosition<VertexType>
|
||||
{
|
||||
return {vertex_position_to_world_space(m_vertex_buffer.at(vao_iterator->x).position),
|
||||
vertex_position_to_world_space(m_vertex_buffer.at(vao_iterator->y).position),
|
||||
vertex_position_to_world_space(m_vertex_buffer.at(vao_iterator->z).position)};
|
||||
}
|
||||
|
||||
private:
|
||||
VectorType m_origin;
|
||||
VectorType m_scale;
|
||||
|
||||
RotationAngles m_rotation_angles;
|
||||
|
||||
mutable std::optional<Mat4X4> m_to_world_matrix;
|
||||
};
|
||||
} // namespace omath::primitives
|
||||
23
include/omath/collision/collider_interface.hpp
Normal file
23
include/omath/collision/collider_interface.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Created by Vladislav on 06.12.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include <omath/linear_algebra/vector3.hpp>
|
||||
|
||||
namespace omath::collision
|
||||
{
|
||||
template<class VecType = Vector3<float>>
|
||||
class ColliderInterface
|
||||
{
|
||||
public:
|
||||
using VectorType = VecType;
|
||||
virtual ~ColliderInterface() = default;
|
||||
|
||||
[[nodiscard]]
|
||||
virtual VectorType find_abs_furthest_vertex_position(const VectorType& direction) const = 0;
|
||||
|
||||
[[nodiscard]]
|
||||
virtual const VectorType& get_origin() const = 0;
|
||||
virtual void set_origin(const VectorType& new_origin) = 0;
|
||||
};
|
||||
}
|
||||
302
include/omath/collision/epa_algorithm.hpp
Normal file
302
include/omath/collision/epa_algorithm.hpp
Normal file
@@ -0,0 +1,302 @@
|
||||
#pragma once
|
||||
#include "simplex.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <memory_resource>
|
||||
#include <queue>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace omath::collision
|
||||
{
|
||||
template<class V>
|
||||
concept EpaVector = requires(const V& a, const V& b, float s) {
|
||||
{ a - b } -> std::same_as<V>;
|
||||
{ a.cross(b) } -> std::same_as<V>;
|
||||
{ a.dot(b) } -> std::same_as<float>;
|
||||
{ -a } -> std::same_as<V>;
|
||||
{ a * s } -> std::same_as<V>;
|
||||
{ a / s } -> std::same_as<V>;
|
||||
};
|
||||
|
||||
template<class ColliderInterfaceType>
|
||||
class Epa final
|
||||
{
|
||||
public:
|
||||
using VectorType = ColliderInterfaceType::VectorType;
|
||||
static_assert(EpaVector<VectorType>, "VertexType must satisfy EpaVector concept");
|
||||
|
||||
struct Result final
|
||||
{
|
||||
VectorType normal{}; // from A to B
|
||||
VectorType penetration_vector;
|
||||
float depth{0.0f};
|
||||
int iterations{0};
|
||||
int num_vertices{0};
|
||||
int num_faces{0};
|
||||
};
|
||||
|
||||
struct Params final
|
||||
{
|
||||
int max_iterations{64};
|
||||
float tolerance{1e-4f}; // absolute tolerance on distance growth
|
||||
};
|
||||
// Precondition: simplex.size()==4 and contains the origin.
|
||||
[[nodiscard]]
|
||||
static std::optional<Result> solve(const ColliderInterfaceType& a, const ColliderInterfaceType& b,
|
||||
const Simplex<VectorType>& simplex, const Params params = {},
|
||||
std::pmr::memory_resource& mem_resource = *std::pmr::get_default_resource())
|
||||
{
|
||||
// --- Build initial polytope from simplex (4 points) ---
|
||||
std::pmr::vector<VectorType> vertexes = build_initial_polytope_from_simplex(simplex, mem_resource);
|
||||
|
||||
// Initial tetra faces (windings corrected in make_face)
|
||||
std::pmr::vector<Face> faces = create_initial_tetra_faces(mem_resource, vertexes);
|
||||
|
||||
auto heap = rebuild_heap(faces, mem_resource);
|
||||
|
||||
Result out{};
|
||||
|
||||
for (int it = 0; it < params.max_iterations; ++it)
|
||||
{
|
||||
// If heap might be stale after face edits, rebuild lazily.
|
||||
if (heap.empty())
|
||||
break;
|
||||
// Rebuild when the "closest" face changed (simple cheap guard)
|
||||
// (We could keep face handles; this is fine for small Ns.)
|
||||
|
||||
if (const auto top = heap.top(); faces[top.idx].d != top.d)
|
||||
heap = rebuild_heap(faces, mem_resource);
|
||||
|
||||
if (heap.empty())
|
||||
break;
|
||||
|
||||
const int fidx = heap.top().idx;
|
||||
const Face face = faces[fidx];
|
||||
|
||||
// Get the furthest point in face normal direction
|
||||
const VectorType p = support_point(a, b, face.n);
|
||||
const float p_dist = face.n.dot(p);
|
||||
|
||||
// Converged if we can’t push the face closer than tolerance
|
||||
if (p_dist - face.d <= params.tolerance)
|
||||
{
|
||||
out.normal = face.n;
|
||||
out.depth = face.d; // along unit normal
|
||||
out.iterations = it + 1;
|
||||
out.num_vertices = static_cast<int>(vertexes.size());
|
||||
out.num_faces = static_cast<int>(faces.size());
|
||||
|
||||
out.penetration_vector = out.normal * out.depth;
|
||||
return out;
|
||||
}
|
||||
|
||||
// Add new vertex
|
||||
const int new_idx = static_cast<int>(vertexes.size());
|
||||
vertexes.emplace_back(p);
|
||||
|
||||
const auto [to_delete, boundary] = mark_visible_and_collect_horizon(faces, p);
|
||||
|
||||
erase_marked(faces, to_delete);
|
||||
|
||||
// Stitch new faces around the horizon
|
||||
for (const auto& e : boundary)
|
||||
faces.emplace_back(make_face(vertexes, e.a, e.b, new_idx));
|
||||
|
||||
// Rebuild heap after topology change
|
||||
heap = rebuild_heap(faces, mem_resource);
|
||||
|
||||
if (!std::isfinite(vertexes.back().dot(vertexes.back())))
|
||||
break; // safety
|
||||
out.iterations = it + 1;
|
||||
}
|
||||
|
||||
if (faces.empty())
|
||||
return std::nullopt;
|
||||
|
||||
const auto best = *std::ranges::min_element(faces, [](const auto& first, const auto& second)
|
||||
{ return first.d < second.d; });
|
||||
out.normal = best.n;
|
||||
out.depth = best.d;
|
||||
out.num_vertices = static_cast<int>(vertexes.size());
|
||||
out.num_faces = static_cast<int>(faces.size());
|
||||
|
||||
out.penetration_vector = out.normal * out.depth;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Face final
|
||||
{
|
||||
int i0, i1, i2;
|
||||
VectorType n; // unit outward normal
|
||||
float d; // n · v0 (>=0 ideally because origin is inside)
|
||||
};
|
||||
|
||||
struct Edge final
|
||||
{
|
||||
int a, b;
|
||||
};
|
||||
|
||||
struct HeapItem final
|
||||
{
|
||||
float d;
|
||||
int idx;
|
||||
};
|
||||
struct HeapCmp final
|
||||
{
|
||||
[[nodiscard]]
|
||||
static bool operator()(const HeapItem& lhs, const HeapItem& rhs) noexcept
|
||||
{
|
||||
return lhs.d > rhs.d; // min-heap by distance
|
||||
}
|
||||
};
|
||||
|
||||
using Heap = std::priority_queue<HeapItem, std::pmr::vector<HeapItem>, HeapCmp>;
|
||||
|
||||
[[nodiscard]]
|
||||
static Heap rebuild_heap(const std::pmr::vector<Face>& faces, auto& memory_resource)
|
||||
{
|
||||
std::pmr::vector<HeapItem> storage{&memory_resource};
|
||||
storage.reserve(faces.size()); // optional but recommended
|
||||
|
||||
Heap h{HeapCmp{}, std::move(storage)};
|
||||
|
||||
for (int i = 0; i < static_cast<int>(faces.size()); ++i)
|
||||
h.emplace(faces[i].d, i);
|
||||
|
||||
return h; // allocator is preserved
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static bool visible_from(const Face& f, const VectorType& p)
|
||||
{
|
||||
// positive if p is in front of the face
|
||||
return f.n.dot(p) - f.d > 1e-7f;
|
||||
}
|
||||
|
||||
static void add_edge_boundary(std::pmr::vector<Edge>& boundary, int a, int b)
|
||||
{
|
||||
// Keep edges that appear only once; erase if opposite already present
|
||||
auto itb = std::ranges::find_if(boundary, [&](const Edge& e) { return e.a == b && e.b == a; });
|
||||
if (itb != boundary.end())
|
||||
boundary.erase(itb); // internal edge cancels out
|
||||
else
|
||||
boundary.emplace_back(a, b); // horizon edge (directed)
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static Face make_face(const std::pmr::vector<VectorType>& vertexes, int i0, int i1, int i2)
|
||||
{
|
||||
const VectorType& a0 = vertexes[i0];
|
||||
const VectorType& a1 = vertexes[i1];
|
||||
const VectorType& a2 = vertexes[i2];
|
||||
VectorType n = (a1 - a0).cross(a2 - a0);
|
||||
if (n.dot(n) <= 1e-30f)
|
||||
{
|
||||
n = any_perp_vec(a1 - a0); // degenerate guard
|
||||
}
|
||||
// Ensure normal points outward (away from origin): require n·a0 >= 0
|
||||
if (n.dot(a0) < 0.0f)
|
||||
{
|
||||
std::swap(i1, i2);
|
||||
n = -n;
|
||||
}
|
||||
const float inv_len = 1.0f / std::sqrt(std::max(n.dot(n), 1e-30f));
|
||||
n = n * inv_len;
|
||||
const float d = n.dot(a0);
|
||||
return {i0, i1, i2, n, d};
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static VectorType support_point(const ColliderInterfaceType& a, const ColliderInterfaceType& b,
|
||||
const VectorType& dir)
|
||||
{
|
||||
return a.find_abs_furthest_vertex_position(dir) - b.find_abs_furthest_vertex_position(-dir);
|
||||
}
|
||||
|
||||
template<class V>
|
||||
[[nodiscard]]
|
||||
static constexpr bool near_zero_vec(const V& v, const float eps = 1e-7f)
|
||||
{
|
||||
return v.dot(v) <= eps * eps;
|
||||
}
|
||||
|
||||
template<class V>
|
||||
[[nodiscard]]
|
||||
static constexpr V any_perp_vec(const V& v)
|
||||
{
|
||||
for (const auto& dir : {V{1, 0, 0}, V{0, 1, 0}, V{0, 0, 1}})
|
||||
if (const auto d = v.cross(dir); !near_zero_vec(d))
|
||||
return d;
|
||||
return V{1, 0, 0};
|
||||
}
|
||||
[[nodiscard]]
|
||||
static std::pmr::vector<Face> create_initial_tetra_faces(std::pmr::memory_resource& mem_resource,
|
||||
const std::pmr::vector<VectorType>& vertexes)
|
||||
{
|
||||
std::pmr::vector<Face> faces{&mem_resource};
|
||||
faces.reserve(4);
|
||||
faces.emplace_back(make_face(vertexes, 0, 1, 2));
|
||||
faces.emplace_back(make_face(vertexes, 0, 2, 3));
|
||||
faces.emplace_back(make_face(vertexes, 0, 3, 1));
|
||||
faces.emplace_back(make_face(vertexes, 1, 3, 2));
|
||||
return faces;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static std::pmr::vector<VectorType> build_initial_polytope_from_simplex(const Simplex<VectorType>& simplex,
|
||||
std::pmr::memory_resource& mem_resource)
|
||||
{
|
||||
std::pmr::vector<VectorType> vertexes{&mem_resource};
|
||||
vertexes.reserve(simplex.size());
|
||||
|
||||
for (std::size_t i = 0; i < simplex.size(); ++i)
|
||||
vertexes.emplace_back(simplex[i]);
|
||||
|
||||
return vertexes;
|
||||
}
|
||||
static void erase_marked(std::pmr::vector<Face>& faces, const std::pmr::vector<bool>& to_delete)
|
||||
{
|
||||
auto* mr = faces.get_allocator().resource(); // keep same resource
|
||||
std::pmr::vector<Face> kept{mr};
|
||||
kept.reserve(faces.size());
|
||||
|
||||
for (std::size_t i = 0; i < faces.size(); ++i)
|
||||
if (!to_delete[i])
|
||||
kept.emplace_back(faces[i]);
|
||||
|
||||
faces.swap(kept);
|
||||
}
|
||||
struct Horizon
|
||||
{
|
||||
std::pmr::vector<bool> to_delete;
|
||||
std::pmr::vector<Edge> boundary;
|
||||
};
|
||||
|
||||
static Horizon mark_visible_and_collect_horizon(const std::pmr::vector<Face>& faces, const VectorType& p)
|
||||
{
|
||||
auto* mr = faces.get_allocator().resource();
|
||||
|
||||
Horizon horizon{std::pmr::vector<bool>(faces.size(), false, mr), std::pmr::vector<Edge>(mr)};
|
||||
horizon.boundary.reserve(faces.size());
|
||||
|
||||
for (std::size_t i = 0; i < faces.size(); ++i)
|
||||
if (visible_from(faces[i], p))
|
||||
{
|
||||
const auto& rf = faces[i];
|
||||
horizon.to_delete[i] = true;
|
||||
add_edge_boundary(horizon.boundary, rf.i0, rf.i1);
|
||||
add_edge_boundary(horizon.boundary, rf.i1, rf.i2);
|
||||
add_edge_boundary(horizon.boundary, rf.i2, rf.i0);
|
||||
}
|
||||
|
||||
return horizon;
|
||||
}
|
||||
};
|
||||
} // namespace omath::collision
|
||||
62
include/omath/collision/gjk_algorithm.hpp
Normal file
62
include/omath/collision/gjk_algorithm.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// Created by Vlad on 11/9/2025.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#include "simplex.hpp"
|
||||
|
||||
namespace omath::collision
|
||||
{
|
||||
template<class VertexType>
|
||||
struct GjkHitInfo final
|
||||
{
|
||||
bool hit{false};
|
||||
Simplex<VertexType> simplex; // valid only if hit == true and size==4
|
||||
};
|
||||
|
||||
template<class ColliderInterfaceType>
|
||||
class GjkAlgorithm final
|
||||
{
|
||||
using VectorType = ColliderInterfaceType::VectorType;
|
||||
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static VectorType find_support_vertex(const ColliderInterfaceType& collider_a,
|
||||
const ColliderInterfaceType& collider_b, const VectorType& direction)
|
||||
{
|
||||
return collider_a.find_abs_furthest_vertex_position(direction)
|
||||
- collider_b.find_abs_furthest_vertex_position(-direction);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static bool is_collide(const ColliderInterfaceType& collider_a, const ColliderInterfaceType& collider_b)
|
||||
{
|
||||
return is_collide_with_simplex_info(collider_a, collider_b).hit;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static GjkHitInfo<VectorType> is_collide_with_simplex_info(const ColliderInterfaceType& collider_a,
|
||||
const ColliderInterfaceType& collider_b)
|
||||
{
|
||||
auto support = find_support_vertex(collider_a, collider_b, VectorType{1, 0, 0});
|
||||
|
||||
Simplex<VectorType> simplex;
|
||||
simplex.push_front(support);
|
||||
|
||||
auto direction = -support;
|
||||
|
||||
while (true)
|
||||
{
|
||||
support = find_support_vertex(collider_a, collider_b, direction);
|
||||
|
||||
if (support.dot(direction) <= 0.f)
|
||||
return {false, simplex};
|
||||
|
||||
simplex.push_front(support);
|
||||
|
||||
if (simplex.handle(direction))
|
||||
return {true, simplex};
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace omath::collision
|
||||
55
include/omath/collision/mesh_collider.hpp
Normal file
55
include/omath/collision/mesh_collider.hpp
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// Created by Vlad on 11/9/2025.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#include "collider_interface.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
|
||||
#ifdef OMATH_BUILD_TESTS
|
||||
// ReSharper disable once CppInconsistentNaming
|
||||
class UnitTestColider_FindFurthestVertex_Test;
|
||||
#endif
|
||||
|
||||
namespace omath::collision
|
||||
{
|
||||
template<class MeshType>
|
||||
class MeshCollider final : public ColliderInterface<typename MeshType::VertexType::VectorType>
|
||||
{
|
||||
#ifdef OMATH_BUILD_TESTS
|
||||
friend UnitTestColider_FindFurthestVertex_Test;
|
||||
#endif
|
||||
public:
|
||||
using VertexType = MeshType::VertexType;
|
||||
using VectorType = MeshType::VertexType::VectorType;
|
||||
explicit MeshCollider(MeshType mesh): m_mesh(std::move(mesh))
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
VectorType find_abs_furthest_vertex_position(const VectorType& direction) const override
|
||||
{
|
||||
return m_mesh.vertex_position_to_world_space(find_furthest_vertex(direction).position);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
const VectorType& get_origin() const override
|
||||
{
|
||||
return m_mesh.get_origin();
|
||||
}
|
||||
void set_origin(const VectorType& new_origin) override
|
||||
{
|
||||
m_mesh.set_origin(new_origin);
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]]
|
||||
const VertexType& find_furthest_vertex(const VectorType& direction) const
|
||||
{
|
||||
return *std::ranges::max_element(
|
||||
m_mesh.m_vertex_buffer, [&direction](const auto& first, const auto& second)
|
||||
{ return first.position.dot(direction) < second.position.dot(direction); });
|
||||
}
|
||||
MeshType m_mesh;
|
||||
};
|
||||
} // namespace omath::collision
|
||||
260
include/omath/collision/simplex.hpp
Normal file
260
include/omath/collision/simplex.hpp
Normal file
@@ -0,0 +1,260 @@
|
||||
#pragma once
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <initializer_list>
|
||||
#include <type_traits>
|
||||
|
||||
namespace omath::collision
|
||||
{
|
||||
|
||||
// Minimal structural contract for the vector type used by GJK.
|
||||
template<class V>
|
||||
concept GjkVector = requires(const V& a, const V& b) {
|
||||
{ -a } -> std::same_as<V>;
|
||||
{ a - b } -> std::same_as<V>;
|
||||
{ a.cross(b) } -> std::same_as<V>;
|
||||
{ a.point_to_same_direction(b) } -> std::same_as<bool>;
|
||||
};
|
||||
|
||||
template<GjkVector VectorType = Vector3<float>>
|
||||
class Simplex final
|
||||
{
|
||||
std::array<VectorType, 4> m_points{};
|
||||
std::size_t m_size{0};
|
||||
|
||||
public:
|
||||
static constexpr std::size_t capacity = 4;
|
||||
|
||||
constexpr Simplex() = default;
|
||||
|
||||
constexpr Simplex& operator=(std::initializer_list<VectorType> list) noexcept
|
||||
{
|
||||
assert(list.size() <= capacity && "Simplex can have at most 4 points");
|
||||
m_size = 0;
|
||||
for (const auto& p : list)
|
||||
m_points[m_size++] = p;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr void push_front(const VectorType& p) noexcept
|
||||
{
|
||||
const std::size_t limit = (m_size < capacity) ? m_size : capacity - 1;
|
||||
for (std::size_t i = limit; i > 0; --i)
|
||||
m_points[i] = m_points[i - 1];
|
||||
m_points[0] = p;
|
||||
if (m_size < capacity)
|
||||
++m_size;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr const VectorType& operator[](std::size_t i) const noexcept
|
||||
{
|
||||
return m_points[i];
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr VectorType& operator[](std::size_t i) noexcept
|
||||
{
|
||||
return m_points[i];
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr std::size_t size() const noexcept
|
||||
{
|
||||
return m_size;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool empty() const noexcept
|
||||
{
|
||||
return m_size == 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr const VectorType& front() const noexcept
|
||||
{
|
||||
return m_points[0];
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr const VectorType& back() const noexcept
|
||||
{
|
||||
return m_points[m_size - 1];
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr const VectorType* data() const noexcept
|
||||
{
|
||||
return m_points.data();
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto begin() const noexcept
|
||||
{
|
||||
return m_points.begin();
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto end() const noexcept
|
||||
{
|
||||
return m_points.begin() + m_size;
|
||||
}
|
||||
|
||||
constexpr void clear() noexcept
|
||||
{
|
||||
m_size = 0;
|
||||
}
|
||||
|
||||
// GJK step: updates simplex + next search direction.
|
||||
// Returns true iff the origin lies inside the tetrahedron.
|
||||
[[nodiscard]] constexpr bool handle(VectorType& direction) noexcept
|
||||
{
|
||||
switch (m_size)
|
||||
{
|
||||
case 0:
|
||||
return false;
|
||||
case 1:
|
||||
return handle_point(direction);
|
||||
case 2:
|
||||
return handle_line(direction);
|
||||
case 3:
|
||||
return handle_triangle(direction);
|
||||
case 4:
|
||||
return handle_tetrahedron(direction);
|
||||
default:
|
||||
std::unreachable();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] constexpr bool handle_point(VectorType& direction) noexcept
|
||||
{
|
||||
const auto& a = m_points[0];
|
||||
direction = -a;
|
||||
return false;
|
||||
}
|
||||
|
||||
template<class V>
|
||||
[[nodiscard]]
|
||||
static constexpr bool near_zero(const V& v, const float eps = 1e-7f)
|
||||
{
|
||||
return v.dot(v) <= eps * eps;
|
||||
}
|
||||
|
||||
template<class V>
|
||||
[[nodiscard]]
|
||||
static constexpr V any_perp(const V& v)
|
||||
{
|
||||
for (const auto& dir : {V{1, 0, 0}, {0, 1, 0}, {0, 0, 1}})
|
||||
if (const auto d = v.cross(dir); !near_zero(d))
|
||||
return d;
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr bool handle_line(VectorType& direction)
|
||||
{
|
||||
const auto& a = m_points[0];
|
||||
const auto& b = m_points[1];
|
||||
|
||||
const auto ab = b - a;
|
||||
const auto ao = -a;
|
||||
|
||||
if (ab.point_to_same_direction(ao))
|
||||
{
|
||||
// ReSharper disable once CppTooWideScopeInitStatement
|
||||
auto n = ab.cross(ao); // Needed to valid handle collision if colliders placed at same origin pos
|
||||
if (near_zero(n))
|
||||
{
|
||||
// collinear: origin lies on ray AB (often on segment), pick any perp to escape
|
||||
direction = any_perp(ab);
|
||||
}
|
||||
else
|
||||
{
|
||||
direction = n.cross(ab);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
*this = {a};
|
||||
direction = ao;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool handle_triangle(VectorType& direction) noexcept
|
||||
{
|
||||
const auto& a = m_points[0];
|
||||
const auto& b = m_points[1];
|
||||
const auto& c = m_points[2];
|
||||
|
||||
const auto ab = b - a;
|
||||
const auto ac = c - a;
|
||||
const auto ao = -a;
|
||||
|
||||
const auto abc = ab.cross(ac);
|
||||
|
||||
// Region AC
|
||||
if (abc.cross(ac).point_to_same_direction(ao))
|
||||
{
|
||||
if (ac.point_to_same_direction(ao))
|
||||
{
|
||||
*this = {a, c};
|
||||
direction = ac.cross(ao).cross(ac);
|
||||
return false;
|
||||
}
|
||||
*this = {a, b};
|
||||
return handle_line(direction);
|
||||
}
|
||||
|
||||
// Region AB
|
||||
if (ab.cross(abc).point_to_same_direction(ao))
|
||||
{
|
||||
*this = {a, b};
|
||||
return handle_line(direction);
|
||||
}
|
||||
|
||||
// Above or below triangle
|
||||
if (abc.point_to_same_direction(ao))
|
||||
{
|
||||
direction = abc;
|
||||
}
|
||||
else
|
||||
{
|
||||
*this = {a, c, b}; // flip winding
|
||||
direction = -abc;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool handle_tetrahedron(VectorType& direction) noexcept
|
||||
{
|
||||
const auto& a = m_points[0];
|
||||
const auto& b = m_points[1];
|
||||
const auto& c = m_points[2];
|
||||
const auto& d = m_points[3];
|
||||
|
||||
const auto ab = b - a;
|
||||
const auto ac = c - a;
|
||||
const auto ad = d - a;
|
||||
const auto ao = -a;
|
||||
|
||||
const auto abc = ab.cross(ac);
|
||||
const auto acd = ac.cross(ad);
|
||||
const auto adb = ad.cross(ab);
|
||||
|
||||
if (abc.point_to_same_direction(ao))
|
||||
{
|
||||
*this = {a, b, c};
|
||||
return handle_triangle(direction);
|
||||
}
|
||||
if (acd.point_to_same_direction(ao))
|
||||
{
|
||||
*this = {a, c, d};
|
||||
return handle_triangle(direction);
|
||||
}
|
||||
if (adb.point_to_same_direction(ao))
|
||||
{
|
||||
*this = {a, d, b};
|
||||
return handle_triangle(direction);
|
||||
}
|
||||
// Origin inside tetrahedron
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace omath::collision
|
||||
12
include/omath/engines/frostbite_engine/mesh.hpp
Normal file
12
include/omath/engines/frostbite_engine/mesh.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include "constants.hpp"
|
||||
#include "omath/3d_primitives/mesh.hpp"
|
||||
#include "traits/mesh_trait.hpp"
|
||||
|
||||
namespace omath::frostbite_engine
|
||||
{
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait>;
|
||||
}
|
||||
19
include/omath/engines/frostbite_engine/traits/mesh_trait.hpp
Normal file
19
include/omath/engines/frostbite_engine/traits/mesh_trait.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include <omath/engines/frostbite_engine/constants.hpp>
|
||||
#include <omath/engines/frostbite_engine/formulas.hpp>
|
||||
|
||||
namespace omath::frostbite_engine
|
||||
{
|
||||
class MeshTrait final
|
||||
{
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation)
|
||||
{
|
||||
return frostbite_engine::rotation_matrix(rotation);
|
||||
}
|
||||
};
|
||||
} // namespace omath::frostbite_engine
|
||||
12
include/omath/engines/iw_engine/mesh.hpp
Normal file
12
include/omath/engines/iw_engine/mesh.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include "constants.hpp"
|
||||
#include "omath/3d_primitives/mesh.hpp"
|
||||
#include "traits/mesh_trait.hpp"
|
||||
|
||||
namespace omath::iw_engine
|
||||
{
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait>;
|
||||
}
|
||||
19
include/omath/engines/iw_engine/traits/mesh_trait.hpp
Normal file
19
include/omath/engines/iw_engine/traits/mesh_trait.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include <omath/engines/iw_engine/constants.hpp>
|
||||
#include <omath/engines/iw_engine/formulas.hpp>
|
||||
|
||||
namespace omath::iw_engine
|
||||
{
|
||||
class MeshTrait final
|
||||
{
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation)
|
||||
{
|
||||
return iw_engine::rotation_matrix(rotation);
|
||||
}
|
||||
};
|
||||
} // namespace omath::iw_engine
|
||||
12
include/omath/engines/opengl_engine/mesh.hpp
Normal file
12
include/omath/engines/opengl_engine/mesh.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include "constants.hpp"
|
||||
#include "omath/3d_primitives/mesh.hpp"
|
||||
#include "traits/mesh_trait.hpp"
|
||||
|
||||
namespace omath::opengl_engine
|
||||
{
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait>;
|
||||
}
|
||||
19
include/omath/engines/opengl_engine/traits/mesh_trait.hpp
Normal file
19
include/omath/engines/opengl_engine/traits/mesh_trait.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include <omath/engines/opengl_engine/constants.hpp>
|
||||
#include <omath/engines/opengl_engine/formulas.hpp>
|
||||
|
||||
namespace omath::opengl_engine
|
||||
{
|
||||
class MeshTrait final
|
||||
{
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation)
|
||||
{
|
||||
return opengl_engine::rotation_matrix(rotation);
|
||||
}
|
||||
};
|
||||
} // namespace omath::opengl_engine
|
||||
13
include/omath/engines/source_engine/collider.hpp
Normal file
13
include/omath/engines/source_engine/collider.hpp
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#include "mesh.hpp"
|
||||
#include "omath/3d_primitives/mesh.hpp"
|
||||
#include "omath/collision/mesh_collider.hpp"
|
||||
|
||||
namespace omath::source_engine
|
||||
{
|
||||
using MeshCollider = collision::MeshCollider<Mesh>;
|
||||
}
|
||||
12
include/omath/engines/source_engine/mesh.hpp
Normal file
12
include/omath/engines/source_engine/mesh.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include "constants.hpp"
|
||||
#include "omath/3d_primitives/mesh.hpp"
|
||||
#include "traits/mesh_trait.hpp"
|
||||
|
||||
namespace omath::source_engine
|
||||
{
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait>;
|
||||
}
|
||||
19
include/omath/engines/source_engine/traits/mesh_trait.hpp
Normal file
19
include/omath/engines/source_engine/traits/mesh_trait.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include <omath/engines/source_engine/constants.hpp>
|
||||
#include <omath/engines/source_engine/formulas.hpp>
|
||||
|
||||
namespace omath::source_engine
|
||||
{
|
||||
class MeshTrait final
|
||||
{
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation)
|
||||
{
|
||||
return source_engine::rotation_matrix(rotation);
|
||||
}
|
||||
};
|
||||
} // namespace omath::source_engine
|
||||
12
include/omath/engines/unity_engine/mesh.hpp
Normal file
12
include/omath/engines/unity_engine/mesh.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include "constants.hpp"
|
||||
#include "omath/3d_primitives/mesh.hpp"
|
||||
#include "traits/mesh_trait.hpp"
|
||||
|
||||
namespace omath::unity_engine
|
||||
{
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
19
include/omath/engines/unity_engine/traits/mesh_trait.hpp
Normal file
19
include/omath/engines/unity_engine/traits/mesh_trait.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include <omath/engines/unity_engine/constants.hpp>
|
||||
#include <omath/engines/unity_engine/formulas.hpp>
|
||||
|
||||
namespace omath::unity_engine
|
||||
{
|
||||
class MeshTrait final
|
||||
{
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation)
|
||||
{
|
||||
return unity_engine::rotation_matrix(rotation);
|
||||
}
|
||||
};
|
||||
} // namespace omath::unity_engine
|
||||
12
include/omath/engines/unreal_engine/mesh.hpp
Normal file
12
include/omath/engines/unreal_engine/mesh.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include "constants.hpp"
|
||||
#include "omath/3d_primitives/mesh.hpp"
|
||||
#include "traits/mesh_trait.hpp"
|
||||
|
||||
namespace omath::unreal_engine
|
||||
{
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
19
include/omath/engines/unreal_engine/traits/mesh_trait.hpp
Normal file
19
include/omath/engines/unreal_engine/traits/mesh_trait.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Created by Vladislav on 09.11.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include <omath/engines/unreal_engine/constants.hpp>
|
||||
#include <omath/engines/unreal_engine/formulas.hpp>
|
||||
|
||||
namespace omath::unreal_engine
|
||||
{
|
||||
class MeshTrait final
|
||||
{
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation)
|
||||
{
|
||||
return unreal_engine::rotation_matrix(rotation);
|
||||
}
|
||||
};
|
||||
} // namespace omath::unreal_engine
|
||||
@@ -46,7 +46,7 @@ namespace omath
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr static MatStoreType get_store_ordering() noexcept
|
||||
consteval static MatStoreType get_store_ordering() noexcept
|
||||
{
|
||||
return StoreType;
|
||||
}
|
||||
@@ -586,6 +586,17 @@ namespace omath
|
||||
{0, 0, 0, 1},
|
||||
};
|
||||
}
|
||||
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
|
||||
[[nodiscard]]
|
||||
constexpr Mat<4, 4, Type, St> mat_scale(const Vector3<Type>& scale) noexcept
|
||||
{
|
||||
return {
|
||||
{scale.x, 0, 0, 0},
|
||||
{0, scale.y, 0, 0},
|
||||
{0, 0, scale.z, 0},
|
||||
{0, 0, 0, 1},
|
||||
};
|
||||
}
|
||||
|
||||
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR, class Angle>
|
||||
[[nodiscard]]
|
||||
|
||||
@@ -26,12 +26,12 @@ namespace omath
|
||||
{
|
||||
}
|
||||
|
||||
Vector3<float> m_vertex1;
|
||||
Vector3<float> m_vertex2;
|
||||
Vector3<float> m_vertex3;
|
||||
Vector m_vertex1;
|
||||
Vector m_vertex2;
|
||||
Vector m_vertex3;
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr Vector3<float> calculate_normal() const
|
||||
constexpr Vector calculate_normal() const
|
||||
{
|
||||
const auto b = side_b_vector();
|
||||
const auto a = side_a_vector();
|
||||
@@ -40,25 +40,25 @@ namespace omath
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
float side_a_length() const
|
||||
Vector::ContainedType side_a_length() const
|
||||
{
|
||||
return m_vertex1.distance_to(m_vertex2);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
float side_b_length() const
|
||||
Vector::ContainedType side_b_length() const
|
||||
{
|
||||
return m_vertex3.distance_to(m_vertex2);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr Vector3<float> side_a_vector() const
|
||||
constexpr Vector side_a_vector() const
|
||||
{
|
||||
return m_vertex1 - m_vertex2;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr float hypot() const
|
||||
constexpr Vector::ContainedType hypot() const
|
||||
{
|
||||
return m_vertex1.distance_to(m_vertex3);
|
||||
}
|
||||
@@ -72,12 +72,12 @@ namespace omath
|
||||
return std::abs(side_a * side_a + side_b * side_b - hypot_value * hypot_value) <= 0.0001f;
|
||||
}
|
||||
[[nodiscard]]
|
||||
constexpr Vector3<float> side_b_vector() const
|
||||
constexpr Vector side_b_vector() const
|
||||
{
|
||||
return m_vertex3 - m_vertex2;
|
||||
}
|
||||
[[nodiscard]]
|
||||
constexpr Vector3<float> mid_point() const
|
||||
constexpr Vector mid_point() const
|
||||
{
|
||||
return (m_vertex1 + m_vertex2 + m_vertex3) / 3;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace omath
|
||||
class Vector2
|
||||
{
|
||||
public:
|
||||
using ContainedType = Type;
|
||||
Type x = static_cast<Type>(0);
|
||||
Type y = static_cast<Type>(0);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace omath
|
||||
class Vector3 : public Vector2<Type>
|
||||
{
|
||||
public:
|
||||
using ContainedType = Type;
|
||||
Type z = static_cast<Type>(0);
|
||||
constexpr Vector3(const Type& x, const Type& y, const Type& z) noexcept: Vector2<Type>(x, y), z(z)
|
||||
{
|
||||
@@ -216,6 +217,11 @@ namespace omath
|
||||
return sum_2d() + z;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool point_to_same_direction(const Vector3& other) const
|
||||
{
|
||||
return dot(other) > static_cast<Type>(0);
|
||||
}
|
||||
[[nodiscard]] std::expected<Angle<float, 0.f, 180.f, AngleFlags::Clamped>, Vector3Error>
|
||||
angle_between(const Vector3& other) const noexcept
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace omath
|
||||
class Vector4 : public Vector3<Type>
|
||||
{
|
||||
public:
|
||||
using ContainedType = Type;
|
||||
Type w;
|
||||
|
||||
constexpr Vector4(const Type& x, const Type& y, const Type& z, const Type& w): Vector3<Type>(x, y, z), w(w)
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
|
||||
// Collision detection
|
||||
#include "omath/collision/line_tracer.hpp"
|
||||
|
||||
#include "omath/collision/gjk_algorithm.hpp"
|
||||
#include "omath/collision/epa_algorithm.hpp"
|
||||
// Pathfinding algorithms
|
||||
#include "omath/pathfinding/a_star.hpp"
|
||||
#include "omath/pathfinding/navigation_mesh.hpp"
|
||||
|
||||
@@ -5,15 +5,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/projection/error_codes.hpp"
|
||||
#include <omath/trigonometry/angle.hpp>
|
||||
#include <expected>
|
||||
#include <omath/trigonometry/angle.hpp>
|
||||
#include <type_traits>
|
||||
|
||||
#ifdef OMATH_BUILD_TESTS
|
||||
// ReSharper disable once CppInconsistentNaming
|
||||
// ReSharper disable CppInconsistentNaming
|
||||
class UnitTestProjection_Projection_Test;
|
||||
class UnitTestProjection_ScreenToNdcTopLeft_Test;
|
||||
class UnitTestProjection_ScreenToNdcBottomLeft_Test;
|
||||
// ReSharper restore CppInconsistentNaming
|
||||
|
||||
#endif
|
||||
|
||||
namespace omath::projection
|
||||
@@ -52,6 +57,8 @@ namespace omath::projection
|
||||
{
|
||||
#ifdef OMATH_BUILD_TESTS
|
||||
friend UnitTestProjection_Projection_Test;
|
||||
friend UnitTestProjection_ScreenToNdcTopLeft_Test;
|
||||
friend UnitTestProjection_ScreenToNdcBottomLeft_Test;
|
||||
#endif
|
||||
public:
|
||||
enum class ScreenStart
|
||||
@@ -152,7 +159,6 @@ namespace omath::projection
|
||||
return m_origin;
|
||||
}
|
||||
|
||||
|
||||
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
|
||||
[[nodiscard]] std::expected<Vector3<float>, Error>
|
||||
world_to_screen(const Vector3<float>& world_position) const noexcept
|
||||
@@ -170,6 +176,53 @@ namespace omath::projection
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_culled_by_frustum(const Triangle<Vector3<float>>& triangle) const noexcept
|
||||
{
|
||||
// Transform to clip space (before perspective divide)
|
||||
auto to_clip = [this](const Vector3<float>& point)
|
||||
{
|
||||
auto clip = get_view_projection_matrix()
|
||||
* mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(point);
|
||||
return std::array<float, 4>{
|
||||
clip.at(0, 0), // x
|
||||
clip.at(1, 0), // y
|
||||
clip.at(2, 0), // z
|
||||
clip.at(3, 0) // w
|
||||
};
|
||||
};
|
||||
|
||||
const auto c0 = to_clip(triangle.m_vertex1);
|
||||
const auto c1 = to_clip(triangle.m_vertex2);
|
||||
const auto c2 = to_clip(triangle.m_vertex3);
|
||||
|
||||
// If all vertices are behind the camera (w <= 0), trivially reject
|
||||
if (c0[3] <= 0.f && c1[3] <= 0.f && c2[3] <= 0.f)
|
||||
return true;
|
||||
|
||||
// Helper: all three vertices outside the same clip plane
|
||||
auto all_outside_plane = [](const int axis, const std::array<float, 4>& a, const std::array<float, 4>& b,
|
||||
const std::array<float, 4>& c, const bool positive_side)
|
||||
{
|
||||
if (positive_side)
|
||||
return a[axis] > a[3] && b[axis] > b[3] && c[axis] > c[3];
|
||||
return a[axis] < -a[3] && b[axis] < -b[3] && c[axis] < -c[3];
|
||||
};
|
||||
|
||||
// Clip volume in clip space (OpenGL-style):
|
||||
// -w <= x <= w
|
||||
// -w <= y <= w
|
||||
// -w <= z <= w
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
if (all_outside_plane(i, c0, c1, c2, false))
|
||||
return true; // x < -w (left)
|
||||
if (all_outside_plane(i, c0, c1, c2, true))
|
||||
return true; // x > w (right)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<Vector3<float>, Error>
|
||||
world_to_view_port(const Vector3<float>& world_position) const noexcept
|
||||
{
|
||||
@@ -206,17 +259,19 @@ namespace omath::projection
|
||||
inverted_projection.at(2, 0)};
|
||||
}
|
||||
|
||||
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
|
||||
[[nodiscard]]
|
||||
std::expected<Vector3<float>, Error> screen_to_world(const Vector3<float>& screen_pos) const noexcept
|
||||
{
|
||||
return view_port_to_screen(screen_to_ndc(screen_pos));
|
||||
return view_port_to_screen(screen_to_ndc<screen_start>(screen_pos));
|
||||
}
|
||||
|
||||
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
|
||||
[[nodiscard]]
|
||||
std::expected<Vector3<float>, Error> screen_to_world(const Vector2<float>& screen_pos) const noexcept
|
||||
{
|
||||
const auto& [x, y] = screen_pos;
|
||||
return screen_to_world({x, y, 1.f});
|
||||
return screen_to_world<screen_start>({x, y, 1.f});
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -286,10 +341,17 @@ namespace omath::projection
|
||||
return {(ndc.x + 1.f) / 2.f * m_view_port.m_width, (ndc.y / 2.f + 0.5f) * m_view_port.m_height, ndc.z};
|
||||
}
|
||||
|
||||
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
|
||||
[[nodiscard]] Vector3<float> screen_to_ndc(const Vector3<float>& screen_pos) const noexcept
|
||||
{
|
||||
return {screen_pos.x / m_view_port.m_width * 2.f - 1.f, 1.f - screen_pos.y / m_view_port.m_height * 2.f,
|
||||
screen_pos.z};
|
||||
if constexpr (screen_start == ScreenStart::TOP_LEFT_CORNER)
|
||||
return {screen_pos.x / m_view_port.m_width * 2.f - 1.f, 1.f - screen_pos.y / m_view_port.m_height * 2.f,
|
||||
screen_pos.z};
|
||||
else if (screen_start == ScreenStart::BOTTOM_LEFT_CORNER)
|
||||
return {screen_pos.x / m_view_port.m_width * 2.f - 1.f,
|
||||
(screen_pos.y / m_view_port.m_height - 0.5f) * 2.f, screen_pos.z};
|
||||
else
|
||||
std::unreachable();
|
||||
}
|
||||
};
|
||||
} // namespace omath::projection
|
||||
|
||||
@@ -23,27 +23,26 @@ namespace omath::rev_eng
|
||||
return *reinterpret_cast<Type*>(reinterpret_cast<std::uintptr_t>(this) + offset);
|
||||
}
|
||||
|
||||
template<std::size_t id, class ReturnType, class... Args>
|
||||
ReturnType call_virtual_method(Args&&... arg_list)
|
||||
template<std::size_t id, class ReturnType>
|
||||
ReturnType call_virtual_method(auto... arg_list)
|
||||
{
|
||||
#ifdef _MSC_VER
|
||||
using VirtualMethodType = ReturnType(__thiscall*)(void*, decltype(arg_list)...);
|
||||
#else
|
||||
using VirtualMethodType = ReturnType (*)(void*, decltype(arg_list)...);
|
||||
#endif
|
||||
return (*reinterpret_cast<VirtualMethodType**>(this))[id](this, std::forward<Args>(arg_list)...);
|
||||
return (*reinterpret_cast<VirtualMethodType**>(this))[id](this, arg_list...);
|
||||
}
|
||||
template<std::size_t id, class ReturnType, class... Args>
|
||||
ReturnType call_virtual_method(Args&&... arg_list) const
|
||||
template<std::size_t id, class ReturnType>
|
||||
ReturnType call_virtual_method(auto... arg_list) const
|
||||
{
|
||||
using This = std::remove_cv_t<std::remove_pointer_t<decltype(this)>>;
|
||||
#ifdef _MSC_VER
|
||||
using VirtualMethodType = ReturnType(__thiscall*)(const void*, decltype(arg_list)...);
|
||||
using VirtualMethodType = ReturnType(__thiscall*)(void*, decltype(arg_list)...);
|
||||
#else
|
||||
using VirtualMethodType = ReturnType (*)(void*, decltype(arg_list)...);
|
||||
#endif
|
||||
return (*reinterpret_cast<VirtualMethodType**>(const_cast<This*>(this)))[id](
|
||||
const_cast<void*>(static_cast<const void*>(this)), std::forward<Args>(arg_list)...);
|
||||
return (*static_cast<VirtualMethodType**>((void*)(this)))[id](
|
||||
const_cast<void*>(static_cast<const void*>(this)), arg_list...);
|
||||
}
|
||||
};
|
||||
} // namespace omath::rev_eng
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace omath
|
||||
{
|
||||
struct Hsv
|
||||
struct Hsv final
|
||||
{
|
||||
float hue{};
|
||||
float saturation{};
|
||||
|
||||
@@ -3,4 +3,5 @@ theme:
|
||||
name: darkly
|
||||
extra_css:
|
||||
- styles/center.css
|
||||
- styles/custom-header.css
|
||||
- styles/custom-header.css
|
||||
- styles/links.css
|
||||
@@ -37,13 +37,6 @@ namespace omath::unreal_engine
|
||||
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
|
||||
const float far) noexcept
|
||||
{
|
||||
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f);
|
||||
|
||||
return {
|
||||
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0},
|
||||
{0, 1.f / (fov_half_tan), 0, 0},
|
||||
{0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)},
|
||||
{0, 0, -1.f, 0},
|
||||
};
|
||||
return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far);
|
||||
}
|
||||
} // namespace omath::unreal_engine
|
||||
|
||||
@@ -320,7 +320,7 @@ namespace omath
|
||||
return std::visit(
|
||||
[base_address, &pattern](const auto& nt_header) -> std::optional<std::uintptr_t>
|
||||
{
|
||||
// Define .code segment as scan area
|
||||
// Define .text segment as scan area
|
||||
const auto start = nt_header.optional_header.base_of_code;
|
||||
const auto scan_size = nt_header.optional_header.size_code;
|
||||
|
||||
|
||||
@@ -82,6 +82,18 @@ TEST(unit_test_unreal_engine, ProjectTargetMovedFromCamera)
|
||||
EXPECT_NEAR(projected->y, 360, 0.00001f);
|
||||
}
|
||||
}
|
||||
TEST(unit_test_unreal_engine, ProjectTargetMovedFromCameraBehind)
|
||||
{
|
||||
constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f);
|
||||
const auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.01f, 10000.f);
|
||||
|
||||
for (float distance = 0.02f; distance < 9000.f; distance += 100.f)
|
||||
{
|
||||
const auto projected = cam.world_to_screen({-distance, 0, 0});
|
||||
|
||||
EXPECT_FALSE(projected.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(unit_test_unreal_engine, CameraSetAndGetFov)
|
||||
{
|
||||
|
||||
40
tests/general/unit_test_colider.cpp
Normal file
40
tests/general/unit_test_colider.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// Created by Vlad on 11/9/2025.
|
||||
//
|
||||
#include "omath/engines/source_engine/collider.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/collision/mesh_collider.hpp>
|
||||
|
||||
TEST(UnitTestColider, CheckToWorld)
|
||||
{
|
||||
omath::source_engine::Mesh mesh = {
|
||||
std::vector<omath::primitives::Vertex<>>{
|
||||
{ { 1.f, 1.f, 1.f }, {}, {} },
|
||||
{ {-1.f, -1.f, -1.f }, {}, {} }
|
||||
},
|
||||
{}
|
||||
};
|
||||
mesh.set_origin({0, 2, 0});
|
||||
const omath::source_engine::MeshCollider collider(mesh);
|
||||
|
||||
const auto vertex = collider.find_abs_furthest_vertex_position({1.f, 0.f, 0.f});
|
||||
|
||||
EXPECT_EQ(vertex, omath::Vector3<float>(1.f, 3.f, 1.f));
|
||||
}
|
||||
|
||||
TEST(UnitTestColider, FindFurthestVertex)
|
||||
{
|
||||
const omath::source_engine::Mesh mesh = {
|
||||
{
|
||||
{ { 1.f, 1.f, 1.f }, {}, {} }, // position, normal, uv
|
||||
{ {-1.f, -1.f, -1.f }, {}, {} }
|
||||
},
|
||||
{}
|
||||
};
|
||||
const omath::source_engine::MeshCollider collider(mesh);
|
||||
const auto vertex = collider.find_furthest_vertex({1.f, 0.f, 0.f}).position;
|
||||
EXPECT_EQ(vertex, omath::Vector3<float>(1.f, 1.f, 1.f));
|
||||
}
|
||||
|
||||
|
||||
|
||||
153
tests/general/unit_test_epa.cpp
Normal file
153
tests/general/unit_test_epa.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#include "omath/collision/epa_algorithm.hpp" // Epa<Collider> + GjkAlgorithmWithSimplex<Collider>
|
||||
#include "omath/collision/gjk_algorithm.hpp"
|
||||
#include "omath/collision/simplex.hpp"
|
||||
#include "omath/engines/source_engine/collider.hpp"
|
||||
#include "omath/engines/source_engine/mesh.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory_resource>
|
||||
|
||||
using Mesh = omath::source_engine::Mesh;
|
||||
using Collider = omath::source_engine::MeshCollider;
|
||||
using GJK = omath::collision::GjkAlgorithm<Collider>;
|
||||
using EPA = omath::collision::Epa<Collider>;
|
||||
|
||||
TEST(UnitTestEpa, TestCollisionTrue)
|
||||
{
|
||||
// Unit cube [-1,1]^3
|
||||
std::vector<omath::primitives::Vertex<>> vbo = {
|
||||
{ {-1.f, -1.f, -1.f}, {}, {} },
|
||||
{ {-1.f, -1.f, 1.f}, {}, {} },
|
||||
{ {-1.f, 1.f, -1.f}, {}, {} },
|
||||
{ {-1.f, 1.f, 1.f}, {}, {} },
|
||||
{ { 1.f, 1.f, 1.f}, {}, {} },
|
||||
{ { 1.f, 1.f, -1.f}, {}, {} },
|
||||
{ { 1.f, -1.f, 1.f}, {}, {} },
|
||||
{ { 1.f, -1.f, -1.f}, {}, {} }
|
||||
};
|
||||
std::vector<omath::Vector3<std::uint32_t>> vao; // not needed
|
||||
|
||||
Mesh a(vbo, vao, {1, 1, 1});
|
||||
Mesh b(vbo, vao, {1, 1, 1});
|
||||
|
||||
// Overlap along +X by 0.5
|
||||
a.set_origin({0, 0, 0});
|
||||
b.set_origin({0.5f, 0, 0});
|
||||
|
||||
Collider A(a), B(b);
|
||||
|
||||
// GJK
|
||||
auto gjk = GJK::is_collide_with_simplex_info(A, B);
|
||||
ASSERT_TRUE(gjk.hit) << "GJK should report collision";
|
||||
|
||||
// EPA
|
||||
EPA::Params params;
|
||||
auto pool = std::make_shared<std::pmr::monotonic_buffer_resource>(1024);
|
||||
params.max_iterations = 64;
|
||||
params.tolerance = 1e-4f;
|
||||
auto epa = EPA::solve(A, B, gjk.simplex, params, *pool);
|
||||
ASSERT_TRUE(epa.has_value()) << "EPA should converge";
|
||||
|
||||
// Normal is unit
|
||||
EXPECT_NEAR(epa->normal.dot(epa->normal), 1.0f, 1e-5f);
|
||||
|
||||
// For this setup, depth ≈ 1.5 (2 - 0.5)
|
||||
EXPECT_NEAR(epa->depth, 1.5f, 1e-3f);
|
||||
|
||||
// Normal axis sanity: near X axis
|
||||
EXPECT_NEAR(std::abs(epa->normal.x), 1.0f, 1e-3f);
|
||||
EXPECT_NEAR(epa->normal.y, 0.0f, 1e-3f);
|
||||
EXPECT_NEAR(epa->normal.z, 0.0f, 1e-3f);
|
||||
|
||||
// Try both signs with a tiny margin (avoid grazing contacts)
|
||||
const float margin = 1.0f + 1e-3f;
|
||||
const auto pen = epa->penetration_vector;
|
||||
|
||||
Mesh b_plus = b;
|
||||
b_plus.set_origin(b_plus.get_origin() + pen * margin);
|
||||
Mesh b_minus = b;
|
||||
b_minus.set_origin(b_minus.get_origin() - pen * margin);
|
||||
|
||||
Collider B_plus(b_plus), B_minus(b_minus);
|
||||
|
||||
const bool sep_plus = !GJK::is_collide_with_simplex_info(A, B_plus).hit;
|
||||
const bool sep_minus = !GJK::is_collide_with_simplex_info(A, B_minus).hit;
|
||||
|
||||
// Exactly one direction should separate
|
||||
EXPECT_NE(sep_plus, sep_minus) << "Exactly one of ±penetration must separate";
|
||||
|
||||
// Optional: pick the resolving direction and assert round-trip
|
||||
const auto resolve = sep_plus ? (pen * margin) : (-pen * margin);
|
||||
|
||||
Mesh b_resolved = b;
|
||||
b_resolved.set_origin(b_resolved.get_origin() + resolve);
|
||||
EXPECT_FALSE(GJK::is_collide(A, Collider(b_resolved))) << "Resolved position should be non-colliding";
|
||||
|
||||
// Moving the other way should still collide
|
||||
Mesh b_wrong = b;
|
||||
b_wrong.set_origin(b_wrong.get_origin() - resolve);
|
||||
EXPECT_TRUE(GJK::is_collide(A, Collider(b_wrong)));
|
||||
}
|
||||
TEST(UnitTestEpa, TestCollisionTrue2)
|
||||
{
|
||||
std::vector<omath::primitives::Vertex<>> vbo = {
|
||||
{ { -1.f, -1.f, -1.f }, {}, {} },
|
||||
{ { -1.f, -1.f, 1.f }, {}, {} },
|
||||
{ { -1.f, 1.f, -1.f }, {}, {} },
|
||||
{ { -1.f, 1.f, 1.f }, {}, {} },
|
||||
{ { 1.f, 1.f, 1.f }, {}, {} },
|
||||
{ { 1.f, 1.f, -1.f }, {}, {} },
|
||||
{ { 1.f, -1.f, 1.f }, {}, {} },
|
||||
{ { 1.f, -1.f, -1.f }, {}, {} }
|
||||
};
|
||||
std::vector<omath::Vector3<std::uint32_t>> vao; // not needed
|
||||
|
||||
Mesh a(vbo, vao, {1, 1, 1});
|
||||
Mesh b(vbo, vao, {1, 1, 1});
|
||||
|
||||
// Overlap along +X by 0.5
|
||||
a.set_origin({0, 0, 0});
|
||||
b.set_origin({0.5f, 0, 0});
|
||||
|
||||
Collider A(a), B(b);
|
||||
|
||||
// --- GJK must detect collision and provide simplex ---
|
||||
auto gjk = GJK::is_collide_with_simplex_info(A, B);
|
||||
ASSERT_TRUE(gjk.hit) << "GJK should report collision for overlapping cubes";
|
||||
// --- EPA penetration ---
|
||||
EPA::Params params;
|
||||
params.max_iterations = 64;
|
||||
params.tolerance = 1e-4f;
|
||||
auto pool = std::make_shared<std::pmr::monotonic_buffer_resource>(1024);
|
||||
auto epa = EPA::solve(A, B, gjk.simplex, params, *pool);
|
||||
ASSERT_TRUE(epa.has_value()) << "EPA should converge";
|
||||
|
||||
// Normal is unit-length
|
||||
EXPECT_NEAR(epa->normal.dot(epa->normal), 1.0f, 1e-5f);
|
||||
|
||||
// For centers at 0 and +0.5 and half-extent 1 -> depth ≈ 1.5
|
||||
EXPECT_NEAR(epa->depth, 1.5f, 1e-3f);
|
||||
|
||||
// Axis sanity: mostly X
|
||||
EXPECT_NEAR(std::abs(epa->normal.x), 1.0f, 1e-3f);
|
||||
EXPECT_NEAR(epa->normal.y, 0.0f, 1e-3f);
|
||||
EXPECT_NEAR(epa->normal.z, 0.0f, 1e-3f);
|
||||
|
||||
constexpr float margin = 1.0f + 1e-3f; // tiny slack to avoid grazing
|
||||
const auto pen = epa->normal * epa->depth;
|
||||
|
||||
// Apply once: B + pen must separate; the opposite must still collide
|
||||
Mesh b_resolved = b;
|
||||
b_resolved.set_origin(b_resolved.get_origin() + pen * margin);
|
||||
EXPECT_FALSE(GJK::is_collide(A, Collider(b_resolved))) << "Applying penetration should separate";
|
||||
|
||||
Mesh b_wrong = b;
|
||||
b_wrong.set_origin(b_wrong.get_origin() - pen * margin);
|
||||
EXPECT_TRUE(GJK::is_collide(A, Collider(b_wrong))) << "Opposite direction should still intersect";
|
||||
|
||||
// Some book-keeping sanity
|
||||
EXPECT_GT(epa->iterations, 0);
|
||||
EXPECT_LT(epa->iterations, params.max_iterations);
|
||||
EXPECT_GE(epa->num_faces, 4);
|
||||
EXPECT_GT(epa->num_vertices, 4);
|
||||
}
|
||||
61
tests/general/unit_test_gjk.cpp
Normal file
61
tests/general/unit_test_gjk.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// Created by Vlad on 11/9/2025.
|
||||
//
|
||||
#include "omath/engines/source_engine/collider.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/collision/gjk_algorithm.hpp>
|
||||
#include <omath/engines/source_engine/mesh.hpp>
|
||||
namespace
|
||||
{
|
||||
const omath::source_engine::Mesh mesh = {
|
||||
{
|
||||
{ {-1.f, -1.f, -1.f}, {}, {} },
|
||||
{ {-1.f, -1.f, 1.f}, {}, {} },
|
||||
{ {-1.f, 1.f, -1.f}, {}, {} },
|
||||
{ {-1.f, 1.f, 1.f}, {}, {} },
|
||||
{ { 1.f, 1.f, 1.f}, {}, {} },
|
||||
{ { 1.f, 1.f, -1.f}, {}, {} },
|
||||
{ { 1.f, -1.f, 1.f}, {}, {} },
|
||||
{ { 1.f, -1.f, -1.f}, {}, {} }
|
||||
},
|
||||
{}
|
||||
};
|
||||
}
|
||||
TEST(UnitTestGjk, TestCollisionTrue)
|
||||
{
|
||||
const omath::source_engine::MeshCollider collider_a(mesh);
|
||||
|
||||
auto mesh_b = mesh;
|
||||
mesh_b.set_origin({0.f, 0.5f, 0.f});
|
||||
|
||||
const omath::source_engine::MeshCollider collider_b(mesh_b);
|
||||
|
||||
using GjkAlgorithm = omath::collision::GjkAlgorithm<omath::source_engine::MeshCollider>;
|
||||
|
||||
const auto result = GjkAlgorithm::is_collide(collider_a, collider_b);
|
||||
|
||||
EXPECT_TRUE(result);
|
||||
}
|
||||
TEST(UnitTestGjk, TestCollisionFalse)
|
||||
{
|
||||
const omath::source_engine::MeshCollider collider_a(mesh);
|
||||
auto mesh_b = mesh;
|
||||
mesh_b.set_origin({0.f, 2.1f, 0.f});
|
||||
const omath::source_engine::MeshCollider collider_b(mesh_b);
|
||||
|
||||
const auto result =
|
||||
omath::collision::GjkAlgorithm<omath::source_engine::MeshCollider>::is_collide(collider_a, collider_b);
|
||||
|
||||
EXPECT_FALSE(result);
|
||||
}
|
||||
|
||||
TEST(UnitTestGjk, TestCollisionEqualOrigin)
|
||||
{
|
||||
const omath::source_engine::MeshCollider collider_a(mesh);
|
||||
const omath::source_engine::MeshCollider collider_b(mesh);
|
||||
|
||||
const auto result =
|
||||
omath::collision::GjkAlgorithm<omath::source_engine::MeshCollider>::is_collide(collider_a, collider_b);
|
||||
|
||||
EXPECT_TRUE(result);
|
||||
}
|
||||
@@ -231,3 +231,13 @@ TEST(UnitTestMatStandalone, Equanity)
|
||||
|
||||
EXPECT_EQ(ndc_left_handed, ndc_right_handed);
|
||||
}
|
||||
TEST(UnitTestMatStandalone, MatPerspectiveLeftHanded)
|
||||
{
|
||||
auto perspective_proj = mat_perspective_left_handed(90.f, 16.f/9.f, 0.1f, 1000.f);
|
||||
auto projected = perspective_proj
|
||||
* mat_column_from_vector<float>({0, 0, 0.1001});
|
||||
|
||||
projected /= projected.at(3, 0);
|
||||
|
||||
EXPECT_TRUE(projected.at(2, 0) > -1.0f && projected.at(2, 0) < 0.f);
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
//
|
||||
// Created by Vlad on 27.08.2024.
|
||||
//
|
||||
#include "omath/engines/unity_engine/camera.hpp"
|
||||
#include <complex>
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/engines/source_engine/camera.hpp>
|
||||
#include <omath/projection/camera.hpp>
|
||||
#include <print>
|
||||
#include <random>
|
||||
|
||||
TEST(UnitTestProjection, Projection)
|
||||
{
|
||||
@@ -22,4 +24,76 @@ TEST(UnitTestProjection, Projection)
|
||||
EXPECT_NEAR(projected->x, 960.f, 0.001f);
|
||||
EXPECT_NEAR(projected->y, 504.f, 0.001f);
|
||||
EXPECT_NEAR(projected->z, 1.f, 0.001f);
|
||||
}
|
||||
TEST(UnitTestProjection, ScreenToNdcTopLeft)
|
||||
{
|
||||
constexpr auto fov = omath::Angle<float, 0.f, 180.f, omath::AngleFlags::Clamped>::from_degrees(90.f);
|
||||
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
|
||||
0.01f, 1000.f);
|
||||
using ScreenStart = omath::source_engine::Camera::ScreenStart;
|
||||
|
||||
const auto ndc_top_left = cam.screen_to_ndc<ScreenStart::TOP_LEFT_CORNER>({1500, 300, 1.f});
|
||||
EXPECT_NEAR(ndc_top_left.x, 0.5625f, 0.0001f);
|
||||
EXPECT_NEAR(ndc_top_left.y, 0.4444f, 0.0001f);
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, ScreenToNdcBottomLeft)
|
||||
{
|
||||
constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f);
|
||||
|
||||
const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f);
|
||||
using ScreenStart = omath::unity_engine::Camera::ScreenStart;
|
||||
|
||||
const auto ndc_bottom_left =
|
||||
cam.screen_to_ndc<ScreenStart::BOTTOM_LEFT_CORNER>({1263.53833f, 547.061523f, 0.99405992f});
|
||||
EXPECT_NEAR(ndc_bottom_left.x, 0.974278628f, 0.0001f);
|
||||
EXPECT_NEAR(ndc_bottom_left.y, 0.519615293f, 0.0001f);
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, ScreenToWorldTopLeftCorner)
|
||||
{
|
||||
std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source
|
||||
|
||||
std::uniform_real_distribution dist_x(1.f, 1900.f);
|
||||
std::uniform_real_distribution dist_y(1.f, 1070.f);
|
||||
|
||||
constexpr auto fov = omath::Angle<float, 0.f, 180.f, omath::AngleFlags::Clamped>::from_degrees(90.f);
|
||||
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
|
||||
0.01f, 1000.f);
|
||||
using ScreenStart = omath::source_engine::Camera::ScreenStart;
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
const auto initial_screen_cords = omath::Vector2{dist_x(gen), dist_y(gen)};
|
||||
|
||||
const auto world_cords = cam.screen_to_world<ScreenStart::TOP_LEFT_CORNER>(initial_screen_cords);
|
||||
const auto screen_cords = cam.world_to_screen<ScreenStart::TOP_LEFT_CORNER>(world_cords.value());
|
||||
|
||||
EXPECT_NEAR(screen_cords->x, initial_screen_cords.x, 0.001f);
|
||||
EXPECT_NEAR(screen_cords->y, initial_screen_cords.y, 0.001f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(UnitTestProjection, ScreenToWorldBottomLeftCorner)
|
||||
{
|
||||
std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source
|
||||
|
||||
std::uniform_real_distribution dist_x(1.f, 1900.f);
|
||||
std::uniform_real_distribution dist_y(1.f, 1070.f);
|
||||
|
||||
constexpr auto fov = omath::Angle<float, 0.f, 180.f, omath::AngleFlags::Clamped>::from_degrees(90.f);
|
||||
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
|
||||
0.01f, 1000.f);
|
||||
using ScreenStart = omath::source_engine::Camera::ScreenStart;
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
const auto initial_screen_cords = omath::Vector2{dist_x(gen), dist_y(gen)};
|
||||
|
||||
const auto world_cords = cam.screen_to_world<ScreenStart::BOTTOM_LEFT_CORNER>(initial_screen_cords);
|
||||
const auto screen_cords = cam.world_to_screen<ScreenStart::BOTTOM_LEFT_CORNER>(world_cords.value());
|
||||
|
||||
EXPECT_NEAR(screen_cords->x, initial_screen_cords.x, 0.001f);
|
||||
EXPECT_NEAR(screen_cords->y, initial_screen_cords.y, 0.001f);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "General purpose math library",
|
||||
"homepage": "https://github.com/orange-cpp/omath",
|
||||
"license": "Zlib",
|
||||
"supports": "windows | linux",
|
||||
"supports": "windows | linux | macos",
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "vcpkg-cmake",
|
||||
@@ -26,6 +26,13 @@
|
||||
"benchmark"
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"description": "Build benchmarks",
|
||||
"dependencies": [
|
||||
"glfw3",
|
||||
"glew"
|
||||
]
|
||||
},
|
||||
"imgui": {
|
||||
"description": "Omath will define method to convert omath types to imgui types",
|
||||
"dependencies": [
|
||||
|
||||
Reference in New Issue
Block a user