🎉 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

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.
# """

View file

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
id="svg2"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="icon_brush.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
id="metadata12"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs10"><inkscape:path-effect
effect="spiro"
id="path-effect6172"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect5927"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect5921"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect5163"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4431"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4425"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4419"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4413"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4407"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4401"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4395"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4389"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect2931"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect2925"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect3156"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4245"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4977"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect3156-0"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect3156-3"
is_visible="true"
lpeversion="1" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1009"
id="namedview8"
showgrid="false"
inkscape:zoom="22.627418"
inkscape:cx="-1.5246989"
inkscape:cy="7.6455917"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:object-paths="false"
inkscape:snap-smooth-nodes="false"
inkscape:object-nodes="true"
inkscape:snap-grids="false"
inkscape:snap-to-guides="true"
inkscape:pagecheckerboard="0"
inkscape:showpageshadow="0"
inkscape:deskcolor="#d1d1d1"><inkscape:grid
type="xygrid"
id="grid4140" /></sodipodi:namedview><g
id="g5578"
transform="translate(-27.379572,-6.4315344)"><path
fill="none"
d="M 0,0 H 24 V 24 H 0 Z"
id="path5566" /></g><path
style="color:#000000;fill:#fc7f7f;stroke-width:1.04669;stroke-linecap:round;-inkscape-stroke:none;paint-order:stroke fill markers"
d="M 0,0 V 0.90647294 5.8222656 H 16 V 0 Z"
id="rect5998"
sodipodi:nodetypes="cccccc" /><path
style="fill:none;stroke:#fc7f7f;stroke-width:1.7;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 0.89644665,6.6249995 7.404e-4,2.0000026 7.34802865,-0.0028 -0.016663,7.0027939"
id="path6170"
inkscape:path-effect="#path-effect6172"
inkscape:original-d="m 0.89644665,6.6249995 7.404e-4,2.0000026 7.34802865,-0.0028 -0.016663,7.0027939"
sodipodi:nodetypes="cccc" /><rect
style="fill:none;stroke:#fc7f7f;stroke-width:1.44866;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="rect7902"
width="2.1763401"
height="3.3013401"
x="7.2028828"
y="11.911829" /><path
id="path12210"
style="fill:#fc7f7f;stroke:none;stroke-width:1.8;stroke-linecap:round;paint-order:stroke fill markers;fill-opacity:1"
d="m 15.21067,10.713897 c 0.280041,1.208799 -0.309734,2.27275 -1.860725,2.405567 -1.152356,0.09868 -2.35276,-1.290185 -1.83455,-2.380469 l 1.827424,-3.8447978 1.867455,3.8158258"
sodipodi:nodetypes="csscc" /></svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cjrjfjmbyseol"
path="res://.godot/imported/icon_brush.svg-8886426485f67abe2233686de39952ce.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/terrain_3d/icons/icon_brush.svg"
dest_files=["res://.godot/imported/icon_brush.svg-8886426485f67abe2233686de39952ce.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
id="svg2"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="icon_color.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
id="metadata12"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs10"><linearGradient
inkscape:collect="always"
id="linearGradient40333"><stop
style="stop-color:#fc7f7f;stop-opacity:1;"
offset="0"
id="stop40329" /><stop
style="stop-color:#7fb0bd;stop-opacity:1;"
offset="0.50160772"
id="stop40337" /><stop
style="stop-color:#7ffc7f;stop-opacity:1;"
offset="1"
id="stop40331" /></linearGradient><inkscape:path-effect
effect="spiro"
id="path-effect8712"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="powerstroke"
id="path-effect8643"
is_visible="true"
lpeversion="1"
offset_points="0.86174895,0.26516503"
not_jump="true"
sort_points="true"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.75"
start_linecap_type="round"
linejoin_type="spiro"
miter_limit="4"
scale_width="1"
end_linecap_type="round" /><inkscape:path-effect
effect="simplify"
id="path-effect8641"
is_visible="true"
lpeversion="1"
steps="1"
threshold="0.0028332064"
smooth_angles="0"
helper_size="0"
simplify_individual_paths="false"
simplify_just_coalesce="false"
step="1" /><inkscape:path-effect
effect="spiro"
id="path-effect5927"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect5921"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect5163"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4431"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4425"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4419"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4413"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4407"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4401"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4395"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4389"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect2931"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect2925"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect3156"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4245"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect4977"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect3156-0"
is_visible="true"
lpeversion="1" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient40333"
id="linearGradient35201"
x1="5.3624678"
y1="3.5122564"
x2="12.772088"
y2="11.423013"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1009"
id="namedview8"
showgrid="false"
inkscape:zoom="22.627418"
inkscape:cx="7.5793003"
inkscape:cy="9.4133584"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:object-paths="false"
inkscape:snap-smooth-nodes="false"
inkscape:object-nodes="true"
inkscape:snap-grids="false"
inkscape:snap-to-guides="true"
inkscape:pagecheckerboard="0"
inkscape:showpageshadow="0"
inkscape:deskcolor="#d1d1d1"><inkscape:grid
type="xygrid"
id="grid4140" /></sodipodi:namedview><path
fill="none"
d="M -0.76108454,-0.19858458 H 16.761085 V 17.323585 H -0.76108454 Z"
id="path34808"
style="stroke-width:0.73009" /><path
d="M 3.3537048,4.6462952 8,0 12.646295,4.6462952 c 4.13963,4.139681 1.207353,11.2167178 -4.6462951,11.2167178 -5.8536477,0 -8.78592555,-7.0770368 -4.6462951,-11.2167178 z"
id="path34810"
style="fill:url(#linearGradient35201);fill-opacity:1;stroke-width:0.73009"
sodipodi:nodetypes="cccsc" /></svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

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