🎉 initial commit

This commit is contained in:
anyreso 2024-04-07 14:25:59 -04:00
commit d02447c7e6
185 changed files with 8442 additions and 0 deletions

81
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,81 @@
[[_TOC_]]
# How to Contribute?
Thank you for considering contributing to our project!
Whether you're an experienced developer or just starting out, we welcome your contributions to help make our game even better.
## Reporting Issues
If you encounter any bugs or issues, please report them in the [Issue Tracker](https://gitlab.com/godotribes/godotribes/-/issues). Include as much detail as possible, such as steps to reproduce the issue and expected vs. actual behavior.
## Merge Requests and Releases
We highly recommend using [gitmoji](https://gitmoji.dev) for expressive and visually appealing commit messages, as it provides an easy way of identifying the purpose or intention of a commit simply by looking at the emojis used.
Every **merge request** (MR) must be merged into the `develop` branch before any release is made on the `main` branch.
The `develop` branch serves as the staging area for upcoming features and fixes.
When the `develop` branch is deemed stable and ready for release, it is merged into the `main` branch to create a new release.
This practice ensures that everyone remains updated on ongoing tasks, fostering transparency and encouraging collaboration within the team.
# Development Guidelines
## Code Style Guide
For consistency across the source code, we *must* follow the [Godot Engine Style Guide](https://docs.godotengine.org/en/stable/tutorials/best_practices/project_organization.html#style-guide) at any time:
>- Use **snake_case** for folder and file names (with the exception of C#
> scripts). This sidesteps case sensitivity issues that can crop up after
> exporting a project on Windows. C# scripts are an exception to this rule,
> as the convention is to name them after the class name which should be
> in PascalCase.
>- Use **PascalCase** for node names, as this matches built-in node casing.
>- In general, keep third-party resources in a top-level `addons/` folder, even
> if they aren't editor plugins. This makes it easier to track which files are
> third-party. There are some exceptions to this rule; for instance, if you use
> third-party game assets for a character, it makes more sense to include them
> within the same folder as the character scenes and scripts.
## Branch Naming Convention
When working on a new *feature*, prefix the branch name with `feat/`. For *bug fixes*, use the prefix `fix/`.
This naming convention helps to categorize branches and makes it easier to identify their purpose at a glance.
# Git Quick Reference
1. Create a new branch for your changes:
```shell
git checkout -b fix/my-branch
```
2. Make your changes, stage then and commit:
```shell
git commit -am "📝 update CONTRIBUTIONS.md"
```
3. Push your changes to the repository:
```shell
git push
```
4. Create a [Merge Request (MR)](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) with a clear description of your changes
## Excluding local files without creating a _.gitignore_ file
If you don't want to add new rules in a `.gitignore` file to be shared with everyone, you can create *exclusion rules* that are not committed with the repository. You can use this technique for locally-generated files that you don't expect other users to generate, such as files created by your editor.
Use your favorite text editor to open the file called `.git/info/exclude` within the root of your git repository. Any rule you add here will not be checked in, and will only ignore files for your local repository.
---
By following these guidelines, we aim to streamline our development process, maintain code quality, and ensure that our releases are stable and reliable.
Happy coding! 🎮✨

438
LICENSE Normal file
View file

@ -0,0 +1,438 @@
Attribution-NonCommercial-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial-ShareAlike 4.0 International Public License
("Public License"). To the extent this Public License may be
interpreted as a contract, You are granted the Licensed Rights in
consideration of Your acceptance of these terms and conditions, and the
Licensor grants You such rights in consideration of benefits the
Licensor receives from making the Licensed Material available under
these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-NC-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution, NonCommercial, and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
l. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
m. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
n. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce, reproduce, and Share Adapted Material for
NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-NC-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.

24
README.md Normal file
View file

@ -0,0 +1,24 @@
# open-fpsz
We love fpsz video games, but we're tired of the genre being abandoned by prominent studios. To properly honor its legacy, we've chosen to develop our own game inspired by it, aiming to surpass its qualities with familiar jetpack and skiing mechanics.
## Prerequisities
Download `Godot Engine` at https://godotengine.org/download
## Getting started
1. Clone the repository
2. Open Godot Engine
3. Navigate to the repository and import the `project.godot`
4. Start coding or run the project!
## Contributing
Before contributing, please take a moment to review our [Contribution Guidelines](CONTRIBUTING.md) in this repository. These guidelines outline best practices, coding standards, and other important information to ensure that your contributions align with the project's goals and maintain consistency across the codebase.
We welcome contributions from the community to help improve and expand the project. Feel free to submit pull requests, report bugs, or share ideas for new features.
## License
This project is licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). For more details, please refer to the [LICENSE](LICENSE) file.

View file

@ -0,0 +1,18 @@
Copyright (c) Mikael Hermansson and Godot Jolt contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,91 @@
Godot Jolt incorporates third-party material from the projects listed below.
Godot Engine (https://github.com/godotengine/godot)
Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md).
Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
godot-cpp (https://github.com/godot-jolt/godot-cpp)
Copyright (c) 2017-present Godot Engine contributors.
Copyright (c) 2022-present Mikael Hermansson.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
Jolt Physics (https://github.com/godot-jolt/jolt)
Copyright (c) 2021 Jorrit Rouwe.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
mimalloc (https://github.com/godot-jolt/mimalloc)
Copyright (c) 2018-2021 Microsoft Corporation, Daan Leijen.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,36 @@
[configuration]
entry_symbol = "godot_jolt_main"
compatibility_minimum = "4.2"
[libraries]
windows.release.x86_64 = "windows/godot-jolt_windows-x64.dll"
windows.debug.x86_64 = "windows/godot-jolt_windows-x64_editor.dll"
windows.release.x86_32 = "windows/godot-jolt_windows-x86.dll"
windows.debug.x86_32 = "windows/godot-jolt_windows-x86_editor.dll"
linux.release.x86_64 = "linux/godot-jolt_linux-x64.so"
linux.debug.x86_64 = "linux/godot-jolt_linux-x64_editor.so"
linux.release.x86_32 = "linux/godot-jolt_linux-x86.so"
linux.debug.x86_32 = "linux/godot-jolt_linux-x86_editor.so"
macos.release = "macos/godot-jolt_macos.framework"
macos.debug = "macos/godot-jolt_macos_editor.framework"
ios.release = "ios/godot-jolt_ios.framework"
ios.debug = "ios/godot-jolt_ios_editor.framework"
android.release.arm64 = "android/libgodot-jolt_android-arm64.so"
android.debug.arm64 = "android/libgodot-jolt_android-arm64_editor.so"
android.release.arm32 = "android/libgodot-jolt_android-arm32.so"
android.debug.arm32 = "android/libgodot-jolt_android-arm32_editor.so"
android.release.x86_64 = "android/libgodot-jolt_android-x64.so"
android.debug.x86_64 = "android/libgodot-jolt_android-x64_editor.so"
android.release.x86_32 = "android/libgodot-jolt_android-x86.so"
android.debug.x86_32 = "android/libgodot-jolt_android-x86_editor.so"

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>godot-jolt_ios</string>
<key>CFBundleName</key>
<string>Godot Jolt</string>
<key>CFBundleDisplayName</key>
<string>Godot Jolt</string>
<key>CFBundleIdentifier</key>
<string>org.godot-jolt.godot-jolt</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.12.0</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>godot-jolt_ios_editor</string>
<key>CFBundleName</key>
<string>Godot Jolt</string>
<key>CFBundleDisplayName</key>
<string>Godot Jolt</string>
<key>CFBundleIdentifier</key>
<string>org.godot-jolt.godot-jolt</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.12.0</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>godot-jolt_macos</string>
<key>CFBundleName</key>
<string>Godot Jolt</string>
<key>CFBundleDisplayName</key>
<string>Godot Jolt</string>
<key>CFBundleIdentifier</key>
<string>org.godot-jolt.godot-jolt</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.12.0</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>macosx</string>
<key>LSMinimumSystemVersion</key>
<string>10.12</string>
</dict>
</plist>

View file

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Resources/Info.plist</key>
<data>
HxpaqrKUN+D+lkF90Phqlb+CZKo=
</data>
</dict>
<key>files2</key>
<dict>
<key>Resources/Info.plist</key>
<dict>
<key>hash2</key>
<data>
ZTtvl19PLRzCoTuDRMZ2FnJXIXsLYRhaBFxjroNsLDw=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^Resources/</key>
<true/>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>godot-jolt_macos_editor</string>
<key>CFBundleName</key>
<string>Godot Jolt</string>
<key>CFBundleDisplayName</key>
<string>Godot Jolt</string>
<key>CFBundleIdentifier</key>
<string>org.godot-jolt.godot-jolt</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.12.0</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>macosx</string>
<key>LSMinimumSystemVersion</key>
<string>10.12</string>
</dict>
</plist>

View file

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Resources/Info.plist</key>
<data>
CpU1kp9qZhdCVqKdSia+981vm40=
</data>
</dict>
<key>files2</key>
<dict>
<key>Resources/Info.plist</key>
<dict>
<key>hash2</key>
<data>
4CTRXI34pj3JIJzQDUUZTlAagKWQeohPQN20M0VCK4o=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^Resources/</key>
<true/>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Cory Petkovsek, Roope Palmroos, and Contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,55 @@
<img src="doc/docs/images/terrain3d.png">
# Terrain3D
A high performance, editable terrain system for Godot 4.
## Features
* Written in C++ as a GDExtension plugin, which works with official engine builds
* Can be accessed by GDScript, C#, and any language Godot supports
* Geometric Clipmap Mesh Terrain, as used in The Witcher 3. See [System Architecture](https://terrain3d.readthedocs.io/en/stable/docs/system_architecture.html)
* Up to 16k x 16k in 1k regions (imagine multiple islands without paying for 16k^2 vram)
* Up to 10 Levels of Detail (LODs)
* Up to 32 texture sets using albedo, normal, roughness, height
* Sculpting, holes, texture painting, texture detiling, painting colors and wetness
* Supports importing heightmaps from [HTerrain](https://github.com/Zylann/godot_heightmap_plugin/), WorldMachine, Unity, Unreal and any tool that can export a heightmap (raw/r16/exr/+). See [importing data](https://terrain3d.readthedocs.io/en/stable/docs/import_export.html)
See [Project Status](https://terrain3d.readthedocs.io/en/stable/docs/project_status.html) for details.
## Getting Started
1. Read through our [documentation](https://terrain3d.readthedocs.io/en/stable/index.html), starting with [Installation](https://terrain3d.readthedocs.io/en/stable/docs/installation.html).
2. For support, read [Getting Help](https://terrain3d.readthedocs.io/en/stable/docs/getting_help.html) or join our [Discord server](https://tokisan.com/discord).
3. Watch the tutorial videos:
**Installation, Setup, Basic Usage**
[![Using Terrain3D - Part 1](https://i.ytimg.com/vi/oV8c9alXVwU/hqdefault.jpg)](https://youtu.be/oV8c9alXVwU)
**Texture Painting, Holes, Navigation, Advanced Usage**
[![Using Terrain3D - Part 2](https://i.ytimg.com/vi/YtiAI2F6Xkk/hqdefault.jpg)](https://youtu.be/YtiAI2F6Xkk)
## Credit
Developed for the Godot community by:
|||
|--|--|
| **Cory Petkovsek, Tokisan Games** | [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/twitter.png?raw=true" width="24"/>](https://twitter.com/TokisanGames) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/github.png?raw=true" width="24"/>](https://github.com/TokisanGames) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/www.png?raw=true" width="24"/>](https://tokisan.com/) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/discord.png?raw=true" width="24"/>](https://tokisan.com/discord) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/youtube.png?raw=true" width="24"/>](https://www.youtube.com/@TokisanGames)|
| **Roope Palmroos, Outobugi Games** | [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/twitter.png?raw=true" width="24"/>](https://twitter.com/outobugi) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/github.png?raw=true" width="24"/>](https://github.com/outobugi) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/www.png?raw=true" width="24"/>](https://outobugi.com/) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/youtube.png?raw=true" width="24"/>](https://www.youtube.com/@outobugi)|
And other contributors displayed on the right of the github page and in [AUTHORS.md](https://github.com/TokisanGames/Terrain3D/blob/main/AUTHORS.md).
Geometry clipmap mesh code created by [Mike J. Savage](https://mikejsavage.co.uk/blog/geometry-clipmaps.html). Blog and repository code released under the MIT license per email communication with Mike.
## Contributing
Please see [CONTRIBUTING.md](https://github.com/TokisanGames/Terrain3D/blob/main/CONTRIBUTING.md) if you would like to help make Terrain3D the best terrain system for Godot.
## License
This plugin has been released under the [MIT License](https://github.com/TokisanGames/Terrain3D/blob/main/LICENSE.txt).

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,28 @@
@tool
extends ConfirmationDialog
var lod: int = 0
var description: String = ""
func _ready() -> void:
set_unparent_when_invisible(true)
about_to_popup.connect(_on_about_to_popup)
visibility_changed.connect(_on_visibility_changed)
%LodBox.value_changed.connect(_on_lod_box_value_changed)
func _on_about_to_popup() -> void:
lod = %LodBox.value
func _on_visibility_changed() -> void:
# Change text on the autowrap label only when the popup is visible.
# Works around Godot issue #47005:
# https://github.com/godotengine/godot/issues/47005
if visible:
%DescriptionLabel.text = description
func _on_lod_box_value_changed(p_value: float) -> void:
lod = %LodBox.value

View file

@ -0,0 +1,41 @@
[gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"]
[ext_resource type="Script" path="res://addons/terrain_3d/editor/components/bake_lod_dialog.gd" id="1_57670"]
[node name="bake_lod_dialog" type="ConfirmationDialog"]
title = "Bake Terrain3D Mesh"
position = Vector2i(0, 36)
size = Vector2i(400, 115)
visible = true
script = ExtResource("1_57670")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 20
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "LOD:"
[node name="LodBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
max_value = 8.0
value = 4.0
[node name="DescriptionLabel" type="Label" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
autowrap_mode = 2

View file

@ -0,0 +1,385 @@
extends Node
const BakeLodDialog: PackedScene = preload("res://addons/terrain_3d/editor/components/bake_lod_dialog.tscn")
const BAKE_MESH_DESCRIPTION: String = "This will create a child MeshInstance3D. LOD4+ is recommended. LOD0 is slow and dense with vertices every 1 unit. It is not an optimal mesh."
const BAKE_OCCLUDER_DESCRIPTION: String = "This will create a child OccluderInstance3D. LOD4+ is recommended and will take 5+ seconds per region to generate. LOD0 is unnecessarily dense and slow."
const SET_UP_NAVIGATION_DESCRIPTION: String = "This operation will:
- Create a NavigationRegion3D node,
- Assign it a blank NavigationMesh resource,
- Move the Terrain3D node to be a child of the new node,
- And bake the nav mesh.
Once setup is complete, you can modify the settings on your nav mesh, and rebake
without having to run through the setup again.
If preferred, this setup can be canceled and the steps performed manually. For
the best results, adjust the settings on the NavigationMesh resource to match
the settings of your navigation agents and collisions."
var plugin: EditorPlugin
var bake_method: Callable
var bake_lod_dialog: ConfirmationDialog
var confirm_dialog: ConfirmationDialog
func _enter_tree() -> void:
bake_lod_dialog = BakeLodDialog.instantiate()
bake_lod_dialog.hide()
bake_lod_dialog.confirmed.connect(func(): bake_method.call())
bake_lod_dialog.set_unparent_when_invisible(true)
confirm_dialog = ConfirmationDialog.new()
confirm_dialog.hide()
confirm_dialog.confirmed.connect(func(): bake_method.call())
confirm_dialog.set_unparent_when_invisible(true)
func _exit_tree() -> void:
bake_lod_dialog.queue_free()
confirm_dialog.queue_free()
func bake_mesh_popup() -> void:
if plugin.terrain:
bake_method = _bake_mesh
bake_lod_dialog.description = BAKE_MESH_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(bake_lod_dialog)
func _bake_mesh() -> void:
var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_NEAREST)
if !mesh:
push_error("Failed to bake mesh from Terrain3D")
return
var undo: EditorUndoRedoManager = plugin.get_undo_redo()
undo.create_action("Terrain3D Bake ArrayMesh")
var mesh_instance := plugin.terrain.get_node_or_null(^"MeshInstance3D") as MeshInstance3D
if !mesh_instance:
mesh_instance = MeshInstance3D.new()
mesh_instance.name = &"MeshInstance3D"
mesh_instance.set_skeleton_path(NodePath())
mesh_instance.mesh = mesh
undo.add_do_method(plugin.terrain, &"add_child", mesh_instance, true)
undo.add_undo_method(plugin.terrain, &"remove_child", mesh_instance)
undo.add_do_property(mesh_instance, &"owner", plugin.terrain.owner)
undo.add_do_reference(mesh_instance)
else:
undo.add_do_property(mesh_instance, &"mesh", mesh)
undo.add_undo_property(mesh_instance, &"mesh", mesh_instance.mesh)
if mesh_instance.mesh.resource_path:
var path := mesh_instance.mesh.resource_path
undo.add_do_method(mesh, &"take_over_path", path)
undo.add_undo_method(mesh_instance.mesh, &"take_over_path", path)
undo.add_do_method(ResourceSaver, &"save", mesh)
undo.add_undo_method(ResourceSaver, &"save", mesh_instance.mesh)
undo.commit_action()
func bake_occluder_popup() -> void:
if plugin.terrain:
bake_method = _bake_occluder
bake_lod_dialog.description = BAKE_OCCLUDER_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(bake_lod_dialog)
func _bake_occluder() -> void:
var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_MINIMUM)
if !mesh:
push_error("Failed to bake mesh from Terrain3D")
return
assert(mesh.get_surface_count() == 1)
var undo: EditorUndoRedoManager = plugin.get_undo_redo()
undo.create_action("Terrain3D Bake Occluder3D")
var occluder := ArrayOccluder3D.new()
var arrays: Array = mesh.surface_get_arrays(0)
assert(arrays.size() > Mesh.ARRAY_INDEX)
assert(arrays[Mesh.ARRAY_INDEX] != null)
occluder.set_arrays(arrays[Mesh.ARRAY_VERTEX], arrays[Mesh.ARRAY_INDEX])
var occluder_instance := plugin.terrain.get_node_or_null(^"OccluderInstance3D") as OccluderInstance3D
if !occluder_instance:
occluder_instance = OccluderInstance3D.new()
occluder_instance.name = &"OccluderInstance3D"
occluder_instance.occluder = occluder
undo.add_do_method(plugin.terrain, &"add_child", occluder_instance, true)
undo.add_undo_method(plugin.terrain, &"remove_child", occluder_instance)
undo.add_do_property(occluder_instance, &"owner", plugin.terrain.owner)
undo.add_do_reference(occluder_instance)
else:
undo.add_do_property(occluder_instance, &"occluder", occluder)
undo.add_undo_property(occluder_instance, &"occluder", occluder_instance.occluder)
if occluder_instance.occluder.resource_path:
var path := occluder_instance.occluder.resource_path
undo.add_do_method(occluder, &"take_over_path", path)
undo.add_undo_method(occluder_instance.occluder, &"take_over_path", path)
undo.add_do_method(ResourceSaver, &"save", occluder)
undo.add_undo_method(ResourceSaver, &"save", occluder_instance.occluder)
undo.commit_action()
func find_nav_region_terrains(p_nav_region: NavigationRegion3D) -> Array[Terrain3D]:
var result: Array[Terrain3D] = []
if not p_nav_region.navigation_mesh:
return result
var source_mode: NavigationMesh.SourceGeometryMode
source_mode = p_nav_region.navigation_mesh.geometry_source_geometry_mode
if source_mode == NavigationMesh.SOURCE_GEOMETRY_ROOT_NODE_CHILDREN:
result.append_array(p_nav_region.find_children("", "Terrain3D", true, true))
return result
var group_nodes: Array = p_nav_region.get_tree().get_nodes_in_group(p_nav_region.navigation_mesh.geometry_source_group_name)
for node in group_nodes:
if node is Terrain3D:
result.push_back(node)
if source_mode == NavigationMesh.SOURCE_GEOMETRY_GROUPS_WITH_CHILDREN:
result.append_array(node.find_children("", "Terrain3D", true, true))
return result
func find_terrain_nav_regions(p_terrain: Terrain3D) -> Array[NavigationRegion3D]:
var result: Array[NavigationRegion3D] = []
var root: Node = plugin.get_editor_interface().get_edited_scene_root()
if not root:
return result
for nav_region in root.find_children("", "NavigationRegion3D", true, true):
if find_nav_region_terrains(nav_region).has(p_terrain):
result.push_back(nav_region)
return result
func bake_nav_mesh() -> void:
if plugin.nav_region:
# A NavigationRegion3D is selected. We only need to bake that one navmesh.
_bake_nav_region_nav_mesh(plugin.nav_region)
print("Terrain3DNavigation: Finished baking 1 NavigationMesh.")
elif plugin.terrain:
# A Terrain3D is selected. There are potentially multiple navmeshes to bake and we need to
# find them all. (The multiple navmesh use-case is likely on very large scenes with lots of
# geometry. Each navmesh in this case would define its own, non-overlapping, baking AABB, to
# cut down on the amount of geometry to bake. In a large open-world RPG, for instance, there
# could be a navmesh for each town.)
var nav_regions: Array[NavigationRegion3D] = find_terrain_nav_regions(plugin.terrain)
for nav_region in nav_regions:
_bake_nav_region_nav_mesh(nav_region)
print("Terrain3DNavigation: Finished baking %d NavigationMesh(es)." % nav_regions.size())
func _bake_nav_region_nav_mesh(p_nav_region: NavigationRegion3D) -> void:
var nav_mesh: NavigationMesh = p_nav_region.navigation_mesh
assert(nav_mesh != null)
var source_geometry_data := NavigationMeshSourceGeometryData3D.new()
NavigationMeshGenerator.parse_source_geometry_data(nav_mesh, source_geometry_data, p_nav_region)
for terrain in find_nav_region_terrains(p_nav_region):
var aabb: AABB = nav_mesh.filter_baking_aabb
aabb.position += nav_mesh.filter_baking_aabb_offset
aabb = p_nav_region.global_transform * aabb
var faces: PackedVector3Array = terrain.generate_nav_mesh_source_geometry(aabb)
if not faces.is_empty():
source_geometry_data.add_faces(faces, p_nav_region.global_transform.inverse())
NavigationMeshGenerator.bake_from_source_geometry_data(nav_mesh, source_geometry_data)
_postprocess_nav_mesh(nav_mesh)
# Assign null first to force the debug display to actually update:
p_nav_region.set_navigation_mesh(null)
p_nav_region.set_navigation_mesh(nav_mesh)
# Trigger save to disk if it is saved as an external file
if not nav_mesh.get_path().is_empty():
ResourceSaver.save(nav_mesh, nav_mesh.get_path(), ResourceSaver.FLAG_COMPRESS)
# Let other editor plugins and tool scripts know the nav mesh was just baked:
p_nav_region.bake_finished.emit()
func _postprocess_nav_mesh(p_nav_mesh: NavigationMesh) -> void:
# Post-process the nav mesh to work around Godot issue #85548
# Round all the vertices in the nav_mesh to the nearest cell_size/cell_height so that it doesn't
# contain any edges shorter than cell_size/cell_height (one cause of #85548).
var vertices: PackedVector3Array = _postprocess_nav_mesh_round_vertices(p_nav_mesh)
# Rounding vertices can collapse some edges to 0 length. We remove these edges, and any polygons
# that have been reduced to 0 area.
var polygons: Array[PackedInt32Array] = _postprocess_nav_mesh_remove_empty_polygons(p_nav_mesh, vertices)
# Another cause of #85548 is baking producing overlapping polygons. We remove these.
_postprocess_nav_mesh_remove_overlapping_polygons(p_nav_mesh, vertices, polygons)
p_nav_mesh.clear_polygons()
p_nav_mesh.set_vertices(vertices)
for polygon in polygons:
p_nav_mesh.add_polygon(polygon)
func _postprocess_nav_mesh_round_vertices(p_nav_mesh: NavigationMesh) -> PackedVector3Array:
assert(p_nav_mesh != null)
assert(p_nav_mesh.cell_size > 0.0)
assert(p_nav_mesh.cell_height > 0.0)
var cell_size: Vector3 = Vector3(p_nav_mesh.cell_size, p_nav_mesh.cell_height, p_nav_mesh.cell_size)
# Round a little harder to avoid rounding errors with non-power-of-two cell_size/cell_height
# causing the navigation map to put two non-matching edges in the same cell:
var round_factor := cell_size * 1.001
var vertices: PackedVector3Array = p_nav_mesh.get_vertices()
for i in range(vertices.size()):
vertices[i] = (vertices[i] / round_factor).floor() * round_factor
return vertices
func _postprocess_nav_mesh_remove_empty_polygons(p_nav_mesh: NavigationMesh, p_vertices: PackedVector3Array) -> Array[PackedInt32Array]:
var polygons: Array[PackedInt32Array] = []
for i in range(p_nav_mesh.get_polygon_count()):
var old_polygon: PackedInt32Array = p_nav_mesh.get_polygon(i)
var new_polygon: PackedInt32Array = []
# Remove duplicate vertices (introduced by rounding) from the polygon:
var polygon_vertices: PackedVector3Array = []
for index in old_polygon:
var vertex: Vector3 = p_vertices[index]
if polygon_vertices.has(vertex):
continue
polygon_vertices.push_back(vertex)
new_polygon.push_back(index)
# If we removed some vertices, we might be able to remove the polygon too:
if new_polygon.size() <= 2:
continue
polygons.push_back(new_polygon)
return polygons
func _postprocess_nav_mesh_remove_overlapping_polygons(p_nav_mesh: NavigationMesh, p_vertices: PackedVector3Array, p_polygons: Array[PackedInt32Array]) -> void:
# Occasionally, a baked nav mesh comes out with overlapping polygons:
# https://github.com/godotengine/godot/issues/85548#issuecomment-1839341071
# Until the bug is fixed in the engine, this function attempts to detect and remove overlapping
# polygons.
# This function has to make a choice of which polygon to remove when an overlap is detected,
# because in this case the nav mesh is ambiguous. To do this it uses a heuristic:
# (1) an 'overlap' is defined as an edge that is shared by 3 or more polygons.
# (2) a 'bad polygon' is defined as a polygon that contains 2 or more 'overlaps'.
# The function removes the 'bad polygons', which in practice seems to be enough to remove all
# overlaps without creating holes in the nav mesh.
var cell_size: Vector3 = Vector3(p_nav_mesh.cell_size, p_nav_mesh.cell_height, p_nav_mesh.cell_size)
# `edges` is going to map edges (vertex pairs) to arrays of polygons that contain that edge.
var edges: Dictionary = {}
for polygon_index in range(p_polygons.size()):
var polygon: PackedInt32Array = p_polygons[polygon_index]
for j in range(polygon.size()):
var vertex: Vector3 = p_vertices[polygon[j]]
var next_vertex: Vector3 = p_vertices[polygon[(j + 1) % polygon.size()]]
# edge_key is a key we can use in the edges dictionary that uniquely identifies the
# edge. We use cell coordinates here (Vector3i) because with a non-power-of-two
# cell_size, rounding errors can cause Vector3 vertices to not be equal.
# Array.sort IS defined for vector types - see the Godot docs. It's necessary here
# because polygons that share an edge can have their vertices in a different order.
var edge_key: Array = [Vector3i(vertex / cell_size), Vector3i(next_vertex / cell_size)]
edge_key.sort()
if !edges.has(edge_key):
edges[edge_key] = []
edges[edge_key].push_back(polygon_index)
var overlap_count: Dictionary = {}
for connections in edges.values():
if connections.size() <= 2:
continue
for polygon_index in connections:
overlap_count[polygon_index] = overlap_count.get(polygon_index, 0) + 1
var bad_polygons: Array = []
for polygon_index in overlap_count.keys():
if overlap_count[polygon_index] >= 2:
bad_polygons.push_back(polygon_index)
bad_polygons.sort()
for i in range(bad_polygons.size() - 1, -1, -1):
p_polygons.remove_at(bad_polygons[i])
func set_up_navigation_popup() -> void:
if plugin.terrain:
bake_method = _set_up_navigation
confirm_dialog.dialog_text = SET_UP_NAVIGATION_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(confirm_dialog)
func _set_up_navigation() -> void:
assert(plugin.terrain)
var terrain: Terrain3D = plugin.terrain
var nav_region := NavigationRegion3D.new()
nav_region.name = &"NavigationRegion3D"
nav_region.navigation_mesh = NavigationMesh.new()
var undo_redo: EditorUndoRedoManager = plugin.get_undo_redo()
undo_redo.create_action("Terrain3D Set up Navigation")
undo_redo.add_do_method(self, &"_do_set_up_navigation", nav_region, terrain)
undo_redo.add_undo_method(self, &"_undo_set_up_navigation", nav_region, terrain)
undo_redo.add_do_reference(nav_region)
undo_redo.commit_action()
plugin.get_editor_interface().inspect_object(nav_region)
assert(plugin.nav_region == nav_region)
bake_nav_mesh()
func _do_set_up_navigation(p_nav_region: NavigationRegion3D, p_terrain: Terrain3D) -> void:
var parent: Node = p_terrain.get_parent()
var index: int = p_terrain.get_index()
var t_owner: Node = p_terrain.owner
parent.remove_child(p_terrain)
p_nav_region.add_child(p_terrain)
parent.add_child(p_nav_region, true)
parent.move_child(p_nav_region, index)
p_nav_region.owner = t_owner
p_terrain.owner = t_owner
func _undo_set_up_navigation(p_nav_region: NavigationRegion3D, p_terrain: Terrain3D) -> void:
assert(p_terrain.get_parent() == p_nav_region)
var parent: Node = p_nav_region.get_parent()
var index: int = p_nav_region.get_index()
var t_owner: Node = p_nav_region.get_owner()
parent.remove_child(p_nav_region)
p_nav_region.remove_child(p_terrain)
parent.add_child(p_terrain, true)
parent.move_child(p_terrain, index)
p_terrain.owner = t_owner

View file

@ -0,0 +1,212 @@
extends Object
const WINDOW_SCENE: String = "res://addons/terrain_3d/editor/components/channel_packer.tscn"
const TEMPLATE_PATH: String = "res://addons/terrain_3d/editor/components/channel_packer_import_template.txt"
enum {
IMAGE_ALBEDO,
IMAGE_HEIGHT,
IMAGE_NORMAL,
IMAGE_ROUGHNESS,
}
var plugin: EditorPlugin
var editor_interface: EditorInterface
var dialog: AcceptDialog
var save_file_dialog: FileDialog
var open_file_dialog: FileDialog
var invert_green_checkbox: CheckBox
var last_opened_directory: String
var last_saved_directory: String
var packing_albedo: bool = false
var queue_pack_normal_roughness: bool = false
var images: Array[Image] = [null, null, null, null]
var status_label: Label
var no_op: Callable = func(): pass
var last_file_selected_fn: Callable = no_op
func pack_textures_popup() -> void:
if dialog != null:
print("Terrain3DChannelPacker: Cannot open pack tool, dialog already open.")
return
dialog = (load(WINDOW_SCENE) as PackedScene).instantiate()
dialog.confirmed.connect(_on_close_requested)
dialog.canceled.connect(_on_close_requested)
status_label = dialog.find_child("StatusLabel")
invert_green_checkbox = dialog.find_child("InvertGreenChannelCheckBox")
editor_interface = plugin.get_editor_interface()
_init_file_dialogs()
editor_interface.popup_dialog_centered(dialog)
_init_texture_picker(dialog.find_child("AlbedoVBox"), IMAGE_ALBEDO)
_init_texture_picker(dialog.find_child("HeightVBox"), IMAGE_HEIGHT)
_init_texture_picker(dialog.find_child("NormalVBox"), IMAGE_NORMAL)
_init_texture_picker(dialog.find_child("RoughnessVBox"), IMAGE_ROUGHNESS)
var pack_button_path: String = "Panel/MarginContainer/VBoxContainer/PackButton"
(dialog.get_node(pack_button_path) as Button).pressed.connect(_on_pack_button_pressed)
func _on_close_requested() -> void:
last_file_selected_fn = no_op
images = [null, null, null, null]
dialog.queue_free()
dialog = null
func _init_file_dialogs() -> void:
save_file_dialog = FileDialog.new()
save_file_dialog.set_filters(PackedStringArray(["*.png"]))
save_file_dialog.set_file_mode(FileDialog.FILE_MODE_SAVE_FILE)
save_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
save_file_dialog.file_selected.connect(_on_save_file_selected)
open_file_dialog = FileDialog.new()
open_file_dialog.set_filters(PackedStringArray(["*.png", "*.bmp", "*.exr", "*.hdr", "*.jpg", "*.jpeg", "*.tga", "*.svg", "*.webp", ".ktx"]))
open_file_dialog.set_file_mode(FileDialog.FILE_MODE_OPEN_FILE)
open_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
dialog.add_child(save_file_dialog)
dialog.add_child(open_file_dialog)
func _init_texture_picker(p_parent: Node, p_image_index: int) -> void:
var line_edit: LineEdit = p_parent.find_child("LineEdit")
var file_pick_button: Button = p_parent.find_child("PickButton")
var clear_button: Button = p_parent.find_child("ClearButton")
var texture_rect: TextureRect = p_parent.find_child("TextureRect")
var texture_button: Button = p_parent.find_child("TextureButton")
var open_fn: Callable = func() -> void:
open_file_dialog.current_path = last_opened_directory
if last_file_selected_fn != no_op:
open_file_dialog.file_selected.disconnect(last_file_selected_fn)
last_file_selected_fn = func(path: String) -> void:
line_edit.text = path
line_edit.caret_column = path.length()
last_opened_directory = path.get_base_dir() + "/"
var image: Image = Image.new()
var code: int = image.load(path)
if code != OK:
_show_error("Failed to load texture '" + path + "'")
texture_rect.texture = null
images[p_image_index] = null
else:
_show_success("Loaded texture '" + path + "'")
texture_rect.texture = ImageTexture.create_from_image(image)
images[p_image_index] = image
open_file_dialog.file_selected.connect(last_file_selected_fn)
open_file_dialog.popup_centered_ratio()
var clear_fn: Callable = func() -> void:
line_edit.text = ""
texture_rect.texture = null
images[p_image_index] = null
# allow user to edit textbox and press enter because Godot's file picker doesn't work 100% of the time
var line_edit_submit_fn: Callable = func(path: String) -> void:
var image: Image = Image.new()
var code: int = image.load(path)
if code != OK:
_show_error("Failed to load texture '" + path + "'")
texture_rect.texture = null
images[p_image_index] = null
else:
texture_rect.texture = ImageTexture.create_from_image(image)
images[p_image_index] = image
line_edit.text_submitted.connect(line_edit_submit_fn)
file_pick_button.pressed.connect(open_fn)
texture_button.pressed.connect(open_fn)
clear_button.pressed.connect(clear_fn)
_set_button_icon(file_pick_button, "Folder")
_set_button_icon(clear_button, "Remove")
func _set_button_icon(p_button: Button, p_icon_name: String) -> void:
var editor_base: Control = editor_interface.get_base_control()
var icon: Texture2D = editor_base.get_theme_icon(p_icon_name, "EditorIcons")
p_button.icon = icon
func _show_error(p_text: String) -> void:
push_error("Terrain3DChannelPacker: " + p_text)
status_label.text = p_text
status_label.add_theme_color_override("font_color", Color(0.9, 0, 0))
func _show_success(p_text: String) -> void:
print("Terrain3DChannelPacker: " + p_text)
status_label.text = p_text
status_label.add_theme_color_override("font_color", Color(0, 0.82, 0.14))
func _create_import_file(png_path: String) -> void:
var dst_import_path: String = png_path + ".import"
var file: FileAccess = FileAccess.open(TEMPLATE_PATH, FileAccess.READ)
var template_content: String = file.get_as_text()
file.close()
var import_content: String = template_content.replace("$SOURCE_FILE", png_path)
file = FileAccess.open(dst_import_path, FileAccess.WRITE)
file.store_string(import_content)
file.close()
func _on_pack_button_pressed() -> void:
packing_albedo = images[IMAGE_ALBEDO] != null and images[IMAGE_HEIGHT] != null
var packing_normal_roughness: bool = images[IMAGE_NORMAL] != null and images[IMAGE_ROUGHNESS] != null
if not packing_albedo and not packing_normal_roughness:
_show_error("Please select an albedo and height texture or a normal and roughness texture.")
return
if packing_albedo:
save_file_dialog.current_path = last_saved_directory + "packed_albedo_height"
save_file_dialog.title = "Save Packed Albedo/Height Texture"
save_file_dialog.popup_centered_ratio()
if packing_normal_roughness:
queue_pack_normal_roughness = true
return
if packing_normal_roughness:
save_file_dialog.current_path = last_saved_directory + "packed_normal_roughness"
save_file_dialog.title = "Save Packed Normal/Roughness Texture"
save_file_dialog.popup_centered_ratio()
func _on_save_file_selected(p_dst_path) -> void:
last_saved_directory = p_dst_path.get_base_dir() + "/"
if packing_albedo:
_pack_textures(images[IMAGE_ALBEDO], images[IMAGE_HEIGHT], p_dst_path, false)
else:
_pack_textures(images[IMAGE_NORMAL], images[IMAGE_ROUGHNESS], p_dst_path, invert_green_checkbox.button_pressed)
if queue_pack_normal_roughness:
queue_pack_normal_roughness = false
packing_albedo = false
save_file_dialog.current_path = last_saved_directory + "packed_normal_roughness"
save_file_dialog.title = "Save Packed Normal/Roughness Texture"
save_file_dialog.call_deferred("popup_centered_ratio")
func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_invert_green: bool) -> void:
if p_rgb_image and p_a_image:
if p_rgb_image.get_size() != p_a_image.get_size():
_show_error("Textures must be the same size.")
return
var output_image: Image = Terrain3D.pack_image(p_rgb_image, p_a_image, p_invert_green)
if not output_image:
_show_error("Failed to pack textures.")
return
output_image.save_png(p_dst_path)
editor_interface.get_resource_filesystem().scan_sources()
_create_import_file(p_dst_path)
_show_success("Packed to " + p_dst_path + ".")
else:
_show_error("Failed to load one or more textures.")

View file

@ -0,0 +1,359 @@
[gd_scene load_steps=5 format=3 uid="uid://nud6dwjcnj5v"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ysabf"]
bg_color = Color(0.211765, 0.239216, 0.290196, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lcvna"]
bg_color = Color(0.168627, 0.211765, 0.266667, 1)
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.270588, 0.435294, 0.580392, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cb0xf"]
bg_color = Color(0.137255, 0.137255, 0.137255, 1)
draw_center = false
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.784314, 0.784314, 0.784314, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7qdas"]
[node name="AcceptDialog" type="AcceptDialog"]
title = "Terrain3D Channel Packer"
initial_position = 1
size = Vector2i(660, 900)
visible = true
ok_button_text = "Close"
[node name="Panel" type="Panel" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ysabf")
[node name="MarginContainer" type="MarginContainer" parent="Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 4.0
offset_top = 4.0
offset_right = -1.0
offset_bottom = -53.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"]
layout_mode = 2
size_flags_vertical = 0
theme_override_constants/separation = 10
[node name="AlbedoHeightPanel" type="Panel" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 250)
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer"]
layout_mode = 2
[node name="AlbedoVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="AlbedoLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
text = "Albedo texture"
[node name="AlbedoHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="HeightVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HeightLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
text = "Height texture"
[node name="HeightHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="NormalRoughnessPanel" type="Panel" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 280)
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer"]
layout_mode = 2
[node name="NormalVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="NormalLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
text = "Normal texture"
[node name="NormalHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="InvertGreenChannelCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
text = "Convert DirectX to OpenGL"
[node name="RoughnessVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RoughnessLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
text = "Roughness texture"
[node name="RoughnessHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="NormalRoughnessHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="PackButton" type="Button" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Pack textures as..."
[node name="StatusLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 10)
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
text = "Use this to create a packed Albedo + Height texture and/or a packed Normal + Roughness texture.
You can then use these textures with Terrain3D."
autowrap_mode = 2

View file

@ -0,0 +1,32 @@
[remap]
importer="texture"
type="CompressedTexture2D"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="$SOURCE_FILE"
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=2
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,55 @@
extends "res://addons/terrain_3d/editor/components/operation_builder.gd"
const PointPicker: Script = preload("res://addons/terrain_3d/editor/components/point_picker.gd")
func _get_point_picker() -> PointPicker:
return tool_settings.settings["gradient_points"]
func _get_brush_size() -> float:
return tool_settings.get_setting("size")
func _is_drawable() -> bool:
return tool_settings.get_setting("drawable")
func is_picking() -> bool:
return not _get_point_picker().all_points_selected()
func pick(p_global_position: Vector3, p_terrain: Terrain3D) -> void:
if not _get_point_picker().all_points_selected():
_get_point_picker().add_point(p_global_position)
func is_ready() -> bool:
return _get_point_picker().all_points_selected() and not _is_drawable()
func apply_operation(p_editor: Terrain3DEditor, p_global_position: Vector3, p_camera_direction: float) -> void:
var points: PackedVector3Array = _get_point_picker().get_points()
assert(points.size() == 2)
assert(not _is_drawable())
var brush_size: float = _get_brush_size()
assert(brush_size > 0.0)
var start: Vector3 = points[0]
var end: Vector3 = points[1]
p_editor.start_operation(start)
var dir: Vector3 = (end - start).normalized()
var pos: Vector3 = start
while dir.dot(end - pos) > 0.0:
p_editor.operate(pos, p_camera_direction)
pos += dir * brush_size * 0.2
p_editor.stop_operation()
_get_point_picker().clear()

View file

@ -0,0 +1,23 @@
extends RefCounted
const ToolSettings: Script = preload("res://addons/terrain_3d/editor/components/tool_settings.gd")
var tool_settings: ToolSettings
func is_picking() -> bool:
return false
func pick(p_global_position: Vector3, p_terrain: Terrain3D) -> void:
pass
func is_ready() -> bool:
return false
func apply_operation(editor: Terrain3DEditor, p_global_position: Vector3, p_camera_direction: float) -> void:
pass

View file

@ -0,0 +1,91 @@
extends HBoxContainer
signal pressed
signal value_changed
const ICON_PICKER: String = "res://addons/terrain_3d/icons/icon_picker.svg"
const ICON_PICKER_CHECKED: String = "res://addons/terrain_3d/icons/icon_picker_checked.svg"
const MAX_POINTS: int = 2
var icon_picker: Texture2D
var icon_picker_checked: Texture2D
var points: PackedVector3Array
var picking_index: int = -1
func _init() -> void:
icon_picker = load(ICON_PICKER)
icon_picker_checked = load(ICON_PICKER_CHECKED)
var label := Label.new()
label.text = "Points:"
add_child(label)
points.resize(MAX_POINTS)
for i in range(MAX_POINTS):
var button := Button.new()
button.icon = icon_picker
button.tooltip_text = "Pick point on the Terrain"
button.set_meta(&"point_index", i)
button.pressed.connect(_on_button_pressed.bind(i))
add_child(button)
_update_buttons()
func _on_button_pressed(button_index: int) -> void:
points[button_index] = Vector3.ZERO
picking_index = button_index
_update_buttons()
pressed.emit()
func _update_buttons() -> void:
for child in get_children():
if child is Button:
_update_button(child)
func _update_button(button: Button) -> void:
var index: int = button.get_meta(&"point_index")
if points[index] != Vector3.ZERO:
button.icon = icon_picker_checked
else:
button.icon = icon_picker
func clear() -> void:
points.fill(Vector3.ZERO)
_update_buttons()
value_changed.emit()
func all_points_selected() -> bool:
return points.count(Vector3.ZERO) == 0
func add_point(p_value: Vector3) -> void:
if points.has(p_value):
return
# If manually selecting a point individually
if picking_index != -1:
points[picking_index] = p_value
picking_index = -1
else:
# Else picking a sequence of points (non-drawable)
for i in range(MAX_POINTS):
if points[i] == Vector3.ZERO:
points[i] = p_value
break
_update_buttons()
value_changed.emit()
func get_points() -> PackedVector3Array:
return points

View file

@ -0,0 +1,66 @@
extends EditorNode3DGizmo
var material: StandardMaterial3D
var selection_material: StandardMaterial3D
var region_position: Vector2
var region_size: float
var grid: Array[Vector2i]
var use_secondary_color: bool = false
var show_rect: bool = true
var main_color: Color = Color.GREEN_YELLOW
var secondary_color: Color = Color.RED
var grid_color: Color = Color.WHITE
var border_color: Color = Color.BLUE
func _init() -> void:
material = StandardMaterial3D.new()
material.set_flag(BaseMaterial3D.FLAG_DISABLE_DEPTH_TEST, true)
material.set_flag(BaseMaterial3D.FLAG_ALBEDO_FROM_VERTEX_COLOR, true)
material.set_shading_mode(BaseMaterial3D.SHADING_MODE_UNSHADED)
material.set_albedo(Color.WHITE)
selection_material = material.duplicate()
selection_material.set_render_priority(0)
func _redraw() -> void:
clear()
var rect_position = region_position * region_size
if show_rect:
var modulate: Color = main_color if !use_secondary_color else secondary_color
if abs(region_position.x) > 8 or abs(region_position.y) > 8:
modulate = Color.GRAY
draw_rect(Vector2(region_size,region_size)*.5 + rect_position, region_size, selection_material, modulate)
for pos in grid:
var grid_tile_position = Vector2(pos) * region_size
if show_rect and grid_tile_position == rect_position:
# Skip this one, otherwise focused region borders are not always visible due to draw order
continue
draw_rect(Vector2(region_size,region_size)*.5 + grid_tile_position, region_size, material, grid_color)
draw_rect(Vector2.ZERO, region_size * 16.0, material, border_color)
func draw_rect(p_pos: Vector2, p_size: float, p_material: StandardMaterial3D, p_modulate: Color) -> void:
var lines: PackedVector3Array = [
Vector3(-1, 0, -1),
Vector3(-1, 0, 1),
Vector3(1, 0, 1),
Vector3(1, 0, -1),
Vector3(-1, 0, 1),
Vector3(1, 0, 1),
Vector3(1, 0, -1),
Vector3(-1, 0, -1),
]
for i in lines.size():
lines[i] = ((lines[i] / 2.0) * p_size) + Vector3(p_pos.x, 0, p_pos.y)
add_lines(lines, p_material, false, p_modulate)

View file

@ -0,0 +1,71 @@
extends HBoxContainer
const Baker: Script = preload("res://addons/terrain_3d/editor/components/baker.gd")
const Packer: Script = preload("res://addons/terrain_3d/editor/components/channel_packer.gd")
var plugin: EditorPlugin
var menu_button: MenuButton = MenuButton.new()
var baker: Baker = Baker.new()
var packer: Packer = Packer.new()
enum {
MENU_BAKE_ARRAY_MESH,
MENU_BAKE_OCCLUDER,
MENU_BAKE_NAV_MESH,
MENU_SEPARATOR,
MENU_SET_UP_NAVIGATION,
MENU_PACK_TEXTURES,
}
func _enter_tree() -> void:
baker.plugin = plugin
packer.plugin = plugin
add_child(baker)
menu_button.text = "Terrain3D Tools"
menu_button.get_popup().add_item("Bake ArrayMesh", MENU_BAKE_ARRAY_MESH)
menu_button.get_popup().add_item("Bake Occluder3D", MENU_BAKE_OCCLUDER)
menu_button.get_popup().add_item("Bake NavMesh", MENU_BAKE_NAV_MESH)
menu_button.get_popup().add_separator("", MENU_SEPARATOR)
menu_button.get_popup().add_item("Set up Navigation", MENU_SET_UP_NAVIGATION)
menu_button.get_popup().add_separator("", MENU_SEPARATOR)
menu_button.get_popup().add_item("Pack Textures", MENU_PACK_TEXTURES)
menu_button.get_popup().id_pressed.connect(_on_menu_pressed)
menu_button.about_to_popup.connect(_on_menu_about_to_popup)
add_child(menu_button)
func _on_menu_pressed(p_id: int) -> void:
match p_id:
MENU_BAKE_ARRAY_MESH:
baker.bake_mesh_popup()
MENU_BAKE_OCCLUDER:
baker.bake_occluder_popup()
MENU_BAKE_NAV_MESH:
baker.bake_nav_mesh()
MENU_SET_UP_NAVIGATION:
baker.set_up_navigation_popup()
MENU_PACK_TEXTURES:
packer.pack_textures_popup()
func _on_menu_about_to_popup() -> void:
menu_button.get_popup().set_item_disabled(MENU_BAKE_ARRAY_MESH, not plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_BAKE_OCCLUDER, not plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_PACK_TEXTURES, not plugin.terrain)
if plugin.terrain:
var nav_regions: Array[NavigationRegion3D] = baker.find_terrain_nav_regions(plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, nav_regions.size() == 0)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, nav_regions.size() != 0)
elif plugin.nav_region:
var terrains: Array[Terrain3D] = baker.find_nav_region_terrains(plugin.nav_region)
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, terrains.size() == 0)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, true)
else:
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, true)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, true)

View file

@ -0,0 +1,284 @@
extends PanelContainer
signal resource_changed(resource: Resource, index: int)
signal resource_inspected(resource: Resource)
signal resource_selected
var list: ListContainer
var entries: Array[ListEntry]
var selected_index: int = 0
func _init() -> void:
list = ListContainer.new()
var root: VBoxContainer = VBoxContainer.new()
var scroll: ScrollContainer = ScrollContainer.new()
var label: Label = Label.new()
label.set_text("Textures")
label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
scroll.set_v_size_flags(SIZE_EXPAND_FILL)
scroll.set_h_size_flags(SIZE_EXPAND_FILL)
list.set_v_size_flags(SIZE_EXPAND_FILL)
list.set_h_size_flags(SIZE_EXPAND_FILL)
scroll.add_child(list)
root.add_child(label)
root.add_child(scroll)
add_child(root)
set_custom_minimum_size(Vector2(256, 448))
func _ready() -> void:
get_child(0).get_child(0).set("theme_override_styles/normal", get_theme_stylebox("bg", "EditorInspectorCategory"))
get_child(0).get_child(0).set("theme_override_fonts/font", get_theme_font("bold", "EditorFonts"))
get_child(0).get_child(0).set("theme_override_font_sizes/font_size",get_theme_font_size("bold_size", "EditorFonts"))
set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel"))
func clear() -> void:
for i in entries:
i.get_parent().remove_child(i)
i.queue_free()
entries.clear()
func add_item(p_resource: Resource = null) -> void:
var entry: ListEntry = ListEntry.new()
var index: int = entries.size()
entry.set_edited_resource(p_resource)
entry.selected.connect(set_selected_index.bind(index))
entry.inspected.connect(notify_resource_inspected)
entry.changed.connect(notify_resource_changed.bind(index))
if p_resource:
entry.set_selected(index == selected_index)
if not p_resource.id_changed.is_connected(set_selected_after_swap):
p_resource.id_changed.connect(set_selected_after_swap)
list.add_child(entry)
entries.push_back(entry)
func set_selected_after_swap(p_old_index: int, p_new_index: int) -> void:
set_selected_index(clamp(p_new_index, 0, entries.size() - 2))
func set_selected_index(p_index: int) -> void:
selected_index = p_index
emit_signal("resource_selected")
for i in entries.size():
var entry: ListEntry = entries[i]
entry.set_selected(i == selected_index)
func get_selected_index() -> int:
return selected_index
func notify_resource_inspected(p_resource: Resource) -> void:
emit_signal("resource_inspected", p_resource)
func notify_resource_changed(p_resource: Resource, p_index: int) -> void:
emit_signal("resource_changed", p_resource, p_index)
if !p_resource:
var last_offset: int = 2
if p_index == entries.size()-2:
last_offset = 3
selected_index = clamp(selected_index, 0, entries.size() - last_offset)
##############################################################
## class ListContainer
##############################################################
class ListContainer extends Container:
var height: float = 0
func _notification(p_what) -> void:
if p_what == NOTIFICATION_SORT_CHILDREN:
height = 0
var index: int = 0
var separation: float = 4
for c in get_children():
if is_instance_valid(c):
var width: float = size.x / 3
c.size = Vector2(width,width) - Vector2(separation, separation)
c.position = Vector2(index % 3, index / 3) * width + Vector2(separation/3, separation/3)
height = max(height, c.position.y + width)
index += 1
func _get_minimum_size() -> Vector2:
return Vector2(0, height)
##############################################################
## class ListEntry
##############################################################
class ListEntry extends VBoxContainer:
signal selected()
signal changed(resource: Terrain3DTexture)
signal inspected(resource: Terrain3DTexture)
var resource: Terrain3DTexture
var drop_data: bool = false
var is_hovered: bool = false
var is_selected: bool = false
var button_clear: TextureButton
var button_edit: TextureButton
var name_label: Label
@onready var add_icon: Texture2D = get_theme_icon("Add", "EditorIcons")
@onready var clear_icon: Texture2D = get_theme_icon("Close", "EditorIcons")
@onready var edit_icon: Texture2D = get_theme_icon("Edit", "EditorIcons")
@onready var background: StyleBox = get_theme_stylebox("pressed", "Button")
@onready var focus: StyleBox = get_theme_stylebox("focus", "Button")
func _ready() -> void:
var icon_size: Vector2 = Vector2(12, 12)
button_clear = TextureButton.new()
button_clear.set_texture_normal(clear_icon)
button_clear.set_custom_minimum_size(icon_size)
button_clear.set_h_size_flags(Control.SIZE_SHRINK_END)
button_clear.set_visible(resource != null)
button_clear.pressed.connect(clear)
add_child(button_clear)
button_edit = TextureButton.new()
button_edit.set_texture_normal(edit_icon)
button_edit.set_custom_minimum_size(icon_size)
button_edit.set_h_size_flags(Control.SIZE_SHRINK_END)
button_edit.set_visible(resource != null)
button_edit.pressed.connect(edit)
add_child(button_edit)
name_label = Label.new()
add_child(name_label, true)
name_label.visible = false
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
name_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
name_label.add_theme_color_override("font_shadow_color", Color.BLACK)
name_label.add_theme_constant_override("shadow_offset_x", 1)
name_label.add_theme_constant_override("shadow_offset_y", 1)
name_label.add_theme_font_size_override("font_size", 15)
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
name_label.text = "Add New"
func _notification(p_what) -> void:
match p_what:
NOTIFICATION_DRAW:
var rect: Rect2 = Rect2(Vector2.ZERO, get_size())
if !resource:
draw_style_box(background, rect)
draw_texture(add_icon, (get_size() / 2) - (add_icon.get_size() / 2))
else:
name_label.text = resource.get_name()
self_modulate = resource.get_albedo_color()
var texture: Texture2D = resource.get_albedo_texture()
if texture:
draw_texture_rect(texture, rect, false)
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS
name_label.add_theme_font_size_override("font_size", 4 + rect.size.x/10)
if drop_data:
draw_style_box(focus, rect)
if is_hovered:
draw_rect(rect, Color(1,1,1,0.2))
if is_selected:
draw_style_box(focus, rect)
NOTIFICATION_MOUSE_ENTER:
is_hovered = true
name_label.visible = true
queue_redraw()
NOTIFICATION_MOUSE_EXIT:
is_hovered = false
name_label.visible = false
drop_data = false
queue_redraw()
func _gui_input(p_event: InputEvent) -> void:
if p_event is InputEventMouseButton:
if p_event.is_pressed():
match p_event.get_button_index():
MOUSE_BUTTON_LEFT:
# If `Add new` is clicked
if !resource:
set_edited_resource(Terrain3DTexture.new(), false)
edit()
else:
emit_signal("selected")
MOUSE_BUTTON_RIGHT:
if resource:
edit()
MOUSE_BUTTON_MIDDLE:
if resource:
clear()
func _can_drop_data(p_at_position: Vector2, p_data: Variant) -> bool:
drop_data = false
if typeof(p_data) == TYPE_DICTIONARY:
if p_data.files.size() == 1:
queue_redraw()
drop_data = true
return drop_data
func _drop_data(p_at_position: Vector2, p_data: Variant) -> void:
if typeof(p_data) == TYPE_DICTIONARY:
var res: Resource = load(p_data.files[0])
if res is Terrain3DTexture:
set_edited_resource(res, false)
if res is Texture2D:
var surf: Terrain3DTexture = Terrain3DTexture.new()
surf.set_albedo_texture(res)
set_edited_resource(surf, false)
func set_edited_resource(p_res: Terrain3DTexture, p_no_signal: bool = true) -> void:
resource = p_res
if resource:
resource.setting_changed.connect(_on_texture_changed)
resource.file_changed.connect(_on_texture_changed)
if button_clear:
button_clear.set_visible(resource != null)
queue_redraw()
if !p_no_signal:
emit_signal("changed", resource)
func _on_texture_changed() -> void:
emit_signal("changed", resource)
func set_selected(value: bool) -> void:
is_selected = value
queue_redraw()
func clear() -> void:
if resource:
set_edited_resource(null, false)
func edit() -> void:
emit_signal("selected")
emit_signal("inspected", resource)

View file

@ -0,0 +1,460 @@
extends PanelContainer
signal picking(type, callback)
signal setting_changed
enum Layout {
HORIZONTAL,
VERTICAL,
GRID,
}
enum SettingType {
CHECKBOX,
SLIDER,
DOUBLE_SLIDER,
COLOR_SELECT,
PICKER,
POINT_PICKER,
}
const PointPicker: Script = preload("res://addons/terrain_3d/editor/components/point_picker.gd")
const DEFAULT_BRUSH: String = "circle0.exr"
const BRUSH_PATH: String = "res://addons/terrain_3d/editor/brushes"
const PICKER_ICON: String = "res://addons/terrain_3d/icons/icon_picker.svg"
const NONE: int = 0x0
const ALLOW_LARGER: int = 0x1
const ALLOW_SMALLER: int = 0x2
const ALLOW_OUT_OF_BOUNDS: int = 0x3
var brush_preview_material: ShaderMaterial
var list: HBoxContainer
var advanced_list: VBoxContainer
var settings: Dictionary = {}
func _ready() -> void:
list = HBoxContainer.new()
add_child(list, true)
add_brushes(list)
add_setting(SettingType.SLIDER, "size", 50, list, "m", 4, 200, 1, ALLOW_LARGER)
add_setting(SettingType.SLIDER, "opacity", 10, list, "%", 1, 100)
add_setting(SettingType.CHECKBOX, "enable", true, list)
add_setting(SettingType.COLOR_SELECT, "color", Color.WHITE, list)
add_setting(SettingType.PICKER, "color picker", Terrain3DEditor.COLOR, list)
add_setting(SettingType.SLIDER, "roughness", 0, list, "%", -100, 100, 1)
add_setting(SettingType.PICKER, "roughness picker", Terrain3DEditor.ROUGHNESS, list)
add_setting(SettingType.SLIDER, "height", 50, list, "m", -500, 500, 0.1, ALLOW_OUT_OF_BOUNDS)
add_setting(SettingType.PICKER, "height picker", Terrain3DEditor.HEIGHT, list)
add_setting(SettingType.DOUBLE_SLIDER, "slope", 0, list, "°", 0, 180, 1)
add_setting(SettingType.POINT_PICKER, "gradient_points", Terrain3DEditor.HEIGHT, list)
add_setting(SettingType.CHECKBOX, "drawable", false, list)
settings["drawable"].toggled.connect(_on_drawable_toggled)
var spacer: Control = Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
list.add_child(spacer, true)
## Advanced Settings Menu
advanced_list = create_submenu(list, "Advanced", Layout.VERTICAL)
add_setting(SettingType.CHECKBOX, "automatic_regions", true, advanced_list)
add_setting(SettingType.CHECKBOX, "align_to_view", true, advanced_list)
add_setting(SettingType.CHECKBOX, "show_cursor_while_painting", true, advanced_list)
advanced_list.add_child(HSeparator.new(), true)
add_setting(SettingType.SLIDER, "gamma", 1.0, advanced_list, "γ", 0.1, 2.0, 0.01)
add_setting(SettingType.SLIDER, "jitter", 50, advanced_list, "%", 0, 100)
func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout) -> Container:
var menu_button: Button = Button.new()
menu_button.set_text(p_button_name)
menu_button.set_toggle_mode(true)
menu_button.set_v_size_flags(SIZE_SHRINK_CENTER)
menu_button.connect("toggled", _on_show_submenu.bind(menu_button))
var submenu: PopupPanel = PopupPanel.new()
submenu.connect("popup_hide", menu_button.set_pressed_no_signal.bind(false))
submenu.set("theme_override_styles/panel", get_theme_stylebox("panel", "PopupMenu"))
var sublist: Container
match(p_layout):
Layout.GRID:
sublist = GridContainer.new()
Layout.VERTICAL:
sublist = VBoxContainer.new()
Layout.HORIZONTAL, _:
sublist = HBoxContainer.new()
p_parent.add_child(menu_button, true)
menu_button.add_child(submenu, true)
submenu.add_child(sublist, true)
return sublist
func _on_show_submenu(p_toggled: bool, p_button: Button) -> void:
var popup: PopupPanel = p_button.get_child(0)
var popup_pos: Vector2 = p_button.get_screen_transform().origin
popup.set_visible(p_toggled)
popup_pos.y -= popup.get_size().y
popup.set_position(popup_pos)
func add_brushes(p_parent: Control) -> void:
var brush_list: GridContainer = create_submenu(p_parent, "Brush", Layout.GRID)
brush_list.name = "BrushList"
var brush_button_group: ButtonGroup = ButtonGroup.new()
brush_button_group.connect("pressed", _on_setting_changed)
var default_brush_btn: Button
var dir: DirAccess = DirAccess.open(BRUSH_PATH)
if dir:
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
if !dir.current_is_dir() and file_name.ends_with(".exr"):
var img: Image = Image.load_from_file(BRUSH_PATH + "/" + file_name)
_black_to_alpha(img)
var tex: ImageTexture = ImageTexture.create_from_image(img)
var btn: Button = Button.new()
btn.set_custom_minimum_size(Vector2.ONE * 100)
btn.set_button_icon(tex)
btn.set_meta("image", img)
btn.set_expand_icon(true)
btn.set_material(_get_brush_preview_material())
btn.set_toggle_mode(true)
btn.set_button_group(brush_button_group)
btn.mouse_entered.connect(_on_brush_hover.bind(true, btn))
btn.mouse_exited.connect(_on_brush_hover.bind(false, btn))
brush_list.add_child(btn, true)
if file_name == DEFAULT_BRUSH:
default_brush_btn = btn
var lbl: Label = Label.new()
btn.add_child(lbl, true)
lbl.text = file_name.get_basename()
lbl.visible = false
lbl.position.y = 70
lbl.add_theme_color_override("font_shadow_color", Color.BLACK)
lbl.add_theme_constant_override("shadow_offset_x", 1)
lbl.add_theme_constant_override("shadow_offset_y", 1)
lbl.add_theme_font_size_override("font_size", 16)
file_name = dir.get_next()
brush_list.columns = sqrt(brush_list.get_child_count()) + 2
if not default_brush_btn:
default_brush_btn = brush_button_group.get_buttons()[0]
default_brush_btn.set_pressed(true)
settings["brush"] = brush_button_group
# Optionally erase the main brush button text and replace it with the texture
# var select_brush_btn: Button = brush_list.get_parent().get_parent()
# select_brush_btn.set_button_icon(default_brush_btn.get_button_icon())
# select_brush_btn.set_custom_minimum_size(Vector2.ONE * 36)
# select_brush_btn.set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER)
# select_brush_btn.set_expand_icon(true)
func _on_brush_hover(p_hovering: bool, p_button: Button) -> void:
if p_button.get_child_count() > 0:
var child = p_button.get_child(0)
if child is Label:
if p_hovering:
child.visible = true
else:
child.visible = false
func _on_pick(p_type: Terrain3DEditor.Tool) -> void:
emit_signal("picking", p_type, _on_picked)
func _on_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3) -> void:
match p_type:
Terrain3DEditor.HEIGHT:
settings["height"].value = p_color.r
Terrain3DEditor.COLOR:
settings["color"].color = p_color
Terrain3DEditor.ROUGHNESS:
# 200... -.5 converts 0,1 to -100,100
settings["roughness"].value = round(200 * (p_color.a - 0.5))
_on_setting_changed()
func _on_point_pick(p_type: Terrain3DEditor.Tool, p_name: String) -> void:
assert(p_type == Terrain3DEditor.HEIGHT)
emit_signal("picking", p_type, _on_point_picked.bind(p_name))
func _on_point_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3, p_name: String) -> void:
assert(p_type == Terrain3DEditor.HEIGHT)
var point: Vector3 = p_global_position
point.y = p_color.r
settings[p_name].add_point(point)
_on_setting_changed()
func add_setting(p_type: SettingType, p_name: StringName, p_value: Variant, p_parent: Control,
p_suffix: String = "", p_min_value: float = 0.0, p_max_value: float = 0.0, p_step: float = 1.0,
p_flags: int = NONE) -> void:
var container: HBoxContainer = HBoxContainer.new()
var label: Label = Label.new()
var control: Control
container.set_v_size_flags(SIZE_EXPAND_FILL)
match p_type:
SettingType.SLIDER, SettingType.DOUBLE_SLIDER:
label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
label.set_custom_minimum_size(Vector2(32, 0))
label.set_v_size_flags(SIZE_SHRINK_CENTER)
label.set_text(p_name.capitalize() + ": ")
container.add_child(label, true)
var slider: Control
if p_type == SettingType.SLIDER:
control = EditorSpinSlider.new()
control.set_flat(true)
control.set_hide_slider(true)
control.connect("value_changed", _on_setting_changed)
control.set_max(p_max_value)
control.set_min(p_min_value)
control.set_step(p_step)
control.set_value(p_value)
control.set_suffix(p_suffix)
control.set_v_size_flags(SIZE_SHRINK_CENTER)
slider = HSlider.new()
slider.share(control)
if p_flags & ALLOW_LARGER:
slider.set_allow_greater(true)
if p_flags & ALLOW_SMALLER:
slider.set_allow_lesser(true)
else:
control = Label.new()
control.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
control.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
slider = DoubleSlider.new()
slider.label = control
slider.suffix = p_suffix
slider.connect("value_changed", _on_setting_changed)
control.set_custom_minimum_size(Vector2(75, 0))
slider.set_max(p_max_value)
slider.set_min(p_min_value)
slider.set_step(p_step)
slider.set_value(p_value)
slider.set_v_size_flags(SIZE_SHRINK_CENTER)
slider.set_h_size_flags(SIZE_SHRINK_END | SIZE_EXPAND)
slider.set_custom_minimum_size(Vector2(100, 10))
container.add_child(slider, true)
SettingType.CHECKBOX:
control = CheckBox.new()
control.set_text(p_name.capitalize())
control.set_pressed_no_signal(p_value)
control.connect("pressed", _on_setting_changed)
SettingType.COLOR_SELECT:
control = ColorPickerButton.new()
control.set_custom_minimum_size(Vector2(100, 10))
control.color = Color.WHITE
control.edit_alpha = false
control.get_picker().set_color_mode(ColorPicker.MODE_HSV)
control.connect("color_changed", _on_setting_changed)
SettingType.PICKER:
control = Button.new()
control.icon = load(PICKER_ICON)
control.tooltip_text = "Pick value from the Terrain"
control.connect("pressed", _on_pick.bind(p_value))
SettingType.POINT_PICKER:
control = PointPicker.new()
control.connect("pressed", _on_point_pick.bind(p_value, p_name))
control.connect("value_changed", _on_setting_changed)
container.add_child(control, true)
p_parent.add_child(container, true)
settings[p_name] = control
func get_setting(p_setting: String) -> Variant:
var object: Object = settings[p_setting]
var value: Variant
if object is Range:
value = object.get_value()
elif object is DoubleSlider:
value = [object.get_min_value(), object.get_max_value()]
elif object is ButtonGroup:
var img: Image = object.get_pressed_button().get_meta("image")
var tex: Texture2D = object.get_pressed_button().get_button_icon()
value = [ img, tex ]
elif object is CheckBox:
value = object.is_pressed()
elif object is ColorPickerButton:
value = object.color
elif object is PointPicker:
value = object.get_points()
return value
func hide_settings(p_settings: PackedStringArray) -> void:
for setting in settings.keys():
var object: Object = settings[setting]
if object is Control:
object.get_parent().show()
for setting in p_settings:
if settings.has(setting):
var object: Object = settings[setting]
if object is Control:
object.get_parent().hide()
func _on_setting_changed(p_data: Variant = null) -> void:
# If a button was clicked on a submenu
if p_data is Button and p_data.get_parent().get_parent() is PopupPanel:
if p_data.get_parent().name == "BrushList":
# Optionally Set selected brush texture in main brush button
# p_data.get_parent().get_parent().get_parent().set_button_icon(p_data.get_button_icon())
# Hide popup
p_data.get_parent().get_parent().set_visible(false)
# Hide label
if p_data.get_child_count() > 0:
p_data.get_child(0).visible = false
emit_signal("setting_changed")
func _on_drawable_toggled(p_button_pressed: bool) -> void:
if not p_button_pressed:
settings["gradient_points"].clear()
func _get_brush_preview_material() -> ShaderMaterial:
if !brush_preview_material:
brush_preview_material = ShaderMaterial.new()
var shader: Shader = Shader.new()
var code: String = "shader_type canvas_item;\n"
code += "varying vec4 v_vertex_color;\n"
code += "void vertex() {\n"
code += " v_vertex_color = COLOR;\n"
code += "}\n"
code += "void fragment(){\n"
code += " vec4 tex = texture(TEXTURE, UV);\n"
code += " COLOR.a *= pow(tex.r, 0.666);\n"
code += " COLOR.rgb = v_vertex_color.rgb;\n"
code += "}\n"
shader.set_code(code)
brush_preview_material.set_shader(shader)
return brush_preview_material
func _black_to_alpha(p_image: Image) -> void:
if p_image.get_format() != Image.FORMAT_RGBAF:
p_image.convert(Image.FORMAT_RGBAF)
for y in p_image.get_height():
for x in p_image.get_width():
var color: Color = p_image.get_pixel(x,y)
color.a = color.get_luminance()
p_image.set_pixel(x, y, color)
#### Sub Class DoubleSlider
class DoubleSlider extends Range:
var label: Label
var suffix: String
var grabbed: bool = false
var _max_value: float
func _gui_input(p_event: InputEvent) -> void:
if p_event is InputEventMouseButton:
if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
grabbed = p_event.is_pressed()
set_min_max(p_event.get_position().x)
if p_event is InputEventMouseMotion:
if grabbed:
set_min_max(p_event.get_position().x)
func _notification(p_what: int) -> void:
if p_what == NOTIFICATION_RESIZED:
pass
if p_what == NOTIFICATION_DRAW:
var bg: StyleBox = get_theme_stylebox("slider", "HSlider")
var bg_height: float = bg.get_minimum_size().y
draw_style_box(bg, Rect2(Vector2(0, (size.y - bg_height) / 2), Vector2(size.x, bg_height)))
var grabber: Texture2D = get_theme_icon("grabber", "HSlider")
var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider")
var h: float = size.y / 2 - grabber.get_size().y / 2
var minpos: Vector2 = Vector2((min_value / _max_value) * size.x - grabber.get_size().x / 2, h)
var maxpos: Vector2 = Vector2((max_value / _max_value) * size.x - grabber.get_size().x / 2, h)
draw_style_box(area, Rect2(Vector2(minpos.x + grabber.get_size().x / 2, (size.y - bg_height) / 2), Vector2(maxpos.x - minpos.x, bg_height)))
draw_texture(grabber, minpos)
draw_texture(grabber, maxpos)
func set_max(p_value: float) -> void:
max_value = p_value
if _max_value == 0:
_max_value = max_value
update_label()
func set_min_max(p_xpos: float) -> void:
var mid_value_normalized: float = ((max_value + min_value) / 2.0) / _max_value
var mid_value: float = size.x * mid_value_normalized
var min_active: bool = p_xpos < mid_value
var xpos_ranged: float = snappedf((p_xpos / size.x) * _max_value, step)
if min_active:
min_value = xpos_ranged
else:
max_value = xpos_ranged
min_value = clamp(min_value, 0, max_value - 10)
max_value = clamp(max_value, min_value + 10, _max_value)
update_label()
emit_signal("setting_changed", value)
queue_redraw()
func update_label() -> void:
if label:
label.set_text(str(min_value) + suffix + "/" + str(max_value) + suffix)

View file

@ -0,0 +1,74 @@
extends VBoxContainer
signal tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation)
const ICON_REGION_ADD: String = "res://addons/terrain_3d/icons/icon_map_add.svg"
const ICON_REGION_REMOVE: String = "res://addons/terrain_3d/icons/icon_map_remove.svg"
const ICON_HEIGHT_ADD: String = "res://addons/terrain_3d/icons/icon_height_add.svg"
const ICON_HEIGHT_SUB: String = "res://addons/terrain_3d/icons/icon_height_sub.svg"
const ICON_HEIGHT_MUL: String = "res://addons/terrain_3d/icons/icon_height_mul.svg"
const ICON_HEIGHT_DIV: String = "res://addons/terrain_3d/icons/icon_height_div.svg"
const ICON_HEIGHT_FLAT: String = "res://addons/terrain_3d/icons/icon_height_flat.svg"
const ICON_HEIGHT_SLOPE: String = "res://addons/terrain_3d/icons/icon_height_slope.svg"
const ICON_HEIGHT_SMOOTH: String = "res://addons/terrain_3d/icons/icon_height_smooth.svg"
const ICON_PAINT_TEXTURE: String = "res://addons/terrain_3d/icons/icon_brush.svg"
const ICON_SPRAY_TEXTURE: String = "res://addons/terrain_3d/icons/icon_spray.svg"
const ICON_COLOR: String = "res://addons/terrain_3d/icons/icon_color.svg"
const ICON_WETNESS: String = "res://addons/terrain_3d/icons/icon_wetness.svg"
const ICON_AUTOSHADER: String = "res://addons/terrain_3d/icons/icon_terrain_material.svg"
const ICON_HOLES: String = "res://addons/terrain_3d/icons/icon_holes.svg"
const ICON_NAVIGATION: String = "res://addons/terrain_3d/icons/icon_navigation.svg"
var tool_group: ButtonGroup = ButtonGroup.new()
func _init() -> void:
set_custom_minimum_size(Vector2(32, 0))
func _ready() -> void:
tool_group.connect("pressed", _on_tool_selected)
add_tool_button(Terrain3DEditor.REGION, Terrain3DEditor.ADD, "Add Region", load(ICON_REGION_ADD), tool_group)
add_tool_button(Terrain3DEditor.REGION, Terrain3DEditor.SUBTRACT, "Delete Region", load(ICON_REGION_REMOVE), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.ADD, "Raise", load(ICON_HEIGHT_ADD), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.SUBTRACT, "Lower", load(ICON_HEIGHT_SUB), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.MULTIPLY, "Expand (Away from 0)", load(ICON_HEIGHT_MUL), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.DIVIDE, "Reduce (Towards 0)", load(ICON_HEIGHT_DIV), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.REPLACE, "Flatten", load(ICON_HEIGHT_FLAT), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.GRADIENT, "Slope", load(ICON_HEIGHT_SLOPE), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.AVERAGE, "Smooth", load(ICON_HEIGHT_SMOOTH), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE, "Paint Base Texture", load(ICON_PAINT_TEXTURE), tool_group)
add_tool_button(Terrain3DEditor.TEXTURE, Terrain3DEditor.ADD, "Spray Overlay Texture", load(ICON_SPRAY_TEXTURE), tool_group)
add_tool_button(Terrain3DEditor.AUTOSHADER, Terrain3DEditor.REPLACE, "Autoshader", load(ICON_AUTOSHADER), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.COLOR, Terrain3DEditor.REPLACE, "Paint Color", load(ICON_COLOR), tool_group)
add_tool_button(Terrain3DEditor.ROUGHNESS, Terrain3DEditor.REPLACE, "Paint Wetness", load(ICON_WETNESS), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.HOLES, Terrain3DEditor.REPLACE, "Create Holes", load(ICON_HOLES), tool_group)
add_tool_button(Terrain3DEditor.NAVIGATION, Terrain3DEditor.REPLACE, "Paint Navigable Area", load(ICON_NAVIGATION), tool_group)
var buttons: Array[BaseButton] = tool_group.get_buttons()
buttons[0].set_pressed(true)
func add_tool_button(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation,
p_tip: String, p_icon: Texture2D, p_group: ButtonGroup) -> void:
var button: Button = Button.new()
button.set_meta("Tool", p_tool)
button.set_meta("Operation", p_operation)
button.set_tooltip_text(p_tip)
button.set_button_icon(p_icon)
button.set_button_group(p_group)
button.set_flat(true)
button.set_toggle_mode(true)
button.set_h_size_flags(SIZE_SHRINK_END)
add_child(button)
func _on_tool_selected(p_button: BaseButton) -> void:
emit_signal("tool_changed", p_button.get_meta("Tool", -1), p_button.get_meta("Operation", -1))

View file

@ -0,0 +1,379 @@
extends Node
#class_name Terrain3DUI Cannot be named until Godot #75388
# Includes
const Toolbar: Script = preload("res://addons/terrain_3d/editor/components/toolbar.gd")
const ToolSettings: Script = preload("res://addons/terrain_3d/editor/components/tool_settings.gd")
const TerrainTools: Script = preload("res://addons/terrain_3d/editor/components/terrain_tools.gd")
const OperationBuilder: Script = preload("res://addons/terrain_3d/editor/components/operation_builder.gd")
const GradientOperationBuilder: Script = preload("res://addons/terrain_3d/editor/components/gradient_operation_builder.gd")
const RING1: String = "res://addons/terrain_3d/editor/brushes/ring1.exr"
const COLOR_RAISE := Color.WHITE
const COLOR_LOWER := Color.BLACK
const COLOR_SMOOTH := Color(0.5, 0, .1)
const COLOR_EXPAND := Color.ORANGE
const COLOR_REDUCE := Color.BLUE_VIOLET
const COLOR_FLATTEN := Color(0., 0.32, .4)
const COLOR_SLOPE := Color.YELLOW
const COLOR_PAINT := Color.FOREST_GREEN
const COLOR_SPRAY := Color.SEA_GREEN
const COLOR_ROUGHNESS := Color.ROYAL_BLUE
const COLOR_AUTOSHADER := Color.DODGER_BLUE
const COLOR_HOLES := Color.BLACK
const COLOR_NAVIGATION := Color.REBECCA_PURPLE
const COLOR_PICK_COLOR := Color.WHITE
const COLOR_PICK_HEIGHT := Color.DARK_RED
const COLOR_PICK_ROUGH := Color.ROYAL_BLUE
var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors
var toolbar: Toolbar
var toolbar_settings: ToolSettings
var terrain_tools: TerrainTools
var setting_has_changed: bool = false
var visible: bool = false
var picking: int = Terrain3DEditor.TOOL_MAX
var picking_callback: Callable
var decal: Decal
var decal_timer: Timer
var gradient_decals: Array[Decal]
var brush_data: Dictionary
var operation_builder: OperationBuilder
@onready var picker_texture: ImageTexture = ImageTexture.create_from_image(Image.load_from_file(RING1))
func _enter_tree() -> void:
toolbar = Toolbar.new()
toolbar.hide()
toolbar.connect("tool_changed", _on_tool_changed)
toolbar_settings = ToolSettings.new()
toolbar_settings.connect("setting_changed", _on_setting_changed)
toolbar_settings.connect("picking", _on_picking)
toolbar_settings.hide()
terrain_tools = TerrainTools.new()
terrain_tools.plugin = plugin
terrain_tools.hide()
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_tools)
decal = Decal.new()
add_child(decal)
decal_timer = Timer.new()
decal_timer.wait_time = .5
decal_timer.one_shot = true
decal_timer.timeout.connect(Callable(func(node):
if node:
get_tree().create_tween().tween_property(node, "albedo_mix", 0.0, 0.15)).bind(decal))
add_child(decal_timer)
func _exit_tree() -> void:
plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar)
plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings)
toolbar.queue_free()
toolbar_settings.queue_free()
terrain_tools.queue_free()
decal.queue_free()
decal_timer.queue_free()
for gradient_decal in gradient_decals:
gradient_decal.queue_free()
gradient_decals.clear()
func set_visible(p_visible: bool) -> void:
visible = p_visible
toolbar.set_visible(p_visible and plugin.terrain)
terrain_tools.set_visible(p_visible)
if p_visible and plugin.terrain:
p_visible = plugin.editor.get_tool() != Terrain3DEditor.REGION
toolbar_settings.set_visible(p_visible and plugin.terrain)
update_decal()
func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void:
clear_picking()
if not visible or not plugin.terrain:
return
if plugin.editor:
plugin.editor.set_tool(p_tool)
plugin.editor.set_operation(p_operation)
if p_tool != Terrain3DEditor.REGION:
# Select which settings to hide. Options:
# size, opactiy, height, slope, color, roughness, (height|color|roughness) picker
var to_hide: PackedStringArray = []
if p_tool == Terrain3DEditor.HEIGHT:
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
if p_operation != Terrain3DEditor.REPLACE:
to_hide.push_back("height")
to_hide.push_back("height picker")
if p_operation != Terrain3DEditor.GRADIENT:
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
elif p_tool == Terrain3DEditor.TEXTURE:
to_hide.push_back("height")
to_hide.push_back("height picker")
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
if p_operation == Terrain3DEditor.REPLACE:
to_hide.push_back("opacity")
elif p_tool == Terrain3DEditor.COLOR:
to_hide.push_back("height")
to_hide.push_back("height picker")
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
elif p_tool == Terrain3DEditor.ROUGHNESS:
to_hide.push_back("height")
to_hide.push_back("height picker")
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
elif p_tool in [ Terrain3DEditor.AUTOSHADER, Terrain3DEditor.HOLES, Terrain3DEditor.NAVIGATION ]:
to_hide.push_back("height")
to_hide.push_back("height picker")
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("opacity")
toolbar_settings.hide_settings(to_hide)
toolbar_settings.set_visible(p_tool != Terrain3DEditor.REGION)
operation_builder = null
if p_operation == Terrain3DEditor.GRADIENT:
operation_builder = GradientOperationBuilder.new()
operation_builder.tool_settings = toolbar_settings
_on_setting_changed()
plugin.update_region_grid()
func _on_setting_changed() -> void:
if not visible or not plugin.terrain:
return
brush_data = {
"size": int(toolbar_settings.get_setting("size")),
"opacity": toolbar_settings.get_setting("opacity") / 100.0,
"height": toolbar_settings.get_setting("height"),
"texture_index": plugin.texture_dock.get_selected_index(),
"color": toolbar_settings.get_setting("color"),
"roughness": toolbar_settings.get_setting("roughness"),
"gradient_points": toolbar_settings.get_setting("gradient_points"),
"enable": toolbar_settings.get_setting("enable"),
"automatic_regions": toolbar_settings.get_setting("automatic_regions"),
"align_to_view": toolbar_settings.get_setting("align_to_view"),
"show_cursor_while_painting": toolbar_settings.get_setting("show_cursor_while_painting"),
"gamma": toolbar_settings.get_setting("gamma"),
"jitter": toolbar_settings.get_setting("jitter"),
}
var brush_imgs: Array = toolbar_settings.get_setting("brush")
brush_data["image"] = brush_imgs[0]
brush_data["texture"] = brush_imgs[1]
update_decal()
plugin.editor.set_brush_data(brush_data)
func update_decal() -> void:
var mouse_buttons: int = Input.get_mouse_button_mask()
if not visible or \
not plugin.terrain or \
brush_data.is_empty() or \
mouse_buttons & MOUSE_BUTTON_RIGHT or \
(mouse_buttons & MOUSE_BUTTON_LEFT and not brush_data["show_cursor_while_painting"]) or \
plugin.editor.get_tool() == Terrain3DEditor.REGION:
decal.visible = false
for gradient_decal in gradient_decals:
gradient_decal.visible = false
return
else:
# Wait for cursor to recenter after right-click before revealing
# See https://github.com/godotengine/godot/issues/70098
await get_tree().create_timer(.05).timeout
decal.visible = true
decal.size = Vector3.ONE * brush_data["size"]
if brush_data["align_to_view"]:
var cam: Camera3D = plugin.terrain.get_camera();
if (cam):
decal.rotation.y = cam.rotation.y
else:
decal.rotation.y = 0
# Set texture and color
if picking != Terrain3DEditor.TOOL_MAX:
decal.texture_albedo = picker_texture
decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing()
match picking:
Terrain3DEditor.HEIGHT:
decal.modulate = COLOR_PICK_HEIGHT
Terrain3DEditor.COLOR:
decal.modulate = COLOR_PICK_COLOR
Terrain3DEditor.ROUGHNESS:
decal.modulate = COLOR_PICK_ROUGH
decal.modulate.a = 1.0
else:
decal.texture_albedo = brush_data["texture"]
match plugin.editor.get_tool():
Terrain3DEditor.HEIGHT:
match plugin.editor.get_operation():
Terrain3DEditor.ADD:
decal.modulate = COLOR_RAISE
Terrain3DEditor.SUBTRACT:
decal.modulate = COLOR_LOWER
Terrain3DEditor.MULTIPLY:
decal.modulate = COLOR_EXPAND
Terrain3DEditor.DIVIDE:
decal.modulate = COLOR_REDUCE
Terrain3DEditor.REPLACE:
decal.modulate = COLOR_FLATTEN
Terrain3DEditor.AVERAGE:
decal.modulate = COLOR_SMOOTH
Terrain3DEditor.GRADIENT:
decal.modulate = COLOR_SLOPE
_:
decal.modulate = Color.WHITE
decal.modulate.a = max(.3, brush_data["opacity"])
Terrain3DEditor.TEXTURE:
match plugin.editor.get_operation():
Terrain3DEditor.REPLACE:
decal.modulate = COLOR_PAINT
decal.modulate.a = 1.0
Terrain3DEditor.ADD:
decal.modulate = COLOR_SPRAY
decal.modulate.a = max(.3, brush_data["opacity"])
_:
decal.modulate = Color.WHITE
Terrain3DEditor.COLOR:
decal.modulate = brush_data["color"].srgb_to_linear()*.5
decal.modulate.a = max(.3, brush_data["opacity"])
Terrain3DEditor.ROUGHNESS:
decal.modulate = COLOR_ROUGHNESS
decal.modulate.a = max(.3, brush_data["opacity"])
Terrain3DEditor.AUTOSHADER:
decal.modulate = COLOR_AUTOSHADER
decal.modulate.a = 1.0
Terrain3DEditor.HOLES:
decal.modulate = COLOR_HOLES
decal.modulate.a = 1.0
Terrain3DEditor.NAVIGATION:
decal.modulate = COLOR_NAVIGATION
decal.modulate.a = 1.0
_:
decal.modulate = Color.WHITE
decal.modulate.a = max(.3, brush_data["opacity"])
decal.size.y = max(1000, decal.size.y)
decal.albedo_mix = 1.0
decal.cull_mask = 1 << ( plugin.terrain.get_mouse_layer() - 1 )
decal_timer.start()
for gradient_decal in gradient_decals:
gradient_decal.visible = false
if plugin.editor.get_operation() == Terrain3DEditor.GRADIENT:
var index := 0
for point in brush_data["gradient_points"]:
if point != Vector3.ZERO:
var point_decal: Decal = _get_gradient_decal(index)
point_decal.visible = true
point_decal.position = point
index += 1
func _get_gradient_decal(index: int) -> Decal:
if gradient_decals.size() > index:
return gradient_decals[index]
var gradient_decal := Decal.new()
gradient_decal = Decal.new()
gradient_decal.texture_albedo = picker_texture
gradient_decal.modulate = COLOR_SLOPE
gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing()
gradient_decal.size.y = 1000.
gradient_decal.cull_mask = decal.cull_mask
add_child(gradient_decal)
gradient_decals.push_back(gradient_decal)
return gradient_decal
func set_decal_rotation(p_rot: float) -> void:
decal.rotation.y = p_rot
func _on_picking(p_type: int, p_callback: Callable) -> void:
picking = p_type
picking_callback = p_callback
update_decal()
func clear_picking() -> void:
picking = Terrain3DEditor.TOOL_MAX
func is_picking() -> bool:
if picking != Terrain3DEditor.TOOL_MAX:
return true
if operation_builder and operation_builder.is_picking():
return true
return false
func pick(p_global_position: Vector3) -> void:
if picking != Terrain3DEditor.TOOL_MAX:
var color: Color
match picking:
Terrain3DEditor.HEIGHT:
color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_HEIGHT, p_global_position)
Terrain3DEditor.ROUGHNESS:
color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_COLOR, p_global_position)
Terrain3DEditor.COLOR:
color = plugin.terrain.get_storage().get_color(p_global_position)
_:
push_error("Unsupported picking type: ", picking)
return
picking_callback.call(picking, color, p_global_position)
picking = Terrain3DEditor.TOOL_MAX
elif operation_builder and operation_builder.is_picking():
operation_builder.pick(p_global_position, plugin.terrain)

View file

@ -0,0 +1,282 @@
@tool
extends EditorPlugin
#class_name Terrain3DEditorPlugin Cannot be named until Godot #75388
# Includes
const UI: Script = preload("res://addons/terrain_3d/editor/components/ui.gd")
const RegionGizmo: Script = preload("res://addons/terrain_3d/editor/components/region_gizmo.gd")
const TextureDock: Script = preload("res://addons/terrain_3d/editor/components/texture_dock.gd")
var terrain: Terrain3D
var nav_region: NavigationRegion3D
var editor: Terrain3DEditor
var ui: Node # Terrain3DUI see Godot #75388
var texture_dock: TextureDock
var texture_dock_container: CustomControlContainer = CONTAINER_INSPECTOR_BOTTOM
var visible: bool
var region_gizmo: RegionGizmo
var current_region_position: Vector2
var mouse_global_position: Vector3 = Vector3.ZERO
func _enter_tree() -> void:
editor = Terrain3DEditor.new()
ui = UI.new()
ui.plugin = self
add_child(ui)
texture_dock = TextureDock.new()
texture_dock.hide()
texture_dock.resource_changed.connect(_on_texture_dock_resource_changed)
texture_dock.resource_inspected.connect(_on_texture_dock_resource_selected)
texture_dock.resource_selected.connect(ui._on_setting_changed)
region_gizmo = RegionGizmo.new()
add_control_to_container(texture_dock_container, texture_dock)
texture_dock.get_parent().visibility_changed.connect(_on_texture_dock_visibility_changed)
func _exit_tree() -> void:
remove_control_from_container(texture_dock_container, texture_dock)
texture_dock.queue_free()
ui.queue_free()
editor.free()
func _handles(p_object: Object) -> bool:
return p_object is Terrain3D or p_object is NavigationRegion3D
func _edit(p_object: Object) -> void:
if !p_object:
_clear()
if p_object is Terrain3D:
if p_object == terrain:
return
terrain = p_object
editor.set_terrain(terrain)
region_gizmo.set_node_3d(terrain)
terrain.add_gizmo(region_gizmo)
terrain.set_plugin(self)
if not terrain.texture_list_changed.is_connected(_load_textures):
terrain.texture_list_changed.connect(_load_textures)
_load_textures()
if not terrain.storage_changed.is_connected(_load_storage):
terrain.storage_changed.connect(_load_storage)
_load_storage()
else:
terrain = null
if p_object is NavigationRegion3D:
nav_region = p_object
else:
nav_region = null
_update_visibility()
func _make_visible(p_visible: bool) -> void:
visible = p_visible
_update_visibility()
func _update_visibility() -> void:
ui.set_visible(visible)
texture_dock.set_visible(visible and terrain)
if terrain:
update_region_grid()
region_gizmo.set_hidden(not visible or not terrain)
func _clear() -> void:
if is_terrain_valid():
terrain.storage_changed.disconnect(_load_storage)
terrain.clear_gizmos()
terrain = null
editor.set_terrain(null)
ui.clear_picking()
region_gizmo.clear()
func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> int:
if not is_terrain_valid():
return AFTER_GUI_INPUT_PASS
## Handle mouse movement
if p_event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
return AFTER_GUI_INPUT_PASS
## Get mouse location on terrain
# Snap terrain to current camera
terrain.set_camera(p_viewport_camera)
# Detect if viewport is set to half_resolution
# Structure is: Node3DEditorViewportContainer/Node3DEditorViewport/SubViewportContainer/SubViewport/Camera3D
var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent()
var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true
# Project 2D mouse position to 3D position and direction
var mouse_pos: Vector2 = p_event.position if full_resolution else p_event.position/2
var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos)
var camera_dir: Vector3 = p_viewport_camera.project_ray_normal(mouse_pos)
# If region tool, grab mouse position without considering height
if editor.get_tool() == Terrain3DEditor.REGION:
var t = -Vector3(0, 1, 0).dot(camera_pos) / Vector3(0, 1, 0).dot(camera_dir)
mouse_global_position = (camera_pos + t * camera_dir)
else:
# Else look for intersection with terrain
var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir)
if intersection_point.z > 3.4e38: # double max
return AFTER_GUI_INPUT_STOP
mouse_global_position = intersection_point
## Update decal
ui.decal.global_position = mouse_global_position
ui.decal.albedo_mix = 1.0
if ui.decal_timer.is_stopped():
ui.update_decal()
else:
ui.decal_timer.start()
## Update region highlight
var region_size = terrain.get_storage().get_region_size()
var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \
/ (region_size * terrain.get_mesh_vertex_spacing()) ).floor()
if current_region_position != region_position:
current_region_position = region_position
update_region_grid()
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and editor.is_operating():
editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
elif p_event is InputEventMouseButton:
ui.update_decal()
if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
if p_event.is_pressed():
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
return AFTER_GUI_INPUT_STOP
# If picking
if ui.is_picking():
ui.pick(mouse_global_position)
if not ui.operation_builder or not ui.operation_builder.is_ready():
return AFTER_GUI_INPUT_STOP
# If adjusting regions
if editor.get_tool() == Terrain3DEditor.REGION:
# Skip regions that already exist or don't
var has_region: bool = terrain.get_storage().has_region(mouse_global_position)
var op: int = editor.get_operation()
if ( has_region and op == Terrain3DEditor.ADD) or \
( not has_region and op == Terrain3DEditor.SUBTRACT ):
return AFTER_GUI_INPUT_STOP
# If an automatic operation is ready to go (e.g. gradient)
if ui.operation_builder and ui.operation_builder.is_ready():
ui.operation_builder.apply_operation(editor, mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
# Mouse clicked, start editing
editor.start_operation(mouse_global_position)
editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
elif editor.is_operating():
# Mouse released, save undo data
editor.stop_operation()
return AFTER_GUI_INPUT_STOP
return AFTER_GUI_INPUT_PASS
func is_terrain_valid() -> bool:
var valid: bool = false
if is_instance_valid(terrain):
valid = terrain.get_storage() != null
return valid
func update_texture_dock(p_args: Array) -> void:
texture_dock.clear()
if is_terrain_valid() and terrain.texture_list:
var texture_count: int = terrain.texture_list.get_texture_count()
for i in texture_count:
var texture: Terrain3DTexture = terrain.texture_list.get_texture(i)
texture_dock.add_item(texture)
if texture_count < Terrain3DTextureList.MAX_TEXTURES:
texture_dock.add_item()
func update_region_grid() -> void:
if !region_gizmo.get_node_3d():
return
if is_terrain_valid():
region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION
region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT
region_gizmo.region_position = current_region_position
region_gizmo.region_size = terrain.get_storage().get_region_size() * terrain.get_mesh_vertex_spacing()
region_gizmo.grid = terrain.get_storage().get_region_offsets()
terrain.update_gizmos()
return
region_gizmo.show_rect = false
region_gizmo.region_size = 1024
region_gizmo.grid = [Vector2i.ZERO]
# Signal handlers
func _load_textures() -> void:
if terrain and terrain.texture_list:
if not terrain.texture_list.textures_changed.is_connected(update_texture_dock):
terrain.texture_list.textures_changed.connect(update_texture_dock)
update_texture_dock(Array())
func _load_storage() -> void:
if terrain:
update_region_grid()
func _on_texture_dock_resource_changed(texture: Resource, index: int) -> void:
if is_terrain_valid():
# If removing last entry and its selected, clear inspector
if not texture and index == texture_dock.get_selected_index() and \
texture_dock.get_selected_index() == texture_dock.entries.size() - 2:
get_editor_interface().inspect_object(null)
terrain.get_texture_list().set_texture(index, texture)
call_deferred("_load_storage")
func _on_texture_dock_resource_selected(texture) -> void:
get_editor_interface().inspect_object(texture, "", true)
func _on_texture_dock_visibility_changed() -> void:
if texture_dock.get_parent() != null:
remove_control_from_container(texture_dock_container, texture_dock)
if texture_dock.get_parent() == null:
texture_dock_container = CONTAINER_INSPECTOR_BOTTOM
if get_editor_interface().is_distraction_free_mode_enabled():
texture_dock_container = CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT
add_control_to_container(texture_dock_container, texture_dock)

View file

@ -0,0 +1,109 @@
// This shader is the minimum needed to allow the terrain to function.
shader_type spatial;
render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx;
// Private uniforms
uniform float _region_size = 1024.0;
uniform float _region_texel_size = 0.0009765625; // = 1/1024
uniform float _mesh_vertex_spacing = 1.0;
uniform float _mesh_vertex_density = 1.0; // = 1/_mesh_vertex_spacing
uniform int _region_map_size = 16;
uniform int _region_map[256];
uniform vec2 _region_offsets[256];
uniform sampler2DArray _height_maps : repeat_disable;
varying vec3 v_vertex; // World coordinate vertex location
////////////////////////
// Vertex
////////////////////////
// Takes in UV world space coordinates, returns ivec3 with:
// XY: (0 to _region_size) coordinates within a region
// Z: layer index used for texturearrays, -1 if not in a region
ivec3 get_region_uv(vec2 uv) {
uv *= _region_texel_size;
ivec2 pos = ivec2(floor(uv)) + (_region_map_size / 2);
int bounds = int(pos.x>=0 && pos.x<_region_map_size && pos.y>=0 && pos.y<_region_map_size);
int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1;
return ivec3(ivec2((uv - _region_offsets[layer_index]) * _region_size), layer_index);
}
// Takes in UV2 region space coordinates, returns vec3 with:
// XY: (0 to 1) coordinates within a region
// Z: layer index used for texturearrays, -1 if not in a region
vec3 get_region_uv2(vec2 uv) {
ivec2 pos = ivec2(floor(uv)) + (_region_map_size / 2);
int bounds = int(pos.x>=0 && pos.x<_region_map_size && pos.y>=0 && pos.y<_region_map_size);
int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1;
return vec3(uv - _region_offsets[layer_index], float(layer_index));
}
float get_height(vec2 uv) {
highp float height = 0.0;
vec3 region = get_region_uv2(uv);
if (region.z >= 0.) {
height = texture(_height_maps, region).r;
}
return height;
}
void vertex() {
// Get vertex of flat plane in world coordinates and set world UV
v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
// UV coordinates in world space. Values are 0 to _region_size within regions
UV = round(v_vertex.xz * _mesh_vertex_density);
// UV coordinates in region space + texel offset. Values are 0 to 1 within regions
UV2 = (UV + vec2(0.5)) * _region_texel_size;
// Get final vertex location and save it
VERTEX.y = get_height(UV2);
v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
}
////////////////////////
// Fragment
////////////////////////
vec3 get_normal(vec2 uv, out vec3 tangent, out vec3 binormal) {
// Get the height of the current vertex
float height = get_height(uv);
// Get the heights to the right and in front, but because of hardware
// interpolation on the edges of the heightmaps, the values are off
// causing the normal map to look weird. So, near the edges of the map
// get the heights to the left or behind instead. Hacky solution that
// reduces the artifact, but doesn't fix it entirely. See #185.
float u, v;
if(mod(uv.y*_region_size, _region_size) > _region_size-2.) {
v = get_height(uv + vec2(0, -_region_texel_size)) - height;
} else {
v = height - get_height(uv + vec2(0, _region_texel_size));
}
if(mod(uv.x*_region_size, _region_size) > _region_size-2.) {
u = get_height(uv + vec2(-_region_texel_size, 0)) - height;
} else {
u = height - get_height(uv + vec2(_region_texel_size, 0));
}
vec3 normal = vec3(u, _mesh_vertex_spacing, v);
normal = normalize(normal);
tangent = cross(normal, vec3(0, 0, 1));
binormal = cross(normal, tangent);
return normal;
}
void fragment() {
// Calculate Terrain Normals
vec3 w_tangent, w_binormal;
vec3 w_normal = get_normal(UV2, w_tangent, w_binormal);
NORMAL = mat3(VIEW_MATRIX) * w_normal;
TANGENT = mat3(VIEW_MATRIX) * w_tangent;
BINORMAL = mat3(VIEW_MATRIX) * w_binormal;
// Apply PBR
ALBEDO=vec3(.2);
}

View file

@ -0,0 +1,83 @@
# This script is an addon for HungryProton's Scatter https://github.com/HungryProton/scatter
# It allows Scatter to detect the terrain height from Terrain3D
# Copy this file into /addons/proton_scatter/src/modifiers
# Then uncomment everything below
# In the editor, add this modifier, then set your Terrain3D node
#@tool
#extends "base_modifier.gd"
#
#
#signal projection_completed
#
#
#@export var terrain_node : NodePath
#@export var align_with_collision_normal := false
#
#var _terrain: Terrain3D
#
#
#func _init() -> void:
# display_name = "Project On Terrain3D"
# category = "Edit"
# can_restrict_height = false
# global_reference_frame_available = true
# local_reference_frame_available = true
# individual_instances_reference_frame_available = true
# use_global_space_by_default()
#
# documentation.add_paragraph(
# "This is a duplicate of `Project on Colliders` that queries the terrain system
# for height and sets the transform height appropriately.
#
# This modifier must have terrain_node set to a Terrain3D node.")
#
# var p := documentation.add_parameter("Terrain Node")
# p.set_type("NodePath")
# p.set_description("Set your Terrain3D node.")
#
# p = documentation.add_parameter("Align with collision normal")
# p.set_type("bool")
# p.set_description(
# "Rotate the transform to align it with the collision normal in case
# the ray cast hit a collider.")
#
#
#func _process_transforms(transforms, domain, _seed) -> void:
# if transforms.is_empty():
# return
#
# if terrain_node:
# _terrain = domain.get_root().get_node_or_null(terrain_node)
#
# if not _terrain:
# warning += """No Terrain3D node found"""
# return
#
# if not _terrain.storage:
# warning += """Terrain3D storage is not initialized"""
# return
#
# # Get global transform
# var gt: Transform3D = domain.get_global_transform()
# var gt_inverse: Transform3D = gt.affine_inverse()
# for i in transforms.list.size():
# var location: Vector3 = (gt * transforms.list[i]).origin
# var height: float = _terrain.storage.get_height(location)
# var normal: Vector3 = _terrain.storage.get_normal(location)
#
# if align_with_collision_normal:
# transforms.list[i].basis.y = normal
# transforms.list[i].basis.x = -transforms.list[i].basis.z.cross(normal)
# transforms.list[i].basis = transforms.list[i].basis.orthonormalized()
#
# transforms.list[i].origin.y = height - gt.origin.y
#
# if transforms.is_empty():
# warning += """Every point has been removed. Possible reasons include: \n
# + No collider is close enough to the shapes.
# + Ray length is too short.
# + Ray direction is incorrect.
# + Collision mask is not set properly.
# + Max slope is too low.
# """

Some files were not shown because too many files have changed in this diff Show more