Download et installation of Height Terrain addon by Zylann
11
addons/zylann.hterrain/LICENSE.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
HeightMap terrain for Godot Engine
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 2016-2020 Marc Gilleron
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
0
addons/zylann.hterrain/doc/.gdignore
Normal file
After Width: | Height: | Size: 313 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/bad_array_blending.png
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/brush_editor.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/channel_packer.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/choose_bullet_physics.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/color_painting.png
Normal file
After Width: | Height: | Size: 338 KiB |
After Width: | Height: | Size: 8.2 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/default_terrain.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/detail_layers.png
Normal file
After Width: | Height: | Size: 722 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/dilation.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/erosion_slope.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/erosion_steps.png
Normal file
After Width: | Height: | Size: 199 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/generator.png
Normal file
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 8.4 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/grass_models.png
Normal file
After Width: | Height: | Size: 207 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/hole_painting.png
Normal file
After Width: | Height: | Size: 238 KiB |
After Width: | Height: | Size: 14 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/importer.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/inspector_texture_set.png
Normal file
After Width: | Height: | Size: 250 KiB |
After Width: | Height: | Size: 28 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/lod_geometry.png
Normal file
After Width: | Height: | Size: 405 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/lookdev_grass.png
Normal file
After Width: | Height: | Size: 724 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/lookdev_menu.png
Normal file
After Width: | Height: | Size: 225 KiB |
After Width: | Height: | Size: 180 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/low_poly.png
Normal file
After Width: | Height: | Size: 228 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/normalmap_conventions.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/overview.png
Normal file
After Width: | Height: | Size: 426 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/panel_import_button.png
Normal file
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 28 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/pbr_textures.png
Normal file
After Width: | Height: | Size: 168 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/resize_tool.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/sculpting_tools.png
Normal file
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 216 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/slope_limit_tool.png
Normal file
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 8.8 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/texture_atlas_example.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/texture_dialog.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/texture_set_editor.png
Normal file
After Width: | Height: | Size: 150 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 149 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/texture_slots.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
addons/zylann.hterrain/doc/docs/images/tiling_artifacts.png
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
addons/zylann.hterrain/doc/docs/images/tiling_reduction.png
Normal file
After Width: | Height: | Size: 1 MiB |
After Width: | Height: | Size: 132 KiB |
After Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 816 KiB |
1150
addons/zylann.hterrain/doc/docs/index.md
Normal file
15
addons/zylann.hterrain/doc/mkdocs.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
site_name: HTerrain plugin documentation
|
||||||
|
theme: readthedocs
|
||||||
|
|
||||||
|
# I had to specify this even though it's supposed to be the default
|
||||||
|
# See https://github.com/mkdocs/mkdocs/issues/2145#issuecomment-735342512
|
||||||
|
docs_dir: docs
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
# Makes permalinks appear on headings
|
||||||
|
- toc:
|
||||||
|
permalink: True
|
||||||
|
# Makes boxes for notes and warnings
|
||||||
|
- admonition
|
||||||
|
# Better highlighter which supports GDScript
|
||||||
|
- codehilite
|
1
addons/zylann.hterrain/doc/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mkdocs>=1.1.2
|
1548
addons/zylann.hterrain/hterrain.gd
Normal file
115
addons/zylann.hterrain/hterrain_chunk.gd
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
tool
|
||||||
|
|
||||||
|
var cell_origin_x := 0
|
||||||
|
var cell_origin_y := 0
|
||||||
|
|
||||||
|
var _visible : bool
|
||||||
|
# This is true when the chunk is meant to be displayed.
|
||||||
|
# A chunk can be active and hidden (due to the terrain being hidden).
|
||||||
|
var _active : bool
|
||||||
|
|
||||||
|
var _pending_update : bool
|
||||||
|
|
||||||
|
var _mesh_instance : RID
|
||||||
|
# Need to keep a reference so that the mesh RID doesn't get freed
|
||||||
|
# TODO Use RID directly, no need to keep all those meshes in memory
|
||||||
|
var _mesh : Mesh = null
|
||||||
|
|
||||||
|
|
||||||
|
# TODO p_parent is HTerrain, can't add type hint due to cyclic reference
|
||||||
|
func _init(p_parent, p_cell_x: int, p_cell_y: int, p_material: Material):
|
||||||
|
assert(p_parent is Spatial)
|
||||||
|
assert(typeof(p_cell_x) == TYPE_INT)
|
||||||
|
assert(typeof(p_cell_y) == TYPE_INT)
|
||||||
|
assert(p_material is Material)
|
||||||
|
|
||||||
|
cell_origin_x = p_cell_x
|
||||||
|
cell_origin_y = p_cell_y
|
||||||
|
|
||||||
|
var vs = VisualServer
|
||||||
|
|
||||||
|
_mesh_instance = vs.instance_create()
|
||||||
|
|
||||||
|
if p_material != null:
|
||||||
|
vs.instance_geometry_set_material_override(_mesh_instance, p_material.get_rid())
|
||||||
|
|
||||||
|
var world = p_parent.get_world()
|
||||||
|
if world != null:
|
||||||
|
vs.instance_set_scenario(_mesh_instance, world.get_scenario())
|
||||||
|
|
||||||
|
_visible = true
|
||||||
|
# TODO Is this needed?
|
||||||
|
vs.instance_set_visible(_mesh_instance, _visible)
|
||||||
|
|
||||||
|
_active = true
|
||||||
|
_pending_update = false
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(p_what: int):
|
||||||
|
if p_what == NOTIFICATION_PREDELETE:
|
||||||
|
if _mesh_instance != RID():
|
||||||
|
VisualServer.free_rid(_mesh_instance)
|
||||||
|
_mesh_instance = RID()
|
||||||
|
|
||||||
|
|
||||||
|
func is_active() -> bool:
|
||||||
|
return _active
|
||||||
|
|
||||||
|
|
||||||
|
func set_active(a):
|
||||||
|
_active = a
|
||||||
|
|
||||||
|
|
||||||
|
func is_pending_update() -> bool:
|
||||||
|
return _pending_update
|
||||||
|
|
||||||
|
|
||||||
|
func set_pending_update(p):
|
||||||
|
_pending_update = p
|
||||||
|
|
||||||
|
|
||||||
|
func enter_world(world):
|
||||||
|
assert(_mesh_instance != RID())
|
||||||
|
VisualServer.instance_set_scenario(_mesh_instance, world.get_scenario())
|
||||||
|
|
||||||
|
|
||||||
|
func exit_world():
|
||||||
|
assert(_mesh_instance != RID())
|
||||||
|
VisualServer.instance_set_scenario(_mesh_instance, RID())
|
||||||
|
|
||||||
|
|
||||||
|
func parent_transform_changed(parent_transform):
|
||||||
|
assert(_mesh_instance != RID())
|
||||||
|
var local_transform = Transform(Basis(), Vector3(cell_origin_x, 0, cell_origin_y))
|
||||||
|
var world_transform = parent_transform * local_transform
|
||||||
|
VisualServer.instance_set_transform(_mesh_instance, world_transform)
|
||||||
|
|
||||||
|
|
||||||
|
func set_mesh(mesh: Mesh):
|
||||||
|
assert(_mesh_instance != RID())
|
||||||
|
if mesh == _mesh:
|
||||||
|
return
|
||||||
|
VisualServer.instance_set_base(_mesh_instance, mesh.get_rid() if mesh != null else RID())
|
||||||
|
_mesh = mesh
|
||||||
|
|
||||||
|
|
||||||
|
func set_material(material: Material):
|
||||||
|
assert(_mesh_instance != RID())
|
||||||
|
VisualServer.instance_geometry_set_material_override( \
|
||||||
|
_mesh_instance, material.get_rid() if material != null else RID())
|
||||||
|
|
||||||
|
|
||||||
|
func set_visible(visible: bool):
|
||||||
|
assert(_mesh_instance != RID())
|
||||||
|
VisualServer.instance_set_visible(_mesh_instance, visible)
|
||||||
|
_visible = visible
|
||||||
|
|
||||||
|
|
||||||
|
func is_visible() -> bool:
|
||||||
|
return _visible
|
||||||
|
|
||||||
|
|
||||||
|
func set_aabb(aabb: AABB):
|
||||||
|
assert(_mesh_instance != RID())
|
||||||
|
VisualServer.instance_set_custom_aabb(_mesh_instance, aabb)
|
||||||
|
|
64
addons/zylann.hterrain/hterrain_chunk_debug.gd
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
tool
|
||||||
|
extends "hterrain_chunk.gd"
|
||||||
|
|
||||||
|
# I wrote this because Godot has no debug option to show AABBs.
|
||||||
|
# https://github.com/godotengine/godot/issues/20722
|
||||||
|
|
||||||
|
|
||||||
|
const DirectMeshInstance = preload("./util/direct_mesh_instance.gd")
|
||||||
|
const Util = preload("./util/util.gd")
|
||||||
|
|
||||||
|
|
||||||
|
var _debug_cube = null
|
||||||
|
var _aabb = AABB()
|
||||||
|
var _parent_transform = Transform()
|
||||||
|
|
||||||
|
|
||||||
|
func _init(p_parent, p_cell_x, p_cell_y, p_material).(p_parent, p_cell_x, p_cell_y, p_material):
|
||||||
|
var wirecube
|
||||||
|
if not p_parent.has_meta("debug_wirecube_mesh"):
|
||||||
|
wirecube = Util.create_wirecube_mesh()
|
||||||
|
var mat = SpatialMaterial.new()
|
||||||
|
mat.flags_unshaded = true
|
||||||
|
wirecube.surface_set_material(0, mat)
|
||||||
|
p_parent.set_meta("debug_wirecube_mesh", wirecube)
|
||||||
|
else:
|
||||||
|
wirecube = p_parent.get_meta("debug_wirecube_mesh")
|
||||||
|
|
||||||
|
_debug_cube = DirectMeshInstance.new()
|
||||||
|
_debug_cube.set_mesh(wirecube)
|
||||||
|
_debug_cube.set_world(p_parent.get_world())
|
||||||
|
|
||||||
|
|
||||||
|
func enter_world(world):
|
||||||
|
.enter_world(world)
|
||||||
|
_debug_cube.enter_world(world)
|
||||||
|
|
||||||
|
|
||||||
|
func exit_world():
|
||||||
|
.exit_world()
|
||||||
|
_debug_cube.exit_world()
|
||||||
|
|
||||||
|
|
||||||
|
func parent_transform_changed(parent_transform):
|
||||||
|
.parent_transform_changed(parent_transform)
|
||||||
|
_parent_transform = parent_transform
|
||||||
|
_debug_cube.set_transform(_compute_aabb())
|
||||||
|
|
||||||
|
|
||||||
|
func set_visible(visible):
|
||||||
|
.set_visible(visible)
|
||||||
|
_debug_cube.set_visible(visible)
|
||||||
|
|
||||||
|
|
||||||
|
func set_aabb(aabb):
|
||||||
|
.set_aabb(aabb)
|
||||||
|
#aabb.position.y += 0.2*randf()
|
||||||
|
_aabb = aabb
|
||||||
|
_debug_cube.set_transform(_compute_aabb())
|
||||||
|
|
||||||
|
|
||||||
|
func _compute_aabb():
|
||||||
|
var pos = Vector3(cell_origin_x, 0, cell_origin_y)
|
||||||
|
return _parent_transform * Transform(Basis().scaled(_aabb.size), pos + _aabb.position)
|
||||||
|
|
123
addons/zylann.hterrain/hterrain_collider.gd
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
tool
|
||||||
|
|
||||||
|
const Logger = preload("./util/logger.gd")
|
||||||
|
|
||||||
|
var _shape_rid = RID()
|
||||||
|
var _body_rid = RID()
|
||||||
|
var _terrain_transform = Transform()
|
||||||
|
var _terrain_data = null
|
||||||
|
var _logger = Logger.get_for(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _init(attached_node: Node, initial_layer: int, initial_mask: int):
|
||||||
|
_logger.debug("HTerrainCollider: creating body")
|
||||||
|
assert(attached_node != null)
|
||||||
|
_shape_rid = PhysicsServer.shape_create(PhysicsServer.SHAPE_HEIGHTMAP)
|
||||||
|
_body_rid = PhysicsServer.body_create(PhysicsServer.BODY_MODE_STATIC)
|
||||||
|
|
||||||
|
PhysicsServer.body_set_collision_layer(_body_rid, initial_layer)
|
||||||
|
PhysicsServer.body_set_collision_mask(_body_rid, initial_mask)
|
||||||
|
|
||||||
|
# TODO This is an attempt to workaround https://github.com/godotengine/godot/issues/24390
|
||||||
|
PhysicsServer.body_set_ray_pickable(_body_rid, false)
|
||||||
|
|
||||||
|
# TODO This is a workaround to https://github.com/godotengine/godot/issues/25304
|
||||||
|
PhysicsServer.shape_set_data(_shape_rid, {
|
||||||
|
"width": 2,
|
||||||
|
"depth": 2,
|
||||||
|
"heights": PoolRealArray([0, 0, 0, 0]),
|
||||||
|
"min_height": -1,
|
||||||
|
"max_height": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
PhysicsServer.body_add_shape(_body_rid, _shape_rid)
|
||||||
|
|
||||||
|
# This makes collision hits report the provided object as `collider`
|
||||||
|
PhysicsServer.body_attach_object_instance_id(_body_rid, attached_node.get_instance_id())
|
||||||
|
|
||||||
|
|
||||||
|
func set_collision_layer(layer: int):
|
||||||
|
PhysicsServer.body_set_collision_layer(_body_rid, layer)
|
||||||
|
|
||||||
|
|
||||||
|
func set_collision_mask(mask: int):
|
||||||
|
PhysicsServer.body_set_collision_mask(_body_rid, mask)
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what):
|
||||||
|
if what == NOTIFICATION_PREDELETE:
|
||||||
|
_logger.debug("Destroy HTerrainCollider")
|
||||||
|
PhysicsServer.free_rid(_body_rid)
|
||||||
|
# The shape needs to be freed after the body, otherwise the engine crashes
|
||||||
|
PhysicsServer.free_rid(_shape_rid)
|
||||||
|
|
||||||
|
|
||||||
|
func set_transform(transform):
|
||||||
|
assert(_body_rid != RID())
|
||||||
|
_terrain_transform = transform
|
||||||
|
_update_transform()
|
||||||
|
|
||||||
|
|
||||||
|
func set_world(world):
|
||||||
|
assert(_body_rid != RID())
|
||||||
|
PhysicsServer.body_set_space(_body_rid, world.get_space() if world != null else RID())
|
||||||
|
|
||||||
|
|
||||||
|
func create_from_terrain_data(terrain_data):
|
||||||
|
assert(terrain_data != null)
|
||||||
|
assert(not terrain_data.is_locked())
|
||||||
|
_logger.debug("HTerrainCollider: setting up heightmap")
|
||||||
|
|
||||||
|
_terrain_data = terrain_data
|
||||||
|
|
||||||
|
var aabb = terrain_data.get_aabb()
|
||||||
|
|
||||||
|
var width = terrain_data.get_resolution()
|
||||||
|
var depth = terrain_data.get_resolution()
|
||||||
|
var height = aabb.size.y
|
||||||
|
|
||||||
|
var shape_data = {
|
||||||
|
"width": terrain_data.get_resolution(),
|
||||||
|
"depth": terrain_data.get_resolution(),
|
||||||
|
"heights": terrain_data.get_all_heights(),
|
||||||
|
"min_height": aabb.position.y,
|
||||||
|
"max_height": aabb.end.y
|
||||||
|
}
|
||||||
|
|
||||||
|
PhysicsServer.shape_set_data(_shape_rid, shape_data)
|
||||||
|
|
||||||
|
_update_transform(aabb)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_transform(aabb=null):
|
||||||
|
if _terrain_data == null:
|
||||||
|
_logger.debug("HTerrainCollider: terrain data not set yet")
|
||||||
|
return
|
||||||
|
|
||||||
|
if aabb == null:
|
||||||
|
aabb = _terrain_data.get_aabb()
|
||||||
|
|
||||||
|
var width = _terrain_data.get_resolution()
|
||||||
|
var depth = _terrain_data.get_resolution()
|
||||||
|
var height = aabb.size.y
|
||||||
|
|
||||||
|
#_terrain_transform
|
||||||
|
|
||||||
|
var trans
|
||||||
|
var v = Engine.get_version_info()
|
||||||
|
if v.major == 3 and v.minor <= 1:
|
||||||
|
# Bullet centers the shape to its overall AABB so we need to move it to match the visuals
|
||||||
|
trans = Transform(Basis(), 0.5 * Vector3(width, height, depth) + Vector3(0, aabb.position.y, 0))
|
||||||
|
else:
|
||||||
|
# In 3.2, vertical centering changed.
|
||||||
|
# https://github.com/godotengine/godot/pull/28326
|
||||||
|
trans = Transform(Basis(), 0.5 * Vector3(width - 1, 0, depth - 1))
|
||||||
|
|
||||||
|
# And then apply the terrain transform
|
||||||
|
trans = _terrain_transform * trans
|
||||||
|
|
||||||
|
PhysicsServer.body_set_state(_body_rid, PhysicsServer.BODY_STATE_TRANSFORM, trans)
|
||||||
|
# Cannot use shape transform when scaling is involved,
|
||||||
|
# because Godot is undoing that scale for some reason.
|
||||||
|
# See https://github.com/Zylann/godot_heightmap_plugin/issues/70
|
||||||
|
#PhysicsServer.body_set_shape_transform(_body_rid, 0, trans)
|
1648
addons/zylann.hterrain/hterrain_data.gd
Normal file
613
addons/zylann.hterrain/hterrain_detail_layer.gd
Normal file
|
@ -0,0 +1,613 @@
|
||||||
|
tool
|
||||||
|
extends Spatial
|
||||||
|
|
||||||
|
# Child node of the terrain, used to render numerous small objects on the ground
|
||||||
|
# such as grass or rocks. They do so by using a texture covering the terrain
|
||||||
|
# (a "detail map"), which is found in the terrain data itself.
|
||||||
|
# A terrain can have multiple detail maps, and you can choose which one will be
|
||||||
|
# used with `layer_index`.
|
||||||
|
# Details use instanced rendering within their own chunk grid, scattered around
|
||||||
|
# the player. Importantly, the position and rotation of this node don't matter,
|
||||||
|
# and they also do NOT scale with map scale. Indeed, scaling the heightmap
|
||||||
|
# doesn't mean we want to scale grass blades (which is not a use case I know of).
|
||||||
|
|
||||||
|
const HTerrainData = preload("./hterrain_data.gd")
|
||||||
|
const DirectMultiMeshInstance = preload("./util/direct_multimesh_instance.gd")
|
||||||
|
const DirectMeshInstance = preload("./util/direct_mesh_instance.gd")
|
||||||
|
const Util = preload("./util/util.gd")
|
||||||
|
const Logger = preload("./util/logger.gd")
|
||||||
|
const DefaultMesh = preload("./models/grass_quad.obj")
|
||||||
|
var HTerrain = load("res://addons/zylann.hterrain/hterrain.gd")
|
||||||
|
|
||||||
|
const CHUNK_SIZE = 32
|
||||||
|
const DEFAULT_SHADER_PATH = "res://addons/zylann.hterrain/shaders/detail.shader"
|
||||||
|
const DEBUG = false
|
||||||
|
|
||||||
|
# These parameters are considered built-in,
|
||||||
|
# they are managed internally so they are not directly exposed
|
||||||
|
const _API_SHADER_PARAMS = {
|
||||||
|
"u_terrain_heightmap": true,
|
||||||
|
"u_terrain_detailmap": true,
|
||||||
|
"u_terrain_normalmap": true,
|
||||||
|
"u_terrain_globalmap": true,
|
||||||
|
"u_terrain_inverse_transform": true,
|
||||||
|
"u_terrain_normal_basis": true,
|
||||||
|
"u_albedo_alpha": true,
|
||||||
|
"u_view_distance": true,
|
||||||
|
"u_ambient_wind": true
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO Should be renamed `map_index`
|
||||||
|
# Which detail map this layer will use
|
||||||
|
export(int) var layer_index := 0 setget set_layer_index, get_layer_index
|
||||||
|
|
||||||
|
# Texture to render on the detail meshes.
|
||||||
|
export(Texture) var texture : Texture setget set_texture, get_texture
|
||||||
|
|
||||||
|
# How far detail meshes can be seen.
|
||||||
|
# TODO Improve speed of _get_chunk_aabb() so we can increase the limit
|
||||||
|
# See https://github.com/Zylann/godot_heightmap_plugin/issues/155
|
||||||
|
export(float, 1.0, 500.0) var view_distance := 100.0 setget set_view_distance, get_view_distance
|
||||||
|
|
||||||
|
# Custom shader to replace the default one.
|
||||||
|
export(Shader) var custom_shader : Shader setget set_custom_shader, get_custom_shader
|
||||||
|
|
||||||
|
# Density modifier, to make more or less detail meshes appear overall.
|
||||||
|
export(float, 0, 10) var density := 4.0 setget set_density, get_density
|
||||||
|
|
||||||
|
# Mesh used for every detail instance (for example, every grass patch).
|
||||||
|
# If not assigned, an internal quad mesh will be used.
|
||||||
|
# I would have called it `mesh` but that's too broad and conflicts with local vars ._.
|
||||||
|
export(Mesh) var instance_mesh : Mesh setget set_instance_mesh, get_instance_mesh
|
||||||
|
|
||||||
|
var _material: ShaderMaterial = null
|
||||||
|
var _default_shader: Shader = null
|
||||||
|
|
||||||
|
# Vector2 => DirectMultiMeshInstance
|
||||||
|
var _chunks := {}
|
||||||
|
|
||||||
|
var _multimesh: MultiMesh
|
||||||
|
var _multimesh_need_regen = true
|
||||||
|
var _multimesh_instance_pool := []
|
||||||
|
var _ambient_wind_time := 0.0
|
||||||
|
#var _auto_pick_index_on_enter_tree := Engine.editor_hint
|
||||||
|
var _debug_wirecube_mesh: Mesh = null
|
||||||
|
var _debug_cubes := []
|
||||||
|
var _logger := Logger.get_for(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
_default_shader = load(DEFAULT_SHADER_PATH)
|
||||||
|
_material = ShaderMaterial.new()
|
||||||
|
_material.shader = _default_shader
|
||||||
|
|
||||||
|
_multimesh = MultiMesh.new()
|
||||||
|
_multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||||
|
_multimesh.color_format = MultiMesh.COLOR_8BIT
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree():
|
||||||
|
var terrain = _get_terrain()
|
||||||
|
if terrain != null:
|
||||||
|
terrain.connect("transform_changed", self, "_on_terrain_transform_changed")
|
||||||
|
|
||||||
|
#if _auto_pick_index_on_enter_tree:
|
||||||
|
# _auto_pick_index_on_enter_tree = false
|
||||||
|
# _auto_pick_index()
|
||||||
|
|
||||||
|
terrain._internal_add_detail_layer(self)
|
||||||
|
|
||||||
|
_update_material()
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
var terrain = _get_terrain()
|
||||||
|
if terrain != null:
|
||||||
|
terrain.disconnect("transform_changed", self, "_on_terrain_transform_changed")
|
||||||
|
terrain._internal_remove_detail_layer(self)
|
||||||
|
_update_material()
|
||||||
|
for k in _chunks.keys():
|
||||||
|
_recycle_chunk(k)
|
||||||
|
_chunks.clear()
|
||||||
|
|
||||||
|
|
||||||
|
#func _auto_pick_index():
|
||||||
|
# # Automatically pick an unused layer
|
||||||
|
#
|
||||||
|
# var terrain = _get_terrain()
|
||||||
|
# if terrain == null:
|
||||||
|
# return
|
||||||
|
#
|
||||||
|
# var terrain_data = terrain.get_data()
|
||||||
|
# if terrain_data == null or terrain_data.is_locked():
|
||||||
|
# return
|
||||||
|
#
|
||||||
|
# var auto_index := layer_index
|
||||||
|
# var others = terrain.get_detail_layers()
|
||||||
|
#
|
||||||
|
# if len(others) > 0:
|
||||||
|
# var used_layers := []
|
||||||
|
# for other in others:
|
||||||
|
# used_layers.append(other.layer_index)
|
||||||
|
# used_layers.sort()
|
||||||
|
#
|
||||||
|
# auto_index = used_layers[-1]
|
||||||
|
# for i in range(1, len(used_layers)):
|
||||||
|
# if used_layers[i - 1] - used_layers[i] > 1:
|
||||||
|
# # Found a hole, take it instead
|
||||||
|
# auto_index = used_layers[i] - 1
|
||||||
|
# break
|
||||||
|
#
|
||||||
|
# print("Auto picked ", auto_index, " ")
|
||||||
|
# layer_index = auto_index
|
||||||
|
|
||||||
|
|
||||||
|
func _get_property_list() -> Array:
|
||||||
|
# Dynamic properties coming from the shader
|
||||||
|
var props := []
|
||||||
|
if _material != null:
|
||||||
|
var shader_params = VisualServer.shader_get_param_list(_material.shader.get_rid())
|
||||||
|
for p in shader_params:
|
||||||
|
if _API_SHADER_PARAMS.has(p.name):
|
||||||
|
continue
|
||||||
|
var cp = {}
|
||||||
|
for k in p:
|
||||||
|
cp[k] = p[k]
|
||||||
|
cp.name = str("shader_params/", p.name)
|
||||||
|
props.append(cp)
|
||||||
|
return props
|
||||||
|
|
||||||
|
|
||||||
|
func _get(key: String):
|
||||||
|
if key.begins_with("shader_params/"):
|
||||||
|
var param_name = key.right(len("shader_params/"))
|
||||||
|
return get_shader_param(param_name)
|
||||||
|
|
||||||
|
|
||||||
|
func _set(key: String, v):
|
||||||
|
if key.begins_with("shader_params/"):
|
||||||
|
var param_name = key.right(len("shader_params/"))
|
||||||
|
set_shader_param(param_name, v)
|
||||||
|
|
||||||
|
|
||||||
|
func get_shader_param(param_name: String):
|
||||||
|
return _material.get_shader_param(param_name)
|
||||||
|
|
||||||
|
|
||||||
|
func set_shader_param(param_name: String, v):
|
||||||
|
_material.set_shader_param(param_name, v)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_terrain():
|
||||||
|
if is_inside_tree():
|
||||||
|
return get_parent()
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func set_texture(tex: Texture):
|
||||||
|
texture = tex
|
||||||
|
_material.set_shader_param("u_albedo_alpha", tex)
|
||||||
|
|
||||||
|
|
||||||
|
func get_texture() -> Texture:
|
||||||
|
return texture
|
||||||
|
|
||||||
|
|
||||||
|
func set_layer_index(v: int):
|
||||||
|
if layer_index == v:
|
||||||
|
return
|
||||||
|
layer_index = v
|
||||||
|
if is_inside_tree():
|
||||||
|
_update_material()
|
||||||
|
Util.update_configuration_warning(self, false)
|
||||||
|
|
||||||
|
|
||||||
|
func get_layer_index() -> int:
|
||||||
|
return layer_index
|
||||||
|
|
||||||
|
|
||||||
|
func set_view_distance(v: float):
|
||||||
|
if view_distance == v:
|
||||||
|
return
|
||||||
|
view_distance = max(v, 1.0)
|
||||||
|
if is_inside_tree():
|
||||||
|
_update_material()
|
||||||
|
|
||||||
|
|
||||||
|
func get_view_distance() -> float:
|
||||||
|
return view_distance
|
||||||
|
|
||||||
|
|
||||||
|
func set_custom_shader(shader: Shader):
|
||||||
|
if custom_shader == shader:
|
||||||
|
return
|
||||||
|
custom_shader = shader
|
||||||
|
if custom_shader == null:
|
||||||
|
_material.shader = load(DEFAULT_SHADER_PATH)
|
||||||
|
else:
|
||||||
|
_material.shader = custom_shader
|
||||||
|
|
||||||
|
if Engine.editor_hint:
|
||||||
|
# Ability to fork default shader
|
||||||
|
if shader.code == "":
|
||||||
|
shader.code = _default_shader.code
|
||||||
|
|
||||||
|
|
||||||
|
func get_custom_shader() -> Shader:
|
||||||
|
return custom_shader
|
||||||
|
|
||||||
|
|
||||||
|
func set_instance_mesh(p_mesh: Mesh):
|
||||||
|
if p_mesh == instance_mesh:
|
||||||
|
return
|
||||||
|
instance_mesh = p_mesh
|
||||||
|
_multimesh.mesh = _get_used_mesh()
|
||||||
|
|
||||||
|
|
||||||
|
func get_instance_mesh() -> Mesh:
|
||||||
|
return instance_mesh
|
||||||
|
|
||||||
|
|
||||||
|
func _get_used_mesh() -> Mesh:
|
||||||
|
if instance_mesh == null:
|
||||||
|
return DefaultMesh
|
||||||
|
return instance_mesh
|
||||||
|
|
||||||
|
|
||||||
|
func set_density(v: float):
|
||||||
|
v = clamp(v, 0, 10)
|
||||||
|
if v == density:
|
||||||
|
return
|
||||||
|
density = v
|
||||||
|
_multimesh_need_regen = true
|
||||||
|
|
||||||
|
|
||||||
|
func get_density() -> float:
|
||||||
|
return density
|
||||||
|
|
||||||
|
|
||||||
|
# Updates texture references and values that come from the terrain itself.
|
||||||
|
# This is typically used when maps are being swapped around in terrain data,
|
||||||
|
# so we can restore texture references that may break.
|
||||||
|
func update_material():
|
||||||
|
_update_material()
|
||||||
|
# Formerly update_ambient_wind, reset
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int):
|
||||||
|
match what:
|
||||||
|
NOTIFICATION_ENTER_WORLD:
|
||||||
|
_set_world(get_world())
|
||||||
|
|
||||||
|
NOTIFICATION_EXIT_WORLD:
|
||||||
|
_set_world(null)
|
||||||
|
|
||||||
|
NOTIFICATION_VISIBILITY_CHANGED:
|
||||||
|
_set_visible(visible)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_visible(v: bool):
|
||||||
|
for k in _chunks:
|
||||||
|
var chunk = _chunks[k]
|
||||||
|
chunk.set_visible(v)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_world(w: World):
|
||||||
|
for k in _chunks:
|
||||||
|
var chunk = _chunks[k]
|
||||||
|
chunk.set_world(w)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_terrain_transform_changed(gt: Transform):
|
||||||
|
_update_material()
|
||||||
|
|
||||||
|
var terrain = _get_terrain()
|
||||||
|
if terrain == null:
|
||||||
|
_logger.error("Detail layer is not child of a terrain!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update AABBs, because scale might have changed
|
||||||
|
for k in _chunks:
|
||||||
|
var mmi = _chunks[k]
|
||||||
|
var aabb = _get_chunk_aabb(terrain, Vector3(k.x * CHUNK_SIZE, 0, k.y * CHUNK_SIZE))
|
||||||
|
# Nullify XZ translation because that's done by transform already
|
||||||
|
aabb.position.x = 0
|
||||||
|
aabb.position.z = 0
|
||||||
|
mmi.set_aabb(aabb)
|
||||||
|
|
||||||
|
|
||||||
|
func process(delta: float, viewer_pos: Vector3):
|
||||||
|
var terrain = _get_terrain()
|
||||||
|
if terrain == null:
|
||||||
|
_logger.error("DetailLayer processing while terrain is null!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if _multimesh_need_regen:
|
||||||
|
_regen_multimesh()
|
||||||
|
_multimesh_need_regen = false
|
||||||
|
# Crash workaround for Godot 3.1
|
||||||
|
# See https://github.com/godotengine/godot/issues/32500
|
||||||
|
for k in _chunks:
|
||||||
|
var mmi = _chunks[k]
|
||||||
|
mmi.set_multimesh(_multimesh)
|
||||||
|
|
||||||
|
var local_viewer_pos = viewer_pos - terrain.translation
|
||||||
|
|
||||||
|
var viewer_cx = local_viewer_pos.x / CHUNK_SIZE
|
||||||
|
var viewer_cz = local_viewer_pos.z / CHUNK_SIZE
|
||||||
|
|
||||||
|
var cr = int(view_distance) / CHUNK_SIZE + 1
|
||||||
|
|
||||||
|
var cmin_x = viewer_cx - cr
|
||||||
|
var cmin_z = viewer_cz - cr
|
||||||
|
var cmax_x = viewer_cx + cr
|
||||||
|
var cmax_z = viewer_cz + cr
|
||||||
|
|
||||||
|
var map_res = terrain.get_data().get_resolution()
|
||||||
|
var map_scale = terrain.map_scale
|
||||||
|
|
||||||
|
var terrain_size_x = map_res * map_scale.x
|
||||||
|
var terrain_size_z = map_res * map_scale.z
|
||||||
|
|
||||||
|
var terrain_chunks_x = terrain_size_x / CHUNK_SIZE
|
||||||
|
var terrain_chunks_z = terrain_size_z / CHUNK_SIZE
|
||||||
|
|
||||||
|
if cmin_x < 0:
|
||||||
|
cmin_x = 0
|
||||||
|
if cmin_z < 0:
|
||||||
|
cmin_z = 0
|
||||||
|
if cmax_x > terrain_chunks_x:
|
||||||
|
cmax_x = terrain_chunks_x
|
||||||
|
if cmax_z > terrain_chunks_z:
|
||||||
|
cmax_z = terrain_chunks_z
|
||||||
|
|
||||||
|
if DEBUG and visible:
|
||||||
|
_debug_cubes.clear()
|
||||||
|
for cz in range(cmin_z, cmax_z):
|
||||||
|
for cx in range(cmin_x, cmax_x):
|
||||||
|
_add_debug_cube(terrain, _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE))
|
||||||
|
|
||||||
|
for cz in range(cmin_z, cmax_z):
|
||||||
|
for cx in range(cmin_x, cmax_x):
|
||||||
|
|
||||||
|
var cpos2d = Vector2(cx, cz)
|
||||||
|
if _chunks.has(cpos2d):
|
||||||
|
continue
|
||||||
|
|
||||||
|
var aabb = _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE)
|
||||||
|
var d = (aabb.position + 0.5 * aabb.size).distance_to(local_viewer_pos)
|
||||||
|
|
||||||
|
if d < view_distance:
|
||||||
|
_load_chunk(terrain, cx, cz, aabb)
|
||||||
|
|
||||||
|
var to_recycle = []
|
||||||
|
|
||||||
|
for k in _chunks:
|
||||||
|
var chunk = _chunks[k]
|
||||||
|
var aabb = _get_chunk_aabb(terrain, Vector3(k.x, 0, k.y) * CHUNK_SIZE)
|
||||||
|
var d = (aabb.position + 0.5 * aabb.size).distance_to(local_viewer_pos)
|
||||||
|
if d > view_distance:
|
||||||
|
to_recycle.append(k)
|
||||||
|
|
||||||
|
for k in to_recycle:
|
||||||
|
_recycle_chunk(k)
|
||||||
|
|
||||||
|
# Update time manually, so we can accelerate the animation when strength is increased,
|
||||||
|
# without causing phase jumps (which would be the case if we just scaled TIME)
|
||||||
|
var ambient_wind_frequency = 1.0 + 3.0 * terrain.ambient_wind
|
||||||
|
_ambient_wind_time += delta * ambient_wind_frequency
|
||||||
|
var awp = _get_ambient_wind_params()
|
||||||
|
_material.set_shader_param("u_ambient_wind", awp)
|
||||||
|
|
||||||
|
|
||||||
|
# Gets local-space AABB of a detail chunk.
|
||||||
|
# This only apply map_scale in Y, because details are not affected by X and Z map scale.
|
||||||
|
func _get_chunk_aabb(terrain, lpos: Vector3):
|
||||||
|
var terrain_scale = terrain.map_scale
|
||||||
|
var terrain_data = terrain.get_data()
|
||||||
|
var origin_cells_x := int(lpos.x / terrain_scale.x)
|
||||||
|
var origin_cells_z := int(lpos.z / terrain_scale.z)
|
||||||
|
var size_cells_x := int(CHUNK_SIZE / terrain_scale.x)
|
||||||
|
var size_cells_z := int(CHUNK_SIZE / terrain_scale.z)
|
||||||
|
|
||||||
|
var aabb = terrain_data.get_region_aabb(
|
||||||
|
origin_cells_x, origin_cells_z, size_cells_x, size_cells_z)
|
||||||
|
|
||||||
|
aabb.position = Vector3(lpos.x, lpos.y + aabb.position.y * terrain_scale.y, lpos.z)
|
||||||
|
aabb.size = Vector3(CHUNK_SIZE, aabb.size.y * terrain_scale.y, CHUNK_SIZE)
|
||||||
|
return aabb
|
||||||
|
|
||||||
|
|
||||||
|
func _load_chunk(terrain, cx: int, cz: int, aabb: AABB):
|
||||||
|
var lpos = Vector3(cx, 0, cz) * CHUNK_SIZE
|
||||||
|
# Terrain scale is not used on purpose. Rotation is not supported.
|
||||||
|
var trans = Transform(Basis(), terrain.get_internal_transform().origin + lpos)
|
||||||
|
|
||||||
|
# Nullify XZ translation because that's done by transform already
|
||||||
|
aabb.position.x = 0
|
||||||
|
aabb.position.z = 0
|
||||||
|
|
||||||
|
var mmi = null
|
||||||
|
if len(_multimesh_instance_pool) != 0:
|
||||||
|
mmi = _multimesh_instance_pool[-1]
|
||||||
|
_multimesh_instance_pool.pop_back()
|
||||||
|
else:
|
||||||
|
mmi = DirectMultiMeshInstance.new()
|
||||||
|
mmi.set_world(terrain.get_world())
|
||||||
|
mmi.set_multimesh(_multimesh)
|
||||||
|
|
||||||
|
mmi.set_material_override(_material)
|
||||||
|
mmi.set_transform(trans)
|
||||||
|
mmi.set_aabb(aabb)
|
||||||
|
mmi.set_visible(visible)
|
||||||
|
|
||||||
|
_chunks[Vector2(cx, cz)] = mmi
|
||||||
|
|
||||||
|
|
||||||
|
func _recycle_chunk(cpos2d: Vector2):
|
||||||
|
var mmi = _chunks[cpos2d]
|
||||||
|
_chunks.erase(cpos2d)
|
||||||
|
mmi.set_visible(false)
|
||||||
|
_multimesh_instance_pool.append(mmi)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_ambient_wind_params() -> Vector2:
|
||||||
|
var aw = 0.0
|
||||||
|
var terrain = _get_terrain()
|
||||||
|
if terrain != null:
|
||||||
|
aw = terrain.ambient_wind
|
||||||
|
# amplitude, time
|
||||||
|
return Vector2(aw, _ambient_wind_time)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_material():
|
||||||
|
# Sets API shader properties. Custom properties are assumed to be set already
|
||||||
|
_logger.debug("Updating detail layer material")
|
||||||
|
|
||||||
|
var terrain_data = null
|
||||||
|
var terrain = _get_terrain()
|
||||||
|
var it = Transform()
|
||||||
|
var normal_basis = Basis()
|
||||||
|
|
||||||
|
if terrain != null:
|
||||||
|
var gt = terrain.get_internal_transform()
|
||||||
|
it = gt.affine_inverse()
|
||||||
|
terrain_data = terrain.get_data()
|
||||||
|
# This is needed to properly transform normals if the terrain is scaled
|
||||||
|
normal_basis = gt.basis.inverse().transposed()
|
||||||
|
|
||||||
|
var mat = _material
|
||||||
|
|
||||||
|
mat.set_shader_param("u_terrain_inverse_transform", it)
|
||||||
|
mat.set_shader_param("u_terrain_normal_basis", normal_basis)
|
||||||
|
mat.set_shader_param("u_albedo_alpha", texture)
|
||||||
|
mat.set_shader_param("u_view_distance", view_distance)
|
||||||
|
mat.set_shader_param("u_ambient_wind", _get_ambient_wind_params())
|
||||||
|
|
||||||
|
var heightmap_texture = null
|
||||||
|
var normalmap_texture = null
|
||||||
|
var detailmap_texture = null
|
||||||
|
var globalmap_texture = null
|
||||||
|
|
||||||
|
if terrain_data != null:
|
||||||
|
if terrain_data.is_locked():
|
||||||
|
_logger.error("Terrain data locked, can't update detail layer now")
|
||||||
|
return
|
||||||
|
|
||||||
|
heightmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_HEIGHT)
|
||||||
|
normalmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_NORMAL)
|
||||||
|
|
||||||
|
if layer_index < terrain_data.get_map_count(HTerrainData.CHANNEL_DETAIL):
|
||||||
|
detailmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_DETAIL, layer_index)
|
||||||
|
|
||||||
|
if terrain_data.get_map_count(HTerrainData.CHANNEL_GLOBAL_ALBEDO) > 0:
|
||||||
|
globalmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_GLOBAL_ALBEDO)
|
||||||
|
else:
|
||||||
|
_logger.error("Terrain data is null, can't update detail layer completely")
|
||||||
|
|
||||||
|
mat.set_shader_param("u_terrain_heightmap", heightmap_texture)
|
||||||
|
mat.set_shader_param("u_terrain_detailmap", detailmap_texture)
|
||||||
|
mat.set_shader_param("u_terrain_normalmap", normalmap_texture)
|
||||||
|
mat.set_shader_param("u_terrain_globalmap", globalmap_texture)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_debug_cube(terrain, aabb: AABB):
|
||||||
|
var world = terrain.get_world()
|
||||||
|
|
||||||
|
if _debug_wirecube_mesh == null:
|
||||||
|
_debug_wirecube_mesh = Util.create_wirecube_mesh()
|
||||||
|
var mat = SpatialMaterial.new()
|
||||||
|
mat.flags_unshaded = true
|
||||||
|
_debug_wirecube_mesh.surface_set_material(0, mat)
|
||||||
|
|
||||||
|
var debug_cube = DirectMeshInstance.new()
|
||||||
|
debug_cube.set_mesh(_debug_wirecube_mesh)
|
||||||
|
debug_cube.set_world(world)
|
||||||
|
#aabb.position.y += 0.2*randf()
|
||||||
|
debug_cube.set_transform(Transform(Basis().scaled(aabb.size), aabb.position))
|
||||||
|
|
||||||
|
_debug_cubes.append(debug_cube)
|
||||||
|
|
||||||
|
|
||||||
|
func _regen_multimesh():
|
||||||
|
# We modify the existing multimesh instead of replacing it.
|
||||||
|
# DirectMultiMeshInstance does not keep a strong reference to them,
|
||||||
|
# so replacing would break pooled instances.
|
||||||
|
_generate_multimesh(CHUNK_SIZE, density, _get_used_mesh(), _multimesh)
|
||||||
|
|
||||||
|
|
||||||
|
func is_layer_index_valid() -> bool:
|
||||||
|
var terrain = _get_terrain()
|
||||||
|
if terrain == null:
|
||||||
|
return false
|
||||||
|
var data = terrain.get_data()
|
||||||
|
if data == null:
|
||||||
|
return false
|
||||||
|
return layer_index >= 0 and layer_index < data.get_map_count(HTerrainData.CHANNEL_DETAIL)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_configuration_warning() -> String:
|
||||||
|
var terrain = _get_terrain()
|
||||||
|
if not (terrain is HTerrain):
|
||||||
|
return "This node must be child of an HTerrain node"
|
||||||
|
var data = terrain.get_data()
|
||||||
|
if data == null:
|
||||||
|
return "The terrain has no data"
|
||||||
|
if data.get_map_count(HTerrainData.CHANNEL_DETAIL) == 0:
|
||||||
|
return "The terrain does not have any detail map"
|
||||||
|
if layer_index < 0 or layer_index >= data.get_map_count(HTerrainData.CHANNEL_DETAIL):
|
||||||
|
return "Layer index is out of bounds"
|
||||||
|
var tex = data.get_texture(HTerrainData.CHANNEL_DETAIL, layer_index)
|
||||||
|
if tex == null:
|
||||||
|
return "The terrain does not have a map assigned in slot {0}".format([layer_index])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
static func _generate_multimesh(resolution: int, density: float, mesh: Mesh, multimesh: MultiMesh):
|
||||||
|
assert(multimesh != null)
|
||||||
|
|
||||||
|
var position_randomness = 0.5
|
||||||
|
var scale_randomness = 0.0
|
||||||
|
#var color_randomness = 0.5
|
||||||
|
|
||||||
|
var cell_count = resolution * resolution
|
||||||
|
var idensity = int(density)
|
||||||
|
var random_instance_count = int(cell_count * (density - floor(density)))
|
||||||
|
var total_instance_count = cell_count * idensity + random_instance_count
|
||||||
|
|
||||||
|
multimesh.instance_count = total_instance_count
|
||||||
|
multimesh.mesh = mesh
|
||||||
|
|
||||||
|
# First pass ensures uniform spread
|
||||||
|
var i = 0
|
||||||
|
for z in resolution:
|
||||||
|
for x in resolution:
|
||||||
|
for j in idensity:
|
||||||
|
|
||||||
|
var pos = Vector3(x, 0, z)
|
||||||
|
pos.x += rand_range(-position_randomness, position_randomness)
|
||||||
|
pos.z += rand_range(-position_randomness, position_randomness)
|
||||||
|
|
||||||
|
multimesh.set_instance_color(i, Color(1, 1, 1))
|
||||||
|
multimesh.set_instance_transform(i, \
|
||||||
|
Transform(_get_random_instance_basis(scale_randomness), pos))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Second pass adds the rest
|
||||||
|
for j in random_instance_count:
|
||||||
|
var pos = Vector3(rand_range(0, resolution), 0, rand_range(0, resolution))
|
||||||
|
multimesh.set_instance_color(i, Color(1, 1, 1))
|
||||||
|
multimesh.set_instance_transform(i, \
|
||||||
|
Transform(_get_random_instance_basis(scale_randomness), pos))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
|
static func _get_random_instance_basis(scale_randomness: float) -> Basis:
|
||||||
|
var sr = rand_range(0, scale_randomness)
|
||||||
|
var s = 1.0 + (sr * sr * sr * sr * sr) * 50.0
|
||||||
|
|
||||||
|
var basis = Basis()
|
||||||
|
basis = basis.scaled(Vector3(1, s, 1))
|
||||||
|
basis = basis.rotated(Vector3(0, 1, 0), rand_range(0, PI))
|
||||||
|
|
||||||
|
return basis
|
351
addons/zylann.hterrain/hterrain_mesher.gd
Normal file
|
@ -0,0 +1,351 @@
|
||||||
|
tool
|
||||||
|
|
||||||
|
const Logger = preload("./util/logger.gd")
|
||||||
|
|
||||||
|
const SEAM_LEFT = 1
|
||||||
|
const SEAM_RIGHT = 2
|
||||||
|
const SEAM_BOTTOM = 4
|
||||||
|
const SEAM_TOP = 8
|
||||||
|
const SEAM_CONFIG_COUNT = 16
|
||||||
|
|
||||||
|
|
||||||
|
# [seams_mask][lod]
|
||||||
|
var _mesh_cache := []
|
||||||
|
var _chunk_size_x := 16
|
||||||
|
var _chunk_size_y := 16
|
||||||
|
|
||||||
|
|
||||||
|
func configure(chunk_size_x: int, chunk_size_y: int, lod_count: int):
|
||||||
|
assert(typeof(chunk_size_x) == TYPE_INT)
|
||||||
|
assert(typeof(chunk_size_y) == TYPE_INT)
|
||||||
|
assert(typeof(lod_count) == TYPE_INT)
|
||||||
|
|
||||||
|
assert(chunk_size_x >= 2 or chunk_size_y >= 2)
|
||||||
|
|
||||||
|
_mesh_cache.resize(SEAM_CONFIG_COUNT)
|
||||||
|
|
||||||
|
if chunk_size_x == _chunk_size_x \
|
||||||
|
and chunk_size_y == _chunk_size_y and lod_count == len(_mesh_cache):
|
||||||
|
return
|
||||||
|
|
||||||
|
_chunk_size_x = chunk_size_x
|
||||||
|
_chunk_size_y = chunk_size_y
|
||||||
|
|
||||||
|
# TODO Will reduce the size of this cache, but need index buffer swap feature
|
||||||
|
for seams in range(SEAM_CONFIG_COUNT):
|
||||||
|
|
||||||
|
var slot = []
|
||||||
|
slot.resize(lod_count)
|
||||||
|
_mesh_cache[seams] = slot
|
||||||
|
|
||||||
|
for lod in range(lod_count):
|
||||||
|
slot[lod] = make_flat_chunk(_chunk_size_x, _chunk_size_y, 1 << lod, seams)
|
||||||
|
|
||||||
|
|
||||||
|
func get_chunk(lod: int, seams: int) -> Mesh:
|
||||||
|
return _mesh_cache[seams][lod] as Mesh
|
||||||
|
|
||||||
|
|
||||||
|
static func make_flat_chunk(quad_count_x: int, quad_count_y: int, stride: int, seams: int) -> Mesh:
|
||||||
|
|
||||||
|
var positions = PoolVector3Array()
|
||||||
|
positions.resize((quad_count_x + 1) * (quad_count_y + 1))
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
for y in range(quad_count_y + 1):
|
||||||
|
for x in range(quad_count_x + 1):
|
||||||
|
positions[i] = Vector3(x * stride, 0, y * stride)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
var indices = make_indices(quad_count_x, quad_count_y, seams)
|
||||||
|
|
||||||
|
var arrays = []
|
||||||
|
arrays.resize(Mesh.ARRAY_MAX);
|
||||||
|
arrays[Mesh.ARRAY_VERTEX] = positions
|
||||||
|
arrays[Mesh.ARRAY_INDEX] = indices
|
||||||
|
|
||||||
|
var mesh = ArrayMesh.new()
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||||
|
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
# size: chunk size in quads (there are N+1 vertices)
|
||||||
|
# seams: Bitfield for which seams are present
|
||||||
|
static func make_indices(chunk_size_x: int, chunk_size_y: int, seams: int) -> PoolIntArray:
|
||||||
|
|
||||||
|
var output_indices := PoolIntArray()
|
||||||
|
|
||||||
|
if seams != 0:
|
||||||
|
# LOD seams can't be made properly on uneven chunk sizes
|
||||||
|
assert(chunk_size_x % 2 == 0 and chunk_size_y % 2 == 0)
|
||||||
|
|
||||||
|
var reg_origin_x := 0
|
||||||
|
var reg_origin_y := 0
|
||||||
|
var reg_size_x := chunk_size_x
|
||||||
|
var reg_size_y := chunk_size_y
|
||||||
|
var reg_hstride := 1
|
||||||
|
|
||||||
|
if seams & SEAM_LEFT:
|
||||||
|
reg_origin_x += 1;
|
||||||
|
reg_size_x -= 1;
|
||||||
|
reg_hstride += 1
|
||||||
|
|
||||||
|
if seams & SEAM_BOTTOM:
|
||||||
|
reg_origin_y += 1
|
||||||
|
reg_size_y -= 1
|
||||||
|
|
||||||
|
if seams & SEAM_RIGHT:
|
||||||
|
reg_size_x -= 1
|
||||||
|
reg_hstride += 1
|
||||||
|
|
||||||
|
if seams & SEAM_TOP:
|
||||||
|
reg_size_y -= 1
|
||||||
|
|
||||||
|
# Regular triangles
|
||||||
|
var ii := reg_origin_x + reg_origin_y * (chunk_size_x + 1)
|
||||||
|
|
||||||
|
for y in range(reg_size_y):
|
||||||
|
for x in range(reg_size_x):
|
||||||
|
|
||||||
|
var i00 := ii
|
||||||
|
var i10 := ii + 1
|
||||||
|
var i01 := ii + chunk_size_x + 1
|
||||||
|
var i11 := i01 + 1
|
||||||
|
|
||||||
|
# 01---11
|
||||||
|
# | /|
|
||||||
|
# | / |
|
||||||
|
# |/ |
|
||||||
|
# 00---10
|
||||||
|
|
||||||
|
# This flips the pattern to make the geometry orientation-free.
|
||||||
|
# Not sure if it helps in any way though
|
||||||
|
var flip = ((x + reg_origin_x) + (y + reg_origin_y) % 2) % 2 != 0
|
||||||
|
|
||||||
|
if flip:
|
||||||
|
|
||||||
|
output_indices.push_back( i00 )
|
||||||
|
output_indices.push_back( i10 )
|
||||||
|
output_indices.push_back( i01 )
|
||||||
|
|
||||||
|
output_indices.push_back( i10 )
|
||||||
|
output_indices.push_back( i11 )
|
||||||
|
output_indices.push_back( i01 )
|
||||||
|
|
||||||
|
else:
|
||||||
|
output_indices.push_back( i00 )
|
||||||
|
output_indices.push_back( i11 )
|
||||||
|
output_indices.push_back( i01 )
|
||||||
|
|
||||||
|
output_indices.push_back( i00 )
|
||||||
|
output_indices.push_back( i10 )
|
||||||
|
output_indices.push_back( i11 )
|
||||||
|
|
||||||
|
ii += 1
|
||||||
|
ii += reg_hstride
|
||||||
|
|
||||||
|
# Left seam
|
||||||
|
if seams & SEAM_LEFT:
|
||||||
|
|
||||||
|
# 4 . 5
|
||||||
|
# |\ .
|
||||||
|
# | \ .
|
||||||
|
# | \.
|
||||||
|
# (2)| 3
|
||||||
|
# | /.
|
||||||
|
# | / .
|
||||||
|
# |/ .
|
||||||
|
# 0 . 1
|
||||||
|
|
||||||
|
var i := 0
|
||||||
|
var n := chunk_size_y / 2
|
||||||
|
|
||||||
|
for j in range(n):
|
||||||
|
|
||||||
|
var i0 := i
|
||||||
|
var i1 := i + 1
|
||||||
|
var i3 := i + chunk_size_x + 2
|
||||||
|
var i4 := i + 2 * (chunk_size_x + 1)
|
||||||
|
var i5 := i4 + 1
|
||||||
|
|
||||||
|
output_indices.push_back( i0 )
|
||||||
|
output_indices.push_back( i3 )
|
||||||
|
output_indices.push_back( i4 )
|
||||||
|
|
||||||
|
if j != 0 or (seams & SEAM_BOTTOM) == 0:
|
||||||
|
output_indices.push_back( i0 )
|
||||||
|
output_indices.push_back( i1 )
|
||||||
|
output_indices.push_back( i3 )
|
||||||
|
|
||||||
|
if j != n - 1 or (seams & SEAM_TOP) == 0:
|
||||||
|
output_indices.push_back( i3 )
|
||||||
|
output_indices.push_back( i5 )
|
||||||
|
output_indices.push_back( i4 )
|
||||||
|
|
||||||
|
i = i4
|
||||||
|
|
||||||
|
if seams & SEAM_RIGHT:
|
||||||
|
|
||||||
|
# 4 . 5
|
||||||
|
# . /|
|
||||||
|
# . / |
|
||||||
|
# ./ |
|
||||||
|
# 2 |(3)
|
||||||
|
# .\ |
|
||||||
|
# . \ |
|
||||||
|
# . \|
|
||||||
|
# 0 . 1
|
||||||
|
|
||||||
|
var i := chunk_size_x - 1
|
||||||
|
var n := chunk_size_y / 2
|
||||||
|
|
||||||
|
for j in range(n):
|
||||||
|
|
||||||
|
var i0 := i
|
||||||
|
var i1 := i + 1
|
||||||
|
var i2 := i + chunk_size_x + 1
|
||||||
|
var i4 := i + 2 * (chunk_size_x + 1)
|
||||||
|
var i5 := i4 + 1
|
||||||
|
|
||||||
|
output_indices.push_back( i1 )
|
||||||
|
output_indices.push_back( i5 )
|
||||||
|
output_indices.push_back( i2 )
|
||||||
|
|
||||||
|
if j != 0 or (seams & SEAM_BOTTOM) == 0:
|
||||||
|
output_indices.push_back( i0 )
|
||||||
|
output_indices.push_back( i1 )
|
||||||
|
output_indices.push_back( i2 )
|
||||||
|
|
||||||
|
if j != n - 1 or (seams & SEAM_TOP) == 0:
|
||||||
|
output_indices.push_back( i2 )
|
||||||
|
output_indices.push_back( i5 )
|
||||||
|
output_indices.push_back( i4 )
|
||||||
|
|
||||||
|
i = i4;
|
||||||
|
|
||||||
|
if seams & SEAM_BOTTOM:
|
||||||
|
|
||||||
|
# 3 . 4 . 5
|
||||||
|
# . / \ .
|
||||||
|
# . / \ .
|
||||||
|
# ./ \.
|
||||||
|
# 0-------2
|
||||||
|
# (1)
|
||||||
|
|
||||||
|
var i := 0;
|
||||||
|
var n := chunk_size_x / 2;
|
||||||
|
|
||||||
|
for j in range(n):
|
||||||
|
|
||||||
|
var i0 := i
|
||||||
|
var i2 := i + 2
|
||||||
|
var i3 := i + chunk_size_x + 1
|
||||||
|
var i4 := i3 + 1
|
||||||
|
var i5 := i4 + 1
|
||||||
|
|
||||||
|
output_indices.push_back( i0 )
|
||||||
|
output_indices.push_back( i2 )
|
||||||
|
output_indices.push_back( i4 )
|
||||||
|
|
||||||
|
if j != 0 or (seams & SEAM_LEFT) == 0:
|
||||||
|
output_indices.push_back( i0 )
|
||||||
|
output_indices.push_back( i4 )
|
||||||
|
output_indices.push_back( i3 )
|
||||||
|
|
||||||
|
if j != n - 1 or (seams & SEAM_RIGHT) == 0:
|
||||||
|
output_indices.push_back( i2 )
|
||||||
|
output_indices.push_back( i5 )
|
||||||
|
output_indices.push_back( i4 )
|
||||||
|
|
||||||
|
i = i2
|
||||||
|
|
||||||
|
if seams & SEAM_TOP:
|
||||||
|
|
||||||
|
# (4)
|
||||||
|
# 3-------5
|
||||||
|
# .\ /.
|
||||||
|
# . \ / .
|
||||||
|
# . \ / .
|
||||||
|
# 0 . 1 . 2
|
||||||
|
|
||||||
|
var i := (chunk_size_y - 1) * (chunk_size_x + 1)
|
||||||
|
var n := chunk_size_x / 2
|
||||||
|
|
||||||
|
for j in range(n):
|
||||||
|
|
||||||
|
var i0 := i
|
||||||
|
var i1 := i + 1
|
||||||
|
var i2 := i + 2
|
||||||
|
var i3 := i + chunk_size_x + 1
|
||||||
|
var i5 := i3 + 2
|
||||||
|
|
||||||
|
output_indices.push_back( i3 )
|
||||||
|
output_indices.push_back( i1 )
|
||||||
|
output_indices.push_back( i5 )
|
||||||
|
|
||||||
|
if j != 0 or (seams & SEAM_LEFT) == 0:
|
||||||
|
output_indices.push_back( i0 )
|
||||||
|
output_indices.push_back( i1 )
|
||||||
|
output_indices.push_back( i3 )
|
||||||
|
|
||||||
|
if j != n - 1 or (seams & SEAM_RIGHT) == 0:
|
||||||
|
output_indices.push_back( i1 )
|
||||||
|
output_indices.push_back( i2 )
|
||||||
|
output_indices.push_back( i5 )
|
||||||
|
|
||||||
|
i = i2
|
||||||
|
|
||||||
|
return output_indices
|
||||||
|
|
||||||
|
|
||||||
|
static func get_mesh_size(width: int, height: int) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"vertices": width * height,
|
||||||
|
"triangles": (width - 1) * (height - 1) * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Makes a full mesh from a heightmap, without any LOD considerations.
|
||||||
|
# Using this mesh for rendering is very expensive on large terrains.
|
||||||
|
# Initially used as a workaround for Godot to use for navmesh generation.
|
||||||
|
static func make_heightmap_mesh(heightmap: Image, stride: int, scale: Vector3,
|
||||||
|
logger = null) -> Mesh:
|
||||||
|
|
||||||
|
var size_x := heightmap.get_width() / stride
|
||||||
|
var size_z := heightmap.get_height() / stride
|
||||||
|
|
||||||
|
assert(size_x >= 2)
|
||||||
|
assert(size_z >= 2)
|
||||||
|
|
||||||
|
var positions := PoolVector3Array()
|
||||||
|
positions.resize(size_x * size_z)
|
||||||
|
|
||||||
|
heightmap.lock()
|
||||||
|
|
||||||
|
var i := 0
|
||||||
|
for mz in size_z:
|
||||||
|
for mx in size_x:
|
||||||
|
var x = mx * stride
|
||||||
|
var z = mz * stride
|
||||||
|
var y := heightmap.get_pixel(x, z).r
|
||||||
|
positions[i] = Vector3(x, y, z) * scale
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
heightmap.unlock()
|
||||||
|
|
||||||
|
var indices := make_indices(size_x - 1, size_z - 1, 0)
|
||||||
|
|
||||||
|
var arrays := []
|
||||||
|
arrays.resize(Mesh.ARRAY_MAX);
|
||||||
|
arrays[Mesh.ARRAY_VERTEX] = positions
|
||||||
|
arrays[Mesh.ARRAY_INDEX] = indices
|
||||||
|
|
||||||
|
if logger != null:
|
||||||
|
logger.debug(str("Generated mesh has ", len(positions),
|
||||||
|
" vertices and ", len(indices) / 3, " triangles"))
|
||||||
|
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||||
|
|
||||||
|
return mesh
|
27
addons/zylann.hterrain/hterrain_resource_loader.gd
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
tool
|
||||||
|
class_name HTerrainDataLoader
|
||||||
|
extends ResourceFormatLoader
|
||||||
|
|
||||||
|
|
||||||
|
const HTerrainData = preload("./hterrain_data.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func get_recognized_extensions():
|
||||||
|
return PoolStringArray([HTerrainData.META_EXTENSION])
|
||||||
|
|
||||||
|
|
||||||
|
func get_resource_type(path):
|
||||||
|
var ext = path.get_extension().to_lower()
|
||||||
|
if ext == HTerrainData.META_EXTENSION:
|
||||||
|
return "Resource"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func handles_type(typename):
|
||||||
|
return typename == "Resource"
|
||||||
|
|
||||||
|
|
||||||
|
func load(path, original_path):
|
||||||
|
var res = HTerrainData.new()
|
||||||
|
res.load_data(path.get_base_dir())
|
||||||
|
return res
|
20
addons/zylann.hterrain/hterrain_resource_saver.gd
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
tool
|
||||||
|
class_name HTerrainDataSaver
|
||||||
|
extends ResourceFormatSaver
|
||||||
|
|
||||||
|
|
||||||
|
const HTerrainData = preload("./hterrain_data.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func get_recognized_extensions(res):
|
||||||
|
if res != null and res is HTerrainData:
|
||||||
|
return PoolStringArray([HTerrainData.META_EXTENSION])
|
||||||
|
return PoolStringArray()
|
||||||
|
|
||||||
|
|
||||||
|
func recognize(res):
|
||||||
|
return res is HTerrainData
|
||||||
|
|
||||||
|
|
||||||
|
func save(path, resource, flags):
|
||||||
|
resource.save_data(path.get_base_dir())
|
243
addons/zylann.hterrain/hterrain_texture_set.gd
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
tool
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
const MODE_TEXTURES = 0
|
||||||
|
const MODE_TEXTURE_ARRAYS = 1
|
||||||
|
const MODE_COUNT = 2
|
||||||
|
|
||||||
|
const _mode_names = ["Textures", "TextureArrays"]
|
||||||
|
|
||||||
|
const SRC_TYPE_ALBEDO = 0
|
||||||
|
const SRC_TYPE_BUMP = 1
|
||||||
|
const SRC_TYPE_NORMAL = 2
|
||||||
|
const SRC_TYPE_ROUGHNESS = 3
|
||||||
|
const SRC_TYPE_COUNT = 4
|
||||||
|
|
||||||
|
const _src_texture_type_names = ["albedo", "bump", "normal", "roughness"]
|
||||||
|
|
||||||
|
# Ground texture types (used by the terrain system)
|
||||||
|
const TYPE_ALBEDO_BUMP = 0
|
||||||
|
const TYPE_NORMAL_ROUGHNESS = 1
|
||||||
|
const TYPE_COUNT = 2
|
||||||
|
|
||||||
|
const _texture_type_names = ["albedo_bump", "normal_roughness"]
|
||||||
|
|
||||||
|
const _type_to_src_types = [
|
||||||
|
[SRC_TYPE_ALBEDO, SRC_TYPE_BUMP],
|
||||||
|
[SRC_TYPE_NORMAL, SRC_TYPE_ROUGHNESS]
|
||||||
|
]
|
||||||
|
|
||||||
|
const _src_default_color_codes = [
|
||||||
|
"#ff000000",
|
||||||
|
"#ff888888",
|
||||||
|
"#ff8888ff",
|
||||||
|
"#ffffffff"
|
||||||
|
]
|
||||||
|
|
||||||
|
# TODO We may get rid of modes in the future, and only use TextureArrays.
|
||||||
|
# It exists for now for backward compatibility, but it makes the API a bit confusing
|
||||||
|
var _mode = MODE_TEXTURES
|
||||||
|
# [type][slot] -> StreamTexture or TextureArray
|
||||||
|
var _textures = [[], []]
|
||||||
|
|
||||||
|
|
||||||
|
static func get_texture_type_name(tt: int) -> String:
|
||||||
|
return _texture_type_names[tt]
|
||||||
|
|
||||||
|
|
||||||
|
static func get_source_texture_type_name(tt: int) -> String:
|
||||||
|
return _src_texture_type_names[tt]
|
||||||
|
|
||||||
|
|
||||||
|
static func get_source_texture_default_color_code(tt: int) -> String:
|
||||||
|
return _src_default_color_codes[tt]
|
||||||
|
|
||||||
|
|
||||||
|
static func get_import_mode_name(mode: int) -> String:
|
||||||
|
return _mode_names[mode]
|
||||||
|
|
||||||
|
|
||||||
|
static func get_src_types_from_type(t: int) -> Array:
|
||||||
|
return _type_to_src_types[t]
|
||||||
|
|
||||||
|
|
||||||
|
static func get_max_slots_for_mode(mode: int) -> int:
|
||||||
|
match mode:
|
||||||
|
MODE_TEXTURES:
|
||||||
|
# This is a legacy mode, where shaders can only have up to 4
|
||||||
|
return 4
|
||||||
|
MODE_TEXTURE_ARRAYS:
|
||||||
|
# Will probably be lifted some day
|
||||||
|
return 16
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
func _get_property_list() -> Array:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "mode",
|
||||||
|
"type": TYPE_INT,
|
||||||
|
"usage": PROPERTY_USAGE_STORAGE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"type": TYPE_ARRAY,
|
||||||
|
"usage": PROPERTY_USAGE_STORAGE
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _get(key: String):
|
||||||
|
if key == "mode":
|
||||||
|
return _mode
|
||||||
|
if key == "textures":
|
||||||
|
return _textures
|
||||||
|
|
||||||
|
|
||||||
|
func _set(key: String, value):
|
||||||
|
if key == "mode":
|
||||||
|
# Not using set_mode() here because otherwise it could reset stuff set before...
|
||||||
|
_mode = value
|
||||||
|
if key == "textures":
|
||||||
|
_textures = value
|
||||||
|
|
||||||
|
|
||||||
|
func get_slots_count() -> int:
|
||||||
|
if _mode == MODE_TEXTURES:
|
||||||
|
return get_texture_count()
|
||||||
|
# TODO What if there are two texture arrays of different size?
|
||||||
|
var texarray = _textures[TYPE_ALBEDO_BUMP][0]
|
||||||
|
if texarray == null:
|
||||||
|
var count = 0
|
||||||
|
texarray = _textures[TYPE_NORMAL_ROUGHNESS][0]
|
||||||
|
if texarray == null:
|
||||||
|
return 0
|
||||||
|
return texarray.get_depth()
|
||||||
|
|
||||||
|
|
||||||
|
func get_texture_count() -> int:
|
||||||
|
var texs = _textures[TYPE_ALBEDO_BUMP]
|
||||||
|
return len(texs)
|
||||||
|
|
||||||
|
|
||||||
|
func get_texture(slot_index: int, ground_texture_type: int) -> Texture:
|
||||||
|
if _mode != MODE_TEXTURES:
|
||||||
|
return null
|
||||||
|
var texs = _textures[ground_texture_type]
|
||||||
|
if slot_index >= len(texs):
|
||||||
|
return null
|
||||||
|
return texs[slot_index]
|
||||||
|
|
||||||
|
|
||||||
|
func set_texture(slot_index: int, ground_texture_type: int, texture: Texture):
|
||||||
|
assert(_mode == MODE_TEXTURES)
|
||||||
|
var texs = _textures[ground_texture_type]
|
||||||
|
if texs[slot_index] != texture:
|
||||||
|
texs[slot_index] = texture
|
||||||
|
emit_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func get_texture_array(ground_texture_type: int) -> TextureArray:
|
||||||
|
if _mode != MODE_TEXTURE_ARRAYS:
|
||||||
|
return null
|
||||||
|
var texs = _textures[ground_texture_type]
|
||||||
|
return texs[0]
|
||||||
|
|
||||||
|
|
||||||
|
func set_texture_array(ground_texture_type: int, texarray: TextureArray):
|
||||||
|
assert(_mode == MODE_TEXTURE_ARRAYS)
|
||||||
|
var texs = _textures[ground_texture_type]
|
||||||
|
if texs[0] != texarray:
|
||||||
|
texs[0] = texarray
|
||||||
|
emit_changed()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO This function only exists because of a flaw in UndoRedo
|
||||||
|
# See https://github.com/godotengine/godot/issues/36895
|
||||||
|
func set_texture_null(slot_index: int, ground_texture_type: int):
|
||||||
|
set_texture(slot_index, ground_texture_type, null)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO This function only exists because of a flaw in UndoRedo
|
||||||
|
# See https://github.com/godotengine/godot/issues/36895
|
||||||
|
func set_texture_array_null(ground_texture_type: int):
|
||||||
|
set_texture_array(ground_texture_type, null)
|
||||||
|
|
||||||
|
|
||||||
|
func get_mode() -> int:
|
||||||
|
return _mode
|
||||||
|
|
||||||
|
|
||||||
|
func set_mode(mode: int):
|
||||||
|
# This effectively clears slots
|
||||||
|
_mode = mode
|
||||||
|
clear()
|
||||||
|
|
||||||
|
|
||||||
|
func clear():
|
||||||
|
match _mode:
|
||||||
|
MODE_TEXTURES:
|
||||||
|
for type in TYPE_COUNT:
|
||||||
|
_textures[type] = []
|
||||||
|
MODE_TEXTURE_ARRAYS:
|
||||||
|
for type in TYPE_COUNT:
|
||||||
|
_textures[type] = [null]
|
||||||
|
emit_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func insert_slot(i: int) -> int:
|
||||||
|
assert(_mode == MODE_TEXTURES)
|
||||||
|
if i == -1:
|
||||||
|
i = get_texture_count()
|
||||||
|
for type in TYPE_COUNT:
|
||||||
|
_textures[type].insert(i, null)
|
||||||
|
emit_changed()
|
||||||
|
return i
|
||||||
|
|
||||||
|
|
||||||
|
func remove_slot(i: int):
|
||||||
|
assert(_mode == MODE_TEXTURES)
|
||||||
|
if i == -1:
|
||||||
|
i = get_slots_count() - 1
|
||||||
|
for type in TYPE_COUNT:
|
||||||
|
_textures[type].remove(i)
|
||||||
|
emit_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func has_any_textures() -> bool:
|
||||||
|
for type in len(_textures):
|
||||||
|
var texs = _textures[type]
|
||||||
|
for i in len(texs):
|
||||||
|
if texs[i] != null:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func emit_changed():
|
||||||
|
emit_signal("changed")
|
||||||
|
|
||||||
|
|
||||||
|
#func set_textures(textures: Array):
|
||||||
|
# _textures = textures
|
||||||
|
|
||||||
|
|
||||||
|
# Cannot type hint because it would cause circular dependency
|
||||||
|
#func migrate_from_1_4(terrain):
|
||||||
|
# var textures := []
|
||||||
|
# for type in TYPE_COUNT:
|
||||||
|
# textures.append([])
|
||||||
|
#
|
||||||
|
# if terrain.is_using_texture_array():
|
||||||
|
# for type in TYPE_COUNT:
|
||||||
|
# var tex : TextureArray = terrain.get_ground_texture_array(type)
|
||||||
|
# textures[type] = [tex]
|
||||||
|
# _mode = MODE_TEXTURE_ARRAYS
|
||||||
|
#
|
||||||
|
# else:
|
||||||
|
# for index in terrain.get_max_ground_texture_slot_count():
|
||||||
|
# for type in TYPE_COUNT:
|
||||||
|
# var tex : Texture = terrain.get_ground_texture(type, index)
|
||||||
|
# textures[type].append(tex)
|
||||||
|
# _mode = MODE_TEXTURES
|
||||||
|
#
|
||||||
|
# _textures = textures
|
14
addons/zylann.hterrain/models/grass_quad.obj
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# Blender v2.80 (sub 75) OBJ File: 'grass.blend'
|
||||||
|
# www.blender.org
|
||||||
|
o Cube
|
||||||
|
v 0.000000 1.000000 -0.500000
|
||||||
|
v 0.000000 0.000000 -0.500000
|
||||||
|
v 0.000000 1.000000 0.500000
|
||||||
|
v 0.000000 0.000000 0.500000
|
||||||
|
vt 0.999900 0.000100
|
||||||
|
vt 0.999900 0.999900
|
||||||
|
vt 0.000100 0.999900
|
||||||
|
vt 0.000100 0.000100
|
||||||
|
vn 1.0000 0.0000 0.0000
|
||||||
|
s off
|
||||||
|
f 2/1/1 1/2/1 3/3/1 4/4/1
|
19
addons/zylann.hterrain/models/grass_quad.obj.import
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wavefront_obj"
|
||||||
|
type="Mesh"
|
||||||
|
path="res://.import/grass_quad.obj-da067750350fe98ec466261b2aeaf486.mesh"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
files=[ "res://.import/grass_quad.obj-da067750350fe98ec466261b2aeaf486.mesh" ]
|
||||||
|
|
||||||
|
source_file="res://addons/zylann.hterrain/models/grass_quad.obj"
|
||||||
|
dest_files=[ "res://.import/grass_quad.obj-da067750350fe98ec466261b2aeaf486.mesh", "res://.import/grass_quad.obj-da067750350fe98ec466261b2aeaf486.mesh" ]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
generate_tangents=true
|
||||||
|
scale_mesh=Vector3( 1, 1, 1 )
|
||||||
|
offset_mesh=Vector3( 0, 0, 0 )
|
||||||
|
optimize_mesh=true
|
24
addons/zylann.hterrain/models/grass_quad_x2.obj
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Blender v2.80 (sub 75) OBJ File: 'grass_x2.blend'
|
||||||
|
# www.blender.org
|
||||||
|
o Cube
|
||||||
|
v 0.000000 1.000000 -0.500000
|
||||||
|
v 0.000000 0.000000 -0.500000
|
||||||
|
v 0.000000 1.000000 0.500000
|
||||||
|
v 0.000000 0.000000 0.500000
|
||||||
|
v -0.500000 1.000000 0.000000
|
||||||
|
v -0.500000 0.000000 0.000000
|
||||||
|
v 0.500000 1.000000 0.000000
|
||||||
|
v 0.500000 0.000000 0.000000
|
||||||
|
vt 0.999900 0.000100
|
||||||
|
vt 0.999900 0.999900
|
||||||
|
vt 0.000100 0.999900
|
||||||
|
vt 0.000100 0.000100
|
||||||
|
vt 0.999900 0.000100
|
||||||
|
vt 0.999900 0.999900
|
||||||
|
vt 0.000100 0.999900
|
||||||
|
vt 0.000100 0.000100
|
||||||
|
vn 1.0000 0.0000 0.0000
|
||||||
|
vn 0.0000 0.0000 -1.0000
|
||||||
|
s off
|
||||||
|
f 2/1/1 1/2/1 3/3/1 4/4/1
|
||||||
|
f 6/5/2 5/6/2 7/7/2 8/8/2
|
19
addons/zylann.hterrain/models/grass_quad_x2.obj.import
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wavefront_obj"
|
||||||
|
type="Mesh"
|
||||||
|
path="res://.import/grass_quad_x2.obj-2054c140f543f2a80e2eb921f865ea49.mesh"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
files=[ "res://.import/grass_quad_x2.obj-2054c140f543f2a80e2eb921f865ea49.mesh" ]
|
||||||
|
|
||||||
|
source_file="res://addons/zylann.hterrain/models/grass_quad_x2.obj"
|
||||||
|
dest_files=[ "res://.import/grass_quad_x2.obj-2054c140f543f2a80e2eb921f865ea49.mesh", "res://.import/grass_quad_x2.obj-2054c140f543f2a80e2eb921f865ea49.mesh" ]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
generate_tangents=true
|
||||||
|
scale_mesh=Vector3( 1, 1, 1 )
|
||||||
|
offset_mesh=Vector3( 0, 0, 0 )
|
||||||
|
optimize_mesh=true
|
34
addons/zylann.hterrain/models/grass_quad_x3.obj
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Blender v2.80 (sub 75) OBJ File: 'grass_x3.blend'
|
||||||
|
# www.blender.org
|
||||||
|
o Cube
|
||||||
|
v 0.000000 1.000000 -0.500000
|
||||||
|
v 0.000000 0.000000 -0.500000
|
||||||
|
v 0.000000 1.000000 0.500000
|
||||||
|
v 0.000000 0.000000 0.500000
|
||||||
|
v -0.433013 1.000000 -0.250000
|
||||||
|
v -0.433013 0.000000 -0.250000
|
||||||
|
v 0.433013 1.000000 0.250000
|
||||||
|
v 0.433013 0.000000 0.250000
|
||||||
|
v -0.433013 1.000000 0.250000
|
||||||
|
v -0.433013 0.000000 0.250000
|
||||||
|
v 0.433013 1.000000 -0.250000
|
||||||
|
v 0.433013 0.000000 -0.250000
|
||||||
|
vt 0.999900 0.000100
|
||||||
|
vt 0.999900 0.999900
|
||||||
|
vt 0.000100 0.999900
|
||||||
|
vt 0.000100 0.000100
|
||||||
|
vt 0.999900 0.000100
|
||||||
|
vt 0.999900 0.999900
|
||||||
|
vt 0.000100 0.999900
|
||||||
|
vt 0.000100 0.000100
|
||||||
|
vt 0.999900 0.000100
|
||||||
|
vt 0.999900 0.999900
|
||||||
|
vt 0.000100 0.999900
|
||||||
|
vt 0.000100 0.000100
|
||||||
|
vn 1.0000 0.0000 0.0000
|
||||||
|
vn 0.5000 0.0000 -0.8660
|
||||||
|
vn -0.5000 0.0000 -0.8660
|
||||||
|
s off
|
||||||
|
f 2/1/1 1/2/1 3/3/1 4/4/1
|
||||||
|
f 6/5/2 5/6/2 7/7/2 8/8/2
|
||||||
|
f 10/9/3 9/10/3 11/11/3 12/12/3
|
19
addons/zylann.hterrain/models/grass_quad_x3.obj.import
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wavefront_obj"
|
||||||
|
type="Mesh"
|
||||||
|
path="res://.import/grass_quad_x3.obj-8691724bc5006b6f65d4e8742ffc84dc.mesh"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
files=[ "res://.import/grass_quad_x3.obj-8691724bc5006b6f65d4e8742ffc84dc.mesh" ]
|
||||||
|
|
||||||
|
source_file="res://addons/zylann.hterrain/models/grass_quad_x3.obj"
|
||||||
|
dest_files=[ "res://.import/grass_quad_x3.obj-8691724bc5006b6f65d4e8742ffc84dc.mesh", "res://.import/grass_quad_x3.obj-8691724bc5006b6f65d4e8742ffc84dc.mesh" ]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
generate_tangents=true
|
||||||
|
scale_mesh=Vector3( 1, 1, 1 )
|
||||||
|
offset_mesh=Vector3( 0, 0, 0 )
|
||||||
|
optimize_mesh=true
|
42
addons/zylann.hterrain/models/grass_quad_x4.obj
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# Blender v2.80 (sub 75) OBJ File: 'grass_x4.blend'
|
||||||
|
# www.blender.org
|
||||||
|
o Cube
|
||||||
|
v 0.250000 1.000000 -0.500000
|
||||||
|
v 0.250000 0.000000 -0.500000
|
||||||
|
v 0.250000 1.000000 0.500000
|
||||||
|
v 0.250000 0.000000 0.500000
|
||||||
|
v 0.500000 0.000000 -0.250000
|
||||||
|
v 0.500000 1.000000 -0.250000
|
||||||
|
v -0.500000 0.000000 -0.250000
|
||||||
|
v -0.500000 1.000000 -0.250000
|
||||||
|
v -0.250000 0.000000 0.500000
|
||||||
|
v -0.250000 1.000000 0.500000
|
||||||
|
v -0.250000 0.000000 -0.500000
|
||||||
|
v -0.250000 1.000000 -0.500000
|
||||||
|
v 0.500000 0.000000 0.250000
|
||||||
|
v 0.500000 1.000000 0.250000
|
||||||
|
v -0.500000 0.000000 0.250000
|
||||||
|
v -0.500000 1.000000 0.250000
|
||||||
|
vt 0.999900 0.000100
|
||||||
|
vt 0.999900 0.999900
|
||||||
|
vt 0.000100 0.999900
|
||||||
|
vt 0.000100 0.000100
|
||||||
|
vt 0.999900 0.000100
|
||||||
|
vt 0.999900 0.999900
|
||||||
|
vt 0.000100 0.999900
|
||||||
|
vt 0.000100 0.000100
|
||||||
|
vt 0.999900 0.000100
|
||||||
|
vt 0.999900 0.999900
|
||||||
|
vt 0.000100 0.999900
|
||||||
|
vt 0.000100 0.000100
|
||||||
|
vt 0.999900 0.000100
|
||||||
|
vt 0.999900 0.999900
|
||||||
|
vt 0.000100 0.999900
|
||||||
|
vt 0.000100 0.000100
|
||||||
|
vn 1.0000 0.0000 0.0000
|
||||||
|
vn 0.0000 0.0000 -1.0000
|
||||||
|
s off
|
||||||
|
f 2/1/1 1/2/1 3/3/1 4/4/1
|
||||||
|
f 7/5/2 8/6/2 6/7/2 5/8/2
|
||||||
|
f 11/9/1 12/10/1 10/11/1 9/12/1
|
||||||
|
f 15/13/2 16/14/2 14/15/2 13/16/2
|
19
addons/zylann.hterrain/models/grass_quad_x4.obj.import
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wavefront_obj"
|
||||||
|
type="Mesh"
|
||||||
|
path="res://.import/grass_quad_x4.obj-c449a7d6c810ba1595ed30df9fbf3d28.mesh"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
files=[ "res://.import/grass_quad_x4.obj-c449a7d6c810ba1595ed30df9fbf3d28.mesh" ]
|
||||||
|
|
||||||
|
source_file="res://addons/zylann.hterrain/models/grass_quad_x4.obj"
|
||||||
|
dest_files=[ "res://.import/grass_quad_x4.obj-c449a7d6c810ba1595ed30df9fbf3d28.mesh", "res://.import/grass_quad_x4.obj-c449a7d6c810ba1595ed30df9fbf3d28.mesh" ]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
generate_tangents=true
|
||||||
|
scale_mesh=Vector3( 1, 1, 1 )
|
||||||
|
offset_mesh=Vector3( 0, 0, 0 )
|
||||||
|
optimize_mesh=true
|
127
addons/zylann.hterrain/native/.clang-format
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
# Commented out parameters are those with the same value as base LLVM style
|
||||||
|
# We can uncomment them if we want to change their value, or enforce the
|
||||||
|
# chosen value in case the base style changes (last sync: Clang 6.0.1).
|
||||||
|
---
|
||||||
|
### General config, applies to all languages ###
|
||||||
|
BasedOnStyle: LLVM
|
||||||
|
AccessModifierOffset: -4
|
||||||
|
AlignAfterOpenBracket: DontAlign
|
||||||
|
# AlignConsecutiveAssignments: false
|
||||||
|
# AlignConsecutiveDeclarations: false
|
||||||
|
# AlignEscapedNewlines: Right
|
||||||
|
# AlignOperands: true
|
||||||
|
AlignTrailingComments: false
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: false
|
||||||
|
# AllowShortBlocksOnASingleLine: false
|
||||||
|
AllowShortCaseLabelsOnASingleLine: true
|
||||||
|
AllowShortFunctionsOnASingleLine: Inline
|
||||||
|
AllowShortIfStatementsOnASingleLine: true
|
||||||
|
# AllowShortLoopsOnASingleLine: false
|
||||||
|
# AlwaysBreakAfterDefinitionReturnType: None
|
||||||
|
# AlwaysBreakAfterReturnType: None
|
||||||
|
# AlwaysBreakBeforeMultilineStrings: false
|
||||||
|
# AlwaysBreakTemplateDeclarations: false
|
||||||
|
# BinPackArguments: true
|
||||||
|
# BinPackParameters: true
|
||||||
|
# BraceWrapping:
|
||||||
|
# AfterClass: false
|
||||||
|
# AfterControlStatement: false
|
||||||
|
# AfterEnum: false
|
||||||
|
# AfterFunction: false
|
||||||
|
# AfterNamespace: false
|
||||||
|
# AfterObjCDeclaration: false
|
||||||
|
# AfterStruct: false
|
||||||
|
# AfterUnion: false
|
||||||
|
# AfterExternBlock: false
|
||||||
|
# BeforeCatch: false
|
||||||
|
# BeforeElse: false
|
||||||
|
# IndentBraces: false
|
||||||
|
# SplitEmptyFunction: true
|
||||||
|
# SplitEmptyRecord: true
|
||||||
|
# SplitEmptyNamespace: true
|
||||||
|
# BreakBeforeBinaryOperators: None
|
||||||
|
# BreakBeforeBraces: Attach
|
||||||
|
# BreakBeforeInheritanceComma: false
|
||||||
|
BreakBeforeTernaryOperators: false
|
||||||
|
# BreakConstructorInitializersBeforeComma: false
|
||||||
|
BreakConstructorInitializers: AfterColon
|
||||||
|
# BreakStringLiterals: true
|
||||||
|
ColumnLimit: 0
|
||||||
|
# CommentPragmas: '^ IWYU pragma:'
|
||||||
|
# CompactNamespaces: false
|
||||||
|
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||||
|
ConstructorInitializerIndentWidth: 8
|
||||||
|
ContinuationIndentWidth: 8
|
||||||
|
Cpp11BracedListStyle: false
|
||||||
|
# DerivePointerAlignment: false
|
||||||
|
# DisableFormat: false
|
||||||
|
# ExperimentalAutoDetectBinPacking: false
|
||||||
|
# FixNamespaceComments: true
|
||||||
|
# ForEachMacros:
|
||||||
|
# - foreach
|
||||||
|
# - Q_FOREACH
|
||||||
|
# - BOOST_FOREACH
|
||||||
|
# IncludeBlocks: Preserve
|
||||||
|
IncludeCategories:
|
||||||
|
- Regex: '".*"'
|
||||||
|
Priority: 1
|
||||||
|
- Regex: '^<.*\.h>'
|
||||||
|
Priority: 2
|
||||||
|
- Regex: '^<.*'
|
||||||
|
Priority: 3
|
||||||
|
# IncludeIsMainRegex: '(Test)?$'
|
||||||
|
IndentCaseLabels: true
|
||||||
|
# IndentPPDirectives: None
|
||||||
|
IndentWidth: 4
|
||||||
|
# IndentWrappedFunctionNames: false
|
||||||
|
# JavaScriptQuotes: Leave
|
||||||
|
# JavaScriptWrapImports: true
|
||||||
|
# KeepEmptyLinesAtTheStartOfBlocks: true
|
||||||
|
# MacroBlockBegin: ''
|
||||||
|
# MacroBlockEnd: ''
|
||||||
|
# MaxEmptyLinesToKeep: 1
|
||||||
|
# NamespaceIndentation: None
|
||||||
|
# PenaltyBreakAssignment: 2
|
||||||
|
# PenaltyBreakBeforeFirstCallParameter: 19
|
||||||
|
# PenaltyBreakComment: 300
|
||||||
|
# PenaltyBreakFirstLessLess: 120
|
||||||
|
# PenaltyBreakString: 1000
|
||||||
|
# PenaltyExcessCharacter: 1000000
|
||||||
|
# PenaltyReturnTypeOnItsOwnLine: 60
|
||||||
|
# PointerAlignment: Right
|
||||||
|
# RawStringFormats:
|
||||||
|
# - Delimiter: pb
|
||||||
|
# Language: TextProto
|
||||||
|
# BasedOnStyle: google
|
||||||
|
# ReflowComments: true
|
||||||
|
# SortIncludes: true
|
||||||
|
# SortUsingDeclarations: true
|
||||||
|
# SpaceAfterCStyleCast: false
|
||||||
|
# SpaceAfterTemplateKeyword: true
|
||||||
|
# SpaceBeforeAssignmentOperators: true
|
||||||
|
# SpaceBeforeParens: ControlStatements
|
||||||
|
# SpaceInEmptyParentheses: false
|
||||||
|
# SpacesBeforeTrailingComments: 1
|
||||||
|
# SpacesInAngles: false
|
||||||
|
# SpacesInContainerLiterals: true
|
||||||
|
# SpacesInCStyleCastParentheses: false
|
||||||
|
# SpacesInParentheses: false
|
||||||
|
# SpacesInSquareBrackets: false
|
||||||
|
TabWidth: 4
|
||||||
|
UseTab: Always
|
||||||
|
---
|
||||||
|
### C++ specific config ###
|
||||||
|
Language: Cpp
|
||||||
|
Standard: Cpp03
|
||||||
|
---
|
||||||
|
### ObjC specific config ###
|
||||||
|
Language: ObjC
|
||||||
|
Standard: Cpp03
|
||||||
|
ObjCBlockIndentWidth: 4
|
||||||
|
# ObjCSpaceAfterProperty: false
|
||||||
|
# ObjCSpaceBeforeProtocolList: true
|
||||||
|
---
|
||||||
|
### Java specific config ###
|
||||||
|
Language: Java
|
||||||
|
# BreakAfterJavaFieldAnnotations: false
|
||||||
|
...
|
4
addons/zylann.hterrain/native/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Build
|
||||||
|
# Ignored locally because there are other folders in which we want to version OBJ files
|
||||||
|
*.obj
|
||||||
|
|
113
addons/zylann.hterrain/native/SConstruct
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
#!python
|
||||||
|
import os
|
||||||
|
|
||||||
|
opts = Variables([], ARGUMENTS)
|
||||||
|
|
||||||
|
# Gets the standard flags CC, CCX, etc.
|
||||||
|
env = DefaultEnvironment()
|
||||||
|
|
||||||
|
# Define our options
|
||||||
|
opts.Add(EnumVariable('target', "Compilation target", 'debug', ['debug', 'release']))
|
||||||
|
opts.Add(EnumVariable('platform', "Compilation platform", '', ['', 'windows', 'linux', 'osx']))
|
||||||
|
opts.Add(BoolVariable('use_llvm', "Use the LLVM / Clang compiler", 'no'))
|
||||||
|
|
||||||
|
# Hardcoded ones
|
||||||
|
target_path = "bin/"
|
||||||
|
TARGET_NAME = "hterrain_native"
|
||||||
|
|
||||||
|
# Local dependency paths
|
||||||
|
godot_headers_path = "godot-cpp/godot_headers/"
|
||||||
|
cpp_bindings_path = "godot-cpp/"
|
||||||
|
cpp_bindings_library = "libgodot-cpp"
|
||||||
|
|
||||||
|
# only support 64 at this time
|
||||||
|
bits = 64
|
||||||
|
|
||||||
|
# Updates the environment with the option variables.
|
||||||
|
opts.Update(env)
|
||||||
|
|
||||||
|
# Process some arguments
|
||||||
|
if env['use_llvm']:
|
||||||
|
env['CC'] = 'clang'
|
||||||
|
env['CXX'] = 'clang++'
|
||||||
|
|
||||||
|
if env['platform'] == '':
|
||||||
|
print("No valid target platform selected.")
|
||||||
|
quit()
|
||||||
|
|
||||||
|
# For the reference:
|
||||||
|
# - CCFLAGS are compilation flags shared between C and C++
|
||||||
|
# - CFLAGS are for C-specific compilation flags
|
||||||
|
# - CXXFLAGS are for C++-specific compilation flags
|
||||||
|
# - CPPFLAGS are for pre-processor flags
|
||||||
|
# - CPPDEFINES are for pre-processor defines
|
||||||
|
# - LINKFLAGS are for linking flags
|
||||||
|
|
||||||
|
# Check our platform specifics
|
||||||
|
if env['platform'] == "osx":
|
||||||
|
target_path += 'osx/'
|
||||||
|
cpp_bindings_library += '.osx'
|
||||||
|
if env['target'] == 'debug':
|
||||||
|
env.Append(CCFLAGS = ['-g', '-O2', '-arch', 'x86_64'])
|
||||||
|
env.Append(LINKFLAGS = ['-arch', 'x86_64'])
|
||||||
|
else:
|
||||||
|
env.Append(CCFLAGS = ['-g', '-O3', '-arch', 'x86_64'])
|
||||||
|
env.Append(LINKFLAGS = ['-arch', 'x86_64'])
|
||||||
|
|
||||||
|
elif env['platform'] == "linux":
|
||||||
|
target_path += 'linux/'
|
||||||
|
cpp_bindings_library += '.linux'
|
||||||
|
if env['target'] == 'debug':
|
||||||
|
# -g3 means we want plenty of debug info, more than default
|
||||||
|
env.Append(CCFLAGS = ['-fPIC', '-g3', '-Og'])
|
||||||
|
env.Append(CXXFLAGS = ['-std=c++17'])
|
||||||
|
else:
|
||||||
|
env.Append(CCFLAGS = ['-fPIC', '-O3'])
|
||||||
|
env.Append(CXXFLAGS = ['-std=c++17'])
|
||||||
|
env.Append(LINKFLAGS = ['-s'])
|
||||||
|
|
||||||
|
elif env['platform'] == "windows":
|
||||||
|
target_path += 'win64/'
|
||||||
|
cpp_bindings_library += '.windows'
|
||||||
|
# This makes sure to keep the session environment variables on windows,
|
||||||
|
# that way you can run scons in a vs 2017 prompt and it will find all the required tools
|
||||||
|
env.Append(ENV = os.environ)
|
||||||
|
|
||||||
|
env.Append(CPPDEFINES = ['WIN32', '_WIN32', '_WINDOWS', '_CRT_SECURE_NO_WARNINGS'])
|
||||||
|
env.Append(CCFLAGS = ['-W3', '-GR'])
|
||||||
|
if env['target'] == 'debug':
|
||||||
|
env.Append(CPPDEFINES = ['_DEBUG'])
|
||||||
|
env.Append(CCFLAGS = ['-EHsc', '-MDd', '-ZI'])
|
||||||
|
env.Append(LINKFLAGS = ['-DEBUG'])
|
||||||
|
else:
|
||||||
|
env.Append(CPPDEFINES = ['NDEBUG'])
|
||||||
|
env.Append(CCFLAGS = ['-O2', '-EHsc', '-MD'])
|
||||||
|
|
||||||
|
if env['target'] == 'debug':
|
||||||
|
cpp_bindings_library += '.debug'
|
||||||
|
else:
|
||||||
|
cpp_bindings_library += '.release'
|
||||||
|
|
||||||
|
cpp_bindings_library += '.' + str(bits)
|
||||||
|
|
||||||
|
# make sure our binding library is properly included
|
||||||
|
env.Append(CPPPATH = [
|
||||||
|
'.',
|
||||||
|
godot_headers_path,
|
||||||
|
cpp_bindings_path + 'include/',
|
||||||
|
cpp_bindings_path + 'include/core/',
|
||||||
|
cpp_bindings_path + 'include/gen/'
|
||||||
|
])
|
||||||
|
env.Append(LIBPATH = [cpp_bindings_path + 'bin/'])
|
||||||
|
env.Append(LIBS = [cpp_bindings_library])
|
||||||
|
|
||||||
|
# Add source files of our library
|
||||||
|
env.Append(CPPPATH = ['src/'])
|
||||||
|
sources = Glob('src/*.cpp')
|
||||||
|
|
||||||
|
library = env.SharedLibrary(target = target_path + TARGET_NAME , source = sources)
|
||||||
|
|
||||||
|
Default(library)
|
||||||
|
|
||||||
|
# Generates help for the -h scons option.
|
||||||
|
Help(opts.GenerateHelpText(env))
|
BIN
addons/zylann.hterrain/native/bin/linux/libhterrain_native.so
Normal file
BIN
addons/zylann.hterrain/native/bin/win64/hterrain_native.dll
Normal file
29
addons/zylann.hterrain/native/factory.gd
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
const NATIVE_PATH = "res://addons/zylann.hterrain/native/"
|
||||||
|
|
||||||
|
const ImageUtilsGeneric = preload("./image_utils_generic.gd")
|
||||||
|
|
||||||
|
# See https://docs.godotengine.org/en/3.2/classes/class_os.html#class-os-method-get-name
|
||||||
|
const _supported_os = {
|
||||||
|
"Windows": true,
|
||||||
|
"X11": true,
|
||||||
|
#"OSX": true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func is_native_available() -> bool:
|
||||||
|
var os = OS.get_name()
|
||||||
|
if not _supported_os.has(os):
|
||||||
|
return false
|
||||||
|
# API changes can cause binary incompatibility
|
||||||
|
var v = Engine.get_version_info()
|
||||||
|
return v.major == 3 and v.minor == 2
|
||||||
|
|
||||||
|
|
||||||
|
static func get_image_utils():
|
||||||
|
if is_native_available():
|
||||||
|
var ImageUtilsNative = load(NATIVE_PATH + "image_utils.gdns")
|
||||||
|
if ImageUtilsNative != null:
|
||||||
|
return ImageUtilsNative.new()
|
||||||
|
return ImageUtilsGeneric.new()
|
||||||
|
|
17
addons/zylann.hterrain/native/hterrain.gdnlib
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[general]
|
||||||
|
|
||||||
|
singleton = false
|
||||||
|
load_once = true
|
||||||
|
symbol_prefix = "godot_"
|
||||||
|
reloadable = false
|
||||||
|
|
||||||
|
[entry]
|
||||||
|
|
||||||
|
Windows.64 = "res://addons/zylann.hterrain/native/bin/win64/hterrain_native.dll"
|
||||||
|
X11.64 = "res://addons/zylann.hterrain/native/bin/linux/libhterrain_native.so"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
X11.64 = []
|
||||||
|
Windows.64 = []
|
||||||
|
OSX.64 = []
|
8
addons/zylann.hterrain/native/image_utils.gdns
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[gd_resource type="NativeScript" load_steps=2 format=2]
|
||||||
|
|
||||||
|
[ext_resource path="res://addons/zylann.hterrain/native/hterrain.gdnlib" type="GDNativeLibrary" id=1]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
resource_name = "image_utils"
|
||||||
|
class_name = "ImageUtils"
|
||||||
|
library = ExtResource( 1 )
|
369
addons/zylann.hterrain/native/image_utils_generic.gd
Normal file
|
@ -0,0 +1,369 @@
|
||||||
|
|
||||||
|
# These functions are the same as the ones found in the GDNative library.
|
||||||
|
# They are used if the user's platform is not supported.
|
||||||
|
|
||||||
|
const Util = preload("../util/util.gd")
|
||||||
|
|
||||||
|
var _blur_buffer : Image
|
||||||
|
|
||||||
|
|
||||||
|
func get_red_range(im: Image, rect: Rect2) -> Vector2:
|
||||||
|
rect = rect.clip(Rect2(0, 0, im.get_width(), im.get_height()))
|
||||||
|
var min_x := int(rect.position.x)
|
||||||
|
var min_y := int(rect.position.y)
|
||||||
|
var max_x := min_x + int(rect.size.x)
|
||||||
|
var max_y := min_y + int(rect.size.y)
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
|
||||||
|
var min_height := im.get_pixel(min_x, min_y).r
|
||||||
|
var max_height := min_height
|
||||||
|
|
||||||
|
for y in range(min_y, max_y):
|
||||||
|
for x in range(min_x, max_x):
|
||||||
|
var h = im.get_pixel(x, y).r
|
||||||
|
if h < min_height:
|
||||||
|
min_height = h
|
||||||
|
elif h > max_height:
|
||||||
|
max_height = h
|
||||||
|
|
||||||
|
im.unlock()
|
||||||
|
|
||||||
|
return Vector2(min_height, max_height)
|
||||||
|
|
||||||
|
|
||||||
|
func get_red_sum(im: Image, rect: Rect2) -> float:
|
||||||
|
rect = rect.clip(Rect2(0, 0, im.get_width(), im.get_height()))
|
||||||
|
var min_x := int(rect.position.x)
|
||||||
|
var min_y := int(rect.position.y)
|
||||||
|
var max_x := min_x + int(rect.size.x)
|
||||||
|
var max_y := min_y + int(rect.size.y)
|
||||||
|
|
||||||
|
var sum := 0.0
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
|
||||||
|
for y in range(min_y, max_y):
|
||||||
|
for x in range(min_x, max_x):
|
||||||
|
sum += im.get_pixel(x, y).r
|
||||||
|
|
||||||
|
im.unlock()
|
||||||
|
|
||||||
|
return sum
|
||||||
|
|
||||||
|
|
||||||
|
func get_red_sum_weighted(im: Image, brush: Image, pos: Vector2,
|
||||||
|
var factor: float) -> float:
|
||||||
|
|
||||||
|
var min_x = int(pos.x)
|
||||||
|
var min_y = int(pos.y)
|
||||||
|
var max_x = min_x + brush.get_width()
|
||||||
|
var max_y = min_y + brush.get_height()
|
||||||
|
var min_noclamp_x = min_x
|
||||||
|
var min_noclamp_y = min_y
|
||||||
|
|
||||||
|
min_x = Util.clamp_int(min_x, 0, im.get_width())
|
||||||
|
min_y = Util.clamp_int(min_y, 0, im.get_height())
|
||||||
|
max_x = Util.clamp_int(max_x, 0, im.get_width())
|
||||||
|
max_y = Util.clamp_int(max_y, 0, im.get_height())
|
||||||
|
|
||||||
|
var sum = 0.0
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
brush.lock()
|
||||||
|
|
||||||
|
for y in range(min_y, max_y):
|
||||||
|
var by = y - min_noclamp_y
|
||||||
|
|
||||||
|
for x in range(min_x, max_x):
|
||||||
|
var bx = x - min_noclamp_x
|
||||||
|
|
||||||
|
var shape_value = brush.get_pixel(bx, by).r
|
||||||
|
sum += im.get_pixel(x, y).r * shape_value * factor
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
brush.unlock()
|
||||||
|
|
||||||
|
return sum
|
||||||
|
|
||||||
|
|
||||||
|
func add_red_brush(im: Image, brush: Image, pos: Vector2, var factor: float):
|
||||||
|
var min_x = int(pos.x)
|
||||||
|
var min_y = int(pos.y)
|
||||||
|
var max_x = min_x + brush.get_width()
|
||||||
|
var max_y = min_y + brush.get_height()
|
||||||
|
var min_noclamp_x = min_x
|
||||||
|
var min_noclamp_y = min_y
|
||||||
|
|
||||||
|
min_x = Util.clamp_int(min_x, 0, im.get_width())
|
||||||
|
min_y = Util.clamp_int(min_y, 0, im.get_height())
|
||||||
|
max_x = Util.clamp_int(max_x, 0, im.get_width())
|
||||||
|
max_y = Util.clamp_int(max_y, 0, im.get_height())
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
brush.lock()
|
||||||
|
|
||||||
|
for y in range(min_y, max_y):
|
||||||
|
var by = y - min_noclamp_y
|
||||||
|
|
||||||
|
for x in range(min_x, max_x):
|
||||||
|
var bx = x - min_noclamp_x
|
||||||
|
|
||||||
|
var shape_value = brush.get_pixel(bx, by).r
|
||||||
|
var r = im.get_pixel(x, y).r + shape_value * factor
|
||||||
|
im.set_pixel(x, y, Color(r, r, r))
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
brush.unlock()
|
||||||
|
|
||||||
|
|
||||||
|
func lerp_channel_brush(im: Image, brush: Image, pos: Vector2,
|
||||||
|
factor: float, target_value: float, channel: int):
|
||||||
|
|
||||||
|
var min_x = int(pos.x)
|
||||||
|
var min_y = int(pos.y)
|
||||||
|
var max_x = min_x + brush.get_width()
|
||||||
|
var max_y = min_y + brush.get_height()
|
||||||
|
var min_noclamp_x = min_x
|
||||||
|
var min_noclamp_y = min_y
|
||||||
|
|
||||||
|
min_x = Util.clamp_int(min_x, 0, im.get_width())
|
||||||
|
min_y = Util.clamp_int(min_y, 0, im.get_height())
|
||||||
|
max_x = Util.clamp_int(max_x, 0, im.get_width())
|
||||||
|
max_y = Util.clamp_int(max_y, 0, im.get_height())
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
brush.lock()
|
||||||
|
|
||||||
|
for y in range(min_y, max_y):
|
||||||
|
var by = y - min_noclamp_y
|
||||||
|
|
||||||
|
for x in range(min_x, max_x):
|
||||||
|
var bx = x - min_noclamp_x
|
||||||
|
|
||||||
|
var shape_value = brush.get_pixel(bx, by).r
|
||||||
|
var c = im.get_pixel(x, y)
|
||||||
|
c[channel] = lerp(c[channel], target_value, shape_value * factor)
|
||||||
|
im.set_pixel(x, y, c)
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
brush.unlock()
|
||||||
|
|
||||||
|
|
||||||
|
func lerp_color_brush(im: Image, brush: Image, pos: Vector2,
|
||||||
|
factor: float, target_value: Color):
|
||||||
|
|
||||||
|
var min_x = int(pos.x)
|
||||||
|
var min_y = int(pos.y)
|
||||||
|
var max_x = min_x + brush.get_width()
|
||||||
|
var max_y = min_y + brush.get_height()
|
||||||
|
var min_noclamp_x = min_x
|
||||||
|
var min_noclamp_y = min_y
|
||||||
|
|
||||||
|
min_x = Util.clamp_int(min_x, 0, im.get_width())
|
||||||
|
min_y = Util.clamp_int(min_y, 0, im.get_height())
|
||||||
|
max_x = Util.clamp_int(max_x, 0, im.get_width())
|
||||||
|
max_y = Util.clamp_int(max_y, 0, im.get_height())
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
brush.lock()
|
||||||
|
|
||||||
|
for y in range(min_y, max_y):
|
||||||
|
var by = y - min_noclamp_y
|
||||||
|
|
||||||
|
for x in range(min_x, max_x):
|
||||||
|
var bx = x - min_noclamp_x
|
||||||
|
|
||||||
|
var shape_value = brush.get_pixel(bx, by).r
|
||||||
|
var c = im.get_pixel(x, y).linear_interpolate(target_value, factor * shape_value)
|
||||||
|
im.set_pixel(x, y, c)
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
brush.unlock()
|
||||||
|
|
||||||
|
|
||||||
|
func generate_gaussian_brush(im: Image) -> float:
|
||||||
|
var sum := 0.0
|
||||||
|
var center := Vector2(im.get_width() / 2, im.get_height() / 2)
|
||||||
|
var radius := min(im.get_width(), im.get_height()) / 2.0
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
|
||||||
|
for y in im.get_height():
|
||||||
|
for x in im.get_width():
|
||||||
|
var d := Vector2(x, y).distance_to(center) / radius
|
||||||
|
var v := clamp(1.0 - d * d * d, 0.0, 1.0)
|
||||||
|
im.set_pixel(x, y, Color(v, v, v))
|
||||||
|
sum += v;
|
||||||
|
|
||||||
|
im.unlock()
|
||||||
|
return sum
|
||||||
|
|
||||||
|
|
||||||
|
func blur_red_brush(im: Image, brush: Image, pos: Vector2, factor: float):
|
||||||
|
factor = clamp(factor, 0.0, 1.0)
|
||||||
|
|
||||||
|
if _blur_buffer == null:
|
||||||
|
_blur_buffer = Image.new()
|
||||||
|
var buffer := _blur_buffer
|
||||||
|
|
||||||
|
var buffer_width := brush.get_width() + 2
|
||||||
|
var buffer_height := brush.get_height() + 2
|
||||||
|
|
||||||
|
if buffer_width != buffer.get_width() or buffer_height != buffer.get_height():
|
||||||
|
buffer.create(buffer_width, buffer_height, false, Image.FORMAT_RF)
|
||||||
|
|
||||||
|
im.lock()
|
||||||
|
buffer.lock()
|
||||||
|
|
||||||
|
var min_x := int(pos.x) - 1
|
||||||
|
var min_y := int(pos.y) - 1
|
||||||
|
var max_x := min_x + buffer.get_width()
|
||||||
|
var max_y := min_y + buffer.get_height()
|
||||||
|
|
||||||
|
var im_clamp_w = im.get_width() - 1
|
||||||
|
var im_clamp_h = im.get_height() - 1
|
||||||
|
|
||||||
|
# Copy pixels to temporary buffer
|
||||||
|
for y in range(min_y, max_y):
|
||||||
|
for x in range(min_x, max_x):
|
||||||
|
var ix := clamp(x, 0, im_clamp_w)
|
||||||
|
var iy := clamp(y, 0, im_clamp_h)
|
||||||
|
var c = im.get_pixel(ix, iy)
|
||||||
|
buffer.set_pixel(x - min_x, y - min_y, c)
|
||||||
|
|
||||||
|
min_x = int(pos.x)
|
||||||
|
min_y = int(pos.y)
|
||||||
|
max_x = min_x + brush.get_width()
|
||||||
|
max_y = min_y + brush.get_height()
|
||||||
|
var min_noclamp_x := min_x
|
||||||
|
var min_noclamp_y := min_y
|
||||||
|
|
||||||
|
min_x = Util.clamp_int(min_x, 0, im.get_width())
|
||||||
|
min_y = Util.clamp_int(min_y, 0, im.get_height())
|
||||||
|
max_x = Util.clamp_int(max_x, 0, im.get_width())
|
||||||
|
max_y = Util.clamp_int(max_y, 0, im.get_height())
|
||||||
|
|
||||||
|
brush.lock()
|
||||||
|
|
||||||
|
# Apply blur
|
||||||
|
for y in range(min_y, max_y):
|
||||||
|
var by := y - min_noclamp_y
|
||||||
|
|
||||||
|
for x in range(min_x, max_x):
|
||||||
|
var bx := x - min_noclamp_x
|
||||||
|
|
||||||
|
var shape_value := brush.get_pixel(bx, by).r * factor
|
||||||
|
|
||||||
|
var p10 = buffer.get_pixel(bx + 1, by ).r
|
||||||
|
var p01 = buffer.get_pixel(bx, by + 1).r
|
||||||
|
var p11 = buffer.get_pixel(bx + 1, by + 1).r
|
||||||
|
var p21 = buffer.get_pixel(bx + 2, by + 1).r
|
||||||
|
var p12 = buffer.get_pixel(bx + 1, by + 2).r
|
||||||
|
|
||||||
|
var m = (p10 + p01 + p11 + p21 + p12) * 0.2
|
||||||
|
var p = lerp(p11, m, shape_value * factor)
|
||||||
|
|
||||||
|
im.set_pixel(x, y, Color(p, p, p))
|
||||||
|
|
||||||
|
im.unlock()
|
||||||
|
buffer.unlock()
|
||||||
|
brush.unlock()
|
||||||
|
|
||||||
|
|
||||||
|
func paint_indexed_splat(index_map: Image, weight_map: Image, brush: Image, pos: Vector2, \
|
||||||
|
texture_index: int, factor: float):
|
||||||
|
|
||||||
|
var min_x := pos.x
|
||||||
|
var min_y := pos.y
|
||||||
|
var max_x := min_x + brush.get_width()
|
||||||
|
var max_y := min_y + brush.get_height()
|
||||||
|
var min_noclamp_x := min_x
|
||||||
|
var min_noclamp_y := min_y
|
||||||
|
|
||||||
|
min_x = Util.clamp_int(min_x, 0, index_map.get_width())
|
||||||
|
min_y = Util.clamp_int(min_y, 0, index_map.get_height())
|
||||||
|
max_x = Util.clamp_int(max_x, 0, index_map.get_width())
|
||||||
|
max_y = Util.clamp_int(max_y, 0, index_map.get_height())
|
||||||
|
|
||||||
|
var texture_index_f := float(texture_index) / 255.0
|
||||||
|
var all_texture_index_f := Color(texture_index_f, texture_index_f, texture_index_f)
|
||||||
|
var ci := texture_index % 3
|
||||||
|
var cm := Color(-1, -1, -1)
|
||||||
|
cm[ci] = 1
|
||||||
|
|
||||||
|
index_map.lock()
|
||||||
|
weight_map.lock()
|
||||||
|
brush.lock()
|
||||||
|
|
||||||
|
for y in range(min_y, max_y):
|
||||||
|
var by := y - min_noclamp_y
|
||||||
|
|
||||||
|
for x in range(min_x, max_x):
|
||||||
|
var bx := x - min_noclamp_x
|
||||||
|
|
||||||
|
var shape_value := brush.get_pixel(bx, by).r * factor
|
||||||
|
if shape_value == 0.0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var i := index_map.get_pixel(x, y)
|
||||||
|
var w := weight_map.get_pixel(x, y)
|
||||||
|
|
||||||
|
# Decompress third weight to make computations easier
|
||||||
|
w[2] = 1.0 - w[0] - w[1]
|
||||||
|
|
||||||
|
# The index map tells which textures to blend.
|
||||||
|
# The weight map tells their blending amounts.
|
||||||
|
# This brings the limitation that up to 3 textures can blend at a time in a given pixel.
|
||||||
|
# Painting this in real time can be a challenge.
|
||||||
|
|
||||||
|
# The approach here is a compromise for simplicity.
|
||||||
|
# Each texture is associated a fixed component of the index map (R, G or B),
|
||||||
|
# so two neighbor pixels having the same component won't be guaranteed to blend.
|
||||||
|
# In other words, texture T will not be able to blend with T + N * k,
|
||||||
|
# where k is an integer, and N is the number of components in the index map (up to 4).
|
||||||
|
# It might still be able to blend due to a special case when an area is uniform,
|
||||||
|
# but not otherwise.
|
||||||
|
|
||||||
|
# Dynamic component assignment sounds like the alternative, however I wasn't able
|
||||||
|
# to find a painting algorithm that wasn't confusing, at least the current one is
|
||||||
|
# predictable.
|
||||||
|
|
||||||
|
# Need to use approximation because Color is float but GDScript uses doubles...
|
||||||
|
if abs(i[ci] - texture_index_f) > 0.001:
|
||||||
|
# Pixel does not have our texture index,
|
||||||
|
# transfer its weight to other components first
|
||||||
|
if w[ci] > shape_value:
|
||||||
|
w -= cm * shape_value
|
||||||
|
|
||||||
|
elif w[ci] >= 0.0:
|
||||||
|
w[ci] = 0.0
|
||||||
|
i[ci] = texture_index_f
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Pixel has our texture index, increase its weight
|
||||||
|
if w[ci] + shape_value < 1.0:
|
||||||
|
w += cm * shape_value
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Pixel weight is full, we can set all components to the same index.
|
||||||
|
# Need to nullify other weights because they would otherwise never reach
|
||||||
|
# zero due to normalization
|
||||||
|
w = Color(0, 0, 0)
|
||||||
|
w[ci] = 1.0
|
||||||
|
i = all_texture_index_f
|
||||||
|
|
||||||
|
# No `saturate` function in Color??
|
||||||
|
w[0] = clamp(w[0], 0.0, 1.0)
|
||||||
|
w[1] = clamp(w[1], 0.0, 1.0)
|
||||||
|
w[2] = clamp(w[2], 0.0, 1.0)
|
||||||
|
|
||||||
|
# Renormalize
|
||||||
|
w /= w[0] + w[1] + w[2]
|
||||||
|
|
||||||
|
index_map.set_pixel(x, y, i)
|
||||||
|
weight_map.set_pixel(x, y, w)
|
||||||
|
|
||||||
|
index_map.lock()
|
||||||
|
weight_map.lock()
|
||||||
|
brush.unlock()
|
0
addons/zylann.hterrain/native/src/.gdignore
Normal file
28
addons/zylann.hterrain/native/src/gd_library.cpp
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#include "image_utils.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *o) {
|
||||||
|
#ifdef _DEBUG
|
||||||
|
printf("godot_gdnative_init hterrain_native\n");
|
||||||
|
#endif
|
||||||
|
godot::Godot::gdnative_init(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *o) {
|
||||||
|
#ifdef _DEBUG
|
||||||
|
printf("godot_gdnative_terminate hterrain_native\n");
|
||||||
|
#endif
|
||||||
|
godot::Godot::gdnative_terminate(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GDN_EXPORT godot_nativescript_init(void *handle) {
|
||||||
|
#ifdef _DEBUG
|
||||||
|
printf("godot_nativescript_init hterrain_native\n");
|
||||||
|
#endif
|
||||||
|
godot::Godot::nativescript_init(handle);
|
||||||
|
|
||||||
|
godot::register_tool_class<godot::ImageUtils>();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
364
addons/zylann.hterrain/native/src/image_utils.cpp
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
#include "image_utils.h"
|
||||||
|
#include "int_range_2d.h"
|
||||||
|
#include "math_funcs.h"
|
||||||
|
|
||||||
|
namespace godot {
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
inline void generic_brush_op(Image &image, Image &brush, Vector2 p_pos, float factor, F op) {
|
||||||
|
IntRange2D range = IntRange2D::from_min_max(p_pos, brush.get_size());
|
||||||
|
int min_x_noclamp = range.min_x;
|
||||||
|
int min_y_noclamp = range.min_y;
|
||||||
|
range.clip(Vector2i(image.get_size()));
|
||||||
|
|
||||||
|
image.lock();
|
||||||
|
brush.lock();
|
||||||
|
|
||||||
|
for (int y = range.min_y; y < range.max_y; ++y) {
|
||||||
|
int by = y - min_y_noclamp;
|
||||||
|
|
||||||
|
for (int x = range.min_x; x < range.max_x; ++x) {
|
||||||
|
int bx = x - min_x_noclamp;
|
||||||
|
|
||||||
|
float b = brush.get_pixel(bx, by).r * factor;
|
||||||
|
op(image, x, y, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
image.unlock();
|
||||||
|
brush.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageUtils::ImageUtils() {
|
||||||
|
#ifdef _DEBUG
|
||||||
|
Godot::print("Constructing ImageUtils");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageUtils::~ImageUtils() {
|
||||||
|
#ifdef _DEBUG
|
||||||
|
// TODO Cannot print shit here, see https://github.com/godotengine/godot/issues/37417
|
||||||
|
// Means only the console will print this
|
||||||
|
//Godot::print("Destructing ImageUtils");
|
||||||
|
printf("Destructing ImageUtils\n");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageUtils::_init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 ImageUtils::get_red_range(Ref<Image> image_ref, Rect2 rect) const {
|
||||||
|
ERR_FAIL_COND_V(image_ref.is_null(), Vector2());
|
||||||
|
Image &image = **image_ref;
|
||||||
|
|
||||||
|
IntRange2D range(rect);
|
||||||
|
range.clip(Vector2i(image.get_size()));
|
||||||
|
|
||||||
|
image.lock();
|
||||||
|
|
||||||
|
float min_value = image.get_pixel(range.min_x, range.min_y).r;
|
||||||
|
float max_value = min_value;
|
||||||
|
|
||||||
|
for (int y = range.min_y; y < range.max_y; ++y) {
|
||||||
|
for (int x = range.min_x; x < range.max_x; ++x) {
|
||||||
|
float v = image.get_pixel(x, y).r;
|
||||||
|
|
||||||
|
if (v > max_value) {
|
||||||
|
max_value = v;
|
||||||
|
} else if (v < min_value) {
|
||||||
|
min_value = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
image.unlock();
|
||||||
|
|
||||||
|
return Vector2(min_value, max_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
float ImageUtils::get_red_sum(Ref<Image> image_ref, Rect2 rect) const {
|
||||||
|
ERR_FAIL_COND_V(image_ref.is_null(), 0.f);
|
||||||
|
Image &image = **image_ref;
|
||||||
|
|
||||||
|
IntRange2D range(rect);
|
||||||
|
range.clip(Vector2i(image.get_size()));
|
||||||
|
|
||||||
|
image.lock();
|
||||||
|
|
||||||
|
float sum = 0.f;
|
||||||
|
|
||||||
|
for (int y = range.min_y; y < range.max_y; ++y) {
|
||||||
|
for (int x = range.min_x; x < range.max_x; ++x) {
|
||||||
|
sum += image.get_pixel(x, y).r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
image.unlock();
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ImageUtils::get_red_sum_weighted(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor) const {
|
||||||
|
ERR_FAIL_COND_V(image_ref.is_null(), 0.f);
|
||||||
|
ERR_FAIL_COND_V(brush_ref.is_null(), 0.f);
|
||||||
|
Image &image = **image_ref;
|
||||||
|
Image &brush = **brush_ref;
|
||||||
|
|
||||||
|
float sum = 0.f;
|
||||||
|
generic_brush_op(image, brush, p_pos, factor, [&sum](Image &image, int x, int y, float b) {
|
||||||
|
sum += image.get_pixel(x, y).r * b;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageUtils::add_red_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor) const {
|
||||||
|
ERR_FAIL_COND(image_ref.is_null());
|
||||||
|
ERR_FAIL_COND(brush_ref.is_null());
|
||||||
|
Image &image = **image_ref;
|
||||||
|
Image &brush = **brush_ref;
|
||||||
|
|
||||||
|
generic_brush_op(image, brush, p_pos, factor, [](Image &image, int x, int y, float b) {
|
||||||
|
float r = image.get_pixel(x, y).r + b;
|
||||||
|
image.set_pixel(x, y, Color(r, r, r));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageUtils::lerp_channel_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor, float target_value, int channel) const {
|
||||||
|
ERR_FAIL_COND(image_ref.is_null());
|
||||||
|
ERR_FAIL_COND(brush_ref.is_null());
|
||||||
|
Image &image = **image_ref;
|
||||||
|
Image &brush = **brush_ref;
|
||||||
|
|
||||||
|
generic_brush_op(image, brush, p_pos, factor, [target_value, channel](Image &image, int x, int y, float b) {
|
||||||
|
Color c = image.get_pixel(x, y);
|
||||||
|
c[channel] = Math::lerp(c[channel], target_value, b);
|
||||||
|
image.set_pixel(x, y, c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageUtils::lerp_color_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor, Color target_value) const {
|
||||||
|
ERR_FAIL_COND(image_ref.is_null());
|
||||||
|
ERR_FAIL_COND(brush_ref.is_null());
|
||||||
|
Image &image = **image_ref;
|
||||||
|
Image &brush = **brush_ref;
|
||||||
|
|
||||||
|
generic_brush_op(image, brush, p_pos, factor, [target_value](Image &image, int x, int y, float b) {
|
||||||
|
const Color c = image.get_pixel(x, y).linear_interpolate(target_value, b);
|
||||||
|
image.set_pixel(x, y, c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Smooth (each pixel being box-filtered, contrary to the existing smooth)
|
||||||
|
|
||||||
|
float ImageUtils::generate_gaussian_brush(Ref<Image> image_ref) const {
|
||||||
|
ERR_FAIL_COND_V(image_ref.is_null(), 0.f);
|
||||||
|
Image &image = **image_ref;
|
||||||
|
|
||||||
|
int w = static_cast<int>(image.get_width());
|
||||||
|
int h = static_cast<int>(image.get_height());
|
||||||
|
Vector2 center(w / 2, h / 2);
|
||||||
|
float radius = Math::min(w, h) / 2;
|
||||||
|
|
||||||
|
ERR_FAIL_COND_V(radius <= 0.1f, 0.f);
|
||||||
|
|
||||||
|
float sum = 0.f;
|
||||||
|
image.lock();
|
||||||
|
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
float d = Vector2(x, y).distance_to(center) / radius;
|
||||||
|
float v = Math::clamp(1.f - d * d * d, 0.f, 1.f);
|
||||||
|
image.set_pixel(x, y, Color(v, v, v));
|
||||||
|
sum += v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
image.unlock();
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageUtils::blur_red_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor) {
|
||||||
|
ERR_FAIL_COND(image_ref.is_null());
|
||||||
|
ERR_FAIL_COND(brush_ref.is_null());
|
||||||
|
Image &image = **image_ref;
|
||||||
|
Image &brush = **brush_ref;
|
||||||
|
|
||||||
|
factor = Math::clamp(factor, 0.f, 1.f);
|
||||||
|
|
||||||
|
// Relative to the image
|
||||||
|
IntRange2D buffer_range = IntRange2D::from_pos_size(p_pos, brush.get_size());
|
||||||
|
buffer_range.pad(1);
|
||||||
|
|
||||||
|
const int image_width = static_cast<int>(image.get_width());
|
||||||
|
const int image_height = static_cast<int>(image.get_height());
|
||||||
|
|
||||||
|
const int buffer_width = static_cast<int>(buffer_range.get_width());
|
||||||
|
const int buffer_height = static_cast<int>(buffer_range.get_height());
|
||||||
|
_blur_buffer.resize(buffer_width * buffer_height);
|
||||||
|
|
||||||
|
image.lock();
|
||||||
|
|
||||||
|
// Cache pixels, because they will be queried more than once and written to later
|
||||||
|
int buffer_i = 0;
|
||||||
|
for (int y = buffer_range.min_y; y < buffer_range.max_y; ++y) {
|
||||||
|
for (int x = buffer_range.min_x; x < buffer_range.max_x; ++x) {
|
||||||
|
const int ix = Math::clamp(x, 0, image_width - 1);
|
||||||
|
const int iy = Math::clamp(y, 0, image_height - 1);
|
||||||
|
_blur_buffer[buffer_i] = image.get_pixel(ix, iy).r;
|
||||||
|
++buffer_i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IntRange2D range = IntRange2D::from_min_max(p_pos, brush.get_size());
|
||||||
|
const int min_x_noclamp = range.min_x;
|
||||||
|
const int min_y_noclamp = range.min_y;
|
||||||
|
range.clip(Vector2i(image.get_size()));
|
||||||
|
|
||||||
|
const int buffer_offset_left = -1;
|
||||||
|
const int buffer_offset_right = 1;
|
||||||
|
const int buffer_offset_top = -buffer_width;
|
||||||
|
const int buffer_offset_bottom = buffer_width;
|
||||||
|
|
||||||
|
brush.lock();
|
||||||
|
|
||||||
|
// Apply blur
|
||||||
|
for (int y = range.min_y; y < range.max_y; ++y) {
|
||||||
|
const int brush_y = y - min_y_noclamp;
|
||||||
|
|
||||||
|
for (int x = range.min_x; x < range.max_x; ++x) {
|
||||||
|
const int brush_x = x - min_x_noclamp;
|
||||||
|
|
||||||
|
const float brush_value = brush.get_pixel(brush_x, brush_y).r * factor;
|
||||||
|
|
||||||
|
buffer_i = (brush_x + 1) + (brush_y + 1) * buffer_width;
|
||||||
|
|
||||||
|
const float p10 = _blur_buffer[buffer_i + buffer_offset_top];
|
||||||
|
const float p01 = _blur_buffer[buffer_i + buffer_offset_left];
|
||||||
|
const float p11 = _blur_buffer[buffer_i];
|
||||||
|
const float p21 = _blur_buffer[buffer_i + buffer_offset_right];
|
||||||
|
const float p12 = _blur_buffer[buffer_i + buffer_offset_bottom];
|
||||||
|
|
||||||
|
// Average
|
||||||
|
float m = (p10 + p01 + p11 + p21 + p12) * 0.2f;
|
||||||
|
float p = Math::lerp(p11, m, brush_value);
|
||||||
|
|
||||||
|
image.set_pixel(x, y, Color(p, p, p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
image.unlock();
|
||||||
|
brush.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageUtils::paint_indexed_splat(Ref<Image> index_map_ref, Ref<Image> weight_map_ref,
|
||||||
|
Ref<Image> brush_ref, Vector2 p_pos, int texture_index, float factor) {
|
||||||
|
|
||||||
|
ERR_FAIL_COND(index_map_ref.is_null());
|
||||||
|
ERR_FAIL_COND(weight_map_ref.is_null());
|
||||||
|
ERR_FAIL_COND(brush_ref.is_null());
|
||||||
|
Image &index_map = **index_map_ref;
|
||||||
|
Image &weight_map = **weight_map_ref;
|
||||||
|
Image &brush = **brush_ref;
|
||||||
|
|
||||||
|
ERR_FAIL_COND(index_map.get_size() != weight_map.get_size());
|
||||||
|
|
||||||
|
factor = Math::clamp(factor, 0.f, 1.f);
|
||||||
|
|
||||||
|
IntRange2D range = IntRange2D::from_min_max(p_pos, brush.get_size());
|
||||||
|
const int min_x_noclamp = range.min_x;
|
||||||
|
const int min_y_noclamp = range.min_y;
|
||||||
|
range.clip(Vector2i(index_map.get_size()));
|
||||||
|
|
||||||
|
const float texture_index_f = float(texture_index) / 255.f;
|
||||||
|
const Color all_texture_index_f(texture_index_f, texture_index_f, texture_index_f);
|
||||||
|
const int ci = texture_index % 3;
|
||||||
|
Color cm(-1, -1, -1);
|
||||||
|
cm[ci] = 1;
|
||||||
|
|
||||||
|
brush.lock();
|
||||||
|
index_map.lock();
|
||||||
|
weight_map.lock();
|
||||||
|
|
||||||
|
for (int y = range.min_y; y < range.max_y; ++y) {
|
||||||
|
const int brush_y = y - min_y_noclamp;
|
||||||
|
|
||||||
|
for (int x = range.min_x; x < range.max_x; ++x) {
|
||||||
|
const int brush_x = x - min_x_noclamp;
|
||||||
|
|
||||||
|
const float brush_value = brush.get_pixel(brush_x, brush_y).r * factor;
|
||||||
|
|
||||||
|
if (brush_value == 0.f) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color i = index_map.get_pixel(x, y);
|
||||||
|
Color w = weight_map.get_pixel(x, y);
|
||||||
|
|
||||||
|
// Decompress third weight to make computations easier
|
||||||
|
w[2] = 1.f - w[0] - w[1];
|
||||||
|
|
||||||
|
if (std::abs(i[ci] - texture_index_f) > 0.001f) {
|
||||||
|
// Pixel does not have our texture index,
|
||||||
|
// transfer its weight to other components first
|
||||||
|
if (w[ci] > brush_value) {
|
||||||
|
w[0] -= cm[0] * brush_value;
|
||||||
|
w[1] -= cm[1] * brush_value;
|
||||||
|
w[2] -= cm[2] * brush_value;
|
||||||
|
|
||||||
|
} else if (w[ci] >= 0.f) {
|
||||||
|
w[ci] = 0.f;
|
||||||
|
i[ci] = texture_index_f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Pixel has our texture index, increase its weight
|
||||||
|
if (w[ci] + brush_value < 1.f) {
|
||||||
|
w[0] += cm[0] * brush_value;
|
||||||
|
w[1] += cm[1] * brush_value;
|
||||||
|
w[2] += cm[2] * brush_value;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Pixel weight is full, we can set all components to the same index.
|
||||||
|
// Need to nullify other weights because they would otherwise never reach
|
||||||
|
// zero due to normalization
|
||||||
|
w = Color(0, 0, 0);
|
||||||
|
w[ci] = 1.0;
|
||||||
|
i = all_texture_index_f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No `saturate` function in Color??
|
||||||
|
w[0] = Math::clamp(w[0], 0.f, 1.f);
|
||||||
|
w[1] = Math::clamp(w[1], 0.f, 1.f);
|
||||||
|
w[2] = Math::clamp(w[2], 0.f, 1.f);
|
||||||
|
|
||||||
|
// Renormalize
|
||||||
|
const float sum = w[0] + w[1] + w[2];
|
||||||
|
w[0] /= sum;
|
||||||
|
w[1] /= sum;
|
||||||
|
w[2] /= sum;
|
||||||
|
|
||||||
|
index_map.set_pixel(x, y, i);
|
||||||
|
weight_map.set_pixel(x, y, w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
brush.lock();
|
||||||
|
index_map.unlock();
|
||||||
|
weight_map.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageUtils::_register_methods() {
|
||||||
|
register_method("get_red_range", &ImageUtils::get_red_range);
|
||||||
|
register_method("get_red_sum", &ImageUtils::get_red_sum);
|
||||||
|
register_method("get_red_sum_weighted", &ImageUtils::get_red_sum_weighted);
|
||||||
|
register_method("add_red_brush", &ImageUtils::add_red_brush);
|
||||||
|
register_method("lerp_channel_brush", &ImageUtils::lerp_channel_brush);
|
||||||
|
register_method("lerp_color_brush", &ImageUtils::lerp_color_brush);
|
||||||
|
register_method("generate_gaussian_brush", &ImageUtils::generate_gaussian_brush);
|
||||||
|
register_method("blur_red_brush", &ImageUtils::blur_red_brush);
|
||||||
|
register_method("paint_indexed_splat", &ImageUtils::paint_indexed_splat);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace godot
|
38
addons/zylann.hterrain/native/src/image_utils.h
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#ifndef IMAGE_UTILS_H
|
||||||
|
#define IMAGE_UTILS_H
|
||||||
|
|
||||||
|
#include <core/Godot.hpp>
|
||||||
|
#include <gen/Image.hpp>
|
||||||
|
#include <gen/Reference.hpp>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace godot {
|
||||||
|
|
||||||
|
class ImageUtils : public Reference {
|
||||||
|
GODOT_CLASS(ImageUtils, Reference)
|
||||||
|
public:
|
||||||
|
static void _register_methods();
|
||||||
|
|
||||||
|
ImageUtils();
|
||||||
|
~ImageUtils();
|
||||||
|
|
||||||
|
void _init();
|
||||||
|
|
||||||
|
Vector2 get_red_range(Ref<Image> image_ref, Rect2 rect) const;
|
||||||
|
float get_red_sum(Ref<Image> image_ref, Rect2 rect) const;
|
||||||
|
float get_red_sum_weighted(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor) const;
|
||||||
|
void add_red_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor) const;
|
||||||
|
void lerp_channel_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor, float target_value, int channel) const;
|
||||||
|
void lerp_color_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor, Color target_value) const;
|
||||||
|
float generate_gaussian_brush(Ref<Image> image_ref) const;
|
||||||
|
void blur_red_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor);
|
||||||
|
void paint_indexed_splat(Ref<Image> index_map_ref, Ref<Image> weight_map_ref, Ref<Image> brush_ref, Vector2 p_pos, int texture_index, float factor);
|
||||||
|
//void erode_red_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<float> _blur_buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace godot
|
||||||
|
|
||||||
|
#endif // IMAGE_UTILS_H
|
59
addons/zylann.hterrain/native/src/int_range_2d.h
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
#ifndef INT_RANGE_2D_H
|
||||||
|
#define INT_RANGE_2D_H
|
||||||
|
|
||||||
|
#include "math_funcs.h"
|
||||||
|
#include "vector2i.h"
|
||||||
|
#include <core/Rect2.hpp>
|
||||||
|
|
||||||
|
struct IntRange2D {
|
||||||
|
int min_x;
|
||||||
|
int min_y;
|
||||||
|
int max_x;
|
||||||
|
int max_y;
|
||||||
|
|
||||||
|
static inline IntRange2D from_min_max(godot::Vector2 min_pos, godot::Vector2 max_pos) {
|
||||||
|
return IntRange2D(godot::Rect2(min_pos, max_pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline IntRange2D from_pos_size(godot::Vector2 min_pos, godot::Vector2 size) {
|
||||||
|
return IntRange2D(godot::Rect2(min_pos, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
IntRange2D(godot::Rect2 rect) {
|
||||||
|
min_x = static_cast<int>(rect.position.x);
|
||||||
|
min_y = static_cast<int>(rect.position.y);
|
||||||
|
max_x = static_cast<int>(rect.position.x + rect.size.x);
|
||||||
|
max_y = static_cast<int>(rect.position.y + rect.size.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool is_inside(Vector2i size) const {
|
||||||
|
return min_x >= size.x &&
|
||||||
|
min_y >= size.y &&
|
||||||
|
max_x <= size.x &&
|
||||||
|
max_y <= size.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void clip(Vector2i size) {
|
||||||
|
min_x = Math::clamp(min_x, 0, size.x);
|
||||||
|
min_y = Math::clamp(min_y, 0, size.y);
|
||||||
|
max_x = Math::clamp(max_x, 0, size.x);
|
||||||
|
max_y = Math::clamp(max_y, 0, size.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void pad(int p) {
|
||||||
|
min_x -= p;
|
||||||
|
min_y -= p;
|
||||||
|
max_x += p;
|
||||||
|
max_y += p;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int get_width() const {
|
||||||
|
return max_x - min_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int get_height() const {
|
||||||
|
return max_y - min_y;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // INT_RANGE_2D_H
|
28
addons/zylann.hterrain/native/src/math_funcs.h
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#ifndef MATH_FUNCS_H
|
||||||
|
#define MATH_FUNCS_H
|
||||||
|
|
||||||
|
namespace Math {
|
||||||
|
|
||||||
|
inline float lerp(float minv, float maxv, float t) {
|
||||||
|
return minv + t * (maxv - minv);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline T clamp(T x, T minv, T maxv) {
|
||||||
|
if (x < minv) {
|
||||||
|
return minv;
|
||||||
|
}
|
||||||
|
if (x > maxv) {
|
||||||
|
return maxv;
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline T min(T a, T b) {
|
||||||
|
return a < b ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Math
|
||||||
|
|
||||||
|
#endif // MATH_FUNCS_H
|
19
addons/zylann.hterrain/native/src/vector2i.h
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#ifndef VECTOR2I_H
|
||||||
|
#define VECTOR2I_H
|
||||||
|
|
||||||
|
#include <core/Vector2.hpp>
|
||||||
|
|
||||||
|
struct Vector2i {
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
|
||||||
|
Vector2i(godot::Vector2 v) :
|
||||||
|
x(static_cast<int>(v.x)),
|
||||||
|
y(static_cast<int>(v.y)) {}
|
||||||
|
|
||||||
|
bool any_zero() const {
|
||||||
|
return x == 0 || y == 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // VECTOR2I_H
|
7
addons/zylann.hterrain/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="Heightmap Terrain"
|
||||||
|
description="Heightmap-based terrain"
|
||||||
|
author="Marc Gilleron"
|
||||||
|
version="1.5.2"
|
||||||
|
script="tools/plugin.gd"
|
169
addons/zylann.hterrain/shaders/array.shader
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
uniform sampler2D u_terrain_heightmap;
|
||||||
|
uniform sampler2D u_terrain_normalmap;
|
||||||
|
// I had to remove `hint_albedo` from colormap because it makes sRGB conversion kick in,
|
||||||
|
// which snowballs to black when doing GPU painting on that texture...
|
||||||
|
uniform sampler2D u_terrain_colormap;
|
||||||
|
uniform sampler2D u_terrain_splat_index_map;
|
||||||
|
uniform sampler2D u_terrain_splat_weight_map;
|
||||||
|
uniform sampler2D u_terrain_globalmap : hint_albedo;
|
||||||
|
uniform mat4 u_terrain_inverse_transform;
|
||||||
|
uniform mat3 u_terrain_normal_basis;
|
||||||
|
|
||||||
|
uniform sampler2DArray u_ground_albedo_bump_array : hint_albedo;
|
||||||
|
uniform sampler2DArray u_ground_normal_roughness_array;
|
||||||
|
|
||||||
|
// TODO Have UV scales for each texture in an array?
|
||||||
|
uniform float u_ground_uv_scale;
|
||||||
|
uniform float u_globalmap_blend_start;
|
||||||
|
uniform float u_globalmap_blend_distance;
|
||||||
|
uniform bool u_depth_blending = true;
|
||||||
|
|
||||||
|
varying float v_hole;
|
||||||
|
varying vec3 v_tint;
|
||||||
|
varying vec2 v_ground_uv;
|
||||||
|
varying float v_distance_to_camera;
|
||||||
|
|
||||||
|
|
||||||
|
vec3 unpack_normal(vec4 rgba) {
|
||||||
|
vec3 n = rgba.xzy * 2.0 - vec3(1.0);
|
||||||
|
// Had to negate Z because it comes from Y in the normal map,
|
||||||
|
// and OpenGL-style normal maps are Y-up.
|
||||||
|
n.z *= -1.0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 get_depth_blended_weights(vec3 splat, vec3 bumps) {
|
||||||
|
float dh = 0.2;
|
||||||
|
|
||||||
|
vec3 h = bumps + splat;
|
||||||
|
|
||||||
|
// TODO Keep improving multilayer blending, there are still some edge cases...
|
||||||
|
// Mitigation: nullify layers with near-zero splat
|
||||||
|
h *= smoothstep(0, 0.05, splat);
|
||||||
|
|
||||||
|
vec3 d = h + dh;
|
||||||
|
d.r -= max(h.g, h.b);
|
||||||
|
d.g -= max(h.r, h.b);
|
||||||
|
d.b -= max(h.g, h.r);
|
||||||
|
|
||||||
|
vec3 w = clamp(d, 0, 1);
|
||||||
|
// Had to normalize, since this approach does not preserve components summing to 1
|
||||||
|
return w / (w.x + w.y + w.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec4 wpos = WORLD_MATRIX * vec4(VERTEX, 1);
|
||||||
|
vec2 cell_coords = (u_terrain_inverse_transform * wpos).xz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results (#183)
|
||||||
|
cell_coords += vec2(0.5);
|
||||||
|
|
||||||
|
// Normalized UV
|
||||||
|
UV = cell_coords / vec2(textureSize(u_terrain_heightmap, 0));
|
||||||
|
|
||||||
|
// Height displacement
|
||||||
|
float h = texture(u_terrain_heightmap, UV).r;
|
||||||
|
VERTEX.y = h;
|
||||||
|
wpos.y = h;
|
||||||
|
|
||||||
|
vec3 base_ground_uv = vec3(cell_coords.x, h * WORLD_MATRIX[1][1], cell_coords.y);
|
||||||
|
v_ground_uv = base_ground_uv.xz / u_ground_uv_scale;
|
||||||
|
|
||||||
|
// Putting this in vertex saves 2 fetches from the fragment shader,
|
||||||
|
// which is good for performance at a negligible quality cost,
|
||||||
|
// provided that geometry is a regular grid that decimates with LOD.
|
||||||
|
// (downside is LOD will also decimate tint and splat, but it's not bad overall)
|
||||||
|
vec4 tint = texture(u_terrain_colormap, UV);
|
||||||
|
v_hole = tint.a;
|
||||||
|
v_tint = tint.rgb;
|
||||||
|
|
||||||
|
// Need to use u_terrain_normal_basis to handle scaling.
|
||||||
|
// For some reason I also had to invert Z when sampling terrain normals... not sure why
|
||||||
|
NORMAL = u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, UV));
|
||||||
|
|
||||||
|
v_distance_to_camera = distance(wpos.xyz, CAMERA_MATRIX[3].xyz);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
if (v_hole < 0.5) {
|
||||||
|
// TODO Add option to use vertex discarding instead, using NaNs
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 terrain_normal_world =
|
||||||
|
u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, UV));
|
||||||
|
terrain_normal_world = normalize(terrain_normal_world);
|
||||||
|
vec3 normal = terrain_normal_world;
|
||||||
|
|
||||||
|
float globalmap_factor =
|
||||||
|
clamp((v_distance_to_camera - u_globalmap_blend_start) * u_globalmap_blend_distance, 0.0, 1.0);
|
||||||
|
globalmap_factor *= globalmap_factor; // slower start, faster transition but far away
|
||||||
|
vec3 global_albedo = texture(u_terrain_globalmap, UV).rgb;
|
||||||
|
ALBEDO = global_albedo;
|
||||||
|
|
||||||
|
// Doing this branch allows to spare a bunch of texture fetches for distant pixels.
|
||||||
|
// Eventually, there could be a split between near and far shaders in the future,
|
||||||
|
// if relevant on high-end GPUs
|
||||||
|
if (globalmap_factor < 1.0) {
|
||||||
|
vec4 tex_splat_indexes = texture(u_terrain_splat_index_map, UV);
|
||||||
|
vec4 tex_splat_weights = texture(u_terrain_splat_weight_map, UV);
|
||||||
|
// TODO Can't use texelFetch!
|
||||||
|
// https://github.com/godotengine/godot/issues/31732
|
||||||
|
|
||||||
|
vec3 splat_indexes = tex_splat_indexes.rgb * 255.0;
|
||||||
|
vec3 splat_weights = vec3(
|
||||||
|
tex_splat_weights.r,
|
||||||
|
tex_splat_weights.g,
|
||||||
|
1.0 - tex_splat_weights.r - tex_splat_weights.g
|
||||||
|
);
|
||||||
|
|
||||||
|
vec4 ab0 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv, splat_indexes.x));
|
||||||
|
vec4 ab1 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv, splat_indexes.y));
|
||||||
|
vec4 ab2 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv, splat_indexes.z));
|
||||||
|
|
||||||
|
vec4 nr0 = texture(u_ground_normal_roughness_array, vec3(v_ground_uv, splat_indexes.x));
|
||||||
|
vec4 nr1 = texture(u_ground_normal_roughness_array, vec3(v_ground_uv, splat_indexes.y));
|
||||||
|
vec4 nr2 = texture(u_ground_normal_roughness_array, vec3(v_ground_uv, splat_indexes.z));
|
||||||
|
|
||||||
|
// TODO An #ifdef macro would be nice! Or copy/paste everything in a different shader...
|
||||||
|
if (u_depth_blending) {
|
||||||
|
splat_weights = get_depth_blended_weights(splat_weights, vec3(ab0.a, ab1.a, ab2.a));
|
||||||
|
}
|
||||||
|
|
||||||
|
ALBEDO = v_tint * (
|
||||||
|
ab0.rgb * splat_weights.x
|
||||||
|
+ ab1.rgb * splat_weights.y
|
||||||
|
+ ab2.rgb * splat_weights.z
|
||||||
|
);
|
||||||
|
|
||||||
|
ROUGHNESS =
|
||||||
|
nr0.a * splat_weights.x
|
||||||
|
+ nr1.a * splat_weights.y
|
||||||
|
+ nr2.a * splat_weights.z;
|
||||||
|
|
||||||
|
vec3 normal0 = unpack_normal(nr0);
|
||||||
|
vec3 normal1 = unpack_normal(nr1);
|
||||||
|
vec3 normal2 = unpack_normal(nr2);
|
||||||
|
|
||||||
|
vec3 ground_normal =
|
||||||
|
normal0 * splat_weights.x
|
||||||
|
+ normal1 * splat_weights.y
|
||||||
|
+ normal2 * splat_weights.z;
|
||||||
|
|
||||||
|
// Combine terrain normals with detail normals (not sure if correct but looks ok)
|
||||||
|
normal = normalize(vec3(
|
||||||
|
terrain_normal_world.x + ground_normal.x,
|
||||||
|
terrain_normal_world.y,
|
||||||
|
terrain_normal_world.z + ground_normal.z));
|
||||||
|
|
||||||
|
normal = mix(normal, terrain_normal_world, globalmap_factor);
|
||||||
|
|
||||||
|
ALBEDO = mix(ALBEDO, global_albedo, globalmap_factor);
|
||||||
|
//ALBEDO = vec3(splat_weight0, splat_weight1, splat_weight2);
|
||||||
|
ROUGHNESS = mix(ROUGHNESS, 1.0, globalmap_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
NORMAL = (INV_CAMERA_MATRIX * (vec4(normal, 0.0))).xyz;
|
||||||
|
}
|
87
addons/zylann.hterrain/shaders/array_global.shader
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
// This shader is used to bake the global albedo map.
|
||||||
|
// It exposes a subset of the main shader API, so uniform names were not modified.
|
||||||
|
|
||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
// I had to remove `hint_albedo` from colormap because it makes sRGB conversion kick in,
|
||||||
|
// which snowballs to black when doing GPU painting on that texture...
|
||||||
|
uniform sampler2D u_terrain_colormap;
|
||||||
|
uniform sampler2D u_terrain_splat_index_map;
|
||||||
|
uniform sampler2D u_terrain_splat_weight_map;
|
||||||
|
|
||||||
|
uniform sampler2DArray u_ground_albedo_bump_array : hint_albedo;
|
||||||
|
|
||||||
|
// TODO Have UV scales for each texture in an array?
|
||||||
|
uniform float u_ground_uv_scale;
|
||||||
|
// Keep depth blending because it has a high effect on the final result
|
||||||
|
uniform bool u_depth_blending = true;
|
||||||
|
|
||||||
|
|
||||||
|
vec3 get_depth_blended_weights(vec3 splat, vec3 bumps) {
|
||||||
|
float dh = 0.2;
|
||||||
|
|
||||||
|
vec3 h = bumps + splat;
|
||||||
|
|
||||||
|
// TODO Keep improving multilayer blending, there are still some edge cases...
|
||||||
|
// Mitigation: nullify layers with near-zero splat
|
||||||
|
h *= smoothstep(0, 0.05, splat);
|
||||||
|
|
||||||
|
vec3 d = h + dh;
|
||||||
|
d.r -= max(h.g, h.b);
|
||||||
|
d.g -= max(h.r, h.b);
|
||||||
|
d.b -= max(h.g, h.r);
|
||||||
|
|
||||||
|
vec3 w = clamp(d, 0, 1);
|
||||||
|
// Had to normalize, since this approach does not preserve components summing to 1
|
||||||
|
return w / (w.x + w.y + w.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec4 wpos = WORLD_MATRIX * vec4(VERTEX, 1);
|
||||||
|
vec2 cell_coords = wpos.xz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results (#183)
|
||||||
|
cell_coords += vec2(0.5);
|
||||||
|
|
||||||
|
// Normalized UV
|
||||||
|
UV = (cell_coords / vec2(textureSize(u_terrain_splat_index_map, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec4 tint = texture(u_terrain_colormap, UV);
|
||||||
|
vec4 tex_splat_indexes = texture(u_terrain_splat_index_map, UV);
|
||||||
|
vec4 tex_splat_weights = texture(u_terrain_splat_weight_map, UV);
|
||||||
|
// TODO Can't use texelFetch!
|
||||||
|
// https://github.com/godotengine/godot/issues/31732
|
||||||
|
|
||||||
|
vec3 splat_indexes = tex_splat_indexes.rgb * 255.0;
|
||||||
|
|
||||||
|
// Get bump at normal resolution so depth blending is accurate
|
||||||
|
vec2 ground_uv = UV / u_ground_uv_scale;
|
||||||
|
float b0 = texture(u_ground_albedo_bump_array, vec3(ground_uv, splat_indexes.x)).a;
|
||||||
|
float b1 = texture(u_ground_albedo_bump_array, vec3(ground_uv, splat_indexes.y)).a;
|
||||||
|
float b2 = texture(u_ground_albedo_bump_array, vec3(ground_uv, splat_indexes.z)).a;
|
||||||
|
|
||||||
|
// Take the center of the highest mip as color, because we can't see details from far away.
|
||||||
|
vec2 ndc_center = vec2(0.5, 0.5);
|
||||||
|
vec3 a0 = textureLod(u_ground_albedo_bump_array, vec3(ndc_center, splat_indexes.x), 10.0).rgb;
|
||||||
|
vec3 a1 = textureLod(u_ground_albedo_bump_array, vec3(ndc_center, splat_indexes.y), 10.0).rgb;
|
||||||
|
vec3 a2 = textureLod(u_ground_albedo_bump_array, vec3(ndc_center, splat_indexes.z), 10.0).rgb;
|
||||||
|
|
||||||
|
vec3 splat_weights = vec3(
|
||||||
|
tex_splat_weights.r,
|
||||||
|
tex_splat_weights.g,
|
||||||
|
1.0 - tex_splat_weights.r - tex_splat_weights.g
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO An #ifdef macro would be nice! Or copy/paste everything in a different shader...
|
||||||
|
if (u_depth_blending) {
|
||||||
|
splat_weights = get_depth_blended_weights(splat_weights, vec3(b0, b1, b2));
|
||||||
|
}
|
||||||
|
|
||||||
|
ALBEDO = tint.rgb * (
|
||||||
|
a0 * splat_weights.x
|
||||||
|
+ a1 * splat_weights.y
|
||||||
|
+ a2 * splat_weights.z
|
||||||
|
);
|
||||||
|
}
|
96
addons/zylann.hterrain/shaders/detail.shader
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
shader_type spatial;
|
||||||
|
render_mode cull_disabled;
|
||||||
|
|
||||||
|
uniform sampler2D u_terrain_heightmap;
|
||||||
|
uniform sampler2D u_terrain_detailmap;
|
||||||
|
uniform sampler2D u_terrain_normalmap;
|
||||||
|
uniform sampler2D u_terrain_globalmap : hint_albedo;
|
||||||
|
uniform mat4 u_terrain_inverse_transform;
|
||||||
|
uniform mat3 u_terrain_normal_basis;
|
||||||
|
|
||||||
|
uniform sampler2D u_albedo_alpha : hint_albedo;
|
||||||
|
uniform float u_view_distance = 100.0;
|
||||||
|
uniform float u_globalmap_tint_bottom : hint_range(0.0, 1.0);
|
||||||
|
uniform float u_globalmap_tint_top : hint_range(0.0, 1.0);
|
||||||
|
uniform float u_bottom_ao : hint_range(0.0, 1.0);
|
||||||
|
uniform vec2 u_ambient_wind; // x: amplitude, y: time
|
||||||
|
uniform vec3 u_instance_scale = vec3(1.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
varying vec3 v_normal;
|
||||||
|
varying vec2 v_map_uv;
|
||||||
|
|
||||||
|
float get_hash(vec2 c) {
|
||||||
|
return fract(sin(dot(c.xy, vec2(12.9898,78.233))) * 43758.5453);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 unpack_normal(vec4 rgba) {
|
||||||
|
vec3 n = rgba.xzy * 2.0 - vec3(1.0);
|
||||||
|
n.z *= -1.0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 get_ambient_wind_displacement(vec2 uv, float hash) {
|
||||||
|
// TODO This is an initial basic implementation. It may be improved in the future, especially for strong wind.
|
||||||
|
float t = u_ambient_wind.y;
|
||||||
|
float amp = u_ambient_wind.x * (1.0 - uv.y);
|
||||||
|
// Main displacement
|
||||||
|
vec3 disp = amp * vec3(cos(t), 0, sin(t * 1.2));
|
||||||
|
// Fine displacement
|
||||||
|
float fine_disp_frequency = 2.0;
|
||||||
|
disp += 0.2 * amp * vec3(cos(t * (fine_disp_frequency + hash)), 0, sin(t * (fine_disp_frequency + hash) * 1.2));
|
||||||
|
return disp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec4 obj_pos = WORLD_MATRIX * vec4(0, 1, 0, 1);
|
||||||
|
vec3 cell_coords = (u_terrain_inverse_transform * obj_pos).xyz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results (#183)
|
||||||
|
cell_coords.xz += vec2(0.5);
|
||||||
|
|
||||||
|
vec2 map_uv = cell_coords.xz / vec2(textureSize(u_terrain_heightmap, 0));
|
||||||
|
v_map_uv = map_uv;
|
||||||
|
|
||||||
|
//float density = 0.5 + 0.5 * sin(4.0*TIME); // test
|
||||||
|
float density = texture(u_terrain_detailmap, map_uv).r;
|
||||||
|
float hash = get_hash(obj_pos.xz);
|
||||||
|
|
||||||
|
if (density > hash) {
|
||||||
|
// Snap model to the terrain
|
||||||
|
float height = texture(u_terrain_heightmap, map_uv).r / cell_coords.y;
|
||||||
|
VERTEX *= u_instance_scale;
|
||||||
|
VERTEX.y += height;
|
||||||
|
|
||||||
|
VERTEX += get_ambient_wind_displacement(UV, hash);
|
||||||
|
|
||||||
|
// Fade alpha with distance
|
||||||
|
vec3 wpos = (WORLD_MATRIX * vec4(VERTEX, 1)).xyz;
|
||||||
|
float dr = distance(wpos, CAMERA_MATRIX[3].xyz) / u_view_distance;
|
||||||
|
COLOR.a = clamp(1.0 - dr * dr * dr, 0.0, 1.0);
|
||||||
|
|
||||||
|
// When using billboards, the normal is the same as the terrain regardless of face orientation
|
||||||
|
v_normal = normalize(u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, map_uv)));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Discard, output degenerate triangles
|
||||||
|
VERTEX = vec3(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
NORMAL = (INV_CAMERA_MATRIX * (WORLD_MATRIX * vec4(v_normal, 0.0))).xyz;
|
||||||
|
ALPHA_SCISSOR = 0.5;
|
||||||
|
ROUGHNESS = 1.0;
|
||||||
|
|
||||||
|
vec4 col = texture(u_albedo_alpha, UV);
|
||||||
|
ALPHA = col.a * COLOR.a;// - clamp(1.4 - UV.y, 0.0, 1.0);//* 0.5 + 0.5*cos(2.0*TIME);
|
||||||
|
|
||||||
|
ALBEDO = COLOR.rgb * col.rgb;
|
||||||
|
|
||||||
|
// Blend with ground color
|
||||||
|
float nh = sqrt(1.0 - UV.y);
|
||||||
|
ALBEDO = mix(ALBEDO, texture(u_terrain_globalmap, v_map_uv).rgb, mix(u_globalmap_tint_bottom, u_globalmap_tint_top, nh));
|
||||||
|
|
||||||
|
// Fake bottom AO
|
||||||
|
ALBEDO = ALBEDO * mix(1.0, 1.0 - u_bottom_ao, UV.y * UV.y);
|
||||||
|
}
|
64
addons/zylann.hterrain/shaders/lookdev.shader
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
// Development shader used to debug or help authoring.
|
||||||
|
|
||||||
|
uniform sampler2D u_terrain_heightmap;
|
||||||
|
uniform sampler2D u_terrain_normalmap;
|
||||||
|
uniform sampler2D u_terrain_colormap;
|
||||||
|
uniform sampler2D u_map; // This map will control color
|
||||||
|
uniform mat4 u_terrain_inverse_transform;
|
||||||
|
uniform mat3 u_terrain_normal_basis;
|
||||||
|
|
||||||
|
varying float v_hole;
|
||||||
|
|
||||||
|
|
||||||
|
vec3 unpack_normal(vec4 rgba) {
|
||||||
|
return rgba.xzy * 2.0 - vec3(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec4 wpos = WORLD_MATRIX * vec4(VERTEX, 1);
|
||||||
|
vec2 cell_coords = (u_terrain_inverse_transform * wpos).xz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results (#183)
|
||||||
|
cell_coords += vec2(0.5);
|
||||||
|
|
||||||
|
// Normalized UV
|
||||||
|
UV = cell_coords / vec2(textureSize(u_terrain_heightmap, 0));
|
||||||
|
|
||||||
|
// Height displacement
|
||||||
|
float h = texture(u_terrain_heightmap, UV).r;
|
||||||
|
VERTEX.y = h;
|
||||||
|
wpos.y = h;
|
||||||
|
|
||||||
|
// Putting this in vertex saves 2 fetches from the fragment shader,
|
||||||
|
// which is good for performance at a negligible quality cost,
|
||||||
|
// provided that geometry is a regular grid that decimates with LOD.
|
||||||
|
// (downside is LOD will also decimate tint and splat, but it's not bad overall)
|
||||||
|
vec4 tint = texture(u_terrain_colormap, UV);
|
||||||
|
v_hole = tint.a;
|
||||||
|
|
||||||
|
// Need to use u_terrain_normal_basis to handle scaling.
|
||||||
|
// For some reason I also had to invert Z when sampling terrain normals... not sure why
|
||||||
|
NORMAL = u_terrain_normal_basis
|
||||||
|
* (unpack_normal(texture(u_terrain_normalmap, UV)) * vec3(1, 1, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
if (v_hole < 0.5) {
|
||||||
|
// TODO Add option to use vertex discarding instead, using NaNs
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 terrain_normal_world =
|
||||||
|
u_terrain_normal_basis * (unpack_normal(texture(u_terrain_normalmap, UV)) * vec3(1,1,-1));
|
||||||
|
terrain_normal_world = normalize(terrain_normal_world);
|
||||||
|
vec3 normal = terrain_normal_world;
|
||||||
|
|
||||||
|
vec4 value = texture(u_map, UV);
|
||||||
|
// TODO Blend toward checker pattern to show the alpha channel
|
||||||
|
|
||||||
|
ALBEDO = value.rgb;
|
||||||
|
ROUGHNESS = 0.5;
|
||||||
|
NORMAL = (INV_CAMERA_MATRIX * (vec4(normal, 0.0))).xyz;
|
||||||
|
}
|
61
addons/zylann.hterrain/shaders/low_poly.shader
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
// This is a very simple shader for a low-poly coloured visual, without textures
|
||||||
|
|
||||||
|
uniform sampler2D u_terrain_heightmap;
|
||||||
|
uniform sampler2D u_terrain_normalmap;
|
||||||
|
// I had to remove `hint_albedo` from colormap because it makes sRGB conversion kick in,
|
||||||
|
// which snowballs to black when doing GPU painting on that texture...
|
||||||
|
uniform sampler2D u_terrain_colormap;// : hint_albedo;
|
||||||
|
uniform mat4 u_terrain_inverse_transform;
|
||||||
|
uniform mat3 u_terrain_normal_basis;
|
||||||
|
|
||||||
|
varying flat vec4 v_tint;
|
||||||
|
|
||||||
|
|
||||||
|
vec3 unpack_normal(vec4 rgba) {
|
||||||
|
vec3 n = rgba.xzy * 2.0 - vec3(1.0);
|
||||||
|
// Had to negate Z because it comes from Y in the normal map,
|
||||||
|
// and OpenGL-style normal maps are Y-up.
|
||||||
|
n.z *= -1.0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec2 cell_coords = (u_terrain_inverse_transform * WORLD_MATRIX * vec4(VERTEX, 1)).xz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results (#183)
|
||||||
|
cell_coords += vec2(0.5);
|
||||||
|
|
||||||
|
// Normalized UV
|
||||||
|
UV = cell_coords / vec2(textureSize(u_terrain_heightmap, 0));
|
||||||
|
|
||||||
|
// Height displacement
|
||||||
|
float h = texture(u_terrain_heightmap, UV).r;
|
||||||
|
VERTEX.y = h;
|
||||||
|
|
||||||
|
// Putting this in vertex saves 2 fetches from the fragment shader,
|
||||||
|
// which is good for performance at a negligible quality cost,
|
||||||
|
// provided that geometry is a regular grid that decimates with LOD.
|
||||||
|
// (downside is LOD will also decimate tint and splat, but it's not bad overall)
|
||||||
|
v_tint = texture(u_terrain_colormap, UV);
|
||||||
|
|
||||||
|
// Need to use u_terrain_normal_basis to handle scaling.
|
||||||
|
NORMAL = u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, UV));
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
if (v_tint.a < 0.5) {
|
||||||
|
// TODO Add option to use vertex discarding instead, using NaNs
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 terrain_normal_world =
|
||||||
|
u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, UV));
|
||||||
|
terrain_normal_world = normalize(terrain_normal_world);
|
||||||
|
|
||||||
|
ALBEDO = v_tint.rgb;
|
||||||
|
ROUGHNESS = 1.0;
|
||||||
|
NORMAL = normalize(cross(dFdx(VERTEX), dFdy(VERTEX)));
|
||||||
|
}
|
||||||
|
|
369
addons/zylann.hterrain/shaders/multisplat16.shader
Normal file
|
@ -0,0 +1,369 @@
|
||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
// WIP
|
||||||
|
// This shader uses a texture array with multiple splatmaps, allowing up to 16 textures.
|
||||||
|
// Only the 4 textures having highest blending weight are sampled.
|
||||||
|
|
||||||
|
uniform sampler2D u_terrain_heightmap;
|
||||||
|
uniform sampler2D u_terrain_normalmap;
|
||||||
|
// I had to remove `hint_albedo` from colormap because it makes sRGB conversion kick in,
|
||||||
|
// which snowballs to black when doing GPU painting on that texture...
|
||||||
|
uniform sampler2D u_terrain_colormap;
|
||||||
|
uniform sampler2D u_terrain_splatmap;
|
||||||
|
uniform sampler2D u_terrain_splatmap_1;
|
||||||
|
uniform sampler2D u_terrain_splatmap_2;
|
||||||
|
uniform sampler2D u_terrain_splatmap_3;
|
||||||
|
uniform sampler2D u_terrain_globalmap : hint_albedo;
|
||||||
|
uniform mat4 u_terrain_inverse_transform;
|
||||||
|
uniform mat3 u_terrain_normal_basis;
|
||||||
|
|
||||||
|
uniform sampler2DArray u_ground_albedo_bump_array : hint_albedo;
|
||||||
|
uniform sampler2DArray u_ground_normal_roughness_array;
|
||||||
|
|
||||||
|
uniform float u_ground_uv_scale = 20.0;
|
||||||
|
uniform bool u_depth_blending = true;
|
||||||
|
uniform float u_globalmap_blend_start;
|
||||||
|
uniform float u_globalmap_blend_distance;
|
||||||
|
uniform bool u_tile_reduction = false;
|
||||||
|
|
||||||
|
varying float v_hole;
|
||||||
|
varying vec3 v_tint;
|
||||||
|
varying vec2 v_terrain_uv;
|
||||||
|
varying vec3 v_ground_uv;
|
||||||
|
varying float v_distance_to_camera;
|
||||||
|
|
||||||
|
// TODO Can't put this in a constant: https://github.com/godotengine/godot/issues/44145
|
||||||
|
//const int TEXTURE_COUNT = 16;
|
||||||
|
|
||||||
|
|
||||||
|
vec3 unpack_normal(vec4 rgba) {
|
||||||
|
vec3 n = rgba.xzy * 2.0 - vec3(1.0);
|
||||||
|
// Had to negate Z because it comes from Y in the normal map,
|
||||||
|
// and OpenGL-style normal maps are Y-up.
|
||||||
|
n.z *= -1.0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 pack_normal(vec3 n, float a) {
|
||||||
|
n.z *= -1.0;
|
||||||
|
return vec4((n.xzy + vec3(1.0)) * 0.5, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blends weights according to the bump of detail textures,
|
||||||
|
// so for example it allows to have sand fill the gaps between pebbles
|
||||||
|
vec4 get_depth_blended_weights(vec4 splat, vec4 bumps) {
|
||||||
|
float dh = 0.2;
|
||||||
|
|
||||||
|
vec4 h = bumps + splat;
|
||||||
|
|
||||||
|
// TODO Keep improving multilayer blending, there are still some edge cases...
|
||||||
|
// Mitigation: nullify layers with near-zero splat
|
||||||
|
h *= smoothstep(0, 0.05, splat);
|
||||||
|
|
||||||
|
vec4 d = h + dh;
|
||||||
|
d.r -= max(h.g, max(h.b, h.a));
|
||||||
|
d.g -= max(h.r, max(h.b, h.a));
|
||||||
|
d.b -= max(h.g, max(h.r, h.a));
|
||||||
|
d.a -= max(h.g, max(h.b, h.r));
|
||||||
|
|
||||||
|
return clamp(d, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 get_triplanar_blend(vec3 world_normal) {
|
||||||
|
vec3 blending = abs(world_normal);
|
||||||
|
blending = normalize(max(blending, vec3(0.00001))); // Force weights to sum to 1.0
|
||||||
|
float b = blending.x + blending.y + blending.z;
|
||||||
|
return blending / vec3(b, b, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 texture_triplanar(sampler2D tex, vec3 world_pos, vec3 blend) {
|
||||||
|
vec4 xaxis = texture(tex, world_pos.yz);
|
||||||
|
vec4 yaxis = texture(tex, world_pos.xz);
|
||||||
|
vec4 zaxis = texture(tex, world_pos.xy);
|
||||||
|
// blend the results of the 3 planar projections.
|
||||||
|
return xaxis * blend.x + yaxis * blend.y + zaxis * blend.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_splat_weights(vec2 uv, out vec4 out_high_indices, out vec4 out_high_weights) {
|
||||||
|
vec4 ew0 = texture(u_terrain_splatmap, uv);
|
||||||
|
vec4 ew1 = texture(u_terrain_splatmap_1, uv);
|
||||||
|
vec4 ew2 = texture(u_terrain_splatmap_2, uv);
|
||||||
|
vec4 ew3 = texture(u_terrain_splatmap_3, uv);
|
||||||
|
|
||||||
|
float weights[16] = {
|
||||||
|
ew0.r, ew0.g, ew0.b, ew0.a,
|
||||||
|
ew1.r, ew1.g, ew1.b, ew1.a,
|
||||||
|
ew2.r, ew2.g, ew2.b, ew2.a,
|
||||||
|
ew3.r, ew3.g, ew3.b, ew3.a
|
||||||
|
};
|
||||||
|
|
||||||
|
// float weights_sum = 0.0;
|
||||||
|
// for (int i = 0; i < 16; ++i) {
|
||||||
|
// weights_sum += weights[i];
|
||||||
|
// }
|
||||||
|
// for (int i = 0; i < 16; ++i) {
|
||||||
|
// weights_sum /= weights_sum;
|
||||||
|
// }
|
||||||
|
// weights_sum=1.1;
|
||||||
|
|
||||||
|
// Now we have to pick the 4 highest weights and use them to blend textures.
|
||||||
|
|
||||||
|
// Using arrays because Godot's shader version doesn't support dynamic indexing of vectors
|
||||||
|
// TODO We should not need to initialize, but apparently we don't always find 4 weights
|
||||||
|
int high_indices_array[4] = {0, 0, 0, 0};
|
||||||
|
float high_weights_array[4] = {0.0, 0.0, 0.0, 0.0};
|
||||||
|
int count = 0;
|
||||||
|
// We know weights are supposed to be normalized.
|
||||||
|
// That means the highest value of the pivot above which we can find 4 results
|
||||||
|
// is 1.0 / 4.0. However that would mean exactly 4 textures have exactly that weight,
|
||||||
|
// which is very unlikely. If we consider 1.0 / 5.0, we are a bit more likely to find
|
||||||
|
// 4 results, and finding 5 results remains almost impossible.
|
||||||
|
float pivot = /*weights_sum*/1.0 / 5.0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
if (weights[i] > pivot) {
|
||||||
|
high_weights_array[count] = weights[i];
|
||||||
|
high_indices_array[count] = i;
|
||||||
|
weights[i] = 0.0;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (count < 4 && pivot > 0.0) {
|
||||||
|
float max_weight = 0.0;
|
||||||
|
int max_index = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
if (/*weights[i] <= pivot && */weights[i] > max_weight) {
|
||||||
|
max_weight = weights[i];
|
||||||
|
max_index = i;
|
||||||
|
weights[i] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
high_indices_array[count] = max_index;
|
||||||
|
high_weights_array[count] = max_weight;
|
||||||
|
++count;
|
||||||
|
pivot = max_weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_high_weights = vec4(
|
||||||
|
high_weights_array[0], high_weights_array[1],
|
||||||
|
high_weights_array[2], high_weights_array[3]);
|
||||||
|
|
||||||
|
out_high_indices = vec4(
|
||||||
|
float(high_indices_array[0]), float(high_indices_array[1]),
|
||||||
|
float(high_indices_array[2]), float(high_indices_array[3]));
|
||||||
|
|
||||||
|
out_high_weights /=
|
||||||
|
out_high_weights.r + out_high_weights.g + out_high_weights.b + out_high_weights.a;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 depth_blend2(vec4 a_value, float a_bump, vec4 b_value, float b_bump, float t) {
|
||||||
|
// https://www.gamasutra.com
|
||||||
|
// /blogs/AndreyMishkinis/20130716/196339/Advanced_Terrain_Texture_Splatting.php
|
||||||
|
float d = 0.1;
|
||||||
|
float ma = max(a_bump + (1.0 - t), b_bump + t) - d;
|
||||||
|
float ba = max(a_bump + (1.0 - t) - ma, 0.0);
|
||||||
|
float bb = max(b_bump + t - ma, 0.0);
|
||||||
|
return (a_value * ba + b_value * bb) / (ba + bb);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 rotate(vec2 v, float cosa, float sina) {
|
||||||
|
return vec2(cosa * v.x - sina * v.y, sina * v.x + cosa * v.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 texture_array_antitile(sampler2DArray albedo_tex, sampler2DArray normal_tex, vec3 uv,
|
||||||
|
out vec4 out_normal) {
|
||||||
|
|
||||||
|
float frequency = 2.0;
|
||||||
|
float scale = 1.3;
|
||||||
|
float sharpness = 0.7;
|
||||||
|
|
||||||
|
// Rotate and scale UV
|
||||||
|
float rot = 3.14 * 0.6;
|
||||||
|
float cosa = cos(rot);
|
||||||
|
float sina = sin(rot);
|
||||||
|
vec3 uv2 = vec3(rotate(uv.xy, cosa, sina) * scale, uv.z);
|
||||||
|
|
||||||
|
vec4 col0 = texture(albedo_tex, uv);
|
||||||
|
vec4 col1 = texture(albedo_tex, uv2);
|
||||||
|
vec4 nrm0 = texture(normal_tex, uv);
|
||||||
|
vec4 nrm1 = texture(normal_tex, uv2);
|
||||||
|
//col0 = vec4(0.0, 0.5, 0.5, 1.0); // Highlights variations
|
||||||
|
|
||||||
|
// Normals have to be rotated too since we are rotating the texture...
|
||||||
|
// TODO Probably not the most efficient but understandable for now
|
||||||
|
vec3 n = unpack_normal(nrm1);
|
||||||
|
// Had to negate the Y axis for some reason. I never remember the myriad of conventions around
|
||||||
|
n.xz = rotate(n.xz, cosa, -sina);
|
||||||
|
nrm1 = pack_normal(n, nrm1.a);
|
||||||
|
|
||||||
|
// Periodically alternate between the two versions using a warped checker pattern
|
||||||
|
float t = 1.1 + 0.5
|
||||||
|
* sin(uv2.x * frequency + sin(uv.x) * 2.0)
|
||||||
|
* cos(uv2.y * frequency + sin(uv.y) * 2.0); // Result in [0..2]
|
||||||
|
t = smoothstep(sharpness, 2.0 - sharpness, t);
|
||||||
|
|
||||||
|
// Using depth blend because classic alpha blending smoothes out details.
|
||||||
|
out_normal = depth_blend2(nrm0, col0.a, nrm1, col1.a, t);
|
||||||
|
return depth_blend2(col0, col0.a, col1, col1.a, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec4 wpos = WORLD_MATRIX * vec4(VERTEX, 1);
|
||||||
|
vec2 cell_coords = (u_terrain_inverse_transform * wpos).xz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results (#183)
|
||||||
|
cell_coords += vec2(0.5);
|
||||||
|
|
||||||
|
// Normalized UV
|
||||||
|
UV = cell_coords / vec2(textureSize(u_terrain_heightmap, 0));
|
||||||
|
|
||||||
|
// Height displacement
|
||||||
|
float h = texture(u_terrain_heightmap, UV).r;
|
||||||
|
VERTEX.y = h;
|
||||||
|
wpos.y = h;
|
||||||
|
|
||||||
|
vec3 base_ground_uv = vec3(cell_coords.x, h * WORLD_MATRIX[1][1], cell_coords.y);
|
||||||
|
v_ground_uv = base_ground_uv / u_ground_uv_scale;
|
||||||
|
|
||||||
|
// Putting this in vertex saves a fetch from the fragment shader,
|
||||||
|
// which is good for performance at a negligible quality cost,
|
||||||
|
// provided that geometry is a regular grid that decimates with LOD.
|
||||||
|
// (downside is LOD will also decimate it, but it's not bad overall)
|
||||||
|
vec4 tint = texture(u_terrain_colormap, UV);
|
||||||
|
v_hole = tint.a;
|
||||||
|
v_tint = tint.rgb;
|
||||||
|
|
||||||
|
// Need to use u_terrain_normal_basis to handle scaling.
|
||||||
|
// For some reason I also had to invert Z when sampling terrain normals... not sure why
|
||||||
|
NORMAL = u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, UV));
|
||||||
|
|
||||||
|
v_distance_to_camera = distance(wpos.xyz, CAMERA_MATRIX[3].xyz);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
if (v_hole < 0.5) {
|
||||||
|
// TODO Add option to use vertex discarding instead, using NaNs
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 terrain_normal_world =
|
||||||
|
u_terrain_normal_basis * (unpack_normal(texture(u_terrain_normalmap, UV)) * vec3(1,1,-1));
|
||||||
|
terrain_normal_world = normalize(terrain_normal_world);
|
||||||
|
vec3 normal = terrain_normal_world;
|
||||||
|
|
||||||
|
float globalmap_factor = clamp((v_distance_to_camera - u_globalmap_blend_start)
|
||||||
|
* u_globalmap_blend_distance, 0.0, 1.0);
|
||||||
|
globalmap_factor *= globalmap_factor; // slower start, faster transition but far away
|
||||||
|
vec3 global_albedo = texture(u_terrain_globalmap, UV).rgb;
|
||||||
|
ALBEDO = global_albedo;
|
||||||
|
|
||||||
|
// Doing this branch allows to spare a bunch of texture fetches for distant pixels.
|
||||||
|
// Eventually, there could be a split between near and far shaders in the future,
|
||||||
|
// if relevant on high-end GPUs
|
||||||
|
if (globalmap_factor < 1.0) {
|
||||||
|
vec4 high_indices;
|
||||||
|
vec4 high_weights;
|
||||||
|
get_splat_weights(UV, high_indices, high_weights);
|
||||||
|
|
||||||
|
vec4 ab0, ab1, ab2, ab3;
|
||||||
|
vec4 nr0, nr1, nr2, nr3;
|
||||||
|
|
||||||
|
if (u_tile_reduction) {
|
||||||
|
ab0 = texture_array_antitile(
|
||||||
|
u_ground_albedo_bump_array, u_ground_normal_roughness_array,
|
||||||
|
vec3(v_ground_uv.xz, high_indices.x), nr0);
|
||||||
|
ab1 = texture_array_antitile(
|
||||||
|
u_ground_albedo_bump_array, u_ground_normal_roughness_array,
|
||||||
|
vec3(v_ground_uv.xz, high_indices.y), nr1);
|
||||||
|
ab2 = texture_array_antitile(
|
||||||
|
u_ground_albedo_bump_array, u_ground_normal_roughness_array,
|
||||||
|
vec3(v_ground_uv.xz, high_indices.z), nr2);
|
||||||
|
ab3 = texture_array_antitile(
|
||||||
|
u_ground_albedo_bump_array, u_ground_normal_roughness_array,
|
||||||
|
vec3(v_ground_uv.xz, high_indices.w), nr3);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ab0 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv.xz, high_indices.x));
|
||||||
|
ab1 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv.xz, high_indices.y));
|
||||||
|
ab2 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv.xz, high_indices.z));
|
||||||
|
ab3 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv.xz, high_indices.w));
|
||||||
|
|
||||||
|
nr0 = texture(u_ground_normal_roughness_array, vec3(v_ground_uv.xz, high_indices.x));
|
||||||
|
nr1 = texture(u_ground_normal_roughness_array, vec3(v_ground_uv.xz, high_indices.y));
|
||||||
|
nr2 = texture(u_ground_normal_roughness_array, vec3(v_ground_uv.xz, high_indices.z));
|
||||||
|
nr3 = texture(u_ground_normal_roughness_array, vec3(v_ground_uv.xz, high_indices.w));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 col0 = ab0.rgb * v_tint;
|
||||||
|
vec3 col1 = ab1.rgb * v_tint;
|
||||||
|
vec3 col2 = ab2.rgb * v_tint;
|
||||||
|
vec3 col3 = ab3.rgb * v_tint;
|
||||||
|
|
||||||
|
vec4 rough = vec4(nr0.a, nr1.a, nr2.a, nr3.a);
|
||||||
|
|
||||||
|
vec3 normal0 = unpack_normal(nr0);
|
||||||
|
vec3 normal1 = unpack_normal(nr1);
|
||||||
|
vec3 normal2 = unpack_normal(nr2);
|
||||||
|
vec3 normal3 = unpack_normal(nr3);
|
||||||
|
|
||||||
|
vec4 w;
|
||||||
|
// TODO An #ifdef macro would be nice! Or copy/paste everything in a different shader...
|
||||||
|
if (u_depth_blending) {
|
||||||
|
w = get_depth_blended_weights(high_weights, vec4(ab0.a, ab1.a, ab2.a, ab3.a));
|
||||||
|
} else {
|
||||||
|
w = high_weights;
|
||||||
|
}
|
||||||
|
|
||||||
|
float w_sum = (w.r + w.g + w.b + w.a);
|
||||||
|
|
||||||
|
ALBEDO = (
|
||||||
|
w.r * col0.rgb +
|
||||||
|
w.g * col1.rgb +
|
||||||
|
w.b * col2.rgb +
|
||||||
|
w.a * col3.rgb) / w_sum;
|
||||||
|
|
||||||
|
ROUGHNESS = (
|
||||||
|
w.r * rough.r +
|
||||||
|
w.g * rough.g +
|
||||||
|
w.b * rough.b +
|
||||||
|
w.a * rough.a) / w_sum;
|
||||||
|
|
||||||
|
vec3 ground_normal = /*u_terrain_normal_basis **/ (
|
||||||
|
w.r * normal0 +
|
||||||
|
w.g * normal1 +
|
||||||
|
w.b * normal2 +
|
||||||
|
w.a * normal3) / w_sum;
|
||||||
|
// If no splat textures are defined, normal vectors will default to (1,1,1),
|
||||||
|
// which is incorrect, and causes the terrain to be shaded wrongly in some directions.
|
||||||
|
// However, this should not be a problem to fix in the shader,
|
||||||
|
// because there MUST be at least one splat texture set.
|
||||||
|
//ground_normal = normalize(ground_normal);
|
||||||
|
// TODO Make the plugin insert a default normalmap if it's empty
|
||||||
|
|
||||||
|
// Combine terrain normals with detail normals (not sure if correct but looks ok)
|
||||||
|
normal = normalize(vec3(
|
||||||
|
terrain_normal_world.x + ground_normal.x,
|
||||||
|
terrain_normal_world.y,
|
||||||
|
terrain_normal_world.z + ground_normal.z));
|
||||||
|
|
||||||
|
normal = mix(normal, terrain_normal_world, globalmap_factor);
|
||||||
|
|
||||||
|
ALBEDO = mix(ALBEDO, global_albedo, globalmap_factor);
|
||||||
|
ROUGHNESS = mix(ROUGHNESS, 1.0, globalmap_factor);
|
||||||
|
|
||||||
|
// if(count < 3) {
|
||||||
|
// ALBEDO = vec3(1.0, 0.0, 0.0);
|
||||||
|
// }
|
||||||
|
// Show splatmap weights
|
||||||
|
//ALBEDO = w.rgb;
|
||||||
|
}
|
||||||
|
// Highlight all pixels undergoing no splatmap at all
|
||||||
|
// else {
|
||||||
|
// ALBEDO = vec3(1.0, 0.0, 0.0);
|
||||||
|
// }
|
||||||
|
|
||||||
|
NORMAL = (INV_CAMERA_MATRIX * (vec4(normal, 0.0))).xyz;
|
||||||
|
}
|
173
addons/zylann.hterrain/shaders/multisplat16_global.shader
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
// This shader uses a texture array with multiple splatmaps, allowing up to 16 textures.
|
||||||
|
// Only the 4 textures having highest blending weight are sampled.
|
||||||
|
|
||||||
|
// I had to remove `hint_albedo` from colormap because it makes sRGB conversion kick in,
|
||||||
|
// which snowballs to black when doing GPU painting on that texture...
|
||||||
|
uniform sampler2D u_terrain_colormap;
|
||||||
|
uniform sampler2D u_terrain_splatmap;
|
||||||
|
uniform sampler2D u_terrain_splatmap_1;
|
||||||
|
uniform sampler2D u_terrain_splatmap_2;
|
||||||
|
uniform sampler2D u_terrain_splatmap_3;
|
||||||
|
|
||||||
|
uniform sampler2DArray u_ground_albedo_bump_array : hint_albedo;
|
||||||
|
|
||||||
|
uniform float u_ground_uv_scale = 20.0;
|
||||||
|
uniform bool u_depth_blending = true;
|
||||||
|
|
||||||
|
// TODO Can't put this in a constant: https://github.com/godotengine/godot/issues/44145
|
||||||
|
//const int TEXTURE_COUNT = 16;
|
||||||
|
|
||||||
|
|
||||||
|
// Blends weights according to the bump of detail textures,
|
||||||
|
// so for example it allows to have sand fill the gaps between pebbles
|
||||||
|
vec4 get_depth_blended_weights(vec4 splat, vec4 bumps) {
|
||||||
|
float dh = 0.2;
|
||||||
|
|
||||||
|
vec4 h = bumps + splat;
|
||||||
|
|
||||||
|
// TODO Keep improving multilayer blending, there are still some edge cases...
|
||||||
|
// Mitigation: nullify layers with near-zero splat
|
||||||
|
h *= smoothstep(0, 0.05, splat);
|
||||||
|
|
||||||
|
vec4 d = h + dh;
|
||||||
|
d.r -= max(h.g, max(h.b, h.a));
|
||||||
|
d.g -= max(h.r, max(h.b, h.a));
|
||||||
|
d.b -= max(h.g, max(h.r, h.a));
|
||||||
|
d.a -= max(h.g, max(h.b, h.r));
|
||||||
|
|
||||||
|
return clamp(d, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_splat_weights(vec2 uv, out vec4 out_high_indices, out vec4 out_high_weights) {
|
||||||
|
vec4 ew0 = texture(u_terrain_splatmap, uv);
|
||||||
|
vec4 ew1 = texture(u_terrain_splatmap_1, uv);
|
||||||
|
vec4 ew2 = texture(u_terrain_splatmap_2, uv);
|
||||||
|
vec4 ew3 = texture(u_terrain_splatmap_3, uv);
|
||||||
|
|
||||||
|
float weights[16] = {
|
||||||
|
ew0.r, ew0.g, ew0.b, ew0.a,
|
||||||
|
ew1.r, ew1.g, ew1.b, ew1.a,
|
||||||
|
ew2.r, ew2.g, ew2.b, ew2.a,
|
||||||
|
ew3.r, ew3.g, ew3.b, ew3.a
|
||||||
|
};
|
||||||
|
|
||||||
|
// float weights_sum = 0.0;
|
||||||
|
// for (int i = 0; i < 16; ++i) {
|
||||||
|
// weights_sum += weights[i];
|
||||||
|
// }
|
||||||
|
// for (int i = 0; i < 16; ++i) {
|
||||||
|
// weights_sum /= weights_sum;
|
||||||
|
// }
|
||||||
|
// weights_sum=1.1;
|
||||||
|
|
||||||
|
// Now we have to pick the 4 highest weights and use them to blend textures.
|
||||||
|
|
||||||
|
// Using arrays because Godot's shader version doesn't support dynamic indexing of vectors
|
||||||
|
// TODO We should not need to initialize, but apparently we don't always find 4 weights
|
||||||
|
int high_indices_array[4] = {0, 0, 0, 0};
|
||||||
|
float high_weights_array[4] = {0.0, 0.0, 0.0, 0.0};
|
||||||
|
int count = 0;
|
||||||
|
// We know weights are supposed to be normalized.
|
||||||
|
// That means the highest value of the pivot above which we can find 4 results
|
||||||
|
// is 1.0 / 4.0. However that would mean exactly 4 textures have exactly that weight,
|
||||||
|
// which is very unlikely. If we consider 1.0 / 5.0, we are a bit more likely to find
|
||||||
|
// 4 results, and finding 5 results remains almost impossible.
|
||||||
|
float pivot = /*weights_sum*/1.0 / 5.0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
if (weights[i] > pivot) {
|
||||||
|
high_weights_array[count] = weights[i];
|
||||||
|
high_indices_array[count] = i;
|
||||||
|
weights[i] = 0.0;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (count < 4 && pivot > 0.0) {
|
||||||
|
float max_weight = 0.0;
|
||||||
|
int max_index = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
if (/*weights[i] <= pivot && */weights[i] > max_weight) {
|
||||||
|
max_weight = weights[i];
|
||||||
|
max_index = i;
|
||||||
|
weights[i] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
high_indices_array[count] = max_index;
|
||||||
|
high_weights_array[count] = max_weight;
|
||||||
|
++count;
|
||||||
|
pivot = max_weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_high_weights = vec4(
|
||||||
|
high_weights_array[0], high_weights_array[1],
|
||||||
|
high_weights_array[2], high_weights_array[3]);
|
||||||
|
|
||||||
|
out_high_indices = vec4(
|
||||||
|
float(high_indices_array[0]), float(high_indices_array[1]),
|
||||||
|
float(high_indices_array[2]), float(high_indices_array[3]));
|
||||||
|
|
||||||
|
out_high_weights /=
|
||||||
|
out_high_weights.r + out_high_weights.g + out_high_weights.b + out_high_weights.a;
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec4 wpos = WORLD_MATRIX * vec4(VERTEX, 1);
|
||||||
|
vec2 cell_coords = wpos.xz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results (#183)
|
||||||
|
cell_coords += vec2(0.5);
|
||||||
|
|
||||||
|
// Normalized UV
|
||||||
|
UV = cell_coords / vec2(textureSize(u_terrain_splatmap, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
// These were moved from vertex to fragment,
|
||||||
|
// so we can generate part of the global map with just one quad and we get full quality
|
||||||
|
vec3 tint = texture(u_terrain_colormap, UV).rgb;
|
||||||
|
vec4 splat = texture(u_terrain_splatmap, UV);
|
||||||
|
|
||||||
|
vec4 high_indices;
|
||||||
|
vec4 high_weights;
|
||||||
|
get_splat_weights(UV, high_indices, high_weights);
|
||||||
|
|
||||||
|
// Get bump at normal resolution so depth blending is accurate
|
||||||
|
vec2 ground_uv = UV / u_ground_uv_scale;
|
||||||
|
float b0 = texture(u_ground_albedo_bump_array, vec3(ground_uv, high_indices.x)).a;
|
||||||
|
float b1 = texture(u_ground_albedo_bump_array, vec3(ground_uv, high_indices.y)).a;
|
||||||
|
float b2 = texture(u_ground_albedo_bump_array, vec3(ground_uv, high_indices.z)).a;
|
||||||
|
float b3 = texture(u_ground_albedo_bump_array, vec3(ground_uv, high_indices.w)).a;
|
||||||
|
|
||||||
|
// Take the center of the highest mip as color, because we can't see details from far away.
|
||||||
|
vec2 ndc_center = vec2(0.5, 0.5);
|
||||||
|
vec3 a0 = textureLod(u_ground_albedo_bump_array, vec3(ndc_center, high_indices.x), 10.0).rgb;
|
||||||
|
vec3 a1 = textureLod(u_ground_albedo_bump_array, vec3(ndc_center, high_indices.y), 10.0).rgb;
|
||||||
|
vec3 a2 = textureLod(u_ground_albedo_bump_array, vec3(ndc_center, high_indices.z), 10.0).rgb;
|
||||||
|
vec3 a3 = textureLod(u_ground_albedo_bump_array, vec3(ndc_center, high_indices.w), 10.0).rgb;
|
||||||
|
|
||||||
|
vec3 col0 = a0 * tint;
|
||||||
|
vec3 col1 = a1 * tint;
|
||||||
|
vec3 col2 = a2 * tint;
|
||||||
|
vec3 col3 = a3 * tint;
|
||||||
|
|
||||||
|
vec4 w;
|
||||||
|
// TODO An #ifdef macro would be nice! Or copy/paste everything in a different shader...
|
||||||
|
if (u_depth_blending) {
|
||||||
|
w = get_depth_blended_weights(high_weights, vec4(b0, b1, b2, b3));
|
||||||
|
} else {
|
||||||
|
w = high_weights;
|
||||||
|
}
|
||||||
|
|
||||||
|
float w_sum = (w.r + w.g + w.b + w.a);
|
||||||
|
|
||||||
|
ALBEDO = (
|
||||||
|
w.r * col0.rgb +
|
||||||
|
w.g * col1.rgb +
|
||||||
|
w.b * col2.rgb +
|
||||||
|
w.a * col3.rgb) / w_sum;
|
||||||
|
}
|
253
addons/zylann.hterrain/shaders/multisplat16_lite.shader
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
// WIP
|
||||||
|
// This shader uses a texture array with multiple splatmaps, allowing up to 16 textures.
|
||||||
|
// Only the 4 textures having highest blending weight are sampled.
|
||||||
|
|
||||||
|
uniform sampler2D u_terrain_heightmap;
|
||||||
|
uniform sampler2D u_terrain_normalmap;
|
||||||
|
// I had to remove `hint_albedo` from colormap because it makes sRGB conversion kick in,
|
||||||
|
// which snowballs to black when doing GPU painting on that texture...
|
||||||
|
uniform sampler2D u_terrain_colormap;
|
||||||
|
uniform sampler2D u_terrain_splatmap;
|
||||||
|
uniform sampler2D u_terrain_splatmap_1;
|
||||||
|
uniform sampler2D u_terrain_splatmap_2;
|
||||||
|
uniform sampler2D u_terrain_splatmap_3;
|
||||||
|
uniform sampler2D u_terrain_globalmap : hint_albedo;
|
||||||
|
uniform mat4 u_terrain_inverse_transform;
|
||||||
|
uniform mat3 u_terrain_normal_basis;
|
||||||
|
|
||||||
|
uniform sampler2DArray u_ground_albedo_bump_array : hint_albedo;
|
||||||
|
|
||||||
|
uniform float u_ground_uv_scale = 20.0;
|
||||||
|
uniform bool u_depth_blending = true;
|
||||||
|
uniform float u_globalmap_blend_start;
|
||||||
|
uniform float u_globalmap_blend_distance;
|
||||||
|
|
||||||
|
varying float v_hole;
|
||||||
|
varying vec3 v_tint;
|
||||||
|
varying vec2 v_terrain_uv;
|
||||||
|
varying vec3 v_ground_uv;
|
||||||
|
varying float v_distance_to_camera;
|
||||||
|
|
||||||
|
// TODO Can't put this in a constant: https://github.com/godotengine/godot/issues/44145
|
||||||
|
//const int TEXTURE_COUNT = 16;
|
||||||
|
|
||||||
|
|
||||||
|
vec3 unpack_normal(vec4 rgba) {
|
||||||
|
vec3 n = rgba.xzy * 2.0 - vec3(1.0);
|
||||||
|
// Had to negate Z because it comes from Y in the normal map,
|
||||||
|
// and OpenGL-style normal maps are Y-up.
|
||||||
|
n.z *= -1.0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blends weights according to the bump of detail textures,
|
||||||
|
// so for example it allows to have sand fill the gaps between pebbles
|
||||||
|
vec4 get_depth_blended_weights(vec4 splat, vec4 bumps) {
|
||||||
|
float dh = 0.2;
|
||||||
|
|
||||||
|
vec4 h = bumps + splat;
|
||||||
|
|
||||||
|
// TODO Keep improving multilayer blending, there are still some edge cases...
|
||||||
|
// Mitigation: nullify layers with near-zero splat
|
||||||
|
h *= smoothstep(0, 0.05, splat);
|
||||||
|
|
||||||
|
vec4 d = h + dh;
|
||||||
|
d.r -= max(h.g, max(h.b, h.a));
|
||||||
|
d.g -= max(h.r, max(h.b, h.a));
|
||||||
|
d.b -= max(h.g, max(h.r, h.a));
|
||||||
|
d.a -= max(h.g, max(h.b, h.r));
|
||||||
|
|
||||||
|
return clamp(d, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 get_triplanar_blend(vec3 world_normal) {
|
||||||
|
vec3 blending = abs(world_normal);
|
||||||
|
blending = normalize(max(blending, vec3(0.00001))); // Force weights to sum to 1.0
|
||||||
|
float b = blending.x + blending.y + blending.z;
|
||||||
|
return blending / vec3(b, b, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 texture_triplanar(sampler2D tex, vec3 world_pos, vec3 blend) {
|
||||||
|
vec4 xaxis = texture(tex, world_pos.yz);
|
||||||
|
vec4 yaxis = texture(tex, world_pos.xz);
|
||||||
|
vec4 zaxis = texture(tex, world_pos.xy);
|
||||||
|
// blend the results of the 3 planar projections.
|
||||||
|
return xaxis * blend.x + yaxis * blend.y + zaxis * blend.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_splat_weights(vec2 uv, out vec4 out_high_indices, out vec4 out_high_weights) {
|
||||||
|
vec4 ew0 = texture(u_terrain_splatmap, uv);
|
||||||
|
vec4 ew1 = texture(u_terrain_splatmap_1, uv);
|
||||||
|
vec4 ew2 = texture(u_terrain_splatmap_2, uv);
|
||||||
|
vec4 ew3 = texture(u_terrain_splatmap_3, uv);
|
||||||
|
|
||||||
|
float weights[16] = {
|
||||||
|
ew0.r, ew0.g, ew0.b, ew0.a,
|
||||||
|
ew1.r, ew1.g, ew1.b, ew1.a,
|
||||||
|
ew2.r, ew2.g, ew2.b, ew2.a,
|
||||||
|
ew3.r, ew3.g, ew3.b, ew3.a
|
||||||
|
};
|
||||||
|
|
||||||
|
// float weights_sum = 0.0;
|
||||||
|
// for (int i = 0; i < 16; ++i) {
|
||||||
|
// weights_sum += weights[i];
|
||||||
|
// }
|
||||||
|
// for (int i = 0; i < 16; ++i) {
|
||||||
|
// weights_sum /= weights_sum;
|
||||||
|
// }
|
||||||
|
// weights_sum=1.1;
|
||||||
|
|
||||||
|
// Now we have to pick the 4 highest weights and use them to blend textures.
|
||||||
|
|
||||||
|
// Using arrays because Godot's shader version doesn't support dynamic indexing of vectors
|
||||||
|
// TODO We should not need to initialize, but apparently we don't always find 4 weights
|
||||||
|
int high_indices_array[4] = {0, 0, 0, 0};
|
||||||
|
float high_weights_array[4] = {0.0, 0.0, 0.0, 0.0};
|
||||||
|
int count = 0;
|
||||||
|
// We know weights are supposed to be normalized.
|
||||||
|
// That means the highest value of the pivot above which we can find 4 results
|
||||||
|
// is 1.0 / 4.0. However that would mean exactly 4 textures have exactly that weight,
|
||||||
|
// which is very unlikely. If we consider 1.0 / 5.0, we are a bit more likely to find
|
||||||
|
// 4 results, and finding 5 results remains almost impossible.
|
||||||
|
float pivot = /*weights_sum*/1.0 / 5.0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
if (weights[i] > pivot) {
|
||||||
|
high_weights_array[count] = weights[i];
|
||||||
|
high_indices_array[count] = i;
|
||||||
|
weights[i] = 0.0;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (count < 4 && pivot > 0.0) {
|
||||||
|
float max_weight = 0.0;
|
||||||
|
int max_index = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
if (/*weights[i] <= pivot && */weights[i] > max_weight) {
|
||||||
|
max_weight = weights[i];
|
||||||
|
max_index = i;
|
||||||
|
weights[i] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
high_indices_array[count] = max_index;
|
||||||
|
high_weights_array[count] = max_weight;
|
||||||
|
++count;
|
||||||
|
pivot = max_weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_high_weights = vec4(
|
||||||
|
high_weights_array[0], high_weights_array[1],
|
||||||
|
high_weights_array[2], high_weights_array[3]);
|
||||||
|
|
||||||
|
out_high_indices = vec4(
|
||||||
|
float(high_indices_array[0]), float(high_indices_array[1]),
|
||||||
|
float(high_indices_array[2]), float(high_indices_array[3]));
|
||||||
|
|
||||||
|
out_high_weights /=
|
||||||
|
out_high_weights.r + out_high_weights.g + out_high_weights.b + out_high_weights.a;
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec4 wpos = WORLD_MATRIX * vec4(VERTEX, 1);
|
||||||
|
vec2 cell_coords = (u_terrain_inverse_transform * wpos).xz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results (#183)
|
||||||
|
cell_coords += vec2(0.5);
|
||||||
|
|
||||||
|
// Normalized UV
|
||||||
|
UV = cell_coords / vec2(textureSize(u_terrain_heightmap, 0));
|
||||||
|
|
||||||
|
// Height displacement
|
||||||
|
float h = texture(u_terrain_heightmap, UV).r;
|
||||||
|
VERTEX.y = h;
|
||||||
|
wpos.y = h;
|
||||||
|
|
||||||
|
vec3 base_ground_uv = vec3(cell_coords.x, h * WORLD_MATRIX[1][1], cell_coords.y);
|
||||||
|
v_ground_uv = base_ground_uv / u_ground_uv_scale;
|
||||||
|
|
||||||
|
// Putting this in vertex saves a fetch from the fragment shader,
|
||||||
|
// which is good for performance at a negligible quality cost,
|
||||||
|
// provided that geometry is a regular grid that decimates with LOD.
|
||||||
|
// (downside is LOD will also decimate it, but it's not bad overall)
|
||||||
|
vec4 tint = texture(u_terrain_colormap, UV);
|
||||||
|
v_hole = tint.a;
|
||||||
|
v_tint = tint.rgb;
|
||||||
|
|
||||||
|
// Need to use u_terrain_normal_basis to handle scaling.
|
||||||
|
// For some reason I also had to invert Z when sampling terrain normals... not sure why
|
||||||
|
NORMAL = u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, UV));
|
||||||
|
|
||||||
|
v_distance_to_camera = distance(wpos.xyz, CAMERA_MATRIX[3].xyz);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
if (v_hole < 0.5) {
|
||||||
|
// TODO Add option to use vertex discarding instead, using NaNs
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 terrain_normal_world =
|
||||||
|
u_terrain_normal_basis * (unpack_normal(texture(u_terrain_normalmap, UV)) * vec3(1,1,-1));
|
||||||
|
terrain_normal_world = normalize(terrain_normal_world);
|
||||||
|
|
||||||
|
float globalmap_factor = clamp((v_distance_to_camera - u_globalmap_blend_start)
|
||||||
|
* u_globalmap_blend_distance, 0.0, 1.0);
|
||||||
|
globalmap_factor *= globalmap_factor; // slower start, faster transition but far away
|
||||||
|
vec3 global_albedo = texture(u_terrain_globalmap, UV).rgb;
|
||||||
|
ALBEDO = global_albedo;
|
||||||
|
|
||||||
|
// Doing this branch allows to spare a bunch of texture fetches for distant pixels.
|
||||||
|
// Eventually, there could be a split between near and far shaders in the future,
|
||||||
|
// if relevant on high-end GPUs
|
||||||
|
if (globalmap_factor < 1.0) {
|
||||||
|
vec4 high_indices;
|
||||||
|
vec4 high_weights;
|
||||||
|
get_splat_weights(UV, high_indices, high_weights);
|
||||||
|
|
||||||
|
vec4 ab0 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv.xz, high_indices.x));
|
||||||
|
vec4 ab1 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv.xz, high_indices.y));
|
||||||
|
vec4 ab2 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv.xz, high_indices.z));
|
||||||
|
vec4 ab3 = texture(u_ground_albedo_bump_array, vec3(v_ground_uv.xz, high_indices.w));
|
||||||
|
|
||||||
|
vec3 col0 = ab0.rgb * v_tint;
|
||||||
|
vec3 col1 = ab1.rgb * v_tint;
|
||||||
|
vec3 col2 = ab2.rgb * v_tint;
|
||||||
|
vec3 col3 = ab3.rgb * v_tint;
|
||||||
|
|
||||||
|
vec4 w;
|
||||||
|
// TODO An #ifdef macro would be nice! Or copy/paste everything in a different shader...
|
||||||
|
if (u_depth_blending) {
|
||||||
|
w = get_depth_blended_weights(high_weights, vec4(ab0.a, ab1.a, ab2.a, ab3.a));
|
||||||
|
} else {
|
||||||
|
w = high_weights;
|
||||||
|
}
|
||||||
|
|
||||||
|
float w_sum = (w.r + w.g + w.b + w.a);
|
||||||
|
|
||||||
|
ALBEDO = (
|
||||||
|
w.r * col0.rgb +
|
||||||
|
w.g * col1.rgb +
|
||||||
|
w.b * col2.rgb +
|
||||||
|
w.a * col3.rgb) / w_sum;
|
||||||
|
|
||||||
|
ALBEDO = mix(ALBEDO, global_albedo, globalmap_factor);
|
||||||
|
ROUGHNESS = mix(ROUGHNESS, 1.0, globalmap_factor);
|
||||||
|
|
||||||
|
// if(count < 3) {
|
||||||
|
// ALBEDO = vec3(1.0, 0.0, 0.0);
|
||||||
|
// }
|
||||||
|
// Show splatmap weights
|
||||||
|
//ALBEDO = w.rgb;
|
||||||
|
}
|
||||||
|
// Highlight all pixels undergoing no splatmap at all
|
||||||
|
// else {
|
||||||
|
// ALBEDO = vec3(1.0, 0.0, 0.0);
|
||||||
|
// }
|
||||||
|
|
||||||
|
NORMAL = (INV_CAMERA_MATRIX * (vec4(terrain_normal_world, 0.0))).xyz;
|
||||||
|
}
|
327
addons/zylann.hterrain/shaders/simple4.shader
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
// This is the reference shader of the plugin, and has the most features.
|
||||||
|
// it should be preferred for high-end graphics cards.
|
||||||
|
// For less features but lower-end targets, see the lite version.
|
||||||
|
|
||||||
|
uniform sampler2D u_terrain_heightmap;
|
||||||
|
uniform sampler2D u_terrain_normalmap;
|
||||||
|
// I had to remove `hint_albedo` from colormap because it makes sRGB conversion kick in,
|
||||||
|
// which snowballs to black when doing GPU painting on that texture...
|
||||||
|
uniform sampler2D u_terrain_colormap;
|
||||||
|
uniform sampler2D u_terrain_splatmap;
|
||||||
|
uniform sampler2D u_terrain_globalmap : hint_albedo;
|
||||||
|
uniform mat4 u_terrain_inverse_transform;
|
||||||
|
uniform mat3 u_terrain_normal_basis;
|
||||||
|
|
||||||
|
// the reason bump is preferred with albedo is, roughness looks better with normal maps.
|
||||||
|
// If we want no normal mapping, roughness would only give flat mirror surfaces,
|
||||||
|
// while bump still allows to do depth-blending for free.
|
||||||
|
uniform sampler2D u_ground_albedo_bump_0 : hint_albedo;
|
||||||
|
uniform sampler2D u_ground_albedo_bump_1 : hint_albedo;
|
||||||
|
uniform sampler2D u_ground_albedo_bump_2 : hint_albedo;
|
||||||
|
uniform sampler2D u_ground_albedo_bump_3 : hint_albedo;
|
||||||
|
|
||||||
|
uniform sampler2D u_ground_normal_roughness_0;
|
||||||
|
uniform sampler2D u_ground_normal_roughness_1;
|
||||||
|
uniform sampler2D u_ground_normal_roughness_2;
|
||||||
|
uniform sampler2D u_ground_normal_roughness_3;
|
||||||
|
|
||||||
|
// Had to give this uniform a suffix, because it's declared as a simple float
|
||||||
|
// in other shaders, and its type cannot be inferred by the plugin.
|
||||||
|
// See https://github.com/godotengine/godot/issues/24488
|
||||||
|
uniform vec4 u_ground_uv_scale_per_texture = vec4(20.0, 20.0, 20.0, 20.0);
|
||||||
|
|
||||||
|
uniform bool u_depth_blending = true;
|
||||||
|
uniform bool u_triplanar = false;
|
||||||
|
// Each component corresponds to a ground texture. Set greater than zero to enable.
|
||||||
|
uniform vec4 u_tile_reduction = vec4(0.0, 0.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
uniform float u_globalmap_blend_start;
|
||||||
|
uniform float u_globalmap_blend_distance;
|
||||||
|
|
||||||
|
uniform vec4 u_colormap_opacity_per_texture = vec4(1.0, 1.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
varying float v_hole;
|
||||||
|
varying vec3 v_tint0;
|
||||||
|
varying vec3 v_tint1;
|
||||||
|
varying vec3 v_tint2;
|
||||||
|
varying vec3 v_tint3;
|
||||||
|
varying vec4 v_splat;
|
||||||
|
varying vec2 v_ground_uv0;
|
||||||
|
varying vec2 v_ground_uv1;
|
||||||
|
varying vec2 v_ground_uv2;
|
||||||
|
varying vec3 v_ground_uv3;
|
||||||
|
varying float v_distance_to_camera;
|
||||||
|
|
||||||
|
|
||||||
|
vec3 unpack_normal(vec4 rgba) {
|
||||||
|
vec3 n = rgba.xzy * 2.0 - vec3(1.0);
|
||||||
|
// Had to negate Z because it comes from Y in the normal map,
|
||||||
|
// and OpenGL-style normal maps are Y-up.
|
||||||
|
n.z *= -1.0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 pack_normal(vec3 n, float a) {
|
||||||
|
n.z *= -1.0;
|
||||||
|
return vec4((n.xzy + vec3(1.0)) * 0.5, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blends weights according to the bump of detail textures,
|
||||||
|
// so for example it allows to have sand fill the gaps between pebbles
|
||||||
|
vec4 get_depth_blended_weights(vec4 splat, vec4 bumps) {
|
||||||
|
float dh = 0.2;
|
||||||
|
|
||||||
|
vec4 h = bumps + splat;
|
||||||
|
|
||||||
|
// TODO Keep improving multilayer blending, there are still some edge cases...
|
||||||
|
// Mitigation: nullify layers with near-zero splat
|
||||||
|
h *= smoothstep(0, 0.05, splat);
|
||||||
|
|
||||||
|
vec4 d = h + dh;
|
||||||
|
d.r -= max(h.g, max(h.b, h.a));
|
||||||
|
d.g -= max(h.r, max(h.b, h.a));
|
||||||
|
d.b -= max(h.g, max(h.r, h.a));
|
||||||
|
d.a -= max(h.g, max(h.b, h.r));
|
||||||
|
|
||||||
|
return clamp(d, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 get_triplanar_blend(vec3 world_normal) {
|
||||||
|
vec3 blending = abs(world_normal);
|
||||||
|
blending = normalize(max(blending, vec3(0.00001))); // Force weights to sum to 1.0
|
||||||
|
float b = blending.x + blending.y + blending.z;
|
||||||
|
return blending / vec3(b, b, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 texture_triplanar(sampler2D tex, vec3 world_pos, vec3 blend) {
|
||||||
|
vec4 xaxis = texture(tex, world_pos.yz);
|
||||||
|
vec4 yaxis = texture(tex, world_pos.xz);
|
||||||
|
vec4 zaxis = texture(tex, world_pos.xy);
|
||||||
|
// blend the results of the 3 planar projections.
|
||||||
|
return xaxis * blend.x + yaxis * blend.y + zaxis * blend.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 depth_blend2(vec4 a_value, float a_bump, vec4 b_value, float b_bump, float t) {
|
||||||
|
// https://www.gamasutra.com
|
||||||
|
// /blogs/AndreyMishkinis/20130716/196339/Advanced_Terrain_Texture_Splatting.php
|
||||||
|
float d = 0.1;
|
||||||
|
float ma = max(a_bump + (1.0 - t), b_bump + t) - d;
|
||||||
|
float ba = max(a_bump + (1.0 - t) - ma, 0.0);
|
||||||
|
float bb = max(b_bump + t - ma, 0.0);
|
||||||
|
return (a_value * ba + b_value * bb) / (ba + bb);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 rotate(vec2 v, float cosa, float sina) {
|
||||||
|
return vec2(cosa * v.x - sina * v.y, sina * v.x + cosa * v.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 texture_antitile(sampler2D albedo_tex, sampler2D normal_tex, vec2 uv, out vec4 out_normal) {
|
||||||
|
float frequency = 2.0;
|
||||||
|
float scale = 1.3;
|
||||||
|
float sharpness = 0.7;
|
||||||
|
|
||||||
|
// Rotate and scale UV
|
||||||
|
float rot = 3.14 * 0.6;
|
||||||
|
float cosa = cos(rot);
|
||||||
|
float sina = sin(rot);
|
||||||
|
vec2 uv2 = rotate(uv, cosa, sina) * scale;
|
||||||
|
|
||||||
|
vec4 col0 = texture(albedo_tex, uv);
|
||||||
|
vec4 col1 = texture(albedo_tex, uv2);
|
||||||
|
vec4 nrm0 = texture(normal_tex, uv);
|
||||||
|
vec4 nrm1 = texture(normal_tex, uv2);
|
||||||
|
//col0 = vec4(0.0, 0.5, 0.5, 1.0); // Highlights variations
|
||||||
|
|
||||||
|
// Normals have to be rotated too since we are rotating the texture...
|
||||||
|
// TODO Probably not the most efficient but understandable for now
|
||||||
|
vec3 n = unpack_normal(nrm1);
|
||||||
|
// Had to negate the Y axis for some reason. I never remember the myriad of conventions around
|
||||||
|
n.xz = rotate(n.xz, cosa, -sina);
|
||||||
|
nrm1 = pack_normal(n, nrm1.a);
|
||||||
|
|
||||||
|
// Periodically alternate between the two versions using a warped checker pattern
|
||||||
|
float t = 1.2 +
|
||||||
|
sin(uv2.x * frequency + sin(uv.x) * 2.0)
|
||||||
|
* cos(uv2.y * frequency + sin(uv.y) * 2.0); // Result in [0..2]
|
||||||
|
t = smoothstep(sharpness, 2.0 - sharpness, t);
|
||||||
|
|
||||||
|
// Using depth blend because classic alpha blending smoothes out details.
|
||||||
|
out_normal = depth_blend2(nrm0, col0.a, nrm1, col1.a, t);
|
||||||
|
return depth_blend2(col0, col0.a, col1, col1.a, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec4 wpos = WORLD_MATRIX * vec4(VERTEX, 1);
|
||||||
|
vec2 cell_coords = (u_terrain_inverse_transform * wpos).xz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results (#183)
|
||||||
|
cell_coords += vec2(0.5);
|
||||||
|
|
||||||
|
// Normalized UV
|
||||||
|
UV = cell_coords / vec2(textureSize(u_terrain_heightmap, 0));
|
||||||
|
|
||||||
|
// Height displacement
|
||||||
|
float h = texture(u_terrain_heightmap, UV).r;
|
||||||
|
VERTEX.y = h;
|
||||||
|
wpos.y = h;
|
||||||
|
|
||||||
|
vec3 base_ground_uv = vec3(cell_coords.x, h * WORLD_MATRIX[1][1], cell_coords.y);
|
||||||
|
v_ground_uv0 = base_ground_uv.xz / u_ground_uv_scale_per_texture.x;
|
||||||
|
v_ground_uv1 = base_ground_uv.xz / u_ground_uv_scale_per_texture.y;
|
||||||
|
v_ground_uv2 = base_ground_uv.xz / u_ground_uv_scale_per_texture.z;
|
||||||
|
v_ground_uv3 = base_ground_uv / u_ground_uv_scale_per_texture.w;
|
||||||
|
|
||||||
|
// Putting this in vertex saves 2 fetches from the fragment shader,
|
||||||
|
// which is good for performance at a negligible quality cost,
|
||||||
|
// provided that geometry is a regular grid that decimates with LOD.
|
||||||
|
// (downside is LOD will also decimate tint and splat, but it's not bad overall)
|
||||||
|
vec4 tint = texture(u_terrain_colormap, UV);
|
||||||
|
v_hole = tint.a;
|
||||||
|
v_tint0 = mix(vec3(1.0), tint.rgb, u_colormap_opacity_per_texture.x);
|
||||||
|
v_tint1 = mix(vec3(1.0), tint.rgb, u_colormap_opacity_per_texture.y);
|
||||||
|
v_tint2 = mix(vec3(1.0), tint.rgb, u_colormap_opacity_per_texture.z);
|
||||||
|
v_tint3 = mix(vec3(1.0), tint.rgb, u_colormap_opacity_per_texture.w);
|
||||||
|
v_splat = texture(u_terrain_splatmap, UV);
|
||||||
|
|
||||||
|
// Need to use u_terrain_normal_basis to handle scaling.
|
||||||
|
NORMAL = u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, UV));
|
||||||
|
|
||||||
|
v_distance_to_camera = distance(wpos.xyz, CAMERA_MATRIX[3].xyz);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
if (v_hole < 0.5) {
|
||||||
|
// TODO Add option to use vertex discarding instead, using NaNs
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 terrain_normal_world =
|
||||||
|
u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, UV));
|
||||||
|
terrain_normal_world = normalize(terrain_normal_world);
|
||||||
|
vec3 normal = terrain_normal_world;
|
||||||
|
|
||||||
|
float globalmap_factor = clamp((v_distance_to_camera - u_globalmap_blend_start)
|
||||||
|
* u_globalmap_blend_distance, 0.0, 1.0);
|
||||||
|
globalmap_factor *= globalmap_factor; // slower start, faster transition but far away
|
||||||
|
vec3 global_albedo = texture(u_terrain_globalmap, UV).rgb;
|
||||||
|
ALBEDO = global_albedo;
|
||||||
|
|
||||||
|
// Doing this branch allows to spare a bunch of texture fetches for distant pixels.
|
||||||
|
// Eventually, there could be a split between near and far shaders in the future,
|
||||||
|
// if relevant on high-end GPUs
|
||||||
|
if (globalmap_factor < 1.0) {
|
||||||
|
vec4 ab0, ab1, ab2, ab3;
|
||||||
|
vec4 nr0, nr1, nr2, nr3;
|
||||||
|
|
||||||
|
if (u_triplanar) {
|
||||||
|
// Only do triplanar on one texture slot,
|
||||||
|
// because otherwise it would be very expensive and cost many more ifs.
|
||||||
|
// I chose the last slot because first slot is the default on new splatmaps,
|
||||||
|
// and that's a feature used for cliffs, which are usually designed later.
|
||||||
|
|
||||||
|
vec3 blending = get_triplanar_blend(terrain_normal_world);
|
||||||
|
|
||||||
|
ab3 = texture_triplanar(u_ground_albedo_bump_3, v_ground_uv3, blending);
|
||||||
|
nr3 = texture_triplanar(u_ground_normal_roughness_3, v_ground_uv3, blending);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (u_tile_reduction[3] > 0.0) {
|
||||||
|
ab3 = texture_antitile(
|
||||||
|
u_ground_albedo_bump_3, u_ground_normal_roughness_3, v_ground_uv3.xz, nr3);
|
||||||
|
} else {
|
||||||
|
ab3 = texture(u_ground_albedo_bump_3, v_ground_uv3.xz);
|
||||||
|
nr3 = texture(u_ground_normal_roughness_3, v_ground_uv3.xz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u_tile_reduction[0] > 0.0) {
|
||||||
|
ab0 = texture_antitile(
|
||||||
|
u_ground_albedo_bump_0, u_ground_normal_roughness_0, v_ground_uv0, nr0);
|
||||||
|
} else {
|
||||||
|
ab0 = texture(u_ground_albedo_bump_0, v_ground_uv0);
|
||||||
|
nr0 = texture(u_ground_normal_roughness_0, v_ground_uv0);
|
||||||
|
}
|
||||||
|
if (u_tile_reduction[1] > 0.0) {
|
||||||
|
ab1 = texture_antitile(
|
||||||
|
u_ground_albedo_bump_1, u_ground_normal_roughness_1, v_ground_uv1, nr1);
|
||||||
|
} else {
|
||||||
|
ab1 = texture(u_ground_albedo_bump_1, v_ground_uv1);
|
||||||
|
nr1 = texture(u_ground_normal_roughness_1, v_ground_uv1);
|
||||||
|
}
|
||||||
|
if (u_tile_reduction[2] > 0.0) {
|
||||||
|
ab2 = texture_antitile(
|
||||||
|
u_ground_albedo_bump_2, u_ground_normal_roughness_2, v_ground_uv2, nr2);
|
||||||
|
} else {
|
||||||
|
ab2 = texture(u_ground_albedo_bump_2, v_ground_uv2);
|
||||||
|
nr2 = texture(u_ground_normal_roughness_2, v_ground_uv2);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 col0 = ab0.rgb * v_tint0;
|
||||||
|
vec3 col1 = ab1.rgb * v_tint1;
|
||||||
|
vec3 col2 = ab2.rgb * v_tint2;
|
||||||
|
vec3 col3 = ab3.rgb * v_tint3;
|
||||||
|
|
||||||
|
vec4 rough = vec4(nr0.a, nr1.a, nr2.a, nr3.a);
|
||||||
|
|
||||||
|
vec3 normal0 = unpack_normal(nr0);
|
||||||
|
vec3 normal1 = unpack_normal(nr1);
|
||||||
|
vec3 normal2 = unpack_normal(nr2);
|
||||||
|
vec3 normal3 = unpack_normal(nr3);
|
||||||
|
|
||||||
|
vec4 w;
|
||||||
|
// TODO An #ifdef macro would be nice! Or copy/paste everything in a different shader...
|
||||||
|
if (u_depth_blending) {
|
||||||
|
w = get_depth_blended_weights(v_splat, vec4(ab0.a, ab1.a, ab2.a, ab3.a));
|
||||||
|
} else {
|
||||||
|
w = v_splat.rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
float w_sum = (w.r + w.g + w.b + w.a);
|
||||||
|
|
||||||
|
ALBEDO = (
|
||||||
|
w.r * col0.rgb +
|
||||||
|
w.g * col1.rgb +
|
||||||
|
w.b * col2.rgb +
|
||||||
|
w.a * col3.rgb) / w_sum;
|
||||||
|
|
||||||
|
ROUGHNESS = (
|
||||||
|
w.r * rough.r +
|
||||||
|
w.g * rough.g +
|
||||||
|
w.b * rough.b +
|
||||||
|
w.a * rough.a) / w_sum;
|
||||||
|
|
||||||
|
vec3 ground_normal = /*u_terrain_normal_basis **/ (
|
||||||
|
w.r * normal0 +
|
||||||
|
w.g * normal1 +
|
||||||
|
w.b * normal2 +
|
||||||
|
w.a * normal3) / w_sum;
|
||||||
|
// If no splat textures are defined, normal vectors will default to (1,1,1),
|
||||||
|
// which is incorrect, and causes the terrain to be shaded wrongly in some directions.
|
||||||
|
// However, this should not be a problem to fix in the shader,
|
||||||
|
// because there MUST be at least one splat texture set.
|
||||||
|
//ground_normal = normalize(ground_normal);
|
||||||
|
// TODO Make the plugin insert a default normalmap if it's empty
|
||||||
|
|
||||||
|
// Combine terrain normals with detail normals (not sure if correct but looks ok)
|
||||||
|
normal = normalize(vec3(
|
||||||
|
terrain_normal_world.x + ground_normal.x,
|
||||||
|
terrain_normal_world.y,
|
||||||
|
terrain_normal_world.z + ground_normal.z));
|
||||||
|
|
||||||
|
normal = mix(normal, terrain_normal_world, globalmap_factor);
|
||||||
|
|
||||||
|
ALBEDO = mix(ALBEDO, global_albedo, globalmap_factor);
|
||||||
|
ROUGHNESS = mix(ROUGHNESS, 1.0, globalmap_factor);
|
||||||
|
|
||||||
|
// Show splatmap weights
|
||||||
|
//ALBEDO = w.rgb;
|
||||||
|
}
|
||||||
|
// Highlight all pixels undergoing no splatmap at all
|
||||||
|
// else {
|
||||||
|
// ALBEDO = vec3(1.0, 0.0, 0.0);
|
||||||
|
// }
|
||||||
|
|
||||||
|
NORMAL = (INV_CAMERA_MATRIX * (vec4(normal, 0.0))).xyz;
|
||||||
|
}
|
83
addons/zylann.hterrain/shaders/simple4_global.shader
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
// This shader is used to bake the global albedo map.
|
||||||
|
// It exposes a subset of the main shader API, so uniform names were not modified.
|
||||||
|
|
||||||
|
// I had to remove `hint_albedo` from colormap because it makes sRGB conversion kick in,
|
||||||
|
// which snowballs to black when doing GPU painting on that texture...
|
||||||
|
uniform sampler2D u_terrain_colormap;// : hint_albedo;
|
||||||
|
uniform sampler2D u_terrain_splatmap;
|
||||||
|
|
||||||
|
uniform sampler2D u_ground_albedo_bump_0 : hint_albedo;
|
||||||
|
uniform sampler2D u_ground_albedo_bump_1 : hint_albedo;
|
||||||
|
uniform sampler2D u_ground_albedo_bump_2 : hint_albedo;
|
||||||
|
uniform sampler2D u_ground_albedo_bump_3 : hint_albedo;
|
||||||
|
|
||||||
|
// Keep depth blending because it has a high effect on the final result
|
||||||
|
uniform bool u_depth_blending = true;
|
||||||
|
uniform float u_ground_uv_scale = 20.0;
|
||||||
|
|
||||||
|
|
||||||
|
vec4 get_depth_blended_weights(vec4 splat, vec4 bumps) {
|
||||||
|
float dh = 0.2;
|
||||||
|
|
||||||
|
vec4 h = bumps + splat;
|
||||||
|
|
||||||
|
h *= smoothstep(0, 0.05, splat);
|
||||||
|
|
||||||
|
vec4 d = h + dh;
|
||||||
|
d.r -= max(h.g, max(h.b, h.a));
|
||||||
|
d.g -= max(h.r, max(h.b, h.a));
|
||||||
|
d.b -= max(h.g, max(h.r, h.a));
|
||||||
|
d.a -= max(h.g, max(h.b, h.r));
|
||||||
|
|
||||||
|
return clamp(d, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec4 wpos = WORLD_MATRIX * vec4(VERTEX, 1);
|
||||||
|
vec2 cell_coords = wpos.xz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results (#183)
|
||||||
|
cell_coords += vec2(0.5);
|
||||||
|
|
||||||
|
// Normalized UV
|
||||||
|
UV = (cell_coords / vec2(textureSize(u_terrain_splatmap, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
// These were moved from vertex to fragment,
|
||||||
|
// so we can generate part of the global map with just one quad and we get full quality
|
||||||
|
vec4 tint = texture(u_terrain_colormap, UV);
|
||||||
|
vec4 splat = texture(u_terrain_splatmap, UV);
|
||||||
|
|
||||||
|
// Get bump at normal resolution so depth blending is accurate
|
||||||
|
vec2 ground_uv = UV / u_ground_uv_scale;
|
||||||
|
float b0 = texture(u_ground_albedo_bump_0, ground_uv).a;
|
||||||
|
float b1 = texture(u_ground_albedo_bump_1, ground_uv).a;
|
||||||
|
float b2 = texture(u_ground_albedo_bump_2, ground_uv).a;
|
||||||
|
float b3 = texture(u_ground_albedo_bump_3, ground_uv).a;
|
||||||
|
|
||||||
|
// Take the center of the highest mip as color, because we can't see details from far away.
|
||||||
|
vec2 ndc_center = vec2(0.5, 0.5);
|
||||||
|
vec3 col0 = textureLod(u_ground_albedo_bump_0, ndc_center, 10.0).rgb;
|
||||||
|
vec3 col1 = textureLod(u_ground_albedo_bump_1, ndc_center, 10.0).rgb;
|
||||||
|
vec3 col2 = textureLod(u_ground_albedo_bump_2, ndc_center, 10.0).rgb;
|
||||||
|
vec3 col3 = textureLod(u_ground_albedo_bump_3, ndc_center, 10.0).rgb;
|
||||||
|
|
||||||
|
vec4 w;
|
||||||
|
if (u_depth_blending) {
|
||||||
|
w = get_depth_blended_weights(splat, vec4(b0, b1, b2, b3));
|
||||||
|
} else {
|
||||||
|
w = splat.rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
float w_sum = (w.r + w.g + w.b + w.a);
|
||||||
|
|
||||||
|
ALBEDO = tint.rgb * (
|
||||||
|
w.r * col0 +
|
||||||
|
w.g * col1 +
|
||||||
|
w.b * col2 +
|
||||||
|
w.a * col3) / w_sum;
|
||||||
|
}
|
||||||
|
|
209
addons/zylann.hterrain/shaders/simple4_lite.shader
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
// This is a shader with less textures, in case the main one doesn't run on your GPU.
|
||||||
|
// It's mostly a big copy/paste, because Godot doesn't support #include or #ifdef...
|
||||||
|
|
||||||
|
uniform sampler2D u_terrain_heightmap;
|
||||||
|
uniform sampler2D u_terrain_normalmap;
|
||||||
|
// I had to remove `hint_albedo` from colormap because it makes sRGB conversion kick in,
|
||||||
|
// which snowballs to black when doing GPU painting on that texture...
|
||||||
|
uniform sampler2D u_terrain_colormap;// : hint_albedo;
|
||||||
|
uniform sampler2D u_terrain_splatmap;
|
||||||
|
uniform mat4 u_terrain_inverse_transform;
|
||||||
|
uniform mat3 u_terrain_normal_basis;
|
||||||
|
|
||||||
|
uniform sampler2D u_ground_albedo_bump_0 : hint_albedo;
|
||||||
|
uniform sampler2D u_ground_albedo_bump_1 : hint_albedo;
|
||||||
|
uniform sampler2D u_ground_albedo_bump_2 : hint_albedo;
|
||||||
|
uniform sampler2D u_ground_albedo_bump_3 : hint_albedo;
|
||||||
|
|
||||||
|
uniform float u_ground_uv_scale = 20.0;
|
||||||
|
uniform bool u_depth_blending = true;
|
||||||
|
uniform bool u_triplanar = false;
|
||||||
|
// Each component corresponds to a ground texture. Set greater than zero to enable.
|
||||||
|
uniform vec4 u_tile_reduction = vec4(0.0, 0.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
varying vec4 v_tint;
|
||||||
|
varying vec4 v_splat;
|
||||||
|
varying vec3 v_ground_uv;
|
||||||
|
|
||||||
|
|
||||||
|
vec3 unpack_normal(vec4 rgba) {
|
||||||
|
vec3 n = rgba.xzy * 2.0 - vec3(1.0);
|
||||||
|
// Had to negate Z because it comes from Y in the normal map,
|
||||||
|
// and OpenGL-style normal maps are Y-up.
|
||||||
|
n.z *= -1.0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blends weights according to the bump of detail textures,
|
||||||
|
// so for example it allows to have sand fill the gaps between pebbles
|
||||||
|
vec4 get_depth_blended_weights(vec4 splat, vec4 bumps) {
|
||||||
|
float dh = 0.2;
|
||||||
|
|
||||||
|
vec4 h = bumps + splat;
|
||||||
|
|
||||||
|
// TODO Keep improving multilayer blending, there are still some edge cases...
|
||||||
|
// Mitigation: nullify layers with near-zero splat
|
||||||
|
h *= smoothstep(0, 0.05, splat);
|
||||||
|
|
||||||
|
vec4 d = h + dh;
|
||||||
|
d.r -= max(h.g, max(h.b, h.a));
|
||||||
|
d.g -= max(h.r, max(h.b, h.a));
|
||||||
|
d.b -= max(h.g, max(h.r, h.a));
|
||||||
|
d.a -= max(h.g, max(h.b, h.r));
|
||||||
|
|
||||||
|
return clamp(d, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 get_triplanar_blend(vec3 world_normal) {
|
||||||
|
vec3 blending = abs(world_normal);
|
||||||
|
blending = normalize(max(blending, vec3(0.00001))); // Force weights to sum to 1.0
|
||||||
|
float b = blending.x + blending.y + blending.z;
|
||||||
|
return blending / vec3(b, b, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 texture_triplanar(sampler2D tex, vec3 world_pos, vec3 blend) {
|
||||||
|
vec4 xaxis = texture(tex, world_pos.yz);
|
||||||
|
vec4 yaxis = texture(tex, world_pos.xz);
|
||||||
|
vec4 zaxis = texture(tex, world_pos.xy);
|
||||||
|
// blend the results of the 3 planar projections.
|
||||||
|
return xaxis * blend.x + yaxis * blend.y + zaxis * blend.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 depth_blend2(vec4 a, vec4 b, float t) {
|
||||||
|
// https://www.gamasutra.com
|
||||||
|
// /blogs/AndreyMishkinis/20130716/196339/Advanced_Terrain_Texture_Splatting.php
|
||||||
|
float d = 0.1;
|
||||||
|
float ma = max(a.a + (1.0 - t), b.a + t) - d;
|
||||||
|
float ba = max(a.a + (1.0 - t) - ma, 0.0);
|
||||||
|
float bb = max(b.a + t - ma, 0.0);
|
||||||
|
return (a * ba + b * bb) / (ba + bb);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 texture_antitile(sampler2D tex, vec2 uv) {
|
||||||
|
float frequency = 2.0;
|
||||||
|
float scale = 1.3;
|
||||||
|
float sharpness = 0.7;
|
||||||
|
|
||||||
|
// Rotate and scale UV
|
||||||
|
float rot = 3.14 * 0.6;
|
||||||
|
float cosa = cos(rot);
|
||||||
|
float sina = sin(rot);
|
||||||
|
vec2 uv2 = vec2(cosa * uv.x - sina * uv.y, sina * uv.x + cosa * uv.y) * scale;
|
||||||
|
|
||||||
|
vec4 col0 = texture(tex, uv);
|
||||||
|
vec4 col1 = texture(tex, uv2);
|
||||||
|
//col0 = vec4(0.0, 0.0, 1.0, 1.0);
|
||||||
|
// Periodically alternate between the two versions using a warped checker pattern
|
||||||
|
float t = 0.5 + 0.5
|
||||||
|
* sin(uv2.x * frequency + sin(uv.x) * 2.0)
|
||||||
|
* cos(uv2.y * frequency + sin(uv.y) * 2.0);
|
||||||
|
// Using depth blend because classic alpha blending smoothes out details
|
||||||
|
return depth_blend2(col0, col1, smoothstep(0.5 * sharpness, 1.0 - 0.5 * sharpness, t));
|
||||||
|
}
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec2 cell_coords = (u_terrain_inverse_transform * WORLD_MATRIX * vec4(VERTEX, 1)).xz;
|
||||||
|
// Must add a half-offset so that we sample the center of pixels,
|
||||||
|
// otherwise bilinear filtering of the textures will give us mixed results.
|
||||||
|
cell_coords += vec2(0.5);
|
||||||
|
|
||||||
|
// Normalized UV
|
||||||
|
UV = cell_coords / vec2(textureSize(u_terrain_heightmap, 0));
|
||||||
|
|
||||||
|
// Height displacement
|
||||||
|
float h = texture(u_terrain_heightmap, UV).r;
|
||||||
|
VERTEX.y = h;
|
||||||
|
|
||||||
|
v_ground_uv = vec3(cell_coords.x, h * WORLD_MATRIX[1][1], cell_coords.y) / u_ground_uv_scale;
|
||||||
|
|
||||||
|
// Putting this in vertex saves 2 fetches from the fragment shader,
|
||||||
|
// which is good for performance at a negligible quality cost,
|
||||||
|
// provided that geometry is a regular grid that decimates with LOD.
|
||||||
|
// (downside is LOD will also decimate tint and splat, but it's not bad overall)
|
||||||
|
v_tint = texture(u_terrain_colormap, UV);
|
||||||
|
v_splat = texture(u_terrain_splatmap, UV);
|
||||||
|
|
||||||
|
// Need to use u_terrain_normal_basis to handle scaling.
|
||||||
|
NORMAL = u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, UV));
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
if (v_tint.a < 0.5) {
|
||||||
|
// TODO Add option to use vertex discarding instead, using NaNs
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 terrain_normal_world =
|
||||||
|
u_terrain_normal_basis * unpack_normal(texture(u_terrain_normalmap, UV));
|
||||||
|
terrain_normal_world = normalize(terrain_normal_world);
|
||||||
|
|
||||||
|
// TODO Detail should only be rasterized on nearby chunks (needs proximity management to switch shaders)
|
||||||
|
|
||||||
|
vec2 ground_uv = v_ground_uv.xz;
|
||||||
|
|
||||||
|
vec4 ab0, ab1, ab2, ab3;
|
||||||
|
if (u_triplanar) {
|
||||||
|
// Only do triplanar on one texture slot,
|
||||||
|
// because otherwise it would be very expensive and cost many more ifs.
|
||||||
|
// I chose the last slot because first slot is the default on new splatmaps,
|
||||||
|
// and that's a feature used for cliffs, which are usually designed later.
|
||||||
|
|
||||||
|
vec3 blending = get_triplanar_blend(terrain_normal_world);
|
||||||
|
|
||||||
|
ab3 = texture_triplanar(u_ground_albedo_bump_3, v_ground_uv, blending);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (u_tile_reduction[3] > 0.0) {
|
||||||
|
ab3 = texture(u_ground_albedo_bump_3, ground_uv);
|
||||||
|
} else {
|
||||||
|
ab3 = texture_antitile(u_ground_albedo_bump_3, ground_uv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u_tile_reduction[0] > 0.0) {
|
||||||
|
ab0 = texture_antitile(u_ground_albedo_bump_0, ground_uv);
|
||||||
|
} else {
|
||||||
|
ab0 = texture(u_ground_albedo_bump_0, ground_uv);
|
||||||
|
}
|
||||||
|
if (u_tile_reduction[1] > 0.0) {
|
||||||
|
ab1 = texture_antitile(u_ground_albedo_bump_1, ground_uv);
|
||||||
|
} else {
|
||||||
|
ab1 = texture(u_ground_albedo_bump_1, ground_uv);
|
||||||
|
}
|
||||||
|
if (u_tile_reduction[2] > 0.0) {
|
||||||
|
ab2 = texture_antitile(u_ground_albedo_bump_2, ground_uv);
|
||||||
|
} else {
|
||||||
|
ab2 = texture(u_ground_albedo_bump_2, ground_uv);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 col0 = ab0.rgb;
|
||||||
|
vec3 col1 = ab1.rgb;
|
||||||
|
vec3 col2 = ab2.rgb;
|
||||||
|
vec3 col3 = ab3.rgb;
|
||||||
|
|
||||||
|
vec4 w;
|
||||||
|
// TODO An #ifdef macro would be nice! Or copy/paste everything in a different shader...
|
||||||
|
if (u_depth_blending) {
|
||||||
|
w = get_depth_blended_weights(v_splat, vec4(ab0.a, ab1.a, ab2.a, ab3.a));
|
||||||
|
} else {
|
||||||
|
w = v_splat.rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
float w_sum = (w.r + w.g + w.b + w.a);
|
||||||
|
|
||||||
|
ALBEDO = v_tint.rgb * (
|
||||||
|
w.r * col0.rgb +
|
||||||
|
w.g * col1.rgb +
|
||||||
|
w.b * col2.rgb +
|
||||||
|
w.a * col3.rgb) / w_sum;
|
||||||
|
|
||||||
|
ROUGHNESS = 1.0;
|
||||||
|
|
||||||
|
NORMAL = (INV_CAMERA_MATRIX * (vec4(terrain_normal_world, 0.0))).xyz;
|
||||||
|
|
||||||
|
//ALBEDO = w.rgb;
|
||||||
|
//ALBEDO = v_ground_uv.xyz;
|
||||||
|
}
|
||||||
|
|